~14 min · lab, protocol, prediction, safetensors, provenance, measured
Level 0스펙 시트 훑는 사람
0 XP0/91 lessons0/19 achievements
0/100 XP to next level100 XP to go0% complete
"예측하지 않은 숫자는 놀랄 수 없는 숫자야. 실험실의 첫 규칙은 놀람을 가능하게 만드는 거야."
프로토콜
이 퀘스트의 모든 실측 숫자는 작은 프로그램 하나, 이 퀘스트를 위해 쓴 silicon_lab.py에서 나왔어. 하루에 이 집의 맥 네 대에서 돌렸어. Air의 기본 M3, 2023 MacBook Pro의 M3 Max, office Studio의 M3 Ultra, music Studio의 M2 Ultra. 다섯 가지를 정해진 순서로 하고, 그 순서가 프로토콜이야. 스탬프: 뭘 돌리기 전에 기계를 역할 별칭으로, 칩을 MLX가 부르는 이름으로, 메모리와 권장 작업 집합, macOS 빌드, MLX와 mlx-lm 버전, UTC 시각을 기록해. 이게 없는 숫자는 소문이야. 바이트 세기: 체크포인트의 safetensors 헤더를 열어 디코드 단계가 실제로 읽는 걸 물리 트랙의 규칙으로 더해. 비전 타워는 빼고, 출력 헤드에 묶이지 않았으면 임베딩 테이블도 빼고, 라우팅되는 전문가는 E 중 k로. config 파일은 모델이 뭔지 말하고, 헤더는 디스크에 뭐가 있는지 말해. 예측: 칩의 스펙 대역폭을 그 바이트로 나눠서 모델을 로드하기 전에 상한을 출력해. 워밍업: 토큰 여덟 개를 생성하고 버려. 여정 트랙의 첫 순전파 페이지 건드리기가 타이밍에 안 들어가도록. 측정: 209토큰쯤의 고정 프롬프트, 그리디로 200토큰 출력, 배치 1, 그리고 프리필 속도, 첫 토큰까지의 시간, 디코드 속도, 피크 메모리를 예측 옆에 기록해.
예측이 먼저인 이유
첫 office 사다리는 틀렸고, 예측이 그걸 잡은 방법이야. 그 실행은 디스크의 모든 바이트를 토큰당 읽는 바이트로 셌어. 0.8B에 0.42 대신 0.63 GB, 9B에 4.47 대신 5.95, 혼합 모델에 1.66 대신 20.39. 잰 디코드 속도는 멀쩡했어. 거기서 유도한 실효 대역폭이 아니었어. 버스가 819를 나르는 칩에서 혼합 모델에 1,815 GB/s. 상한 위의 숫자는 빠른 기계가 아니라 틀린 집계야. 그럴듯해 보이는 속도들의 화면에서 그 줄이 튀어나온 유일한 이유는 상한이 옆에 출력돼 있었기 때문이야. 실행 1은 버렸고, 위의 바이트 규칙을 썼고, 물리 트랙의 전문가 레슨이 그 고백을 실어. 예측이 없었다면 틀린 숫자가 결과였을 거야.
측정 기록엔 뭐가 들어가나
실행당 한 줄, JSON, 기계가 보관하는 파일에 덧붙여. 실험실의 기록은 스탬프, 바이트 집계(전체, 비전, 임베딩, 전문가, 묶임 여부, 토큰당), config가 말하는 양자화, 프롬프트와 생성 토큰 수, 속도 넷, 피크 메모리, 그리고 실효 대역폭을 옆에 둔 예측을 실어. 이 퀘스트의 모든 표는 그 줄들에서 네가 같은 파일에 돌릴 수 있는 스크립트로 만들었고, 퀘스트가 보관하는 측정 동반 문서는 그것들의 사람이 읽는 색인이야. 네가 채워 온 맥 카드는 같은 아이디어를 손으로 한 거야. 별칭, 날짜, 빌드, 버전, 모델, 비트, 토큰당 바이트, 예측, 측정, 그리고 격차. 아래 표는 벤더의 819 GB/s로 예측해. 물리 트랙의 같은 행들은 office에서 커널이 실제로 스트림한 638 GB/s로 예측했고, 그래서 거기 비율이 더 높게 읽혀(양 끝에서 22%와 74%). 같은 측정, 정직한 분모 둘. 카드는 어느 걸 썼는지 말해야 해.
def stamp(alias: str) -> dict:
"""Who, what, when: a measurement without this is a rumour."""
info = mx.device_info()
return {
"alias": alias,
"device": info.get("device_name"),
"memory_gb": round(info.get("memory_size", 0) / 1e9, 1),
"recommended_working_set_gb": round(info.get("max_recommended_working_set_size", 0) / 1e9, 1),
"macos": macos_build(),
"mlx": mx.__version__,
"mlx_lm": mlx_lm.__version__,
"date": datetime.now(timezone.utc).isoformat(timespec="seconds"),
}
def weight_bytes(path: Path) -> dict:
"""Bytes a token must read during decode — from the safetensors headers, not the config.
Not every byte on disk is read per token: a vision tower never is; the input embedding is a
one-row lookup unless tied to the output head; a mixture reads k of E routed experts."""
cfg = json.loads((path / "config.json").read_text())
text_cfg = cfg.get("text_config", cfg)
n_experts = text_cfg.get("num_experts") or text_cfg.get("num_local_experts") or 0
k = text_cfg.get("num_experts_per_tok") or 0
tied = bool(cfg.get("tie_word_embeddings", text_cfg.get("tie_word_embeddings", False)))
total = vision = embed = expert = 0
for f in sorted(path.glob("*.safetensors")):
with f.open("rb") as fh:
n = struct.unpack("<Q", fh.read(8))[0]
header = json.loads(fh.read(n))
for name, meta in header.items():
if name == "__metadata__":
continue
a, b = meta["data_offsets"]
size = b - a
total += size
if "vision" in name or "visual" in name:
vision += size
elif "embed_tokens" in name:
embed += size
elif "switch_mlp" in name or ".experts." in name:
expert += size
touched = total - vision - (0 if tied else embed)
if n_experts:
touched -= expert * (1 - k / n_experts)
return {"weight_bytes_total": total, "weight_bytes_vision": vision, "weight_bytes_embed": embed,
"weight_bytes_experts": expert, "tied_embeddings": tied, "experts": n_experts,
"experts_per_token": k, "weight_bytes_per_token": int(touched)}
def ladder(args) -> None:
base = stamp(args.alias)
for repo in args.models:
path = snapshot_dir(repo)
wb = weight_bytes(path)
predicted = args.spec_gbps * 1e9 / wb["weight_bytes_per_token"] # THE PREDICTION, before load()
print(f"prediction: {args.spec_gbps} GB/s / {wb['weight_bytes_per_token']/1e9:.2f} GB = {predicted:.1f} tok/s ceiling")
model, tokenizer = load(str(path))
prompt_ids = build_prompt(tokenizer, args.prompt_tokens, QUESTION)
run_once(model, tokenizer, prompt_ids, 8) # warm the kernels; discard
rec = {**base, "experiment": "ladder", "model": repo, **wb}
rec.update(run_once(model, tokenizer, prompt_ids, args.max_tokens)) # prefill, TTFT, decode, peak
rec["predicted_decode_tps"] = round(predicted, 1)
rec["effective_gbps"] = round(rec["generation_tps"] * wb["weight_bytes_per_token"] / 1e9, 1)
with Path(args.out).open("a") as fh:
fh.write(json.dumps(rec) + "\n") # one JSON line per run; the file is the record
# office, run 1 (every byte on disk counted per token), 2026-09-15:
# Qwen3.5-35B-A3B-4bit 20.39 GB/tok predicted 40.2 measured 88.99 -> effective 1814.6 GB/s on an 819 GB/s bus
# corrected: 1.66 GB/tok, predicted 493.7, measured 89.1, effective 147.9 GB/s
뭘 돌리기 전에: 네 캐시의 4비트 모델 하나에 대해 safetensors 헤더를 열고, 바이트 규칙 셋을 적용하고, 네 맥의 상한(스펙 대역폭 ÷ 토큰당 바이트)을 카드에 적어. 그다음에야 그 모델 하나로 실험실의 사다리를 돌리고 실측 디코드를 옆에 적어. 비율이 100% 위면 다른 무엇보다 먼저 집계 오류를 찾아.
Hint
흔한 집계 오류, 빈도순: 비전 타워(작은 VLM 바이트의 5분의 1), 묶이지 않은 모델의 임베딩 테이블, 혼합 모델의 전문가를 전부 센 것. 4B보다 큰 모델에서 비율이 15% 아래면 워밍업이 안 됐거나 기계가 바빴던 거야. 다시 돌려.
Progress
Progress is local-only — sign in to sync across devices.