Skip to content

EP: CUDA-graph-safe HT internode dispatch+combine (worst-tokens mode) - #7

Closed
fergusfinn wants to merge 3 commits into
cxi-epfrom
ht-cudagraph-worst-tokens
Closed

EP: CUDA-graph-safe HT internode dispatch+combine (worst-tokens mode)#7
fergusfinn wants to merge 3 commits into
cxi-epfrom
ht-cudagraph-worst-tokens

Conversation

@fergusfinn

Copy link
Copy Markdown
Owner

Makes the high-throughput internode path fully CUDA-graph capturable and replay-safe when num_worst_tokens is set, validated end-to-end on Isambard (GH200/CXI, 2 nodes x 4 ranks). All changes are inert when num_worst_tokens == 0.

Commits

  1. Replay safety: notify_dispatch spin-waits for the host to reset host-mapped counters before writing sums; host code doesn't re-execute under replay, so the second replay deadlocked. The wait is now skipped in worst-tokens mode. Also fixes the combine NVL sender's last-slot bound, which fell back to the (padded) input row count and sent garbage tail tokens into flow-control timeouts — it now reads the real total from the handle's recv_gbl_rank_prefix_sum on device.
  2. Device-side per-expert counts: notify_dispatch optionally writes per-expert recv counts to a device int32[num_local_experts] tensor, and dispatch() returns it in the counts slot under worst-tokens mode, so graph-mode consumers (masked grouped GEMM) never touch the host.
  3. Test: ep/bench/test_ht_cudagraph.py — captures layout+dispatch+expert-standin+combine into one graph, 50 replays with fresh data and routing, validated against the normal host-synced path, an analytic oracle, and the device counts; plus eager/replay interleaving on one Buffer.

Validation: all phases pass on 2x4 GH200 over Slingshot/CXI (jobs 5132248 and 5132662; eager 0.76 vs replay 0.72 ms/step at test size). Known cosmetic issue: the test prints cudaErrorContextIsDestroyed at interpreter exit after buffer.destroy() — teardown ordering, after all checks pass.

Two fixes, both inert when num_worst_tokens == 0:

1. notify_dispatch spin-waits for the host to reset the host-mapped
   recv counters to -1 before writing its sums. Host code does not
   re-execute under CUDA graph replay, so the second replay deadlocks
   on the stale counter. Skip the wait when num_worst_tokens > 0; the
   writes stay unconditional.

2. The combine NVL sender bounds its last (rank, channel) slot by
   num_tokens, which under num_worst_tokens is the padded input row
   count, so it sends garbage tail tokens and hits flow-control
   timeouts. Plumb the dispatch handle's recv_gbl_rank_prefix_sum
   (device tensor) into the kernel and use its last element as the
   real total.
…mode

CUDA-graph consumers of num_worst_tokens dispatch cannot read the
host-mapped per-expert counters without a sync, and the host list is
deliberately skipped in that mode. Have notify_dispatch also write the
counts to an optional device int32[num_local_experts] buffer, and have
dispatch() return that tensor in the num_recv_tokens_per_expert_list
slot when num_worst_tokens > 0 (downstream: masked grouped GEMM reads
it directly inside the graph).
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

2-node test: captures layout+dispatch+expert-standin+combine into one
torch.cuda.CUDAGraph with num_worst_tokens, replays 50x with fresh data
and routing per replay, validates against the normal host-synced path,
an analytic oracle, and the device expert-counts tensor; also checks
eager/replay interleaving on one Buffer. Validated on Isambard
(GH200/CXI, 2x4 ranks): all phases pass.
@fergusfinn

Copy link
Copy Markdown
Owner Author

Moved to the org fork: superseded by doublewordai#3 (same branch, same description plus a note that it is CXI-agnostic).

@fergusfinn fergusfinn closed this Jun 10, 2026
fergusfinn added a commit to doublewordai/vllm that referenced this pull request Jun 10, 2026
With the UCCL ht-cudagraph-worst-tokens kernels (fergusfinn/uccl#7),
DeepEP high-throughput dispatch+combine are stream-capturable: no host
count sync, worst-case static recv shapes, per-expert counts in a
device tensor. Behind VLLM_DEEPEP_HT_WORST_TOKEN_DISPATCH (default off):

- allow FULL_DECODE_ONLY for deepep_high_throughput instead of forcing
  cudagraph_mode NONE
- size num_worst_tokens per captured shape (padded step tokens x
  num_dispatchers) from the forward context; eager steps (prefill,
  mixed, uncaptured) keep the host-synced path with zero padding cost
- build ExpertTokensMetadata directly from the device counts tensor
  (expert_num_tokens_cpu=None); fused_marlin_moe never reads counts
- join dispatch AND combine back to the compute stream (async off)
  whenever worst-token mode is active: async comm-stream work in
  flight races subsequently replayed graphs on the UCCL ring buffers

Validated 2-node EP8 DSv4-Flash (capture sizes 64..2048):
conc 1024 ISL/OSL 1024/128 -> 1642 tok/s (+45% vs the thread-DBO
baseline); conc 8192 1024/1024 -> 9243 tok/s (+13%), 8192/8192
requests, 0 failures. Known: shapes <64 tokens crash capture (suspected
lazy NCCL/EPLB init mid-capture; floor the capture list via compilation
config), and the blanket sync fence taxes prefill (event fence is
follow-up).
fergusfinn added a commit to doublewordai/vllm that referenced this pull request Jun 11, 2026
With the UCCL ht-cudagraph-worst-tokens kernels (fergusfinn/uccl#7),
DeepEP high-throughput dispatch+combine are stream-capturable: no host
count sync, worst-case static recv shapes, per-expert counts in a
device tensor. Behind VLLM_DEEPEP_HT_WORST_TOKEN_DISPATCH (default off):

- allow FULL_DECODE_ONLY for deepep_high_throughput instead of forcing
  cudagraph_mode NONE
- size num_worst_tokens per captured shape (padded step tokens x
  num_dispatchers) from the forward context; eager steps (prefill,
  mixed, uncaptured) keep the host-synced path with zero padding cost
- build ExpertTokensMetadata directly from the device counts tensor
  (expert_num_tokens_cpu=None); fused_marlin_moe never reads counts
- join dispatch AND combine back to the compute stream (async off)
  whenever worst-token mode is active: async comm-stream work in
  flight races subsequently replayed graphs on the UCCL ring buffers

Validated 2-node EP8 DSv4-Flash (capture sizes 64..2048):
conc 1024 ISL/OSL 1024/128 -> 1642 tok/s (+45% vs the thread-DBO
baseline); conc 8192 1024/1024 -> 9243 tok/s (+13%), 8192/8192
requests, 0 failures. Known: shapes <64 tokens crash capture (suspected
lazy NCCL/EPLB init mid-capture; floor the capture list via compilation
config), and the blanket sync fence taxes prefill (event fence is
follow-up).
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.

1 participant