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

컨텍스트 10, 50, 90%에서의 디코드와 TTFT

~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 s6,572343.62.911.63실측 2026-09-15
16,302(50%)2.54 s6,562237.44.211.97실측
29,406(90%)5.82 s5,105208.54.802.38실측
Qwen3.5-9B, 4비트3,2332.69 s1,27794.410.606.77실측
16,33713.12 s1,25986.311.598.13실측
29,38524.90 s1,18778.112.809.39실측
Qwen3.5-27B, 4비트3,2338.96 s36732.331.0018.21실측
16,33745.94 s35729.334.0920.46실측
29,38590.38 s32627.536.3522.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

External links

Exercise

네 메모리가 담는 창(창을 고르기 전에 피크 GB를 확인해)의 10, 50, 90%에서 모델 하나로 네 맥의 곡선을 돌려. 90%에서의 TTFT와 디코드를 중요한 행으로 카드에 적고, 기울기에서 캐시 읽기 대역폭을 유도해서 레슨 2의 맞춤 가중치 대역폭과 비교해.
Hint
하이브리드 모델에서 90% 디코드가 10% 디코드의 절반 넘게면 창이 가중치에 비해 작은 거야. 괜찮아, 창을 말해 두기만 해. 90% TTFT가 10% TTFT의 대략 아홉 배가 아니면 프롬프트 채우기가 기대한 수로 토큰화되지 않은 거야.

Progress

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

댓글 0

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

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