You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
qwen3_5_moe is TP=1-only because the model never matched the engine's per-rank GDN state contract — patch makes TP=2 work on 2x RX 9070 XT (ROCm) #478
kvcache/linear_state_pool.py::_linear_local_dims already allocates the Gated-DeltaNet conv and recurrent state per rank (div_even(heads, tp_size)), so the engine expects the GDN layers to be head-sharded at TP>1. But no GDN family implements that sharding, and every one is gated with NotImplementedError("... supports TP=1 only"). Replicating GDN instead (the natural first attempt) writes full-head state into half-size per-rank buffers and produces garbage from the first token.
Two adjacent issues block TP once the gate is lifted:
moe/host_banks.py::alloc_persistent_layer_banks has rank 0 fill the expert banks and every other rank map rank 0's files — which cannot represent an intermediate-sharded expert layout.
PersistentPool.prepare_dir evicts every sibling pool under the root, so per-rank pools delete each other.
With the GDN layers head-sharded to match the state pool, the experts sharded per rank, and per-rank pools that don't evict each other, TP=2 produces answers identical to TP=1 on 2× RX 9070 XT (gfx1201, ROCm 7.2.1).
How it was found
Greedy decoding (temperature 0) as a correctness fingerprint: run the patched tree at TP=1 as a control (must be byte-identical to unpatched TP=1), then TP=2, and compare. Bisecting by replicating one component at a time is what pointed at the state pool — output stayed wrong with every weight replicated, which meant the engine was allocating per-rank state the model had to match, not that sharding was buggy.
I have a working patch (9 files) against qwen3_5_moe:
GDN heads sharded in contiguous halves (exact — each head owns its recurrent state, and the fla kernels pair k-head i with the contiguous v-head group i_hv // (HV // H)); the merged input projection uses per-segment slots so the per-slot TP division keeps [q|k|v|z] layout.
a segment-aware loader slicer for the GDN tensors and their fp8 block scales; three TP=1-only gates removed.
attention head-sharded (LinearOProj); fp8-block experts sharded on the intermediate dim (no extra all-reduce — MoELayer._maybe_all_reduce already covers it).
rank-sharded expert banks: each rank fills and pins its own pool; rank 0 sweeps the root first (sparing same-run sibling pools via a manifest match), barrier, then the others create without evicting.
vision tower built replicated (as qwen3_vl already does under TP).
Happy to open a PR if that's useful — let me know the preferred shape (squashed onto main, or against a branch). Also glad to share the greedy-fingerprint validation script.
Two smaller things I can split out if you'd rather:
layers/base.py state-dict shape assertion currently gives no parameter name; adding the name + both shapes made this a one-run diagnosis.
fast_index_copy AOT variants are sized from the full moe_intermediate_size, so under TP the gather falls to the JIT path (correct, just not precompiled).
Cross-referencing related work I found after opening this, so it's clear where this sits: #447, #385 (draft), #440.
What #447 covers, and what it leaves open.#447 removes exactly one TP gate — qwen4_exp weight loading supports TP=1 only. For qwen3_5_moe it keeps the existing gate and adds a second restriction:
ifownershipisnotNone:
raiseNotImplementedError("block-FP8 expert banks do not support owner-local TP+EP")
ifget_tp_info().size>1:
raiseNotImplementedError("qwen3_5_moe fp8 expert banks support TP=1 only")
It also scopes its GDN TP to the unquantized path:
ifself._split_in_projandtp_size>1:
raiseNotImplementedError("qwen4_exp dense TP currently supports the BF16 GDN path only")
So this issue is the complementary gap rather than a duplicate: qwen3_5_moe at TP>1, with block-FP8 experts and the quantized (split_in_proj) GDN path that an FP8 checkpoint like Qwen3.6-35B-A3B-FP8 takes.
Independent convergence on the GDN design. I implemented this for qwen3_5_moe before reading #447, and the two arrived at the same shape, which is probably worth something as cross-validation — same approach, different model family, different vendor:
div_even(num_k_heads/num_v_heads, tp_size, allow_replicate=True) for rank-local head counts
LinearReplicated → LinearOProj on out_proj
the fused input GEMM built from global sizes, with the forward splitting by rank-local sizes
a segment-aware loader shard that splits in_proj_qkv / conv1d into q/k/v, shards each, and re-concatenates (a plain contiguous half of the fused block would hand rank 0 [q|k] and rank 1 [v])
A_log / dt_bias sized to local num_v_heads
The framing in my original report still holds as the underlying reason all of this is needed: _linear_local_dims already allocates GDN conv and recurrent state per rank, so the engine has always expected head-sharded GDN, and replicating it writes full-head state into half-size buffers.
What my patch adds on top of #447's direction:qwen3_5_moe GDN and attention; block-FP8 routed experts sharded on the intermediate dim; and per-rank expert bank pools, since alloc_persistent_layer_banks has rank 0 fill and every rank map rank 0's files, which cannot represent a rank-sharded expert layout (and PersistentPool.prepare_dir evicts sibling per-rank pools).
Happy to rebase onto #447 once it lands rather than propose anything competing — it looks like the better base. Validated on 2× RX 9070 XT (gfx1201, ROCm 7.2.1): greedy output identical to TP=1, 120-request soak with 0 errors, 13.5 → 17.7 tok/s, first-token 10.0 → 5.0 s.
Data point from the NVIDIA side, in case it helps situate the patch: we ran the qwen3_5_moe TP=2 work that grew out of #104 (RuixiangMa's base TP + a 9-commit stack completing the quantized loaders and a resident NVFP4 fused path) on 2× RTX PRO 4000 Blackwell with nvidia/Qwen3.6-35B-A3B-NVFP4:
--moe-backend fused (GPU-resident experts), TP=2: 141 tok/s client wall (engine ~146), 22.4 GB VRAM/GPU, boots clean, coherent output. That's 5–9× the offload (~15) and hybrid (~27) TP=2 paths on the same box, since the experts are fully resident and CUDA graphs apply.
For reference, that stack is parked at CraigStone-Dev/FreeToken:feat/qwen3-5-tp-support (we held off pushing it forward while #447 and the sharded-bank direction in the #385 thread settled), and it is now rebased onto current main as rebase/tp-20260923 (5 commits, CPU suite green) if anyone wants the quantized-loader side (per-head GDN sharding, block-FP8/mixed checkpoint handling, resident NVFP4 expert banks) as a reference.
Activity
No-LetterHead4141 commentedon Sep 14, 2026
Cross-referencing related work I found after opening this, so it's clear where this sits: #447, #385 (draft), #440.
What #447 covers, and what it leaves open. #447 removes exactly one TP gate —
qwen4_exp weight loading supports TP=1 only. Forqwen3_5_moeit keeps the existing gate and adds a second restriction:It also scopes its GDN TP to the unquantized path:
So this issue is the complementary gap rather than a duplicate:
qwen3_5_moeat TP>1, with block-FP8 experts and the quantized (split_in_proj) GDN path that an FP8 checkpoint like Qwen3.6-35B-A3B-FP8 takes.Independent convergence on the GDN design. I implemented this for
qwen3_5_moebefore reading #447, and the two arrived at the same shape, which is probably worth something as cross-validation — same approach, different model family, different vendor:div_even(num_k_heads/num_v_heads, tp_size, allow_replicate=True)for rank-local head countsLinearReplicated→LinearOProjonout_projin_proj_qkv/conv1dinto q/k/v, shards each, and re-concatenates (a plain contiguous half of the fused block would hand rank 0[q|k]and rank 1[v])A_log/dt_biassized to localnum_v_headsThe framing in my original report still holds as the underlying reason all of this is needed:
_linear_local_dimsalready allocates GDN conv and recurrent state per rank, so the engine has always expected head-sharded GDN, and replicating it writes full-head state into half-size buffers.What my patch adds on top of #447's direction:
qwen3_5_moeGDN and attention; block-FP8 routed experts sharded on the intermediate dim; and per-rank expert bank pools, sincealloc_persistent_layer_bankshas rank 0 fill and every rank map rank 0's files, which cannot represent a rank-sharded expert layout (andPersistentPool.prepare_direvicts sibling per-rank pools).Happy to rebase onto #447 once it lands rather than propose anything competing — it looks like the better base. Validated on 2× RX 9070 XT (gfx1201, ROCm 7.2.1): greedy output identical to TP=1, 120-request soak with 0 errors, 13.5 → 17.7 tok/s, first-token 10.0 → 5.0 s.
CraigStone-Dev commentedon Sep 24, 2026
Data point from the NVIDIA side, in case it helps situate the patch: we ran the qwen3_5_moe TP=2 work that grew out of #104 (RuixiangMa's base TP + a 9-commit stack completing the quantized loaders and a resident NVFP4 fused path) on 2× RTX PRO 4000 Blackwell with
nvidia/Qwen3.6-35B-A3B-NVFP4:--moe-backend fused(GPU-resident experts), TP=2: 141 tok/s client wall (engine ~146), 22.4 GB VRAM/GPU, boots clean, coherent output. That's 5–9× the offload (~15) and hybrid (~27) TP=2 paths on the same box, since the experts are fully resident and CUDA graphs apply.For reference, that stack is parked at
CraigStone-Dev/FreeToken:feat/qwen3-5-tp-support(we held off pushing it forward while #447 and the sharded-bank direction in the #385 thread settled), and it is now rebased onto currentmainasrebase/tp-20260923(5 commits, CPU suite green) if anyone wants the quantized-loader side (per-head GDN sharding, block-FP8/mixed checkpoint handling, resident NVFP4 expert banks) as a reference.