[Sampling] Fix HSA OOB in TopP/TopKTopPSamplingFromProbKernel - #3658
Merged
valarLip merged 1 commit intoJun 16, 2026
Merged
Conversation
Contributor
🏷️ CI GuideRuns automatically on every PR:
Extended tests (opt-in via labels):
|
chuanbowang2026
approved these changes
Jun 10, 2026
ZiguanWang
force-pushed
the
fix/sampling-oob-uninit-last-valid-id
branch
2 times, most recently
from
June 15, 2026 09:02
e559bd2 to
8e40003
Compare
Root cause ---------- `SamplingTempStorage::last_valid_id` was never initialized. In each iteration of the rejection loop, `DeviceSamplingFromProb` computes a per-thread `thread_last_valid` and then reduces with `hipcub::Max()` into `max_valid`. The write-back `temp_storage.last_valid_id = max_valid` is guarded by `max_valid != -1`, so when no thread satisfies the predicate `x > low` (e.g. a numerically all-zero row, or NaN/Inf probs coming out of an upstream op), the field is left untouched. The fallback path `if (sampled_id == d) sampled_id = temp_storage.last_valid_id;` then reads uninitialized shared memory, and the subsequent `probs[row_idx * d + sampled_id]` dereferences a garbage index and HSA-faults on a page boundary. Under CUDA Graph capture the shared-memory residue becomes stable across replays, which is why the fault is deterministic in graph mode but only occasionally reproducible in eager mode. VLLM Qwen3.6-35B-A3B-FP8 reproduce steps: 1. VLLM_ROCM_USE_AITER=1 vllm serve Qwen/Qwen3.6-35B-A3B-FP8 --tensor-parallel-size 1 --trust-remote-code --no-enable-prefix-caching --gpu-memory-utilization 0.9 2. vllm bench serve --backend vllm --num-prompts 20 --max-concurrency 4 --dataset-nam random --random-input-len 8192 --random-output-len 1024 A standalone reproducer that forces an all-zero or NaN probs row and replays the kernel under a CUDA Graph reliably triggers the fault on the unfixed kernel and passes on the fixed one. Fix --- 1. Initialize `temp_storage.last_valid_id = 0` at the top of each loop iteration (before the `__syncthreads()` that fences the writer). 2. Defensive clamp on the loaded `sampled_id` before it indexes `probs`. Both changes are applied to `TopPSamplingFromProbKernel` and `TopKTopPSamplingFromProbKernel`, which share the same pattern. Validation ---------- - Standalone reproducer (all-zero / NaN ): HSA fault on pre-fix, 0 OOB ids on post-fix. - New parametrized regression tests in `op_tests/test_sampling.py` all PASS on the fixed kernel. - End-to-end vLLM serve+bench on Qwen3.6-35B-A3B-FP8: previously crashed with "Memory access fault by GPU node-N", now completes 80/80 requests (163840 tokens) with no fault.
ZiguanWang
force-pushed
the
fix/sampling-oob-uninit-last-valid-id
branch
from
June 16, 2026 02:38
8e40003 to
e4e83f5
Compare
valarLip
approved these changes
Jun 16, 2026
shikamd123
added a commit
to shikamd123/vllm
that referenced
this pull request
Jul 20, 2026
…-agnostic wave wake - Remove in-core DP-rank routing from async_llm.py (drop the ROCm auto-route block, _pick_dp_rank_for_request / _ensure_disagg_transfer_id helpers, and the hashlib/current_platform imports). Rank pinning is delegated to the llm-d sidecar/router, per njhill/NickLucche. Delete the now-obsolete tests/v1/engine/test_dp_rank_routing.py. - Reframe the DP first-wave wake in core.py from a current_platform.is_rocm() gate to the external-LB DP mode (data_parallel_external_lb). In external LB the router addresses engines directly, so the coordinator is not in the per-request wake path; internal/hybrid LB stays bit-identical to upstream. Add a debug trace at the wake point (njhill vllm-project#5). - Revert VLLM_ENGINE_HANDSHAKE_TIMEOUT_MINUTES; restore the constant HANDSHAKE_TIMEOUT_MINS = 5 (njhill vllm-project#6). - Guard against division by zero on data_parallel_size_local in moriio_common.py port allocation (Doug vllm-project#10). - Fill the AITER root-cause/fix link (ROCm/aiter#3658) in the sampler warm-up skip TODO. - Pin the decode leg to the prefill DP rank via the X-data-parallel-rank header in the moriio toy proxy example. Signed-off-by: Shiksha Patel <shikpate@amd.com>
shikamd123
added a commit
to shikamd123/vllm
that referenced
this pull request
Jul 20, 2026
…t#3658 The ROCm/DP sampler warm-up skip (_should_skip_rocm_sampler_warmup) was a workaround for a hang during distributed bring-up on MI300X (gfx942). Root cause was in AITER, not vLLM: the top_k_top_p / top_p rejection-sampling kernels read an uninitialized `last_valid_id` and index `probs` out of bounds when every thread fails the first bisection predicate, raising an HSA memory fault. Under DP=EP=16 (2P2D) / DP=EP=8 (1P1D) Wide-EP the faulting rank's HSA signal never retires, so the lockstep DP collective deadlocks the whole group. This is fixed in AITER by initializing last_valid_id and clamping the sampled index (ROCm/aiter#3658). With that fix present, the warm-up runs cleanly, so the vLLM-side skip is no longer needed and this restores the stock upstream warm-up path. AITER vllm-project#3658 is REQUIRED to run this configuration (DP=EP=16, TP=1 disaggregated P/D) without the hang. Deployments must use an AITER build that includes vllm-project#3658 (min-AITER bump), or bake the fix into the AITER wheel until it is released. Validated on DP=EP=16 (2P2D) MI300X: clean bring-up with the skip removed and non-greedy sampling (temp/top_p/top_k) at 8192/1024 and 1024/1024 with zero failures and zero engine restarts.
shikamd123
added a commit
to shikamd123/vllm
that referenced
this pull request
Jul 29, 2026
…-agnostic wave wake - Remove in-core DP-rank routing from async_llm.py (drop the ROCm auto-route block, _pick_dp_rank_for_request / _ensure_disagg_transfer_id helpers, and the hashlib/current_platform imports). Rank pinning is delegated to the llm-d sidecar/router, per njhill/NickLucche. Delete the now-obsolete tests/v1/engine/test_dp_rank_routing.py. - Reframe the DP first-wave wake in core.py from a current_platform.is_rocm() gate to the external-LB DP mode (data_parallel_external_lb). In external LB the router addresses engines directly, so the coordinator is not in the per-request wake path; internal/hybrid LB stays bit-identical to upstream. Add a debug trace at the wake point (njhill vllm-project#5). - Revert VLLM_ENGINE_HANDSHAKE_TIMEOUT_MINUTES; restore the constant HANDSHAKE_TIMEOUT_MINS = 5 (njhill vllm-project#6). - Guard against division by zero on data_parallel_size_local in moriio_common.py port allocation (Doug vllm-project#10). - Fill the AITER root-cause/fix link (ROCm/aiter#3658) in the sampler warm-up skip TODO. - Pin the decode leg to the prefill DP rank via the X-data-parallel-rank header in the moriio toy proxy example. Signed-off-by: Shiksha Patel <shikpate@amd.com>
shikamd123
added a commit
to shikamd123/vllm
that referenced
this pull request
Jul 29, 2026
…t#3658 The ROCm/DP sampler warm-up skip (_should_skip_rocm_sampler_warmup) was a workaround for a hang during distributed bring-up on MI300X (gfx942). Root cause was in AITER, not vLLM: the top_k_top_p / top_p rejection-sampling kernels read an uninitialized `last_valid_id` and index `probs` out of bounds when every thread fails the first bisection predicate, raising an HSA memory fault. Under DP=EP=16 (2P2D) / DP=EP=8 (1P1D) Wide-EP the faulting rank's HSA signal never retires, so the lockstep DP collective deadlocks the whole group. This is fixed in AITER by initializing last_valid_id and clamping the sampled index (ROCm/aiter#3658). With that fix present, the warm-up runs cleanly, so the vLLM-side skip is no longer needed and this restores the stock upstream warm-up path. AITER vllm-project#3658 is REQUIRED to run this configuration (DP=EP=16, TP=1 disaggregated P/D) without the hang. Deployments must use an AITER build that includes vllm-project#3658 (min-AITER bump), or bake the fix into the AITER wheel until it is released. Validated on DP=EP=16 (2P2D) MI300X: clean bring-up with the skip removed and non-greedy sampling (temp/top_p/top_k) at 8192/1024 and 1024/1024 with zero failures and zero engine restarts. Signed-off-by: Shiksha Patel <shikpate@amd.com>
raviguptaamd
pushed a commit
to raviguptaamd/vllm
that referenced
this pull request
Aug 1, 2026
…-agnostic wave wake - Remove in-core DP-rank routing from async_llm.py (drop the ROCm auto-route block, _pick_dp_rank_for_request / _ensure_disagg_transfer_id helpers, and the hashlib/current_platform imports). Rank pinning is delegated to the llm-d sidecar/router, per njhill/NickLucche. Delete the now-obsolete tests/v1/engine/test_dp_rank_routing.py. - Reframe the DP first-wave wake in core.py from a current_platform.is_rocm() gate to the external-LB DP mode (data_parallel_external_lb). In external LB the router addresses engines directly, so the coordinator is not in the per-request wake path; internal/hybrid LB stays bit-identical to upstream. Add a debug trace at the wake point (njhill vllm-project#5). - Revert VLLM_ENGINE_HANDSHAKE_TIMEOUT_MINUTES; restore the constant HANDSHAKE_TIMEOUT_MINS = 5 (njhill vllm-project#6). - Guard against division by zero on data_parallel_size_local in moriio_common.py port allocation (Doug vllm-project#10). - Fill the AITER root-cause/fix link (ROCm/aiter#3658) in the sampler warm-up skip TODO. - Pin the decode leg to the prefill DP rank via the X-data-parallel-rank header in the moriio toy proxy example. Signed-off-by: Shiksha Patel <shikpate@amd.com>
raviguptaamd
pushed a commit
to raviguptaamd/vllm
that referenced
this pull request
Aug 1, 2026
…t#3658 The ROCm/DP sampler warm-up skip (_should_skip_rocm_sampler_warmup) was a workaround for a hang during distributed bring-up on MI300X (gfx942). Root cause was in AITER, not vLLM: the top_k_top_p / top_p rejection-sampling kernels read an uninitialized `last_valid_id` and index `probs` out of bounds when every thread fails the first bisection predicate, raising an HSA memory fault. Under DP=EP=16 (2P2D) / DP=EP=8 (1P1D) Wide-EP the faulting rank's HSA signal never retires, so the lockstep DP collective deadlocks the whole group. This is fixed in AITER by initializing last_valid_id and clamping the sampled index (ROCm/aiter#3658). With that fix present, the warm-up runs cleanly, so the vLLM-side skip is no longer needed and this restores the stock upstream warm-up path. AITER vllm-project#3658 is REQUIRED to run this configuration (DP=EP=16, TP=1 disaggregated P/D) without the hang. Deployments must use an AITER build that includes vllm-project#3658 (min-AITER bump), or bake the fix into the AITER wheel until it is released. Validated on DP=EP=16 (2P2D) MI300X: clean bring-up with the skip removed and non-greedy sampling (temp/top_p/top_k) at 8192/1024 and 1024/1024 with zero failures and zero engine restarts. Signed-off-by: Shiksha Patel <shikpate@amd.com>
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.
Motivation
VLLM Qwen3.6-35B-A3B-FP8 reproduce steps:
crashed with "Memory access fault by GPU node-N",
Technical Details
Root cause
SamplingTempStorage::last_valid_idwas never initialized. In each iteration of the rejection loop,DeviceSamplingFromProbcomputes a per-threadthread_last_validand then reduces withhipcub::Max()intomax_valid. The write-backtemp_storage.last_valid_id = max_validis guarded bymax_valid != -1, so when no thread satisfies the predicatex > low(e.g. a numerically all-zero row, or NaN/Inf probs coming out of an upstream op), the field is left untouched. The fallback pathif (sampled_id == d) sampled_id = temp_storage.last_valid_id;then reads uninitialized shared memory, and the subsequentprobs[row_idx * d + sampled_id]dereferences a garbage index and HSA-faults on a page boundary.Under CUDA Graph capture the shared-memory residue becomes stable across replays, which is why the fault is deterministic in graph mode but only occasionally reproducible in eager mode.
A standalone reproducer that forces an all-zero or NaN probs row and replays the kernel under a CUDA Graph reliably triggers the fault on the unfixed kernel and passes on the fixed one.
Fix
temp_storage.last_valid_id = 0at the top of each loop iteration (before the__syncthreads()that fences the writer).sampled_idbefore it indexesprobs.Both changes are applied to
TopPSamplingFromProbKernelandTopKTopPSamplingFromProbKernel, which share the same pattern.Test Plan
Validation
op_tests/test_sampling.pyall PASS on the fixed kernel.Test Result
Submission Checklist