Skip to content

[Bugfix][MoE] Keep DeepEP high-throughput buffers alive across DBO streams - #60735

Merged
AndreasKaratzas merged 4 commits into
vllm-project:mainfrom
aniravich:fix/deepep-ht-dbo-record-stream
Oct 18, 2026
Merged

AndreasKaratzas merged 4 commits into
vllm-project:mainfrom
aniravich:fix/deepep-ht-dbo-record-stream

Conversation

@aniravich

@aniravich aniravich commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Overview

Under DBO, DeepEPHTPrepareAndFinalize has DeepEP allocate its dispatch outputs on the DBO comm stream, then reads them on the compute stream; the combine input crosses the other way. The caching allocator is never told about these cross-stream reads, so the other microbatch's next DeepEP call can reuse a block and overwrite it before it is read. With the DeepGEMM FP8 MoE this crashes the engine at startup with an illegal memory access in ep_scatter. This PR calls record_stream on every tensor that crosses between the two streams.

A GitHub search found no existing issue or PR for this. Related: #46990 added a ROCm-only host sync after HT dispatch/combine under DBO, possibly for the same hazard (see Details). #27666 and #38451 fixed other DBO + DeepEP HT problems.

Claims

  • Fixes the startup crash with DBO + DeepEP high-throughput + the DeepGEMM FP8 MoE (VLLM_USE_DEEP_GEMM=1 VLLM_MOE_USE_DEEP_GEMM=1, the defaults). For GLM-5.3 FP8 on main, 2 of 2 runs crashed in the first microbatched step without the fix. On 0.29.0, so did all 7 runs that had neither the fix nor a serializing workaround (CUDA_LAUNCH_BLOCKING, no caching allocator, host sync). With the fix, full GSM8K scores 0.927 with CUTLASS linears and 0.912 with DeepGEMM linears (DBO off: 0.923).
  • Covers both directions for every experts backend behind DeepEP HT: DeepEP's outputs (comm stream to compute stream) and the reduced expert output passed to combine (compute stream to comm stream; defensive on main, see Details). With --enable-dbo, main runs Model Runner V1 unless VLLM_USE_V2_MODEL_RUNNER=1 is set. MRV2's opt-in DBO uses the same microbatch contexts and streams, so it gets the same fix. Only Model Runner V1 was run.
  • No change when DBO is off. With DBO on, there are no host syncs and no extra kernels. The cost is at most 9 record_stream calls per MoE layer per microbatch, plus one CUDA event per recorded block when it is freed; the allocator reuses that block only after the event completes. The throughput cost was not measured.
  • DeepEP low-latency, NIXL-EP and DeepEP v2 are not affected, because on main they do not switch streams under DBO. deepep_ll.py is unchanged. The open [DBO] Add DBO support to the DeepEP V2 backend #52177 adds the same comm-stream switching to DeepEP v2, so it may need the same treatment.

Validation

On main, without and with this PR. Setup: GLM-5.3 FP8 on 1 x 8 H200, DP4 x TP2, EP8, --all2all-backend deepep_high_throughput --enable-dbo, cudagraph_mode NONE, fp8 KV, prefix caching, Model Runner V1. Env: VLLM_USE_DEEP_GEMM=1 VLLM_MOE_USE_DEEP_GEMM=1 VLLM_USE_DEEP_GEMM_E8M0=0 VLLM_DEEP_GEMM_WARMUP=skip; the logs show Using DEEPGEMM Fp8 MoE backend. Build: main @ 4a705c7 + #60732 + #60734. Running GLM-5.x with DBO needs both of those; this PR is independent of them. Eval: lm_eval GSM8K 5-shot, 1,319 questions, num_concurrent=256, standard error ±0.007–0.008. Microbatched steps were counted with a diagnostic log that is not part of this PR.

Linears DBO This PR GSM8K flexible / strict Microbatched steps per worker
CUTLASS (--linear-backend cutlass) on no crash in the profile run, 2 / 2 runs dies in the first one (2 x 4,096 tokens)
CUTLASS on yes 0.927 / 0.927 100–149
DeepGEMM on yes 0.912 / 0.913 100–149
DeepGEMM off yes (no-op) 0.923 / 0.923 0

DeepGEMM-linears DBO on and DBO off differ by 0.010, about one standard error of the difference. We did not run CUTLASS linears with DBO off, or DeepGEMM linears with DBO on without the fix, on main. On 0.29.0 the latter crashes the same way (see below).

The crash without the fix:

Run What fails
no fix The server never becomes ready. In the first microbatched step of the profile run, one worker raises RuntimeError: DeepEP error: CPU recv timeout in buffer.dispatch. The other microbatch thread gets CUDA error: an illegal memory access was encountered at comm_stream.wait_event, and the run ends with torch.cat(): expected a non-empty list of Tensors.
no fix, CUDA core dump CUDA Exception: Warp Out-of-range Address in _fwd_kernel_ep_scatter_2<<<(8192,1,1),(256,1,1)>>> at deep_gemm_utils.py:109, which is tl.load(expert_map + expert_id) in apply_expert_map, inlined from :241 (expert_id = tl.load(recv_topk + ...)). The faulting SASS loads expert_map + expert_id * 4 after the expert_id != -1 check, so the expert id read from the torch.where over DeepEP's topk ids was out of range.

Isolating the cause on vLLM 0.29.0. Same 1-node GLM-5.3 setup, with backports of #60732 and #60734, DeepGEMM MoE and CUTLASS linears unless noted. The record_stream rows use an env-gated version of this patch: same three call sites, using torch.cuda.current_stream().

Variant Questions GSM8K flexible / strict Microbatched steps per worker
no fix: stock 0.29.0; with backports; with backports + the ep_gather int64 fix from fe284ea (#53914); DeepGEMM or CUTLASS linears; with a core dump crash in the profile run, 5 / 5 runs. Core dump: same kernel and source line as on main (deep_gemm_utils.py:110 on 0.29)
no fix, VLLM_DBO_COMM_SMS=0 crash
no fix, VLLM_RANDOMIZE_DP_DUMMY_INPUTS=1 crash
no fix, CUDA_LAUNCH_BLOCKING=1 200 0.920 / 0.920 69
no fix, PYTORCH_NO_CUDA_MEMORY_CACHING=1 200 0.945 / 0.945 71
no fix, host sync after HT dispatch/combine (the ROCm path forced on) 300 0.943 / 0.943 90
record_stream 1,319 0.926 / 0.926 118
record_stream, DeepGEMM linears 1,319 0.923 / 0.923 132
reference: no fix, FLASHINFER_CUTLASS MoE, DeepGEMM linears 1,319 0.926 / 0.927 124
reference: DBO off, DeepGEMM MoE and linears 1,319 0.919 / 0.919 0

Taken together, these runs narrow the cause:

  • Disabling the SM partition and randomizing the dummy inputs do not help.
  • Serializing launches, disabling the caching allocator, a host sync, or record_stream alone each remove the crash.
  • The FLASHINFER_CUTLASS MoE is fine with the same DeepGEMM linears.

That points at allocator reuse of DeepEP's buffers across DBO streams, which only the DeepGEMM MoE turns into a crash.

Production shape (vLLM 0.29.0). Setup: 4 x 8 H200, one engine per GPU (TP1, DP32/EP32, external load balancing), GLM-5.3-BF16 quantized online with fp8_per_block, DeepEP HT, eager, prefix caching, fp8 KV, --max-num-batched-tokens 4096, --gpu-memory-utilization 0.70. Each row is one run with the same backports as above. Prefill load: three waves of 1,536 unique prompts of about 9.7k tokens each, max_tokens=1, 6 in flight per rank. GSM8K (1,319 questions) was then run against rank 0.

MoE / linears DBO Fix GSM8K flexible / strict Microbatched steps per worker KV cache per GPU (tokens) Prefill tok/s, wave 3
FLASHINFER_CUTLASS / CUTLASS (FlashInfer autotune off*) on no 0.924 / 0.923 278 645k 79.4k
DeepGEMM / DeepGEMM on record_stream 0.917 / 0.917 280 736k min, 796k median 75.0k
DeepGEMM / DeepGEMM off n/a 0.925 / 0.925 0 733k min, 803k median 67.5k

* Two earlier attempts with FlashInfer autotune on stalled during startup autotuning. Throughput is the third wave over all 32 GPUs. DeepGEMM kernels JIT-compile during the first waves (VLLM_DEEP_GEMM_WARMUP=skip), and all three runs were still speeding up in wave 3, so these are not steady-state numbers. This table shows that DBO + DeepGEMM MoE works at production scale with the fix. It is not a performance claim for this PR. Without the fix this configuration does not start, so measuring the fix's own cost needs a MoE backend that runs either way, such as FLASHINFER_CUTLASS. We did not do that.

CI coverage. This PR adds no test. The existing DBO test, tests/v1/distributed/test_dbo.py (DeepSeek-V2-Lite-Chat, DP2), already runs the changed lines in its deepep_high_throughput case, with a BF16 MoE (TritonExperts) rather than the DeepGEMM path. It runs in the 2 x H100 "Distributed Features" job (optional), the B200 "Distributed" job (also optional; xfailed on Blackwell for unstable DBO accuracy; we have not checked whether that is related) and the AMD mirrors. With this patch, on 2 x H200 it passes both cases (deepep_low_latency, deepep_high_throughput) in 9 minutes, with 200+ microbatched steps per worker in the HT case. To build a 2-GPU reproducer, we quantized that model's MoE online to block FP8 with --quantization online --quantization-config '{"moe": "fp8_per_block"}' --moe-backend deep_gemm. The logs confirm the DEEPGEMM MoE behind DeepEPHTPrepareAndFinalize. That variant passed without the fix every time, so it cannot catch a regression:

DeepSeek-V2-Lite-Chat (H200, threshold 0.62) This PR GSM8K strict Microbatched steps per worker
DP2, 256 questions no 0.648 200+
DP2, --max-num-batched-tokens 16384 --max-num-seqs 256, 512 questions no 0.666 200+
DP8 / EP8, 256 questions no 0.652 100+
DP2, 256 questions yes 0.645 200+

It would add DeepGEMM-MoE coverage of the patched path for 5–7 minutes on 2 GPUs. We can add it if reviewers want it, but it would not have caught this bug. The regression evidence is the manual GLM-5.3 runs above. Separately, quantizing V2-Lite's linears as well (--quantization fp8_per_block) scored about 0 on GSM8K in this DBO setup, both with and without this PR. That is unrelated and was not investigated.

Lint: pre-commit passes on the rebased commit: ruff check and format (0.14.0), typos (1.43.5), mypy (1.20.2), and the local SPDX, forbidden-import, torch.cuda and boolean-context-manager hooks. Before the rebase, with torch types resolved, mypy reported 2 errors, which also appear on the unpatched file.

Not run:

Reproduction commands
# GLM-5.3 with DBO also needs #60732 and #60734.
VLLM_USE_DEEP_GEMM=1 VLLM_MOE_USE_DEEP_GEMM=1 VLLM_USE_DEEP_GEMM_E8M0=0 VLLM_DEEP_GEMM_WARMUP=skip \
vllm serve zai-org/GLM-5.3 --tensor-parallel-size 2 --data-parallel-size 4 --enable-expert-parallel \
  --all2all-backend deepep_high_throughput --enable-dbo --compilation-config '{"cudagraph_mode": "NONE"}' \
  --kv-cache-dtype fp8 --max-model-len 8192 --gpu-memory-utilization 0.90 --enable-prefix-caching \
  --linear-backend cutlass   # omit for DeepGEMM linears
lm_eval --model local-completions \
  --model_args model=zai-org/GLM-5.3,base_url=http://127.0.0.1:8000/v1/completions,num_concurrent=256,tokenized_requests=False \
  --tasks gsm8k --num_fewshot 5

# Without this PR the server fails during startup (profile run). To get the core dump, add:
CUDA_ENABLE_COREDUMP_ON_EXCEPTION=1 \
CUDA_COREDUMP_GENERATION_FLAGS=skip_nonrelocated_elf_images,skip_global_memory,skip_shared_memory,skip_local_memory \
CUDA_COREDUMP_FILE=core_%h_%p.nvcudmp
cuda-gdb -ex 'target cudacore core_<host>_<pid>.nvcudmp'

Details

Root cause (main @ 240785b, before this PR):

  • Shared streams. UBatchWrapper creates one comm stream (gpu_ubatch_wrapper.py:76) and gives both microbatches that comm stream and the same compute stream (:288-291). MRV2's UBatchRunner does the same (gpu/ubatch_utils.py:327,458-462).
  • Where DeepEP allocates. _do_dispatch switches to the comm stream (deepep_ht.py:126). It then calls get_dispatch_layout and dispatch with allocate_on_comm_stream=False (:139,168) and, under DBO, async_finish=False (:138,167). DeepEP therefore allocates recv x, scales, topk ids and topk weights on the current stream, which is the DBO comm stream's allocator pool. It runs its kernels on its own internal stream, which waits only on previous_event. With async_finish=False it then makes the calling stream wait for that internal stream, and it records no other stream on its tensors: its HT dispatch and combine call record_stream only when async_finish=True (csrc/deep_ep.cpp).
  • Where they are read and freed. dbo_switch_to_compute_sync() (:177) orders the compute stream after the dispatch, and _receiver reads the outputs there: first the torch.where over the topk ids (:225), then the experts. Once those kernels are enqueued the Python references are dropped; for example, expert_topk_ids is rebound by the torch.where. The blocks go back to the comm pool, free for any later comm-stream work.
  • How they get overwritten. The other microbatch's next DeepEP call allocates from the same comm-stream pool. Its kernels wait only on its own previous_event, captured on the compute stream before it yielded ([Perf] Fix DBO overlap: capture DeepEP event before yield #38451). The DBO comm stream itself waits only on that microbatch's compute-done event (ubatching.py:86-87, recorded in yield_and_switch_from_compute_to_comm before the yield). Both events were recorded before this microbatch enqueued its reads. So that call's outputs can land in the freed blocks and be written before the reads run. The DeepGEMM contiguous path then loads expert_map[expert_id] for whatever id it finds (deep_gemm_utils.py:107-110, called from _fwd_kernel_ep_scatter_2 at :244-247), which is the faulting load in the core dump.
  • Reverse direction. For experts that leave the weight-and-reduce to the prepare/finalize (TopKWeightAndReduceDelegate), _finalize reduces into a new compute-stream tensor (apply(output=None), :365), and combine reads it on the comm stream (:376). dbo_switch_to_compute() (:389) does not order the compute stream after the combine. The tensor is freed when _finalize returns, so later compute-stream work, such as shared experts or the other microbatch, can reuse the block while combine is still reading it. On main, though, no experts class that can run behind DeepEP HT delegates: the delegating classes all use the batched activation format, DeepEP HT uses the standard one, and the standard CUTLASS FP8 and Marlin experts return TopKWeightAndReduceNoOP. Every experts class that can run behind DeepEP HT reduces itself (DeepGEMM, Triton, FlashInfer CUTLASS, ...), so the combine input is the per-microbatch workspace (or the caller's output tensor when the shapes match), which is not freed there. The _finalize call (1 of the 9) is therefore defensive on main, for a future delegating experts class. We did not observe this direction.
  • Dispatch inputs. Tokens, scales, topk ids and topk weights are already safe today, because they are freed only after dbo_switch_to_compute_sync() makes the compute stream wait on the comm stream. They are recorded anyway (4 of the 9 calls), so their safety does not depend on that ordering; this is also the version validated on 0.29.

Fix: _dbo_record_stream() calls t.record_stream(current_stream()) on each non-None tensor when dbo_enabled(). It is called in three places:

  • in _do_dispatch, after switching to comm, on the dispatch inputs (compute to comm);
  • in _receiver, before the torch.where, on DeepEP's outputs (comm to compute);
  • in _finalize, after switching to comm, on the reduced expert output (compute to comm).

When a recorded tensor is freed, the allocator records an event on the consuming stream and does not reuse the block until that event has completed. For the two compute-to-comm sites the actual reader is DeepEP's internal stream. Recording the DBO comm stream still covers it, because with async_finish=False the DBO comm stream waits for DeepEP's kernels before dispatch or combine returns, and the tensors are freed after that. DeepEP's handle and layout tensors and combined_x are used only on the DBO comm stream and DeepEP's internal stream, so they are not recorded (but see "Not covered").

CUDA graphs: DeepEP HT is never captured. vLLM forces cudagraph_mode NONE for HT with DP > 1, and DBO needs DP > 1. PyTorch also supports record_stream during capture.

Why FLASHINFER_CUTLASS does not crash, but may still race. The hazard is in the prepare/finalize, so it does not depend on the experts backend; what differs is what a corrupted id does. FlashInfer's CUTLASS MoE receives ep_rank/ep_size and keeps only ids in this rank's range (expert >= start_expert && expert < end_expert, cutlass_fused_moe_kernels.cuh:223,449 in FlashInfer 0.7.0.post1). A garbage id is therefore dropped instead of dereferenced, and garbage activations or weights would only produce wrong numbers. With FLASHINFER_CUTLASS, DBO matches DBO off on GSM8K: 0.922–0.927 on main in #60734, and 0.924 at EP32 above. So we have no evidence that the race fires there in practice. We also do not know why the window opens reliably for GLM-5.3 with DeepGEMM but not for DeepSeek-V2-Lite with DeepGEMM. #60734 notes an illegal memory access with DeepSeek-V3.2 and DBO that also uses FLASHINFER_CUTLASS and also disappears with CUDA_LAUNCH_BLOCKING=1. We have not tested whether this PR fixes it.

Why ROCm already host-syncs. #46990 added _sync_dbo_comm_if_needed (deepep_ht.py:61,71-76), a host synchronize() of the current stream after HT dispatch and combine under DBO, on ROCm only. Its comment cites reuse of "Buffer-owned communication workspace". Forcing the same sync on CUDA also removed the crash (0.29 table). We suspect ROCm hit the same allocator hazard, but we have no ROCm hardware to check, so this PR leaves the ROCm sync in place; on ROCm, record_stream only adds bookkeeping.

Alternatives considered
  1. Use the host sync on CUDA as well. It works (0.29 table), but it blocks the CPU after every HT dispatch and combine of every microbatch: 300 times per step for GLM-5.3's 75 MoE layers. That gives up the CPU run-ahead DBO needs for overlap. Not benchmarked.
  2. allocate_on_comm_stream=True. This moves the outputs to DeepEP's internal stream, but the compute stream still reads them, so on its own it does not order reuse. DeepEP also accepts it only together with previous_event and async_finish=True, and vLLM disables async_finish under DBO. Not tried.
  3. record_stream (this PR). It marks exactly the tensors that cross streams, adds no syncs or kernels, and keeps the fix inside the one prepare/finalize that switches streams under DBO.

Not covered: blocks freed on the comm stream itself, for example combined_x after output.copy_ in the finalize receiver, are reused by later comm-stream allocations. DeepEP's kernels for that later call wait on previous_event, a compute-stream event, rather than on the comm stream, so in principle they could run before the earlier comm-stream copy finishes. We inferred this from DeepEP's source (its internal stream waits on previous_event when one is given, otherwise on the calling stream) and have not observed it. record_stream cannot express this ordering. Passing previous_event=None under DBO would make DeepEP wait on the comm stream instead. That is left for a follow-up because it changes the ordering #38451 tuned.

Related:

The first three came out of the same GLM-5.3 bring-up and are independent of this PR. The ep_gather int64 fix in fe284ea (#53914) is already on main; the 0.29 no-fix runs crash with and without a backport of it.

AI assistance: Claude Code was used to trace the root cause, write the patch, run the validations above, and draft this description. I reviewed every changed line, can explain and defend the change, and checked the test results above.


Pull Request Checklist
  • I used vLLM's /pr-checklist skill. (Mandatory for agents, optional for humans).

  • AI assistance was used during the creation of this PR.

  • Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.

  • Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.

  • Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.

  • Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.

🤖 Generated with Claude Code

…reams

Under DBO, DeepEPHTPrepareAndFinalize runs DeepEP dispatch and combine on
the shared ubatch comm stream with allocate_on_comm_stream=False. The
dispatch outputs (recv x, x scales, topk ids and weights) are therefore
allocated in the comm stream's caching-allocator pool but read by the
experts on the compute stream, and combine reads the reduced expert output
that was allocated on the compute stream. Nothing recorded these
cross-stream uses, so a block could be freed and handed to the other
ubatch's next DeepEP call (or to compute work) while kernels queued on the
other stream still read it. With the DeepGEMM MoE this produced garbage
expert ids and an illegal memory access in ep_scatter.

Record the consuming stream on every tensor that crosses between the DBO
compute and comm streams.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Anirudh Ravichandran <ani@primeintellect.ai>
@mergify mergify Bot added the bug Something isn't working label Oct 8, 2026
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

👋 Hi! Thank you for contributing to the vLLM project.

💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in #pr-reviews, coordinate on features in #feat- channels, or join special interest groups in #sig- channels.

PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment /ci run for upstream CI or /amd-ci run for AMD CI only whenever CI signals are needed.

Once the PR is approved or has the ready label, the PR author can also use the corresponding /ci run, /ci retry, and /ci cancel commands, or their /amd-ci variants. New commits do not start upstream CI automatically.

If you have any questions, please reach out to us on Slack at https://slack.vllm.ai.

Agent Guidelines

IMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban.

🚀

@aniravich
aniravich marked this pull request as ready for review October 8, 2026 23:40

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@aniravich

Copy link
Copy Markdown
Contributor Author

@SageMoore could you take a look? DeepEP-HT outputs are allocated on the DBO comm stream but used on the compute stream without record_stream, so the allocator can reuse them too early. cc @LucasWilkinson

@AndreasKaratzas

Copy link
Copy Markdown
Member

This also fixes an intermittent ROCm failure. MI355 Distributed Features crashed in main nightly build 95634 on test_dbo.py::test_dbo_dp_ep_gsm8k[deepep_high_throughput] with Memory access fault by GPU node-2 ... Reason: Unknown while DeepEP HT was dispatching on a DBO microbatch thread. The ROCm host sync from #46990 only waits for the comm stream, so it doesn't cover the compute-stream reads this PR records.

On MI355 (gfx950, DeepEP 0f63d3e9) the fault didn't show in 50 normal runs, so I widened the race by sleeping on the compute stream right after HT dispatch switches back to it (torch.cuda._sleep(3_000_000), about 10 ms):

Delay on compute stream Runs Result
main (9c778e9ff8) yes 10 10/10 memory access fault
main + this PR yes 15 15/15 pass
main + this PR no 12 12/12 pass

@AndreasKaratzas AndreasKaratzas added the verified Run pre-commit for new contributors without triggering other tests label Oct 18, 2026
@AndreasKaratzas

Copy link
Copy Markdown
Member

/ci run

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #95673 for commit 7078e877fa58.

AndreasKaratzas and others added 2 commits October 18, 2026 19:54
…lready orders

On ROCm the comm stream is host-synced right after HT dispatch and combine,
so the dispatch inputs and the combine input are no longer read when they
can be freed. Only the dispatch outputs read on the compute stream still
need recording. Each record_stream costs about 10 us on ROCm, so this
removes about 4% TPOT with DBO + DeepEP HT on MI355.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Andreas Karatzas <akaratza@amd.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Andreas Karatzas <akaratza@amd.com>
@AndreasKaratzas

Copy link
Copy Markdown
Member

I pushed 6b323f5 on top, plus a merge of main. On ROCm, _sync_dbo_comm_if_needed() host-syncs the comm stream right after HT dispatch and combine, so the dispatch inputs and fused_expert_output are no longer read by the time they can be freed. Only the dispatch outputs _receiver reads on the compute stream still need recording there, so the two input-side _dbo_record_stream calls are now skipped when self.sync_dbo_comm is set. CUDA is unchanged.

The skipped calls weren't free: each record_stream adds about 10 µs to that tensor's free and next allocation on ROCm (14.7 µs vs 4.1 µs per tensor in a microbenchmark on MI355), about 7 times per MoE layer per microbatch.

MI355, DeepSeek-V2-Lite-Chat, DP2 + EP, --enable-dbo, deepep_high_throughput, vllm bench serve random 1024/256, 512 prompts, concurrency 64, variants alternated:

Runs Output tok/s Mean TPOT (ms) GSM8K 5-shot (flexible)
main 9 527.6 ± 20.6 119.2 ± 3.2 0.663, 0.671
7078e87 9 513.1 ± 13.2 123.6 ± 3.1 0.667, 0.665
6b323f5 4 534.7 ± 12.4 118.4 ± 2.8 0.664

6b323f5 also passed 10/10 under the compute-stream delay that faults main 10/10. One of the 9 main runs crashed with the same memory access fault without any delay.

@AndreasKaratzas

Copy link
Copy Markdown
Member

/ci run

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #95740 for commit 79cfc6d94c49.

@AndreasKaratzas
AndreasKaratzas merged commit 36e0ae2 into vllm-project:main Oct 18, 2026
127 checks passed
michael-gpu pushed a commit to practical-inference/vllm-ampere-extended that referenced this pull request Oct 20, 2026
…reams (vllm-project#60735)

Signed-off-by: Anirudh Ravichandran <ani@primeintellect.ai>
Signed-off-by: Andreas Karatzas <akaratza@amd.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Andreas Karatzas <akaratza@amd.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working verified Run pre-commit for new contributors without triggering other tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants