Skip to content

feat(distributed): block-diagonal varlen CP for packed sequences - #2989

Merged
yuhezhang-ai merged 8 commits into
mainfrom
yuhez/upstream-cp-blockdiag
Jul 22, 2026
Merged

feat(distributed): block-diagonal varlen CP for packed sequences#2989
yuhezhang-ai merged 8 commits into
mainfrom
yuhez/upstream-cp-blockdiag

Conversation

@yuhezhang-ai

@yuhezhang-ai yuhezhang-ai commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds nemo_automodel/components/distributed/blockdiag_cp/, a self-contained context-parallel
implementation for packed (multi-document) sequences, where correct masking is
block-causal per document. Softmax attention runs as a FlashAttention/TransformerEngine
varlen kernel driven by per-step precomputed cu_seqlens (zero per-layer host syncs),
with a dense masked-SDPA fallback that is numerically identical. K/V delivery across the CP
group is a fused differentiable [K;V] all-gather by default, with opt-in needed-only
exchanges (left-neighbor halo p2p, or all-to-all-v when a document spans more than two
ranks) that cut per-rank K/V from O(S) to O(S/cp) + straddle.

Part of #2985.

Scope and follow-up

This PR intentionally lands the standalone block-diagonal CP primitive and its correctness
tests. It does not modify any model, recipe, or example config. A follow-up Amber upstream
PR will wire it into the Qwen3.5 VLM path using a real configuration with context
parallelism and packed sequences, building on this PR and #2990. That integration will
attach make_cp_blockdiag_batch_and_ctx as the model's _cp_make_batch_fn and route its
attention through cp_blockdiag_sdpa.

Motivation

Sequence packing is the standard way to train VLMs/LLMs on mixed-length documents without
padding waste, and long-context packed training needs CP. The stock DTensor
context_parallel path (ring attention over load-balanced shards) assumes one causal
document per sequence; a packed sequence attended that way leaks attention across document
boundaries. TE's THD path covers input_ids-based LLM batches but not pre-embedded VLM
batches (inputs_embeds, deepstack visual embeds, mRoPE position ids).

This PR provides CP that:

  • keeps every document's attention exactly block-causal (bit-parity with the non-CP mask),
  • shards pre-embedded VLM batches (differentiable inputs_embeds slice, so gradients flow
    back to the trainable vision tower),
  • stays collective-safe under activation-checkpoint recompute and degenerate layouts (a
    rank whose whole chunk is padding must still participate in every collective — see the
    gradient-attachment tests),
  • and reuses the model-owned CP hook introduced with the existing CP-for-VLM
    infrastructure (_cp_make_batch_fn in cp_utils.make_cp_batch_and_ctx), so no changes
    to cp_utils are needed.

Design overview

blockdiag_cp/
  state.py     knob normalization (attn_backend: flash|te|dense, kv_exchange:
               allgather|halo|a2a) + AC-recompute-safe per-step state
  batch.py     make_cp_blockdiag_batch_and_ctx: pad to cp multiple, contiguous
               per-rank sequence slice of every seq-aligned tensor, per-step
               varlen-metadata precompute, train context
  runtime.py   cp_blockdiag_sdpa: drop-in SDPA; selects the KV path per step
               (with named downgrade reasons), runs varlen or dense attention;
               collective preflight + post-kernel consensus so no rank can
               diverge in collective order
  kernels.py   varlen segmentation + host-side validation of every index a
               varlen CUDA kernel consumes; flash long-left-prefix guard;
               cached TE DotProductAttention; dense [B,1,L,S] mask builder
  exchange.py  differentiable collectives: fused [K;V] all-gather (backward =
               reduce-scatter SUM), left-halo neighbor p2p, needed-only
               all-to-all-v with index_add backward
  packed.py    cp_size==1 degenerate path: same varlen kernel for packed
               sequences on one rank (row_offset=0), armed per forward and
               stable across AC recompute

Key correctness properties, each pinned by a test:

  1. Parity: per-rank block-diagonal attention concatenated over ranks equals full
    attention under the block-causal mask (fp32 CPU, and bf16 flash/TE vs dense on GPU).
  2. Boundary documents: a document straddling a rank boundary yields an asymmetric
    (K > Q) first varlen segment with bottom-right causal alignment; a guard peels it
    into a fixed-shape Flash call to avoid a known varlen failure mode on long prefixes.
  3. Collective safety: all-padding ranks keep their output attached to the gathered
    K/V (0-weighted touch) so backward reduce-scatter fires symmetrically; needed-only
    paths reach an all-reduce consensus before and after the kernel so every rank raises
    together instead of forking the collective order.
  4. Kernel-input hygiene: every cu_seqlens/offset consumed by a varlen kernel is
    validated on the host once per step (FlashAttention trusts these; a bad terminal
    offset is an async illegal memory access, not a Python exception).

Proof

  • tests/unit_tests/distributed/test_blockdiag_cp.py (CPU): mask construction,
    CP-vs-full parity for world 2/4 incl. GQA, all-gather fwd/bwd, all-pad-rank gradient
    attachment, metadata validation accept/reject, halo/a2a plan geometry, KV path
    downgrade reasons, long-prefix guard, dropout forwarding, 1-D document ids, and the
    cp1 packed hook contract.
  • tests/unit_tests/distributed/test_blockdiag_cp_varlen_gpu.py (1 GPU, skipped
    without CUDA): flash and TE varlen vs dense-mask SDPA across multi-doc packs with
    padding tails, GQA (incl. 8:1, head_dim 256), rank-straddling docs, single doc
    spanning all ranks, heavy padding; bit-identity of precomputed metadata vs inline
    segmentation; the cp_size==1 packed hook; native Flash dropout and TE's correct
    dense block-diagonal dropout fallback.
  • tests/unit_tests/distributed/test_blockdiag_cp_flash_boundary_gpu.py (1 GPU,
    skipped without CUDA): production-sized FlashAttention boundary shapes under
    checkpointed forward replay and backward.
  • tests/functional_tests/context_parallel/test_blockdiag_cp_parity.py (2 GPUs,
    scheduled L2): real NCCL forward/backward parity for dense, FlashAttention, and TE
    across all-gather, halo, and all-to-all-v exchange. It checks local outputs, input
    gradients, and every Q/K/V/output-projection parameter gradient, including a document
    straddling the rank boundary and an all-padding rank.

The implementation has been validated in internal large-scale runs (up to 128 GPUs),
including long-context (32k+) packed VLM training with FSDP2 and multi-node CP, where
loss curves match the cp_size=1 baseline.

Notes

  • The needed-only (halo/a2a) exchanges default off (kv_exchange="allgather") and
    auto-downgrade with a logged reason (non-varlen backend, missing metadata, or a CP
    group spanning nodes; NEMO_CP_ALLOW_XNODE=1 overrides for validation).

🤖 Generated with Claude Code

yuhezhang-ai and others added 2 commits July 9, 2026 08:44
…packed sequences

Add a self-contained CP implementation for packed (multi-document)
sequences where masking must stay block-causal per document, which the
load-balanced DTensor context_parallel path cannot express:

- batch: contiguous sequence sharding + per-step train context, pluggable
  via the model-owned _cp_make_batch_fn hook in cp_utils.make_cp_batch_and_ctx
- runtime: drop-in SDPA that exchanges K/V across the CP group and runs
  per-document causal attention on local queries, with collective-safe
  path selection and fallback consensus across ranks
- kernels: FlashAttention/TransformerEngine varlen kernels driven by
  per-step precomputed cu_seqlens (zero per-layer host syncs), a dense
  masked-SDPA fallback, host-side validation of all kernel indices, and
  a long-left-prefix guard for boundary documents
- exchange: differentiable K/V collectives -- fused [K;V] all-gather with
  reduce-scatter backward (default), plus opt-in needed-only delivery via
  left-neighbor halo p2p or all-to-all-v for documents spanning >2 ranks
- packed: the cp_size==1 degenerate path that routes stock SDPA calls of
  a packed sequence through the same varlen kernel
- state: knob normalization and activation-checkpointing-safe step state
  (readable from the autograd recompute worker thread)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
- CPU unit tests: block-causal mask construction, CP-vs-full attention
  parity (simulated ranks, incl. GQA), all-gather forward/backward,
  all-padding-rank gradient attachment, varlen metadata validation,
  halo/a2a exchange-plan geometry, KV path-selection downgrades, the
  flash long-prefix guard, and the cp1 packed hook contract
- 1-GPU parity tests (skipped without CUDA): flash and TE varlen outputs
  vs the dense-mask SDPA reference across multi-doc packs with padding
  tails, GQA layouts, rank-straddling documents, single documents
  spanning all ranks, and head_dim 256; bit-identity of the per-step
  precomputed metadata against inline segmentation; the cp_size==1
  packed-sequence SDPA hook

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
@copy-pr-bot

copy-pr-bot Bot commented Jul 9, 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.

@yuhezhang-ai yuhezhang-ai changed the title feat(distributed): block-diagonal varlen context parallelism for packed sequences feat(distributed): block-diagonal varlen CP for packed sequences Jul 9, 2026
yuhezhang-ai and others added 4 commits July 9, 2026 10:43
…ions

The pre-embedded-input contract check and the defense-in-depth CP-group
world-size guard vanish under python -O as asserts. Raise ValueError /
RuntimeError instead, matching the adjacent local_batch_size guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
…on hooks

enable_cp1_packed_varlen permanently monkeypatched
torch.nn.functional.scaled_dot_product_attention process-wide, leaving a
shape-heuristic dispatch latched for any coincidentally-shaped SDPA call.
Rescope it with the repo's bounded pattern (cp_utils.attach_cp_sdpa_hooks):
attach_cp1_packed_varlen_hooks installs/restores the patch via forward
pre/post hooks (always_call=True) on the checkpoint-wrapped inner self_attn
module, which also covers activation-checkpointing recompute in backward.
enable/disable now only arm the per-forward doc_ids/backend state; the
latching _PACKED_SDPA_INSTALLED global is gone.

Also shrink the package __all__ to the genuine integration entry points
(make_cp_blockdiag_batch_and_ctx, cp_blockdiag_sdpa, configure_cp_varlen,
and the cp1 packed hook API); knob normalization, the varlen metadata
precompute, and the fire counters stay module-internal (tests import them
directly). Model wiring is a follow-up PR.

Tests: new CPU unit test pins the scoping contract (process-wide SDPA
untouched, patch live inside hooked forwards and during AC recompute,
restored after); the GPU cp1 parity test now runs the full hook chain
through a hooked module instead of the global patch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
… helpers

Replace the bare try/except import probes for flash-attn and
TransformerEngine with module-level safe_import_from / safe_import_te from
nemo_automodel.shared.import_utils, per the repo's optional-dependency
convention. The kernel symbols themselves are still imported at the call
sites so per-call stubs (tests) and lazy TE extension loading keep working.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
Add the first real-process-group coverage for the differentiable K/V
collectives (_AllGatherSeqDiff, _LeftHaloExchange, _NeededKVExchange) and
the halo/a2a attention paths: a 2-rank NCCL torchrun runner
(run_blockdiag_cp_2rank.py, launched from pytest via
L2_CP_BlockDiag_Varlen_Test.sh) drives the full production path
(configure_cp_varlen -> make_cp_blockdiag_batch_and_ctx on a real
DeviceMesh -> cp_blockdiag_sdpa) forward + backward and compares local
outputs, the input-embedding grad slice, and cross-rank-summed
per-parameter grads of a tiny q/k/v/o attention module against a
single-process dense block-causal reference.

Matrix: kv_exchange allgather / halo / a2a (both needed-only paths
selectable at cp=2 via the knob; the selector's decision is pinned so a
silent downgrade to all-gather cannot fake coverage) x backend dense
(fp32), flash, TE (bf16), over three doc layouts: a document straddling
the rank boundary, an entire rank chunk of padding (collective no-hang),
and a single document spanning both ranks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
@yuhezhang-ai

Copy link
Copy Markdown
Contributor Author

/ok to test abb90e2

…diagnostics

Add a single-GPU shape-stress sweep for the block-diagonal CP FlashAttention
boundary guard. The sweep drives production-sized query/key segment lengths that
bracket FlashAttention tile boundaries plus maximally packed layouts with
non-tile-aligned tails, each through a non-reentrant activation checkpoint that
replays the attention forward during backward, and asserts finite forward and
gradient outputs. Explicit synchronizations pin any delayed illegal-address
report to the exact case that launched the offending kernel.

Carry compact host-only forensic context so an asynchronous kernel fault named
at the post-exchange consensus can report the exact shape without adding a
hot-path synchronization: the varlen validator retains a validation snapshot of
its shape metadata, the needed-only halo/all-to-all exchanges record a
diagnostic describing the chosen path, and the collective success check prints
that diagnostic to stderr before re-raising an async CUDA error.

Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
@yuhezhang-ai

Copy link
Copy Markdown
Contributor Author

/ok to test dff34c2

Signed-off-by: Yuhe Zhang <yuhez@nvidia.com>
@yuhezhang-ai

Copy link
Copy Markdown
Contributor Author

/ok to test 1e3b7c9

@yuhezhang-ai
yuhezhang-ai merged commit 53ef40b into main Jul 22, 2026
83 checks passed
@yuhezhang-ai
yuhezhang-ai deleted the yuhez/upstream-cp-blockdiag branch July 22, 2026 18:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants