Skip to content

Restore a shard from the map the layer published - #8622

Merged
delock merged 3 commits into
deepspeedai:masterfrom
Achyuthan-S:step4c-restore-reads-map
Sep 29, 2026
Merged

delock merged 3 commits into
deepspeedai:masterfrom
Achyuthan-S:step4c-restore-reads-map

Conversation

@Achyuthan-S

@Achyuthan-S Achyuthan-S commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

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.md describes 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_t out 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/ and tests/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-existing st_st_size typo in test_convert_checkpoint.py.

The tests are built to discriminate

test_restore_prefers_the_map_over_the_category_keys gives 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_published keeps 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

Copilot AI lite review requested due to automatic review settings September 21, 2026 23:47
@Achyuthan-S
Achyuthan-S force-pushed the step4c-restore-reads-map branch from 91ee0cc to 49dca60 Compare September 21, 2026 23:49

Copilot AI 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.

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 High severity

Open (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()
@Achyuthan-S

Copy link
Copy Markdown
Contributor Author

Fixed by threading the state key through: restore resolves a partition once per state file,
so each one now takes its own power of a piece's scale rather than the parameter's.

The power table moves to 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 instead of two
that could drift — the same reasoning as shared_qk_value_head_ids in #8575.

Worth noting what threading it also buys: _reject_scaled_optimizer_state only triggers on
a non-parameter power, so the refusal built for scaled optimizer states needs the real power
to reach it. With the state threaded, a scaled moment now raises rather than being rescaled.

No layer emits a scaled map today — the only scaled case, Yuan's o_proj bias, is still
unsupported — so this is ahead of the path rather than behind it. It becomes live when
rollout needs scale, which is the mid-term goal in #8230.

Test asserts the parameter restores through its own power and both moments raise; reverting
the threading fails it.

@Achyuthan-S

Copy link
Copy Markdown
Contributor Author

@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.
112 tests pass on the rebased tree (the affine suite, the resume matrix, the coverage test, tensor_parallel, and #8544's new AutoEP suite). Could you take a look when you have a moment?

@delock

delock commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator

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>
@Achyuthan-S
Achyuthan-S force-pushed the step4c-restore-reads-map branch from b7ce240 to 22c22f0 Compare September 29, 2026 05:08
@Achyuthan-S

Copy link
Copy Markdown
Contributor Author

@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.

@delock
delock enabled auto-merge September 29, 2026 08:47
@delock
delock added this pull request to the merge queue Sep 29, 2026
Merged via the queue into deepspeedai:master with commit a80df94 Sep 29, 2026
13 checks passed
banxingmjj pushed a commit to openanolis/DeepSpeed that referenced this pull request Oct 16, 2026
…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>
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.

3 participants