Skip to content

[Perf] MAGI-2: drop the Ulysses send/receive buffer copies - #8595

Draft
yeahdongcn wants to merge 4 commits into
vllm-project:mainfrom
yeahdongcn:xd/magi2-ulysses-send-buffer
Draft

yeahdongcn wants to merge 4 commits into
vllm-project:mainfrom
yeahdongcn:xd/magi2-ulysses-send-buffer

Conversation

@yeahdongcn

@yeahdongcn yeahdongcn commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Purpose

Each MAGI-2 attention layer runs two Ulysses all-to-alls, and both carried extra full-buffer copies. Before the Q/K/V all-to-all, every input was copied into a destination-rank-major temporary and the three temporaries were concatenated into the send buffer, so the whole buffer moved twice. After the output all-to-all, each rank copied its [world, S_rank, H, D] receive buffer to [S_rank, world*H, D] before the gated output projection. This PR removes those copies in three commits and keeps the result fast under dynamic-shape compilation with a fourth:

  1. [Perf] Copy Ulysses Q/K/V straight into the all-to-all send buffer (all platforms). scatter_heads_gather_seqlen allocates one [world, S_rank, sum(H_i), D] send buffer and copies each input once into its head slice, so the concatenation pass goes away. The send buffer has the same shape, dtype, layout and bytes as before, and the receive buffer and the per-input views FlashAttention reads are unchanged. Inputs on different devices are now rejected up front.
  2. [Perf] Read the Ulysses attention output in place on MUSA. The new scatter_seqlen_gather_head_shards returns the [S_rank, world, H, D] view of the receive buffer. On MUSA the attention kernel hands back that view, and Magi2Attention.output flattens it after the token gather, so the compiled output region reads the receive buffer directly. scatter_seqlen_gather_heads keeps its contract.
  3. [Perf] Store Ulysses Q/K/V straight into the send buffer on MUSA. On MUSA, Magi2Attention.project returns Q/K/V as [S_rank, world, H_i, D] head-shard views of one destination-rank-major buffer (pack_ulysses_head_shards), so Q/K/V are laid out in send order and scatter_heads_gather_seqlen can send that buffer without a copy. Any other input, including a view autograd tracks, takes the single copy from commit 1.
  4. [Perf] Pin the Ulysses head-shard layout in the output projection. With diffusion_compile_dynamic=True, the default, every input dimension of a compiled region is symbolic and Inductor passes symbolic sizes to Triton as 64-bit arguments, so the attention-gate kernel that reads the [T, world, H/world, D] view split each flat index with four 64-bit divisions and remainders, and the gated product was copied once more before the output projection. When the head shards come from the module's own Ulysses group, Magi2Attention.output now specializes the shard count and checks the per-rank head count and the head size with torch._check, so only the token count stays symbolic and the kernel indexes with constants; head shards of any other shard count take the unpinned path. Static compiles and eager runs are unchanged.

Commit 1 is the only change to the data path on other platforms. Commits 2 and 3 change the layouts inside compiled regions, so they are enabled on MUSA only, decided once in Magi2Attention.__init__; the exchange helpers they extend keep their 3-D contracts. Commit 3 also requires the query and KV head counts to divide by the Ulysses degree; with MAGI-2's 24 heads that holds at SP8 (3 heads per rank) and SP4 (6 heads per rank). No change alters a value: Q/K/V and the attention output are computed by the same operations and are only stored at different addresses, and the generated video and audio stay byte-identical (Test Result).

Based on main and independent of the other open MAGI-2 PRs. The overlaps are textual: with #8511 the .parallel import list in modeling_magi2.py, and with the per-request rope/modality and sink-LSE caches PR three adjacent lines in ulysses_packed_attention_with_sink and Magi2PackedAttentionKernel; whichever lands second needs a trivial rebase. Running MAGI-2 on MUSA needs #8498.

Test Plan

python -m pytest -q -o addopts='' \
  tests/diffusion/models/magi2/test_ulysses_exchange.py \
  tests/diffusion/models/magi2/test_native_distributed_parity.py \
  tests/diffusion/models/magi2/test_native_compile_distributed.py

test_ulysses_exchange.py runs both exchanges for every rank of 2-, 4- and 8-rank groups in one process, with uneven splits, a rank without tokens, unequal head counts and mixed dtypes. It compares the send buffers, split sizes and receive views bit for bit with the concatenating implementation, checks that packed head shards are sent in place and that inputs which do not tile one buffer are copied, checks the MUSA gating, and checks that Magi2Attention.project and Magi2Attention.output give bitwise-identical results with and without the head-shard layouts. On a MUSA device it compiles Magi2Attention.output and Magi2TransformerLayer._attention_input statically with emulate_precision_casts at production model widths with 3702 tokens (8 ranks) and 3651 (4 ranks), plus a 14-token case, compares every output bit for bit with the unpacked layout, and asserts that the compiled region hands back views of one send buffer. No test counts kernels, so that the compiled producers store straight into that buffer, rather than Inductor adding a copy into it, is shown only indirectly, by the one-run end-to-end gain of the three commits together. These tests compile with Inductor's deterministic mode and test_configs.force_filter_reduction_configs, so two compiles of the same code pick the same reduction configs. For commit 4, a CPU test traces Magi2Attention.output with dynamic shapes and checks that only the token dimension of matching head shards stays symbolic, and that a mismatched shard count still compiles and gives the same result; on a MUSA device a test compiles the output projection dynamically at world sizes 8 and 4 with one and three modalities and asserts one gather kernel without symbolic divisions, the same number of Triton kernels as the static compile, and output bitwise equal to the static compile.

The 4-rank gloo parity test adds an SP4 run with the MUSA layouts forced on CPU and requires it to equal plain SP4 bit for bit. The distributed compile test adds the same layouts as an SP2 variant traced with fullgraph=True, backend="eager" and dynamic shapes under error_on_recompile, and requires its compiled run to equal its eager run and its eager run to equal plain SP2 bit for bit; on MUSA it needs two torchada fixes and passes with them (Test Result).

End to end: MAGI-2 Preview T2VA, prompt "A golden retriever running through a sunlit meadow, cinematic camera movement", 272p (448x256), 125 frames, 100 steps, seed 42, diffusion_compile_dynamic=False, on 8x MTT S5000. Configurations (--expert-parallel-size is from #8511):

  • SP8xCFG1 with EP4: --ulysses-degree 8 --cfg-parallel-size 1 --enable-expert-parallel --expert-parallel-size 4
  • SP4xCFG2: --ulysses-degree 4 --cfg-parallel-size 2
  • SP4xCFG2 with EP4: --ulysses-degree 4 --cfg-parallel-size 2 --enable-expert-parallel --expert-parallel-size 4

The per-step figures come from an out-of-tree timing harness around AsyncOmni with the per-step boundary from #7274; the metric is the per-step sampler.diffuse time, maximum over the 8 ranks, mean over steps 2-99.

Byte-identity check: Inductor benchmarks reduction configs on every fresh compile, so the outputs of one code base differ across fresh caches, and TORCHINDUCTOR_DETERMINISTIC=1 does not reach the compiled regions: the MAGI-2 pipeline calls torch.use_deterministic_algorithms at construction, which resets Inductor's deterministic flag. The fixed-config comparisons therefore ran with TORCHINDUCTOR_DETERMINISTIC=1 TORCHINDUCTOR_FORCE_FILTER_REDUCTION_CONFIGS=1 and a fresh TORCHINDUCTOR_CACHE_DIR per run, and compare the sha256 of the generated video and audio. GEMM shape padding is still chosen by benchmarking under this setting, so differing hashes alone would not show a change in numerics; identical hashes do show identical output.

vLLM Version: 0.28.0 (MUSA image vllm:v0.28.0-ph1-5.2.0-torch2.11.0.post2-20261001, torch 2.11.0.post2, torch_musa 2.11.0.post2+musa5.2.0), with a local test-only shim for the vLLM 0.30 names that c1e84ce imports; current main aligns with vLLM 0.31, which was not run. The single-change A/B ran with torchada c045bd3 (MooreThreads/torchada#120); the device suites and the fixed-config byte-identity runs also had torchada's cpp_extension path-signature fix MooreThreads/torchada#121 (host-side build paths only), without which cold-cache CPU Inductor compiles fail on MUSA.

vLLM-Omni Commit: 5a9da74 on top of c1e84ce, the base of the other MAGI-2 PRs; no MAGI-2 file has changed on main since. The tests and runs used the same three changes on c1e84ce, where their code is identical apart from neighbouring lines of other changes (the .parallel import list, and the sink-LSE arguments next to the new attention parameter). Trees named in Test Result:

Test Result

AI assistance: Claude Code drafted the change, the tests and this description and ran the MUSA validation listed above.

…uffer

The packed Q/K/V exchange copied every input into a destination-rank-major
temporary and then concatenated the three temporaries into the send buffer,
moving the whole buffer twice per layer. Each input is now copied once,
directly into its head slice of a destination-rank-major send buffer, so
the concatenation pass goes away.

The send buffer has the same shape, dtype, layout and bytes as the
concatenated one, and the receive buffer and the per-input views that
FlashAttention reads are unchanged; only the order of the copies differs.
Inputs on different devices are rejected up front instead of by the
concatenation.

Signed-off-by: Xiaodong Ye <xiaodong.ye@mthreads.com>
After the output all-to-all, each rank copied its [world, S_rank, H, D]
receive buffer to [S_rank, world*H, D] before the gated output projection.
On MUSA the attention kernel now hands back the [S_rank, world, H, D]
head-shard view of the receive buffer, and Magi2Attention.output flattens
it after the token gather, so inside the compiled region the gather reads
the receive buffer directly and the eager copy goes away.

Only the layout of the compiled region's attention input changes: every
value reaching the gate multiply, and so the output projection input, is
the same element of the same receive buffer. scatter_seqlen_gather_heads
keeps its contract and the other platforms keep the flattened output.

Signed-off-by: Xiaodong Ye <xiaodong.ye@mthreads.com>
On MUSA, Magi2Attention.project returns Q/K/V as [S_rank, world, H_i, D]
head-shard views of one [world, S_rank, sum(H_i), D] buffer, so Q/K/V are
laid out in all-to-all send order. scatter_heads_gather_seqlen recognises views that
tile one destination-rank-major buffer and sends that buffer as is; any
other input, including a view that autograd tracks, still takes the single
copy into a fresh send buffer, and a single-rank exchange flattens head
shards back to [S_rank, H, D].

The Q/K/V values are computed by the same operations as before and only
stored at different addresses: the head-shard layout is a pure permutation
of the old send buffer, whose bytes, split sizes and receive-side views are
unchanged. The layout is used only when every Ulysses rank gets the same
number of query and KV heads.

Signed-off-by: Xiaodong Ye <xiaodong.ye@mthreads.com>
…tion

Under dynamic-shape compilation every input dimension of a compiled
region is symbolic, and Inductor passes symbolic sizes to Triton as
64-bit arguments. The attention-gate kernel that gathers the
[T, world, H/world, D] head-shard view of the receive buffer therefore
splits each flat index with 64-bit divisions and remainders by the row
size H*D, the head size D, the head count H and the per-rank head count
H/world, and the gated product is copied once more before the output
projection.

When the head shards come from the module's own Ulysses group,
Magi2Attention.output specializes their shard count and checks the
per-rank head count and the head size with torch._check. A
dynamic-shape compile then keeps only the token count symbolic: the
gate kernel indexes the receive buffer with constants and the output
projection reads its result directly. Head shards of any other shard
count take the unpinned path. Static compiles and eager runs are
unaffected, and the gate arithmetic is unchanged, so the outputs are
bitwise identical in both compile modes.

Signed-off-by: Xiaodong Ye <xiaodong.ye@mthreads.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants