perf(distributed): ring-pooled PP recv buffers and Kimi-K3 2k benchmark shape - #3780
Open
yisongbetter wants to merge 5 commits into
Open
perf(distributed): ring-pooled PP recv buffers and Kimi-K3 2k benchmark shape#3780yisongbetter wants to merge 5 commits into
yisongbetter wants to merge 5 commits into
Conversation
3 tasks
yisongbetter
force-pushed
the
yisongbetter/perf/k3-pp-recv-pool
branch
3 times, most recently
from
September 2, 2026 06:05
9314231 to
e040d26
Compare
yisongbetter
force-pushed
the
yisongbetter/perf/k3-pp-recv-pool
branch
from
September 3, 2026 14:04
e040d26 to
224b48e
Compare
…nc pins Two launch-tax reductions for the Kimi-K3 GB200 benchmark, measured at 256 GPUs (64x GB200 NVL72, pp8 x EP32, 1k rows x GBS 4096): 1. BackendConfig.compile_norm (default False): torch.compile the fp32 RMSNorm chain once per process, same lazy pattern as the merged compile_situ. At 256 GPUs this is worth +11% end-to-end (launch-bound regime: fusing the per-token norm chain pays in both busy time and inter-kernel gaps); the same change gains only +5.3% on a 2-node mini. 2. BackendConfig.benchmark_static_routing (default False, validated to require fake_balanced_gate with zero noise): under forced-balanced routing the per-microbatch routing metadata is constant, so the recurring device-to-host syncs on it are skipped after the first microbatch: tokens_per_expert.tolist() in GroupedExpertsTE, count_nonzero + the CPU tokens_per_expert copy in GroupedExpertsDeepEP, and the HybridEP num_permuted_tokens reset in the flex dispatcher (config field moe_benchmark_static_routing). Worth +2.4% together with the mask fixes below. Plus two unconditional fixes on the same hot path: - _update_linear_attn_mask returns early when attention_mask is None, skipping a per-microbatch D2H sync on cache_position[0] (both branches returned None for that input). - _make_causal_mask caches one upper-triangular mask per (dtype, device), grown on demand and sliced per call, instead of rebuilding the [S, S] mask every microbatch. The benchmark YAML enables both flags; its documented throughput moves from ~441 tok/s/GPU (12.2% MFU) to ~496 tok/s/GPU (13.7% MFU), measured wall-clock with the same formula as before. Mini-scale gates: loss parity across all steps with flags on/off; RMSNorm compile parity is covered by unit tests (allclose forward and backward). Signed-off-by: Yisong Li <yisongbetter@gmail.com>
Address the review bot's two observations: extract the TE-path split-size resolution into _resolve_m_splits (unchanged logic, now directly unit-testable on CPU) with tests asserting cache reuse under static routing and re-materialization without it; and read BackendConfig.benchmark_static_routing directly instead of a defensive getattr on the typed field. Signed-off-by: Yisong Li <yisongbetter@gmail.com>
The 13.7% MFU figure was measured before NVIDIA-NeMo#3792 aligned the KimiK3TextConfig defaults with the released checkpoint (56-head MLA / 18432-wide dense FFN, ~101.5B active matmul parameters instead of 104.0B). Say so in the header, and point at the recipes/llm/benchmark.py entry that prints the recipe's own MFU, until a re-measurement on the aligned shape replaces the numbers. Signed-off-by: Yisong Li <yisongbetter@gmail.com>
…rk shape
torch.distributed.pipelining pre-allocates one full-size P2P recv buffer
per microbatch and per direction (activations forward, gradients
backward). At large microbatch counts this dominates pipeline-stage
memory: Kimi-K3 at 2k rows x GBS 4096 / mbs 2 / dp32 means 64 buffer sets
per direction, ~52 GiB on middle stages, and the shape OOMs during step 1
on 64 GB200 nodes (measured 56 MiB short).
Under 1F1B only (num_stages - stage_index) microbatches are in flight per
stage: a forward recv buffer is live from irecv-post until that chunk's
backward consumes the stage input (activation-checkpoint recompute reads
it); a grad recv buffer only until the chunk's backward returns. The new
components/distributed/pipelining/recv_buffer_pool.py therefore rebinds
the per-chunk recv-info maps onto a ring of K = inflight + slack real
buffer sets (~45 GiB saved on middle stages at the shape above). Grad
accumulation on reused leaf buffers is impossible upstream:
stage_backward() harvests input grads and sets val.grad = None per chunk.
- Opt-in: distributed.pipeline.pp_recv_buffer_pool (+ slack, default 2),
threaded PipelineConfig -> AutoPipeline.build().
- Schedule-gated: allowlist {1f1b} via schedule_supports_recv_pool();
schedules with unbounded in-flight depth (e.g. GPipe) would silently
corrupt gradients through reused buffers (verified negative on the CPU
parity harness during development) and keep stock behavior with a
warning.
- torch-version adaptive, fail-open: supports both the >= 2.13 layout
(_setup_forward/backward_recv_info) and the 2.12 line in current NGC
containers (_prepare_forward/backward_infra); anything else logs a
warning and keeps stock allocation.
New examples/llm_benchmark/kimi/kimi_k3_gb200_2k.yaml documents the shape
this unlocks: 615.9 tok/s/GPU = 17.1% MFU at 256 GPUs (vs 13.7% for the 1k
config), peak memory 139.6 GiB. Mechanism: the workload is launch-bound at
this scale, so doubling tokens per row halves the per-token launch tax,
and K3's KDA linear attention does not penalize longer rows.
Correctness evidence: in-PR gloo parity test (pp4 x 1F1B x 16 microbatches
x 3 steps, stock vs pooled bitwise-identical losses and per-stage param
sums; the test catches ring-too-small corruption because linear weight
grads read the saved stage input, which IS the recv buffer); pp2
cluster ring-activation gate with bitwise-identical finite-loss trajectory
over 8 steps; 256-GPU production run rc=0 with 44 GiB headroom.
Signed-off-by: Yisong Li <yisongbetter@gmail.com>
The 17.1% MFU figure predates NVIDIA-NeMo#3792's KimiK3TextConfig default alignment (56-head MLA / 18432-wide dense FFN, ~101.5B active matmul parameters instead of 104.0B). Record that in the header and point at the recipes/llm/benchmark.py entry that prints the recipe's own MFU, until a re-measurement on the aligned shape replaces the numbers. Signed-off-by: Yisong Li <yisongbetter@gmail.com>
yisongbetter
force-pushed
the
yisongbetter/perf/k3-pp-recv-pool
branch
from
September 3, 2026 17:34
224b48e to
8a58634
Compare
Contributor
Author
|
/ok to test 8a58634 |
3 tasks
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 does this PR do ?
Opt-in ring pooling of
torch.distributed.pipeliningP2P recv buffers under1F1B (
distributed.pipeline.pp_recv_buffer_pool), which removes theper-microbatch buffer pre-allocation that OOMs Kimi-K3 at 2k rows × GBS 4096
on 64 GB200 nodes, plus the new
kimi_k3_gb200_2k.yamlbenchmark shape itunlocks.
torch.distributed.pipeliningpre-allocates one full-size P2P recv bufferper microbatch and per direction (activations forward, gradients backward).
At large microbatch counts this dominates stage memory: Kimi-K3 at 2k rows x
GBS 4096 / mbs 2 / dp32 means 64 buffer sets per direction, ~52 GiB on middle
stages — more than the activations — and the shape OOMs during step 1 on
64 GB200 nodes (by only 56 MiB, measured).
Under 1F1B only
num_stages - stage_indexmicrobatches are in flight perstage: a forward recv buffer is live from irecv-post until that chunk's
backward consumes the stage input (AC recompute reads it); a grad recv buffer
only until the chunk's backward returns. New module
components/distributed/pipelining/recv_buffer_pool.pyrebinds the per-chunkrecv-info maps onto a ring of
K = inflight + slackreal buffer sets(~45 GiB saved on middle stages at the shape above).
distributed.pipeline.pp_recv_buffer_pool: true(+
pp_recv_buffer_pool_slack, default 2), threadedPipelineConfig -> AutoPipeline.build().schedules log a warning and keep stock behavior — GPipe's unbounded
in-flight depth silently corrupts gradients through reused buffers
(verified negative on the CPU parity harness during development).
layout (
_setup_forward/backward_recv_info) and the 2.12 line in currentNGC containers (
_prepare_forward/backward_infra); anything else logs awarning and leaves stock behavior.
upstream —
stage_backward()harvests input grads and setsval.grad = Noneper chunk.Changelog
nemo_automodel/components/distributed/pipelining/recv_buffer_pool.py(new):ring-pooled recv buffers,
schedule_supports_recv_pool()allowlist(
{"1f1b"}), torch-version probe with fail-open install.nemo_automodel/components/distributed/pipelining/config.py:PipelineConfig.pp_recv_buffer_pool(defaultFalse),pp_recv_buffer_pool_slack(default 2), documented in the class docstring.nemo_automodel/components/distributed/pipelining/autopipeline.py: thread thetwo fields into
AutoPipeline.build()and install the pool before stages build.examples/llm_benchmark/kimi/kimi_k3_gb200_2k.yaml(new): the measured 2k-rowchampion config with the pool enabled; header states the measured shape.
tests/unit_tests/distributed/pipelining/test_recv_buffer_pool.py(new): CPUgloo parity harness, schedule gating, fail-open probe.
Validation
New benchmark config
kimi_k3_gb200_2k.yaml— the pool unlocks 2k rows xGBS 4096 at 64 nodes, the new best-efficiency K3 shape:
Mechanism: the workload is launch-bound at this scale, so doubling tokens
per row halves the per-token launch tax; K3's KDA linear attention does not
penalize longer rows. PP bubble 9.9% (64 microbatches). MFU basis
tok/s/GPU x 6 x 104e9 / 2.25e15. The file ships the exact measuredchampion config (compile_norm off there; the 2k x compile_norm stack is
pending measurement and only changes numbers, not code).
Measurement provenance: the 17.1% figure was measured before #3792, when
from_configbuilt a 56-head MLA / 18432-wide dense FFN model (~101.5B activematmul parameters instead of 104.0B). On the checkpoint-aligned shape this
file builds today expect ~2% fewer tok/s at about the same MFU and ~1 GiB more
per GPU (44 GiB headroom measured). A 64-node re-run on the aligned shape is
queued (
w53prung = this exact file); numbers here and in the header will bereplaced when it lands. With #3792 the recipe prints MFU on the config-derived
basis via
nemo_automodel/recipes/llm/benchmark.py --config ….Correctness evidence:
steps, stock vs pooled — loss trajectory and per-stage parameter sums
bitwise identical. The test catches ring-too-small corruption because
linear weight grads read the saved stage input, which IS the recv buffer.
(4/8 sets), finite-loss trajectory bitwise-identical over 8 steps,
throughput neutral at small scale. Re-verified on the current PR tree
with
BenchmarkingRecipe(pp2×ep4, seq 2048, 1F1B): 12/12 iterations,recv_buffer_pool: installed (slack=2), recipe MFU within 1% of wall-clock.(44 GiB headroom), loss pattern identical to non-pooled baseline regime.
Review pre-answers:
schedule_supports_recv_pool()— allowlist, currently{"1f1b"}.returns False and logs; stock allocation is kept, nothing breaks.
torch's
pipeline_modelflow; class-level rebinding before build is theminimal seam that needs no fork of the stage class. Idempotent + opt-in.
Before your PR is "Ready for review"
Pre checks:
test_recv_buffer_pool.py, listed above)PipelineConfigdocstring for both fields; yaml header documents the shape and provenance. No docs page enumeratesPipelineConfigfields.)Additional Information
34f28f9,22be971,b5900fc) appear in this diff until perf(kimi-k3): RMSNorm compile island and benchmark static-routing sync pins #3779 merges; review the commitsafter
b5900fc. Merge order: perf(kimi-k3): RMSNorm compile island and benchmark static-routing sync pins #3779 first.mainafter fix(kimi-k3): correct MFU accounting (FLOPs formula, config defaults) #3792 (K3 FLOPs formula +KimiK3TextConfigdefaults aligned with the checkpoint).