Skip to content

feat(jit): forward sub-part geometry env vars to the JIT - #1

Open
whn09 wants to merge 5 commits into
amazon-contributing:mainfrom
whn09:jit-forward-subpart-env
Open

feat(jit): forward sub-part geometry env vars to the JIT#1
whn09 wants to merge 5 commits into
amazon-contributing:mainfrom
whn09:jit-forward-subpart-env

Conversation

@whn09

@whn09 whn09 commented Aug 21, 2026

Copy link
Copy Markdown

Re-post of Xuan-1998/DeepEP#42 against this repo, rebased onto main (ec623f3) and re-measured on that tree — the old PR's numbers were taken on the pre-refactor tree, so they are not quoted here.

What

hybrid_dispatch_unordered.cuh gates its sub-part geometry behind #ifndef:

macro default
EP_NUM_SUB_PARTS 2
EP_MIN_SUB_TOKENS 1
EP_SM100_MIN_SUB_TOKENS 15

Nothing in the tree ever sets those macros, so the only way to try a different split today is to edit the header and reinstall. This forwards the three names as JIT -D flags, following the EP_NUM_TOPK_IDX_BITS block immediately above it (and that block's EP_JIT_EXTRA_FLAGS TODO).

Unset ⇒ byte-identical behaviour.

Why it is safe

  • All three are device-only. No host translation unit reads them, so a JIT-only define cannot desync host-side sizing from device-side sizing. (This is the reason the patch stops at these three and does not forward, say, kNumMaxTokensPerRank.)
  • flags is part of kernel_signature, so changing the env re-JITs instead of silently serving a cached cubin compiled with a different geometry.

Verified end-to-end

EP_JIT_PRINT_COMPILER_COMMAND=1 EP_NUM_SUB_PARTS=1 puts -DEP_NUM_SUB_PARTS=1 on the nvcc line, and the resulting cubin lands in a distinct JIT cache entry.

Measured effect of the knob it exposes

tests/elastic/test_ep.py, 2 × p5en.48xlarge (8×H200 + 16 EFA each), EP8×2 = 16 ranks, --hidden=7168 --num-topk=8 --num-experts=256 --num-sms=12 --allow-hybrid-mode=1 --prefer-overlap-with-compute=0 --test-first-only. 3 reps, variants interleaved within each rep, each variant on its own EP_JIT_CACHE_DIR. Mean over all 16 ranks then over reps; ± is stdev across reps. All 48 rounds exited 0.

EP_NUM_SUB_PARTS=1 on its own:

op baseline (unset) EP_NUM_SUB_PARTS=1
prefill (8192 tok) dispatch 1665.1 ± 12.4 µs 1631.9 ± 2.8 µs −2.0%
prefill cached dispatch 1662.8 ± 8.7 µs 1635.7 ± 1.2 µs −1.6%
prefill combine 3560.8 ± 9.2 µs 3545.8 ± 5.0 µs −0.4%
decode (128 tok) dispatch 367.0 ± 12.1 µs 373.7 ± 3.6 µs +1.8%
decode combine 178.1 ± 5.0 µs 178.5 ± 0.3 µs +0.2%

So on its own the knob is roughly a wash — a small prefill win, a small decode regression. Its value is that it composes: stacked with #2 it takes decode dispatch from 367.0 µs to 166.1 ± 0.4 µs (−54.7%), where that PR alone reaches 239.8 µs (−34.7%). That is the case this PR is really enabling — being able to find such a combination without a rebuild.

This PR is pure plumbing and changes no default, so it is worth taking independently of whether #2 is accepted.

Xuan-1998 and others added 5 commits August 21, 2026 00:05
Infrastructure the unordered dispatch/combine kernels build on: a
unified GIN context/signal layout with per-peer barrier signal
indexing (gin_resource_alloc, qp_mapping), the NCCL device-comm setup
reconciled with upstream's runtime-version probe, and the elastic
buffer / Python plumbing that resolves the QP budget at construction.
One GIN context supplies one QP; the context count (default 11,
explicit via num_allocated_qps within [2, 17]) sets the per-context
indexed-signal budget that bounds the per-channel part count and the
ScaleOut rank count. Also the ptx/layout/comm helpers these paths
need.

Co-authored-by: Vladimir Aerov <vaerov@amazon.com>
Signed-off-by: Xuan Jiang <xuanj@amazon.com>
Split each channel's token stream into parts and sub-parts, each sent
as one batched put that carries an in-band header (iteration, token
count, continuation flag) and completes a per-part counting signal.
The receiver treats the signal purely as a completion count and
validates each batch through its header, so correctness holds no
matter what order the puts land in. Front-load a smaller first part to
cut time-to-first-forward, share one signal across a part's sub-parts
to stay inside the per-channel signal budget, and record a
per-(token, k) recv-slot map that combine later returns partials
through.

Co-authored-by: Vladimir Aerov <vaerov@amazon.com>
Signed-off-by: Xuan Jiang <xuanj@amazon.com>
Pack the scale-out return partials contiguously per channel and send
them as batched puts, each fused with a signal add on a shared
per-channel accumulator; the receiver gates on the accumulated count
instead of on arrival order. A dedicated proxy warp takes put issuance
off the data warps' critical path through a shared-memory hand-off
ring. The reduce epilogue locates partials through the per-(token, k)
recv map recorded at dispatch rather than assuming in-place token
slots. Carries upstream's expanded-send weight handling
(kDoExpandedSend) through the slot walk.

Co-authored-by: Vladimir Aerov <vaerov@amazon.com>
Signed-off-by: Xuan Jiang <xuanj@amazon.com>
The hybrid (scale-out) path now carries two kernel pairs. The
unordered pair (default, hybrid_dispatch_unordered.cuh /
hybrid_combine_unordered.cuh) synchronizes through in-band headers
and counting signals, so strong signal is not required, and weak
signal is enough. The ordered pair is the upstream implementation,
kept untouched under its original names (hybrid_dispatch.cuh /
hybrid_combine.cuh), and runs under the upstream communication
configuration (exclusive per-channel contexts, depth-1024 rings,
barrier-only signal budget, VA/strong signals); it publishes a tail
via a trailing signal and requires the backend to support strong
signals and VA signals.

Selection happens at JIT-generation time via EP_HYBRID_KERNEL: the
generated source names the variant's header and kernel, so the JIT
cache keys the two apart automatically. The combine reduce epilogue is
shared between the two paths via a kOrderedLayout template parameter.

Signed-off-by: Xuan Jiang <xuanj@amazon.com>
`hybrid_dispatch_unordered.cuh` gates the sub-part geometry behind `#ifndef`
(`EP_NUM_SUB_PARTS` 2, `EP_MIN_SUB_TOKENS` 1, `EP_SM100_MIN_SUB_TOKENS` 15), but
nothing in the tree sets those macros, so the only way to try a different split
is to edit the header and reinstall. Forward the three names as JIT `-D` flags,
following the `EP_NUM_TOPK_IDX_BITS` block immediately above (and its
`EP_JIT_EXTRA_FLAGS` TODO).

All three are device-only -- no host translation unit reads them -- so a
JIT-only define cannot desync host and device sizing. `flags` is part of
`kernel_signature`, so changing the env re-JITs instead of serving a cached
cubin. Unset => no behaviour change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants