Skip to content

feat(dllm): add Transformer Engine context parallelism for DiffusionGemma - #3651

Open
akoumpa wants to merge 7 commits into
mainfrom
akoumpa/feat/diffusion-gemma-te-cp
Open

feat(dllm): add Transformer Engine context parallelism for DiffusionGemma#3651
akoumpa wants to merge 7 commits into
mainfrom
akoumpa/feat/diffusion-gemma-te-cp

Conversation

@akoumpa

@akoumpa akoumpa commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add model-owned context-parallel sharding for DiffusionGemma encoder and diffusion-canvas streams
  • run causal encoder attention and bidirectional diffusion attention through Transformer Engine context parallelism
  • compute encoder cross-entropy on local hidden-state shards and align diffusion targets with the canvas CP layout
  • preserve the encoder objective as a mean of per-example sequence means under data, gradient-accumulation, and context parallelism
  • add a streamed long-context dataset generator and a two-node 100K-token example

Loss semantics

The diffusion decoder remains non-causal. Its exact block-attention mask is supplied as a post-scale attention bias while Transformer Engine performs context-parallel attention. Padding and dummy cache positions are excluded from the objective.

The encoder AR term first averages valid next-token CE within each example, then averages those per-example means over the global batch. Each CP rank contributes only its local token numerator while using the replicated full-sequence token count. The existing DP x CP backward scaling combines those disjoint contributions into the same objective as the non-CP path. Empty examples are excluded from the denominator.

Validation

  • 85 focused loss unit tests passed; unequal lengths, empty examples, gradient accumulation, and DP-global denominators are covered
  • Ruff, compile, and whitespace checks passed
  • cw-dfw Slurm 16555164: two nodes, four H100s, Transformer Engine CP=4, completed successfully
  • real model encoder loss: 19.021338 with CP versus 19.028961 from an independent full-logit oracle (0.0401% relative difference)
  • real model Q-projection gradient cosine: 0.99997425
  • unequal-length fused reduction [17, 7]: loss 6.396856 versus 6.406250 and LM-head gradient cosine 0.99997294
  • 100,000-token Transformer Engine CP forward/backward completed successfully

Signed-off-by: Alexandros Koumparoulis <akoumparouli@nvidia.com>
@akoumpa
akoumpa requested a review from a team as a code owner August 25, 2026 02:16
@copy-pr-bot

copy-pr-bot Bot commented Aug 25, 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.

@akoumpa

akoumpa commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

Multinode Transformer Engine loss parity completed on cw-dfw (Slurm 16548495, 2 nodes x 2 H100s, CP=4). The comparison used identical initialized weights, token IDs, masks, BF16 precision, and a 1,024-token encoder / 256-token bidirectional canvas. Both paths ran forward and backward.

Loss no CP CP=4 absolute difference relative difference
Total 29.39624786 29.39549255 0.00075531 0.00257%
Diffusion canvas 0.67053974 0.67070723 0.00016749 0.02498%
Encoder AR 28.72570801 28.72478294 0.00092506 0.00322%

The small BF16 differences are consistent with the different distributed attention/reduction order; the loss is numerically equivalent.

  • no-CP W&B run
  • CP=4 W&B run
  • remote log: /lustre/fsw/portfolios/coreai/projects/coreai_dlalgo_llm/users/akoumparouli/what/codex_remote_work/diffusion_gemma_te_cp/dg_cp_loss_wb_16548495.log

@akoumpa

akoumpa commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

Red-team loss validation

Status: the TE context-parallel loss and gradients pass an independent oracle; one pre-existing objective-reduction caveat remains.

Independent checks:

  • Explicit PyTorch log-softmax/gather CE: four disjoint CP partitions sum to the exact unpartitioned loss and gradient (zero numerical error in float64).
  • Multinode TE CP=4: cw-dfw job 16551110, 2 nodes x 2 H100, completed successfully.
  • Tested a later canvas (canvas 1 of 3), not only canvas 0. Mutating all future clean/noisy tokens changed its logits by exactly 0; mutating the previous clean canvas changed them substantially.
  • The full training mask matched an independently isolated prompt + previous-canvas forward exactly (max error 0).
  • FP32 native isolated-canvas logits matched the eager reference at cosine 0.99999994 and 0.043% relative mean error.
  • TE CP versus non-CP SDPA: combined diffusion + AR loss was 9.679240 vs 9.680479 (0.0128% relative); selected logits cosine was 0.999761.
  • Summed CP gradients versus non-CP explicit-CE gradients: Q 0.998440, K 0.998003, O 0.998551, tied embedding/LM head 0.998361 cosine.

The diffusion term matches the documented objective: plain CE over every supervised token in the selected canvas, including uncorrupted tokens, with no inverse-noise weighting. Since selected canvases are fixed-width after EOS fill, token reduction and per-example reduction coincide for this term.

One caveat is unrelated to CP: the encoder AR term currently reduces as sum(CE) / sum(valid tokens), while Eq. 13 specifies a per-example sequence mean followed by a batch mean. These are identical for equal-length examples but weight longer examples more heavily in variable-length batches. An exaggerated 11/5/2-token oracle case differed by 7.67%. CP preserves the implemented token-weighted objective correctly, but we should not describe the encoder term as exact Eq. 13 unless that weighting is an explicit design choice.

Signed-off-by: Alexandros Koumparoulis <akoumparouli@nvidia.com>
@akoumpa

akoumpa commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test 4c50835

@akoumpa

akoumpa commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

Encoder-loss equivalence follow-up

The variable-length reduction caveat is resolved in 4c508358d2b174a12838490f087bc8897f75dcc6.

The encoder objective is now:

mean_over_nonempty_examples(sum(valid next-token CE for example) / valid pair count for example)

Under CP, every rank divides its local numerator by the replicated full-sequence pair count. The recipe uses the DP-global nonempty-example count across gradient-accumulation microbatches, and the existing DP x CP backward scaling combines the disjoint token shards.

Independent validation:

  • cw-dfw Slurm 16555164, 2 nodes x 2 H100s, TE CP=4, completed successfully
  • real model CP loss 19.021338 versus independent full-logit PyTorch loss 19.028961 (0.0401% relative)
  • real model Q-projection gradient cosine 0.99997425
  • unequal valid lengths [17, 7]: fused distributed loss 6.396856 versus explicit PyTorch 6.406250; LM-head gradient cosine 0.99997294
  • 100,000-token TE-CP forward/backward completed, with 25,004 local encoder positions per rank
  • 85 focused loss tests pass, including unequal lengths, an empty example, gradient accumulation, and the DP-global denominator

Matched W&B rerun (cw-dfw Slurm 16556014, 2 nodes x 2 H100s, completed):

Loss no CP CP=4 relative difference
Total 29.39624786 29.39549255 0.00257%
Diffusion canvas 0.67053974 0.67070723 0.02498%
Encoder AR 28.72570801 28.72478676 0.00321%
  • no-CP W&B run
  • CP=4 W&B run
  • remote parity log: /lustre/fsw/portfolios/coreai/projects/coreai_dlalgo_llm/users/akoumparouli/what/codex_remote_work/diffusion_gemma_te_cp/dg_cp_equiv_only_16555164.log
  • remote W&B log: /lustre/fsw/portfolios/coreai/projects/coreai_dlalgo_llm/users/akoumparouli/what/codex_remote_work/diffusion_gemma_te_cp/dg_cp_loss_wb_16556014.log

@akoumpa

akoumpa commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

100K-token CP training curve

The 100K validation now has a real multi-step W&B history rather than a one-point snapshot:

  • TE CP=4, 100K tokens, 10 optimization steps
  • cw-dfw Slurm 16590283, 2 nodes x 2 H100s, completed successfully
  • W&B persisted 10 history rows (step=0 through step=9)
  • encoder AR loss decreased monotonically from 28.709860 to 27.937515 (2.69%)
  • global gradient norm remained finite and decreased to 0.713014
  • each rank processed 25,004 padded encoder positions under TE CP=4
  • source commit: 4c508358d2b174a12838490f087bc8897f75dcc6

This uses the same one-layer tiny DiffusionGemma and real model/TE-CP/fused-CE path as the 100K correctness probe. It performs synchronized AdamW updates: CP-local loss-shard gradients are summed across all four ranks before every optimizer step. The plotted objective is the encoder AR loss; the diffusion-canvas loss is not included in this long-context curve.

Remote log: /lustre/fsw/portfolios/coreai/projects/coreai_dlalgo_llm/users/akoumparouli/what/codex_remote_work/diffusion_gemma_te_cp/dg_cp_100k_curve_16590283.log

Signed-off-by: Alexandros Koumparoulis <akoumparouli@nvidia.com>
@akoumpa

akoumpa commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Updated every dLLM diffusion training recipe to retain FP32 parameters (distributed.mp_policy.param_dtype: float32) while keeping BF16 compute through autocast_dtype: bfloat16.

Validation:

  • discovered and linted all 14 Diffusion*Recipe YAMLs
  • test_diffusion_recipe_precision.py: 1 passed
  • ruff check / ruff format --check: passed
  • git diff --check: passed

A focused regression test now prevents future diffusion recipes from reverting the master-parameter precision contract.

@akoumpa

akoumpa commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test 747f423

akoumpa added a commit that referenced this pull request Aug 26, 2026
diffusion_gemma_sft.yaml and diffusion_gemma_lora.yaml pinned
`dispatcher: torch`. Both run ep_size=8 on a single node, so this forced the
DTensor fallback even on DeepEP-capable images such as the CI container.

At ep_size=8 that fallback runs, for every MoE layer on every forward pass: a
token-count all_gather, an `[int(t.item()) ...]` host sync, and four
variable-length all_gathers (activations, router weights, indices, token mask)
that replicate every token to all 8 ranks, then an all-reduce to combine.
DiffusionGemma runs the shared stack three times per step (causal encoder,
no_grad self-conditioning decode, real decode) and activation-checkpoint
recompute replays the dispatch again in backward.

Measured with ntrace on 8xH100 (google/diffusiongemma-26B-A4B-it), per step
per rank: 59,482 kernel launches, 1,184 NCCL collectives holding 97.4% of GPU
kernel time, 99.9% of that communication exposed (1,091 of the 1,184 are
issued on the same CUDA stream as compute, so overlap is impossible), 2,175
blocking device-to-host copies, and only 528 ms of compute-kernel time of
which 73 ms is GEMM.

Benchmarked on eos, 8xH100, 8 steps, shipped recipe with only this change:

  | dispatcher      | step    | throughput  | peak mem |
  |-----------------|---------|-------------|----------|
  | torch (before)  | 15.7 s  | 263 tok/s   | 59.0 GiB |
  | deepep (after)  | 10.6 s  | 388 tok/s   | 59.2 GiB |

1.47x faster steps at equal memory. Loss trajectories match to within
floating-point reassociation (step 0: 4.9946 vs 4.9583), as expected for a
change that alters only how tokens are exchanged.

Set explicitly rather than left unset: the value is visible in the recipe, and
BackendConfig's default would otherwise resolve differently per image.

"hybridep" was benchmarked as the alternative and is not a drop-in for this
recipe -- it fails with CheckpointError because the tokens routed to a rank
differ between forward and activation-checkpoint recompute (481 vs 1087 rows),
even though ignore_router_for_ac already defaults to True.

The diffusion_gemma_te_cp_100k.yaml recipe added by #3651 is deliberately left on `dispatcher: torch`:
it runs ep_size=1 across two nodes, and the dispatcher branch in
moe/layers.py keys off world size rather than ep_size, so switching it would
activate the DeepEP path on an untested internode configuration.

Signed-off-by: Alexandros Koumparoulis <akoumparouli@nvidia.com>
akoumpa added a commit that referenced this pull request Aug 26, 2026
diffusion_gemma_sft.yaml and diffusion_gemma_lora.yaml pinned
`dispatcher: torch`. Both run ep_size=8 on a single node, so this forced the
DTensor fallback even on DeepEP-capable images such as the CI container.

At ep_size=8 that fallback runs, for every MoE layer on every forward pass: a
token-count all_gather, an `[int(t.item()) ...]` host sync, and four
variable-length all_gathers (activations, router weights, indices, token mask)
that replicate every token to all 8 ranks, then an all-reduce to combine.
DiffusionGemma runs the shared stack three times per step (causal encoder,
no_grad self-conditioning decode, real decode) and activation-checkpoint
recompute replays the dispatch again in backward.

Measured with GPU kernel tracing on 8xH100 (google/diffusiongemma-26B-A4B-it), per step
per rank: 59,482 kernel launches, 1,184 NCCL collectives holding 97.4% of GPU
kernel time, 99.9% of that communication exposed (1,091 of the 1,184 are
issued on the same CUDA stream as compute, so overlap is impossible), 2,175
blocking device-to-host copies, and only 528 ms of compute-kernel time of
which 73 ms is GEMM.

Benchmarked on eos, 8xH100, 8 steps, shipped recipe with only this change:

  | dispatcher      | step    | throughput  | peak mem |
  |-----------------|---------|-------------|----------|
  | torch (before)  | 15.7 s  | 263 tok/s   | 59.0 GiB |
  | deepep (after)  | 10.6 s  | 388 tok/s   | 59.2 GiB |

1.47x faster steps at equal memory. Loss trajectories match to within
floating-point reassociation (step 0: 4.9946 vs 4.9583), as expected for a
change that alters only how tokens are exchanged.

Set explicitly rather than left unset: the value is visible in the recipe, and
BackendConfig's default would otherwise resolve differently per image.

"hybridep" was benchmarked as the alternative and is not a drop-in for this
recipe -- it fails with CheckpointError because the tokens routed to a rank
differ between forward and activation-checkpoint recompute (481 vs 1087 rows),
even though ignore_router_for_ac already defaults to True.

The diffusion_gemma_te_cp_100k.yaml recipe added by #3651 is deliberately left on `dispatcher: torch`:
it runs ep_size=1 across two nodes, and the dispatcher branch in
moe/layers.py keys off world size rather than ep_size, so switching it would
activate the DeepEP path on an untested internode configuration.

Signed-off-by: Alexandros Koumparoulis <akoumparouli@nvidia.com>
Signed-off-by: Alexandros Koumparoulis <akoumparouli@nvidia.com>
@akoumpa

akoumpa commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Fixed the CP=8 decoder NaN at exact head c4dbf8b84e4e233c4d443b78d964ef91df401b78.

TE P2P context parallelism evaluates K/V blocks incrementally, so an intermediate block can be fully masked even when the complete attention row is valid. Using the dtype minimum for that block produced a non-finite partial softmax. The additive mask now uses -1e4, which underflows masked BF16/FP16 probabilities to zero while keeping each partial block finite.

Validation on cw-dfw:

  • Slurm 16630080: CP=8, 4096 tokens, 256 canvas tokens, FP32 resident parameters + BF16 autocast; finite forward, backward, optimizer step, and gradient norm.
  • Slurm 16630260: 3 focused tests passed, covering BF16 TE Q/K/V under FP32 resident parameters, finite CP mixed-stream mask layout, and all diffusion-recipe precision invariants.

I am now running the final matched 10-step CP=1/CP=8 W&B comparison at this head.

@akoumpa

akoumpa commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test c4dbf8b

@akoumpa

akoumpa commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Final exact-head cw-dfw validation completed for c4dbf8b84e4e233c4d443b78d964ef91df401b78 (Slurm 16630433, 1 node / 8 H100s, 4096 sequence tokens, 256 diffusion-canvas tokens, 10 optimizer steps, identical seeds/data/model):

Both runs are finished and each W&B history contains 10 loss rows. CP=1 vs CP=8 total-loss parity:

  • step 0: 29.4225101 vs 29.4224682 (absolute delta 4.20e-5, relative 1.43e-6)
  • maximum over 10 steps: absolute delta 0.0223770, relative delta 0.0792%
  • step 9: 26.9868679 vs 26.9929066 (absolute delta 0.0060387, relative 0.0224%)

AR maximum relative delta is 0.0720%. DLLM maximum absolute delta is 0.01103; because that component is only about 0.1, its maximum relative delta is 8.81%. The aggregate objective tracks to below 0.08% throughout, consistent with BF16 attention/reduction-order differences.

The runtime dtype audit also verifies FP32 master/resident parameters do not make the whole model run in FP32. In both runs:

  • resident parameter: FP32
  • Q projection output: BF16
  • TE Q/K/V: BF16 / BF16 / BF16
  • attention output projection: BF16
  • LM-head matmul output: BF16
  • CP=8 fused CE operands: BF16 / BF16
  • final canvas logits: FP32 (intentional loss-stability boundary)

So FP32 is retained for master parameter storage while the expensive linear, TE attention, and fused CE compute remains BF16.

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.

1 participant