Conversation
gty111
requested review from
WoosukKwon,
njhill and
yewentao256
as code owners
August 20, 2026 06:11
Collaborator
|
Can you be more exact about when the race condition happens? |
Collaborator
|
Basically, the idea behind the current design is that we can avoid race condition without such a barrier because async scheduling bounds the maximum concurrency. If I understand correctly, race condition shouldn't happen unless we accidentally reuse the buffer for more than one data in the same step. |
This comment was marked as outdated.
This comment was marked as outdated.
…input buffers Signed-off-by: Tianyu Guo <guoty@inferact.ai>
gty111
force-pushed
the
fix/uva-buffer-pool-reuse-race
branch
from
August 20, 2026 08:13
dfd7d93 to
ea19d0d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Purpose
An encoder-only instance can overwrite the input metadata of a step that is
still running on the device, which surfaces as
CUDA error: an illegal memory access was encounteredat an unrelatedstream_synchronize.UvaBufferPoolhands out pinned host buffers that the device reads in place,and holds exactly
max_concurrent_batchesof them(
model_runner.py,set_default_max_concurrency). A slot therefore comes up forreuse as soon as the device falls one step behind, and
copy_to_uva()writesinto it with no synchronisation — by design, because the batch queue bounds how
many steps are in flight.
That bound only reaches the device if each step has a point where the host waits
for it. Sampling instances do:
AsyncOutputrecordscopy_eventon a copystream that has done
copy_stream.wait_stream(main_stream), so the event isordered after every kernel of that step, and
get_output()blocks on it beforethe step's future resolves. The batch queue pops step N during step N+1 while
step N's slot is not reused until step N+2, leaving one step of margin.
An encoder-only step has no such point. It returns
make_empty_encoder_model_runner_output(), built on the host fromscheduler_output, with no D2H copy and no wait, so its future resolves as soonas the host has finished launching. The host can then run arbitrarily far ahead
while slots keep being recycled every
max_concurrent_batchessteps._apply_write_kernelthen loads a stalerow_idxfrom a recycled slot, computesrow_ptr = base + row_idx * row_stride + start_idx, and stores through it.Grepping the V2 step path confirms that
copy_eventis the only host/devicesynchronisation per step: the three
torch.accelerator.synchronize()calls ingpu/model_runner.pyare inprofile_run()andshutdown(). One of them isalready
if self.is_encoder_only: torch.accelerator.synchronize()— the profilepath needed the same barrier the step path is missing.
V1 is unaffected:
synchronize_input_prep()waits and records around inputpreparation for every step, so it never depends on the output path for ordering.
The fix arms one event for encoder-only instances, waits on it before the step's
first host write and records it after the last one. Sampling instances keep a
synchronisation-free input path (
encoder_input_reuse_eventstaysNone).Test Plan
Repro: two GB200 nodes, Qwen3.5-35B-A3B, disaggregated encoder
(
--mm-encoder-onlyproducer on one node, consumer on the other), V2 runner onboth, MuirBench through the EPD proxy, 8 client processes x 96 concurrent
requests, GPU core dumps enabled (
CUDA_ENABLE_COREDUMP_ON_EXCEPTION=1)..venv/bin/python -m pytest tests/v1/worker/test_gpu_block_table.py \ tests/v1/worker/test_gpu_input_batch.py -q pre-commit run ruff-check ruff-format check-torch-cuda-call --files \ vllm/v1/worker/gpu/model_runner.py pre-commit run mypy-3.12 --hook-stage manual --files \ vllm/v1/worker/gpu/model_runner.pyNo unit test: the change is a device event in the step loop, and covering it
would mean standing up a full
GPUModelRunner. V1's equivalent(
synchronize_input_prep) is likewise covered only end to end. The evidence isthe repro below.
Test Result
mypy-3.12: all Passed.
_apply_write_kernelin the log: 9,532 encoder requests with the mooncake ECconnector, then 15,275 / 15,279 / 23,542 with a CPU/NIXL one — 7m29s to 8m52s
of load each. A GPU core dump puts the fault at that kernel's
tl.store, all32 lanes of the warp on one invalid base address (a garbage
row_idx, not anindex merely out of range), in the last block of the grid in both dumps — the
block whose
pid-indexed metadata is loaded latest.no core dump, engine alive at the end. That clears the observed crash window by
4.4x in requests and ~2.4x in wall clock.
runs — no output change.
(20.70 / 20.92 / 20.59 / 20.12 / 20.11), against 18.4–18.7 for the same
configuration before the fix. The wait only blocks when the device is already
a step behind, which the pool sizing already implies.