Repository navigation
Conversation
Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
Address review on the affine transfer executor: - Decide stream-ordered completion with is_synchronized_device() instead of the device type, for both the per-round handle and the final stream synchronization. - Run the multi-rank executor tests on any accelerator. The disjoint-endpoint case now uses two or four ranks, so it fits the two-device PR runners and the four-rank CPU nightly. - Move the executor documentation from the AutoEP page to a new Checkpointing API page, and record that the staging arena and StagingBudget belong to the current torch point-to-point transport. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
RL frameworks build their trainer <-> engine weight-update group
outside the default world: each process keeps its own default group
(world size 1 for a TP=1 trainer or engine) and joins a separate group
created with _new_process_group_helper on its own store, as slime,
SGLang and vLLM do. torch's rank bookkeeping for such a group maps
every member's default-world rank 0 to group rank 0, so
dist.get_rank(group) returned 0 on both ends and the preflight refused
the whole batch ("target location 0 is not owned by this process").
When a group is passed, read the rank and size from the group itself.
Every other path is unchanged.
Add a CPU reproduction: two processes, each with a world-size-1
default group, joined by an externally created gloo group. It fails
before this change and passes after it. The new test file builds that
group with torch.distributed directly, so it joins the existing
check-torchdist exclusion next to tests/unit/comm/test_dist.py.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
The test builds the trainer <-> engine weight-update group the way RL frameworks do, outside deepspeed.comm, so calling torch.distributed directly is intended. Say so next to the import, as requested in review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
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>
|
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 |
The external-group test failed once in the merge queue with "process 0 terminated with signal SIGABRT" and "terminate called without an active exception". Rank 0 only sends and hosts the TCPStore, so it could return and exit while rank 1 was still receiving; its gloo and store threads were then destroyed at interpreter exit with the peer still connected. Have both ranks meet on the group, destroy the external and the default group, and drop the store before returning. The result check now runs after the teardown, so a mismatch still fails the test without leaving rank 0 waiting on a barrier that rank 1 never reaches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
…ive capacity Signed-off-by: Achyuthan Sivasankar <achyuthan.sivasankar@gmail.com>
|
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 |
…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>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>
|
@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 |
Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>
… 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>
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>
|
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. |
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>
|
@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. |
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