Skip to content

[AffineIR] Affine storage maps for live ZeRO parameters and optimizer states - #8885

Open
jinyouzhi wants to merge 12 commits into
deepspeedai:masterfrom
jinyouzhi:zero-storage-affine-map
Open

jinyouzhi wants to merge 12 commits into
deepspeedai:masterfrom
jinyouzhi:zero-storage-affine-map

Conversation

@jinyouzhi

@jinyouzhi jinyouzhi commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Describes, as affine maps in global ranks, where a live ZeRO engine stores each element of a parameter and its optimizer states, so plan_transfer can read from or write into training storage directly (e.g. training → rollout weight sync) without gathering full parameters first.

  • zero_affine.py
    • zero_param_storage_affine_map: low-precision replicas under ZeRO-0/1/2 (DP or expert-DP group), ds_tensor partitions under ZeRO-3, padding excluded.
    • zero_optimizer_storage_affine_maps: fp32 master + optimizer states (ZeRO-1/2, bf16 optimizer, ZeRO-3).
    • zero_param_storage_shard / zero_optimizer_storage_shard: flat views of ZeRO's own buffers matching the maps.
    • autotp_storage_affine_maps: AutoTP layout × ZeRO storage, one all-gather over the TP group.
  • affine.py: nest_affine_maps composes a layout map (TP/EP shard placement) with per-shard storage maps. A process's stored run of an N-D shard is described as at most 2·ndim−1 N-D boxes in the parameter's own shape, so plans stay small (row-parallel shards are not cut per row).
  • affine_transfer.py: plan_transfer accepts an N-D piece in a 1-D (flat) shard only when the piece is dense there; every other rank mismatch is still refused.
  • affine_ir_spec.md: phase-2 section on layout vs storage maps.
  • Fix test_convert_checkpoint stat().size → st_size (from Preserve non-floating buffers in ZeRO checkpoint conversion #8515).

No change to ZeRO, AutoTP or the executor. Offloaded optimizer states raise NotImplementedError; MiCS / hpZ are not covered.

Tests

Qwen3-8B, 8×L20 ZeRO-2 gather ZeRO-2 affine lowest ZeRO-2 affine balanced ZeRO-3 gather ZeRO-3 affine
maps + plan (once) - 0.63 + 0.57 s 0.70 + 0.70 s - 0.82 + 0.60 s
first sync 1.01 s 1.16 s 0.48 s 1.71 s 1.18 s
repeated syncs 0.78-0.82 s 1.05-1.35 s 0.49-0.55 s 1.22-1.28 s 0.45-0.74 s
device memory added 3.48 GB 0.30 GB 0.16 GB 3.48 GB 0.66 GB
sent / local copy - 35.2 / 5.0 GB 15.1 / 25.1 GB - 34.0 / 6.3 GB
executor rounds - 96 41 - 32

Each map has 3,192 pieces on both sides; before N-D boxes (one piece per stored row) the same ZeRO-3 maps had 592k source / 2.36M target pieces and planning took 207 s. Under ZeRO-2 every DP rank holds a replica: lowest is the planner's default holder choice, balanced is a replica_selector in the example (local copy first, else the least-loaded holder).

jinyouzhi and others added 7 commits October 22, 2026 15:35
Describe, in global ranks, where ZeRO keeps each element of a live
parameter, so a transfer planner can read or write training storage
directly:

- zero_param_storage_affine_map: low-precision replicas under ZeRO-0/1/2
  (DP group, or the expert DP group for MoE) and ds_tensor partitions
  under ZeRO-3, excluding padding.
- zero_optimizer_storage_affine_maps: the fp32 master copy and its
  optimizer states. ZeRO-1/2 and the bf16 optimizer gather each rank's
  hp fragment address with one all-gather per DP group; ZeRO-3 reuses the
  partition map. Frozen parameters have no map.
- zero_param_storage_shard / zero_optimizer_storage_shard: flat views of
  ZeRO's own buffers laid out as the maps describe.

Offloaded optimizer states raise NotImplementedError on every rank; MiCS
and hpZ are not covered. No change to ZeRO, the planner or the executor.

Signed-off-by: Jin, Youzhi <[email protected]>
A layout map says which tensor- or expert-parallel rank holds which shard; a
ZeRO storage map says which data-parallel ranks store which interval of one
such shard. nest_affine_maps combines the two into a map of what every
process stores, so a transfer can be planned straight from runtime storage.

The result describes the flattened parameter. A ZeRO partition of an N-D
shard is generally not an N-D box, and plan_transfer addresses shards as
boxes, so storage maps are flat. Replicated layout pieces are cut wherever
any holder's storage begins or ends, and every holder is listed in
locations.

Co-authored-by: Copilot <[email protected]>
Signed-off-by: Jin, Youzhi <[email protected]>
autotp_storage_affine_maps nests each parameter's published AutoTP layout
map with the ZeRO storage of every tensor-parallel shard, giving a map of
what each process stores, keyed by global rank. The shards are the ones
zero_param_storage_shard and zero_optimizer_storage_shard already return,
so a transfer can read from or write into a live AutoTP + ZeRO engine.
Parameters AutoTP leaves whole are replicated over the tensor-parallel
group.

The tensor-parallel ranks exchange their shards' storage maps in one
all-gather, so no rank layout is assumed.

Co-authored-by: Copilot <[email protected]>
Signed-off-by: Jin, Youzhi <[email protected]>
Add a phase 2 section to the affine IR spec: layout maps versus storage maps,
why storage maps describe the flattened parameter, how nest_affine_maps
derives them for dense, AutoTP and AutoEP parameters, replica choice, plan
lifetime, and what is not described yet.

Co-authored-by: Copilot <[email protected]>
Signed-off-by: Jin, Youzhi <[email protected]>
nest_affine_maps described the flattened parameter, so a layout piece
was cut into its contiguous runs: a row-parallel shard became one piece
per row, which made planning large models slow and could exceed
PlanBudget. A process's stored run of its layout shard is a partial row,
whole rows, and a partial row, so describe it as at most 2 * ndim - 1
boxes in the parameter's own shape.

plan_transfer now accepts such a piece in a 1-D shard when it is dense
there, and treats its storage as one interval of the shard.

Co-authored-by: Copilot <[email protected]>
Signed-off-by: Jin, Youzhi <[email protected]>
all_gather_into_tensor on gloo requires the output to stack the inputs
along their first axis; the (world, n, 3) output raised on CPU.

Co-authored-by: Copilot <[email protected]>
Signed-off-by: Jin, Youzhi <[email protected]>
pathlib's stat() returns os.stat_result, whose size field is st_size.

Co-authored-by: Copilot <[email protected]>
Signed-off-by: Jin, Youzhi <[email protected]>
@jinyouzhi jinyouzhi changed the title Affine storage maps for live ZeRO / AutoTP parameters and optimizer states [AffineIR] Affine storage maps for live ZeRO / AutoTP parameters and optimizer states Oct 22, 2026
jinyouzhi added a commit to jinyouzhi/DeepSpeedExamples that referenced this pull request Oct 22, 2026
autotp_rollout_e2e.py trains Qwen3-style dense models with AutoTP + ZeRO
and moves the weights into rollout engines with a different TP size
through a Universal checkpoint, through gathered parameters, and through
ZeRO storage affine maps (plan_transfer + execute_transfers), checking
every path bit for bit and recording time and peak memory per sync.

A --replica_policy chooses which ZeRO-0/1/2 replica each target block is
read from; the default reads locally when possible and otherwise from
the least-loaded holder.

Needs DeepSpeed with deepspeedai/DeepSpeed#8809 and deepspeedai/DeepSpeed#8885.

Co-authored-by: Copilot <[email protected]>
Signed-off-by: Jin, Youzhi <[email protected]>
@jinyouzhi jinyouzhi changed the title [AffineIR] Affine storage maps for live ZeRO / AutoTP parameters and optimizer states [AffineIR] Affine storage maps for live ZeRO parameters and optimizer states Oct 22, 2026
jinyouzhi and others added 2 commits October 22, 2026 21:46
A replicated layout block can be contiguous in one holder's shard and
strided in another's. nest_affine_maps merged rows wherever any copy
allowed it, so the box was not one run of addresses in the strided copy
and nesting failed; merge only where every copy is contiguous.

A scalar parameter has no axis to split, so describe it as a single 0-D
box, and let plan_transfer place a 0-D piece in a flat shard.

Co-authored-by: Copilot <[email protected]>
Signed-off-by: Jin, Youzhi <[email protected]>
@jinyouzhi
jinyouzhi marked this pull request as ready for review October 22, 2026 14:17
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-22T14:29:00.833416Z 2d8d873 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2d8d873e4f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread deepspeed/checkpoint/zero_affine.py
Comment thread deepspeed/checkpoint/zero_affine.py
Comment thread deepspeed/checkpoint/zero_affine.py
- autotp_storage_affine_maps: a parameter AutoTP leaves whole in a model
  built under zero.Init keeps its partition over the whole world, so every
  TP rank reports the same storage. Describe that shared copy on its own
  instead of nesting it once per TP rank, which was rejected.
- zero_param_storage_shard: reject a gathered ZeRO-3 parameter. Persistent
  parameters stay gathered between steps and the forward reads that copy,
  so a write to the partition alone would be lost. Callers release gathered
  copies with engine.empty_partition_cache() first.
- Reject NVMe-offloaded ZeRO-3 parameter storage, whose partition is a
  placeholder while swapped out.

Co-authored-by: Copilot <[email protected]>
Signed-off-by: Jin, Youzhi <[email protected]>
…o zero-storage-affine-map

Signed-off-by: Jin, Youzhi <[email protected]>

# Conflicts:
#	deepspeed/checkpoint/affine_ir_spec.md
@0z5a

0z5a commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

Thanks @jinyouzhi, I went through the plan_transfer relaxation. From the planner side it is safe and I'm happy with it as written.

Why the invariant from #8624 still holds. For a flat shard, a piece's local footprint becomes the run [dest_offset, dest_offset + numel). _shards_tile still checks that the runs of one rank are pairwise disjoint and add up to the shard size. So two pieces still cannot write the same shard address, and a hole is still caught. The dense check (dest strides must equal the box's own row-major strides) refuses everything that is not one run. The source side keeps its existing check against the logical row-major strides. Nothing beyond a dense run is newly accepted.

I added adversarial cases locally against 42bafc5 and all of them behave as expected:

  • refused: two flat pieces whose runs overlap, a hole in the shard, a run past the end of the shard, transposed / padded / gapped dest strides, a 1-D piece describing a matrix, a 2-D shard of a 3-D parameter;
  • planned and checked element by element: flat storage on the target side (a ZeRO cut inside a row on the write side), flat storage on both sides (40 random TP×ZeRO-3 → TP×ZeRO-3 relayouts with partitions that start mid-row), ZeRO-2 replicas with a random replica_selector, and one map that mixes (numel,) and N-D shards across ranks;
  • Qwen3-8B dense shapes (254 matrices), TP2×ZeRO-3 DP4 → TP4×ZeRO-3 DP2 through nest_affine_maps: 2,032 pieces per side, maps + plans in 0.28 s on a laptop CPU.

Executor (#8809). No change is needed for flat segments. Segments on the flat side carry the piece's own row-major strides. _validate_buffers already bounds the reach of those strides, and _region already adds the buffer's own storage offset. The shards that zero_param_storage_shard / zero_optimizer_storage_shard return are packed views, which is what _flat requires. I cherry-picked #8809 onto 42bafc5. The only conflict is in docs. On CPU (gloo, 4 ranks) the executor, planner and ZeRO-map tests pass together, apart from the four TestAutoTPStorageMaps cases in 1 below.

Three small requests:

  1. TestAutoTPStorageMaps fails on CPU because of the test, not the code. With DS_ACCELERATOR=cpu (4 ranks, gloo), all four cases fail at the planned-copy check. I reproduced this on two machines. _all_shards gathers shard.detach().cpu(). On CPU that is the view itself, so the unpickled bias keeps its storage offset into the flat buffer (72 elements in the first case). torch.as_strided(..., segment.source_offset) then counts from the start of the storage and reads the weight's bytes. On XPU, .cpu() makes a fresh copy, which hides this. Gathering shard.detach().cpu().clone(), or adding storage_offset() the way the executor's _region does, makes all 27 tests in the file pass on CPU.
  2. A target-side flat test. test_a_flat_shard_holds_a_dense_box_of_a_matrix covers flat storage only as the source. A restore or reverse sync writes into ZeRO storage, so a case with a flat target cut inside a row would pin the write direction. I can push mine if that helps.
  3. Mixed shard ranks in one map. _normalized now lets one map have (numel,) shards on some ranks and N-D shards on others. That plans correctly in my test. If it is intended, a one-line test would document it. If not, it is cheap to refuse.

On the numbers: balanced beating lowest (0.49 s vs 1.05–1.35 s for repeated ZeRO-2 syncs) is a nice confirmation that holder choice belongs in a replica_selector policy and not in the planner.

…uffer offset

On CPU, .cpu() returns the view itself, so the gathered shard kept its storage
offset into the flat buffer and as_strided read the neighbouring parameter.
Also pin flat target shards cut mid-row and a map mixing flat and matrix shards.

Co-authored-by: Copilot <[email protected]>
Signed-off-by: Jin, Youzhi <[email protected]>

This branch has not been deployed

No deployments
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