[DO NOT review, pending K3 text PP merging] [Kimi K3] Add K3 MoonViT DEP support to schedule ViT stages into text LLM PP bubbles - #4381
Draft
QIU023 wants to merge 15 commits into
Draft
[DO NOT review, pending K3 text PP merging] [Kimi K3] Add K3 MoonViT DEP support to schedule ViT stages into text LLM PP bubbles#4381QIU023 wants to merge 15 commits into
QIU023 wants to merge 15 commits into
Conversation
Add pipeline parallelism for the Kimi K3 text decoder on top of PR-4025: - pipeline_adapter.py: PP pipelining_fn with FQN injection for the text decoder (tok_embeddings + layers.N + norm/lm_head/output_res_proj/ output_res_norm split) and the CrossStageCacheAdapter for the AttnRes block-residual cross-stage carry under Interleaved1F1B. - model.py: thread block_residual_TND across stage boundaries and return (hidden, block_residual) from a non-head stage; the head stage aggregates. - parallelize.py: allow PP; set pp_enabled on the FSDP calls. - layout.py, knobs.py: block stack/unstack layout tables and the attn_res_cache topology knob the adapter reads. - __init__.py, config_registry.py: register the pipelining_fn and the text-only debugmodel_text / debugmodel_text_32l flavors. - tests/test_pp_fqn_injection.py: CPU test for the FQN split.
…ation cells The FQN-injection test sat in torchtitan/models/kimi_k3/tests/, the only in-model tests directory in the tree; the convention is tests/unit_tests/. Moved and renamed to carry the model name. Adds two recipes and their integration cells: pp2 on the plain schedule, and pp2 with two virtual stages on the interleaved one, so both the block-residual carry and the interleaved path are covered by CI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WBy1d9YVu44nYCVqykRqL1
The pp2 x vp2 cell exercised two boundaries. One layer per stage over 32 stages puts the block residual across every boundary the schedule has, which is the thing this PR adds; the 32-layer text flavor is what makes it expressible. Runs in the 8-GPU features workflow (ngpu=8); the pp2 cell stays for the cheap path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WBy1d9YVu44nYCVqykRqL1
The CPU unit-test workflow collects tests/unit_tests/cpu, not the top level, so the FQN-injection test would have sat there un-run; move it. And both PP integration cells now declare use_real_pg: validate_fake_pg_compatibility rejects pipeline_parallel_degree > 1 under Fake PG, so without it the features suite fails to build rather than skipping. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WBy1d9YVu44nYCVqykRqL1
pipeline_kimi_k3_with_cache_adapter becomes pipeline_kimi_k3; the environment variable debug channel and its six call sites go, and with them the last reader of os in this file; the knob record keeps the one field this branch carries and drops the environment fallback, so a knob is honoured from config or not at all rather than silently from a stale export. Comment blocks compressed to the invariant they guard. adapter_enabled's docstring no longer says the curves match from step 1 -- the transport changes the order the block gradients are summed and is not bitwise against the naive one, which is what the measurements show. The even-split requirement now names the two config fields a user would have to check. Configuration-identical: 4810, 4678 and 6198 fields compared over the union of both key sets, zero differences.
… caller goes knobs.py existed for one boolean and had exactly one importer. Folded into pipeline_adapter.py above its only reader, where the register-once/read-many lifecycle is visible in one screen, and the three entry points drop to module-private. layout.py stays its own file: it is offline algebra over (pp, vp, num_blocks, n_layers, layers_per_block) with no torch distributed, no schedule and no state, and it is the part of this PR that can be tested on its own. stack_blocks had no caller anywhere on this branch and is removed; the carrier is only ever unstacked. Configuration-identical: 4810, 4678 and 6198 fields over the union of both key sets, zero differences.
…at all register_topology reads eight knobs off the model config with getattr and a default. None of them was ever declared on KimiK3Model.Config, so until the environment fallbacks were retired they were reachable only through KIMI_VIT_* and TORCHTITAN_ATTNRES_CACHE; since then they have been reachable through nothing. adapter_enabled's docstring calls attn_res_cache a config gate and the config had no such gate. The consequence for attn_res_cache is the one that matters: with it unreachable, pipeline_kimi_k3 returns passthrough on every run, so the cross-stage cache adapter and the whole of layout.py never execute. Thirteen pipeline cells were measured that way and none of them touched the code they were meant to cover -- the adapter logs a line when it wraps stages, and that line appears in none of the run logs. Declared in the shape the neighbouring fields already use: a dataclass field with the invariant it guards above it. kimi_k3_debugmodel_text_32l_cache turns the transport on, the same way kimi_k3_debugmodel_dyncp lowers the dynamic-CP threshold. Defaults are unchanged, so no existing configuration moves.
…llelism It was declared off, which made the fallback the thing every configuration selected and the transport the thing nothing selected. Under pipeline parallelism the transport is what this model ships: shipping the whole block stack on every hop is the fallback for the schedules it cannot serve, not the normal path. It still engages only on Interleaved1F1B with an even split; plain 1F1B warns and passes through, unchanged. kimi_k3_debugmodel_text_32l_naive selects the fallback so a matrix can put the two side by side.
…ibutes The adapter read num_blocks and layers_per_block off the stage's module and num_hidden_layers off the model config. None of those three exists here: they are the reference tree's shapes, where a wrapper model carried the first two and the config carried the third. Every pipeline run therefore took the branch that says 'this is a baseline non-AttnRes run' and returned passthrough, so the adapter and the whole of layout.py never executed -- on any cell, at any degree, whatever attn_res_cache was set to. The layout is defined by the config in the first place: a block opens every attn_res_block_size layers, so the layer count and that size give both numbers. Deriving them there needs nothing added to the model. The baseline-vs-AttnRes branch goes with it. That distinction belonged to a tree with two models; this one has a single model whose attn_res_block_size is a required parameter, so the branch could only ever fire on the shape mismatch it was not written for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WBy1d9YVu44nYCVqykRqL1 EOF
Four assumptions in the adapter belonged to the reference tree's model and none
of them held here, so the delta transport had never executed once.
The carrier reaches the model as its second positional argument, not as a
blocks= keyword. The token axis is the hidden state's first dimension, because
batch and sequence are folded into one; multiplying the first two dimensions
gave T * D and sized a receive buffer that failed the residual's concatenation.
The model returns the carrier it was handed with this stage's commits appended,
not the commits alone, so the new blocks are the tail past what went in. And
model_parts is left alone: the schedule runs what stage.submod points at, while
the trainer reaches through model_parts for the real module -- it sets
_skip_lm_head there, so substituting the wrapper made the last stage apply
lm_head and the loss apply it again, and it would have prefixed every checkpoint
key with the wrapper's attribute name.
pp2 x vp2, 32 layers, seed 42, deterministic, three steps, transport against
fallback on the same tree:
step 1 12.46351 12.46351 grad_norm 15.3125 both
step 2 9.49685 9.49556
step 3 6.95172 6.95147
The forward is bit-identical, which is what routing the same blocks a different
way should give. The gradients are not, which is what changing the order they
are summed in should give. Both statements were already in the file, in two
comments that read as contradicting each other; they do not.
… cut carried along It overrides a core hook this branch does not introduce, so nothing calls it here.
…avors go Same review direction as the EP and CP PRs: no text-only flavor. The 32-layer flavor keeps the tower -- it rides with the embedding on the first stage and takes no stage of its own, so the pipeline x virtual-stage arithmetic is unchanged and 32 still divides every product. The pp2 and pp8_vp4 CI cells and their recipes move to the multimodal names, and kimi_k3_debugmodel_text plus its model-registry entry leave the branch. Smoked before the switch: pp8 x vp4 on kimi_k3_debugmodel_32l, the adapter wrapping the tower-bearing first stage -- wrapped [0, 8, 16, 24] on rank 0, zero fallback warnings, two steps, exit 0.
… a recipe override
QIU023
requested review from
fegin,
tianyu-l,
wconstab and
wwwjn
as code owners
August 30, 2026 06:48
QIU023
marked this pull request as draft
August 30, 2026 06:49
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.
Draft, stacked on the text-side PP PR: the diff tab shows that PR's content too, so review only the last commit (
kimi_k3: DEP -- the vision tower takes a pipeline stage of its own). It will be rebased when the text PR lands.Summary
Report sec 5.2.3: the vision tower gets a pipeline stage of its own ahead of the text stages, so its compute leaves the critical path of the stage that owns the embedding and can hide in pipeline bubbles. Opt-in via
vit_depbecause it changes the stage count, which the schedule and the checkpoint layout both see; with it off, this commit changes nothing.Design
_inject_kimi_k3_fqnsprepends a[tok_embeddings, vision_encoder]stage and takes it OUT of the text stage budget (the schedule assertsnum_stages % pp_degree == 0, so appending would break pp=2 at the first vision stage).tok_embeddingsas "input IS the hidden state".knobs.pyand gains the vision fields (vit_dep,vit_dep_stages,vit_prefetch,vit_bubble): these decide the pipeline topology, so every rank must resolve them identically, once, at the pipelining entry.vit_dep_stages > 1raises: splitting the tower across stages needs the split to addressvision_encoder.layers, which core's_split_modulecannot reach on this layout; the share entry points (forward_head/body/tail, contiguous block ranges recomputing position tables per share) are in place for when it can.vit_prefetchissues the encode for micro-batch m+k on a side CUDA stream during m's text compute (cross-stream tensor lifetime handled by wait/record_stream on both edges), andvit_bubbleruns planned encodes in the schedule's idle intervals on the main stream, with deferred tower backwards bounded by aGradQueue.Results
Draft placeholder: the tables below were measured on the integration tree this commit is extracted from (8x RTX 5060 Ti, seed 42,
--debug.deterministic, steps 1/3/10 protocol), not on this branch; they will be re-measured on this branch before the draft is undrafted.Tower-stage split: pp2/pp4/pp8 all reproduce dp1's step-1 loss bitwise, with step-2 drift 1.85e-3 (reduction order across the stage boundary); with the delta transport on, pp2xvp2 / pp2xvp4 / pp4xvp2 / pp8xvp4 keep step 1 bitwise and stay within 1.5e-3 at step 2. Timing at debug scale: the bubble arm fills 8/8 planned slots with 0 fallbacks and costs +4.2% per step (the tower is too small to pay for placement there), and the prefetch arm is bitwise on loss with noise-level step time -- both are groundwork knobs, off by default, measured so their engagement is provable rather than assumed.
Changed files
CI/CD Coverage
CPU tests: the share split's block-bound invariants (
test_kimi_k3_vit_stage_shares); the FQN-injection tests on the base branch already pin the DEP stage placement, including the text-model-with-None-tower shape that once produced a silently wrong pipeline.