Repository navigation
Conversation
|
Thanks, this is the input the cost model needs. I'll build the cost-aware policy on #8936 from
Your B70 numbers already show why the cap matters. If I read the per-link fit right, seven senders give about 7 × 2.6 ≈ 18 GB/s into one receiver, which is less than one pair alone (28.2 GB/s). |
|
@Achyuthan-S Yes, you're very observant—I agree. Based on these experiments, we can refine some of our policies. |
|
Thanks! If it's easier, I can send both as a small commit to your branch: |
|
@Achyuthan-S Sure, I have added you as my fork's collabration or I also can cherry-pick a commit provided by you. Thanks |
0z5a
left a comment
There was a problem hiding this comment.
Read the last commit (3cfa4153), as asked. The measurement design is careful: the barrier and synchronize brackets keep collectives out of timed regions, the pair sweep isolates each link, and the fans separate sender and receiver sharing. On CPU (gloo, 4 ranks), the calibration tests pass together with #8967's and the executor's (62).
A few thoughts, mostly about feeding a cost model directly:
- Per-process send and receive capacity. A model of the form t = max over processes of max(sent / send_capacity, received / receive_capacity) needs each process's aggregate send and receive capacity.
fit_linksgives per-path link costs; the fan fits use the peers' times, and the roots' times, which carry the aggregate, stay only in the raw result. Computingbytes * len(peers) / root_secondsfrom the raw fan entries is a few lines. On CPU it gives a per-root figure for each direction. Havingfit_linksfit those per process (or per root) assendandreceivewould let #8967'sProcessReport.sent_bytes/received_bytesbe priced directly. - Fan peers. The peers are
root + 1 … root + fan_width, so withfan_widthbelow the processes per node every fan stays within a node. If receive capacity differs across nodes, a way to choose the peers (afan_peers(root)hook, or a stride) would let the fans cover it. - Defaults at scale. With the default
fan_width = world - 1, each process can allocate(1 + world) * max(sizes), about 16 GiB at 64 ranks and 256 MiB, and the pair sweep is O(world²) barriers per size. The docstring says so, but a default capped at the processes per node, or a check against free memory, might save someone a surprise. - Process group. Calibration always uses the default group, while the executor runs on whatever group
TransferEndpointsnames. An optionalgroupargument would let it measure the group the executor will actually use, e.g. an external trainer–rollout group, with the same rank numbering the analysis report uses.
|
@Achyuthan-S @0z5a Thanks both. I agree with the direction: a list of fan widths and a fitted root time (per-process Topology. Four PCIe switches, each with two cards behind one Gen5 x16 root port, and two switches per socket: {0,1} {2,3} on socket 0 and {4,5} {6,7} on socket 1 (assuming ranks follow PCI address order). GH200 over Slingshot is a Clos, so every sender set looks alike there, which fits your flat incast. Pair bandwidth, GB/s (row = sender, column = receiver): These are stable across runs. Cross-socket traffic sits near 27 GB/s. Within a socket, bandwidth depends on the sender and the direction (2→0 is 54, 0→2 is 7.9, on the same switches), which the tree alone does not predict. Fan-in root aggregate (
Three cases explain most of it:
So the finish time looks like Proposal for this PR:
On 0z5a's other points:
@Achyuthan-S Please push your width-list and root-fit commit. I'll add the per-pair fit and the |
|
Pushed the width-list and root-fit commit:
Your B70 table is a great counterexample. It fits the same form once every shared resource is a term, not only the endpoints: a fan finishes when the busiest resource it crosses does, i.e. the max over each sender's link, the receiver, and any shared link such as your cross-socket hop, of the bytes through it over its capacity. Your three cases are one of each:
So your per-pair fit and |
|
@Achyuthan-S @0z5a Pushed ba8b39f (on top of Achyuthan's commit, with #8809's 11fe945 merged in). Updates
On the B70 counterexample: Validation: 8× L20 (driver 580.173.02, torch 2.14.1+cu130): calibration tests on 4 GPUs, 10 passed; external-group test on CPU passed 20 of 20 runs; pre-commit passed. Multi-node not validated. |
|
Thanks, this is the right split. Keying by peer set captures exactly what the B70 table showed, and I agree on the semantics: One small thing for consumers: the fits now have tuple keys ( |
|
Good catch. I added a note to the |
|
Thanks for the JSON note and the H20 multi-node numbers. The same NVLink pair measuring 275 GB/s in one group and 54 GB/s in another is a useful warning, and I'll make "calibrate in the transfer's own group" explicit on the policy side. One concern with dropping the concurrent pattern: some transfers do have processes sending and receiving at once.
@0z5a's B300 runs show what this costs: 618 GB/s per direction duplex, against 664 GB/s one-way. On GH200 the overlap held because the NIC's receive and the NVLink send are separate resources. So the policy needs a duplex number per path. The shift pattern's labelling was the problem, not the measurement, and a simpler pattern would do: for example, each ordered pair exchanging both ways at once, labelled by the pair's path in each direction. If you'd rather keep this PR one-way, a line in the docs saying duplex isn't measured, plus a follow-up, works too. |
|
@Achyuthan-S Thanks. I had assumed I agree the labelling was the problem. The old shift pattern made it unclear which participant each sample described. |
|
@Achyuthan-S Pushed d00ba30, following your suggestion:
H20, one node, 4 GPUs over NVLink, 256 MiB, median of 10, GB/s per direction: one-way 331–335, both ways 276–287 (0.83–0.86×), relay 282–287 (≈0.85×). Calibration tests pass on 4 GPUs (12 passed). Cross-node relay is not measured yet. |
|
Thanks @jinyouzhi, d00ba30 covers all four cases. This is how the #8936 policy will read it:
Your H20 numbers (both ways at 0.83–0.86× one-way) and @0z5a's B300 (618 vs 664 GB/s, 0.93×) show the same thing: the cost is real and depends on the machine, so it has to be calibrated. On CPU (gloo, 4 ranks) the calibration tests pass at d00ba30 (12 passed). One small thing: the raw format gained When you get a multi-node slot, the |
|
Re-read up to
|
57a47ff to
851ea68
Compare
Codex Review Summary: ✅ CompletedThis 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: 851ea68c11
ℹ️ 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".
| fans = {'fan_out': [], 'fan_in': []} | ||
| for size in sizes: | ||
| for process in range(world): | ||
| local.append({'bytes': size, 'process': process, 'path': LOCAL, 'seconds': gathered[process][0][size]}) |
There was a problem hiding this comment.
Label local samples through path_of
When a custom path_of distinguishes local devices, such as on a heterogeneous host, every local sample is still assigned the constant local instead of path_of(process, process). fit_links consequently pools unlike device-local costs into one fit despite the documented per-path sample contract, producing incorrect local estimates for such calibrations.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in d465334: local samples are now labeled path_of(process, process), like every other sample, so a path_of that tells local devices apart gets a separate local fit per label. test_fans_label_each_message_by_its_direction now checks the local labels too.
| return { | ||
| 'schema': CALIBRATION_SCHEMA, | ||
| 'world_size': world, | ||
| 'communication_backend': get_accelerator().communication_backend_name(), |
There was a problem hiding this comment.
Report the measured process group's backend
When the supplied or default process group uses a backend different from the accelerator's preferred default (for example, an explicitly created Gloo group on a CPU installation that also provides CCL), this records the accelerator default rather than the backend _exchange actually measured. Persisted calibrations are therefore mislabeled and cannot be reliably compared or reused; derive this metadata from the active process group instead.
AGENTS.md reference: AGENTS.md:L40-L40
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in d465334: communication_backend now comes from the measured group, via a new deepspeed.comm.get_backend(group) that wraps the backend's existing get_backend. test_records_the_backend_it_measured patches the accelerator's preferred backend name to a decoy and checks that the recorded backend is still the group's.
Add calibrate_links/fit_links: raw local, pairwise, concurrent, 1-to-N and N-to-1 timings labelled by a path_of(from, to) hook, with a per-path alpha + bytes/bandwidth summary, as measured inputs for transfer cost models. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com> (cherry picked from commit 3cfa415)
…ive capacity Signed-off-by: Achyuthan Sivasankar <achyuthan.sivasankar@gmail.com> (cherry picked from commit dc1722e)
…ode-capped default - fit_links keys fan_out, fan_in, send and receive by (root, sorted peers), so fans of one width with different senders stay apart; the width is len(peers). Adds peer_pairs, a fit per ordered (from, to) pair. - calibrate_links takes fan_peers(root, width) to choose the peers, and an optional group whose ranks number the processes. - The default fan width is one less than the fewest processes on any node, and default peers are on the root's node first. - Docs state that width-keyed fits assume a uniform fabric, with B70 data. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com> (cherry picked from commit ba8b39f)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com> (cherry picked from commit f00ce17)
Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com> (cherry picked from commit 2c43824)
… a path A concurrent sample times a process's send to process + shift and its receive from process - shift together, but was labelled by the send path alone. Across nodes, an intra-node send paired with an inter-node receive takes the inter-node time, so the intra_node concurrent fit came out at 7.5 GB/s over TCP against 65 GB/s for clean intra-node processes on 2x8 H20. Record receive_paths and fit each path only from processes whose send and receive both took it. Document calibrating on the group and settings the transfer uses. Signed-off-by: iLeGend <824040212@qq.com> (cherry picked from commit 575bef3)
Weight transfers are one-way: a training process sends and a rollout process receives, so no process sends and receives at once. The concurrent shift pattern timed every process doing both, which matches no transfer, and needed per-direction path filtering to fit at all. Remove it with its fit, docs and tests, and drop the hardware-specific measurements from the docs. Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com> (cherry picked from commit 17aa897)
Some transfers have a process send and receive at the same time: colocated resharding, the all-gather stage of a relay, a pipelined relay, and moves between trainers of different DP sizes. peer samples gain both_ways. A two-way sample has a pair send to each other at once, with every other process idle, and gives one sample per direction labeled with that direction's path. fit_links keeps peer and peer_pairs one-way and adds peer_both_ways and peer_both_ways_pairs, so a policy can see what a link loses per direction when both directions are busy. The shift pattern returns as relay: every process receives from process - shift while sending to process + shift. Each sample is one process's own time, labeled with who it received from and sent to and with both the receive_path and the send_path, since that time covers both. fit_links fits relay per (receive_path, send_path), so no sample is filed under a path it only half used. Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com> (cherry picked from commit d00ba30)
Both directions of a both_ways sample carry the pair's completion time, so peer_both_ways_pairs[(a, b)] equals peer_both_ways_pairs[(b, a)]. Say so in fit_links and the docs, and compare against the slower of the two one-way fits. Keep the external-group executor test's assertion to one-way peer samples, since calibrate_links now also returns both_ways samples. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com> (cherry picked from commit 57a47ff)
…peedai#8936) Implements the planner side of deepspeedai#8935. It does not close the issue: the cost-aware policies are the policy layer under deepspeedai#8252. **What it adds** - `plan_transfers(pairs, limits, *, source_processes, target_processes, element_size, min_split_bytes, replica_policy)` in `deepspeed/checkpoint/affine_transfer.py`. It plans every parameter of one sync and calls one replica policy with every request at once. - `ReplicaRequest` and `ReplicaPart`. - A request names the parameter, the target location, the region and its validated holders. When the caller passes the location→process mappings, it also names the processes behind them. - Regions with a single holder are included, so a policy sees load it cannot move. - Requests arrive in a fixed order: parameter, then target location, then origin. - An answer is either a holder location, or `ReplicaPart`s that cut the region along axis 0. - The planner checks every answer before it emits any segment, and raises `InvalidReplicaPolicyError` otherwise: - one answer per request; - only validated holders; - parts that tile the request exactly along axis 0; - part fields that are not iterable coordinates, rather than a raw `TypeError`; - with `min_split_bytes`, every part of a split is at least that large; - the parts of one parameter stay within `PlanBudget.max_pair_checks`, checked from the answer's length before the parts are expanded. - `deepspeed/checkpoint/affine_replica_policy.py` holds three policies: - `first_holder`: what `plan_transfer` does by default. - `local_first_least_loaded`: the `plan_transfers` default. 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 existing `replica_selector` as a batch policy. **Review follow-up** (commit `773ddfd3`) - Non-iterable `ReplicaPart.logical_origin`/`shape` fields are refused as `InvalidReplicaPolicyError`; the pair budget now uses that error too, so every refused answer uses the documented class. - An over-budget answer is refused on its length before `_resolve` expands it. - `plan_transfers`, the request/answer protocol, the process mappings, `element_size` (one entry per parameter) and the split constraints are documented in `docs/code-docs/source/affine-transfer.rst`, the page from deepspeedai#8809. - Policy authors are told that every rank must return the same answers, so cost inputs must come from data every rank shares (for example `fit_links` on the calibration from deepspeedai#8966), never from a rank-local measurement; the executor's schedule digest refuses a mismatch. - The deepspeedai#8252 correctness theorem is cited as "Any consistent plan is exact" instead of by number. **What it does not change** `plan_transfer` behaves exactly as before; it shares its validation and emission steps with `plan_transfers`. AutoEP's `plan_expert_transfer` and its docs are untouched. **Decisions from deepspeedai#8935** - One sync per batch. - Only axis-0 splits for now. Other split shapes are refused, so widening later is an additive change. - Plain location→process mappings rather than `TransferEndpoints`, so the planner does not import the executor. - Policies live in their own module. - The per-request adapter stays. `min_split_bytes` comes from my last comment on deepspeedai#8935 and has not been agreed yet. It is optional and off by default. The `process_node` mapping proposed there is deferred. **Tests** `tests/unit/checkpoint/test_affine_replica_policy.py`, CPU only: - Random policies over 24 seeds, including random splits across holders, on five parameters: whole replicas, a partly replicated fused block, a vector and a scalar. Every plan writes each target element exactly once and matches `extract`. This is the check that matches the "Any consistent plan is exact" theorem of the correctness draft for deepspeedai#8252. - `first_holder` and `per_request` reproduce `plan_transfer`'s plans exactly. - The default policy: - copies locally when it can; - balances load, counting single-holder load; - never assumes a local copy without mappings. - Refused answers: - holders that are not candidates; - bools, floats, `None` and strings; - gaps, overlaps and parts outside the region; - cuts along axis 1, empty parts and parts with the wrong number of axes; - non-iterable part fields; - the wrong number of answers; - splits under `min_split_bytes`, and more parts than the budget allows, including parts accumulated across requests. - The maps are validated before the policy runs. - The pair budget is decided from the answer's length before the planner reads its parts. With `DS_ACCELERATOR=cpu` on macOS arm64 (Python 3.11, torch 2.15.0), the new file passes (65 tests) together with `test_affine_transfer_plan.py`, `test_autoep_affine.py` and `test_affine_shard_map.py` (306 passed), and `tests/unit/v1/moe/test_autoep_unit.py` (which covers `plan_expert_transfer`) together with `test_autoep_affine_universal.py` pass 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** - The branch is rebased on current master (after deepspeedai#8809, deepspeedai#8885 and deepspeedai#8975); `plan_transfer` still shares `_prepare` / `_emit` with `plan_transfers`. - @jinyouzhi: the `affine_geometry.py` move can land before or after this; I'll rebase in whichever order suits you. - deepspeedai#8966 supplies the link calibration (`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 --------- Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Ma, Guokai <guokai.ma@gmail.com>
A path_of that tells local devices apart, as on a host with unlike accelerators, had every local copy labeled 'local' and pooled into one fit. Local samples now carry path_of(process, process) like every other sample. communication_backend recorded the accelerator's preferred backend, which need not be the one the measured group runs on. It now comes from the group itself, through a new deepspeed.comm.get_backend that wraps the backend's existing get_backend. Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>
|
Thanks @jinyouzhi, d465334 covers both Codex points, and @0z5a's two are in as well. One from my earlier comment is still open at d465334: |
|
@Achyuthan-S Fixed in f678c9d: |
fit_links reads peer samples' both_ways and the raw relay list. A raw calibration saved before those existed still claimed schema 1, so it passed the schema check and then raised KeyError. Schema 2 makes fit_links refuse it with a clear error instead. Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>
Calibrate affine transfer links per path
Stacked on #8809 only for its docs page (
affine-transfer.rst); the module itself needs nothing beyond master. Until #8809 merges, please review only the commits after the #8809 merge.Why
A replica policy or cost model choosing between plans (#8252, #8935/#8936) needs link costs measured on the machine it runs on. Executor measurements show that round count and receiver sharing dominate sync time, so per-pair numbers are not enough on their own; this PR also measures 1→N and N→1.
What
deepspeed/checkpoint/affine_transfer_calibration.py:calibrate_links(sizes, repeats, warmup, path_of, fan_width, fan_peers, group): collective overgroup(the default group unless given). Returns the same raw samples on every process, with no aggregation, for:localcopy;peer(each ordered pair alone);fan_out(1→N);fan_in(N→1, receiver contention).Sizes run from 4 KiB to 256 MiB by default.
Each message is labelled by
path_of(from_process, to_process).default_path_ofgiveslocal/peer;node_path_of(n)givesintra_node/inter_node; any callable works.fit_links(calibration): per-pathalpha + bytes / bandwidth, fitted to the per-size medians;peer_pairsper ordered pair; fans keyed by(root, peers), withsend/receivegiving each root's aggregate rate.Weight transfers are one-way (a training process sends, a rollout process receives), so no pattern has a process send and receive at once.
It measures only. It does not plan, select replicas, or change the planner or executor.
Example (one node, 8 devices, default sizes)
* poor linear fit (max relative error 0.77); small copies are cache-resident.
Multi-node (2 × 8 H20, RoCE 8 × 200 Gb/s per node,
node_path_of(8), 16 processes)Tests
tests/unit/checkpoint/test_affine_transfer_calibration.py:path_of.10 passed on 8× B70 (xccl).
pytest tests/unit/checkpoint/ -k affine: 344 passed, 1 skipped on 8× H20 with torch 2.13.0+cu129 / NCCL 2.29.7 and with torch 2.9.1+cu129 / NCCL 2.29.7. Mutation check: flipping the fan_in label direction fails a test. Cross-node runs used NCCL 2.29.7 on both nodes (torch 2.13.0 and 2.9.1).AI assistance (GitHub Copilot) was used for the multi-node runs and this description; I reviewed the changes and the results.
Follow-ups