~/satyajit

Edge0 streams a 35B MoE off SSD. I counted the bytes it can't stream.

mdjsonmcp

2026-09-18 · 22 min · edge-inference · mixture-of-experts · quantization · inference-optimization · model-architecture · explainer

On 8 September 2026 a team at AutoArk pushed two checkpoints and an inference framework to Hugging Face under Apache-2.0, and on the 16th the paper behind them landed as arXiv:2609.18063The Other Half of the Memory Wall: Serving 35B MoEs from SSD with Trained Routing Prediction, by Yu Lin, Yiming Wang, Runyuan Cai, Hanze Liu and Xiaodong Zeng. The pitch is one sentence: a 35B sparse MoE that decodes at interactive speed while holding under 3 GiB of memory, because the expert weights never enter RAM at all.

That is the kind of claim this site exists to check, because it is checkable. A quantized checkpoint is a file. Its tensor shapes are in a header. Whether the thing that ships matches the thing the announcement describes is not a matter of opinion.

So I cloned Edge0-AI/Edge0, read every safetensors header in both model repos by HTTP range request, and rebuilt the memory budget from the bytes. The core claim holds up better than most: the memory arithmetic is honest, the mechanism is real, and the paper is noticeably more careful than the README that links to it. The problem is the release publishes four sets of numbers — a README, a model card, a paper, and constants pinned in the source — and they do not agree with each other.

Edge0/Edge0-35B-A3B-preview@e21098e · snapshot 2026-09-18
parameters
34.66B
repo size
19.73 GB
architecture
Qwen3_5MoeForConditionalGeneration
task
text-generation
library
mlx
license
apache-2.0
safetensors
6 shards
largest file
5.37 GB
files
20
downloads
52.5K
likes
3.4K
parameters by dtype
BF161.2MU3234.66B
moeedge-inferenceprerouterlorassd-offload

repo last modified 2026-09-17

The Hub's own accounting for that repo — 34.66B parameters, 19.73 GB of storage — is the first hint, and it agrees to the byte with what I get by walking the tensor headers myself: 34,660,610,688 parameters, 19,738,195,814 bytes in the repository. Hold that against the "~23 GB" the README asks you to budget for.

The mechanism, and why it needs a predictor

A 35B MoE activates a few experts per token, which shrinks what you compute and not one byte of what you store. Edge0's move is to stop storing it: expert weights stay on SSD as per-layer stacked int4 safetensors, get mmap-ed, and fault in on demand. Peak memory becomes a function of the active set.

On its own that is too slow to use, for a structural reason. At decode step t, layer N+1's expert selection depends on layer N's output — which does not exist yet. So the read cannot start until the router has run, and the engine stalls on disk latency once per layer per token. Forty layers, forty stalls.

Edge0's answer is a trained prerouter: a small per-layer head that predicts layer N+1's routing from layer N's state at the previous token, so the reads issue early and overlap compute. The part that matters, and that distinguishes this from ordinary pre-gating, is that the prediction is then consumed as the routing — the staged expert set and the routed set are identical by construction, so nothing is dropped and no fallback load is needed. The approximation is paid once, in training, by a recovery LoRA distilled from the fp16 teacher under the student's routing path.

System diagram of Edge0 in four columns. Left: an SSD storage tier holding int4 experts, labelled 19.5 GB, as stacked safetensors. Second: a streaming expert pool with OS page cache feeding an LRU cache feeding staged slots. Third: a frozen int4 base of decoder layers L0 to L39 with attention, norm and MoE blocks, a red prerouter head at layer N that predicts N+1 at token t+1 and writes into the staged slots, and a parallel red LoRA A-then-B branch summed into the residual. Right: sampler and output token. Along the bottom, a decode timeline shows SSD read bars for predicted experts starting before and overlapping the MoE compute bars, for token t and token t+1.
The whole system in one picture: experts live on SSD, the prerouter head at layer N predicts layer N+1's routing for the next token so the read overlaps this token's compute, and an unmerged LoRA runs as a parallel delta on the frozen int4 base. Note the storage tier's own label — 19.5 GB (paper, Figure 1).

The head itself is three linear layers, and its input is the tell:

# src/edge0/prerouter/heads.py
class PrerouterHead(nn.Module):
    def __init__(self, hidden, num_experts, prerouter_hidden, dtype=core.float16):
        f_in = hidden + 2 * num_experts
        self.fc1 = nn.Linear(f_in, prerouter_hidden, bias=False)
        self.fc2 = nn.Linear(prerouter_hidden, num_experts, bias=False)
        self.linear_init = nn.Linear(f_in, num_experts, bias=False)
 
    def __call__(self, h, executed_oh, prev_oh):
        feats = core.concatenate([h, executed_oh, prev_oh], axis=-1)
        return self.linear_init(feats) + self.fc2(gelu_erf(self.fc1(feats)))

f_in = hidden + 2 * num_experts — the hidden state, plus one-hot masks of this token's executed experts and the previous token's. It is a Markov predictor over routing history, not a re-derivation of the router. For the 35B tier that is 2048 + 2 × 256 = 2560, and the shipped prerouter_edge0_35b.safetensors has fc1.weight at exactly [512, 2560]. For the 8B tier, 1536 + 2 × 128 = 1792, and its fc1.weight is [512, 1792]. The code and the weights agree.

Counting the bytes without downloading 20 GB

You do not need the weights to audit a checkpoint. A safetensors file starts with a little-endian u64 header length, then that many bytes of JSON naming every tensor's dtype and shape. Two range requests per file gets you the whole inventory:

import json, struct, subprocess
 
def curl(url, rng):
    return subprocess.run(["curl", "-sSL", "-r", rng, url], capture_output=True).stdout
 
def header(url):
    n = struct.unpack("<Q", curl(url, "0-7"))[0]
    return json.loads(curl(url, f"8-{8 + n - 1}"))

For an MLX affine-quantized tensor the packed weight is u32, so the logical width has to be recovered from the .scales group count — in = scales.shape[-1] × group_size — and then checked against the packed width, in × bits / 32. All 512 quantized tensors in the 35B checkpoint pass that assertion, which is itself a useful result: the quantization block in config.json describes the file that actually shipped, including the 80 router gates it promotes to 8-bit.

That gives 1,757 tensors across four shards, resolved from about a megabyte of headers:

receiptscaptured 2026-09-18

Edge0's README says the edge0-35b checkpoint is "~23 GB". It is not. Every tensor in Edge0/Edge0-35B-A3B-preview, read out of the four safetensors JSON headers by HTTP range request, totals 19,508,787,456 bytes — 19.51 GB, or 18.17 GiB. The paper says 19.5 GB and its own Figure 1 prints "int4 experts (19.5 GB)", so the README is the one number out of step with both the artifact and the paper. The same headers give the parameter count (34,660,610,688) and the memory floor: 1,389,394,176 bytes of non-expert weights that have to be resident no matter how aggressively the experts stream.

componentparametersbytes on diskMiBresident at decode?
routed experts (40 layers x 256)32,212,254,72018,119,393,28017280.00no — streamed from SSD
attention (40 layers, 30 linear + 10 full)1,284,188,800723,784,960690.26yes
lm_head (untied)508,559,360286,064,640272.81yes
embed_tokens508,559,360286,064,640272.81yes
shared expert (40 layers)125,911,04070,865,92067.58yes
router gates (40 layers, 8-bit)20,971,52022,282,24021.25yes
norms (bf16)165,888331,7760.32yes
= base checkpoint total34,660,610,68819,508,787,45618605.03
prerouter heads (33 x 2,097,152, fp16)69,206,016138,412,032132.00yes
recovery LoRA (r=16, alpha=32, fp16)21,166,08042,332,16040.37yes
= non-expert resident floor2,538,728,0641,570,138,3681497.40yes
+ one K=4 decode step of experts (4 x 40 x 1,769,472 B)503,316,480283,115,520270.00transient
= floor with every layer's K=4 set held at once3,042,044,5441,853,253,8881767.40

Bytes are on-disk safetensors bytes, so a 4-bit affine group-64 tensor costs params/2 for the packed weights plus params/16 for the bf16 scales and biases (0.5625 bytes/param); the 8-bit router gates cost 1.0625. "Parameters" is the unpacked logical count. The vision tower that config.json declares (Qwen3_5MoeForConditionalGeneration, vision_config.depth 27, image_token_id, video_token_id) contributes zero of these bytes: all 1,757 tensors sit under the language_model. prefix and not one visual.* tensor ships. The config's mtp_num_hidden_layers: 1 likewise has no tensors behind it.

method For each file: curl -sSL -X HEAD and read Content-Length after the CDN redirect (not the git-lfs pointer), cross-checked against the Hub API blob sizes. For each tensor: read the safetensors u64 header length from bytes 0-7 and the JSON header from bytes 8..n with an HTTP range request, then recover the unpacked width from the .scales group count (in = scales.shape[-1] x group_size) and verify it against the packed u32 width (in x bits / 32). No weights were downloaded — 1,757 tensors across four shards, resolved from about 1 MB of headers.
data /articles/edge0-streaming-moe/data/checkpoint-bytes.json (13 rows, 3.9 KB)
measured memory floor vs. published peaksafetensors headers, 18 Sept 2026
routed width K

Edge0/Edge0-35B-A3B-preview · config.json says num_experts_per_tok: 8 · edge0 ships top_k=4 · 1.69 MiB per expert, 40 MoE layers

1.7 GiB
iPhone 16 Pro demo video
2.9 GiB
README + paper, M4 Pro
3.32 GiB
peak_active_mem_mb=3400
fixed floor
1.29 GiB
non-expert weights
+ adapters
1.46 GiB
prerouter + LoRA
+ experts K=4
1.73 GiB
every layer's set at once
expert share
15.28%
of the measured total
non-expert weights1325.03 MiBadapters172.37 MiBexperts, K=4270 MiB

Only the right-hand segment moves with K. The 35B tier’s floor is 1.46 GiB of weights that have to be resident for the whole forward pass before a single expert is read, and 172 MiB of that is the two adapter files the pipeline needs to work at all — the prerouter alone is 132 MiB, about 4.4% of the 2.9 GiB budget it is there to defend. Hold every layer’s K=4 set simultaneously and you get 1.73 GiB; hold only the four-layer staged window, 1.49. The iPhone demo’s 1.7 GiB peak sits inside that window. The Mac mini’s 2.9 GiB is the same floor plus about 1.2 GiB of expert cache, KV and allocator slack. The 8B tier is the same shape one order down: a 0.61 GiB floor, 0.84 GiB with every layer’s K=8 set held at once, against published peaks of 1.0, 1.37, 1.4 and 1.5 GiB.

Read the widget and the claim resolves cleanly in Edge0's favour. "Peak memory is bounded by the active set, not the parameter count" is true — 92.9% of the parameters and 92.9% of the bytes are routed experts, and those genuinely never need to be resident. But the floor underneath is fixed: 1.29 GiB of attention, embeddings, the untied lm_head, the shared expert and the router gates, plus 172 MiB of adapters, equals 1.46 GiB before a single expert is read. Add one decode step of experts at K=4 across all forty layers and you get 1.73 GiB; add only the four-layer staged window the config actually keeps (hot_window=4) and you get 1.49 GiB.

Which is a satisfying place to land, because Edge0's own iPhone demo prints 1.7 GiB / 1.8 GB as its session peak. My floor, computed from tensor headers alone with no access to the running process, brackets the number their phone reports. The Mac mini's 2.9 GiB is the same floor plus roughly 1.2 GiB of expert cache, KV growth and MLX allocator slack.

One measurement worth singling out: the 8B tier's per-expert cost comes out of my headers at 1,327,104 bytes, or 1.2656 MiB. The framework's own source says "every cached bundle is 1.27 MiB of RAM" in the prod_k8 docstring in src/edge0/streaming/options.py. Independent derivation, same number. That is what a codebase looks like when someone actually measured it.

The one number that is simply wrong

The README says the 4-bit checkpoints are "~23 GB (edge0-35b) and ~4.2 GB (edge0-8b)".

The 8B figure is fine if you read it as GiB: measured 4,508,697,328 bytes, which is 4.51 GB or 4.20 GiB.

The 35B figure is not fine in either unit. The four weight shards total 19,508,787,456 bytes — 19.51 GB, 18.17 GiB — confirmed twice, once from the Hub API's blob sizes and once from Content-Length on each file after the CDN redirect. The entire repository, adapters and tokenizer and a 20 MB demo video included, is 19,738,195,814 bytes. There is no reading of "~23 GB" that gets within 3.4 GB of the artifact.

The interesting part is who gets it right. The paper's abstract opens with "a 35B-class model is 19.5 GB at 4-bit". The paper's Figure 1 prints int4 experts (19.5 GB) on the storage tier. The framework's own staged_k4 docstring says "19.7 GB checkpoint", which is the whole-directory figure. Three sources, all correct, all in the same release. The README is the only place the number is inflated, and it is the first thing anyone reads.

"A3B" describes a model that isn't the one serving

The repo is called Edge0-35B-A3B-preview. A3B means roughly three billion active parameters per token, inherited from the Qwen3.6-35B-A3B base.

The base's own config.json — shipped inside the Edge0 checkpoint, unmodified — says num_experts_per_tok: 8. Edge0 does not serve it that way:

# src/edge0/models/edge0_35b/__init__.py
moe_spec=MoESpec(
    num_experts=256, top_k=4, intermediate_size=512,
    router=RouterKind.SOFTMAX_TOPK, norm_topk_prob=True,
    shared_experts=1,
    quant=QuantSpec(bits=4, group_size=64, mode="affine"),
    ...
),
options=LayerOptions.staged_k4(),
prerouter_top_k=4,

Half the native routed width. From the tensor shapes, the active set is 2,443,115,136 parameters at K=4 against 2,946,431,616 at K=8 — the "3B" only appears at a width the release does not run. It is an inherited name rather than a fresh claim, and the model card is honest about it further down the page (Experts / active per token — 256 / 4 (K=4)), but the paper's own Table 1 puts Active params / token ≈3B and Routing width K — 4 in the same column, and those two cells are 0.5B apart.

For contrast, the 8B tier's A1B checks out: measured 1,170,908,064 active at its native K=8. It only reaches 1.2B because tie_word_embeddings is false and the 241,434,624-parameter lm_head really is a separate tensor in the file — had the embeddings been tied, the honest number would have been 0.93B. I checked, because assuming a tied embedding is exactly how active-parameter counts go wrong.

The second-order consequence matters more than the naming. Running at K=4 halves the routed compute, and the quality table compares that against an fp16 base running at K=8. More on which direction that cuts below.

Five decode rates, one machine

every decode rate Edge0 publishes for its own tiersread 18 Sept 2026, commit 5b65e6f

edge0-35b

1213141516171819202122decode (tok/s)13.0target_tok_s (code)14.9–17.7README + HF card table15HF card headline~16demo video overlay20.4arXiv Table 1

edge0-8b

22242628303234decode (tok/s)23.9–25.3README + HF card table25.0options.py docstring28.0arXiv Table 133.0target_tok_s (code)
README / model cardarXiv:2609.18063v2pinned in the sourcedemo video, on-device

Five figures for the 35B tier spanning 13.0 to 20.4 tok/s, a 57% spread, and two of them — 14.9–17.7 and 20.4 — name the same Mac mini M4 Pro, 24 GB. The 8B tier is the same shape: the README and the framework’s own docstring agree at ~25 tok/s, the paper says 28.0, and the acceptance constant the CLI prints when you run edge0 models says 33.0. None of the four sources says which of the others it supersedes.

receiptscaptured 2026-09-18

Edge0 publishes five different decode rates and four different peak-memory figures for the same 35B tier, and two of the decode figures claim the same machine. Nothing in the release says which supersedes which. The spread on decode is 13.0 to 20.4 tok/s — 57% — and the README's headline prerouter gain ("up to +59%") appears nowhere in the paper it links to.

quantitypublished valuemachinewhere it is published
edge0-35b decode13.0 tok/sunstatedsrc/edge0/models/edge0_35b/__init__.py — target_tok_s=13.0
edge0-35b decode14.9–17.7 tok/sMac mini M4 Pro, 24 GBREADME.md Benchmark table; HF card Performance table
edge0-35b decode15 tok/sunstatedHF card headline + "Fast enough for interactive use"
edge0-35b decode~16 tok/siPhone 16 Pro, 8 GB20260910-105854.mp4 overlay, 0:12 (HF repo)
edge0-35b decode20.4 tok/sMac mini M4 Pro, 24 GBarXiv 2609.18063v2 Table 1; abstract says "≈20 tok/s"
edge0-35b peak memory1.7 GiB / 1.8 GBiPhone 16 Pro, 8 GB20260910-105854.mp4 overlay + caption
edge0-35b peak memory2.9 GiBMac mini M4 Pro, 24 GBREADME.md; HF card; arXiv Table 1 — all agree
edge0-35b peak memory3400 MB (3.32 GiB)unstatedsrc/edge0/models/edge0_35b/__init__.py — peak_active_mem_mb=3400.0
edge0-8b decode23.9–25.3 tok/sMac mini M4 Pro, 24 GBREADME.md Benchmark table; HF card
edge0-8b decode25.0 tok/s (40.02 ms/step)Mac mini M4 Pro, 24 GBsrc/edge0/streaming/options.py — prod_k8 docstring
edge0-8b decode28.0 tok/sMac mini M4 Pro, 24 GBarXiv 2609.18063v2 Table 1
edge0-8b decode33.0 tok/sunstatedsrc/edge0/models/edge0_8b/__init__.py — target_tok_s=33.0
edge0-8b peak memory1.0 GiBMac mini M4 Pro, 24 GBREADME.md; HF card headline "1 GiB active memory"
edge0-8b peak memory1.4 GiBMac mini M4 Pro, 24 GBsrc/edge0/streaming/options.py — prod_k8 docstring
edge0-8b peak memory1400 MB (1.37 GiB)unstatedsrc/edge0/models/edge0_8b/__init__.py — peak_active_mem_mb=1400.0
edge0-8b peak memory1.5 GiBMac mini M4 Pro, 24 GBarXiv 2609.18063v2 Table 1
prerouter decode gain+2.5% (19.9 → 20.4 tok/s)Mac mini M4 Pro, 24 GBarXiv 2609.18063v2, Table 1 caption
prerouter decode gain~5%Mac mini M4 Pro (8B tier)src/edge0/streaming/options.py — prod_k8 docstring
prerouter decode gainup to +59%unstatedREADME.md and HF card — not reproduced in the paper
prerouter decode gain+80 / +82 / +84% (K=2/4/8)MacBook M2, 16 GB (checkpoint does not fit)arXiv 2609.18063v2 Table 3 and Figure 3a

The 35B tier's routed width is the other thing worth reading twice. The checkpoint's own config.json sets num_experts_per_tok: 8; edge0 serves it with MoESpec(top_k=4). Measured from the tensor headers, the active set is 2,443,115,136 parameters at K=4 against 2,946,431,616 at K=8 — so the "A3B" in Edge0-35B-A3B-preview describes the base model's native width, not the one that ships. The 8B tier's "A1B" does check out: 1,170,908,064 active at its native K=8, and it only reaches 1.2B because tie_word_embeddings is false and the 241,434,624-parameter lm_head really is a separate tensor in the file.

method Read out of the four places Edge0 publishes numbers, all at the same commit (5b65e6f, 18 Sept 2026): the GitHub README, the Hugging Face model card, arXiv:2609.18063v2 (paper/main.pdf in the repo, identical to the arXiv listing), and the pinned acceptance constants in src/edge0/models/edge0_35b/__init__.py. The iPhone figures are read off the on-screen overlay of 20260910-105854.mp4, the 89-second screen recording committed to the Hugging Face repo, at 0:12.
data /articles/edge0-streaming-moe/data/published-numbers.json (20 rows, 5.0 KB)

Here is the thing I could not resolve. For edge0-35b, the release publishes:

SourceDecodeMachine
target_tok_s in models/edge0_35b/__init__.py13.0 tok/sunstated
README + model-card Performance table14.9–17.7 tok/sMac mini M4 Pro, 24 GB
Model-card headline15 tok/sunstated
Demo video overlay, 0:12~16 tok/siPhone 16 Pro, 8 GB
arXiv Table 1 (and "≈20 tok/s" in the abstract)20.4 tok/sMac mini M4 Pro, 24 GB

Rows 2 and 5 name the same machine and the same harness shape and differ by 15–37%. The paper's Table 1 caption describes a stricter protocol — "each arm in its own process, warm, arm order rotated, medians over 3 rounds" — against the README's "2 runs per tier", so the paper's number is plausibly the better one. Nothing anywhere says so. The two tables also disagree on the 8B tier's warm prefill (1428 against 1102 tok/s) and its peak memory (1.0 against 1.5 GiB), while agreeing exactly on the 35B tier's prefill and peak. That is not a version-drift pattern; it is two measurement campaigns published side by side.

The 8B memory figure is the one I would fix first. The README and the model card say 1.0 GiB. The paper says 1.5 GiB. The framework's own prod_k8 docstring says peak goes "1.4 → 2.6 GiB" when you grow the LRU, i.e. 1.4 GiB as shipped. And peak_active_mem_mb=1400.0 is pinned in the source as the acceptance yardstick. Three of the four cluster at 1.4–1.5 GiB; the one in the headline is 30% below them.

What the prerouter actually buys

The README and the model card both headline the prerouter as worth "up to +59% decode throughput". I went looking for that number and could not find it.

It is not in the paper. The paper's contributions list says "+80 to +84% decode". Table 3 and Figure 3a give +80%, +82% and +84% for K=2, 4 and 8. The appendix prices a different A/B at +34%, and another at 6.8 → 12.5 tok/s, and explicitly warns those "are not comparable with Table 3". None of them is 59%, and neither 59 nor any near neighbour appears anywhere in the PDF, the docs, or the source. It appears in exactly two files: README.md and README_zh.md.

Two grouped bar charts comparing on-demand streaming in grey against the prerouter in red. Left panel, decode throughput in tokens per second: at K equals 2, 4.8 versus 8.6, marked plus 80 percent; at K equals 4, 3.5 versus 6.4, plus 82 percent; at K equals 8, 1.8 versus 3.3, plus 84 percent. Right panel, read per expert load in MiB: at K equals 2, 0.31 at 20 percent cold versus 1.53 at 98 percent cold; at K equals 4, 0.27 at 17 percent cold versus 1.32 at 84 percent cold; at K equals 8, 0.32 at 20 percent cold versus 1.40 at 90 percent cold.
The prerouter's headline win, and the condition attached to it: a 16 GB MacBook M2 serving an 18.4 GiB checkpoint, where every step faults experts back from SSD. Note the absolute rates on the left axis — 1.8 to 8.6 tok/s (paper, Figure 3).

Look at the left axis before the percentages. The machine in Figure 3 decodes at 1.8 to 8.6 tok/s — this is a 16 GB MacBook M2 with an 18.4 GiB checkpoint, a configuration in which the model cannot fit and every step faults from disk. That is the honest setting for the mechanism and the paper says so plainly. It is also not the machine anyone is quoting when they say Edge0 does 15 tok/s.

On the machine the README does benchmark, the same paper gives the A/B in one line of Table 1's caption: "Disabling the prerouter on the 35B tier measures 19.9 tok/s." Against 20.4. That is +2.5%. The 8B tier's own source docstring says the prerouter "routes ~5% faster than gate routing with the same loading path" on the M4 Pro.

one decode step, MacBook M2 16 GB, 18.4 GiB checkpointarXiv:2609.18063v2 §5.3, Tables 3–4
routed width K
shipped 35B tier runs K=4
on-demand streaming3.5 tok/s · 285.7 ms/step · 244 ms blocked on expert loads
prerouter (all layers)6.4 tok/s · 156.3 ms/step · 101.9 ms blocked on expert loads

solid = step wall time (1000 / tok/s) · hatched = time the main thread spent blocked on expert loads, summed over layers. At K=8 the on-demand sum (575.0 ms) exceeds the step it belongs to (559.1 ms) because different layers’ loads overlap — the bar clamps it to the step.

armloads/stepMiB/stepMiB/loadcoldms/load
on-demand streaming189.450.90.2717%1.29
prerouter (all layers)44.758.91.3284%2.28

machine that cannot hold the model

+82% at K=4

3.56.4 tok/s. MacBook M2, 16 GB, no artificial memory limit.

machine in the README’s benchmark table

+2.5%

19.9 → 20.4 tok/s, Mac mini M4 Pro 24 GB, from the paper’s own Table 1 caption. The 8B tier’s code docstring says ~5% on the same box.

The two arms read within a few percent of the same bytes at K=2 and K=8 — 29.5 against 30.4, and 125.1 against 126.9 MiB per step. At K=4, the width that actually ships, the prerouter arm reads 16% more (58.9 against 50.9 MiB). So nothing is saved. What changes is when the bytes are read: 44.7 loads a step of 1.32 MiB each instead of 189.4 loads of 0.27 MiB, at 84% cold instead of 17%. Moving a read off the critical path can hide it; it cannot delete it. Which is exactly why the gain collapses to +2.5% on a box with enough RAM to keep the pages warm — there is no exposed cold-read time left to hide.

So the honest characterisation is: the prerouter is not a speedup, it is a swap-avoidance device, and it pays in proportion to how badly the machine cannot hold the model. Table 4 makes that mechanical rather than rhetorical. Both arms move within a few percent of the same bytes per step at K=2 and K=8; at K=4, the width that actually ships, the prerouter arm reads 16% more (58.9 against 50.9 MiB). Nothing is saved. What changes is that 189 small warm reads become 45 large cold ones, which is a better shape for an SSD and, more importantly, happens off the critical path. When the page cache can hold the working set there is no exposed cold-read time to hide, and the gain goes to zero.

That framing also puts a price on the mechanism. The prerouter costs 138,412,032 bytes of always-resident fp16 — 69,206,016 parameters, 33 heads of exactly 2,097,152 parameters (4 MiB of fp16) each — which is 4.4% of the 2.9 GiB budget it exists to defend, for +2.5% on the flagship machine. That is still a good trade, because the 2.9 GiB budget is the product and the +80% is what makes a 16 GB laptop viable at all. It is just not the trade "up to +59% decode throughput" describes.

The iPhone

The announcement thread said "a 35B language model running on an iPhone using only 1–2.5 GB of peak memory". The repo's Requirements section says the MLX backend "runs on macOS with Apple Silicon (M1/M2/M3/M4)". pyproject.toml pins mlx-metal; platform_system == 'Darwin'. There is no iOS target, no Swift, no .xcodeproj, and every measurement in the paper is a Mac mini M4 Pro or a MacBook M2. The model card's own phrasing is the careful one: "phone-class memory", meaning an amount of memory a phone has.

I expected that to be the end of it. It isn't — the evidence is sitting in the model repo, as an 89-second screen recording nobody links from the README:

Frame from Edge0's demo screen recording. A large caption on the left reads 35B model running on iPhone, 1.8GB Memory Peak, iPhone 16 Pro 8 GB, highest peak in this session, Edge0-35b-a3b, 35B total 3B active. Centre: an iPhone showing a chat app labelled Edge0-35B answering the question what is on-device AI, with a status pill under the answer reading approximately 88 tok, approximately 16 tok per second, 1.7G memory peak. Bottom right: no speed-up, 1x speed.
The only published on-device measurement of Edge0, and it is a real one: iPhone 16 Pro, 8 GB, ~16 tok/s, 1.7 GiB peak, labelled 1x speed with no speed-up (Edge0-AI, 20260910-105854.mp4 in Edge0/Edge0-35B-A3B-preview, frame at 0:12).

So the iPhone claim is substantiated, by the vendor's own recording, at 1× speed with the no-speed-up label volunteered rather than extracted. ~16 tok/s and 1.7 GiB on an iPhone 16 Pro — and 1.7 GiB is, to within a couple of percent, the floor I derived from the tensor headers. That is the best result in this piece: two completely independent methods, one a byte count and one a running phone, landing on the same number.

What remains unsupported is narrower. There is no per-device table: one phone, one session, ~88 tokens, no seed, no prompt disclosed, no second run. "1–2.5 GB" matches neither endpoint of anything published — the 8B is 1.0 GiB, the 35B on the phone is 1.7, on the Mac mini 2.9. And the one A18-class data point the repository does contain is a bug report: issue #8, garbled mixed-language output on Apple A18 / A18 Pro, traced in pyproject.toml to MLX ≤ 0.30.4 mis-gating the NAX matmul kernels — "silently wrong numbers", fixed upstream in 0.30.5, which is why the dependency is pinned to an exact version. Nothing about the phone path is loose, but one recording is one recording.

The quality table cuts the right way

| Benchmark    | edge0-35b (int4) | Qwen3.6-35B-A3B (fp16) | edge0-8b (int4) | Ling 3.0 tiny (fp16) |
| AIME 2026    |             86.6 |                   92.7 |            63.3 |                 73.3 |
| HumanEval    |             90.9 |                   95.1 |            91.5 |                 92.7 |
| GPQA-Diamond |             79.8 |                   81.8 |            70.7 |                 71.2 |
| MMLU-Pro     |             81.0 |                   84.6 |            70.1 |                 65.8 |
| IFBench      |             57.9 |                   61.7 |            53.9 |                 60.6 |
| Average      |             79.2 |                   83.2 |            69.9 |                 72.7 |

The arithmetic checks: 396.2/5 = 79.24 and 415.9/5 = 83.18, a 3.94-point mean gap; 349.5/5 = 69.9 against 363.6/5 = 72.72, a 2.82-point gap. Both round as claimed.

This comparison is asymmetric — int4 plus a LoRA plus predicted routing at K=4, against fp16 with the native gate at K=8 — and it is worth saying which way that cuts. It favours the baseline. The fp16 arm gets more precision and twice the routed compute. So the 3.9 points is the cost of the entire deployment pipeline, not of quantization alone, which is the right thing to measure and the conservative direction to measure it in. I have no complaint about the table's construction.

I have two about its reporting. First, the 8B's "2.8 points on average" is the mean of a set spanning 14.3 points: −10.0 on AIME, −6.7 on IFBench, −1.2, −0.5, and +4.3 on MMLU-Pro. A distilled adapter beating its own teacher by 4.3 points on one benchmark while losing 10.0 on another is not a small residual drift, and "quality-matched" does a lot of work over that spread. The paper's Limitations section does say the loss concentrates in long-chain reasoning, and names both numbers. The README and the model card lead with the average.

Second, the denominator is never stated. There are no sample counts, no seeds, no avg@k, no temperature — "OpenCompass on a compute server under identical settings" is the whole protocol description. And the arithmetic gives the denominator away: 63.3 and 73.3 are exactly 19/30 and 22/30, which is what you get scoring the 30-problem AIME 2026 set (AIME I + II, fifteen each) once. If that is right, the 8B tier's headline 10.0-point AIME gap is three problems, and the 92.7 cell — which is not a multiple of 1/30 — was produced some other way. Nothing about the conclusion is necessarily wrong. But a 10.0 and a 3 feel very different, and only one of them is printed.

Things the config says that the checkpoint does not have

Small stuff, found by walking the tensor list, and none of it affects edge0 itself because edge0 never reads these paths. It will bite anything that loads the directory the way the config describes it.

What I take away

The engineering is good and the paper is careful. Same-session rotated A/B, medians over repeated runs, an explicit statement that single-shot benchmarks on this hardware carry ±40% run-to-run spread and up to 2.3× across sessions — and then a refusal to use any cross-session number as evidence. A limitations section that names the 10.0-point AIME gap. An appendix that says two of its own measurements "are not comparable" with the main table. A demo video labelled 1× speed. This is a group that measures things.

The release around the paper is where it falls down, and in a specific way: the marketing surface is not a compression of the technical one, it is a fourth independent set of numbers. "~23 GB" where the artifact is 19.5. "Up to +59%" where the paper says +2.5% here and +84% there. 1.0 GiB where the source, the acceptance constant and the paper all say 1.4–1.5. Five decode rates for one tier. Every one of those is individually small. Together they mean a reader cannot cite Edge0 without first deciding which Edge0 to cite — and the fix costs nothing, because in every single case the correct number already exists inside the same repository.

If you want to use it: it is Apple Silicon only today, mlx==0.30.6 exactly (0.30.4 is silently wrong on A18, 0.31.2 breaks the streaming pool's threads), one request at a time, FIFO. edge0 serve edge0-35b gives you an OpenAI-compatible endpoint backed by 19.5 GB of disk and about 3 GiB of RAM. On that hardware, that is a thing nothing else does.

What would change my mind

5 claims above, and what would falsify each

  1. The 35B checkpoint is 19.51 GB, not ~23 GB.

    Show me a released revision of Edge0/Edge0-35B-A3B-preview whose weight shards sum to ~23 GB. The Hub keeps history; if an earlier commit was that size, the README is stale rather than wrong and I will say so. As of the version I measured, the four shards are 19,508,787,456 bytes and the whole repo is 19,738,195,814.

  2. The README's 'up to +59%' prerouter gain is unsourced.

    A benchmark — in the repo, the paper, an issue, a blog post, anywhere — that produces 59% on some stated machine at some stated K. I searched the PDF, both READMEs, all eight docs pages and the full source tree. If it exists and I missed it, this claim is simply wrong, and the honest version becomes "the README quotes a number whose provenance is not linked".

  3. On a machine that can hold the model, the prerouter is worth about +2.5%.

    A paired A/B on an M4 Pro-class box showing a materially larger gap than 19.9 against 20.4 tok/s. I am taking the paper's own Table 1 caption at face value on hardware I do not own; I have not run this. A same-session sweep on any 24 GB Apple Silicon machine would settle it in an afternoon.

  4. The memory floor is 1.46 GiB and the 2.9 GiB peak is honest.

    An MLX allocator trace showing the resident set dipping meaningfully below 1.46 GiB during decode, which would mean some of what I counted as always-resident is itself being streamed or recomputed. My figure is derived from tensor headers, not from a running process, and it assumes the non-expert weights stay resident for the whole forward pass.

  5. The 8B AIME gap of 10.0 points is three problems out of thirty.

    An OpenCompass config for this run showing a different denominator or an avg@k over multiple samples. 63.3 and 73.3 being exactly 19/30 and 22/30 is suggestive, not conclusive — and 92.7 is not a multiple of 1/30, so at least one cell in that row was scored differently.

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "Edge0 streams a 35B MoE off SSD. I counted the bytes it can't stream.", ai.thesatyajit.com, September 2026.

bibtex
@misc{ghana2026edge0streamingmoe,
  author = {Satyajit Ghana},
  title  = {Edge0 streams a 35B MoE off SSD. I counted the bytes it can't stream.},
  url    = {https://ai.thesatyajit.com/articles/edge0-streaming-moe},
  year   = {2026}
}
share