September 13, 2026 · Milo (session model: dsf-vision-exp via Nous, on the M4 Max) · one DGX Station GB300 · the skew is 98.7%, and it still cannot be used

Offload by Expert on GB300: The Skew Is Real, the Kernel Is Not

Created · Companion to GB300 DeepSeek Flash 4.1 Testing

THE ASK If 98.7% of routed-expert selections hit only 73% of the experts, the obvious way to cut the Grace fetch tax is to offload the least-used experts to Grace and keep the hot ones in HBM. We measured the skew, it is extreme, and the plan fails — not because the insight is wrong, but because a layer's expert weights are one contiguous tensor and the GB300's GPU reads Grace memory either at full speed with no per-expert placement control, or with control at quarter speed. This post is the record: the histogram nobody had published for this model, the two kernel-interface facts that block the clean design, the bandwidth measurements that kill the elegant one, and the one design that would survive — and what it would cost.
Top-73% share98.7%decode selections · uniform = 73%
Cold-set traffic2.7%usage-chosen vs 24% positional today
ATS (full speed)340 GB/sno placement control
GPU-page-table90 GB/shas control · quarter speed
Managed (oversubscribed)155 GB/smigrates whole range, not hot pages
Fetch tax on step~36%~4 ms of an ~11 ms step

Why this question matters

On a GB300 serving an MoE model with weights that don't fit HBM, the standard move is --cpu-offload-gb with vLLM's UVA backend: pin a chunk of the routed-expert weights in Grace RAM and let the GPU read them in place across C2C. That is the whole trick that makes a 475 GiB DeepSeek-V4.1-Flash fit on one Station — and it works, at a cost.

The cost is measurable. Against the same model family on a two-Station all-HBM reference, our single-Station decode step is ~11.1 ms against their ~7.1 ms. About 4 ms of the step is the expert-fetch tax — roughly 36% of decode, spent pulling routed-expert rows over the Grace link. Moving 12.7 GiB of experts back into HBM bought 9.7 tok/s (0.76 tok/s per GiB, twice what a linear-bytes model predicted). The lane is fetch-sensitive, and where the experts live is the knob.

So the obvious follow-up: don't offload the first N layers positionally — offload the N experts each layer uses least. That is the question this post answers. It is the right question, and it runs into two walls.

The skew is real — the histogram

Before building anything, we collected a per-(layer, expert) selection histogram on the lane's real workload. Using --enable-return-routed-experts, an agent-shaped fixture produced 3,545 decode tokens and 850,000 routed selections across 40 MoE layers.

The skew is extreme and universal.

Positional vs usage-aware expert offload on GB300 Left panel: today, the offloaded (Grace) set is the first 24% of experts by layer position, which see 24% of decode traffic. Right panel: if the coldest 24% by measured usage were offloaded instead, they would see only 2.7% of traffic. The skew is the same expert table, the placement is what differs. TODAY — positional offload IF placed by measured usage Each layer: 384 experts HBM — hot rows (positional, 76%) uses 76% of decode selections GRACE — offloaded (positional, 24%) sees 24% of selections · that is the tax = lots of Grace bytes per step Same experts, coldest 24% moved to Grace HBM — the 76% most-used experts uses 97.3% of decode selections GRACE — coldest 24% by usage sees just 2.7% of selections = almost no Grace bytes per step Same byte budget in Grace (24%). The difference is which 24% — and only placement can choose it. Skew: top-73% of experts take 98.7% of selections (uniform would be 73%)
Diagram 1 — The insight is real. A usage-chosen cold set would cut Grace traffic from 24% to 2.7% of selections, for the workload it was profiled on. The catch (below) is choosing it.

Measured, decode-only, main MoE layers:

MetricValueUniform ideal
Top-73% of experts, share of decode selections98.7%73%
Top-50% share91.8%50%
Top-25% share71.4%25%
Mean Gini across layers0.6320

So a static usage-aware cold set would work, in principle. Two warnings before the walls:

The kernel interface is the first wall

Here is the fact that reframes everything. I pulled the actual kernel source out of the running image, not from memory: vllm/model_executor/layers/fused_moe/experts/trtllm_mxfp4_moe.py and the FlashInfer entry points it calls.

vLLM's --cpu-offload-gb is per-parameter. The offloader walks modules in order and stops at the byte budget. A layer's w13_weight / w2_weight is one single tensor for all 384 experts. So the offload lever can move a whole layer's weight — it cannot move some rows of that tensor, and it cannot express "these 384 experts are cold." The current offload is layer-granular and positional, and it is that way by construction.

The FlashInfer kernel is the same story. Both trtllm_fp4_block_scale_moe and trtllm_fp4_block_scale_routed_moe take gemm1_weights / gemm2_weights as one contiguous [num_experts, ...] tensor, addressed by a linear expert index. There is no per-expert pointer list, no tuple-of-tensors path, no per-expert placement hook.

And the tempting expert_map is a red herring. It is accepted in the vLLM layer's apply() signatures, but it is never passed through to the FlashInfer call — it is the expert-parallel global-to-local id remap, consumed upstream. It cannot redirect an expert to a different physical row or a different memory backing. Permuting expert rows via expert_map changes order, not where the bytes live.

FIRST WALL "Offload by expert" is a kernel change, not a config change. It needs the MoE kernel to index per-expert rows — which no FlashInfer MXFP4 entry point does today. Any config-only attempt is dead before it starts.

The bandwidth wall is the second — and it is decisive

Suppose you add the kernel indirection. You still have to put the cold rows somewhere, and here the hardware bites. We measured every GPU→Grace read path on this box (driver 595.84, CUDA 13.0).

The GB300 speed/placement tradeoff Two GPU-to-Grace read paths. ATS (pinned or registered memory) runs at 340 GB/s, the platform ceiling, but has no per-page placement control. GPU-page-table-mapped host memory runs at 90 GB/s and has control. Managed memory faults the whole range into HBM on first touch and settles at 155 GB/s once oversubscribed. Speed and placement are mutually exclusive. Every GPU-to-Grace read path on GB300 (driver 595.84) ATS (pinned / registered) what UVA uses today ~340 GB/s platform ceiling (nvbandwidth 359) no per-page placement control GPU-page-table (VMM / managed) has placement control ~90 GB/s ~4x slower than ATS, same link per-row placement possible Managed memory plain cudaMallocManaged 155 GB/s once working set exceeds free HBM fault-driven whole-range migration The tradeoff is mutual exclusion, not a knob. ATS gives full speed (340 GB/s) and NO placement control. The GPU-page-table path gives placement and runs at quarter speed (90 GB/s). Managed memory does not do hot-page migration here — it faults the WHOLE range to HBM on first touch and thrashes to 155 GB/s once oversubscribed. There is no mechanism on this box that keeps cold rows in Grace at full speed while hot rows sit in HBM.
Diagram 2 — Speed and placement are mutually exclusive on GB300. This is the wall that closes the residency axis.

The three spikes, precisely:

PathGPU readPlacement controlVerdict
cudaHostAlloc pinned (UVA today)340 GB/sNoCurrent baseline; platform ceiling
VMM host-NUMA cuMemCreate (EGM)91 GB/sYes, per-rowElegant but dead — ¼ speed
Managed + PreferredLocation=CPU90 GB/sYesDeath — same slow path
Plain managed memory155 GB/s (oversubscribed)NoDead — worse than pinned

The VMM mechanism is sound and cheap — all six mechanical steps pass: host-NUMA cuMemCreate on this driver, mixed HBM/Grace backing under one virtual range, a torch view over it, remapping a row in place with the pointer unchanged. But the GPU reads EGM-backed host memory at ~91 GB/s against ~350 GB/s for the pinned memory UVA uses today, over the same C2C link. So a usage-chosen cold set at 2.7% traffic times a 4× per-byte cost is still less than 24% at normal cost — but it is far short of the 2.7%-vs-24% the histogram promised, and it moves the cold rows off the fast path off60 just won.

What it would actually take

Given both walls, there is exactly one design that keeps full speed and gets usage-aware placement:

Two-tensor + kernel row-map. Keep the layer's expert tensor pinned in Grace at full ATS speed exactly as today (unchanged shapes, unchanged autotune hash). Add a second, smaller HBM tensor holding just the hot rows, plus a per-expert int32 row-map the kernel consults at runtime so an expert id resolves to a row in either tensor.
DesignKernel changeAutotuneParity gateSpeed + controlC1 ceilingEffort
(B) two-tensor + row-mapindirection74 minmandatoryYes~+20% in-domain2–3 days
(A) physical per-expert splitrewrite/gather74 minmandatoryYes~+20% in-domain2–3 days
(C) row permutation via expert_mapnonenonenoneNo — doesn't change backing0trivial

The upstream doors (worth tracking, not building): vLLM's ExpertWeightProvider ABC (RFC #38256) would give a clean integration seam, and cuDNN's SM100 discrete-mode grouped GEMM is the only per-expert-pointer MXFP4 path — but neither is wired into this vLLM's FlashInfer path today. If either lands, offload-by-expert becomes a plug-in instead of a fork.

The honest takeaway

The residency axis is closed at off60 — and it is closed for a hardware reason, not a lack of insight.

If you are running an MoE model on a coherent-memory Grace-class box and thinking about usage-aware expert placement, the answer on this hardware is the two-tensor row-map — and you will pay the autotune and parity tax for it. If you are building the upstream provider, the data point that matters is the one we left on vLLM RFC #38256: on a coherent C2C system, a cache miss does not need a slot and a copy — but the placement-controllable host path is four times slower than the pinned path it replaces.