Skip to content

Engine and model-layer refactor#70

Merged
MasterJH5574 merged 39 commits into
mlc-ai:mainfrom
haok1402:0709-codebase-refactor
Jul 14, 2026
Merged

Engine and model-layer refactor#70
MasterJH5574 merged 39 commits into
mlc-ai:mainfrom
haok1402:0709-codebase-refactor

Conversation

@haok1402

@haok1402 haok1402 commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Engine and model-layer refactor

A cleanup of the engine and model layer. The training recipe is unchanged; the work removes plumbing that only forwarded arguments around and tightens the interfaces between the models, the pipeline, and the runtime.

Changes

Runtime contexts. Process-global state (distributed groups, the model and optimizers, the logger) lives in per-concern context modules, each filled in once at startup by a setup_* function and read directly where needed. This replaces the per-task Ctx objects that were threaded through every call and the *_context managers that wrapped setup.

Config passed down, not expanded early. Modules take the transformer config object and read the fields they need locally, instead of callers unpacking scalars and threading them through constructors. Fields are pulled out only where a value genuinely differs from the config (an override or a derived size), which keeps each module's dependencies visible at its own definition.

Decoder-layer stage naming. The layer's three compute entry points are renamed to forward_stage1/3/5, matching the pipeline's five-stage attention → dispatch → MLP → combine → aggregate split (stages 2 and 4 are the engine-driven all-to-alls). The routing metadata that stage 1 produces and stage 5 consumes now travels as one named record instead of a bare tuple.

Engine consolidation. The autograd wrappers and dispatch/combine helpers moved in alongside the stage implementations, so the forward/backward flow reads in one place. The overlap loop and layer partitioning were simplified in the same pass.

Variable-sequence-length interface. The pipeline can carry per-document boundaries through to attention and the position embedding, the groundwork for packed / variable-length (SFT) training. The residual stream stays batch-of-one BSHD; only attention sees the flattened view, and with no boundaries set the behavior is identical to before. No entrypoint produces packed batches yet (the corpus-packing data pipeline is a follow-up); the framework side is checked with synthetic boundaries in the multi-GPU test.

Cleanups. Each model defines its own stage methods and keeps its helpers local to its file, so it reads top to bottom on its own; DeepSeek dropped its "lite" naming for the base family; run logs unified under one directory; the multi-GPU test renamed; wandb off by default in examples; docs updated.

Validation

Compared against the pre-refactor baseline on two models spanning different model families, parallelism meshes, and load-balance strategies, both at a constant learning rate. The base-vs-feature load-balance loss curves are on wandb:

Model Mesh Load-balance wandb (base vs feature)
Qwen3-30B-A3B PP4 · EP8, 4 nodes global-batch wandb report
DeepSeek-V2-Lite PP2 · EP2 · DP2, 1 node sequence wandb report

The curves match: load-balance loss rises, turns over, and decays on both, and cross-entropy agrees within bf16 run-to-run noise.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors PithTrain's state management by replacing context managers with process-global context modules, updates the 5-stage decoder layer protocol, and introduces support for variable-length sequence packing with token-weighted loss. It also renames the FSDP integration tests to test_dualpipev. The review comments correctly identify critical runtime bugs where torch.distributed.broadcast is called with the invalid argument group_src instead of src in both pithtrain/dualpipe/dualpipev.py and pithtrain/tasks/pretrain_lm.py.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread pithtrain/dualpipe/dualpipev.py
Comment thread pithtrain/dualpipe/dualpipev.py
Comment thread pithtrain/tasks/pretrain_lm.py
@haok1402
haok1402 marked this pull request as ready for review July 11, 2026 23:08
@haok1402
haok1402 requested a review from MasterJH5574 July 11, 2026 23:17
@haok1402

haok1402 commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator Author

As part of the refactor, we've bumped cutlass-dsl >= 4.6.0 and get rid of the hack that avoids redundant -libs-base wheel install. See NVIDIA/cutlass#3259 (comment) for more details.

@haok1402
haok1402 force-pushed the 0709-codebase-refactor branch from 8c05124 to 972e712 Compare July 13, 2026 15:49
@haok1402

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Jul 13, 2026

Copy link
Copy Markdown

Code review

Reviewed with two independent CLAUDE.md/AGENTS.md-compliance passes and two independent bug-hunting passes, then adversarially validated every flagged issue. No logic bugs were found — the group_src= broadcast kwarg usage, the cu_seqlens/num_tokens pipeline broadcasts, layer_partition, and the RoutingInfo/AllToAllSplits plumbing all check out correctly. 3 documentation/testing-gotcha regressions introduced by this PR were confirmed:

  1. pithtrain/operators/grouped_linear.py breaks the documented "tests skip gracefully without deep_gemm" gotcha. The new consolidated file does an unconditional, module-top-level import deep_gemm (no try/except), merging what used to be a deep_gemm-free BF16-only module (pithtrain/layers/group_linear.py) with the FP8 code. tests/test_grouped_linear_correctness.py imports GroupedLinear from it at module scope with no skip guard (unlike sibling files test_fp8_quantize_kernels.py / test_deepgemm_fp8_linear_correctness.py, which use a try/except ImportError + pytest.mark.skipif pattern). Result: collecting this test now hard-fails with ImportError in any environment without deep_gemm installed, contradicting AGENTS.md's Testing Gotchas: "Tests skip gracefully when deep_gemm is not installed or CUDA is unavailable."

    from functools import partial
    from typing import Optional
    import deep_gemm
    import torch
    import torch.nn as nn
    import torch.nn.functional as F

  2. New AGENTS.md sentence cites the class name this same PR deletes. pithtrain/models/interface.py renames DecoderLayerProtocolLayerProtocol in this PR, but the newly-added AGENTS.md sentence in the "DualPipeV Pipeline" section still reads "...on DecoderLayerProtocol (pithtrain/models/interface.py)" — stale the moment it's introduced.

    pith-train/AGENTS.md

    Lines 84 to 88 in 972e712

    5. **Aggregate** — Weighted expert output + residual connection
    Stages 1, 3, and 5 are the layer's compute entry points — `forward_stage1`, `forward_stage3`, `forward_stage5` on `DecoderLayerProtocol` (`pithtrain/models/interface.py`); stages 2 and 4 are all-to-all communication driven by the execution machinery (`pithtrain/dualpipe/execution.py`), not layer methods.
    Key files:

  3. AGENTS.md's "Key files" list still describes two files this PR deletes. pithtrain/dualpipe/modeling.py and pithtrain/dualpipe/layer_partition.py are both deleted (folded into execution.py/dualpipev.py), but the "Key files:" bullet list under "DualPipeV Pipeline" is left unchanged and still documents them as if they exist.

    pith-train/AGENTS.md

    Lines 90 to 95 in 972e712

    - `overlap.py``overlapped_forward_backward()` interleaved loop for one pair of micro-batches
    - `execution.py` — Stage implementations (`stage1_f`, `stage1_b`, etc.) and `ExecutionCtx`
    - `modeling.py``decoder_layer_forward/backward` autograd wrappers, dispatch/combine helpers
    - `layer_partition.py` — Distributes decoder layers across pipeline stages; edge stages (which hold `embed_tokens` / `norm`+`lm_head`) get fewer layers to balance memory.
    - `comm.py` — P2P communication setup between pipeline ranks
    - `utils.py``FP8WeightCacheControl` (cache quantized weights across micro-batches), `WeightGradStore` (deferred wgrad for zero-bubble scheduling)

Note: inline PR comments couldn't be posted in this run (the inline-comment tool wasn't available and gh api write access is blocked in this environment), so all findings are consolidated here instead.

@haok1402
haok1402 force-pushed the 0709-codebase-refactor branch from 989428c to 8e882e3 Compare July 13, 2026 17:10
@haok1402

Copy link
Copy Markdown
Collaborator Author
  1. pithtrain/operators/grouped_linear.py breaks the documented "tests skip gracefully without deep_gemm" gotcha. The new consolidated file does an unconditional, module-top-level import deep_gemm (no try/except), merging what used to be a deep_gemm-free BF16-only module (pithtrain/layers/group_linear.py) with the FP8 code. tests/test_grouped_linear_correctness.py imports GroupedLinear from it at module scope with no skip guard (unlike sibling files test_fp8_quantize_kernels.py / test_deepgemm_fp8_linear_correctness.py, which use a try/except ImportError + pytest.mark.skipif pattern). Result: collecting this test now hard-fails with ImportError in any environment without deep_gemm installed, contradicting AGENTS.md's Testing Gotchas: "Tests skip gracefully when deep_gemm is not installed or CUDA is unavailable."

Fixed.

@haok1402

Copy link
Copy Markdown
Collaborator Author

2. New AGENTS.md sentence cites the class name this same PR deletes. pithtrain/models/interface.py renames DecoderLayerProtocolLayerProtocol in this PR, but the newly-added AGENTS.md sentence in the "DualPipeV Pipeline" section still reads "...on DecoderLayerProtocol (pithtrain/models/interface.py)" — stale the moment it's introduced.

pith-train/AGENTS.md

Lines 84 to 88 in 972e712

5. **Aggregate** — Weighted expert output + residual connection
Stages 1, 3, and 5 are the layer's compute entry points — `forward_stage1`, `forward_stage3`, `forward_stage5` on `DecoderLayerProtocol` (`pithtrain/models/interface.py`); stages 2 and 4 are all-to-all communication driven by the execution machinery (`pithtrain/dualpipe/execution.py`), not layer methods.
Key files:

3. AGENTS.md's "Key files" list still describes two files this PR deletes. pithtrain/dualpipe/modeling.py and pithtrain/dualpipe/layer_partition.py are both deleted (folded into execution.py/dualpipev.py), but the "Key files:" bullet list under "DualPipeV Pipeline" is left unchanged and still documents them as if they exist.

pith-train/AGENTS.md

Lines 90 to 95 in 972e712

- `overlap.py``overlapped_forward_backward()` interleaved loop for one pair of micro-batches
- `execution.py` — Stage implementations (`stage1_f`, `stage1_b`, etc.) and `ExecutionCtx`
- `modeling.py``decoder_layer_forward/backward` autograd wrappers, dispatch/combine helpers
- `layer_partition.py` — Distributes decoder layers across pipeline stages; edge stages (which hold `embed_tokens` / `norm`+`lm_head`) get fewer layers to balance memory.
- `comm.py` — P2P communication setup between pipeline ranks
- `utils.py``FP8WeightCacheControl` (cache quantized weights across micro-batches), `WeightGradStore` (deferred wgrad for zero-bubble scheduling)

Fixed.

Comment thread pithtrain/models/gpt_oss.py
Comment thread pithtrain/dualpipe/execution.py Outdated
@haok1402
haok1402 force-pushed the 0709-codebase-refactor branch from a09513c to 94600e2 Compare July 13, 2026 19:40
@haok1402

Copy link
Copy Markdown
Collaborator Author

@claude review

Comment on lines +376 to +384
# Token-weighted reduction. The criterion returns per-micro-batch loss SUMS, so gradients
# accumulate d(sum loss); dividing by the total non-ignored token count yields the correct
# token-mean regardless of how tokens split across micro-batches. The count lives on pipeline
# rank 0 (the only rank with labels), so broadcast it to every stage for the gradient scale.
num_tokens = torch.ones((), device=device)
if distributed.pp_rank == 0:
num_tokens.fill_((global_labels != -100).sum().clamp_min(1))
if distributed.pp_size > 1:
torch.distributed.broadcast(num_tokens, group_src=0, group=distributed.pp_group)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Latent correctness gap: num_tokens is a per-rank local count, not the "global" count the comment claims — biased once packed/variable-length training goes live.

num_tokens is (global_labels != -100).sum() on this (dp, cp, ep) coordinate's pp-rank-0, broadcast only along the PP group. It is never all-reduced across DP/CP/EP. The final token-mean gradient is therefore correct only because FSDP2 reduces gradients with ReduceOp.AVG over the FSDP mesh (dp,cp,ep for non-expert params, dp,cp for experts — modules/training.py:341-346) and every rank's token count is equal:

AVG_r(dS_r / N) == (Σ_r dS_r) / (R·N) == Σ dS_r / Σ N_r ✅ only when all N_r are equal.

For the live fixed-sequence-length pretraining path all N_r are equal, so this matches main exactly and the wandb parity curves are expected. But this PR's headline feature is the variable-length/packed interface, and once tokens-per-rank differ:

AVG_r(dS_r / N_r) ≠ Σ dS_r / Σ N_r

the gradient (and the logged loss, via the sum/cp_size on lines 390-391) becomes biased by the per-rank imbalance. The fix is to all-reduce the count over the same mesh FSDP reduces on before dividing, rather than relying on equal counts.

At minimum, the "global non-ignored token count" wording here and in criterion (lines 116-118) overclaims — it's local — and will mislead whoever wires up the packed data pipeline into thinking the scaling is already correct for unequal counts.

@claude

claude Bot commented Jul 13, 2026

Copy link
Copy Markdown

Correctness Review

Traced the engine (execution/overlap/dualpipev), the 5-stage layer refactor + RoutingInfo, the FP8 linear consolidation + weight-cache change, the new cu_seqlens plumbing, and the loss/token-weighting rewrite against origin/main. The refactor is behavior-preserving and correct for the live dense-pretraining path — I could not find a bug that corrupts gradients, a parallel dimension, or a kernel edge case in that path. Detail below.

Verified sound (called out because these are the easy places to get a "forward-identical, gradients-wrong" refactor bug):

  • Stage refactor / RoutingInfo — every field the old tuple carried (topk_weight, expert_idxs, moe_local_idxs, expand_idx, dispatch_splits, combine_splits) is produced by prepare_dispatch and consumed by stage3/stage5 in the same positions; residual (incl. DeepSeek shared-experts fold-in) is carried unchanged; topk_weight is applied exactly once in stage5; the dispatch/combine input/output_splits orientation is identical in both fwd and bwd. run_backward now filters None pairs so the dense-layer topk_weight=None slot is handled.
  • cu_seqlens interface — with cu_seqlens=None the dense causal path and forward_posemb are bit-identical to before; varlen passes the single cu_seqlens/max_seqlen for both q and k (correct for self-attn); per-document position reset has no off-by-one and equals contiguous positions at cp==1; qwen3.5 asserts cu_seqlens is None. It is threaded as an explicit forward arg and the batch_dim=0 micro-batch scatter does not touch it.
  • group_src= broadcasts (dualpipev.setup_cu_seqlens, pretrain_lm.train_step) — correct, contrary to the earlier Gemini review. group_src=0 is a group-relative source rank (torch >= 2.12, which is pinned); using src=0 would target global rank 0, which is wrong for every non-first PP group. Please do not "fix" these back to src=.
  • FP8 weight cache — dropping FP8WeightCacheControl.enabled (was always True under FP8) for always-on, version-gated caching is behavior-preserving; step()/clear() now run unconditionally but only matter when FP8 modules exist, and the version bump each step() means the post-clear() stale-_wq_cache window is never observed in the train/inference loop.

One correctness gap worth addressing (latent, not live) — see inline comment on pretrain_lm.py:376-384:
The token-weighting rewrite divides gradients by num_tokens, but num_tokens is a per-rank local count broadcast only along PP — never all-reduced across DP/CP/EP. The token-mean is correct only because FSDP2 averages gradients and every rank has an equal token count. That holds for fixed-length pretraining (hence the wandb parity), but it silently biases the gradient and logged loss the moment tokens-per-rank differ — i.e. exactly under the variable-length/packed path this PR is scaffolding. The criterion/step comments calling this the "global non-ignored token count" overclaim (it is local) and will mislead whoever wires up packed data. Fix: all-reduce the count over the FSDP reduction mesh before dividing.

Evidence assessment:
The base-vs-feature wandb curves for Qwen3-30B-A3B (PP4·EP8) and DeepSeek-V2-Lite (PP2·EP2·DP2) are the right evidence for a "recipe unchanged" refactor and support parity on the dense path. Two things they do not exercise, worth noting: (1) the FP8 linear/grouped-linear consolidation — please confirm at least one of the two runs used fp8, otherwise that path has no loss-curve coverage (I believe it is behavior-preserving by inspection, but it is untested by these curves); (2) the packed/cu_seqlens path, which is only structurally checked via synthetic offsets at cp==1 in test_dualpipev and would not surface the num_tokens bias above. Neither blocks this PR since packed training is not yet a live entrypoint — flagging so the count reduction lands before the data pipeline does.

Comment on lines +185 to +186
torch.distributed.broadcast(width, group_src=0, group=self.pp_group)
if int(width) == 0:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setup_cu_seqlens runs at the start of every DualPipeV.step(), including the live dense-pretraining path (cu_seqlens=None). When pp_size > 1 this now issues a per-step PP-group broadcast(width) followed by int(width), which forces a host↔device sync (.item()-equivalent) before the pipeline warmup can launch.

The cost is small — one scalar collective + one sync per step (not per micro-batch or per layer), so it's a fraction of a hundreds-of-ms step — but it's a genuinely new per-step sync + collective on the hot loop, added to support a feature (packed sequences) that no entrypoint drives yet. Because it precedes the whole pipeline schedule, the sync effectively acts as a PP-group barrier at each step boundary.

Worth a note rather than a blocker: consider skipping the width broadcast when the framework knows the run is dense (e.g. a config flag set once at setup), so the live path pays nothing until packed data is actually wired up.

@claude

claude Bot commented Jul 13, 2026

Copy link
Copy Markdown

Performance Review

Scope: judged whether this refactor holds training step-time and peak memory vs. main. I traced the hot path — the DualPipeV overlap loop (overlap.py/execution.py), the FP8 linear + grouped-linear consolidation and weight-cache change (operators/linear.py, operators/grouped_linear.py, dualpipe/utils.py), the attention custom ops (operators/flash_attn_v4.py), and the per-step reduction in tasks/pretrain_lm.py.

No concrete throughput/memory regression found in the mechanics. The comm-overlap structure is preserved: dispatch/combine still run on comm_stream with wait_event/comm_work.wait() and are not moved onto the compute stream or made synchronous. The FP8 weight cache is still version-gated with the torch.compiler.is_compiling() guard, so no new graph break or .item() lands in a compiled region. RoPE is now hoisted to one forward_posemb per stage-forward and reused across layers (overlap.py, execution.py:797) instead of being recomputed inside each layer’s attention — neutral-to-favorable. The unconditional per-param grad scale (pretrain_lm.py:393-396) already ran every real step (num_chunks > 1), so it is not new cost.

1. Evidence gap (primary). The validation is loss-parity wandb curves only. For a PR that restructures the overlap engine, the FP8 GEMM path, and the per-step reduction, loss parity does not establish that step-time and peak memory are unchanged — those are exactly the axes this framework exists to optimize, and they can regress with the loss curve still matching bit-for-bit. The step logger already emits tokens-per-second, step-time, and peak-gpu-memory, so the measurement that would settle it is cheap: a base-vs-feature run of ~30-50 steady-state steps at the two validated meshes (Qwen3-30B-A3B PP4·EP8, DeepSeek-V2-Lite PP2·EP2·DP2), reporting median step-time and peak memory. Additionally — as the correctness pass also noted — please confirm at least one validated run used fp8, since the FP8 linear/grouped-linear consolidation has no loss-curve coverage otherwise.

2. New per-step sync + collective on the live dense path (minor). setup_cu_seqlens runs every DualPipeV.step(); with pp_size > 1 and cu_seqlens=None (the only live path today) it issues a PP-group broadcast(width) + int(width) host sync before the pipeline warmup. One scalar collective + one sync per step is negligible against a hundreds-of-ms step, but it is a genuinely new per-step sync added to support packed sequences, which no entrypoint drives yet. Details inline:

torch.distributed.broadcast(width, group_src=0, group=self.pp_group)
if int(width) == 0:

Neither point blocks the refactor. Item 1 is the one I would want addressed before merge given the surface area touched.

@claude

claude Bot commented Jul 13, 2026

Copy link
Copy Markdown

Consistency Review

This refactor renames the decoder-layer stages (forward_attn/forward_mlp/forward_aggregateforward_stage1/forward_stage3/forward_stage5), replaces ForwardAttnOutput with RoutingInfo, deletes pithtrain/layers/ (moved to operators/), deletes pithtrain/dualpipe/modeling.py and layer_partition.py (folded into execution.py / dualpipev.py), removes ModelImplMode/use_reference_fwd, renames GroupLinearGroupedLinear and IntermediateTensors(Layer)ChunkRecord/LayerRecord, changes layer_partition to a 3-arg signature (num_layers, stage_count, stage_index), and replaces TrainingCfg.fp8_training (str) with fp8 (bool).

The primary prose docs — AGENTS.md, README.md, docs/, CONTRIBUTING.md — are updated correctly. But several agent-native skills and helper tools still describe the pre-refactor interface:

1. add-new-model skill still documents the old stage names and deleted symbols.
The reference docs and templates describe forward_attn/forward_mlp/forward_aggregate, ForwardAttnOutput, ModelImplMode/use_reference_fwd, IntermediateTensors, imports from pithtrain.layers.factory, and the old 2-arg layer_partition. An agent following this skill would write code against a protocol that no longer exists. It should read: forward_stage1/forward_stage3/forward_stage5, RoutingInfo, imports from pithtrain.operators, layer_partition(config.num_hidden_layers, stage_count, stage_index) imported from pithtrain.dualpipe.dualpipev, and ChunkRecord/LayerRecord — with no ModelImplMode/use_reference_fwd/reference-fwd short-circuit.

2. add-memory-prints skill points at the deleted pithtrain/dualpipe/modeling.py.
modeling.py was folded into execution.py; the skill's Groups 1B/5A still import from and instrument pithtrain.dualpipe.modeling, decoder_layer_forward(), IntermediateTensorsLayer(), and layer.forward_attn/forward_mlp/forward_aggregate. It should reference pithtrain/dualpipe/execution.py, the record types (LayerRecord), and forward_stage1/forward_stage3/forward_stage5.

3. capture-nsys-profile and validate-correctness scripts set the removed training.fp8_training.
TrainingCfg.fp8_training (str) is now fp8 (bool), and TrainingCfg is slotted, so training.fp8_training = "disabled" raises AttributeError. It should read training.fp8 = False.

4. convert_checkpoint/qwen35_moe.py comments name the renamed GroupLinear.
The runtime expert class is now GroupedLinear; the comments describing the .weight suffix should say GroupedLinear.

5. tools/memory_estimator comments reference deleted files / renamed types.

  • model_profile.py L78 — "Matches the algorithm in pithtrain/dualpipe/layer_partition.py"; that file is deleted, the algorithm now lives in pithtrain/dualpipe/dualpipev.py.
  • activation_profile.py L125, L170-L226 and report.py L69, L154 — comments describe IntermediateTensors records and _forward_attn_compute/forward_mlp/forward_aggregate; these are now ChunkRecord/LayerRecord and forward_stage1/3/5.

Comment thread pithtrain/models/interface.py Outdated
@haok1402

Copy link
Copy Markdown
Collaborator Author

Consistency Review

Fixed!

Comment thread pithtrain/modules/training.py
Comment thread pithtrain/operators/deepgemm_quantize.py
haok1402 added 24 commits July 14, 2026 06:50
…w+sink

Sibling-model cosmetic/cleanup pass (E), built to the caf8f50 end-state (last
model-cleanup commit before variable-seqlen):

- gpt-oss forward compute rewrite + torch.compile on stage1 (caf8f50, 22ef069);
  FA4 wrapper gains sliding-window (window_size) and attention-sink
  (learnable_sink) support that gpt-oss needs.
- single-config pass-through construction across the four models (bf2140e).
- unified modeling docstrings linking the HF reference models (32cd343);
  further modeling cleanup (ce9affb).

Engine (execution/overlap/dualpipev/pretrain_lm) untouched. BF16 gate
(deepseek-v2-lite, pp2/ep2, 32 steps): lb tracks base (max |Δ|=0.015, same
1.06->1.26->1.10 arc); CE tracks (6.436 vs 6.424). NOTE: deepseek changes only
2 lines here, so this gate validates the shared files (FA4/interface/contexts)
and deepseek; the gpt-oss/qwen compute rewrites are not numerically exercised
by a deepseek run. Variable-seqlen and the routing-info rename remain deferred.
Variable-length / packed-sequence support (8de11e2): cu_seqlens threaded
through the pipeline as an explicit forward argument (sibling of
rotary_posemb) into attention (flash_attn_varlen_func) and the per-document
position reset; DualPipeV broadcasts it along the PP group. Loss becomes
token-weighted: criterion returns the summed CE and train_step divides every
gradient by the global non-ignored token count. Plus the MoERouting ->
RoutingInfo rename (7f37007).

NOTE: this batch diverged the lb-loss gate on its own (router gradient scaled
~num_tokens too small because the separately-injected load-balance gradient
does not flow through criterion, yet is hit by the 1/num_tokens grad scale).
Fixed in the immediately following commit; committed separately to record the
regression and its fix distinctly.
The lb gradient is injected at the gate via MoELoadBalanceLossInjector, not
routed through criterion, so the token-weighted-loss change (summed CE +
1/num_tokens gradient scaling) shrank it by ~num_tokens and the router stopped
balancing (lb-loss climbs instead of decaying). Weight the injected lb_loss by
the micro-batch token count (topk_weight.shape[0]) so the uniform 1/num_tokens
scale normalizes it correctly -- equal to the previous 1/accumulate_steps mean
for equal-length micro-batches. The returned/logged lb_loss is unchanged.
Applied to all four gates (deepseek-v2, qwen3, qwen3.5, gpt-oss).
…ackward

torch._dynamo.allow_in_graph(MoELoadBalanceLossInjector) at module scope.
The gpt-oss router's forward is @torch.compile(fullgraph=True) and calls
MoELoadBalanceLossInjector.apply inside it; without allow_in_graph Dynamo
traces into the custom autograd Function and can drop its injected backward
(the deepseek/qwen gates are not compiled, so this only bites gpt-oss). Also
folds the dualpipev cu_seqlens line-wrap (640e471, cosmetic).

Orthogonal to the load-balance gradient-scaling fix: that changes the injected
value, this preserves the injected backward across compilation.
Pull the last deferred non-framework changes so 0709 equals the full 0630
codebase refactor plus the load-balance gradient-scaling fix, and nothing
else:

- pyproject.toml: flash-attn 4 b19 -> b20; drop the nvidia-cutlass-dsl-libs-base
  override in favor of prerelease="allow" + linux environment pin (78576f6).
- deepseek-v2-lite/config.json: drop moe_layer_freq / norm_topk_prob /
  scoring_func (unused by the model) (5ac3a99).
- test formatting (ruff) + agent-guide doc text (GroupLinear -> GroupedLinear,
  layers/ -> operators/, drop ConvertCheckpointCtx).

After this, `git diff 0709 0630-codebase-refactor` is exactly the 4-gate
load-balance fix.
deepseek_v2_lite.py -> deepseek_v2.py (theme E rename) and the layer
stage methods forward_attn/forward_mlp/forward_aggregate ->
forward_stage1/forward_stage3/forward_stage5. Both the supported-models
line and the MLA note pointed at the old names.
Expand the paired semicolon-separated annotations in contexts/distributed.py
to one per line and remove the now-unneeded E702 per-file-ignore. Reduce the
three context modules to a single-line docstring; the access contract and
usage examples are documented once in AGENTS.md instead.
Add a Runtime Contexts section (the pithtrain.contexts access contract:
import the module, read fields off it, populated once by setup_*). Fix two
stale spots from the contexts refactor: the training loop uses setup_logging/
setup_distributed/setup_training, not the old *_context managers, and tasks
no longer carry a per-task <Task>Ctx object.
…e-training and we shouldn't specify the max position embeddings from config since the pre-training will overwrite with sequence length ; then the model.json from benchmarks was moved ot the config.json inside pretrain examples for the qwen3.5 model
@haok1402
haok1402 force-pushed the 0709-codebase-refactor branch from 529c3ec to dc93ac5 Compare July 14, 2026 10:50

@MasterJH5574 MasterJH5574 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@MasterJH5574
MasterJH5574 merged commit f53f32d into mlc-ai:main Jul 14, 2026
4 checks passed
@haok1402
haok1402 deleted the 0709-codebase-refactor branch July 14, 2026 23:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants