Skip to content

[Sampling] Fix HSA OOB in TopP/TopKTopPSamplingFromProbKernel - #3658

Merged
valarLip merged 1 commit into
ROCm:mainfrom
ZiguanWang:fix/sampling-oob-uninit-last-valid-id
Jun 16, 2026
Merged

[Sampling] Fix HSA OOB in TopP/TopKTopPSamplingFromProbKernel#3658
valarLip merged 1 commit into
ROCm:mainfrom
ZiguanWang:fix/sampling-oob-uninit-last-valid-id

Conversation

@ZiguanWang

Copy link
Copy Markdown
Contributor

Motivation

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

crashed with "Memory access fault by GPU node-N",

Technical Details

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.

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.

Test Plan

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.

Test Result

Submission Checklist

@ZiguanWang
ZiguanWang requested a review from a team June 10, 2026 10:37
@github-actions

Copy link
Copy Markdown
Contributor

🏷️ CI Guide

Runs automatically on every PR:

  • ✅ Pre-checks (submodule verification, code formatting)
  • ✅ Aiter op tests (gfx942 + gfx950)
  • ✅ Triton tests on MI35X (only when aiter/ops/triton/** or related paths are changed)

Extended tests (opt-in via labels):

Label Tests
ci:triton-300x Run an additional Triton test job on MI300X in PRs; main branch always runs both MI35X and MI300X
ci:sglang SGLang integration tests: DeepSeek-R1-MXFP4 accuracy, Qwen 3.5 accuracy
ci:atom ATOM benchmark: DeepSeek-R1-0528, GPT-OSS-120B
ci:atom_full ATOM accuracy suite for PR and main models from ATOM models_accuracy.json
ci:vllm vLLM benchmark: GPT-OSS-120B, DeepSeek-R1-0528, Kimi-K2.5
ci:all All standard extended tests (excludes ci:atom_full)

Only add ci:atom_full for FlyDSL or Triton upgrades.
Add labels via the sidebar or gh pr edit 3658 --add-label <label>

@ZiguanWang
ZiguanWang force-pushed the fix/sampling-oob-uninit-last-valid-id branch 2 times, most recently from e559bd2 to 8e40003 Compare June 15, 2026 09:02
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
ZiguanWang force-pushed the fix/sampling-oob-uninit-last-valid-id branch from 8e40003 to e4e83f5 Compare June 16, 2026 02:38
@valarLip
valarLip merged commit adfa4a8 into ROCm:main Jun 16, 2026
43 checks passed
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>
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.

3 participants