Skip to content

perf: raise the GPU-to-NIC submission rate with channels, QPs, forward pairs - #9

Open
Xuan-1998 wants to merge 2 commits into
amazon-contributing:mainfrom
Xuan-1998:perf/combine-better-submission
Open

perf: raise the GPU-to-NIC submission rate with channels, QPs, forward pairs#9
Xuan-1998 wants to merge 2 commits into
amazon-contributing:mainfrom
Xuan-1998:perf/combine-better-submission

Conversation

@Xuan-1998

Copy link
Copy Markdown

Raise the unordered combine's submission rate three ways: drop the 4-channel/SM cap and let shared memory and the GIN signal budget decide (8/SM at 12 SM) for more parallel submitters, raise the default GIN contexts 11 -> 13 so every SM of the recommended 12-SM launch owns its QP instead of sharing WQE rings, and add cooperative forward warp pairs that split each token's hidden dim across two warps so tokens are reduced faster and therefore submitted faster.

Xuan-1998 and others added 2 commits August 28, 2026 17:26
…stagger

The combine forward warps replayed tokens in the dispatch arrival order
frozen into the handle, which interleaves local-bypass tokens (no RDMA)
with remote tokens in a network-timing-dependent way. A node whose
replay front-loads local tokens back-loads all of its scale-out puts so
with the NIC near saturation the shift never drains and surfaces as the
peer node's exit-wait tail, so one node runs ~62 GB/s SO while the other
runs ~76.

Here we make the schedule deterministic: the scale-up warps sweep the
linked list twice (remote-destined tokens first, local-bypass second)
and the forward warps replay in the same two-pass order, so count-based
tail gating is unchanged and every rank issues its puts on the same
schedule. Pass 0 flushes its TMA record and remainder batches before
pass 1 so the wire keeps draining while locals are reduced.
…s, forward pairs

Raise the unordered combine's submission rate three ways: drop
the 4-channel/SM cap and let shared memory and the GIN signal budget
decide (8/SM at 12 SM) for more parallel submitters, raise the default
GIN contexts 11 -> 13 so every SM of the recommended 12-SM launch owns
its QP instead of sharing WQE rings, and add cooperative forward warp
pairs that split each token's hidden dim across two warps so tokens
are reduced faster and therefore submitted faster.
#pragma unroll
for (int i = 0; i < kNumScaleupRanksPerLane; ++ i)
stored_token_idx[i] = -1;
#pragma unroll 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

General note about this commit:
perf(combine): schedule scale-out puts remote-first to mitigate node stagger

We already know that there is an issue with the EFA latency, and that the odd nodes are slower then the even, and most likely we spend the time in the end of combine which is the wait.

So instead of root cause it and understand why we wait so long, you changed the ordering of the traffic which is to first handle the RDMA traffic and then the NVLINK traffic.

And this masked the issue, as the RDMA now have more time to arrive.

however, you added more compute operation and if the EFA latency was solved then this code impact would be much lower and potentially can even slow down the perf as you added more compute operation (the double sweep).

Having sayed that, i think this approach is intresting, but it should be addressed the following manner:

  1. first solve the actual EFA issue.
  2. apply this patch and see how it impact the result.

@Xuan-1998 Xuan-1998 Aug 30, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I think these are separate issues, no matter how EFA behave, we should always deal with RDMA tokens then local tokens. We still see some variance which may or may not come from EFA. But worth noting that on p6 the max-min is only 1-5GB/s now, I shared the raw data.

static constexpr int kMaxGinContextCnt = kMaxGinContextBudget;

// Default context count (== default QP count). 11 contexts -> 21 signals/context.
// Default context count (== default QP count). 13 contexts -> 17 signals/context.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is the biggest change, it change the sharing mode from _GPU to _CTA on 12SM configuration.
I remember it had some impact on the dispatch kernel and that 11 gin context are the max that does not impact it. was it "solved" because you also forced the "prefer_overlap_with_compute" to be true?

this need to be better highlight in the commit message.

Comment thread csrc/elastic/buffer.hpp
num_smem_bytes,
num_qps, num_gpu_timeout_cycles,
cached_mode, do_cpu_sync,
not prefer_overlap_with_compute,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This control the double buffer you used. If now you are forcing the the overlap with compute then you dont need the double buffer as you have double warp count. This will actually reduce the number of warp due to SMEM limitation.

Comment thread csrc/elastic/buffer.hpp
num_smem_bytes and
"dispatch TMA pool exceeds the shared-memory budget");
if (not prefer_overlap_with_compute)
num_channels_per_sm = std::min<int>(num_channels_per_sm, 4);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You basically deprecated this flag and changed the customer facing API. I am fine with it, but now the perf base line that we compare against is not the same, saying that we reach the same performance as official DeepEP on IB is not correct as they set this flag to false.

We need to make it clear that this flag is no longer behave the same when we publish those results both internally and externally.

constexpr int kVecPerRole = kHiddenVec / kNumFwWarpsPerChannel;
constexpr int kUnrollFactor = get_max_unroll_factor<kVecPerRole, kAdjustRegisters ? 8 : 4>();
const int vec_off = fw_role * kVecPerRole;
combine_reduce<kVecPerRole, kUnrollFactor, math::constexpr_ceil_div(kNumTopk, kNumScaleoutRanks)>(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Interesting idea, as the claim is that we context bounded and not compute bounded and we are not fully utilizing the GPU FLOPS if i got it correctly, and in this specific case we increase it by adding additional 32 threads.

  1. How much does it actually add to the perf as compare to the other changes added here, as each one by itself can boost it.
  2. In addition why did not you applied it also for combine reduced, as it should also boost perf?
  3. why not having the second warp spin here waiting for a "go" signal and then both of them do the reduction, why does it need to go through the entire flow, for me it make more sense if you will have some kind of helper warps that are all waiting here and all are getting a job and you sync them, e.g a pool of 4 extra warp in your case.

@whn09

whn09 commented Aug 31, 2026

Copy link
Copy Markdown

We ran this branch against main on 4x p5en.48xlarge (H200, sm_90) and can add the
missing before/after numbers, since the PR body doesn't have any.

Setup. EFA installer 1.50.0, NCCL_GIN_TYPE=5 + NCCL_SYM_GIN_KERNELS_ENABLE=0.
main at 54fffef vs this PR's head 3c737dc (so #8 + #9 together), same image
recipe, same nodes, arms interleaved. tests/elastic/test_ep.py --num-sms=12 --allow-hybrid-mode=1 --prefer-overlap-with-compute=0 --test-first-only -- so every
number is FP8 dispatch at expert_alignment=128. 3 rotated reps per cell, all
ranks pooled from every node's log, not the leader's (combine is layered by node
here, up to 11%, so a leader-only table is wrong). --ignore-local-traffic is off, so
time is the metric and the SO GB/s column is not a wire rate. EP_BUFFER_DEBUG off,
because it printf()s inside dispatch's timed loop.

Prefill, 8192 tok/rank (us, all-rank mean)

op 2N main 2N #9 Δ 4N main 4N #9 Δ
dispatch 1499.8 1498.4 −0.1% 3965.2 3961.2 −0.1%
cached dispatch 1588.8 1496.2 −5.8% 4254.0 3946.2 −7.2%
combine 3587.8 3172.5 −11.6% 7872.1 7761.8 −1.4%
reduced combine 4238.0 3670.5 −13.4% 7947.3 7773.2 −2.2%
dispatch + reduced combine 5737.8 5168.8 −9.9% 11912.5 11734.5 −1.5%

Decode, 128 tok/rank (us, all-rank mean)

op 2N main 2N #9 Δ 4N main 4N #9 Δ
dispatch 169.6 123.5 −27.2% 184.0 171.0 −7.1%
cached dispatch 166.2 120.2 −27.7% 178.9 167.6 −6.4%
combine 162.5 143.6 −11.6% 244.8 234.6 −4.2%
reduced combine 179.2 149.5 −16.6% 253.6 237.2 −6.5%
dispatch + reduced combine 348.8 273.0 −21.7% 437.6 408.2 −6.7%

Four observations:

1. The node-variance claim from #8 reproduces at 2 nodes and does not at 4.
Using the same statistic quoted in #8's description -- combine SO GB/s min-max across
ranks, 2N/8192:

main this PR
combine 60-73 GB/s 70-77 GB/s
reduced combine 53-60 61-66

Per-node mean reduced combine tells the same story: main spreads 9.1 / 8.8 / 8.9%
across the three reps and which node is slow flips between reps (rep1 n1 4434 vs n2
4066 us, rep2 4053 vs 4410); this PR gives 3.6 / 3.8 / 1.8%.

At 4 nodes it does not hold. 4N/128 per-node mean reduced combine max-min is
9.2-10.6% on main and 10.8-11.5% on this PR, and it is monotone and stable in node
index (rep2: 225 / 229 / 245 / 251 us) rather than flipping between reps. So there is a
second, systematic stagger at >2 nodes that remote-first scheduling does not address --
worth knowing before this is read as a general fix for node variance. Dispatch layering
is <=2.6% on every arm at both scales, so this is combine-specific.

2. cached dispatch on main is slower than plain dispatch (1588.8 vs 1499.8 us
at 2N, 4254.0 vs 3965.2 at 4N -- and SO drops 81-82 to 77-77 GB/s). This PR doesn't
speed that path up so much as remove the penalty: its cached dispatch (1496.2 / 3946.2)
lands at parity with its own plain dispatch. At 4 nodes this is the only prefill row
with a delta clear of run noise.

3. #QPs reads 13/13 in every log on this branch vs 11/11 on main, so the
kDefaultGinContextCnt bump does take effect on EFA.

4. Most of the win is at 2 nodes. 2N decode dispatch −27.2% becomes −7.1% at 4N;
2N prefill reduced-combine −13.4% becomes −2.2%. Directionally still a win everywhere
we measured, but the headline number should probably carry its node count.

Two things this does not answer, to be explicit:

  • @vladimiraerov's attribution question. This measures the sum of all four
    changes. We did confirm from the diff that both the channel-clamp removal and the
    forward-warp pairing are gated on not prefer_overlap_with_compute, which is what we
    pass, so both are live on the measured path -- but we can't say how the −21.7% splits
    between them. An arm at --prefer-overlap-with-compute=1 would bracket
    clamp+pairing without a rebuild; happy to run it if useful.
  • The 4 -> 8 channels/SM figure in the PR body is not something we observed; #SM
    prints the same on both arms and we ran without EP_BUFFER_DEBUG.

And one caveat on reading the tables: since this PR deletes the
if (not prefer_overlap_with_compute) num_channels_per_sm = min(..., 4) clamp,
--prefer-overlap-with-compute=0 no longer selects the channel count it used to. The
comparison above is main-vs-branch on identical hardware with an identical command
line, so it is internally valid, but it is not comparable against any baseline that
was getting 4 channels/SM from that flag.

Separately, for merge ordering: PR #1's compiler.hpp change (forwarding
EP_NUM_SUB_PARTS into the JIT flags) is not in this branch, and without it the env
var is inert -- the .cuh reads the macro but nothing defines it. On the #1+#2 branch
with EP_NUM_SUB_PARTS=1 we measure 4N decode dispatch at 156.4 us (−15.0% vs main,
about twice this PR's −7.1%) and a 4N layer total of 409.7 us, i.e. a tie with this
PR's 408.2. Landing #9 without #1 removes that lever.

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.

3 participants