Skip to content

[AffineIR] Analyze affine transfer batches before execution (Plan analysis & calibration Part-1) - #8967

Draft
jinyouzhi wants to merge 17 commits into
deepspeedai:masterfrom
jinyouzhi:affine-transfer-analysis
Draft

jinyouzhi wants to merge 17 commits into
deepspeedai:masterfrom
jinyouzhi:affine-transfer-analysis

Conversation

@jinyouzhi

Copy link
Copy Markdown
Contributor

Analyze affine transfer batches before execution

Stacked on #8809 (executor) and #8966 (calibration path labels). Only the last commit is new.

Why

Plans that move the same bytes can differ a lot in time (up to 1.75× on 8× B70 between replica choices), and executor measurements show that the round count under a StagingBudget dominates sync time. A policy (#8935/#8936) or cost model (#8252) needs these facts before it runs a plan.

What

deepspeed/checkpoint/affine_transfer_analysis.py:

  • analyze_transfers(plans, endpoints, element_size, budget, path_of) is a pure function with no communication. It replays the executor's build_operations and round packing and reports:
    • volume;
    • segment, chunk and message size distributions;
    • rounds;
    • per process: sent/received bytes, peers, messages, pack/unpack bytes, staging arena;
    • per-pair and per-path bytes.
  • expected_stats(report, process) predicts that process's TransferStats.
  • report_to_dict returns a JSON-ready summary.
  • Messages use the same path_of labels as calibrate_links, so per-path totals can be priced with fit_links.

It does not choose replicas, rank plans, or model time.

Note: it imports the executor's private _packed_rounds / build_operations. @0z5a, would a public dry-run schedule API on the executor be preferable?

Tests

  • Hand-counted rounds and bytes on an uneven handoff.
  • Staging of strided sources.
  • A batch with a shared plan.
  • Path and pair labels in the JSON summary.
  • 4 ranks: expected_stats == the executor's TransferStats, for a strided and a replicated parameter under budgets of 3 and 64 elements.

Results: 6 passed on 4× L20 (nccl) and on 4× Intel Arc Pro B70 (xccl). Mutation checks (dropping the receiver's round count, or the local bytes in the path summary) are caught.

@jinyouzhi jinyouzhi changed the title Analyze affine transfer batches before execution [AffineIR] Analyze affine transfer batches before execution (Plan analysis & calibration Part-1) Oct 31, 2026

@0z5a 0z5a left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read the last commit (72081c1b). The report is what a policy or cost model needs: per-process sent and received bytes, rounds and messages give the bandwidth and per-message terms, and expected_stats matching the executor's TransferStats on 4 ranks is a good anchor. It passes on CPU (gloo, 4 ranks) here.

I checked the schedule reconstruction against the executor at e38477d7. The direct-send test (_is_packed versus region.is_contiguous()), the per-peer, per-direction arena sized by the largest staged round, and rounds as the maximum over a process's peer queues all match today. Two structural suggestions:

  • Share the schedule instead of re-deriving it. The module imports the executor's private _packed_rounds, and build_operations isn't in its __all__ either. It then re-implements the direct-versus-staged decision and the arena sizing. Those will drift when the executor's staging changes. staging_bytes in particular isn't covered by the expected_stats comparison, since TransferStats has no staging field. Would a public schedule helper in the executor work for you, returning each peer's rounds and whether each round is sent directly? Both sides could then call it.
  • Ordering. This stacks on #8966 only for LOCAL and default_path_of. Defining those here, or in a small shared module that calibration imports, would let this land first.

Minor: the report numbers processes by transport-group rank, while calibration uses default-group ranks, so the path labels join only when the two groups coincide. The docstring says so, but recording that assumption in the report itself would stop a consumer from mixing the two.

Add schedule_transfers, which returns what execute_transfers would run for
a batch without buffers or communication: for each process, the rounds per
peer and direction with their element and chunk counts and whether each is
sent or received in place, the staging arena each peer direction allocates,
the local copies, and the round count TransferStats reports. A replica
policy or cost model can then price a plan before running it, without
importing the executor's private helpers (deepspeedai#8967 asked for this).

execute_transfers now builds its rounds and arenas from the same private
computation, so the two cannot drift apart. The in-place decision is made
from geometry, not from the buffer: _region is an as_strided view of a
packed shard, so the view is contiguous exactly when (chunk_shape, strides)
is row-major packed, extent-1 axes excepted. plan_digest, the batch
fingerprint and TransferStats are unchanged, and build_operations and
_packed_rounds keep their names and signatures.

Tests: test_schedule_is_what_the_executor_runs (2 and 4 ranks, mixed packed
and strided regions, 16-element messages) checks rounds, sent, received and
local copy bytes against the executor's TransferStats, that both sides of
every message agree, the arena sizes, and that the batch exercises both the
in-place and the staged path. test_packed_regions_are_exactly_the_contiguous_views
compares the geometric rule with Tensor.is_contiguous over 2000 random
shapes and strides. A single-process case covers local-only batches and the
argument checks. Forcing every round to staged, or dropping the extent-1
exception, fails these tests. On macOS arm64 CPU (Python 3.12, torch
2.15.0), the executor, external-group, planner and shard-map suites pass
(263). All affine checkpoint tests pass except the four
TestAffineUniversalCheckpointResume cases, which fail the same way on master
here (uninstalled package version). pre-commit (yapf, flake8, codespell,
license) is clean.

Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
@0z5a

0z5a commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

@jinyouzhi on the schedule question: yes, I'd rather the executor own it, and I can add it. #8809 is in master now, and the executor is unchanged from the e38477d7 copy this module mirrors, so the report still matches today.

What I have in mind is a public, tensor-free helper in affine_transfer_executor, say schedule_transfers(plans, endpoints, budget, element_size, process=None). For one process, or for all of them, it returns, per peer and direction, the packed rounds with their element counts, whether each round goes direct, and the arena size, plus the local copies. The direct decision needs only geometry: _region is an as_strided view of a contiguous flat shard, so region.is_contiguous() depends only on (chunk_shape, strides), the same rule as your _is_packed. execute_transfers would build its rounds and arenas from that result, and this module could drop _packed_rounds, build_operations and its own direct-send and arena logic, so staging_bytes can't drift from what the executor allocates. That's now #9027, and this one can switch to it once it lands.

Separately, this head is stacked on 17aa897e, before #8966's d00ba303. Once it's rebased, it needs the external-group test fix from my #8966 comment.

…e executor would refuse

schedule_transfers now checks the staging budget the same way execute_transfers does, so an
empty batch can no longer be scheduled under a budget the executor refuses, and refuses a
process the endpoints don't name instead of returning an all-zero schedule. _is_packed treats
a zero-extent region as packed, matching Tensor.is_contiguous.

Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
@jinyouzhi
jinyouzhi force-pushed the affine-transfer-analysis branch from 8870b61 to 1ac1de1 Compare November 11, 2026 07:11
@jinyouzhi

Copy link
Copy Markdown
Contributor Author

Rebased onto #8966's 57a47ff, which carries the test fix. I'll switch to schedule_transfers and drop the mirrored scheduling once #9027 lands.

@jinyouzhi
jinyouzhi force-pushed the affine-transfer-analysis branch from 1ac1de1 to b97d6af Compare November 11, 2026 10:11
jinyouzhi and others added 10 commits November 11, 2026 18:10
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)
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>
@jinyouzhi
jinyouzhi force-pushed the affine-transfer-analysis branch from b97d6af to 1da4d66 Compare November 13, 2026 15:31
@jinyouzhi

Copy link
Copy Markdown
Contributor Author

@0z5a Switched to schedule_transfers (#9027 is merged into this branch until it lands): the module no longer imports _packed_rounds / build_operations or re-derives direct sends and arenas, so staging_bytes is the executor's arena. Reports are identical to the previous version on 300 random batches, except the per-chunk size distribution, which the schedule doesn't carry (the chunk count stays). The summary now records process_numbering, and the docs say to calibrate on the transfer's group. Since #8966 is close, I kept LOCAL / default_path_of there rather than moving them. Tests: 72 pass on 4×H20.

jinyouzhi and others added 4 commits November 13, 2026 23:13
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>
Add analyze_transfers/expected_stats/report_to_dict: a dry-run report of an
execute_transfers batch (volume, size distributions, rounds under a
StagingBudget, per-process load and staging, per-pair and per-path bytes),
labelled with the same path_of hook as calibrate_links.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>
(cherry picked from commit 1ac1de1)
…ysis

Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>
The analyzer rebuilt the executor's rounds with the private _packed_rounds
and build_operations, and re-derived the direct-send rule and arena sizes,
so staging_bytes could drift from what execute_transfers allocates. It now
reads rounds, direct flags, arenas and local copies from deepspeedai#9027's public
schedule_transfers. Per-chunk sizes are not part of that schedule, so the
report keeps the chunk count and drops the chunk-size distribution.

The summary also records process_numbering: processes are the
transport-group ranks TransferEndpoints binds, which join with a
calibration only when it measured the same group.

Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>
@gpu-test-labeler gpu-test-labeler Bot added the skip-gpu-test Requires positive evidence that every changed behavior can be validated without a GPU. label Nov 14, 2026

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

skip-gpu-test Requires positive evidence that every changed behavior can be validated without a GPU.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants