~15 min · lab, context, kv-cache, ttft, slope, measured
Level 0스펙 시트 훑는 사람
0 XP0/91 lessons0/19 achievements
0/100 XP to next level100 XP to go0% complete
"헤드라인 디코드 숫자는 실제 사용 몇 분 안에 기계가 떠나는 상태를 서술해. 네가 실제로 있을 자리에서 재. 창의 끝 근처, 캐시가 찬 채로."
유일하게 중요한 벤치마크
하드웨어 타이밍에 대한 이 집의 독트린엔 벤치마크에 대한 줄이 하나 있어. 유일하게 중요한 건 컨텍스트 창의 90퍼센트에서의 디코드 초당 토큰과 첫 토큰까지의 시간. 사다리가 재는 모든 건 209토큰 프롬프트에서 일어나고, 거기서 일하는 사람은 없어. 코딩 세션이나 긴 문서는 수만 토큰에 살고, 거기선 KV 캐시가 모델 하나만큼의 바이트고 프리필은 사용자가 뒤에서 기다리는 벽이야. 그래서 실험실의 두 번째 실험은 32,768토큰 창을 고정하고 모델 셋을 그 10, 50, 90%에서, 200토큰 출력으로, office에서 돌려. 캐시 대 가중치 비율로 고른 모델 셋. 순수 어텐션 1B(가중치 0.7 GB에 토큰당 캐시 32 KB), 하이브리드 9B(4.5 GB에 32 KB), 하이브리드 27B(14.4 GB에 64 KB).
모델, office
컨텍스트
TTFT
프리필 tok/s
디코드 tok/s
토큰당 ms
피크 GB
증거
Llama-3.2-1B, 4비트
3,254(10%)
0.56 s
6,572
343.6
2.91
1.63
실측 2026-09-15
16,302(50%)
2.54 s
6,562
237.4
4.21
1.97
실측
29,406(90%)
5.82 s
5,105
208.5
4.80
2.38
실측
Qwen3.5-9B, 4비트
3,233
2.69 s
1,277
94.4
10.60
6.77
실측
16,337
13.12 s
1,259
86.3
11.59
8.13
실측
29,385
24.90 s
1,187
78.1
12.80
9.39
실측
Qwen3.5-27B, 4비트
3,233
8.96 s
367
32.3
31.00
18.21
실측
16,337
45.94 s
357
29.3
34.09
20.46
실측
29,385
90.38 s
326
27.5
36.35
22.57
실측
벽 둘, 따로 읽기
프리필 벽. TTFT는 거의 일정한 프리필 속도에서 프롬프트 길이에 선형이야. 27B에서 10%에 9초, 90%에 90초, 이 집에서 가장 빠른 맥에서. 프리필은 컨텍스트에 거의 안 느려지니(367 → 326 tok/s) 벽은 그저 프롬프트 크기를 연산 단계의 속도로 나눈 거야. 27B로 32K 문서면 첫 토큰까지 1분 반이고, 대역폭의 무엇도 그걸 안 바꿔. 디코드 기울기. 토큰당 밀리초는 컨텍스트에 선형으로 올라. 각 토큰이 이제 가중치에 더해 캐시도 읽으니까. 1B는 창 전체에서 디코드 속도의 39%를 잃고, 9B는 17%, 27B는 15%. 물리 트랙이 캐시 대 가중치에서 비율을 예측했고 여기가 그걸 잰 자리야. 1B가 예측된 2.3배에 못 미치는 건 3밀리초짜리 토큰을 고정 비용이 희석하기 때문이고, 대역폭 레슨의 맞춤이 그걸 1.67 ms로 이미 값 매겼어.
기울기가 숨긴 숫자
컨텍스트 토큰당 더해지는 캐시 바이트를 컨텍스트 토큰당 더해지는 밀리초로 나누면 캐시가 실제로 읽힌 대역폭이 나와. 1B에 444 GB/s, 9B에 379, 27B에 313. 이 맥에서 가중치는 497로 스트림됐어. 캐시는 가중치보다 덜 효율적으로 읽히고, 모델의 토큰당 캐시가 클수록 더 덜 효율적이야. 길고 띄엄띄엄한 캐시를 읽는 어텐션 커널은 연속된 가중치를 읽는 행렬-벡터 곱이 아니고, 실험실은 커널 수준의 원인을 이름 붙이지 않고 비율을 보고해. 카드엔: 기계 사이에 비교할 건 10%가 아니라 90%의 숫자야. 209토큰에서 이기고 29,000에서 지는 맥은 캐시 읽기가 느린 쪽이고, 그건 사다리가 할 수 없는 측정이야.
Code
lab_curve.py — 곡선을 표로, 그리고 기울기에서 캐시 읽기 대역폭·python
#!/usr/bin/env python3
"""Decode and TTFT at 10 / 50 / 90 % of a 32K window. Seconds per token is linear
in context: t(N) = t0 + (KV bytes per token × N) / BW_cache, so the slope of the
three points gives the bandwidth at which the cache was actually read -- compare it
with the weight-streaming bandwidth the ladder fit. Standard library only.
Usage: lab_curve.py curve-office.jsonl"""
import json, sys
KV_PER_TOKEN = {"Llama-3.2-1B-Instruct-4bit": 32e3, "Qwen3.5-9B-4bit": 32e3, "Qwen3.5-27B-4bit": 64e3} # bytes, from the configs (T6)
WEIGHT_BW = 497e9 # office ladder fit (previous lesson)
rows = [json.loads(l) for l in open(sys.argv[1])]
by = {}
for r in rows:
if r.get("experiment") == "curve":
by.setdefault(r["model"].split("/")[-1], []).append(r)
print(f"{'model':28} {'ctx tokens':>10} {'TTFT s':>8} {'prefill':>8} {'decode':>8} {'ms/token':>9} {'peak GB':>8}")
for name, rs in by.items():
rs.sort(key=lambda r: r["prompt_tokens"])
for r in rs:
print(f"{name:28} {r['prompt_tokens']:10d} {r['ttft_s']:8.2f} {r['prompt_tps']:8.0f} {r['generation_tps']:8.1f} {1e3/r['generation_tps']:9.2f} {r['peak_memory_gb']:8.2f}")
n0, n2 = rs[0]["prompt_tokens"], rs[-1]["prompt_tokens"]
t0, t2 = 1 / rs[0]["generation_tps"], 1 / rs[-1]["generation_tps"]
slope = (t2 - t0) / (n2 - n0) # seconds per token per context token
bw_cache = KV_PER_TOKEN[name] / slope
w = rs[0]["weight_bytes_per_token"]
naive = rs[0]["generation_tps"] * (w + KV_PER_TOKEN[name] * n0) / (w + KV_PER_TOKEN[name] * n2) # every byte at one bandwidth
print(f"{'':28} decode 10% -> 90%: {100*(rs[-1]['generation_tps']-rs[0]['generation_tps'])/rs[0]['generation_tps']:+.0f}% "
f"TTFT at 90%: {rs[-1]['ttft_s']:.1f} s cache read at {bw_cache/1e9:.0f} GB/s (weights: {WEIGHT_BW/1e9:.0f}) "
f"naive one-bandwidth prediction at 90%: {naive:.1f} tok/s\n")
# office, M3 Ultra, 2026-09-15, window 32,768, 200 generated:
# Llama-3.2-1B-Instruct-4bit 3254 / 16302 / 29406 ctx: TTFT 0.56 / 2.54 / 5.82 s; decode 343.6 / 237.4 / 208.5 (-39%); cache read at 444 GB/s
# Qwen3.5-9B-4bit 3233 / 16337 / 29385: TTFT 2.69 / 13.12 / 24.90; decode 94.4 / 86.3 / 78.1 (-17%); cache read at 379 GB/s
# Qwen3.5-27B-4bit 3233 / 16337 / 29385: TTFT 8.96 / 45.94 / 90.38; decode 32.3 / 29.3 / 27.5 (-15%); cache read at 313 GB/s