~15 min · lab, regression, bandwidth, fixed-overhead, stream, measured
Level 0스펙 시트 훑는 사람
0 XP0/91 lessons0/19 achievements
0/100 XP to next level100 XP to go0% complete
"모델 다섯, 점 다섯, 선 하나. 기울기는 네 런타임이 보는 버스고, 절편은 네 런타임이 하는 일 중 읽기가 아닌 전부야."
맞춤
물리 트랙의 디코드 공식엔 항이 둘이야. 토큰당 초 = 고정 비용 + 토큰당 바이트 ÷ 대역폭. 같은 가족, 같은 양자화의 밀집 모델 사다리를 한 기계에서 돌리고, x축에 토큰당 바이트, y축에 토큰당 초를 놓으면 점들이 선 위에 떨어져. 고정 비용은 바이트에 관심이 없고 대역폭 항은 바이트에 선형이니까. 보통 최소제곱이 어떤 벤치마크 표도 출력하지 않는 숫자 둘을 줘. 절편, 모델 하나만큼의 커널을 띄우고 샘플링하고 루프를 도는 토큰당 비용. 그리고 기울기, 그 역수가 런타임이 풀에서 실제로 끌어온 대역폭. 혼합 전문가 모델은 일부러 뺐어. 토큰당 바이트는 작고 커널 수는 작지 않아서 선 위쪽에 앉고, 전문가 레슨이 이미 왜인지 설명했어.
맥, 2026-09-15
칩, 스펙 GB/s
토큰당 고정 비용
맞춤 대역폭
맞춤 ÷ 스펙
최악 잔차
증거
air
M3, 100
1.45 ms
89 GB/s
89%
0.92 ms
실측, 밀집 모델 5
pro2023
M3 Max, 400
1.12 ms
347 GB/s
87%
0.30 ms
실측, 5
office
M3 Ultra, 819
1.67 ms
497 GB/s
61%
0.73 ms
실측, Llama-1B 포함 6
music
M2 Ultra, 800
1.80 ms
546 GB/s
68%
0.51 ms
실측, 5
두 숫자 읽기
고정 비용은 어느 맥에서든 토큰당 1.1에서 1.8밀리초고, 단일 다이 Max에서 가장 낮아. Ultra 둘은 다이 둘을 먹이느라 토큰당 더 치르고, 그게 가장 작은 모델이 M3 Max에서 가장 빨리 도는 이유의 전부야. 27B의 30밀리초짜리 토큰에 대해선 잡음이고, 0.8B의 3밀리초짜리 토큰에 대해선 시간의 절반이야. 앞 레슨의 상한 대비 비율 칸의 모양이 그거야. 맞춤 대역폭이 더 놀라운 숫자야. 기본 M3와 M3 Max는 디코드 커널에 스펙의 87–89%를 전달해. M3 Ultra는 61%, 한 세대 오래된 같은 메모리 시스템의 M2 Ultra는 68%. 읽고 더하기만 하는 스트리밍 커널, GPU 트랙의 스트림 테스트는 M3 Ultra에서 78%, M2 Ultra에서 92%를 얻어. 그러니 M3 Ultra에선 격차 일부가 메모리 시스템이고 일부가 다이 둘에서 코어 80개를 먹이는 디코드 커널 자체의 비효율인데, 92%를 스트림해서 단일 다이 대역에 있는 M2 Ultra에선 격차가 거의 전부 커널 몫이야. 실험실은 사용자 공간에서 그 둘을 갈라 놓을 수 없어. 두 숫자와 그 사이의 격차를 보고해.
이 숫자의 쓸모
예측해. 네 맥의 a와 BW가 있으면 아직 안 받은 어떤 밀집 모델의 디코드 속도든 1 ÷ (a + 바이트 ÷ BW)고, 이 네 기계에서 사다리 전체의 최악 오차는 토큰당 1밀리초 아래야. 버스에 대해선 상한이 아니라 바닥이야. 더 좋은 커널은 더 끌어올 수 있고(여정 트랙의 PyTorch 행렬-벡터 곱은 이 맞춤의 497에 대해 office에서 605 GB/s를 끌어왔어), 스펙보다 많이 끌어올 수 있는 건 없어. 그리고 추측 디코딩을 드러내. 스트림한 대역폭보다 많이 함의하는 속도는 패스당 토큰 하나 넘게고, Ollama 레슨이 그걸 찾았어. 카드엔 두 숫자와 날짜가 들어가. 런타임 릴리스가 어느 쪽이든 움직일 수 있으니까.
Code
lab_fit.py — 토큰당 바이트에 대한 토큰당 초, 맥마다 맞춤·python
#!/usr/bin/env python3
"""Bandwidth from decode speed. Over the dense models of one machine's ladder,
fit seconds-per-token = a + bytes-per-token / BW. The slope's reciprocal is the
bandwidth your runtime actually pulled; the intercept is its fixed cost per token.
Standard library only. Usage: lab_fit.py ladder-office.jsonl [more.jsonl ...]"""
import json, sys
def dense_rows(path):
for line in open(path):
r = json.loads(line)
if r.get("experiment") == "ladder" and r.get("experts", 0) == 0: # dense only: experts break the straight line
yield r
def fit(xs, ys): # ordinary least squares, y = a + b x
n = len(xs); mx = sum(xs) / n; my = sum(ys) / n
b = sum((x - mx) * (y - my) for x, y in zip(xs, ys)) / sum((x - mx) ** 2 for x in xs)
return my - b * mx, b
for path in sys.argv[1:]:
rs = list(dense_rows(path))
xs = [r["weight_bytes_per_token"] for r in rs] # bytes read per token
ys = [1.0 / r["generation_tps"] for r in rs] # seconds per token
a, b = fit(xs, ys)
bw, spec = 1.0 / b, rs[0].get("spec_gbps")
worst = max(abs(y - (a + b * x)) for x, y in zip(xs, ys))
print(f"{rs[0]['alias']:8} {rs[0]['device']:16} {len(rs)} dense models: "
f"fixed {a*1e3:.2f} ms/token + bytes / {bw/1e9:.0f} GB/s"
+ (f" ({bw/1e9/spec*100:.0f}% of spec {spec:.0f})" if spec else "")
+ f" worst residual {worst*1e3:.2f} ms")
for r, x, y in zip(rs, xs, ys):
print(f" {r['model'].split('/')[-1]:30} {x/1e9:6.2f} GB measured {1/y:7.1f} tok/s fit {1/(a+b*x):7.1f}")
# office Apple M3 Ultra 6 dense models: fixed 1.67 ms/token + bytes / 497 GB/s (61% of spec 819) worst residual 0.73 ms
# pro2023 Apple M3 Max 5 dense models: fixed 1.12 ms/token + bytes / 347 GB/s (87% of spec 400) worst residual 0.30 ms
# music Apple M2 Ultra 5 dense models: fixed 1.80 ms/token + bytes / 546 GB/s (68% of spec 800) worst residual 0.51 ms
# air Apple M3 5 dense models: fixed 1.45 ms/token + bytes / 89 GB/s (89% of spec 100) worst residual 0.92 ms