Skip to content

Add state-passing context parallelism for Mamba2 - #6808

Open
shcho1118 wants to merge 5 commits into
NVIDIA:devfrom
shcho1118:feature/mamba2-state-passing-cp-upstream
Open

Add state-passing context parallelism for Mamba2#6808
shcho1118 wants to merge 5 commits into
NVIDIA:devfrom
shcho1118:feature/mamba2-state-passing-cp-upstream

Conversation

@shcho1118

@shcho1118 shcho1118 commented Aug 24, 2026

Copy link
Copy Markdown

Mamba2 state-passing context parallelism

Adds an opt-in, sequence-sharded context-parallel path for Mamba2 training. The existing all-to-all Mamba CP path stays the default and is untouched.

The existing path redistributes activations so every rank holds the whole sequence for a subset of heads and groups, costing O(local_sequence * hidden) per rank. The state-passing path keeps the sequence shard local and exchanges only what the causal boundary needs — the convolution halo and an FP32 state summary — neither of which scales with sequence length:

A2A:                O(local_sequence * hidden)
State-passing:      Conv: O(batch * channels * (d_conv - 1))
                    SSD : O(batch * heads * headdim * dstate) + O(batch * heads)

The SSD transform of one causal segment is affine in its initial state, S_out = a_block * S_in + S_ext, so each rank computes its own (S_ext, a_block), all-gathers the packed summary, and recovers its true initial state with an exclusive causal scan. The gather is issued asynchronously and overlaps the local CB computation. Backward mirrors this with a reverse boundary scan and recomputes everything that scales with sequence length rather than saving it.

Configuration

--use-mamba-state-passing-cp
--mamba-state-passing-cp-load-balancing {none,permute_p2p,permute_a2a,virtual}

Standard Megatron CP hands the mixer a front/back balanced (zigzag) shard, so the mixer rejects none, which expects an already-contiguous shard and exists for direct calls into the kernels.

Mode Layout handling
permute_p2p Exchanges chunks for a contiguous causal shard, point-to-point
permute_a2a The same exchange in one unequal-split all_to_all_single
virtual No activation exchange; each balanced half becomes an independent causal segment

Why virtual is the mode to prefer

virtual was fastest in every valid measured cell, and the reason is structural rather than incidental:

  1. It does not move activations at all. The permute modes must physically exchange O(local_L * hidden) twice in forward (in and out) and again in backward — reintroducing exactly the activation traffic that state passing exists to remove. virtual reinterprets the layout instead, so its only communication is the sequence-length-independent boundary summary.
  2. The boundary payload does not grow. Each rank owns two causal segments instead of one, so the gathered summary has 2 * cp_size entries instead of cp_size. That is a handful of KiB and still independent of sequence length.
  3. No permutation autograd node and no exchange buffers. The permute modes need a custom autograd function holding send/receive buffers on both edges, which adds peak memory and two synchronization points per layer.
  4. More kernel parallelism at the same token count. Packing the two segments onto the batch axis gives the SSD Triton kernels 2 * batch program instances over the same number of tokens. This is visible at large CP sizes where each rank's local sequence is short and the kernels would otherwise be launch-bound — at CP=8, virtual still gives 1.2–1.3x while the permute modes are at parity with A2A.

The trade-off is a shape constraint: each virtual segment must be a multiple of the SSD chunk_size, so the sequence length must be a multiple of 4 * cp_size * chunk_size rather than 2 * cp_size * chunk_size.

Benchmark

Measured on a single B200 node on the development branch this was ported from (2026-07-21, PyTorch 2.11.0a0+a6c236b9fd.nv26.03, CUDA 13.2, NCCL 2.29.7, BF16). Mamba core shape nheads=64, headdim=64, ngroups=8, dstate=128, d_conv=4, chunk_size=128. Each cell is the rank-maximum p50 of 10 forward+backward iterations after 5 warmups, as p50 ms (speedup vs A2A). Only the post-projection mixer core is timed — see examples/mamba_state_passing_context_parallel/.

CP=2:

L Batch A2A permute P2P permute A2A virtual
8K 1 4.75 (1.00x) 4.87 (0.98x) 4.87 (0.98x) 3.93 (1.21x)
16K 2 7.73 (1.00x) 6.02 (1.29x) 6.71 (1.15x) 4.62 (1.67x)
32K 4 29.04 (1.00x) 20.49 (1.42x) 24.21 (1.20x) 16.87 (1.72x)
64K 1 13.01 (1.00x) 8.69 (1.50x) 10.10 (1.29x) 6.66 (1.95x)
128K 1 25.44 (1.00x) 17.25 (1.47x) FAIL 12.60 (2.02x)
128K 4 115.02 (1.00x) 82.46 (1.39x) FAIL 66.71 (1.72x)

CP=4:

L Batch A2A permute P2P permute A2A virtual
8K 1 5.19 (1.00x) 4.89 (1.06x) 4.81 (1.08x) 4.17 (1.24x)
32K 4 16.20 (1.00x) 12.83 (1.26x) 14.31 (1.13x) 8.60 (1.88x)
64K 1 7.50 (1.00x) 6.03 (1.24x) 6.65 (1.13x) 4.27 (1.76x)
128K 1 14.27 (1.00x) 10.77 (1.32x) 11.92 (1.20x) 6.82 (2.09x)
128K 8 123.12 (1.00x) 92.54 (1.33x) FAIL 67.48 (1.82x)

CP=8:

L Batch A2A permute P2P permute A2A virtual
8K 1 5.24 (1.00x) 4.73 (1.11x) 4.75 (1.10x) 4.00 (1.31x)
32K 4 8.87 (1.00x) 8.38 (1.06x) 9.17 (0.97x) 4.92 (1.80x)
64K 4 17.13 (1.00x) 13.39 (1.28x) 15.03 (1.14x) 8.87 (1.93x)
128K 1 8.96 (1.00x) 7.33 (1.22x) 7.96 (1.13x) 4.31 (2.08x)
128K 8 65.84 (1.00x) 47.24 (1.39x) 53.49 (1.23x) 34.06 (1.93x)

TP2-local shape (global heads/groups divided to a 32/4 local shape, no TP communication), CP=4, MBS=3, 20 iterations after 5 warmups:

L A2A permute P2P permute A2A virtual A2A peak virtual peak peak delta
32K 6.512 ms 5.847 ms (1.114x) 6.177 ms (1.054x) 4.357 ms (1.495x) 2.079 GiB 2.135 GiB +2.69%
128K 23.975 ms 18.561 ms (1.292x) 20.969 ms (1.143x) 13.293 ms (1.804x) 8.309 GiB 8.382 GiB +0.87%

virtual trades a small amount of allocated memory (+0.9% to +2.7% in this workload) for the latency win.

A short re-run of the benchmark included in this PR, on the current branch and a different B200 host (CP=4, L=4096, batch 1), reproduces the ordering: A2A 5.89 ms, permute-p2p 5.41 ms (1.088x), permute-a2a 5.16 ms (1.141x), virtual 4.61 ms (1.277x).

Caveat on permute_a2a

The FAIL cells above are runs that terminated with a CUDA illegal memory access. They appear first and only in permute_a2a at large exchange payloads. This has not been diagnosed, so the table should not be read as a statement of supported shapes for that mode. It is one more reason to prefer virtual, and reviewers may reasonably ask for permute_a2a to be dropped until it is understood.

Overlap with context_parallel_layout

permute_p2p and permute_a2a convert the balanced layout to a contiguous causal shard, which is the same conversion Megatron Core already models as CpPartitionMode = Literal["zigzag", "contiguous"]. That abstraction was introduced with DeepSeek-v4 CP support in #5087, refactored into megatron/core/context_parallel_layout/ in #6387 (+ #6515 for main branch), and is exercised by MTP fixes such as #5706 and #6246. Gated DeltaNet already uses it at its module entry point via convert_module_input_tensors_cp_partition_mode() when running its chunkwise (sequence-sharded) CP path — the closest existing analogue to what this PR does for Mamba2.

The permutation here is implemented separately for two reasons: it is driven from inside the fused Conv+SSD autograd function rather than at the module entry point, and it adds a point-to-point backend the shared helper does not have. This is noted in the module docstring and the feature doc, and consolidating onto the shared helper is intended follow-up work rather than a judgement that the shared path is unsuitable. Happy to do that consolidation in this PR instead if reviewers prefer it; the recommended virtual mode does not depend on either implementation.

Not supported yet

Each is rejected with an explicit assertion rather than silently producing wrong results:

  • inference
  • seq_idx and packed sequences
  • hybrid and dynamic (variable-length) CP
  • an externally supplied SSD initial_states
  • --mamba-training-ssm-states-dtype
  • fused RMSNorm and output projection inside the custom function (both remain available on the mixer path, outside it)

Testing

All new tests pass on 8xB200, on every rank. Dimensions are Nemotron-3 Nano's mixer shape; the parametrization varies batch size and sequence length, which is what the CP sharding and boundary exchange actually depend on.

Test Coverage Result
tests/unit_tests/ssm/ops/test_ssd_state_passing_cp.py Balanced/contiguous permutation exactness, and the conv and SSD kernels against a full-sequence reference; cp∈{2,4} x virtual∈{F,T} x (batch,L)∈{(1,2048),(3,4096)} 20 passed
tests/unit_tests/ssm/test_mamba_mixer_state_passing_cp.py Every mode end to end against a full-sequence mixer and against the A2A CP path, for output, input gradient, and all parameter gradients 4 passed
tests/unit_tests/ssm/test_mamba_state_passing_cp_cuda_graph.py Local and Transformer Engine CUDA Graph capture and replay for all three modes 6 passed

Gradient comparisons use a relative-RMS criterion rather than elementwise allclose, because the state-passing path reassociates the SSD scan across CP boundaries, changing BF16 rounding without changing the mathematics. Observed errors are ~1e-6 for the output and most gradients across L ∈ {2048, 8192, 32768}; ddt_bias is the loosest at ~1.5e-3 against a 3e-3 threshold, stable across sequence lengths.

The comparisons are all evaluated before anything is asserted and the verdict is all-reduced, so a mismatch fails every rank identically instead of leaving some ranks inside a collective, which would deadlock the run instead of reporting a failure.

tests/unit_tests/ssm/ otherwise unchanged: 122 passed, 150 skipped, and 1 pre-existing failure unrelated to this PR (test_hyper_connection_gdn_gpu_forward needs flash-linear-attention, which is absent from the environment used here).

pylint 10.00/10 and ruff clean on the changed files under megatron/core and tests/.

Add the Triton kernels and the fused Conv1d+SSD autograd function that let
Mamba2 run context parallelism with the sequence shard kept local. Only the
causal boundary is exchanged: the convolution halo and an FP32 state summary
of (final state, block decay) per segment, neither of which scales with
sequence length.

The SSD transform of one causal segment is affine in its initial state,

  S_out = a_block * S_in + S_ext

so each rank computes its own (S_ext, a_block), all-gathers the packed
summary, and recovers its true initial state with an exclusive causal scan
over the gathered summaries. The gather is issued asynchronously and overlaps
the local CB computation.

Backward mirrors this with a reverse boundary scan and recomputes everything
that scales with sequence length rather than saving it, writing SSD gradients
straight into the buffers the convolution backward consumes.

Two sequence layouts are supported. A contiguous causal shard is the natural
one; the virtual layout instead treats each half of Megatron's balanced
front/back shard as an independent causal segment, which lets the caller skip
the activation exchange entirely. The permutation helpers that convert
between the balanced and contiguous layouts are also here, with p2p and
all-to-all backends.

Nothing calls this yet; the mixer is wired up separately.

Signed-off-by: Sanghun Cho <highcredit1118@gmail.com>
Make the state-passing CP path reachable from training through two new
options:

  --use-mamba-state-passing-cp
  --mamba-state-passing-cp-load-balancing {none,permute_p2p,permute_a2a,virtual}

The feature is opt-in and the existing all-to-all Mamba CP path stays the
default. When enabled, the mixer skips MambaContextParallel's activation
redistribution and hands the projected zxBCdt straight to the fused
state-passing function.

Standard Megatron CP gives the mixer a front/back balanced shard, so the
mixer rejects the 'none' layout, which expects an already-contiguous shard
and exists for direct calls into the kernels. The permute modes exchange
chunks for a contiguous shard; virtual reinterprets each balanced half as an
independent causal segment and moves no activations.

The path uses the TP-local convolution and SSM parameters rather than
MambaContextParallel's CP slices, since the heads are not sharded across CP
here. RMSNorm and the output projection stay outside the fused function, so
both remain available.

Inference, packed sequences, hybrid and dynamic CP, and
--mamba-training-ssm-states-dtype are not supported yet; each is rejected
with an explicit assertion rather than silently producing wrong results.

Signed-off-by: Sanghun Cho <highcredit1118@gmail.com>
Cover both graph backends for every load-balancing mode: Megatron's local
CUDA Graph lifecycle and the Transformer Engine helper. The boundary
collectives sit inside the captured region, so capture and replay are worth
asserting explicitly rather than inferring from the eager tests.

Each test records a graph, replays it, requires bitwise equality with eager
execution on the first replay, and then perturbs the input to confirm the
static input buffer is actually refreshed rather than a stale result being
returned.

Both backends need the TE RNG tracker, and the local backward capture
accumulates weight gradients into main_grad, which DDP normally provides, so
the fixture creates those buffers the way
tests/unit_tests/transformer/test_cuda_graphs.py does. Teardown goes through
delete_cuda_graphs() to clear the global cudagraph record; leaving it in
place makes the next test see cudagraph_created with no matching runners.

Signed-off-by: Sanghun Cho <highcredit1118@gmail.com>
Describe the communication trade-off against the existing all-to-all Mamba
CP path, the three load-balancing modes, the forward and backward flow
including which tensors are saved versus recomputed, and the currently
unsupported configurations.

Also record that the permute modes overlap with
megatron.core.context_parallel_layout, whose CpPartitionMode models the same
zigzag/contiguous pair and which Gated DeltaNet already uses, and that
consolidating onto that helper is intended follow-up work.

Signed-off-by: Sanghun Cho <highcredit1118@gmail.com>
Compare the existing all-to-all Mamba CP path against each state-passing
load-balancing mode, so the mode can be chosen from measurements rather than
by guessing.

Only the post-projection part of the mixer is timed. That is where the paths
differ; the projections and RMSNorm are identical across paths and would
dilute the comparison. The benchmark reproduces just the mixer attributes
MambaContextParallel and MambaStatePassingCPAdapter read, which keeps it
independent of the surrounding model, spec, and process-group plumbing.

Each iteration is reduced across ranks with MAX rather than averaged, since a
CP path is only as fast as its slowest rank, and rank alignment happens on a
barrier outside the timed interval so waiting for stragglers is not counted
twice. Alongside the human-readable lines, one RESULT line per path is
emitted for scripted collection.

Signed-off-by: Sanghun Cho <highcredit1118@gmail.com>
@copy-pr-bot

copy-pr-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@shcho1118
shcho1118 marked this pull request as ready for review August 24, 2026 14:47
@shcho1118
shcho1118 requested review from a team as code owners August 24, 2026 14:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant