Skip to content

Add fused pre-gated-delta-rule (GDN conv fusion) - #14

Closed
wplf wants to merge 35 commits into
jinliangl/qwen35-vl-central-devfrom
jinliangl/gdn-conv-fusion
Closed

Add fused pre-gated-delta-rule (GDN conv fusion)#14
wplf wants to merge 35 commits into
jinliangl/qwen35-vl-central-devfrom
jinliangl/gdn-conv-fusion

Conversation

@wplf

@wplf wplf commented Jun 3, 2026

Copy link
Copy Markdown
Owner

Integrates Yuzhong Wang's fused pre-gated-delta-rule (GDN conv fusion) onto jinliangl/qwen35-vl-central-dev.

Squash of the 11 GDN-fusion commits from yuzhongw-nvidia:yuzhongw/gdn_conv_fusion (b3c1c7e..05bd2da), authored by Yuzhong Wang, without the unrelated upstream dev syncs that branch also carried.

What it adds

  • megatron/core/fusions/fused_pre_gated_delta_rule.py — streamed fused pre-GDR fwd/bwd kernels.
  • megatron/core/fusions/fused_mega_pre_gated_delta_rule.py — mega-fused pre-GDR fwd/bwd kernels.
  • transformer_config.py / arguments.py--pre-gated-delta-rule-impl {unfused, fused_streamed, fused_mega}.
  • tests/unit_tests/ssm/test_gated_delta_net.py — fused-vs-unfused parity tests.

Integration notes (differs from the source branch)

central-dev already restructured the GDN forward for gdn_qkv discard-output recompute, while the source branch restructured the same forward for the fused kernels — the two are not textually mergeable. So instead of taking the source branch's forward rewrite, the fused dispatch is wired into the existing _compute_qkv_for_gated_delta_rule:

  • after the CP all-to-all, it dispatches on pre_gated_delta_rule_impl to _fused_streamed / _fused_mega (or the unfused pre_gated_delta_rule), reordering the fused (q,k,v,gate,beta,g) to this method's (q,k,v,g,beta,gate) layout;
  • the unfused body is factored into pre_gated_delta_rule(qkvzba, ...);
  • gdn_qkv / gdn_norm_out discard-output recompute is preserved unchanged (the fused path is recomputed too).

The external causal_conv1d import is also made fully optional (guarded) so importing GatedDeltaNet never fails when the package is absent; the fused backward raises a clear error only if actually invoked without it.

Testing (oci-hsg GB200, container has causal-conv1d installed)

  • TestFusedPreGatedDeltaRule + config tests: 14 passed (fused == unfused for forward / THD / padding / pre_gated_delta_rule / packed fwd+bwd / conv boundary).
  • recompute regression test_selective_recompute_gdn_qkv + test_selective_recompute_norm_out: 12 passed (gdn_qkv / gdn_norm_out unaffected across TP/SP/CP).

🤖 Generated with Claude Code

wplf and others added 30 commits May 20, 2026 10:00
Adds a standalone VLM training playground under
``examples/multimodal_dev/`` with Qwen3.5-VL end-to-end.

Highlights
- Model-agnostic entry point (``pretrain_multimodal.py``) with a
  ``MODEL_REGISTRY`` so adding a new architecture is just a registry
  entry plus a backing module.
- Qwen3.5-VL model: vision encoder, MRoPE, decoder, factory, specs,
  configurations covering proxy / 9B / 397B-A17B variants.
- Datasets: mock data and CORD-V2 VLM dataset, with THD pack/pad in the
  collate function.
- THD + CP support consolidated in ``forward_step.py`` and the model
  layer (uses MRoPE THD pre-computation and ``cu_seqlens_q_padded`` CP
  partitioning).
- Run script + README, plus tests for MRoPE parity, CP correctness, CP
  support, and THD correctness / e2e.

Also gates the torch DataLoader vanilla-collate path on the new
``use_vanilla_collate_fn`` arg (one-line change to
``megatron/training/datasets/data_samplers.py``) so CORD-V2 works under
BSHD.

Functional dependency: the new model arch sets ``mrope_interleaved=True``
in its config and relies on the core MRoPE interleaved layout introduced
in a separate PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Co-Authored-By: BestJuly <19769279+BestJuly@users.noreply.github.com>
… preprocessing

Fixes 8 issues in vlm_dataset.py found by review against Megatron-Bridge's
qwen2_5_collate_fn reference implementation.

- loss_mask off-by-one (Bug 1): the previous mask was built on input_ids
  while labels were shifted, dropping the image->text supervision signal
  at the boundary. Now masks structural tokens on the shifted labels and
  also shifts loss_mask itself left by 1.
- missing SFT prompt masking (Bug 2): user-turn and chat-template tokens
  were trained on. Now uses backward substring token search (mirroring
  create_multiturn_loss_mask_by_search) to unmask only the assistant
  answer span.
- seq_length not enforced (Bug 3): long CORD-V2 samples could overflow.
  Now end-truncates input_ids in __getitem__ with a warning.
- unsafe pad_token_id fallback (Bug 4): falling back to 0 silently masked
  a real vocab token. Now falls back to EOS and raises if neither is set.
- silent image_token_id miss (Bug 6): fallback could return None, causing
  dataset / model disagreement. Now raises ValueError.
- stale docstrings (Bug 8): updated Qwen2.5-VL / --image-size references
  to Qwen3.5-VL / --total-seq-length.
- narrow skipped_tokens set (Bug 14): vision_start/end, im_start/end,
  video_pad, endoftext were not masked on labels. Now uses
  tok.all_special_ids union {pad_id, image_token_id}.
- lost Qwen-VL dynamic resolution (Bugs 15/17/19): fixed-square resize
  removed; conversation content carries the image object;
  qwen_vl_utils.process_vision_info extracts images; processor is called
  with min_pixels / max_pixels.
- pixel_values bf16 conversion (Bug 18): moved from forward_step into the
  dataset so per-step dtype checks become no-ops.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- raise --manual-gc-interval 5 → 50 to cut GC pause frequency on long runs.
- enable --moe-permute-fusion and --moe-router-fusion in the MoE branch
  (no-op for dense variants since MOE_ARGS is gated on NUM_EXPERTS>0).
- enable grad-accumulation fusion under FSDP by dropping
  --no-gradient-accumulation-fusion from FSDP_ARGS.
- add --log-timers-to-tensorboard and --log-params-norm to surface timer
  breakdown and parameter L2 norm in TB/wandb.
- drop the hardcoded CKPT_LOAD path from the in-script example invocations
  so the comment reflects from-scratch CP correctness runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Update the 'Copyright (c) 2025, NVIDIA CORPORATION' line to 2026 across
all newly-added Python files under examples/multimodal_dev/ for the
Qwen3.5-VL training example.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…1 vs CP=4 correctness

- test_thd_e2e.py: rewrite TestPackBatch -> TestPackOrPadBatchPacked/Padded
  using the per-sample dict input shape produced by the dataset; drop
  attention_mask / position_ids / MRoPE / user cu_seqlens cases that are
  no longer the helper's concern.  Add TestPackOrPadBatchDivisibleBy4
  covering per-sample alignment when cp_size=2 forces divisible_by=4 (via
  monkeypatched mpu).

- test_thd_correctness.py: swap _pack_batch(batch_dict) for
  pack_or_pad_batch(per-sample list, use_packed_sequence=True); compute
  THD position_ids locally since the helper no longer carries them.

- test_cp_thd_correctness.py (new): single-torchrun script comparing CP=1
  and CP=4 in one process via destroy + re-initialize model_parallel,
  with weights pinned by a state_dict snapshot.  Uses MultimodalModel with
  a stub vision encoder (vision branch skipped via pixel_values=None);
  loss aggregated by AllReduce-SUM of (num, den) on the CP group;
  grad_norm aggregated by AllReduce-SUM of gradients on the CP group then
  dividing by cp_size, so each rank holds the CP-mean gradient (equivalent
  to CP=1's backward on the full-batch mean loss).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ess + small cleanups

- forward_step.py:pack_or_pad_batch — previously crashed for TP>1 because
  the per-sample loop dereferenced data on non-source ranks (which receive
  data=None from get_batch), and cu_seqlens/max_seqlen used to build
  PackedSeqParams were local Python lists never broadcast.  Now: gate the
  build loop on TP rank 0, broadcast cu_seqlens / cu_seqlens_padded as
  part of the data dict, and derive max_seqlen / total_tokens from the
  (broadcast) cu_seqlens on every rank — no extra collective.

- models/base.py — add public MultimodalModel.cp_split_loss_mask
  (staticmethod) so the post-forward loss path doesn't need to import the
  module's private _cp_split_tensor / _thd_cp_partition_index.
  forward_step.py uses it instead of duplicating the slicing logic.

- forward_step.py — replace bare `except Exception` around get_args() with
  `getattr(get_args(), 'sequence_parallel', False)` + AssertionError-only
  fallback (matches what megatron's get_args() actually raises when args
  are uninitialised in tests).  Strip three WHAT/TODO comments that
  narrated intent rather than explaining a non-obvious why.

- tests/_helpers.py (new) — shared grad_norm / mean_loss helpers.
  test_thd_correctness.py uses them in place of its local copies.
  test_cp_thd_correctness.py keeps its CP-aware _global_loss /
  _global_grad_norm (genuinely different — they add AllReduce on the CP
  group); the duplication noted in review was overstated.

- tests/test_cp_thd_correctness.py — drop _StubVisionEncoder's dummy
  nn.Linear(1,1); MegatronModule does not need a parameter for state_dict
  round-tripping (verified by re-running the CP=1 vs CP=4 suite — numbers
  identical to the previous commit).

Verified locally:
  - test_thd_e2e.py: 20/20 passed
  - test_thd_correctness.py: ALL PASSED (BSHD vs THD equal-length parity)
  - test_cp_thd_correctness.py: ALL PASSED (CP=1 vs CP=4, same numbers as
    previous commit ec6d2d3: BSHD/THD loss + grad_norm)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…(pylint 10/10)

Run `bash tools/autoformat.sh` toolchain (black --skip-magic-trailing-comma
--skip-string-normalization, isort, ruff check, pylint, mypy) directly on
the changed files (autoformat.sh only scans megatron/core + tests/, so
these don't normally go through the gate):

- black: reformat 6 files for line length / wrapping
- pylint: drop unused imports (test_thd_correctness.py: parallel_state,
  _build_packed_seq_params; test_cp_thd_correctness.py: parallel_state);
  add docstrings to 9 small functions / methods (test methods,
  _NoCPGroup.size/rank, _StubVisionEncoder.__init__/forward, main()
  entrypoints); add module-level `# pylint: disable=bad-builtin` to the
  two stdout-reporting standalone scripts (test_thd_correctness.py,
  test_cp_thd_correctness.py) where the many `print()`s are intentional.
- mypy: replace implicit Optional defaults — `seq_length: int = None` →
  `Optional[int] = None` in pack_or_pad_batch; same for `mrope_section`
  and `mtp_block_spec` in MultimodalModel.__init__; tighten
  `get_batch(data_iterator: Iterator[Dict[str, Any]])` to
  `Iterator[list[Dict[str, Any]]]` so the call to pack_or_pad_batch
  type-checks.

Remaining mypy diagnostic — `transformer_engine.pytorch` missing
library-stub marker — is repo-wide (also flagged on `megatron/core/` files
in the main run) and tolerated because autoformat.sh runs mypy with
`|| true`.

Verified locally:
  - test_thd_e2e.py: 20/20 passed
  - test_thd_correctness.py: ALL PASSED
  - test_cp_thd_correctness.py: ALL PASSED (CP=1 vs CP=4, numbers
    identical to ec6d2d3 / 4332813)
  - pylint score: 10.00/10 (was 9.48/10 → 9.95/10 → 10.00/10)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
One-line follow-up to 0211665 — `_helpers.py` was missed in the
preceding bulk lint commit's `git add`.  Black removes the spaces around
`**` (`total ** 0.5` -> `total**0.5`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… overlap profiling

Wrap the 6 execution entry points in SharedExpertMLP with nvtx_range_push/pop
so the dedicated shared-expert stream shows up as named slices in nsys
profiles. Useful for inspecting where shared-expert FFN sits relative to
the hybrid-ep dispatch A2A when --moe-shared-expert-overlap is on.
The Blackwell-FSDP path in `validate_args` auto-appends 'dp_cp' (and
'ep_dp' when EP>1) to `args.high_priority_stream_groups` whenever
`use_megatron_fsdp` / `use_torch_fsdp2` is set on `sm_10x` devices.
Combined with `--fake-process-group`, this makes `initialize_model_parallel`
call `create_group(..., pg_options=get_nccl_options(...))` with a
`ProcessGroupNCCL.Options` object for those groups. PyTorch's
`FakeProcessGroup._create_internal` rejects non-fake options with:

    TypeError: _create_internal(): incompatible function arguments.
    Invoked with: 0, 64, <ProcessGroupNCCL.Options ...>

Short-circuit `get_nccl_options` to return `None` whenever the default
process group is the fake backend, so the fake sub-groups are created
without NCCL-specific options. The change is a no-op for real backends.
…nly path

- MultimodalModel.build_schedule_plan: run vision encoder + embedding scatter
  eagerly (main path), then delegate decoder-layer schedule plan to inner
  GPTModel. Vision encoder intentionally NOT part of A2A overlap.
- multimodal forward_step: handle return_schedule_plan=True by calling
  model.build_schedule_plan(...).
- combined_1f1b: relax GPTModel isinstance assertion to also accept
  MultimodalModel (decoder-only A2A overlap).
torch.cuda.memory._snapshot() at dump time gave only segment state — empty
frames in blocks and 0 events in device_traces — because Megatron never
called torch.cuda.memory._record_memory_history() to start the recorder.

Enable the recorder at pretrain() time (before model+optimizer init),
gated on the existing --record-memory-history flag. Mode='all' captures
both segment state AND the full allocation/free event timeline with
python stacks, so a downstream mem-profile peak summary / pytorch
memory_viz can attribute each tensor to its allocation site.
Squash the MRoPE fusion changes from lit/qwen35-mrope-fusion onto jinliangl/qwen35-vl-hybridep-deploy for benchmark deployment.

Source commits: 71dc40c, d2bd427, 0885b1d, 39187d8, b529407, a604225ff, ab075bee0.
…aunch path

The fused THD dispatch (rope_utils.apply_rotary_pos_emb -> fused_apply_mrope_thd)
calls the kernel directly and only validated total seqlen % cp_size, not each
packed sub-sequence. The unfused per-sequence check in _get_thd_cp_splits() is
bypassed on the fused path, so for CP>1 variable-length packing where the total
is divisible but an individual sub-sequence is not, the kernel would silently
compute wrong local->global CP token indices (global_start // cp_size).

Add the per-sequence guard in get_fused_mrope_thd_unavailable_reason (only on the
cp_size>1 path); when it triggers, the dispatch falls back to the unfused path.
…ivisibility

- Add fwd/bwd parity at the real deployment shape head_dim=256, rotary_dim=64
  (rotary_percent=0.25, 75% pass-through) with mrope_section=[11,11,10], plus the
  non-interleaved and full-rotary variants, for both BSHD and THD. The existing
  parametrized tests only covered head_dim=16/20 with rotary_dim=16 (~80% rotated).
- Add a regression test that get_fused_mrope_thd_unavailable_reason rejects a
  packed batch whose total length is CP-divisible but an individual sub-sequence
  is not (and accepts the all-divisible control).
…n-overlap)

Switch shared_experts selective recompute from a standard checkpoint (keeps the
output) to CheckpointWithoutOutput: discard the shared-expert output in the
forward and regenerate it in backward from a grad hook. The recompute is
registered AFTER any pre_mlp_layernorm recompute (so the shared expert's input
is restored first) and before its backward — via the moe node's expert_output in
the A2A-overlap fine-grained callables, or via mlp_output_with_bias[0] in
TransformerLayer._forward_post_mlp on the single-call path. Split
CheckpointWithoutOutput.discard_output_and_register_recompute into discard_output()
+ register_recompute_hook() for the overlap path (which frees the output and
registers the hook in different callables); disabled under MoE cudagraph partial
capture.

Validated on Qwen3.5-VL 397B proxy (8xGB200, EP=8, mbs=4): both paths run with
stable loss / no NaN; controlled memory 85.94 -> 84.75 GB (-1.19 GB); 3-way
numerical parity within the run-to-run FP-noise floor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- pin checkout to 12605c5 (first correct + NaN-free DHU backward)
- use pinned submodules (warn against --remote → old buggy kernel)
- add nested-cutlass symlink fix + 4th fused extension build
- add portable-wheel build + force-reinstall flow
- document the fused dv_dhu (DV_DHU=1) wheel-import caveat + .so copy workaround
- add DHU-backward kernel version history (3a371f5→297386a→5661cfa→12605c5)
- add 397B-proxy 20-step validation (grad norm finite, matches FLA)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Recompute the GatedDeltaNet QKV projection + preparation block (in_proj -> CP
all-to-all -> conv1d -> _prepare_qkv -> g/beta) as a discard-output checkpoint,
selected via recompute_modules="gdn_qkv". The block outputs (query/key/value/
g/beta/gate) are discarded in the forward and regenerated from a grad hook in
the backward, freeing the large GDN QKV-prep activations.

When gdn_norm_out recompute is also enabled, the two discard-output checkpoints
have a forward-order data dependency (the QKV block output `gate` feeds the
gated-norm block), so both are registered to a single CheckpointManager that
replays their recompute in forward order (qkv -> norm_out) from one unified grad
hook on the layer output.

Adds "gdn_qkv" to the selective-recompute allowed_modules in TransformerConfig.
The memory-opt test lists "shared_experts" in recompute_modules but never set
moe_shared_expert_intermediate_size, so the model had no shared experts and the
shared-experts discard-output recompute path was never exercised. Set
shared_expert_intermediate_size=512 so the recompute actually fires (in both the
overlap and non-overlap paths compared by the test).
wplf and others added 5 commits June 3, 2026 17:41
Adds test_selective_recompute_gdn_qkv (mirrors test_selective_recompute_norm_out)
to TestGatedDeltaNet: builds a no-recompute baseline GatedDeltaNet and one with
recompute_modules=["gdn_qkv"], runs forward+backward, and asserts the output,
all parameter grads and the input grad match bit-for-bit. Verifies the QKV
projection+prep discard-output recompute is numerically exact.
Add gdn_qkv whole-block recompute for GatedDeltaNet
Support discard-output recompute for MoE shared experts under A2A overlap
Add optional mcore GDN optimized wrapper
Squash of yuzhongw/gdn_conv_fusion (Yuzhong Wang, 11 commits b3c1c7e..05bd2da)
onto qwen35-vl-central-dev.

- megatron/core/fusions/fused_pre_gated_delta_rule.py: streamed fused pre-GDR
  forward/backward kernels (fused_streamed_pre_gated_delta_rule).
- megatron/core/fusions/fused_mega_pre_gated_delta_rule.py: mega-fused pre-GDR
  forward/backward kernels (fused_mega_pre_gated_delta_rule).
- transformer_config.py / arguments.py: --pre-gated-delta-rule-impl
  {unfused, fused_streamed, fused_mega} (auto-generated from config).
- tests/unit_tests/ssm/test_gated_delta_net.py: packed fused GDN path tests.

Integration note: central-dev already restructured the GDN forward for gdn_qkv
discard-output recompute, so rather than the source branch's forward rewrite the
fused dispatch is wired into _compute_qkv_for_gated_delta_rule: after the CP
all-to-all it calls the fused wrapper (returning (q,k,v,gate,beta,g)) when
pre_gated_delta_rule_impl selects a fused path, reordered to the method's
(q,k,v,g,beta,gate) layout; the unfused path and the gdn_qkv/gdn_norm_out
recompute structure are unchanged.

Co-authored-by: Yuzhong Wang <yuzhongw@nvidia.com>
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.

5 participants