Repository navigation
[AffineIR] Affine storage maps for live ZeRO parameters and optimizer states - #8885
Conversation
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 <youzhi.jin@intel.com>
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 <223556219+Copilot@users.noreply.github.com> Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>
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 <223556219+Copilot@users.noreply.github.com> Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>
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 <223556219+Copilot@users.noreply.github.com> Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>
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 <223556219+Copilot@users.noreply.github.com> Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>
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 <223556219+Copilot@users.noreply.github.com> Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>
pathlib's stat() returns os.stat_result, whose size field is st_size. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>
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 <223556219+Copilot@users.noreply.github.com> Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>
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 <223556219+Copilot@users.noreply.github.com> Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 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".
- 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 <223556219+Copilot@users.noreply.github.com> Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>
…o zero-storage-affine-map Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com> # Conflicts: # deepspeed/checkpoint/affine_ir_spec.md
|
Thanks @jinyouzhi, I went through the Why the invariant from #8624 still holds. For a flat shard, a piece's local footprint becomes the run I added adversarial cases locally against 42bafc5 and all of them behave as expected:
Executor (#8809). No change is needed for flat segments. Segments on the flat side carry the piece's own row-major strides. Three small requests:
On the numbers: |
…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 <223556219+Copilot@users.noreply.github.com> Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>
|
Thanks @0z5a for the thorough check, and for the adversarial cases. All three are addressed in 7f92794:
On replica selection: I agree the planner should not hard-code a hardware cost model (links, contention, topology), because costs depend on the machine and on the goal. But the planner is the right place to apply one: given a pluggable cost or policy, it sees every request and can balance and split globally, which a per-request selector cannot. Concretely:
It is not strictly necessary to achieve the absolute optimum right away; my view is that we should provide as much room as possible for the cost model to perform optimizations, while avoiding premature optimization. Happy to open a separate issue for this if you think it's worth discussing. |
|
And I want share some follow-up ideas for the planner from #8623, which I hit while building storage maps. @0z5a Could you have a look on it? Thanks!
Structural fast paths
|
@0z5a I'd like to clarify one point, since my earlier changes and demo may have given the wrong impression. I don't think That said, this is just my personal view, so feel free to take it or leave it. Of course, the above are just my personal thoughts—feel free to take them as a reference. |
|
@jinyouzhi @0z5a On replica selection: +1 to a batch interface where the policy sees every request and may split regions, with the planner checking the split tiles exactly. That's the policy layer I took on in #8252 (replica policy above the plan, meeting at the hook), and your local-first / fewest-bytes measurement is a great baseline for it. I'm on the correctness proof and exactness evaluation until mid-November. Could we write the interface up in an issue now so we agree on it, and I'll pick up the policy work after that? Happy to pair on it if either of you wants to start sooner. |
|
Thanks @jinyouzhi, and sorry for the slow reply. Planner complexity. Your table matches the code, and one point makes it more than a speed question: each of these checks is guarded by The order I'd take them in:
On the shared Replica selection. Thanks for the clarification; I had read
That keeps the policy pluggable, which is what I meant earlier, and gives it the global view. The default can be your local-first, then fewest-assigned-bytes rule, which is deterministic and needs no hardware information. @Achyuthan-S I can open the issue with a strawman of that interface this week, so it is agreed before you pick up the policy in mid-November. |
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_transfercan read from or write into training storage directly (e.g. training → rollout weight sync) without gathering full parameters first.zero_affine.pyzero_param_storage_affine_map: low-precision replicas under ZeRO-0/1/2 (DP or expert-DP group),ds_tensorpartitions 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_mapscomposes 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 most2·ndim−1N-D boxes in the parameter's own shape, so plans stay small (row-parallel shards are not cut per row).affine_transfer.py:plan_transferaccepts 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.test_convert_checkpointstat().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
tests/unit/checkpoint/test_zero_affine.py,test_affine_transfer_plan.py: ZeRO-0/1/2/3 param + optimizer maps checked against ZeRO's own buffers (bitwise rebuild), AutoTP nest, N-D box count, flat-shard acceptance/refusal.autotp_storage_affine_maps+plan_transfer+ executor ([checkpoint] Execute affine transfer plans with bounded peer staging #8809), ZeRO-2 and ZeRO-3, weights bitwise equal after every sync. Plans are built once and reused.lowestbalancedEach 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:
lowestis the planner's default holder choice,balancedis areplica_selectorin the example (local copy first, else the least-loaded holder).