Add context parallelism to the Gated Delta Net (Qwen3-Next / Qwen3.5) - #5040
Open
NuojCheng wants to merge 1 commit into
Open
Add context parallelism to the Gated Delta Net (Qwen3-Next / Qwen3.5)#5040NuojCheng wants to merge 1 commit into
NuojCheng wants to merge 1 commit into
Conversation
Shards the GDN sequence across the `context` mesh axis with a cross-rank
payload that is constant in sequence length.
The chunked gated delta rule is affine in the incoming recurrent state:
h_out = (gamma * I - k_tilde^T @ w) @ h + k_tilde^T @ u == M @ h + U
o = (q_g - attn_i @ w) @ h + attn_i @ u
Affine maps compose associatively, so a rank's entire shard collapses to a
single pair (M_r, U_r) and combining shards is a prefix scan over those pairs.
M_r is (B, H, K, K) and U_r is (B, H, K, V) -- both independent of sequence
length, unlike softmax-attention context parallelism which exchanges O(S)
key/value tensors.
`jax_chunk_gated_delta_rule_cp` implements this in three phases: a local pass
from a zero state that carries the composed transition and skips output
computation, an all-gather plus exclusive prefix scan over (M_r, U_r), then a
local pass with the true incoming state that emits outputs. The WY factors are
computed once and reused across both passes. M is composed in its low-rank
form so the extra cost is 2*K^2*C per chunk rather than a dense K^3 product.
The monolithic `jax_chunk_gated_delta_rule` is split into `_gdn_chunk_prepare`,
`_gdn_scan_body` and `_gdn_finalize` so both the single-device and the
context-parallel paths share the same numerics; its signature and behavior are
unchanged.
On the layer, the pre-conv sharding constraint no longer replicates the
sequence when the context axis is active -- doing so would all-gather the whole
sequence and defeat the split. The causal depthwise conv only needs a
`gdn_conv_kernel_dim - 1` token halo, which GSPMD emits as a collective-permute.
`context_parallel_load_balance` must be off: its zigzag reorder gives each rank
two non-contiguous chunks of the sequence, which a sequential recurrence cannot
consume. The layer raises a ValueError explaining this rather than silently
computing the wrong result.
Verified on a v5 TPU VM:
- kernel forward/backward parity vs. the single-device recurrence at cp=4,
relative error ~1e-7 on outputs, final state and all five gradients
- compiled HLO shows a single f32[8,4,128,128] all-gather that stays the same
size at S=512, 1024 and 2048
- AOT compile of qwen3-next-80b-a3b on v5p-64 at 8192 context: FLOPs +0.20%,
HBM bytes +3.6%, peak temp +8.7% at cp=4
- matched 6-step training runs at 4096 context reproduce the same loss curve
at cp=1, 2 and 4
NuojCheng
requested review from
A9isha,
RissyRan,
SurbhiJainUSC,
abhinavclemson,
aireenmei,
bvandermoon,
darisoy,
dipannita08,
gagika,
gobbleturk,
hengtaoguo,
huytransformer,
igorts-git,
jiangjy1982,
khatwanimohit,
parambole,
richjames0,
shralex,
shuningjin,
vipannalla and
xibinliu
as code owners
August 28, 2026 03:47
There was a problem hiding this comment.
Code Review
This pull request introduces context parallelism for the Gated Delta Net (GDN) in Qwen3. It refactors the jax_chunk_gated_delta_rule implementation into modular helper functions and adds jax_chunk_gated_delta_rule_cp to support context-parallel execution. Additionally, a new unit test suite tests/unit/gdn_context_parallelism_test.py is added to verify correctness, sequence-length independence of cross-rank communication, and proper error handling. I have no feedback to provide as there are no review comments.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds context parallelism to the Gated Delta Net used by Qwen3-Next and Qwen3.5, with a cross-rank payload that does not grow with sequence length.
Why
GDN currently forces the full sequence onto every rank. The sharding constraint before the causal conv replicates the sequence axis, and
jax_chunk_gated_delta_ruleis alax.scanover chunks that assumes it owns the whole sequence. Withici_context_parallelism > 1the GDN layers all-gather everything back, so the context axis buys nothing on the linear-attention half of these hybrid models.Softmax-attention context parallelism (ring, all-gather, Ulysses) exchanges O(S) key/value tensors. Linear attention does not have to. The chunked gated delta rule is affine in the incoming recurrent state:
Affine maps compose associatively, so a rank's entire shard collapses to a single pair
(M_r, U_r), and stitching the ranks back together is a prefix scan over those pairs.M_ris(B, H, K, K)andU_ris(B, H, K, V)— both independent of sequence length. For qwen3.5-35b-a3b the crossover against softmax-attention CP is around 1024 tokens per shard; past that, GDN's exchange is strictly cheaper and stays flat while everything else grows.Note this is not the Mamba-2/GLA situation. Those have a scalar decay, so
Mdegenerates to a scalar and the combination is trivial. GDN's delta rule makesMa genuineK x Kmatrix, which is why it needs the explicit transition-composition machinery below.Implementation
jax_chunk_gated_delta_rule_cpruns three phases:M_ralongside the state. Outputs are skipped, since the true incoming state is not known yet.all_gatherthe(M_r, U_r)pairs and run an exclusive prefix scan, giving every rank the state that enters its shard.Mis composed in its low-rank form (M_new = gamma * M - k_tilde^T (w @ M)), costing2 * K^2 * Cper chunk instead of a denseK^3product.Phase 1 makes the GDN core about 50% more expensive, but the core is only ~8% of the GDN block (the projections dominate: ~67 MFLOP/token vs ~5.9 MFLOP/token). AOT confirms this: +0.20% total model FLOPs at cp=4.
To share numerics between the two paths, the monolithic
jax_chunk_gated_delta_ruleis split into_gdn_chunk_prepare/_gdn_scan_body/_gdn_finalize. Its public signature and behavior are unchanged; the single-device path is the same computation, just reassembled.On the layer side:
collective-permuteofbf16[1,3,512]— exactlygdn_conv_kernel_dim - 1tokens — and zero all-gathers.state_pspecis untouched and stays replicated across the context axis.g=0(identity transition) andk=v=q=0(zero contribution), so they are a no-op for the recurrence and get sliced off the output.Limitation: load balancing must be off
context_parallel_load_balance(defaulttrue) applies a zigzag reorder so rankrholds global chunksrand2*cp-1-r. That gives each rank two non-contiguous pieces of the sequence, which a sequential recurrence cannot consume — the prefix scan assumes rank order equals sequence order. Rather than silently computing the wrong result, the layer raises:Since GDN has no causal-mask triangle, its per-rank cost is uniform anyway, so it gains nothing from load balancing. The full-attention layers of these hybrid models do lose the balancing, which is a real cost and a candidate for future work (the reorder is applied at the data-iterator level in
train_utils.py, so making it layer-local would be the fix).Tests
New
tests/unit/gdn_context_parallelism_test.py(5 tests, all passing on a v5 TPU VM):test_forward_matches_single_device/test_backward_matches_single_device— kernel parity against the single-device recurrence undershard_map.test_cross_rank_payload_is_sequence_length_independent— parses the compiled HLO, sums all-gather volume at S=512/1024/2048, asserts it is constant and equals exactlycp * B * H * K * (K + V)elements.test_layer_output_matches_without_context_parallelism— the fullQwen3NextGatedDeltaNetlayer is invariant to CP degree.test_load_balancing_is_rejected— the guard above fires.Measured results:
f32[8,4,128,128], identical at S = 512 / 1024 / 2048collective-permuteofbf16[1,3,512], zero all-gathersAOT compile, qwen3-next-80b-a3b on v5p-64 at 8192 context:
Real training runs, 4x TPU v5, 4096 context, 6 steps, synthetic data, seeded:
Matching to float reduction-order noise. Also verified cp=1 vs cp=2 at 1024 context (identical: 8.126/8.098/8.073/8.055/8.044/8.039).
One note for reviewers reproducing this: cp=4 at 1024 context fails with
q_block_size=512 should divide q_seq_len_per_shard=256. That is the pre-existing splash-attention kernel constraint on the hybrid model's full-attention layers, not GDN. Hence 4096 context for the cp=4 comparison.Also ran
pyinkandpylint(10.00/10) over both changed files.Checklist
gemini-reviewlabel.