Signed-off-by: aoshen02 <aoshen@inferact.ai> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
42 KiB
Training-side model support
How to build a training implementation cheaply and prove it computes the same math as inference. Where training weights come from (master -> training forward weights) is in Precision, quantization and QAT.
Three routes
The routes differ in where parallelism and kernels come from. In all three we write the model.
| MCore (Megatron-Bridge) | FSDP2 (VERL's HF interface path) | mLite | |
|---|---|---|---|
| Positioning | Mature TP/PP/EP/CP, main production RL path | Cheapest: write only the model, no parallelism | Built for consistency: forward calls vLLM kernels, backward reuses the training implementation |
| Forward source | MCore kernels (TE, grouped GEMM) | Our PyTorch model to vLLM semantics, in the Transformers PreTrainedModel interface for the VERL loader and PEFT |
vLLM kernels (visible_forward(visible, native, ...)) |
| Achievable consistency | BI=0, via R3 + importance sampling | BI=0 | BI=1, logprob raw bytes identical |
| What to write for a new model | Bridge weight mapping + modules MCore lacks (Mamba/SSM, special norms) | Whole model: config class, checkpoint loading (with dequantization), per-layer forward, per-operator precision contract; autograd backward, filled in where missing | One model/<name>/: config, checkpoint mapping, assembly, protocol; modules from primitive/modules |
| Typical pitfalls | Mapping checks values, not semantics; MCore and vLLM math differ (MoE activation clamp) | bf16 autocast around the VERL forward rewrites module precision; --system-site-packages ABI; PyTorch reference (Mamba scan) too slow; loading a third-party implementation from the image by mistake |
Private modules inside the model directory, bypassing the shared skeleton |
Choose after the module inventory, by: is BI=1 needed, how many backwards are missing, what parallelism is needed. Nemotron looked cheap by name; FA4, Mamba scan and W4A16 MoE all lacked a backward.
Find by symptom
| Symptom / question | Where to go |
|---|---|
| New model loads and config parses, but unclear whether it can train | Module inventory |
| Want to reuse vLLM forward code, but tangled in paged KV / cudagraph / scheduling | Call kernels only |
| Weight mapping shapes are right, output silently degrades | Mapping checks values and semantics |
| Inference kernel has no backward; should we write one | Three-case decision |
| Wrote a VJP; how to prove it is right | Module-level comparison steps |
| Unit tests pass but real training fails / step 0 occasionally not bitwise | Two test-infrastructure false negatives; BI kernels |
| Changes after modifying packing or CP/TP/EP; wrong only on long sequences | Layout and length |
ppo_kl non-zero after rollout switches parallel layout (e.g. DP+EP to TP1 per GPU) |
Bitwise identity after changing the parallel layout |
modified by an inplace operation after no longer recomputing old-logprob |
Entropy when old-logprob is not recomputed |
| How to hook into VERL / Megatron | Framework integration |
| Which kernel an operator uses in BI mode and its cost | BI kernels |
Key rules
- Semantics follow the vLLM implementation and the checkpoint.
- Before starting, list for every module whether its inference kernel has a backward.
- Call kernels only; do not port the scheduler. mLite (BI=1): FP32 logprob raw bytes vs vLLM-captured trajectories, target 0 diff. FSDP2 / MCore (BI=0): no 0-diff target; judge each side against an independent reference, then look at precision.
- Mapping checks cover values and semantics, not only shape; fused parameters are overwritten whole.
- The precision contract lives in the model. Where vLLM computes in FP32 (position-encoding angles, router logits and bias, norm statistics, etc.), the training module sets the dtype and disables the outer autocast. Under VERL's bf16 autocast, casting only the inputs of matmul-type operators (including
F.linearandeinsumdecomposed into bmm) does not stop the downgrade. Measure output dtype and values under the real autocast. - Backward follows the formula, not the forward kernel. Only rounding differs -> reuse. Quantization rounding -> fix the STE contract, reuse BF16 GEMM. No whole-segment backward -> write glue, GEMM still TE. "No backward kernel at the same precision" is not a reason to rewrite.
- Reuse model-agnostic operators: mLite
primitive/modulesandopsfirst, then MCore/TE/mamba_ssm; only then is it new code. - Backward need not be bitwise; report bitwise and tolerance separately; the reference builds its graph independently with ATen autograd; tolerances are fixed before seeing results; synthetic inputs are labeled as not real activations.
- Do not recompute activations; use the forward's saved intermediates (DS4: gate_up / LSE).
- The model directory holds only config / checkpoint / model / protocol; reusable parts go into
primitive/; no same-namedprimitive/under the model directory; impl namelite. - Long-sequence boundary points (KV page, packing prepend, the 7000/9216 tiers) need dedicated tests.
- MTP block, EPLB redundant experts and draft are explicitly skipped in mapping and sync.
Module inventory and placement rules
List per module: inference kernel, same-math training implementation, quantization rounding, discrete selection, backward. Then pick the route and each module's place in the code tree. This catches missing backwards before they cost weeks.
Training support means an implementation we maintain that backpropagates correctly. Config registration, same-family fallback, the vLLM inference implementation and third-party implementations in the image (newer Transformers modeling, NeMo AutoModel) do not count. Write it per Reproducing the forward into real source files and verify by loading through the actual loader in a new process. A REPL-only implementation or runtime monkeypatch is not evidence.
1. How to build the inventory
One row per module, every column filled; write unknown, not "should exist".
| Column | Question to answer |
|---|---|
| Inference kernel | The function vLLM actually calls (not the model-file class name), with quantization variants, attention backend, cudagraph branch |
| Same-math implementation in the training framework | mLite primitive/modules, ops; MCore / TE; mamba_ssm, causal_conv1d. Rounding-only differences count as same math; a different activation clamp does not. Third-party same-model implementations are not entered |
| Quantization rounding | FP8/NVFP4 rounding in the forward; weights and activations separately |
| Discrete selection | top-k expert IDs, sparse indices; IDs fixed and not differentiated, the continuous score needs a derivative |
| backward | Does the library have one; do its intermediates (LSE, rstd, token permutation) match what our forward saves |
| Conclusion | Reuse / glue / new code (decision rules) |
Nemotron-H:
| Module | Inference kernel | Same-math training implementation | Rounding | Discrete | backward | Conclusion |
|---|---|---|---|---|---|---|
| Mamba SSD scan | mamba_chunk_scan_combined_varlen |
mamba_ssm | None | None | mamba_ssm _mamba_chunk_scan_combined_bwd |
Reuse |
| conv1d | causal_conv1d_fn |
causal_conv1d | None | None | Yes; dW needs fixed-order FP32 | Reuse + small glue |
| Attention (FP8 KV) | FA4 fixed split + FP8 Q/K/V | FA4 BF16 | Yes | None | FA4 BF16 backward only | Dequantize, recompute BF16 O/LSE, reuse |
| Mamba in/out_proj (FP8), shared expert (NVFP4) | FlashInfer / NVFP4 GEMM | TE general_gemm |
Yes | None | TE | Fix STE, reuse |
| Routed experts (NVFP4 W4A16) | CuTe-DSL / Humming MoE | TE grouped GEMM | Yes | Yes | No whole-segment backward | Glue routed_vjp |
| RMSNorm / gated RMSNorm, routing score | BI kernel / grouped top-k | PyTorch formula | None | None for the score | autograd | Reuse |
| MTP block, draft | Not part of training | - | - | - | - | See §4 |
3. Placement rules
3.1 mLite (Megatron experimental/lite/megatron/lite)
Read experimental/lite/docs/porting.md and architecture.md first. Three layers: runtime (lifecycle, training-step orchestration), model (registration + model-specific build/load/export protocol), primitive (reusable: parallel state, TP layers, checkpoint conversion, MoE utilities, optimizer wrappers). The runtime knows no model details: it gets the protocol from the registry, builds a typed config, and lets the protocol build the model.
| Rule | Content |
|---|---|
| The model directory holds only four things | model/<name>/: config, checkpoint mapping (WeightSpec), assembly, protocol. Impl name lite (<name>/lite/) |
| Modules go into primitive | norm / linear / router / experts / dispatcher / attention -> primitive/modules; generic VJP bridge -> primitive/ops; quantization encoding -> primitive/quantization; PP splitting, thd, CP -> primitive/parallel; HF read/write via primitive/ckpt/hf_weights (SafeTensorReader, load_hf_weights, export_hf_weights) |
| Model-specific math still goes into primitive and is registered | e.g. Mamba2 bridge, FP8-KV attention -> primitive/modules/<x>.py; no same-named primitive/ under the model directory |
| No file for reused backward | Reusing TE / mamba_ssm backward means no backward.py; glue goes in the module file |
| Dependencies are one-directional | model/ imports primitive/, never the reverse; top-level checkpoint/quantization does not import the impl subtree |
| Layering guards must be registered | Add the model to tests/unit/runtime/test_layering_contracts.py or the guard does not apply |
| No hybrid architecture packages | porting.md: "Keep these out of the first PR: Hybrid model implementation packages"; hybrid (Mamba + attention + MoE) modules are split into primitive |
| State third-party kernel dependencies | Document hard dependencies on vllm, flashinfer private APIs, mamba_ssm, causal_conv1d, and the init_batch_invariance() call in build_model |
DS4 is the positive example: model.py (650 lines) assembles te.RMSNorm, CSA, HCA/mHC, thd and the fp8 recipe with three model-specific deviations; moe.py (102 lines) = SigmoidTopKRouter + Experts + TokenDispatcher + SwiGLUMLP, hash-route the only model-specific code; checkpoint.py is one WeightSpec using shared load/export.
3.2 MCore / Megatron-Bridge
- Bridge
models/<name>/: provider + HF<->MCore mapping; modules MCore lacks (Mamba/SSM variants, special norms, HC) go intomegatron/core, not Bridge. - Mapping checks values, not only shape (§4.3).
- MCore and vLLM may use different formulas (MoE activation clamp); BI=0 cannot align those either, so list them first.
- The Megatron fork works only with Megatron-Bridge v0.6.2, pinned in the image.
3.3 FSDP2 (VERL's HF interface path)
- Write a PyTorch model to vLLM semantics in the Transformers
PreTrainedModel/ config interface so the VERL loader, PEFT and HF save/load use it directly. "HF" is only the interface and weight format. - Delivery: the model goes into the verl trunk, registered to the Auto classes (
AutoConfig/AutoModelForCausalLM, etc.), and ships with the recipe as one verl PR (Code and delivery §8). The verl FSDP extension guide's advice against copying modeling intoverl/modelstargets models already in transformers. Register at the current checkout's FSDP load chain (get_hf_auto_model_class->from_pretrained). - Develop and deliver from the same verl source and registration entry. Do not modify the checkpoint's
config.json/auto_map(the low-precision checkpoint is the single source of truth) or enabletrust_remote_code. The load log must show our registered class was used. - VERL's automodel engine (
automodel_actor) uses NeMo AutoModel and is not this route; routes are distinguished by forward source, not by FSDP2 sharding. - Under bf16 autocast, FP32 computations (position-encoding angles, router logits and bias, norm statistics, following vLLM) disable autocast inside the module and set the dtype.
- Backward prefers autograd; add operators next to the model file only when a backward is missing or too slow (the 160-line PyTorch Mamba scan reference).
- Common pitfalls:
--system-site-packagesABI, the actualuse_remove_padding, PEFT targets not going through nn.Linear. - Under EP4 + FSDP2, all ranks' DCP expert shards share key and offset, so restore reads another rank's experts; the key must carry the EP rank (checkpoint guide).
4. Parts that do not take part in training
Each gets its own inventory row:
| Part | Mapping | Facts |
|---|---|---|
| MTP block | Not loaded for training; WeightSpec lists it as explicit skip, not missing-key tolerance; _ignore_unexpected_suffixes etc. only for listed suffixes |
Nemotron num_nextn_predict_layers: 1, MTP unquantized bf16; usable with BI=0, then in both arms; BI=1 + MTP requires speculative-verification logprob bitwise equal to the full-segment forward |
| EPLB redundant experts | Training has logical experts only; physical-to-logical mapping is inference-side | Completeness counts logical experts, not physical tensors |
| draft / speculative model | Independent checkpoint and mapping | Shared weights (embedding, lm_head) state their owner |
Skipped parts appear in the load log and manifest as "explicitly skipped", not "no error reported".
5. Example: reuse audit of Nemotron-H against mLite
Nemotron-H is 3824 lines. From primitive/ it reuses only protocol-layer orchestration utilities, load_hf_config_dict and vocab_parallel_entropy; every modeling module is rewritten under model/nemotron_h/vllm/primitive/.
File (N = model/nemotron_h) |
Lines | What it does | Verdict |
|---|---|---|---|
N/config.py |
155 | HF config dataclass, validation | Keep (model dir) |
N/quantization.py |
207 | NVFP4/FP8 requantize, E2M1 grid, FP8 encoding, hand-read index.json | Numerics -> primitive/quantization/nvfp4.py (parameterize E2M1 RNE vs mxfp4 ties-down); IO via SafeTensorReader |
N/checkpoint.py |
486 | HF name mapping, Export spec, BF16 master invertibility check, θ0 threshold, own load_hf_weights |
Ordinary tensors via load_hf_weights + WeightSpec; keep master check and θ0 threshold; importing the impl subtree is a layering inversion |
N/vllm/model.py |
188 | Layer/Model assembly, PP stage | Keep; PP split via build_pipeline_chunk_layout |
N/vllm/protocol.py |
409 | protocol, contract validation, BI check, FSDP2/dist_opt, post-step requant | Keep; hard-coded 52-layer pattern and (2688,128,1856,6) make it single-checkpoint |
N/vllm/primitive/dense.py |
647 | visible forward + BF16-master VJP bridge; RMSNorm VJP; Humming/CuTe NVFP4 linear; projection factory; FP8/NVFP4 TrainingLinear | Bridge and RMSNorm VJP -> primitive/ops/aligned_vjp.py; native_linear_vjp duplicates _te_general_gemm; serving kernel adaptation stays |
N/vllm/primitive/logprob.py |
36 | Chunked lm_head + vLLM compute_token_logprobs + FP32 log_softmax STE |
Keep; the docstring's DS4 function does not exist |
N/vllm/primitive/attention/* |
231 | Fa4Fp8KVAttention (FlashAttnFixedSplit 6768-token page, FP8 Q/KV), _Fp8AttentionVJP |
Model-specific; reusable once GQAttention supports rotary=None, qk_norm=False |
N/vllm/primitive/mamba/* |
316 | _PackedConvVJP, _PackedScanVJP, SSMMeta, MambaMixer |
Keep both VJPs; generic Mamba2 -> primitive/modules/mamba2.py; derive SSMMeta from PackedSeqParams/ThdPackMeta |
N/vllm/primitive/moe/module.py |
185 | _FixedRouteVJP + Router, _CombineVJP, ReLU² SharedExperts |
Router VJP equals SigmoidTopKRouter, reusable, not hooked to router_replay; add an activation argument to mlp.py for SharedExperts |
N/vllm/primitive/moe/grouped.py |
705 | FlashInfer W4A16 signature/sha pinning, CuteDslRoutedExperts, Nvfp4ExpertWeights, _te_grouped_gemm rewrite, ReLU² routed_vjp |
Keep W4A16 forward + grouped VJP; TE grouped GEMM wrapper duplicated; ep4_routed_experts hard-codes 4 ranks |
N/vllm/primitive/moe/communication.py |
251 | DeepEP normal-mode EP, reduce_ep4_parts (FlashInfer one-sided combine order), EPRoutedExpertsVJP |
Use _build_deepep_buffer; keep serving-order reduction, parameterize EP size/topk |
Six placement violations: impl name vllm not lite; hybrid architecture package; same-named primitive/ in the model layer; circular imports between checkpoint.py/quantization.py and the impl subtree; nemotron_h missing from the layering guard; third-party kernel dependencies and init_batch_invariance() undocumented.
Should have been reused: safetensors index reading (5 copies); HF load of ordinary tensors; DeepEP buffer; TE GEMM wrapper (2 copies); visible forward + reference VJP bridge; Router (SigmoidTopKRouter + group_limited_topk, forward switched to vLLM grouped_topk, hooked to router_replay); shared expert; E2M1 grid and FP8 encoding; PP splitting; SSMMeta; two docstrings citing nonexistent DS4 functions.
Model-specific, keep: the two Mamba2 VJPs + packed conv/scan; Fa4Fp8KVAttention + FP8 attention VJP; W4A16 MoE forward + ReLU² grouped VJP; serving-order reduction (parameterized); Humming/CuTe NVFP4 and FP8 serving linear; vLLM forward-value logprob; BF16-master invertibility check and θ0 threshold; ModelOpt 4over6 requantize rule.
Reproducing the forward: call kernels only, do not port the scheduler
The first deliverable after the inventory is a training forward with vLLM's math. Logprob measurement basis and R3 route replay: workflow, rollout engine.
1. First separate the responsibilities of the model implementation and the training framework
A training framework (FSDP2, MCore, mLite runtime) shards a trainable model; it does not make an inference-only kernel differentiable or write the model. Write a minimal PyTorch model for the current loader interface covering the config, checkpoint loading, forward, gradients and save interface actually used; full Transformers integration, generation and all parallelism are not needed first. The semantic basis is vLLM and the checkpoint, not a third-party implementation in the image.
| Capability | What the training route still has to do | What in inference / Megatron is not copied |
|---|---|---|
| Model math | Real semantics of attention/SSM, norm, position encoding, routing, residuals, special embeddings | vLLM paged KV, decode scheduling, cudagraph management; MCore provider/ModuleSpec wiring |
| Backpropagation | Input gradients of every training-path operator; trained-parameter gradients; recompute correctness (Backward validation) | TP/SP/CP-specific collectives and gradient reduction |
| Weights | Names, axes, fused order, expert IDs, sharing, derived state | HF<->Megatron shard layout, PP export protocol |
| LoRA | Target coverage, A/B direction and scaling, non-zero adapter taking effect | Parallel linear wrapper, TP adapter aggregation |
| Memory | Initialization peak, large frozen table residency, save/export peak | Megatron memory saver, PP scheduling |
dtype, dequantization, QAT and update format: precision guide. Here each module states its input/output precision and differentiable boundary.
2. How to wire kernel-only calls
Each layer's forward runs once: call the vLLM kernel for the visible output, bitwise identical to inference, and save what the backward needs; see backward wiring. Borrow only the kernel, no vLLM runtime object:
- No KV paging, decode scheduling or CUDA Graph management. Training runs a full-sequence teacher-forced forward; packing passes boundaries via thd/cu_seqlens.
- Reuse order: model-agnostic differentiable operators -> concise PyTorch reference to vLLM semantics -> ready-made operators with backward -> custom autograd/kernel only if necessary. This order only says where to look first; the training-step cost of each choice is checked at C6 against the C1 budget, not deferred to C10.
- Handle discrete top-k / index selection apart from the selected differentiable tensors. What is trained, frozen or given an auxiliary loss follows the model definition; do not detach to get it running or add router/indexer objectives.
- vLLM layer modules used directly (FusedMoE, ModelOpt linear) need
get_tp/ep_group, so the training process creates vLLM process groups (Nemotron does; DS4's own modules do not). - Take the same branch as inference: reproduce the quantization config, attention backend and BI-switch kernel selection, or the comparison is against a different path. Tests use the production
CompilationConfig(custom_ops=...)(the two false negatives).
"The same kernel" still has pitfalls (Nemotron 7-layer localization):
- Direct and compiled paths of the same gated RMS kernel differ; calling it through an opaque custom-op boundary removed the difference.
- A differing reduction block was wrongly blamed alone; the same captured kernel showed both blocks match.
- The last 4.768e-7 logprob difference came from the HF forward calling
.float()after lm_head; keeping the real BF16 head output and sharing the scoring fixed it. This is not truncating an FP32 projection to BF16 to manufacture equality.
3. Read the inference implementation along the full support chain
Order: model file (layer types and order, residual and norm positions, position encoding, attention visibility, router/indexer, special embeddings) -> each layer's actual kernel and quantization variant -> attention backend and KV dtype -> weight loader (name mapping, fused order, derived scale) -> BI-switch branches. Confirm the parameter/buffer list from the same checkpoint/config.
Sizes sharing one implementation need no separate port. For ordinary dense models, look for a reusable generic skeleton first; we still write the model. Multimodal is validated per task; diffusion is out of scope.
4. The minimal implementation that really has to be filled in
4.1 Reproduce the math
Confirm from the checkpoint/config and inference code: layer types and order, parameters/buffers, residual and norm positions, position encoding, attention visibility, router/indexer, special embeddings. Model-specific math (sqrt(softplus) scoring, SwiGLU with clamp, mHC Sinkhorn, ReLU² experts) is in no library; write it from the inference source, not the paper.
4.2 A frozen base still has to propagate input gradients
W.requires_grad_(False)
y = linear(x, W) + scale * B(A(x)) # no gradient for W, but still propagate to x
loss(y).backward()
Wrapping a frozen block in no_grad() cuts LoRA gradients of earlier layers; only a frozen table lookup needing no input/parameter derivative is exempt. With initial B=0 a zero gradient for A can be normal; test with a non-zero adapter or a real step. A finite loss does not prove backward.
4.3 Mapping checks cover values and semantics, not only shape
Per weight class: source name -> training name -> serving name, axis transforms, sharing. Focus on fused QKV, gate/up order, expert numbering, extra norm/bias, tie weights, non-persistent buffers, derived state (FP8 KV scale, NVFP4 alpha). Every missing/extra item needs a reason; quantization-derived items follow the precision guide and need not be byte-identical to the low-precision source. FSDP needs no HF<->Megatron bridge but may need checkpoint->training and export mappings.
4.4 Packing boundaries for Mamba / stateful modules
Every stateful operator (conv1d, SSM scan, recurrence, PLE shift) must consume sequence boundaries; a correct attention mask proves nothing about them. Incidents:
- verl via Megatron-Bridge did not pass boundaries to Mamba; sequence states mixed and logprob was silently wrong, visible only as a larger mismatch.
- Qwen3.8 Flash-Next's PLE shift made the second sample reference the first sample's last token.
Check: unequal-length A/B run separately and merged; sentinel-initialized outputs to catch missed writes. In mLite, derive boundary metadata from PackedSeqParams/ThdPackMeta, not a separate SSMMeta. See also Long-sequence boundary points.
4.5 Recompute and small-model trimming
With activation checkpointing on, verify recompute reads the same state and gives reasonable gradients. Trim to the smallest layer set covering every non-equivalent path and its dependencies, usually 4-6 layers; keep mixed attention, cross-layer sharing and layer-specific table lookups. Record kept layer indices and the config transform; a slice proves only covered paths, not full-model mismatch or score gain.
5. How to prove it computes the same math
Feed the training forward vLLM-captured trajectories (fixed tokens, weight version, position, mask, routing); never compare probability summaries or argmax. mLite (BI=1): FP32 logprob raw bytes, target 0 diff. FSDP2 / MCore (BI=0): judge each side against an independent reference, then check the remaining difference comes only from precision rules.
- Same policy weights. Differing dtypes are a contract mismatch; do not convert to fake bitwise.
- Reference artifact (Nemotron): frozen trainer image, full actor, 2048 tokens, FP32 0 diff; five real updates, 883,998 tokens of identical FP32 logprob.
- Old-logprob recomputation and the grad-enabled forward are two paths; evidence for each (dual-path contract).
- Find the first divergence per debugging: per-module dump, single-request prefill, then the operator.
6. Small tasks handed to agents and stop conditions
Each outputs a short conclusion, source version, reproducible command and artifact.
| Task | Minimal acceptance | If it fails |
|---|---|---|
| Implementation inventory | Training model class, operator backward, checkpoint loader; gaps listed | Write the minimal model; stop cold-starting the large model |
| Module comparison | Same weights/inputs; gradients vs a differentiable reference | Locate the earliest difference |
| Few-layer training | Non-zero loss, finite gradients, intended adapter changes | Separate broken path, operator bug, gradient-less target |
| Save and restore | Fresh-process restore: consistent forward and next step | Check state, adapter, optimizer, residency |
Distribute only after the first two pass; scale up after the few-layer two-step loop passes.
7. How to judge development cost
Training implementation exists: wire loading, LoRA, sharding. Inference only with differentiable references: fill in assembly, mapping, interfaces. Key operator lacks backward or clear semantics: separate blocker; reference and gradient validation first; no uniform person-day figure.
Distributed layout, length boundaries and training-side acceptance
Record trainer and rollout topologies separately. TP/CP/EP, padding, packing and collectives change data mapping and reduction paths; long sequences hit untested boundaries; forward 0 diff does not vouch for gradients or the optimizer.
Compute resources first, then request them
Write trainer and rollout process groups separately. At four GPUs per node, an inference DP8/EP8 group spans two nodes; two DP4/EP4 replicas are not one EP8 group and skip cross-node EP communication. The runtime decides grouping.
If dense DP excludes CP: W = TP × PP × CP × DP. Expert side: verify W = ETP × EP × PP × EDP and the CP mapping separately. Do not multiply EP into the dense world again. W=8, TP1/PP2/CP2 gives DP=2; a log calling DP×CP "DP" shows 4. Clarify naming and members; do not change GPU count to match a config string.
GPU memory follows the layout. With PP1 each rank holds all layers' activations, linear in sequence length. Per-route EP backward state stored for other ranks' slots grows by the average ranks per token; store only own routes. Rollout gpu_memory_utilization is set from what training still holds during rollout, per training mode, in the recipe (Memory budget 02 §5).
Packing and CP
For the target UID reconstruct global request [seq_start, seq_end), rank range [rank_start, rank_end), local rows, cu_seqlens, positions, all compression/reordering mappings.
The intersection [max(seq_start, rank_start), min(seq_end, rank_end)) contributes rows only when start < end. The overlap window must not give a disjoint request this rank's output. Host cumulative boundaries equal the rows the device compactor produces.
Cover: before/after/at the boundary, empty intersection, cross-rank, compression-ratio and tile remainders. Window constant 8: check 1…7, not only multiples. Start from a hand-computable counterexample, then enumerate nearby legal configs.
Passing cu_seqlens does not mean a fallback consumes it (§4.4).
EP and collectives
Record expert mapping, tokens per rank, padding capacity, route order, shared/routed combine, quantization scales. Empty ranks may still join collectives; partially written reused buffers leave stale data. Use different sentinels per side; zero-filling both gives a false green.
Pinning NCCL algorithm/channels may remove reduction differences on one stack and cut throughput significantly; verify it per topology, not as a universal env list, before relaxing config or building a deterministic collective.
MCore dev's grad-ready assertion takes first-batch hook counts as golden and errors on a different count. Short-sequence micro-batches can make counts data-dependent (Nemotron r10 at step 2; r12 did not reproduce). With dp×cp=1 and expert_dp=1, overlap_grad_reduce off avoids it without changing values; root cause unknown. With EP4 + dist_opt and dp>1, multi-micro-batch reduction fires early (reported).
Bitwise identity after changing the parallel layout
Principle. Floating-point addition is not associative. Layout sets reduction grouping, order and rounding: TP splits a matmul into per-rank partial sums; EP computes a token's top-k experts on different ranks and merges. Last-bit differences become different tokens after dozens of layers and sampling, so bitwise identity holds only for the verified layout; any TP/EP/DP change must be proven again.
Rules.
- The reduction tree is part of the numerical contract: per level, accumulation dtype, order and rounding points. All legal rollout layouts and training walk the same tree.
- A degenerate layout (TP=1, EP=1) must simulate the tree: per-rank partial sums, same rounding, same merge order.
- After a layout change, compare per-token logprob raw bytes on fixed inputs before training. Non-zero
ppo_kl/probs_diff is fixed, not avoided by reverting the layout. - Locate: dump per layer, find the first differing layer, confirm its inputs (MoE: router logits too) are bitwise equal; the bug is that layer's reduction (Fault localization).
- Measure the cost of simulating the order and report it apart from the layout gain.
Examples.
| Model | Layout change | Where the difference is | Fix and result |
|---|---|---|---|
| Nemotron-H (W4A16 MoE, BI=1) | rollout from DP4EP4 per node to TP1 per GPU | EP4: per-rank FP32 sums rounded to BF16, merged in FP32 as ((s0+s1)+(s2+s3))+(s4+s5). EP1: six outputs summed sequentially in FP32, no intermediate rounding. Max logprob diff 0.4; greedy tokens diverge at positions 10–129 |
Without EP, BI mode computes all experts in one launch and reduces in EP4 order (vLLM 661cde6833, not in the image). Both layouts match training bitwise; ppo_kl 0 over 58 steps; multi-turn replay 404 s vs 485 s, reduction cost about 2.5% |
| DeepSeek V4.1 | training TP=1, inference TP>1 | order of per-TP-rank partial sums | both sides use one fixed reduction tree, TP=1 included (VLLM_TP_INVARIANT_LEAVES) |
Asynchronous communication and lifetime
Check streams, events, buffer owners and reuse timing. Passing with a global synchronize only proves a timing issue and is not the fix; find the missing dependency and accept on the same path.
Long-sequence boundary points
Test at real lengths:
| Boundary | Fact | How to test |
|---|---|---|
| KV page | training attention replicates vLLM's KV page size; page boundaries are numerical boundaries | sparse dY across the page boundary |
| Fixed split attention | fixed split count and keys per split; the resulting MAX_SEQ_LEN is an invariance bound, not a performance knob |
above: error, not silent truncation; below: two real lengths × random/zero/sparse dY |
| Packing prepend | one prepended short sequence shifts every target row offset | two prepend controls vs none, bitwise |
| Micro-batch fits the longest sequence | verl needs each micro-batch to fit the longest sequence (ppo_max_token_len_per_gpu, Nemotron 16384 vs DS4 4096; Nemotron training has no CP) |
OOM probe at formal length; do not extrapolate from 8K |
| Embedding backward | CUDA embedding_dense_backward accumulates at reduced precision with ≤3072 indices, e.g. a short sequence alone in a micro-batch |
length-tiered tolerance |
| CP compressed boundary | see Packing and CP | before/after/at the boundary |
The D term in attention backward
With P rounded to FP8, visible O = fp8(P)·V ≠ softmax(QKᵀ)·V. If D = rowsum(dO⊙O) uses visible O while dS uses recomputed P, rowsum(dS_i) ≠ 0 and dQ gains a same-sign per-row bias (Nemotron: about 3% of dQ/dK, v2 FAIL at steps 150/250; fixed 0.006–0.009, PASS). Mainstream FP8 attention (TE fp8_dpa, SageBwd) uses visible O, so a human decides after being informed.
Four checks:
- FA4 interface signature:
_flash_attn_bwdderives D in preprocess from theoutpassed; D cannot be passed. The public interface errors underrequires_grad; the actor calls_flash_attn_fwd/_bwd. Fix: recompute BF16 O and LSE from dequantized Q/K/V, pass to bwd (layer backward +4–15%, end to end <0.05%). - varlen LSE layout: ln vs log2, per head vs per token, softmax scale folded or not. On mismatch, recompute with the training kernel.
- GQA 32/2: dK/dV summed within each Q head group; A/A needs fixed summation order.
- When the saved Q/K/V are released under recompute=full: if only dequantized Q/K/V are saved, recompute must read the same state; early release changes the values.
Microbatch split and gradient difference
Different microbatch splits give about 2e-3 relative gradient difference (loss normalization, accumulation order, reduced-precision embedding backward at ≤3072 indices). Not a bug, but:
- Criterion denominator =
max(B16 reference floor, noise floor of the implementation under test), never the FP32 ideal. - Both arms use the same split; if it changes, rerun A/A.
- grad_norm is not comparable across different microbatch counts.
Control matrix
With policy/tokens fixed, test original layout, reversed order, target first/last, changed micro-batch, CP-boundary sample, each reconstructed by UID/global token coordinates. Real TP/EP changes may legally change the reduction; the experiment contract decides whether cross-topology bitwise equality is required.
Safe packing is not a fix for a metadata bug; it may only lower trigger probability. After the fix, the original layout and nearby boundaries must pass.
Training-side correctness acceptance: gradient comparison criterion v2 and optauth
Two independent checks beyond forward 0 diff.
Gradient comparison criterion v2
- Freeze the criterion (tolerance, denominator, thresholds) before the run, record its sha, never change it after.
- Planted-error check first: gradients ×1.25 and ×1.05 must both FAIL.
- Accept on several snapshots (steps 0/50/150/250); the D-term problem appeared only at step 150.
- On failure, inject reference gradients block by block to find the first module over the limit.
- Extended to FSDP2: EP4 + FSDP2 all four snapshots PASS, worst-class median 1.08–1.39, all floors from B16.
- Tool bug: with
overlap_grad_reduce=Falsegradients were reduced again before the EP4 dump (fixed). The export path needs A/A too: ‖g‖ bitwise equal, local shards bitwise equal to the dump, replica diff 0/0. - Cross-process bitwise backward is not required. An occasional non-bitwise first step comes from the thread-local Inductor deterministic config (the autograd thread's first compile picks 512/1024 blocking by timing); fix in BI kernels: determinism.
Optimizer acceptance (optauth)
Verify the optimizer consumes clipped main_grad, the update lands on master, and deployed bytes = requantize(master). Freeze the criterion first. Split failures into criterion modeling gaps (e.g. a check inapplicable without replicated parameters; the equal-magnitude first Adam step invalidating γ-type criteria while the per-coordinate bound holds), documented and accepted, and implementation errors, fixed. Cover all parameter group types: a first-step probe without an FP32 wd_mult=0 parameter misses weight decay applied to that group.
Facts outside the two criteria
- Frozen base with LoRA: base modules still propagate input gradients (§4.2).
- The two-step RL loop (θ0→θ1→θ2) accepts the forward; three-arm SFT accepts training effect; report all three separately (validation).
VERL / Megatron integration
Upstream config keys may differ from what was used here. Find the actual call paths of rollout dump, old-logprob and skip/repeat in the current checkout and record the commit; local-patch options are not upstream API. Process content (recipe and PR contract, review material, two-step acceptance, E2E targets) is in validation.md; PR ownership in Code and delivery §8.
Where the integration layer is
Megatron's experimental/lite/examples/verl/verl_mlite/ provides the verl engine adapter (model_engine=mlite, pkg://verl_mlite.config), installable into the unified image without reimplementing the engine in verl. Check engine registration, model factory, batch conversion, loss normalization and optimizer path; list reusable interfaces and gaps. Consistency uses mLite; the training baseline uses the existing recipe's default MCore path; do not mix them.
Capture location
Save the DataProto (or equivalent) after rollout returns and before the first old-logprob; append trainer scores after. Record policy version, UID, input/response IDs, mask, position IDs, reward, route replay data if needed, and the UID -> packed-row mapping if reordered. Keep a pre-update checkpoint bound to the batch by content fingerprint; after strict load compare structure and keys and reject missing/unexpected keys.
Historical DS4 entry point
DS4 used patched VERL options (skip.rollout.*, DS4_SKIP_UPDATE=1); confirm in code and logs that any such switch takes effect.
Minimal adapter contract
The synchronous entry point takes batch, checkpoint, variant and output directory, then:
- Records actual policy, effective config, libraries and parallel layout.
- Reads the same rollout, repacks per variant, keeps logical UIDs.
- Runs the real old-logprob forward and exports per-token logprobs in UID order (Saving and comparing).
- Records no generation and no update, and mutable-buffer initialization.
- Waits for workers/collectives, closes its resources, returns an exit code.
It keeps native no_grad/forward_only semantics and replaces neither the grad-enabled forward comparison nor update acceptance. A/B/C may share one long-lived worker once weight and state isolation is verified. The runner does not take over Ray/Slurm; the launcher blocks on remote jobs and reclaims on timeout. For injected or overlaid adapters, record the actual class and source file and assert the callsite was hit; PYTHONPATH can shadow the overlay.
Entropy when old-logprob is not recomputed (bypass)
Bypass uses rollout logprob as old-logprob: actor/ppo_kl then measures rollout vs training forward exactly (0 under BI=1), and entropy comes from the grad-enabled update forward.
- Put metric-only quantities under
no_grad. Withentropy_coeff=0the loss still holds0 * entropy_loss, so entropy's backward runs and saves a logits copy. - Chunked entropy in the verl Megatron engine cannot backpropagate. With
entropy_from_logits_with_chunking=True, each chunk view's backward overwrites the shared saved logits in place; the shared version counter then raisesone of the variables needed for gradient computation has been modified by an inplace operation. Hit whenever chunking meets entropy. Fix: verl-project/verl#8195; until merged, withentropy_coeff=0compute entropy underno_gradwith the same chunking. - Re-verify gradients after changing forward computation. Nemotron chunked logprob: BF16 bitwise equal to unchunked; FP32 max diff about 1e-8 at max gradient about 4.
Training extension
Pairing a shared inference forward with a native VJP: check recompute, CP metadata lifetime, PP payload, parameter registration, through the standard training entry point. After a real update, export a new rollout and trainer scores; old bundles replay only with the old policy, and new-vs-old output differences are expected, not a consistency failure. Modules using vLLM layers need vLLM process groups. param_offload=True changes the quantized master's data_ptr and fails the pre-export check; use optimizer offload only.