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

M3 사다리 위의 프리필과 디코드

~15 min · lab, m3, ladder, prefill, decode, measured

Level 0스펙 시트 훑는 사람
0 XP0/91 lessons0/19 achievements
0/100 XP to next level100 XP to go0% complete
"마이크로아키텍처 하나, 크기 셋, 집 하나. 다이를 두 배로 해도 토큰이 두 배가 되진 않아. 그리고 두 단계가 다른 만큼 모자라는데, 그게 레슨이야."

왜 한 세대가 실험인가

이 집은 우연히 M3를 세 크기 다 갖고 있어. Air의 기본 칩(GPU 코어 10, 100 GB/s, 24 GB), 2023 MacBook Pro의 Max(코어 40, 400 GB/s, 128 GB), office Studio의 Ultra(코어 80, 819 GB/s, 512 GB). 같은 GPU 코어, 같은 메모리 기술, 같은 macOS 빌드, 같은 MLX. 바뀌는 건 개수뿐이고, 벤더의 표가 묶어 놓은 두 차원에서 바뀌어. 대역폭은 1 : 4 : 8.2, GPU 코어는 1 : 4 : 8. 물리 트랙은 프리필이 연산 단계고 디코드가 대역폭 단계라고 말해. 그게 참이면 프리필은 코어를 따르고 디코드는 대역폭을 따라야 하고, 다른 게 끼어들지 않는 한 둘 다 선형으로 늘어야 해. 사다리가 그 테스트야.

모델(4비트)GB/토큰air, M3 · 프리필 / 디코드pro2023, M3 Maxoffice, M3 Ultra증거
Qwen3.5-0.8B0.421,707 / 167.64,017 / 416.66,312 / 338.2실측 2026-09-15
Qwen3.5-2B1.06774 / 76.82,441 / 246.93,992 / 257.1실측
Qwen3.5-4B2.37308 / 34.41,126 / 121.31,748 / 147.6실측
Qwen3.5-9B4.47172 / 19.4642 / 72.91,043 / 95.1실측
Qwen3.5-27B14.4249 / 6.1195 / 23.4315 / 32.6실측
Qwen3.5-35B-A3B1.66—(20 GB, 24 GB에선 안 돌림)909 / 109.91,276 / 89.1실측

기본에서 Max로: 선형

코어 네 배, 대역폭 네 배. 9B에서 프리필 3.7배, 디코드 3.8배. 27B에서 프리필 4.0배, 디코드 3.8배. 0.8B에선 디코드 2.5배. 고정 비용의 몫이야. Max의 낮은 토큰당 오버헤드(Air의 1.45에 대해 1.12 ms)도 토큰당 3밀리초에선 다 못 숨겨. 바쁜 노트북의 잡음 안에서 Max는 Air 넷이고, 물리 트랙의 두 단계는 벤더가 그럴 거라고 말한 두 가지에 따라 늘어.

Max에서 Ultra로: 두 번째 다이의 절반

다이 둘, 그러니 종이 위에선 코어 두 배, 대역폭 두 배(2.05배). 9B 실측: 프리필 1.62배, 디코드 1.30배. 27B: 프리필 1.62배, 디코드 1.39배. 혼합 모델: 프리필 1.40배, 디코드 0.81배. 더 느려. 두 번째 다이는 적힌 프리필의 60퍼센트, 적힌 디코드의 30에서 40을 사 주고, 작은 커널이 많은 모델엔 배치 1에서 아무것도 못 사 줘. 앞 레슨의 맞춤이 숫자 둘로 왜인지 말해. Ultra의 디코드 커널은 Max의 87%에 대해 스펙의 61%를 끌어오고, 토큰당 고정 비용은 1.12에 대해 1.67 ms. 연산인 프리필은 덜 잃어. 코어 80개가 전부 바쁠 수 있는 단계니까. M2 대 M3 Ultra 레슨이 세대 사이에서도 찾을 바로 그 패턴이야. 어느 것도 결함이 아니야. 토큰 하나의 규모에서 인터포저가 치르는 값이고, 비교의 양쪽을 다 가진 집에서 쟀어.

사다리를 올라가며 사는 건 실제로 뭔가

셋이고, 서로 다른 것들이야. 기본에서 Max는 실리콘 값에 비례한 속도를 사. Air 넷만큼의 프리필과 디코드. Max에서 Ultra는 속도보다 용량을 사. 4비트 750B급 혼합 모델(450 GB쯤. 저장소의 DeepSeek-V4.1-Flash는 출하 그대로 510 GB)을 담는 512 GB, 어떤 Max도 아예 못 담는 것. 거기에 둘 다 들어가는 모델에서 디코드 3분의 1 더, 프리필 60퍼센트 더. Air는 한편 27B를 작업 집합 안에서 초당 6.1토큰으로 돌렸어. 실효 88 GB/s로, 사다리의 맞춤값 89에 딱 붙어서. 사다리의 바닥은 느린 거지 망가진 게 아니야. 이 레슨의 네 카드 행은 네가 가진 티어와 다음 티어에서 얻을 비율이야. 스펙 시트가 아니라 표에서 읽어.

Code

lab_ladder.py — M3 사다리를 표 하나로, 기본 칩 대비 비율과 함께·python
#!/usr/bin/env python3
"""The M3 ladder, one table: base (air) / Max (pro2023) / Ultra (office), plus the
M2 Ultra (music) for the next lessons. Prefill and decode per model, and each
number's ratio to the base M3 -- bandwidth ratio is 1 : 4 : 8.2, GPU cores 1 : 4 : 8.
Standard library only. Usage: lab_ladder.py air.jsonl pro2023.jsonl office.jsonl music.jsonl"""
import json, sys

machines = []
for path in sys.argv[1:]:
    rows = {}
    for line in open(path):
        r = json.loads(line)
        if r.get("experiment") == "ladder":
            rows[r["model"].split("/")[-1]] = r
    first = next(iter(rows.values()))
    machines.append((first["alias"], first["device"], first["spec_gbps"], rows))

order = ["Qwen3.5-0.8B-4bit", "Qwen3.5-2B-4bit", "Qwen3.5-4B-4bit", "Qwen3.5-9B-4bit", "Qwen3.5-27B-4bit", "Qwen3.5-35B-A3B-4bit"]
base = machines[0][3]
print("model                 GB/tok |" + "".join(f" {m[0]:>9} {m[1].replace('Apple ',''):<9} |" for m in machines))
print("                             |" + "".join(f" {'prefill':>9} {'decode':<9} |" for _ in machines))
for name in order:
    line = f"{name:21} {base[name]['weight_bytes_per_token']/1e9:6.2f} |" if name in base else f"{name:21} {'':6} |"
    for alias, dev, spec, rows in machines:
        r = rows.get(name)
        line += f" {r['prompt_tps']:9.0f} {r['generation_tps']:<9.1f} |" if r else f" {'-':>9} {'-':<9} |"
    print(line)
print("\nratios to the base M3 (air), 9B row:")
r0 = base["Qwen3.5-9B-4bit"]
for alias, dev, spec, rows in machines:
    r = rows["Qwen3.5-9B-4bit"]
    print(f"  {alias:8} spec BW x{spec/100:4.1f}   prefill x{r['prompt_tps']/r0['prompt_tps']:4.1f}   decode x{r['generation_tps']/r0['generation_tps']:4.1f}")

# 2026-09-15, mlx 0.32.2 / mlx-lm 0.31.3, prompt 209 tokens, 200 generated, greedy, batch 1:
# model                 GB/tok |       air M3        |   pro2023 M3 Max    |    office M3 Ultra  |     music M2 Ultra  |
#                              |   prefill decode    |   prefill decode    |   prefill decode    |   prefill decode    |
# Qwen3.5-0.8B-4bit       0.42 |      1707 167.6     |      4017 416.6     |      6312 338.2     |      5396 369.4     |
# Qwen3.5-2B-4bit         1.06 |       774 76.8      |      2441 246.9     |      3992 257.1     |      3055 275.8     |
# Qwen3.5-4B-4bit         2.37 |       308 34.4      |      1126 121.3     |      1748 147.6     |      1333 153.3     |
# Qwen3.5-9B-4bit         4.47 |       172 19.4      |       642 72.9      |      1043 95.1      |       789 105.5     |
# Qwen3.5-27B-4bit       14.42 |        49 6.1       |       195 23.4      |       315 32.6      |       233 35.3      |
# Qwen3.5-35B-A3B-4bit         |         - -         |       909 109.9     |      1276 89.1      |       986 101.5     |
# ratios to the base M3, 9B row: pro2023 BW x4.0 prefill x3.7 decode x3.8; office BW x8.2 prefill x6.1 decode x4.9; music BW x8.0 prefill x4.6 decode x5.5

External links

Exercise

닿을 수 있는 한 세대의 모든 맥에서, 아니면 네 맥 하나에 친구 것 하나를 더해 사다리를 돌리고, lab_ladder.py로 표를 만들어. 9B(또는 둘 다 담는 가장 큰 모델)의 프리필과 디코드 비율을 스펙 비율 옆에 카드에 적어. 그다음 답해. 어느 티어 경계가 선형으로 늘었고 어느 게 아니었는지, 아닌 쪽에선 어느 단계가 더 잃었는지.
Hint
기본에서 Max는 거의 선형이고 인터포저를 건너는 건 뭐든 모자랄 거라고, 프리필보다 디코드가 더 모자랄 거라고 예상해. 어떤 경계가 선형보다 잘 늘면 한 기계가 열로 스로틀됐거나 바빴던 거야. 느린 쪽을 다시 돌려.

Progress

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

댓글 0

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

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