Skip to content

perf(moe): split hybrid decode routes inside the ensure kernel - #685

Merged
jason-fxz merged 3 commits into
mainfrom
perf/hybrid-route-split
Oct 14, 2026
Merged

jason-fxz merged 3 commits into
mainfrom
perf/hybrid-route-split

Conversation

@jason-fxz

@jason-fxz jason-fxz commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

After _ensure_experts_hybrid_kernel rewrites a layer's routes to cache slots, hybrid decode built the GPU and CPU inputs with torch ops on every MoE layer: a clone of the ids, a >= 0 compare, two torch.where, a clamp_min(0) for MoE kernels without supports_inactive_slots, and with collect_stats three sums and eight in-place adds in record_decode_stats_hybrid. The ensure kernel now does this in the same launch:

  • It writes the CPU's routes (expert id, -1 for a GPU route) and the GPU's weights (0 for a CPU route) next to the GPU slots.
  • CLAMP: CPU routes get slot 0 instead of -1 when the GPU kernel reads every route's slot.
  • STATS: the hybrid missing / fetched / active / calls counters and their per-layer copies. The stat_* tensors become views of one buffer, so the kernel takes them as two pointers; decode_miss_stats and decode_miss_stats_per_layer read them as before.

ensure_experts_hybrid now takes the router weights and returns HybridRoutes(gpu_slots, gpu_w, cpu_ids). _decode_hybrid is its only caller, so the path without the split goes away: record_decode_stats_hybrid, the CPU reference _ensure_experts_hybrid_cpu, the two tests in tests/moe/test_hybrid_fetch.py that drove the kernel without the split, and the split-rule test there that only checked its own reference helper are removed.

This is independent of #670. Both change _decode_hybrid, in different hunks; a trial merge of the two branches merges without conflicts.

Validation

H100 80GB HBM3, Xeon Platinum 8480+, torch 2.14.1+cu130.

  • Equivalence with main (a script, not added to tests/): the same 300 random decode steps (3 layers, 32 experts, 40 slots, top-8, batch 1 and 4, fetch fraction 0.5, clamp on and off, fp32 and bf16 weights) through main's torch split with record_decode_stats_hybrid and through HybridRoutes, in two processes. GPU slots, CPU ids, GPU weights, slot_for_id and id_of_slot are equal after every step; the totals and per-layer stats are equal at the end.
  • tests/moe tests/engine/test_moe_cpu_layers.py on 930cc18: 155 passed, 6 skipped. Main gives 157 passed on this machine; the difference is the two removed tests. 47aa1a1 then drops one more test, which ran only Python code; the 2 tests left in tests/moe/test_hybrid_fetch.py pass.
  • Decode throughput, bench_decode_moe.py --backend hybrid --greedy --decode 512 --cache-rate 0.25, ABBA against main (9c635a1), tok/s, measured on the first commit (13a4887):
Model main this PR
Qwen3.6-35B-A3B-FP8 106.60 / 106.72 111.37 / 111.09
nvidia/Qwen3.6-35B-A3B-NVFP4 126.86 / 126.93 132.66 / 132.64

Output text sha1 matches main in every run. On the final commit, one Qwen3.6-35B-A3B-FP8 hybrid run decodes at 111.08 tok/s with the same output sha1.

Not covered: ROCm.

@jason-fxz jason-fxz mentioned this pull request Oct 14, 2026
3 of 10 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant