Repository navigation
[checkpoint] Batch replica selection for affine transfer plans - #8936
Conversation
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: 24881a444e
ℹ️ 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".
| return _emit(prepared, choices, budget) | ||
|
|
||
|
|
||
| def plan_transfers(pairs: Mapping[str, Tuple[ParamAffineMap, ParamAffineMap]], |
There was a problem hiding this comment.
Document the new batch planning API
For downstream users adopting plan_transfers, this patch exposes the entry point in __all__ plus a new callback/split protocol and default selection behavior, but adds no user documentation; the existing AutoEP docs only explain plan_expert_transfer and the old per-request replica_selector, so there is no supported reference for the process mappings, element sizes, split constraints, or policy answers. Add the required documentation alongside the feature.
AGENTS.md reference: AGENTS.md:L71-L71
Useful? React with 👍 / 👎.
| if type(part.source_location) is not int or part.source_location not in holders: | ||
| raise InvalidReplicaPolicyError(f'{where}: location {part.source_location!r} is not a validated ' | ||
| f'holder {sorted(holders)}') | ||
| origin, shape = tuple(part.logical_origin), tuple(part.shape) |
There was a problem hiding this comment.
Validate part fields before tuple conversion
When a custom policy returns a ReplicaPart whose logical_origin or shape is non-iterable (for example None), these tuple(...) calls raise a raw TypeError. The function's new contract says every invalid answer raises InvalidReplicaPolicyError; callers that catch AffineTransferError to reject a policy or fall back will instead abort, so convert and validate these fields under the policy-error path.
Useful? React with 👍 / 👎.
| parts = _resolve(request, item, answer, min_split_bytes, element_size) | ||
| choices[name][index] = parts | ||
| parts_per_name[name] += len(parts) | ||
| if parts_per_name[name] > budget.max_pair_checks: |
There was a problem hiding this comment.
Enforce split budgets before materializing parts
When a custom policy accidentally returns a very large split for one request, _resolve materializes every LogicalBox and sorts the full answer before this cumulative check runs. Consequently even a tiny max_pair_checks does not bound planner work or memory, so a malformed policy can cause a large allocation or OOM before it is rejected; enforce the remaining per-parameter allowance while or before resolving the answer.
Useful? React with 👍 / 👎.
Achyuthan-S
left a comment
There was a problem hiding this comment.
I reviewed this from the policy side, since the cost-aware policy will sit on this interface. It has what that policy needs:
- Each request carries its bytes, the target process and every candidate's process.
- The whole sync arrives in one call, in a fixed order, so a policy can balance load over all of it, including the single-holder load it can't move.
The topology and the calibration (#8966's fit_links) can live inside the policy, so I agree the process_node mapping can stay out of the planner. The refactor keeps plan_transfer's selection and emission as they were, and local_first_least_loaded does what its docstring says.
Checked on CPU:
- At 24881a4:
test_affine_replica_policy.py,test_affine_transfer_plan.pyandtest_affine_shard_map.pypass (251). - Merged with current master (after #8809, #8885 and #8975), the merge is clean, and the planner, executor, AutoEP and ZeRO-affine tests pass together (583 passed, 8 skipped).
A few small things:
- Codex's first P2 is real: a
ReplicaPartwithlogical_origin=Noneraises a rawTypeError(I checked), notInvalidReplicaPolicyError. The second is minor: the policy's answer is already in memory by then, so checkinglen(answer)against the remainingmax_pair_checksallowance before_resolveonly saves the planner's own work on a malformed split. It's still cheap to add. - Docs: #8809 has landed, so the
plan_transferssection can go intoaffine-transfer.rstnow. - A note for policy authors: every rank has to return the same answers. #8809's schedule digest already refuses mismatched plans ("ranks disagree about the transfer schedule"), so this fails loudly. Still, one line in the docstring saying that cost inputs should come from data every rank shares, such as
fit_linkson the gathered calibration, would help. - The test docstring cites "Theorem 3 of the proof draft". In the PDF I posted it's Theorem 4, "Any consistent plan is exact". Citing it by name would survive renumbering.
LGTM from the policy side. The first part of 1 is worth doing before merge, and the rest is up to you.
Add plan_transfers, which plans every parameter of one sync and hands a replica policy every request at once, as agreed in deepspeedai#8935. The policy returns a holder location per request, or splits a request along axis 0 across validated holders. The planner checks every answer before it emits a segment: one answer per request, only validated holders, and parts that tile the request exactly. So a policy decides where data is read from, never what arrives. Requests name map locations and, when the caller passes the location-to-process mappings, the processes behind them, so a policy can tell a local copy from a transfer. element_size turns elements into bytes and min_split_bytes bounds every part of a split. Policies live in affine_replica_policy.py: first_holder (plan_transfer's default), local_first_least_loaded (the plan_transfers default) and per_request, which runs an existing replica_selector as a batch policy. plan_transfer itself is unchanged; it now shares its validation and emission steps with plan_transfers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
A ReplicaPart whose logical_origin or shape is not iterable is now refused with InvalidReplicaPolicyError instead of raising a raw TypeError, and an answer with more parts than the remaining max_pair_checks allowance is refused on its length before _resolve expands it. The pair budget now raises InvalidReplicaPolicyError as well, so every refused answer uses the error the planner documents. Document plan_transfers, the request and answer protocol, the process mappings, element sizes (one entry per parameter), split constraints and the built-in policies in affine-transfer.rst, and tell policy authors that answers must agree across ranks, so cost inputs must come from data every rank shares. Cite the deepspeedai#8252 theorem "Any consistent plan is exact" by name instead of by number. Tests: the rejected-answer cases include non-iterable part fields, test_the_pair_budget_is_checked_before_the_parts_are_read pins that the planner decides from len(answer) without reading the parts, and both budget tests pin the policy error, including parts accumulated across requests. All five fail on the previous planner. On macOS arm64 CPU (Python 3.11, torch 2.15.0), test_affine_replica_policy.py passes (65 tests), the four affine checkpoint suites pass together (306), and the AutoEP unit suites pass (215 passed, 8 skipped). pre-commit (yapf, flake8, codespell, license) is clean. Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
24881a4 to
773ddfd
Compare
|
@Achyuthan-S thanks — all four are in
Validation on macOS arm64 CPU (Python 3.11, torch 2.15.0): 65 tests in the new file, 306 across the four affine checkpoint suites, and 215 passed / 8 skipped for the AutoEP suites; pre-commit (yapf, flake8, codespell, license) is clean. No GPU path is touched. |
|
Re-checked 773ddfd: all four are in.
I merged the branch with current master: the merge is clean, and the planner, replica-policy, shard-map, ZeRO-affine, AutoEP-affine and executor suites (including the external-group test) pass together on CPU, with 383 passed. LGTM from the policy side. The cost-aware policy will build directly on this interface. |
Implements the planner side of #8935. It does not close the issue: the cost-aware policies are the policy layer under #8252.
What it adds
plan_transfers(pairs, limits, *, source_processes, target_processes, element_size, min_split_bytes, replica_policy)indeepspeed/checkpoint/affine_transfer.py. It plans every parameter of one sync and calls one replica policy with every request at once.ReplicaRequestandReplicaPart.ReplicaParts that cut the region along axis 0.InvalidReplicaPolicyErrorotherwise:TypeError;min_split_bytes, every part of a split is at least that large;PlanBudget.max_pair_checks, checked from the answer's length before the parts are expanded.deepspeed/checkpoint/affine_replica_policy.pyholds three policies:first_holder: whatplan_transferdoes by default.local_first_least_loaded: theplan_transfersdefault. It takes a holder in the target's own process first, then the sending process with the fewest bytes assigned so far in this batch. Ties go to the lowest location, and it never splits.per_request(selector): runs an existingreplica_selectoras a batch policy.Review follow-up (commit
773ddfd3)ReplicaPart.logical_origin/shapefields are refused asInvalidReplicaPolicyError; the pair budget now uses that error too, so every refused answer uses the documented class._resolveexpands it.plan_transfers, the request/answer protocol, the process mappings,element_size(one entry per parameter) and the split constraints are documented indocs/code-docs/source/affine-transfer.rst, the page from [checkpoint] Execute affine transfer plans with bounded peer staging #8809.fit_linkson the calibration from [AffineIR] Calibrate affine transfer links per path (Plan analysis & calibration Part-2) #8966), never from a rank-local measurement; the executor's schedule digest refuses a mismatch.What it does not change
plan_transferbehaves exactly as before; it shares its validation and emission steps withplan_transfers. AutoEP'splan_expert_transferand its docs are untouched.Decisions from #8935
TransferEndpoints, so the planner does not import the executor.min_split_bytescomes from my last comment on #8935 and has not been agreed yet. It is optional and off by default. Theprocess_nodemapping proposed there is deferred.Tests
tests/unit/checkpoint/test_affine_replica_policy.py, CPU only:extract. This is the check that matches the "Any consistent plan is exact" theorem of the correctness draft for Define affine IR for checkpointing #8252.first_holderandper_requestreproduceplan_transfer's plans exactly.Noneand strings;min_split_bytes, and more parts than the budget allows, including parts accumulated across requests.With
DS_ACCELERATOR=cpuon macOS arm64 (Python 3.11, torch 2.15.0), the new file passes (65 tests) together withtest_affine_transfer_plan.py,test_autoep_affine.pyandtest_affine_shard_map.py(306 passed), andtests/unit/v1/moe/test_autoep_unit.py(which coversplan_expert_transfer) together withtest_autoep_affine_universal.pypass too (215 passed, 8 skipped). yapf 0.43.0, flake8 5.0.4, codespell, the license check and the other pre-commit hooks are clean. The five rejection/budget tests added here fail on the previous planner and pass now.How it fits with other PRs
plan_transferstill shares_prepare/_emitwithplan_transfers.affine_geometry.pymove can land before or after this; I'll rebase in whichever order suits you.fit_links) that cost-aware policies will read; the planner does not consume it.Scope, compatibility and AI assistance
Only the CPU planning interface, its tests and its documentation change: no training-loop, GPU, ZeRO, offload or precision path is touched, so no compatibility-matrix cell or on-device serving check is affected, and none was run. AI assistance was used: the initial implementation was co-authored with Claude Opus 5.5, this review follow-up was prepared with an AI coding agent, and the submitter reviewed every changed line and ran the tests above.
cc @Achyuthan-S @delock