Skip to content
C.W.K.
Stream
Lesson 02 of 07 · published

The Decode Ceiling: Bandwidth ÷ Bytes per Token

~16 min · llm-physics, decode-ceiling, bytes-per-token, safetensors, prediction, measured

Level 0Spec-Sheet Skimmer
0 XP0/91 lessons0/19 achievements
0/100 XP to next level100 XP to go0% complete
"The fastest a model can decode is the number of times per second its weights can be read. Everything else is a reason it is slower."

The Formula

If decode must read every active weight once per token, then tokens per second cannot exceed bytes per second divided by bytes per token. That is the whole formula — ceiling = bandwidth ÷ bytes per token — and it has two inputs the previous tracks have already produced: achieved memory bandwidth (the GPU track) and the model's bytes per token, which this lesson defines carefully, because getting it wrong is the commonest way the formula is misused.

Which Bytes Count

The lab script reads every tensor's size from the safetensors headers — the file is the truth, not the model card — and applies four rules. A vision tower is never read by text decode; Qwen3.5 checkpoints carry one (0.9 GB on the 9B), and counting it was this quest's first mistake. The input embedding is a table lookup — one row per token, not the whole matrix — unless the checkpoint ties it to the output head, in which case the matrix is read in full as the head. Routed experts in a mixture-of-experts layer are read k of E per token: the 35B-A3B reads 8 of 256, so its 18 GB of experts count as 0.57. Everything else — attention, linear attention, dense MLPs, norms, the head — is read once per token.

Model (4-bit)On diskVisionEmbeddingExperts (k/E)Bytes per tokenEvidence
Qwen3.5-0.8B0.625 GB0.2010.143, tied (counted)0.424 GBphysics from the headers
Qwen3.5-9B5.950.9120.572, untied (excluded)4.466physics
Qwen3.5-27B16.050.9210.715, untied (excluded)14.42physics
Qwen3.5-35B-A3B20.390.8930.286, untied18.12 at 8/2561.659physics
Llama-3.2-1B0.6950.148, tied (counted)0.695physics

Prediction Against Measurement

With the achieved bandwidths from the GPU track, the ceilings and the lab's measured decode rates line up like this on office:

ModelCeiling (638 GB/s ÷ bytes)Measured (office)Measured ÷ ceilingEvidence
Qwen3.5-0.8B1,505 tok/s33822%measured 2026-09-15
Qwen3.5-2B60225743%measured
Qwen3.5-4B26914855%measured
Qwen3.5-9B1439567%measured
Qwen3.5-27B4432.674%measured
Qwen3.5-35B-A3B3858923%measured

The ceiling is never exceeded — which is the first check on any decode number you are shown — and the fraction of it a model reaches rises with model size: 22% for the smallest, 74% for the largest dense model. The reason is a fixed cost per token that does not scale with bytes: launching each layer's kernels, sampling, the Python loop. The lab track fits it directly — on office about 1.7 milliseconds per token plus bytes ÷ 497 GB/s — and 1.7 milliseconds is most of a 0.8B token and a tenth of a 27B token. The mixture-of-experts row is the exception the experts lesson explains: its bytes are small but its kernel count is large.

So the honest form of the formula for a real machine is seconds per token = overhead + bytes per token ÷ effective bandwidth, where both constants are measured per machine and per runtime. The ceiling is the limit as the overhead goes to zero and the effective bandwidth goes to the streaming figure. Nothing you can download changes the first term; only bandwidth or fewer bytes change the second.

Code

bytes_per_token.py — the accounting, from the safetensors headers·python
#!/usr/bin/env python3
"""Bytes a token must read during decode, from the checkpoint's own headers.
Rules: skip the vision tower; skip the embedding table unless tied to the
head; count routed experts at k/E; count everything else once."""
import json, struct, sys
from pathlib import Path
from huggingface_hub import snapshot_download

repo = sys.argv[1] if len(sys.argv) > 1 else "mlx-community/Qwen3.5-9B-4bit"
path = Path(snapshot_download(repo, local_files_only=True))
cfg = json.loads((path / "config.json").read_text()); tc = cfg.get("text_config", cfg)
E = tc.get("num_experts") or tc.get("num_local_experts") or 0
k = tc.get("num_experts_per_tok") or 0
tied = bool(cfg.get("tie_word_embeddings", tc.get("tie_word_embeddings", False)))

total = vision = embed = expert = 0
for f in sorted(path.glob("*.safetensors")):
    with f.open("rb") as fh:
        header = json.loads(fh.read(struct.unpack("<Q", fh.read(8))[0]))
    for name, meta in header.items():
        if name == "__metadata__":
            continue
        size = meta["data_offsets"][1] - meta["data_offsets"][0]
        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

per_token = total - vision - (0 if tied else embed)
if E:
    per_token -= expert * (1 - k / E)
print(f"{repo}: disk {total/1e9:.3f} GB, vision {vision/1e9:.3f}, embed {embed/1e9:.3f} ({'tied' if tied else 'untied'}), "
      f"experts {expert/1e9:.3f} at {k}/{E} -> {per_token/1e9:.3f} GB per token")
for bw in (638, 391, 740, 97):
    print(f"   ceiling at {bw} GB/s: {bw*1e9/per_token:7.1f} tok/s")

External links

Exercise

Run bytes_per_token.py on a checkpoint you have downloaded (any mlx-community 4-bit model) and write the ceiling for your Mac's achieved bandwidth on your card BEFORE running the model. Then decode 200 tokens with mlx-lm and record the measured rate. Compute measured ÷ ceiling and compare it with the office table: does your fraction sit where the model's size predicts?
Hint
Small model, low fraction; large model, high fraction. If your fraction is far above the office row for a similar size, your runtime has less per-token overhead than mlx-lm 0.31 on an Ultra — which is possible, and worth writing down with the version. If it is above 100%, your bytes per token are wrong.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.