perf(reshard): deduplicate replica reads and batch receiver installation - #635
Draft
KavinKrishnan wants to merge 3 commits into
Draft
perf(reshard): deduplicate replica reads and batch receiver installation#635KavinKrishnan wants to merge 3 commits into
KavinKrishnan wants to merge 3 commits into
Conversation
A full-pulled source is staged whole and re-sliced locally into the receive buffers, one copy per view the loader recorded. On a real model that is thousands of views, and thousands of individual copy_() launches cost enough Python and launch overhead to rival the RDMA they follow. Collect the copies and issue them as a single torch._foreach_copy_ instead. The destinations are disjoint and nothing reads them until the re-slice completes, so this is the same set of copies rather than a different one. MX_RESHARD_BATCH_INSTALL=0 restores the per-view loop for an A/B. The stage record now carries which arm produced it, and reports the view count rather than the source count: the per-view launch count is what batching removes, and full_pull_sources already reports sources. This differs from the reference implementation, where reslice_copies duplicated full_pull_sources. Ported onto main from kavink/stepstamp-snapshot-2026-07-30 (a86a11c) as part of the umbrella PR #482 parity work, with the flag routed through modelexpress.envs rather than read at import time. Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
Retain one representative for each byte-identical DP/EDP shard geometry so refits do not issue duplicate reads or defeat full-pull planning. Keep source selection deterministic; the experimental source-spreading arm regressed and is intentionally excluded. Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
A read descriptor is a (session, src_addr, dst_addr, nbytes) tuple derived from the transfer plan and the registered buffer addresses. The plan is built once and cached, and the buffers are registered once, so the descriptor lists are invariant across steps -- but they were rebuilt on every refit, re-deriving an identical list of hundreds of thousands of objects in Python. On a Qwen3-30B MoE refit that is 413k descriptors per step, costing more than the local re-slice it precedes. The build was also outside every timed stage, so it appeared only as unattributed time. Measured on GB200 at EP4 to TP2 it left attribution at 60-86% against a 95% floor, which makes a stage breakdown unreportable: the largest single entry in the table was the part nobody had named. So time it as descriptor_build_s and cache it per plan. The cache is keyed on the fused/phased arm, because the phased arm never builds the exact descriptors and serving it to the fused arm would skip those reads entirely -- fewer bytes and no error. It is dropped wherever the plan is rebuilt, since the entries hold the old plan's source addresses. Gated on MX_RESHARD_CACHE_DESCRIPTORS (default on) so the rebuild-per-step behaviour stays available as an A/B arm. Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
KavinKrishnan
force-pushed
the
kavink/upstream-reshard-perf
branch
from
August 13, 2026 21:32
2738c45 to
3ddb9b1
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes, and why it is worth 10x
A reshard refit on
maintoday is not bandwidth-bound. On real Qwen3-30B withMegatron EP8 publishers feeding a vLLM TP2 receiver, it moved 47.58 GiB per rank
at an effective 146 Gbps on a 400 Gbps rail, and took 5102.6 ms.
The cause is range count, not bytes. Under DP/EDP every replica publishes the
same non-expert tensors, and
merge_shard_tableskeeps every offer it is given.Nothing downstream removes the duplicates, so the planner cannot use its dim-0
partitioner and one fused transfer degenerates into 809,112 small ranges.
Picking one owner per byte-identical offer lets the partitioner work again and
the same payload crosses the wire as 19,011 large ranges.
The arithmetic is the point. Removing duplicate bytes is only a 20.5% reduction
and would predict a 1.26x speedup on its own. The measured wire leg improved
5.9x. The other 4.9x is entirely the number of ranges the receiver asks for.
That is why the dedup here is unconditional rather than a tunable: on any
topology with replicas it is closer to a planning fix than an optimization.
mainWhy dropping reads here is safe
The obvious worry about a change that removes reads is that it removes something
needed. Three things argue against that:
tensor geometry and owner checks are all preserved. A shard that differs in
any of those is not a duplicate and is kept.
TP ranks,
max_abs_err0.0, with zero fallback and zero conversions.Deliberate corruption was detected and recovery generation agreement was 1.0.
wire volume an independent campaign measured on this model three weeks
earlier through a different harness. The duplicates were redundant, not load
that went missing.
Review guide
Three commits, ordered as validated. They are separately reviewable, and the
smallest one carries most of the win.
perf(reshard): batch the full-pull re-slice into one _foreach_copy_receiver.py(+34) — replaces thousands of per-tensor destination copies with one_foreach_copy_. Re-slice 302.2 → 123.0 ms against the same 6,192 copies.perf(reshard): deduplicate replica reads before planningrendezvous.pyonly. Start here. This is the 6x, in 31 net lines.perf(reshard): build read descriptors once per plan, and time the buildreceiver.py(+114) — caches descriptor lists per stable plan, invalidating on plan rebuild or wire-arm change, and addsdescriptor_build_s.On commit 3, please read the invalidation conditions rather than the cache
itself. Its direct value is small and honestly reported:
descriptor_build_sfalls from 12.6 ms to 0.01 ms, about 1.5% of an 800 ms refit. It is included
because rebuilding 413,772 descriptor objects per step was happening inside no
timed stage at all, which is what held stage attribution at 54.5% and made the
breakdown unreportable. The larger arm-to-arm deltas around it are run-to-run
variance, not the cache.
Source spreading was validated alongside these and is deliberately omitted.
It cost 165 ms on the
pwalinstaller, with the damage in the wire leg, and didnothing measurable on
mdl. With single-rank legs already at ~1250 Gbps of a1600 Gbps fabric there is no headroom for it to win, so spreading reads across
more peers adds contention instead of bandwidth. It should be re-judged on a
fabric with headroom, not landed on these numbers.
Evidence, and what it does not yet cover
Measured on real
Qwen/Qwen3-30B-A3B-Instruct-2507BF16 tensors — not syntheticgeometry — with Megatron EP8 publishers across two GB200 nodes and a vLLM TP2
receiver on a third, over NIXL/RDMA. One warm-up plus ten measured updates per
arm.
These figures are evidence for this implementation shape. They are not a
final benchmark row for this rebased PR head, and this is not yet a canonical
PASS: it still needs three independent cold starts, fifty synchronized updateson the selected arm, multi-step KL sanity, framework E2E alongside receiver E2E,
and a run through the production GRPO recipe. Treat it as
PERF_PASS_CORRECTNESS_PENDING.Note also that this topology is smaller than our earlier large-scale reference
runs, so the 480.7 ms figure should not be compared against those directly.
Test plan
pytest tests/test_reshard_refit_replica_merge.py tests/test_reshard_refit_batch_install.py tests/test_reshard_refit_descriptor_cache.py tests/test_envs.py tests/test_reshard_refit_fused_wire.py -q— 32 passed_alloc_ext; that is an environment gap, not a pass)