Skip to content

[Feature][Diffusion] Compose Ulysses with AllGather-KV as a 2D SP topology, with MiniMax-H3 NPU bring-up fixes - #7937

Draft
brandneway wants to merge 4 commits into
vllm-project:mainfrom
brandneway:deploy/h3-usp-cp-16card
Draft

brandneway wants to merge 4 commits into
vllm-project:mainfrom
brandneway:deploy/h3-usp-cp-16card

Conversation

@brandneway

Copy link
Copy Markdown
Contributor

Purpose

Bring up the Ulysses × AllGather-KV 2D diffusion SP topology and the MiniMax-H3 NPU serving path on top of it. This PR contains one feature commit cherry-picked from @dengyunyang's cpandusp branch (authorship preserved) plus three bugfixes discovered while bringing up the topology on Ascend NPU (16×950PR, MiniMax-H3 FL2VA).

1. Ulysses × AllGather-KV 2D topology (cherry-picked from @dengyunyang's cpandusp)

Composes Ulysses (head sharding via all-to-all) with AllGather-KV (sequence gathering) as an orthogonal 2D SP topology:

Ulysses all-to-all (U group):  Q, K, V → [B, S / A,     H / U, D]
K/V AllGather     (A group):   K, V    → [B, S,         H / U, D]
local-Q / global-KV attention: O       → [B, S / A,     H / U, D]
reverse Ulysses all-to-all:    O       → [B, S / (U × A), H,    D]

The kernel always runs a local-Q/global-KV problem with a single, ordered global K/V tensor — which matters for attention backends that need stable global K/V indexing (e.g. sparse-mask generation), unlike Ring Attention. It also avoids the head-padding tax of large pure-Ulysses degrees: MiniMax-H3 has 56 attention heads, so usp16 needs advanced_uaa padding 56→64 (+14.3% attention work) while usp8+cp2 divides evenly.

  • UlyssesAllGatherKVParallelAttention reuses the Ulysses strategy with defer_joint=True (joint tensors stay head-sliced in metadata and are re-attached after the K/V gather — attaching earlier would replicate joint K/V once per AllGather rank).
  • build_ulysses_allgather_rank_groups builds the orthogonal groups (contiguous Ulysses blocks × stride-U AllGather groups; position p → ulysses_rank = p % U, allgather_rank = p // U).
  • sequence_parallel_size = ulysses × ring × allgather; allgather composes with ulysses but not with ring.
  • Includes the commit's tests (orthogonal group layout, collective ordering mocks, config validation) and the dense compute-vs-comm benchmark benchmarks/diffusion/bench_ulysses_cp_topology.py.

2. [Bugfix] Default SP size derivation (omni_engine_base.py)

The default diffusion stage config still derived sequence_parallel_size under the v1 exclusivity rule (allgather_degree when allgather>1, else ulysses×ring). With the composed topology this contradicts the triple-product validation and fails startup (e.g. usp8+cp2 on 16 devices: asserted 16, computed 8).

3. [Bugfix][MiniMax-H3] Text-encoder TP group tiling

_build_text_encoder_group built a single group covering only the first text_encoder_tp_size DiT ranks; GroupCoordinator.__init__ asserts every constructing rank belongs to a group, so tp_size < world (e.g. tp=8 on 16 devices — the only legal topology for Qwen3-VL's 64 Q / 8 KV heads) crashed the remaining ranks at startup. The DiT world is now tiled into world / tp_size contiguous groups; only the first group encodes, and its output still reaches all DiT ranks through the existing world-group broadcast. tp_size == world is unchanged.

4. [Bugfix][NPU] Packed varlen contract generalized to local-Q/global-KV

Under AllGather-KV SP the backend sees local Q against global K/V, so the square packed contract (used_q <= total_q) declined and the mask-rebuild fallback rejected the global valid_kv_length against the local query length — every packed H3 request failed with "packed metadata is unusable". _resolve_packed_seq_npu now accepts the query_ranges recorded by the AllGather-KV strategy: a single packed range means the local Q rows are a contiguous slice of the same global real document, so seq_q becomes the slice's [local_real, local_total] pair while seq_k keeps the global [real, pad] pair — same "pad is a strict suffix" contract, no mask materialized. Square (non-SP) behavior is unchanged; the mask fallback now checks valid_kv_length against the K/V domain (key.shape[1]) as the safety net.

Test Plan

  • Existing tests from the cherry-picked topology commit: tests/diffusion/distributed/test_parallel_state_sp_groups.py (orthogonal layout), tests/diffusion/attention/test_attention_sp.py (collective ordering mocks), tests/config/test_omni_config.py (triple-product validation).
  • New CPU unit tests: tests/diffusion/models/minimax_h3/test_minimax_h3_encoder_group.py (encoder group tiling), tests/diffusion/attention/test_flash_attn.py group A2 (per-rank packed boundary resolution under AllGather-KV, unsupported-range rejection, square-contract preservation).

Test Result

  • pytest tests/diffusion/distributed/test_parallel_state_sp_groups.py: 28 passed.
  • pytest tests/diffusion/attention/test_flash_attn.py -k "packed_seq or cp_": 16 passed.
  • NPU serving validation (Ascend 950PR, MiniMax-H3 FL2VA): 4-card usp2+cp2 end-to-end video generation verified, with outputs size-identical to the pure-Ulysses baseline under the same seed; 16-card usp8+cp2 / usp16+advanced_uaa bring-up in progress.

Supersedes #7889.

brandneway and others added 4 commits September 20, 2026 17:01
… world works

The encoder TP group was built as a single group covering only the first
text_encoder_tp_size DiT ranks via init_world_group. GroupCoordinator
asserts that every constructing rank belongs to one of its groups, so with
tp_size smaller than the DiT world (e.g. tp=8 on 16 NPUs, the only legal
topologies for Qwen3-VL's 64 Q / 8 KV heads) every rank outside [0, tp)
failed the assert at startup.

Tile the DiT world into dit_world/tp_size contiguous groups instead. Every
rank holds membership in exactly one group; only the first group encodes,
and its output still reaches all DiT ranks through the existing world-group
broadcast. tp_size == world yields the same single group as before, so the
existing deployments are unchanged.

Signed-off-by: brandneway <gyuan4892@gmail.com>
Under AllGather-KV sequence parallelism (allgather_degree > 1), the
attention backend sees local Q against global K/V, so the square packed
contract (used_q <= total_q) declines and the mask fallback then rejects
the global valid_kv_length against the local query length, refusing every
packed request with 'packed metadata is unusable'.

_resolve_packed_seq_npu now accepts the query_ranges recorded by the
AllGather-KV strategy: a single packed range means the local Q rows are a
contiguous slice of the same global real document, so seq_q becomes the
slice's [local_real, local_total] pair while seq_k keeps the global
[real, pad] pair — the same 'pad is a strict suffix, pad rows are never
consumed' contract, no mask materialized. Both packed NPU paths (varlen,
laser prefix-slice) forward the ranges; their square behavior is unchanged.

The mask rebuild fallback keeps working as the safety net for multi-range
layouts by checking valid_kv_length against the K/V domain (key.shape[1])
instead of the local query length.

Signed-off-by: brandneway <gyuan4892@gmail.com>
…logy

AllGather-KV was mutually exclusive with Ulysses and Ring. Allow it to be
composed with Ulysses, forming two orthogonal process-group dimensions:

    sequence_parallel_size = ulysses_degree * ring_degree * allgather_degree

The initial scope requires ring_degree == 1 and non-causal attention; composing
all three dimensions at once stays unsupported.

Per rank the tensors start at [B, S/(U*A), H, D] and the attention is:

    Ulysses all-to-all (U group):  Q, K, V -> [B, S/A, H/U, D]
    K/V AllGather     (A group):   K, V    -> [B, S,   H/U, D]
    local-Q / global-KV attention
    reverse Ulysses all-to-all

The collective order is load-bearing: gathering the pre-transform shards would
concatenate the strided head layout of different regions and permute the
sequence. The rank layout mirrors it -- Ulysses groups are contiguous blocks of
U ranks and AllGather-KV groups stride by U -- so the flat contiguous sharding
produced by sp_shard makes each Ulysses group hold one contiguous region and the
AllGather rebuild the global sequence in order. The attention backend therefore
keeps seeing a plain local-Q/global-KV problem, which is what sparse-attention
backends such as FastH3 VSA need; Ring keeps K/V block-local and owns
distributed attention itself.

UlyssesAllGatherKVParallelAttention reuses both halves instead of reimplementing
them. It runs the Ulysses strategy with defer_joint=True, which reshards the
image Q/K/V and records the head-sliced joint query/key/value in the attention
metadata rather than concatenating them into K/V, then delegates to the
AllGather-KV path to gather K/V and re-attach the joint tensors, and finishes
with the Ulysses reverse transform.

Unsupported combinations fail closed with actionable errors rather than falling
back to dense attention: ring_degree > 1, causal attention, 2D key masks, uneven
per-rank regions, Scheduler-paged KV, and TRTLLM_ATTN.

Signed-off-by: dengyunyang <584797741@qq.com>
(cherry picked from commit 547bfa7)
…ather

The default diffusion stage config still derived sequence_parallel_size
under the v1 exclusivity rule (allgather_degree when allgather_degree>1,
else ulysses_degree*ring_degree). With the composed Ulysses x AllGather-KV
topology this contradicts the triple-product validation and fails startup
(e.g. usp8+cp2: asserted 16, computed 8). Use the triple product here too.

Signed-off-by: brandneway <gyuan4892@gmail.com>
@vllm-omni-review-bot

Copy link
Copy Markdown

This PR appears to belong to: docs/design/module/diffusion/index.md, docs/design/module/diffusion/parallelism.md.

Module owners: @princepride @xuechendi @david6666666

Routing: @princepride via module of the changed files, module named in the PR description, semantic router, CODEOWNERS; @xuechendi via module of the changed files, module named in the PR description, semantic router; @david6666666 via module of the changed files, module named in the PR description, model owner

@brandneway, please review your own changes and leave a short self-review comment describing what you checked. PRs without author self-review may not be assigned a reviewer.

Please take a look when you have a chance. If you would like an automated review, mention @vllm-omni-review-bot in a comment.

@vllm-omni-review-bot

Copy link
Copy Markdown

Omni ReviewBot triage note

Automated triage of commit 7973c3562c2c produced:

  • Priority: high. Prompt maintainer attention is suggested.

These are automated triage suggestions only — the final decision belongs to the maintainers.

@hsliuustc0106 hsliuustc0106 added diffusion codes related to diffusion models enhancement New feature or request NPU PR related to Ascend NPU high priority high priority issue, needs to be done asap labels Sep 22, 2026
@vllm-omni-review-bot

vllm-omni-review-bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Omni ReviewBot: resolved

The CI failure noted on 7973c3562c2c no longer applies.

@Dong1017

Copy link
Copy Markdown
Contributor

Thanks @brandneway and @Bounty-hunter. #7641 introduces the shared dense Ulysses × AllGather-KV topology and reports HunyuanImage3.0 DiT-only validation. #7937 cherry-picks its feature commit (547bfa7 → 00d2aaf), including the tests and benchmark, and adds MiniMax-H3 NPU fixes and validation. The two PRs therefore carry one shared implementation with distinct model evidence; #7937 explicitly supersedes #7889, but I have not seen a decision about #7641.

Could you and the maintainers confirm which PR should carry the shared topology and the merge order? If #7641 lands first, #7937 could rebase to its MiniMax-H3/NPU fixes and tests. If #7937 is the integration PR, the cherry-picked commit already preserves #7641's authorship and tests; please confirm how its HunyuanImage3.0 validation should be referenced and what should happen to #7641.

One separate code detail: 7973c35 edits an earlier _create_default_diffusion_stage_cfg definition that is overridden by a later definition delegating to StageConfigFactory. The active factory path already derives the three-degree product when SP size is omitted. Was this commit intended for another active path, or should the inactive change be adjusted or dropped?

@vllm-omni-review-bot

Copy link
Copy Markdown

Omni ReviewBot: no human activity for 7 days

@brandneway this pull request has had no human commit, comment or review since 2026-09-23. Please confirm the current plan and next step. The author or a maintainer decides whether to change the PR state.

To keep it moving, any one of these is enough: push an update, reply to the open blocker, or post the current plan and timeline.

@hsliuustc0106

Copy link
Copy Markdown
Collaborator

@brandneway this PR is labeled ready + high priority, but CI is failing on the latest commit:

Could you please take a look and push an update to get CI green? Once the checks pass we can proceed with review/merge. Thanks!

@vllm-omni-review-bot

Copy link
Copy Markdown
Omni ReviewBot routing record

Assigned Strict on cursor (cursor-grok-4.6-high) under experiment fleet-strict-cursor-grok46-zcode-glm53flash-5050-c5-z10-20261002.

value: torch.Tensor,
attn_metadata: AttentionMetadata | None,
):
if attn_metadata is not None and attn_metadata.attn_mask is not None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Qwen-Image passes its text padding mask in joint_attn_mask. With defer_joint=True, the Ulysses mask merge is skipped and AllGather ignores that field, so padded text keys become visible. Could we merge it after gathering, or reject it here too?

# inherited AllGather-KV path slice the attention metadata, prepend
# the joint tensors, and build the local-Q/global-KV view.
self._assert_equal_region_lengths(key.shape[1], key.device)
query, key, value, attn_metadata, _ = super().pre_attention(query, key, value, attn_metadata)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MiniMax-H3 on CUDA still passes global cu_seqlens_q to flash_attn_varlen_func. Here Q has only S / allgather_degree rows, but the final query offset remains S. Please remap the packed query boundaries using query_ranges, or reject this backend/topology combination before calling the kernel.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

diffusion codes related to diffusion models enhancement New feature or request high priority high priority issue, needs to be done asap NPU PR related to Ascend NPU

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants