Skip to content

perf(distributed): ring-pooled PP recv buffers and Kimi-K3 2k benchmark shape - #3780

Open
yisongbetter wants to merge 5 commits into
NVIDIA-NeMo:mainfrom
yisongbetter:yisongbetter/perf/k3-pp-recv-pool
Open

perf(distributed): ring-pooled PP recv buffers and Kimi-K3 2k benchmark shape#3780
yisongbetter wants to merge 5 commits into
NVIDIA-NeMo:mainfrom
yisongbetter:yisongbetter/perf/k3-pp-recv-pool

Conversation

@yisongbetter

@yisongbetter yisongbetter commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

What does this PR do ?

Opt-in ring pooling of torch.distributed.pipelining P2P recv buffers under
1F1B (distributed.pipeline.pp_recv_buffer_pool), which removes the
per-microbatch buffer pre-allocation that OOMs Kimi-K3 at 2k rows × GBS 4096
on 64 GB200 nodes, plus the new kimi_k3_gb200_2k.yaml benchmark shape it
unlocks.

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 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_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 (AC recompute reads it); a grad recv buffer
only until the chunk's backward returns. New module
components/distributed/pipelining/recv_buffer_pool.py 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).

  • Opt-in: distributed.pipeline.pp_recv_buffer_pool: true
    (+ pp_recv_buffer_pool_slack, default 2), threaded
    PipelineConfig -> AutoPipeline.build().
  • Schedule-gated: only 1F1B (bounded in-flight depth proof). Other
    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).
  • torch-version adaptive, fail-open: adapts both the torch >= 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 leaves stock behavior.
  • Gradient safety: grad accumulation on reused leaf buffers is impossible
    upstream — stage_backward() harvests input grads and sets
    val.grad = None per 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 (default False),
    pp_recv_buffer_pool_slack (default 2), documented in the class docstring.
  • nemo_automodel/components/distributed/pipelining/autopipeline.py: thread the
    two fields into AutoPipeline.build() and install the pool before stages build.
  • examples/llm_benchmark/kimi/kimi_k3_gb200_2k.yaml (new): the measured 2k-row
    champion config with the pool enabled; header states the measured shape.
  • tests/unit_tests/distributed/pipelining/test_recv_buffer_pool.py (new): CPU
    gloo parity harness, schedule gating, fail-open probe.
  • The perf(kimi-k3): RMSNorm compile island and benchmark static-routing sync pins #3779 commits appear in this diff until perf(kimi-k3): RMSNorm compile island and benchmark static-routing sync pins #3779 merges (stacked PR).

Validation

New benchmark config kimi_k3_gb200_2k.yaml — the pool unlocks 2k rows x
GBS 4096 at 64 nodes, the new best-efficiency K3 shape:

shape tok/s/GPU MFU peak mem
1k x GBS4096 (kimi_k3_gb200.yaml + #3779 flags) 495.7 13.7% ~135 GiB
2k x GBS4096 + recv pool (this file) 615.9 17.1% 139.6 GiB

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 measured
champion 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_config built a 56-head MLA / 18432-wide dense FFN model (~101.5B active
matmul 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 (w53p rung = this exact file); numbers here and in the header will be
replaced when it lands. With #3792 the recipe prints MFU on the config-derived
basis via nemo_automodel/recipes/llm/benchmark.py --config ….

Correctness evidence:

  • CPU gloo parity (in-PR unit test): pp4 x 1F1B x 16 microbatches x 3
    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.
  • pp2 ring-activation gate at cluster (dev evidence): ring active
    (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.
  • 256-GPU production evidence: w53 rescue run rc=0, mem 139.6 GiB
    (44 GiB headroom), loss pattern identical to non-pooled baseline regime.

Review pre-answers:

  1. Interleaved PP / other schedules? Gated off by
    schedule_supports_recv_pool() — allowlist, currently {"1f1b"}.
  2. torch private API drift? Version-adaptive probe with fail-open: install
    returns False and logs; stock allocation is kept, nothing breaks.
  3. Why monkeypatch instead of subclass? Stages are constructed inside
    torch's pipeline_model flow; class-level rebinding before build is the
    minimal seam that needs no fork of the stage class. Idempotent + opt-in.

Before your PR is "Ready for review"

Pre checks:

  • Make sure you read and followed Contributor guidelines
  • Did you write any new necessary tests? (test_recv_buffer_pool.py, listed above)
  • Did you add or update any necessary documentation? (PipelineConfig docstring for both fields; yaml header documents the shape and provenance. No docs page enumerates PipelineConfig fields.)

Additional Information

@yisongbetter
yisongbetter requested a review from a team as a code owner September 1, 2026 13:53
@copy-pr-bot

copy-pr-bot Bot commented Sep 1, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

…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
yisongbetter force-pushed the yisongbetter/perf/k3-pp-recv-pool branch from 224b48e to 8a58634 Compare September 3, 2026 17:34
@yisongbetter

Copy link
Copy Markdown
Contributor Author

/ok to test 8a58634

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants