Repository navigation
Restore a shard from the map the layer published - #8622
Conversation
91ee0cc to
49dca60
Compare
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Optimizer moments currently use the default scaling power, which can rescale mapped states incorrectly.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
What changed in this PR
Updates AutoTP universal-checkpoint restore to use the target layer’s affine shard map, with legacy metadata fallback.
Changes:
- Restores mapped parameters through affine shard extraction.
- Adds tests for map precedence and fallback behavior.
| File | Summary |
|---|---|
tests/unit/runtime/tensor_parallel/test_autotp_universal_checkpoint.py |
Tests map precedence and legacy fallback. |
deepspeed/checkpoint/universal_checkpoint.py |
Implements affine-map-based shard restoration. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if affine_map is not None: | ||
| restored = ParamAffineMap.from_dict(affine_map) | ||
| if tp_rank in restored.pieces_by_rank and restored.numel == full_hp_param.numel(): | ||
| return restored.extract(full_hp_param, tp_rank).flatten() |
|
Fixed by threading the state key through: restore resolves a partition once per state file, The power table moves to Worth noting what threading it also buys: No layer emits a scaled map today — the only scaled case, Yuan's o_proj bias, is still Test asserts the parameter restores through its own power and both moments raise; reverting |
62704c3 to
cb48ab5
Compare
cb48ab5 to
b7ce240
Compare
|
@delock #8622 is rebased onto master and conflict-free — it had picked up a conflict with #8544's import block, which is resolved. This is the last step-4 piece: restore reads the map, so convert and restore are the same piece list run in both directions. |
|
Hi @Achyuthan-S the formatting error needs to be fixed, thanks! |
Conversion has rebuilt a parameter from its pieces since the map existed, but restore still read the per-category metadata keys, so the two sides described the same layout in two different ways and could disagree. Restore now takes this rank's shard from the map directly, which is the same piece list run in the opposite direction. A parameter whose layer published no map falls back to the existing keys unchanged. Signed-off-by: Achyuthan Sivasankar <achyuthan.sivasankar@gmail.com>
Give a parameter contradictory geometry: the keys describe a plain dim-0 split while the map hands rank 0 the second half. Which half rank 0 receives says which side of the metadata restore consulted, so removing the map branch fails this test instead of quietly producing the same answer through the old path. The companion test covers a parameter with no map, which must still restore through the keys unchanged. Signed-off-by: Achyuthan Sivasankar <achyuthan.sivasankar@gmail.com>
Restore resolves a partition once per state file, so passing the parameter's power for a moment would apply the wrong factor -- and would slip past the refusal built for scaled optimizer states, since that only triggers on a non-parameter power. Move the power table beside the map so conversion and restore read one definition rather than a copy each. Signed-off-by: Achyuthan Sivasankar <achyuthan.sivasankar@gmail.com>
b7ce240 to
22c22f0
Compare
|
@delock Fixed and rebased. Adding SCALE_POWER_BY_STATE took the export list to 131 characters, and yapf 0.43.0 rewraps it where the version I had locally left it alone — which is why it passed here and failed in CI. I'm on the pinned version now. |
…ai#8706) CodeGen was the last layout in `AUTOTP_UNSUPPORTED_PARAMETER_PATTERNS` whose geometry had no description. Its weight now publishes an affine map, so conversion stops refusing it. Follows deepspeedai#8519 (layers emit the map), deepspeedai#8575 (GPTBigCode and Yuan describe themselves) and deepspeedai#8622 (restore reads it). ### What the layout actually is `_codegen_type_transpose` views the weight as `codegen_mp_num` blocks, cuts each into q/k/v, subdivides every third across the ranks, then reassembles the pieces q, k, v at a time. The result is **one fixed permutation of the whole weight** — rank-independent — from which each rank takes a contiguous slice. So the outer split is a plain even row split, and the interleaving lives entirely inside each rank's region, where the rows are a permutation of equal-sized blocks. That is why one `partition_dim` cannot say it and a block selection can. `block_gather_map` already expresses this, so there is **no new IR primitive here**. I said when deepspeedai#8575 landed that codegen "needs more than a segment list or a block selection"; that was wrong, and this is the correction. The work is deriving the block order, not extending the language. ### How the description is kept honest `codegen_block_layout` derives the order independently, because the partition reaches it through reshapes and never names a block. That departs from deepspeedai#8575, where `shared_qk_value_head_ids` is shared between the partition and its description. Sharing here would mean refactoring `_codegen_type_transpose` itself, which is riskier than the description it would protect, so the drift guard is instead a test requiring the derived map to equal one **probed from the real partition function**. That checks behaviour rather than structure. @delock — if you would rather have the refactor so there is one definition, say so and I will do it that way. The helper refuses every case the partition does not produce: no head count, a head count it cannot divide (the same condition `_codegen_type_transpose` asserts), fewer than two ranks, and rows that do not divide into equal blocks. An earlier version of this change published a map for `AutoTPMeta()` with no head count — a config whose own assert cannot be evaluated — and `test_collect_records_fused_layouts_without_a_sub_param_split` caught it. ### The tests discriminate | corruption | result | |---|---| | q/k/v loop order swapped with the block loop | 6 fail | | block size missing its `world_size` factor | 6 fail | | hook renamed so it never fires | 1 fail — the wiring test, which is its only job | The piece-count test is the one that matters beyond correctness. A compressor that missed the permutation would emit one piece per row, still cover the tensor and still round-trip, so only the count reveals it. Across rows 24 → 192 at TP 2 the count holds at 11. ### Validation The derived map equals the probed map for 24x8 TP2, 48x16 TP2, 96x32 TP2, 96x8 TP4 and 192x8 TP8. In every case `extract` is bit-exact against the real partition, `rebuild` round-trips, the map covers `F`, and both `validate()` and `validate_coverage()` pass. 11 pieces per rank at TP 2, 12 at TP 4 and TP 8 — matching the count in §5 of `affine_ir_spec.md`. 126 passed on this rebase across the affine suite, the resume matrix, the coverage test, `tests/unit/runtime/tensor_parallel/` and the AutoEP affine suite, on CPU/gloo (`DS_ACCELERATOR=cpu LOCAL_SIZE=4`) — now on top of deepspeedai#8624's stricter `validate()`, which every codegen piece passes with its destination checked. With deepspeedai#8623 merged, codegen maps also compose through `plan_transfer` exactly: TP 2↔4, 2↔8 and 4↔8, with source shards taken from the real partition function and every target shard compared element for element. deepspeedai#8623's own suite passes here too (119). Earlier, at base `581ac41bd`, I ran `tests/unit/checkpoint/` and `tests/unit/runtime/tensor_parallel/` on this branch and on a pristine worktree of that commit: 244 passed here against 236 on master — the 8 this PR adds — with 74 failed and 106 errors on both, and the failing set identical **by name** rather than merely in count, 180 names either way with none on one side only. All of them are environmental: 126 FusedAdam JIT (no ninja), 54 fp16-on-CPU, 4 cpu_adam JIT. Master has moved since, so that pairing is against `581ac41bd` rather than today's tip; I can redo it if the numbers matter for review. ### What this does not close Yuan's o_proj bias is still refused, so the unsupported set is not empty yet. That one is not a description gap: its partition pre-divides by the world size and that path returns an empty bias rather than a usable one, so there is no layout to describe until the partition is fixed. Separate change. Related: deepspeedai#8252, deepspeedai#8230. cc @delock Signed-off-by: Achyuthan Sivasankar <achyuthan.sivasankar@gmail.com>

Restore now takes this rank's shard from the map the layer published, which is the same piece list conversion uses run in the opposite direction. That completes the symmetry §7 of
affine_ir_spec.mddescribes and that conversion has had since #8385: until now the two sides described the same layout in two different ways, and could disagree.Follows #8519 (layers emit the map) and #8575 (GPTBigCode and Yuan describe themselves).
This one is not additive
The map lives on the parameter, built by this job's layer. It is
M_t— it describes the topology being restored into, regardless of how the source checkpoint was converted. So the new path is taken for every AutoTP restore, not only for checkpoints that were converted through a map. A parameter whose layer published no map falls back to the existing category keys unchanged.That is the correct reading of the spec rather than a shortcut: §6.1 keeps
M_tout of the file precisely because the restoring job derives it from its own layers. But it does mean the blast radius here is wider than the three PRs before it, so the equivalence is worth checking rather than assuming.I ran
tests/unit/checkpoint/andtests/unit/runtime/tensor_parallel/(385 tests) against this branch and against upstream master in a separate worktree. The failing set is identical by name, not merely the same count — 75 failed, 187 passed either way, all of them FusedAdam JIT, fp16-on-CPU, or the pre-existingst_st_sizetypo intest_convert_checkpoint.py.The tests are built to discriminate
test_restore_prefers_the_map_over_the_category_keysgives one parameter deliberately contradictory geometry: the category keys describe a plain dim-0 split, so rank 0 would take the first half, while the map hands rank 0 the second half. Which half rank 0 receives says which side of the metadata restore actually consulted.This matters because the old path covers the same layouts correctly. Without a discriminating case the map branch could be deleted and every existing test would stay green. I checked: removing the branch fails that test, while
test_restore_falls_back_when_no_map_was_publishedkeeps passing.Validation
112 passed on CPU/gloo (
DS_ACCELERATOR=cpu LOCAL_SIZE=4): the affine suite, the resume matrix, the coverage test,tests/unit/runtime/tensor_parallel/, and #8544's AutoEP affine suite, which this now sits on top of after the rebase.Scale powers
Restore resolves a partition once per state file, so each state takes its own power of a piece's scale. The power table lives in affine.py as SCALE_POWER_BY_STATE — ds_to_universal.py had its own copy since #8519, so conversion and restore now read one definition rather than two that could drift.
This also reaches the refusal built for scaled optimizer states, which only triggers on a non-parameter power. No layer emits a scaled map today, so it is ahead of the path rather than behind it.
Related: #8252, #8230.
cc @delock