본문 바로가기
C.W.K.
Lesson 03 of 06 · published

Ollama 경로

~16 min · journey, ollama, speculative-decoding, mtp, gguf, measured

Level 0스펙 시트 훑는 사람
0 XP0/91 lessons0/19 achievements
0/100 XP to next level100 XP to go0% complete
"런타임의 초당 토큰은 네가 느끼는 거야. 물리 트랙의 초당 토큰은 가중치를 한 번 지나서 나오는 거야. 둘은 런타임이 영리해지기 전까지만 같은 숫자야."

데몬 하나, 엔진 둘

Ollama는 대부분의 사람이 쓰는 문이야. 데몬 하나, 레지스트리 하나, ollama run. 맥에서 0.34 데몬은 그 동사 뒤에 엔진 둘을 갖고 있어. 하나는 GGUF를 읽는 llama.cpp 계열 llama-server 서브프로세스(앞 트랙의 커뮤니티 런타임, Metal 포함)고, 다른 하나는 Ollama 자체의 MLX 엔진이야. MLX의 C API 위에 올린 Go 러너고, 로그에 "MLX engine initialized … device=gpu"라고 알리며, 모델을 텐서 하나당 블롭 하나로 저장해. 태그가 골라. qwen3.8:27b-mlx는 실험이 잰 것과 같은 27B 아키텍처(64 레이어, 풀 어텐션 16과 리니어 48)를 NVFP4로 양자화한 거야. 16개마다 8비트 스케일 하나가 붙는 4비트 값, 가중치당 4.5비트, 실험의 4비트 그룹 64 체크포인트와 같은 비트. 블롭 크기를 물리 트랙의 규칙으로 읽으면 토큰당 14.45 GB가 나와. 실험의 14.42에 대해서. qwen3.8:latest는 16.8 GB짜리 Q4_K_M GGUF야. 둘 다 하나의 풀에 살고, ollama ps는 어느 쪽에나 100% GPU라고 말해. 맥에서 그건 모든 레이어가 작업 집합 안에서 GPU에 있다는 뜻이야. 별도 GPU 기계에서 같은 칸은 VRAM 벽인 CPU/GPU 분할을 보여 줘.

물리 트랙이 불가능하다고 말하는 숫자

office에서 첫 --verbose 실행은 MLX 엔진 27B에 초당 43.2토큰을 보고했어. 산문 512토큰의 API 실행은 스트림에서 벽시계로 재서 47.6–50.6. 물리 트랙의 상한은 토큰당 14.45 GB에 office의 실측 638 GB/s로 44야. mlx-lm은 32.6에 닿았고. 런타임은 가중치를 읽어서는 상한을 못 넘어. 그러니 토큰마다 한 번씩 읽고 있는 게 아니야. 러너 자체의 로그가 어떻게인지 말해. speculative decode stats iterations=225 drafted=458 accepted=287 acceptance=0.63. 체크포인트는 다중 토큰 예측 헤드(mtp.*, 0.24 GB. DeepSeek-V3가 학습시킨 그 아이디어)를 싣고 오고, Ollama의 MLX 러너는 그걸로 토큰 여러 개를 싸게 초안 잡은 다음 전체 가중치를 한 번 지나며 검증해. 512토큰이 217–225 패스 걸렸어. 패스당 2.3토큰, 패스당 45–50 ms. 패스당으로는 mlx-lm의 30.7 ms보다 느려. 실효 대역폭 290–320 GB/s 대 470. 검증 패스는 작은 배치고, 배치 레슨이 말했듯 토큰 하나는 바이트 값을 치르는데 배치는 연산 값을 치르니까. 이득은 전부 패스당 토큰이야. 문장 하나를 마흔 번 반복하라고 하면 수락률이 1.00으로 가고, 패스당 4.9토큰, 초당 90토큰. 무작위 16진수를 달라고 해도 0.85를 수락하고, 77–82.

다른 엔진에서 같은 요령

GGUF 엔진도 같은 헤드로 초안을 잡아. 로그가 "creating MTP draft context against the target model"이라고 말하고, 수락률 0.867, 평균 초안 길이 4.47. 그리고 초당 27.5토큰을 내놔. 바로 그 블롭에 llama-bench, 토큰 하나씩, 초안 없이: 26.4 ± 0.4. MLX 엔진에 절반을 더 사 준 같은 요령이 이쪽엔 4퍼센트를 사 줬어. 왜 이쪽의 검증 패스가 그렇게 훨씬 비싼지는 이 퀘스트가 잰 게 아니야. 엔진 둘, 풀 하나, 헤드 하나, 반대의 결과. 그리고 어느 쪽 eval rate도 디코드 상한 레슨이 정의하는 숫자가 아니야.

런타임, office, 가중치당 4–5비트 27B토큰당 바이트패스 하나가 내놓는 것tok/s증거
mlx-lm 0.31.3, Qwen3.5-27B 4-bit14.42 GB토큰 132.6실측
Ollama 0.34 MLX 엔진, qwen3.8:27b-mlx NVFP414.45 GB (+0.24 MTP)2.3토큰(산문)에서 4.9(반복)47.6–50.6(산문), 90(반복)실측, 러너 로그
Ollama 0.34 llama.cpp 러너, qwen3.8:latest Q4_K_M16.8 GB 파일MTP 초안, 평균 4.4727.5실측, 러너 로그
llama-bench 4c9233c, 같은 GGUF 블롭16.8 GB 파일토큰 126.4 ± 0.4실측

이 집의 Ollama

Ollama는 이 집의 편의 계층이지 물리 계층이 아니야. 코딩 에이전트는 Ollama 위의 로컬 레그를 갖고, 로컬 모델은 금고 전체를 담을 수 없어서 컴파일된 미니 프로토콜을 건네받아. 영상 기억 엔진의 요약은 server나 worker의 Ollama에서 돌고, 기본은 클라우드 핀이야. 피파의 형제 문은 Ollama 모델을 local, server, cloud 세 계층으로 열거해. 그리고 파트 4가 다루는 기억 검색은 Ollama의 변경이 임베딩 모델 밑의 땅을 움직였을 때 Ollama를 떠나 oMLX로 갔어. 대부분의 사람이 쓰는 문은 움직이는 문이기도 해. 다음 레슨은 부서지는 경로야.

Code

엔진 둘, 풀 하나 — 그리고 불가능한 숫자를 설명하는 로그 한 줄·bash
# office, 2026-09-15, Ollama 0.34.0 (Homebrew, launchd). The -mlx tag selects Ollama's MLX engine.
ollama show qwen3.8:27b-mlx | head -8      # architecture qwen3_5, 27.8B, quantization nvfp4
ollama show qwen3.8:latest  | head -8      # architecture qwen35,  27.3B, quantization Q4_K_M

ollama run qwen3.8:27b-mlx --verbose "Explain unified memory in 150 words." >/dev/null
# load duration:        3.64s
# prompt eval count:    21 token(s)
# prompt eval rate:     13.59 tokens/s      <- the first forward touching every page (previous lesson)
# eval count:           305 token(s)
# eval rate:            43.23 tokens/s      <- above the physics track's 44 ceiling? read on
ollama ps
# NAME               ID            SIZE   PROCESSOR  CONTEXT  UNTIL
# qwen3.8:27b-mlx    5642e97495e1  18 GB  100% GPU   131072   Forever

# the runner's own accounting (Ollama.app writes ~/.ollama/logs/server.log; this launchd install logs to ~/Library/Logs)
grep "speculative decode stats" ~/Library/Logs/ollama.err.log | tail -1
# msg="speculative decode stats" iterations=225 drafted=458 accepted=287 acceptance=0.63 avg_draft=2.04 max_draft=4
#   -> 512 tokens in 225 passes through the weights: 2.3 tokens per pass, 45 ms per pass (217-225 over four runs)

# the GGUF engine drafts with the same head, and the community benchmark on the same blob does not:
grep -E "MTP draft|draft acceptance" ~/Library/Logs/ollama.err.log | tail -2
# common_speculative_init_result: creating MTP draft context against the target model ...
# slot print_timing: draft acceptance = 0.86695 ( 2906 accepted /  3352 generated), mean len =  4.47
./llama.cpp/build/bin/llama-bench -m ~/.ollama/models/blobs/sha256-<the qwen3.8:latest blob> -p 512 -n 128 -ngl 99
# | qwen35 27B Q4_K - Medium | 15.65 GiB | 27.32 B | MTL,BLAS | tg128 | 26.39 ± 0.44 |
ollama_wall.py — 벽시계로 안 잰 속도는 믿지 마·python
#!/usr/bin/env python3
"""Stream from the Ollama API and time the tokens yourself. eval_count / eval_duration
is the daemon's own rate; first-chunk-to-done is the wall clock. They should agree.
What neither tells you is how many passes through the weights produced them —
that is in the runner's log."""
import json, time, urllib.request


def run(model: str, prompt: str, n: int = 512) -> None:
    body = {"model": model, "prompt": prompt, "stream": True, "think": False,
            "options": {"num_predict": n, "temperature": 0, "seed": 1}}
    req = urllib.request.Request("http://127.0.0.1:11434/api/generate", data=json.dumps(body).encode(),
                                 headers={"Content-Type": "application/json"})
    first = None
    with urllib.request.urlopen(req, timeout=900) as resp:
        for line in resp:
            d = json.loads(line)
            if d.get("done"):
                last = d
                break
            first = first or time.perf_counter()
    wall = time.perf_counter() - first
    ec, ed = last["eval_count"], last["eval_duration"] / 1e9
    print(f"{model}: {ec} tokens  reported {ec/ed:.1f} tok/s  wall {ec/wall:.1f} tok/s  "
          f"(load {last['load_duration']/1e9:.2f}s, prompt {last['prompt_eval_duration']/1e9:.2f}s)")


for p in ("Explain unified memory in 400 words.",
          "Repeat this sentence exactly 40 times, one per line: The quick brown fox jumps over the lazy dog."):
    run("qwen3.8:27b-mlx", p)

# office, 2026-09-15, four prose runs: reported 47.57 / 47.60 / 49.33 / 50.62; wall 47.79 / 47.98 / 49.63 / 50.93
# repetition (384 tokens): wall 90.2 and 91.0 tok/s -- acceptance 1.00, 4.9 tokens per pass in the runner log
# the ceiling for 14.45 GB per token at 638 GB/s is 44: every number above it is more than one token per pass

External links

Exercise

모델 하나를 Ollama로 --verbose를 붙여 두 번 돌리고, 그다음 ollama_wall.py를 돌려. 네 데몬의 로그를 찾아서 speculative decode stats를 찾아봐. 있으면 패스당 토큰과 패스당 밀리초를 계산해서 eval rate 옆에 둘 다 카드에 적어. 없으면 네 eval rate는 패스당 속도고, 그렇게 카드에 올라가. 그다음 네 맥에서 그 모델의 토큰당 바이트로 구한 상한과 비교해.
Hint
초당 패스 × 토큰당 바이트가 런타임이 실제로 끌어온 대역폭이고, 패스당 토큰이 추측의 이득이야. 로그에 추측 통계가 없는데 상한 위의 속도가 나오면, 네 토큰당 바이트가 틀렸거나(비전 타워나 임베딩을 셌거나. 물리 트랙의 고백) 그 로그가 러너의 것이 아닌 거야.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.