Repository navigation
Conversation
…vation checkpointing
- Build DeepEP's communication buffer when the dispatcher is created. Built lazily inside the
first checkpointed forward, it shifted the recompute's cached ops ("Autograd not support
dtype: Byte").
- Replace the local permute chain around DeepEP (index_select, vstack + gather, index_put, fp32
score passes, scatter_add) with one Triton gather into the padded expert-grouped layout and one
fp32 gather-sum back, weighted by the routing scores. Backward is also gathers: deterministic,
no atomics.
- Size DeepEP's RDMA queue pairs from num_sms so num_sms > 24 works internode.
- 12-node V4.1 16k example uses DeepEP: 9.3 s -> 5.6 s per step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…en available - V4.1 indexer: `dsa_indexer_topk` (fused fp8 scoring + top-k + candidate blocks) replaces the chunked indexer when built for the GPU and shape. - V4.1 mHC: `mhc_projection` fuses the projection, gates and stream collapse. - `model.moe.compute.backend = "prime_kernels"`: `moe_experts` grouped GEMMs with the clamped SwiGLU fused in. Each falls back to prime-rl's implementation when prime-kernels does not provide it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oAllEmbeddingParallel V4.1's engram module now holds a plain nn.Embedding. The trainer row-shards it over the FSDP mesh with a new AllToAllEmbeddingParallel style next to EmbeddingParallel: deduplicated ids go to their owning ranks and only those rows come back, instead of RowwiseParallel's all-gather of every rank's ids and reduce-scatter of partial rows for all of them (~19 GB per table per rank at 96 GPUs). The tables stay out of FSDP. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Subclasses with their own forward (DeepSeek-V4's block-diagonal grouped o_a_proj) were replaced by plain low-precision linears, computing the wrong product and failing in DeepGEMM downstream. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`model.moe.compute = {type = "deepgemm_fp8", backend = "prime_kernels"}` runs DeepGEMM's blockwise
FP8 grouped GEMMs (forward, dgrad, wgrad) with the quantization and clamped SwiGLU fused into the
passes around them (~1.4x faster than bf16 at V4.1's EP=96 shape). V4.1's `fp8.toml` overlay
enables it with FP8 dense linears: 12-node 16k step 5.65 s -> 4.9 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`prime_rl::deepep_moe` dispatches every token chunk up front and runs each chunk's experts while later chunks are in flight; its backward does the same with the gradients, recomputing each chunk's experts from the saved received tokens. Activation checkpointing saves the op's outputs, so recompute neither resends tokens nor reruns the experts. `token_chunk_size` no longer slows a step down (old chunking: 9.0 s -> 11.3 s), but on 12 nodes it does not speed it up either: the overlapped DeepEP kernels wait for SMs held by the GEMMs and for DeepEP's per-chunk barriers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One GEMM per group reads and writes strided slices instead of a batched matmul over a transposed, group-major view, which copied the output in forward and the gradient in backward. Bit-identical; 1.23x faster fwd+bwd at V4.1's shape (8 x 4096 -> 1024 over 16k tokens). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Per-group reciprocal with an FMA correction instead of an exact divide per element (bit-identical, checked exhaustively), a transposed cast that zero-pads the token dimension itself (no F.pad copies), and scales written in DeepGEMM's layout (no scale-transpose kernels). ~5% less GPU time per FP8 linear fwd+bwd; outputs and gradients unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… under fp8.toml Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r kernel when available ~1.2x cuDNN's DSA backward at V4.1 shapes (prime-kernels dsa_sparse_attn_bwd); falls back to cuDNN. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
samsja
force-pushed
the
feat/deepseek-v41
branch
from
October 5, 2026 21:48
09deef1 to
3a77ae0
Compare
Updates each parameter as soon as its gradient is final and frees the gradient, so no step holds every gradient at once. Exact for one micro-batch per step: sign updates ignore the positive loss scaling and clipping applied after backward; the gradient norm is accumulated for logging. DeepSeek-V4.1 on 12 nodes at 16k: peak memory 117.6 -> 96.9 GiB at the same step time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`[model.moe.dispatch] fp8 = true` (DeepEP with deepgemm_fp8 experts) quantizes tokens per 1 x 128 block with power-of-two scales before the forward dispatch and dequantizes them on arrival, exactly, to the bf16 the experts quantize again. Forward dispatch traffic and the received tokens kept for backward halve; gradients stay bf16. V4.1 Flash, 12 nodes, 16k, fp8.toml: 4.8 s -> 4.7 s per step, peak memory 117.6 -> 113.8 GiB, loss curve unchanged within noise. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…adient The query's rotation and both rotations in backward overwrite their input instead of copying every 512-wide head; only the 64 rotary channels are read and written (6.3x faster per call at 16k tokens). Bit-identical to the copying op, eager and compiled, with and without activation checkpointing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…verloads a rank prime-kernels' FP8 expert path addresses a padded copy of the received rows with 32-bit offsets, so a rank receiving more than ~419k token-expert pairs at V4.1's width raised "too many rows", and every other rank then hung in DeepEP until its GPU timeout. Skewed routing reaches that easily (440k-855k rows on 12 nodes with the real router). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…E backend Pipeline parallelism: - `model.pp`, `pp_schedule`, `pp_stages_per_rank`, `pp_layers_per_stage`; stage build in `model.py`, `trainer/pipeline.py` (pooled receive buffers, async 1F1B, shape probe), `prune_to_pipeline_stage` / `pipeline_stage_forward` for V4.1, PP-aware data ranks and grad clip. Expert path: - `model.moe.dispatch.keep_expert_activations`: keep routed-expert activations for backward. - FP8 expert weights quantized once per optimizer step; FP8 weight gradients accumulate into FSDP's fp32 buffer (prime-kernels `moe_experts`). - `dispatch.weight_grads_after_combine`: send each chunk's gradient back before its weight-gradient GEMMs so they hide the send (bitwise-identical gradients). - `moe.reduce_local_expert_grads_once`: single-rank expert FSDP groups reduce once per step instead of per micro-batch. - `dispatch.fp8_grad` (off by default, changes numerics): FP8 backward dispatch. - Spread balanced routing for `debug.force_balanced_routing`, so each token reaches ~4-5 nodes. - `dispatch.type = "mega_moe"`: prime-mega-moe's fused BF16 Hopper MoE as a backend (off by default). Non-MoE: - `prime_rl::dsv4_linear_rope`: query projection and RoPE as one op (rotates in place, keeps no unrotated copy); out-of-place RoPE ops that selective AC can save. - mHC stream-gradient sum folded into prime-kernels' projection backward when available. - Fused Triton cross-entropy gradient in the chunked LM head. - Per-parameter seeded `debug.random_init`; setup order AC -> FSDP -> compile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drop the attention score/value term from the FLOPs per token, so MFU is 6 x active matmul params x tokens (LoRA keeps its 4/2/6 split). PerfCounter and get_perf_counter no longer take seq_len. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sign_sgd_update_ applies weight decay and the sign step in one kernel pass over param and grad instead of four torch ops, matching them bit for bit; non-contiguous or CPU tensors keep the torch path. Used by SignSGD and SignSGDInBackward. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ate for Muon
optim.embedding_update (default None, unchanged routing) applies the V4.1 tech report's split
(§2.5): embeddings, LM head and engram tables get the Sinkhorn-balanced momentum update
("sinkhorn") or AdamW without weight decay ("adamw"); norms AdamW with decay; biases and scales
AdamW without. MuonWithSinkhorn adds the "sinkhorn" algorithm to dion's Muon with fused Triton
row/column steps that work on row-sharded tables. Adds a GPU test against a dense reference.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ention - dsv4_mhc: the h_post/bda backward computes g_x, g_orig, g_hp and g_hr in one pass over go. - dsv4_rope: the in-place RoPE kernel can save the unrotated rotary channels. - dsv41_sparse_attn_rope: sparse attention plus in-place inverse RoPE of its output, keeping only the rotary channels the backward needs unrotated (1/8 of the output for V4.1) instead of a rotated copy. V4.1 attention uses it with the FlashMLA kernel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
moe.dispatch.fp8 (default off) runs the routed experts on prime-mega-moe's SM90 FP8 Mega MoE: the forward keeps each routed row's FP8 input and bf16 gate/up output, the backward computes data gradients from them and adds DeepGEMM K-grouped FP8 weight gradients into FSDP's fp32 accumulators. Flags (FP8 only, default off): capacity_factor, wgrad_tile_scales (128x128 tile scales for the wgrad operands, changes numerics), free_bf16_expert_weights (frees the unsharded bf16 expert weights once quantized, same numerics). prime_rl::mega_moe_fp8 is a mandatory AC save. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
model.ac.mode gains full_moe (full that also recomputes the FP8 Mega MoE), projections, attention and matmul, between full and selective. model.ac.layer_modes sets one mode (or none) per decoder layer, indexed by full-model layer so pipeline stages can retain less on early stages. Default (mode=full, layer_modes=None) is unchanged. Random init strips the checkpoint wrapper prefix so parameters draw the same values whether or not their layer is checkpointed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d lookups - The engram gate multiplies q_weight * k_weight in fp32, as DeepSeek's reference (it rounded to bf16 first). Changes numerics toward the reference. - The gate no longer checkpoints wkv: the compiled gate keeps kv and recomputes its fp32 math. model.ac.recompute_engram (default off) recomputes the projection too for layers in full/full_moe. - Engram lookups are routed ahead of the forward (prefetch), moving the split-size host sync before the first layer. - Later micro-batches add their table rows straight into the step's gradient instead of a dense per-micro-batch table gradient. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… PP activation offload - Async1F1B moves to a generic executor (AsyncPipelineSchedule) that runs any precomputed op order and never waits on a stage transfer before it is needed; it also runs the new DualPipeV schedule (two stages per rank in a V). pp_warmup_step tunes 1F1B warmup per neighbour pair. - pp_transport: nccl send/recv (default) or copy_engine (symmetric-window all-gather on copy engines, NCCL >= 2.32); pp_transport_ctas and pp_gemm_sms pin SM budgets. - pp_layers_per_stage accepts multiples of 0.5 to cut a V4.1 layer between attention and MoE. - pp_activation_offload (default off) offloads kept activations to pinned host memory between a micro-batch's forward and backward. - Sent stage outputs no backward reads are freed early; grad clipping and loss broadcast handle several stages per rank and V schedules. Adds schedule-order unit tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
model.engram_offload (default off) keeps each rank's engram table shard and its optimizer state in pinned host memory: lookups gather rows over PCIe on a side stream ahead of the forward, and the AdamW/SignSGD step streams the shard through the GPU with the same update kernel, overlapped with the next forward. Checkpointing offloaded tables is not implemented. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ge transfers PutEdge carries each edge direction over its own 2-rank zero-CTA communicator: the sender packs a micro-batch into its registered symmetric window and ncclPutSignal's it into the receiver's window; the receiver waits on the signal, copies out and returns a credit with ncclSignal. A CPU proxy drives the NIC, so transfers hold no SMs and stop slowing persistent kernels (Mega MoE, DeepGEMM, sparse attention). Unlike copy_engine it needs no global transfer order and sends one way only. Costs one micro-batch buffer per edge end; needs NCCL >= 2.30 at runtime. Default stays nccl. 8-node PP16-stage emulation (stages 0-7): H200 1F1B 28.9% -> 34.0% MFU, 80 GB 27.6% -> 31.6%; step-10 loss and grad norm match nccl. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
moe.dispatch.fp8_transposed_on_demand (FP8 only, default off) keeps only the forward FP8 expert weights for the step and rebuilds the transposed copy each backward with a Triton 128 x 128 byte transpose (scales via torch), freeing it after. Frees 1 byte per local expert parameter (1.7 GB per V4.1 MoE block at EP8) for ~0.95 ms per MoE block backward on H200. Bitwise equal to the cached transposed copy. 8-node 80 GB emulation (put, 1F1B): 31.6% MFU at 78.7 GB vs 30.4% projected baseline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
model.pp_activation_offload.target = "fp8" (default "host") keeps a stage's kept activations on the GPU as FP8 with one fp32 scale per 128 values instead of copying them to host memory, and dequantizes them into their original storage before the backward (Triton kernels). Only contiguous bf16 tensors with a last dimension divisible by 128 are compressed. Changes numerics (backward and recompute read dequantized values) and is unverified beyond an unchanged loss and a +0.2% grad norm on a PP2 rig; the 8-node 80 GB emulation measured 30.0% MFU, below fp8t alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The one-sided ncclPutSignal / ncclWaitSignal used by `model.pp_transport = "put"` need NCCL >= 2.30; torch 2.13 pins nvidia-nccl-cu13 2.29.7. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…real transfer end pp_transport=put now fails with a clear error when the loaded NCCL is older than 2.30 instead of hanging or crashing. Also documents the ctypes arguments, checks _views against the actual end of the packed tensors, and adds type hints. Same code path otherwise. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
moe.dispatch.fp8_transposed_on_demand also accepts a list of pipeline stages, so the transposed FP8 weights are rebuilt on demand only where memory binds. Default False; bitwise either way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…input-grad send model.pp_defer_expert_weight_grads (Async1F1B / DualPipeV, default off): a backward leaves the FP8 Mega MoE experts' weight-gradient GEMMs until after the stage posts its input-gradient send, so the previous stage gets its gradient one weight-gradient pass earlier. Not deferred on a step's last micro-batch (it starts the reduction) or the blocking first step. Same GEMMs, same order, same fp32 accumulators; operands (~1 GB per MoE block at 8k tokens) live until they run. 6-node: steps 1-3 bitwise, later steps within FP8 run-to-run noise. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
model.pp_offload_masters (Async1F1B / DualPipeV, default off): after each step's first forward (FSDP's only read of the shards in the pipeline) the sharded fp32 parameters go to pinned host memory on a side stream, and come back after the last forward; empty_cache() after the optimizer step releases the reduced-gradient pool cached on FSDP's reduce-scatter stream. Frees 8 bytes per parameter / FSDP group size (1.13 GB per MoE block at D=12). Same values; GPU validation pending. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… stream x_t (~250 MB per MoE block per kept micro-batch) is allocated on the compute stream and filled by column_quant on the side stream, so its memory returns to the pool activations draw from instead of the side stream's. Bitwise (same kernel). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… cuts model.ac.layer_modes entries may be [attention-half mode, MoE-half mode] for a layer cut by a stage boundary; each stage's half takes its own mode. A pair on an uncut layer is an error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…every, paced - Reloads allocate from the compute stream's pool; the copy stream's own pool kept freed memory out of reach of activations (same values). - every (default 1): offload only every k-th micro-batch, to stay within host-link bandwidth. - paced (default off): before an offloaded forward, the GPU waits for the previous swap-out, so warmup forwards cannot outrun the copies. GPU validation pending. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…DP's fp32 buffer model.quantization.accumulate_wgrad_fp32 (FP8Config, default off): each Float8BlockwiseLinear's DeepGEMM weight-gradient GEMM writes into FSDP's fp32 unsharded accumulator, as the FP8 experts already do, skipping the fp32 zero fill, the bf16 cast and FSDP's bf16->fp32 add per micro-batch. Registered in setup_fsdp; no effect without FSDP. Changes numerics (no per-micro-batch bf16 rounding of dense weight gradients; per-op rel L2 vs fp64 3.465e-2 vs 3.470e-2 today). Stage bench (EP8, 8k, 6 layers): -0.52 ms/layer, ~0.25 GB/layer less transient memory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ckpoint model.compile.inside_checkpoint (default off): apply_compile compiles the block inside its CheckpointWrapper, the path it already takes for blocks holding a nested FSDP unit. With AC on, the expert dispatch (torch.compiler.disable) is a graph break inside the checkpoint's higher-order op, which sends the whole block to eager, so today's block compile does nothing; inside an eager checkpoint the dense code around the break compiles. Works with every AC mode, per-layer modes and cut pipeline stages. Changes numerics slightly (Inductor rounding). Stage bench (EP8, 8k, 6 layers): -0.73 ms/layer, -0.2 GiB peak. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… accumulate_wgrad_fp32 With model.quantization.accumulate_wgrad_fp32 on, dense FP8 linears hold their gradient in FSDP's fp32 accumulator while the group's other parameters hold bf16 gradients, so a reduce on the same micro-batch (non-PP, or the last micro-batch) failed with "FSDP reduce-scatter expects uniform gradient dtype". When reduce_grads is set, post_backward now first converts every parameter of such a group to an fp32 accumulated gradient (FSDP's to_accumulated_grad_if_needed). Deferred reduces were already converted by FSDP. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e mHC write-back For V4/V4.1 shared experts whose projections are all Float8BlockwiseLinear (always on, bitwise): - One gate|up FP8 GEMM (prime_rl::fp8_gate_up_mm): x cast once, both weights block-cast into one stacked operand. - A Triton clamped-SwiGLU kernel that also emits h's per-token FP8 cast, consumed by the down projection through new optional x_fp8 / x_sf arguments of prime_rl::fp8_blockwise_mm. - A backward kernel writing d[gate|up] straight into the FP8 operands the dgrad/wgrad GEMMs read; x's transposed cast runs once. accumulate_wgrad_fp32 works as before. - MoE.forward(split_shared_output=True) returns routed and shared outputs separately and the V4.1 layer adds them inside fused_post_bda (HAS_X2), rounded to bf16 exactly as the add was. fp8_gate_up_mm is saved wherever fp8_blockwise_mm is (FUSED_OPERATIONS); the SwiGLU is recomputed. Outputs, activation and weight gradients torch.equal to before on 1 GPU under AC none, selective, projections, full and full_moe; 8-GPU stage forward and shared-expert fp32 grads equal. Stage bench (6 layers, 8k, selective): -0.66 ms/layer. Not yet run inside a PP job. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s into one GEMM launch model.moe.dispatch.fused_wgrad_micro_batches (FP8 Mega MoE, 1-4, default 1 = today's behaviour): n > 1 holds a layer's expert wgrad operands (~1.17 GB per MoE layer per held micro-batch at 8k) until the n-th micro-batch's backward, which adds all held sets with one K-grouped GEMM launch per weight (prime-mega-moe k_grouped_fp8_gemm_nt_contiguous_multi, 125a11a). Bitwise equal to one launch per micro-batch in order (torch.equal on the fp32 accumulators). The step's last micro-batch never holds: the pipeline and the non-PP reduce-once path mark it with set_expert_wgrad_final_micro_batch; flush_pending_expert_wgrads raises if a held gradient would outlive its accumulator. With n = 1 the kernel is called without the new arguments, so older prime-mega-moe builds keep working. At most 2 with wgrad_tile_scales. Stage bench (EP8, 8k, 6 layers): n = 2 -0.77 ms/layer (+7 GiB peak), n = 4 -1.51 ms/layer. Validated in PP on 6 nodes by Track B (losses equal within FP8 noise, -2.0% cycle at n = 2). Net loss at 80 GB; recommended 2 on H200. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
model.moe.dispatch.fused_wgrad_micro_batches accepts an int (every stage, unchanged) or a list with one entry per pipeline stage (pp * pp_stages_per_rank, stage order as pp_layers_per_stage), so memory-bound plans can fuse expert wgrads only on stages with headroom. configure_moe_runtime picks the stage's entry. The list length is validated, and every entry must be <= 2 with wgrad_tile_scales. Default unchanged (1); bitwise as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… source The index-source layer builds the (1, t, 1, 640) int32 window + top-k slot list as before and stores it in SharedAttnState.top_k_indices in place of the raw picks; consumer layers pass it to SparseAttnInputs.build(indices=...), which then only lays out kv_buf. Removes five small eager kernels (~75 us) per consumer layer in forward and again in recompute (~-0.11 ms/layer averaged over layers 20-39). Bitwise: the kernels receive the same int32 values. Pipeline stages now send the int32 slot list instead of the int64 picks (~35% fewer bytes per boundary for that tensor). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion checkpoint" This reverts commit e3b8e41. model.compile.inside_checkpoint changes gradients well beyond rounding under selective-style activation checkpointing, where its gain comes from (step-1 grad norm +1.5% against baselines that agree to 1e-4, loss +0.06-0.08); the root op is not pinned. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
samsja
added a commit
that referenced
this pull request
Oct 10, 2026
Replaces the three switches `model.fsdp_cpu_offload`, `model.optim_cpu_offload` and `model.full_offload` with one field: ```toml [model] offload = "optimizer" # default; also "full", "none" ``` This applies to both the RL trainer and SFT. The `fsdp` switch is not migrated: the mode is removed outright (rationale below). The `[model.full_offload] numa_bind` knob is dropped: no recipe, deployment, template or doc sets it. Full offload now always pins each rank's CPUs to its GPU's NUMA node. That was already the default. Ranks whose GPU reports no NUMA node still skip binding with a warning. On hosts without GPU sysfs topology (`/sys/bus/pci/devices/<bus>/numa_node`), binding is no longer optional: full offload fails there instead of being switched off. Why: the three switches could only be used one at a time, enforced by `cpu_offload_mutual_exclusion`. Because `optim_cpu_offload` defaulted to `true`, enabling full offload also meant writing `optim_cpu_offload = false`, otherwise validation failed (see the old `configs/advanced/deepseek-v4-flash/sft.toml` comment). With one value the conflicting combinations cannot be expressed, so that validator is deleted. The default behaviour does not change: optimizer-state offload stays on. Why drop `fsdp`: the mode was the stock torch FSDP2 `CPUOffloadPolicy` with no in-tree config, example, or test user; Muon rejected it and it broke NCCL weight broadcast. It was kept only for long-context runs whose fp32 masters/moments no longer fit on GPU (the reason it was restored in #3052); the `full` mode added in #3234 keeps FP32 masters, moments, and gradients in host RAM, covering that memory pressure instead. Gone with it: the `CPUOffloadPolicy` selection in `setup_fsdp`, CPU-side model init in `setup_model`/`load_dcp_from_hf`, `_move_buffers_to_cuda`, and the fsdp leg of the Gloo special case. Changes: - `ModelConfig.offload: Literal["none", "optimizer", "full"] = "optimizer"` replaces the three fields. `OptimizerInBackwardOffloadConfig` (and with it `numa_bind`) and its bool-normalizing `BeforeValidator` are gone. - Validators in `TrainerConfig` and `SFTConfig`: - full offload accepts only AdamW or SignSGD and disables clipping, now reading `offload == "full"`; - the `Muon optimizer does not support FSDP CPU offload` validators are deleted with the mode. - Runtime: - `setup_optimizer(..., offload=...)` replaces `cpu_offload` + `full_offload_config`, and its "both enabled" check is gone. - `FullCPUOffloadOptimizer` drops its unused `offload_config` argument. - `setup_full_cpu_optimizer_offload()` takes no arguments and always binds. - `model.py`: FSDP always uses the default `OffloadPolicy()`; CPU-side model init in `setup_model`/`load_dcp_from_hf` and `_move_buffers_to_cuda` are gone. - Tomls: `glm-4.5-air/swe-budget.toml` and `ci/integration/reverse-text-sft/full-offload-resume.toml` → `offload = "full"`; `glm-5.3/sft/h200/16k-12-node.toml` → `offload = "none"`; `glm-5.3/sft/h200/base.toml` → `offload = "optimizer"`. Also updated `docs/scaling.md`, the `deepseek-v4-flash/sft.toml` comment, and `skills/training/start-run/SKILL.md`, which documented the removed fields. Breaking: the old keys now fail with `extra_forbidden`, and `offload = "fsdp"` fails the `Literal`. There is no shim. Interaction with #3849 (one trainer-common config base): #3849 moves the full-offload and Muon/FSDP validators from `TrainerConfig`/`SFTConfig` onto `BaseTrainerConfig`. This PR edits the full-offload validator's condition and deletes the Muon/FSDP one, so the conflict is textual only: keep #3849's single copy of the full-offload validators with `self.model.offload == "full"` and drop its Muon/FSDP copy. Whichever lands second takes a small rebase. Other open PRs that set or read the old keys need a rebase: - tomls: #3883, #3879, #3766, #3765, #3729, #3629, #3627, #3616, #3544 - code: #3885 (`optim_cpu_offload`), #3739 (`full_offload` validators), #3724, #3719, #3699, #3610, #3487 (`fsdp_cpu_offload` / `enable_gloo`) Test plan - `pytest tests/unit/orchestrator tests/unit/test_configs.py tests/unit/train/test_cpu_adam.py tests/unit/train/test_state_offload.py -m "not gpu"`: 336 passed, 1 skipped (Qwen3-VL snapshot not cached on this host), 4 deselected (GPU; this host has no GPU). The existing offload config tests are updated to the new field. `test_load_configs` covers the 4 edited tomls. - `cli(TrainerConfig|SFTConfig, ...)`: `--model.offload full|none` and the default (`optimizer`) resolve as expected; `optim_cpu_offload` and `offload = "fsdp"` are rejected. - `import prime_rl.trainer.rl.train, prime_rl.trainer.sft.train` works. - `ruff check` and `ruff format --check` on the changed Python files: clean. - Not run: GPU or multi-node training with `offload = "full"`. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Breaking TOML/API surface touches distributed training memory paths (FSDP offload, optimizer-in-backward, Gloo); behavior should match prior modes when mapped correctly, but mis-migrated configs will fail at parse time. > > **Overview** > **Breaking config change:** `model.fsdp_cpu_offload`, `model.optim_cpu_offload`, and `model.full_offload` are replaced by a single **`model.offload`** setting (`"optimizer"` default, or `"full"`, `"fsdp"`, `"none"`). RL and SFT share the same field; example TOMLs and scaling docs are updated accordingly. > > The mutual-exclusion validator between the old flags is removed because only one mode can be expressed. **`[model.full_offload]` / `numa_bind` is dropped** — `"full"` always runs NUMA binding and CPU optimizer thread setup via parameterless `setup_full_cpu_optimizer_offload()`. Runtime paths (`setup_optimizer`, FSDP CPU offload in `model.py`, Gloo when offload is `"full"` or `"fsdp"`) key off the new enum; `FullCPUOffloadOptimizer` no longer takes an offload config object. > > Default training behavior is unchanged: optimizer-state CPU offload remains the default (`"optimizer"`). Old TOML keys are not aliased and should fail validation. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 35da73a. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: sami jaghouar <sami.jaghouar@gmail.com>
…r DeepSeek-V4.1 residual_type on the V4.1 text config: "mhc" (default, today's code path, bitwise unchanged), "gated" or "layerscale", the latter two requiring hc_mult = 1. "gated" is mHC's post gate at one stream, x + g * (f(x) [+ f2(x)]) with g = 2 sigmoid(scale * rstd(x) (x . w) + base) per token, initialized to g = 1; "layerscale" is x + lambda * f(x) per channel. Both keep the gates_and_collapse / update_states interface, so attention, MoE, engram and pipeline stage I/O are unchanged. A fused Triton forward/backward (prime_rl::dsv41_gated_residual) does the gate in one pass per token; it is a default selective-AC save target. Experimental architecture option: not checkpoint-compatible with DeepSeek's mHC residual gates; quality ablation pending. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
b6460ed (on top of 125a11a) adds the L2 prefetch of the accumulator tile and 128x144 dW2 tiles to the multi-set K-grouped wgrad: bitwise identical, -0.55 ms/layer at fused_wgrad_micro_batches >= 2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rime-kernels has it When prime-kernels provides fp8_dual_cast (a CuTe kernel writing both the 1x128 per-token and the token-padded 128x1 transposed e4m3 casts in one read), Float8BlockwiseLinear's backward casts grad_output with it, and dsv4_linear_rope's FP8 backward fuses the query gradient's inverse RoPE into the same pass, feeding fp8_blockwise_mm_backward through new optional precomputed-cast arguments. Bitwise identical to the Triton casts; falls back to them without the op, on UE8M0 devices or for block sizes other than 128. Stage trace: -0.17 ms/layer (cast kernels 0.93 -> 0.77 ms). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…batches 5827a5b (on top of b6460ed) orders the FP8 Mega MoE forward and backward tasks expert-major whenever no expert exceeds 16 M blocks, so each expert's weights are read about once: bitwise identical, -0.53 ms/layer, on the default path too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
optim.recipe = "deepseek" (Muon; default "dion", unchanged; requires optim.embedding_update) follows the V4 report (§2.4, Algorithm 1) and V4.1 §2.5: Nesterov momentum, 10 hybrid Newton-Schulz iterations in bf16 with fp32 accumulation, updates rescaled to RMS optim.update_rms (0.18), AdamW epsilon 1e-20, and one Newton-Schulz per logically independent matrix: each routed expert (gate and up split), each attention and indexer query head, each o_a group, the engram wkv keys and value, and the mHC pre/post/comb mixes. Engram gate q/k weights are treated as norm weights (AdamW with weight decay) in the embedding_update split. muon_step orthogonalizes parameters whose shard holds whole matrices (the experts, on their expert-DP shard owner) in place, and sends every other matrix to one owner rank chosen by FLOP balance with one bf16 all_to_all per FSDP group, overlapped with the local expert work, Sinkhorn and AdamW. Triton Newton-Schulz stays off (it kept gradients alive). GPU tests (test_muon, test_sinkhorn): 11/11 on H200; 8-GPU sharded step bitwise vs single GPU (one case 3e-4 from bf16 batching); 250-step 2-node proxy within run-to-run noise. Cost on a 3-layer stage: optimizer step 18 -> 150 ms, +4 B/param momentum. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Performance work for DeepSeek-V4.1 Flash, stacked on #3879 (base
feat/deepseek-v41).Changes
DeepEP works under activation checkpointing: the communication buffer is now built when the dispatcher is created. Built lazily inside the first checkpointed forward, it shifted the recompute's cached ops.
Single-pass DeepEP local layout: one Triton gather into the padded expert-grouped layout and one fp32 score-weighted gather-sum back, with gathers in backward too (
models/kernels/moe_permute.py). This replaces about 7 full copies of the routed activations per layer.DeepEP in the 12-node example: 9.3 s -> 5.6 s per step.
Hopper kernels from prime-kernels, each used only when available:
dsa_indexer_topk, and its mHC usesmhc_projection.model.moe.compute.backend = "prime_kernels"runsmoe_experts.FP8 routed experts (
model.moe.compute = {type = "deepgemm_fp8", backend = "prime_kernels"}): DeepGEMM blockwise FP8 grouped GEMMs (forward, dgrad, wgrad). The quantization and clamped SwiGLU are fused into the passes around them, via prime-kernels'moe_experts(fp8=True)(feat: Hopper DeepSeek-V4.1 training kernels (indexer top-k, MoE experts bf16/FP8, mHC, sparse attention bwd) prime-kernels#15). At V4.1's EP=96 shape this is ~1.4x faster than bf16. The existing DeepGEMM path is no faster than bf16, because its standalone quantization passes eat the gain.fp8.tomloverlay for V4.1: FP8 dense linears plus FP8 experts. The 512-wide KV projections and the indexer's query projection stay in bf16; measured in FP8, they come out at 0.81x and 0.99x of bf16 speed.Cheaper FP8 casts for dense linears: each cast takes one reciprocal per group with an FMA correction instead of an exact divide per element, and is exhaustively bit-identical. The transposed cast pads the token dimension itself, so there are no
F.padcopies. Scales are written in DeepGEMM's layout, which removes its scale-transpose kernels. About 5% less GPU time per FP8 linear.Sparse attention backward: uses prime-kernels' Hopper
dsa_sparse_attn_bwdwhen it is available (~1.2x cuDNN), and falls back to cuDNN otherwise. The gain is within noise end to end, because the saved time lands in DeepEP waits.optim.apply_in_backward(SignSGD, SFT): applies the update to each parameter as soon as its gradient is reduced, then frees the gradient. It is exact, because sign updates ignore the loss scaling. Peak memory drops from 117.6 to 96.9 GiB at the same step time. Spending that memory on 24k tokens per GPU fits but is slower per token (242k vs 330k tokens/s).Copy-free grouped
o_a_proj(V4 and V4.1): one GEMM per group on strided slices, instead of a batched matmul over a transposed view that copied the output in forward and the gradient in backward. Bit-identical, and 1.23x faster fwd+bwd.Fix: FP8 / MXFP8 linear conversion now swaps only plain
nn.Linear. Subclasses with their own forward (V4's block-diagonalo_a_proj) were silently replaced and broke DeepGEMM downstream.Engram: the tables are plain
nn.Embeddings, row-sharded by a newAllToAllEmbeddingParallelstyle next toEmbeddingParallel. Deduplicated ids go to their owners and only those rows come back.RowwiseParallelwould instead reduce-scatter ~19 GB of partial rows per table per rank at 96 GPUs. The custom table class is gone from the model.DeepEP as one custom op (
prime_rl::deepep_moe):Optional FP8 token dispatch (
[model.moe.dispatch] fp8 = true, DeepEP withdeepgemm_fp8experts): tokens are quantized per 1x128 block with power-of-two scales before the forward dispatch, then dequantized exactly on arrival to the bf16 the experts quantize again. Forward dispatch traffic and the received tokens kept for backward halve; gradients stay bf16. In a microbenchmark at the trainer's shapes (64 ranks) a dispatch takes 7.5 ms instead of 14.2 ms. End to end: 4.8 s -> 4.7 s per step and 4 GiB less peak memory, with the loss curve unchanged within noise.In-place RoPE for V4.1's query and for the gradient of the attention output: only the 64 rotary channels of each 512-wide head are read and written, instead of copying the whole head (6.3x faster per call at 16k tokens, bit-identical). Within noise end to end: like the faster attention backward, the saved compute turns into DeepEP waits.
Fix: DeepEP's local experts run in row slices when routing sends a rank more token-expert pairs than prime-kernels' FP8 path addresses (32-bit offsets, ~419k rows at V4.1's width). Before, that rank raised and every other rank hung in DeepEP until its GPU timeout.
Pipeline parallelism (
model.pp,pp_schedule,pp_stages_per_rank,pp_layers_per_stage): stage build inmodel.py,trainer/pipeline.py(pooled receive buffers, an async 1F1B schedule, a no-grad shape probe for static shapes),prune_to_pipeline_stage/pipeline_stage_forwardfor V4.1, PP-aware data ranks and grad clipping across the PP group. Losses matchpp = 1exactly.Kept expert activations (
model.moe.dispatch.keep_expert_activations): the routed experts' activations are kept for backward instead of recomputed (−5.8% step on a 5-layer EP64 proxy, +3.2 GB per layer).FP8 expert weights quantized once per optimizer step, and FP8 weight gradients accumulated straight into FSDP's fp32 buffer (prime-kernels
moe_experts_fp8_quantize_weights/moe_experts_fp8_backward_accumulate): +8.7% and +9.1% on a PP2×4 proxy.dispatch.weight_grads_after_combine: each chunk's gradient is sent back before its weight-gradient GEMMs run, so those GEMMs hide the send. Bitwise-identical gradients; −2.5 ms per layer at EP48.moe.reduce_local_expert_grads_once: when each expert FSDP group is a single rank, its per-micro-batch FSDP work (cast, reduce copies) runs once per step. −4.2 ms per layer at EP48; same up to fp32 summation order; ~0.65 GiB more peak memory per layer.dispatch.fp8_grad(off by default, changes numerics): FP8 backward dispatch, halving its bytes. −2.5 ms per layer at EP48.Spread balanced routing:
debug.force_balanced_routingnow strides experts by 97, so a token's 6 experts land on ~4–5 nodes like the real router. The old round-robin sent every token to one rank, which hid nearly all inter-node traffic.prime_rl::dsv4_linear_rope: V4.1's query projection and its RoPE as one op that rotates the projection output in place and keeps no unrotated copy for backward. Bitwise identical; −0.46 ms and −1 GiB per layer. It is in the default selective-AC save list; runs passing an explicit--model.ac.targetsmust add it. RoPE, mHC and q-norm ops are also saveable by selective AC.mHC stream-gradient sum folded into prime-kernels' projection backward when the installed build has
hyper_connection_streams(bitwise identical, −0.47 ms per layer); otherwise the old path.Fused Triton cross-entropy gradient in the chunked LM head: 131.7 -> 96.1 ms per 16k tokens, exact.
dispatch.type = "mega_moe"(off by default): prime-mega-moe's fused BF16 Hopper Mega MoE as a backend (distributed/mega_moe.py), one custom op with gradients for x, top-k weights, routed and shared expert weights.dispatch.num_smscaps its SMs: at the full count it deadlocks against FSDP's NCCL kernels. On one node it is slower than DeepEP + FP8 experts (97–105 vs 80.3 ms per layer), because the backward recomputes the forward and it is BF16.debug.random_initdraws per-parameter seeded N(0, 0.02). Before, it materialized zeros and kernels ran 1.5–3.5x too fast. Setup order is now AC -> FSDP -> compile.Pipeline-parallel push (PP16 x EP8 plan of record)
perf.pydrops the attention term; FLOPs per token = 6 x active matmul params (LoRA keeps its 4/2/6 split).PerfCounterno longer takesseq_len.AsyncPipelineSchedule): runs a precomputed op order and never waits on a stage transfer before it is needed. SchedulesAsync1F1B(withpp_warmup_step) andDualPipeV.pp_layers_per_stagetakes multiples of 0.5, so a stage boundary can cut a V4.1 layer between its attention and MoE blocks. Sent outputs that no backward reads are freed early. Grad clipping and the loss broadcast handle several stages per rank and V schedules. Schedule-order unit tests are added.pp_transport):nccl(default),copy_engine, andput.putuses one-sidedncclPutSignal/ncclWaitSignalon zero-CTA 2-rank communicators, so a transfer in flight holds no SMs and no longer slows the persistent kernels. It needs NCCL >= 2.30, andnvidia-nccl-cu13is now overridden to 2.32.3.pp_transport_ctasandpp_gemm_smspin SM budgets fornccl.pp_activation_offload, default off): keeps in-flight micro-batches' saved activations in pinned host memory between forward and backward.target = "fp8"instead keeps them on the GPU as FP8 with 1x128 scales. That changes numerics, is unverified, and measured slower than fp8t.model.pp_defer_expert_weight_grads, default off): FP8 Mega MoE weight-gradient GEMMs run after the stage posts its input-gradient send. Same GEMMs, order and accumulators. On 6 nodes, steps 1–3 are bitwise and later steps are within FP8 noise. +1–3 GB.model.pp_offload_masters, default off; functionally validated; memory/step cost unvalidated, needs a run with expert DP > 1; keep default off): the sharded fp32 masters stay in pinned host memory between a step's first and last forward. The reduced-grad pool is released after the optimizer step. Frees 1.13 GB per MoE block at D=12; same values.every(default 1) offloads one micro-batch in k.paced(default off) throttles warmup forwards to the swap-outs.PutEdgecleanup was parity-checked on 6 nodes (identical losses, per-stage times within 1 ms).fp8_transposed_on_demandalso accepts a list of pipeline stages.model.ac.layer_modesaccepts[attention, MoE]pairs for layers that a stage boundary cuts.PutEdgechecks for NCCL >= 2.30 at runtime. The FP8 Mega MoE wgrad operand is allocated from the compute stream's pool (bitwise).moe.dispatch.fp8, default off): prime-mega-moe's SM90 FP8 kernels for the routed experts. The forward keeps FP8 inputs and the bf16 gate/up output, so the backward does not recompute the forward. Weight gradients go through DeepGEMM K-grouped FP8 GEMMs into FSDP's fp32 accumulators. Sub-flags, each default off:capacity_factorwgrad_tile_scales(changes numerics)free_bf16_expert_weights(same numerics; −3.4 GB per layer at EP8)fp8_transposed_on_demand(bitwise; −1.7 GB per layer, ~0.95 ms per MoE backward)model.quantization.accumulate_wgrad_fp32, default off; 250-step study: within run-to-run noise): each dense FP8 linear's weight-gradient GEMM writes straight into FSDP's fp32 accumulator. This removes the bf16 rounding per micro-batch. −0.52 ms/layer on a 6-layer EP8 stage bench. Before a reducing post-backward, the group's other gradients are upcast to fp32, so FSDP's reduce-scatter sees one dtype.torch.equalunder every AC mode. On 8 GPUs, the stage forward and the shared-expert fp32 grads are equal.moe.dispatch.fused_wgrad_micro_batches, FP8 Mega MoE; default 1 = today's behaviour; recommended 2 on H200): n > 1 holds a layer's wgrad operands, then adds n micro-batches with one K-grouped GEMM launch per weight.feat/sm90-fp8-mega-moe-v2, also onkern2/moe), which includes b6460ed and 125a11a._Cmust be rebuilt for b6460ed.prime_mega_moeout of band), so that commit is not pinned inpyproject.toml.residual_type = "mhc" | "gated" | "layerscale"on the V4.1 config; default"mhc", bitwise unchanged; quality ablation pending):gatedis mHC's post gate athc_mult = 1, run by a fused Triton op (prime_rl::dsv41_gated_residual, a default selective-AC save target). Not checkpoint-compatible with DeepSeek's mHC gates.layerscalewas worse.fp8_dual_cast,grad_output's per-token and transposed casts take one pass, and the q_b gradient's inverse RoPE is fused into it. −0.17 ms/layer (cast kernels 0.93 → 0.77 ms) once prime-kernels hasfp8_dual_cast(prime-kernels 2ef8b4c, PR update qwen #15).model.ac.modeaddsfull_moe(also recomputes the FP8 Mega MoE),projections,attentionandmatmul.model.ac.layer_modessets a mode per layer, indexed by full-model layer so it works across pipeline stages. The default (full) is unchanged.q_weight * k_weightin fp32, as DeepSeek's reference does. This changes numerics toward the reference.wkvprojection is no longer checkpointed by default.model.ac.recompute_engram(default off) restores the recompute onfull/full_moelayers. This keeps 51 KB/token more per engram layer than before.model.engram_offload(default off, prototype) keeps the table shards and their optimizer state in host memory. Checkpointing offloaded tables is not implemented.optim.embedding_updatefor Muon (default off): V4.1's parameter split, with the Sinkhorn-balanced momentum update for embeddings, LM head and engram tables (MuonWithSinkhorn).optim.recipe = "deepseek", default"dion"unchanged; requiresembedding_update): Muon as in the V4/V4.1 reports. It uses Nesterov momentum, 10 hybrid Newton-Schulz iterations in bf16, update RMS 0.18 and AdamW eps 1e-20. Each expert, query head, o_a group, engram wkv K/V and mHC mix is orthogonalized as its own matrix. The engram gate q/k weights are treated as norm weights with weight decay.dsv41_sparse_attn_ropefuses V4.1's sparse attention with the in-place inverse RoPE of its output, keeping only the rotary channels the backward needs.Required kernel commits
feat/hopper-dsv41-kernels, feat: Hopper DeepSeek-V4.1 training kernels (indexer top-k, MoE experts bf16/FP8, mHC, sparse attention bwd) prime-kernels#15):out_tail, the register-sourced dKV MMAs andfp8_dual_cast.feat/sm90-fp8-mega-moe-v2, also onkern2/moe) for the FP8 Mega MoE.Known numerics
Pipeline results (8-node emulation of PP16 stages 0–7, 8k tokens/GPU/micro-batch, M = 320, 1F1B)
MFU is 6·N·D (16.13B active, 989 TFLOP/s). Days are for 20T tokens. All rows are measured on the emulation and extrapolated with the measured-cycle method.
pp_transport = ncclpp_transport = putputput+fp8_transposed_on_demandWith
put, the pipeline cycle drops by 24.3 ms (14.9%) on H200. The step-10 loss and grad norm matchnccl.Results with spread routing (16k tokens/GPU, random init, selective AC, FP8, FP8 dispatch)
Per-layer cost is the marginal between two layer counts of reduced proxies; the 40-layer projection adds it to the measured step. MFU is 6·N·D with 16.13B active params at 989 TFLOP/s.
weight_grads_after_combine,reduce_local_expert_grads_once,fp8_gradThe 12-node table below predates spread routing and the random-init fix, so it understates communication.
Earlier results (12x8 H200, 16k tokens/GPU, real weights, fake data, round-robin routing)
overlap_chunks = 2)fp8.toml)dispatch.fp8 = true)These results use
fake.toml, which also forces round-robin expert routing (force_balanced_routing). With the real router the 12-node config does not fit yet: on real math data (INTELLECT-3-SFT-10K) a rank runs out of memory in the first step. On 2 nodes with the first 4 layers, real data gives a max expert load ~20x the mean (Max Vio18-20) and 1.1-2.0 s steps against 0.72 s with forced balancing.cached_notifybarrier.Tried and parked: two-batch overlap
Branch
exp/deepseek-v41-tbo(not part of this PR).How it works:
Results:
Why so little: in the trace, DeepEP's internode kernels start a median of 12–16 ms after launch, because they need all their SMs co-resident and wait behind persistent or cooperative GEMMs.
Testing
tests/unit/test_configs.py(203 passed).dsv4_linear_rope(bf16 and FP8; eager, compiled, SAC), the mHC stream-gradient path,weight_grads_after_combineand the Mega MoE backend; prime-kernelsmhc_projectiontests (16 passed).ruff checkandruff format --checkon every changed file: clean.tests/unit/test_configs.py,tests/unit/train/test_perf.py,tests/unit/train/test_pipeline_orders.py: all passed (test_configs,test_pipeline_orders,test_checkpointingandtest_deepseek_v4rerun after the latest commits;test_deepseek_v4's 8 GPU-only tests fail here with no NVIDIA driver, the same 8 as on the base).tests/unit/train/models/test_checkpointing.py: 2 failures, with the same 2 on the base commit.prime_rl::fp8_indexeris not registered without a GPU.tests/unit/train/test_sinkhorn.pyandtest_muon.py: GPU-only, so they fail here (no NVIDIA driver). Track optim ran them on H200: 11/11 pass.🤖 Generated with Claude Code