Skip to content

[HIP] [CK] [MoE] Reject non-int32 index buffers in the topk kernels - #5255

Merged
valarLip merged 6 commits into
ROCm:mainfrom
i-kosarev:guard-topk-index-dtype
Sep 7, 2026
Merged

[HIP] [CK] [MoE] Reject non-int32 index buffers in the topk kernels#5255
valarLip merged 6 commits into
ROCm:mainfrom
i-kosarev:guard-topk-index-dtype

Conversation

@i-kosarev

@i-kosarev i-kosarev commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Problem

topk_softmax, grouped_topk and biased_grouped_topk write their outputs through reinterpret_cast<int*>(...) without checking the tensor's dtype:

// csrc/kernels/topk_softmax_kernels.cu
reinterpret_cast<int*>(topk_indices.data_ptr()),

A caller that allocates a 64-bit index buffer gets only the low half of every slot written; the rest keeps whatever the allocation happened to contain. Nothing fails — the call returns normally with a plausible-looking tensor.

Measurement

gfx950 (MI350X), one direct topk_softmax call, buffer pre-filled with -999999 so an untouched slot is unmistakable. 256 tokens, topk 6, 64 experts:

buffer dtype ids landing in [0, 64) min / max
torch.int32 1536 / 1536 (100%) 0 / 63
torch.int64 14 / 1536 (0.9%) -999999 / 270582939709

The surviving poison is the proof those slots were never written.

repro
import torch
from aiter import topk_softmax

M, E, TOPK = 256, 64, 6
gating = torch.randn(M, E, device="cuda", dtype=torch.float32)

for dt in (torch.int32, torch.int64):
    w = torch.empty(M, TOPK, device="cuda", dtype=torch.float32)
    ids = torch.empty(M, TOPK, device="cuda", dtype=dt).fill_(-999999)
    tei = torch.empty(M, TOPK, device="cuda", dtype=torch.int32)
    topk_softmax(w, ids, tei, gating, False, 0, "")
    torch.cuda.synchronize()
    ok = int(((ids >= 0) & (ids < E)).sum())
    print(f"{dt}: in_range={ok}/{ids.numel()} min={int(ids.min())} max={int(ids.max())}")

Why it matters

Downstream this corrupts whatever consumes the ids, far from the kernel that produced them. In vLLM's ROCm expert-parallel path it reached DeepEP, which indexes with them, producing two unrelated-looking crash signatures from one cause: an out-of-bounds write (Memory access fault ... Write access to a read-only page), and — once the obviously invalid ids were clamped — duplicate expert ids per token tripping DeepEP's own device assert and aborting with HSA_STATUS_ERROR_EXCEPTION. Tracking that back to a dtype mismatch in the routing kernel took a while.

The change

Add the dtype checks that topk_gating already performs for exactly these arguments:

AITER_CHECK(topk_weights.dtype() == AITER_DTYPE_fp32, "topk_weights must be float32");
AITER_CHECK(topk_indices.dtype() == AITER_DTYPE_i32,  "topk_indices must be int32");

So this brings its siblings in line rather than introducing a new contract.

Compatibility

No behaviour change for correct callers — every allocation in this repository already uses int32:

  • aiter/fused_moe.py:4261,4279
  • aiter/fused_moe_bf16_asm.py:608
  • op_tests/test_topk_softmax.py:22-23, op_tests/test_moe_topk_gating.py

The caller-side fix for the vLLM case is vllm-project/vllm#55147. This change is what turns the same mistake into an immediate, attributable error for the next caller instead of silent memory corruption.

@i-kosarev
i-kosarev requested review from a team and a lite review from Copilot September 3, 2026 13:15
`topk_softmax`, `grouped_topk` and `biased_grouped_topk` write their outputs
through `reinterpret_cast<int*>(...)` without checking the tensor's dtype.
A caller that allocates a 64-bit index buffer therefore gets only the low
half of every slot written; the rest keeps whatever the allocation happened
to contain. Nothing fails -- the call returns normally and hands back a
plausible-looking tensor.

Measured on gfx950 (MI350X) with one direct `topk_softmax` call, buffer
pre-filled with -999999 so an untouched slot is unmistakable, 256 tokens,
topk 6, 64 experts:

    buffer dtype     ids landing in [0, 64)     min / max
    torch.int32      1536 / 1536  (100%)        0 / 63
    torch.int64        14 / 1536  (0.9%)        -999999 / 270582939709

The surviving poison is the proof that those slots were never written.

Downstream this corrupts whatever consumes the ids. In vLLM's ROCm
expert-parallel path it reached DeepEP V2, which indexes with them: an
out-of-bounds write ("Memory access fault ... Write access to a read-only
page"), and -- once the obviously invalid ids were clamped -- duplicate
expert ids per token tripping DeepEP's own device assert and aborting with
HSA_STATUS_ERROR_EXCEPTION. Two unrelated-looking crash signatures, one
cause, and a long way from the kernel that produced it.

`topk_gating` already validates exactly these arguments:

    AITER_CHECK(topk_weights.dtype() == AITER_DTYPE_fp32, "topk_weights must be float32");
    AITER_CHECK(topk_indices.dtype() == AITER_DTYPE_i32,  "topk_indices must be int32");

so this only brings its siblings in line rather than introducing a new
contract.

No behaviour change for correct callers: every allocation in this repository
already uses int32 (`fused_moe.py:4261,4279`, `fused_moe_bf16_asm.py:608`),
as do the op_tests (`test_topk_softmax.py:22-23`,
`test_moe_topk_gating.py`).

The caller-side fix for the vLLM case is vllm-project/vllm#55147; this change
is what turns the same mistake into an immediate, attributable error for the
next caller instead of silent memory corruption.

Signed-off-by: Ilia Kosarev <ilia.kosarev@amd.com>
@github-actions github-actions Bot changed the title [MoE] Reject non-int32 index buffers in the topk kernels [HIP] [MoE] Reject non-int32 index buffers in the topk kernels Sep 3, 2026
@github-actions github-actions Bot added the HIP label Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

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:gfx1250-ffm-triton Run the five-shard gfx1250 FFM Triton test suite
ci:triton-300x Run an additional Triton test job on MI300X in PRs; main branch always runs both MI35X and MI300X
multigpu Aiter multi-GPU tests on the 8-GPU runner
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 5255 --add-label <label>

PR title tags & labels:
Component tags ([Triton/Gluon], [HIP], [CK], [ASM], ...) are added to the PR title and as PR labels automatically from the changed files and re-synced on every push — change-type tags like [fix]/[Perf], op tags like [MLA], and human labels (ci:*) are left untouched. Add the no-auto-title label to opt this PR out.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The new fail-fast behavior is not covered by negative tests (e.g., asserting int64 index buffers raise), which risks regressions back to silent corruption.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds runtime dtype validation to MoE top-k routing kernels to prevent silent memory corruption when callers provide non-int32 index output buffers, aligning behavior with existing topk_gating contract checks.

Changes:

  • Add AITER_CHECK dtype guards to topk_softmax for topk_weights (fp32), topk_indices (int32), and token_expert_indices (int32).
  • Add AITER_CHECK dtype guards to grouped_topk / biased_grouped_topk for topk_weights (fp32) and topk_ids (int32).
File summaries
File Description
csrc/kernels/topk_softmax_kernels.cu Adds fp32/int32 dtype checks before writing outputs via reinterpret_cast<int*>.
csrc/kernels/topk_softmax_kernels_group.cu Adds fp32/int32 dtype checks for grouped routing variants that write topk_ids via reinterpret_cast<int*>.
Review details

Suppressed comments (1)

csrc/kernels/topk_softmax_kernels_group.cu:1297

  • Similarly for grouped_topk, there’s no test coverage asserting that passing a non-int32 topk_ids buffer fails fast. Adding an explicit negative test would help ensure this ABI constraint doesn’t accidentally get relaxed or broken later (especially since the kernel writes through reinterpret_cast<int*>).
    const aiter_tensor_t& correction_bias = topk_ids;
    AITER_CHECK(topk_weights.dtype() == AITER_DTYPE_fp32,
                "topk_weights must be float32");
    AITER_CHECK(topk_ids.dtype() == AITER_DTYPE_i32,
                "topk_ids must be int32");
  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread csrc/kernels/topk_softmax_kernels.cu
Comment thread csrc/kernels/topk_softmax_kernels_group.cu

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Related ASM, sigmoid, and fused-gate routes remain unguarded, and the rejection behavior lacks tests.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (2)

csrc/kernels/topk_softmax_kernels.cu:804

  • The existing op_tests/test_topk_softmax.py coverage only allocates the three outputs with their valid dtypes, so none of these new rejection contracts is exercised. Add negative tests using pytest.raises(RuntimeError, match=...) for non-float32 weights and non-int32 values of each index buffer; otherwise these guards can regress while the current correctness matrix stays green.
    AITER_CHECK(topk_weights.dtype() == AITER_DTYPE_fp32,
                "topk_weights must be float32");
    AITER_CHECK(topk_indices.dtype() == AITER_DTYPE_i32,
                "topk_indices must be int32");
    AITER_CHECK(token_expert_indices.dtype() == AITER_DTYPE_i32,
                "token_expert_indices must be int32");

csrc/kernels/topk_softmax_kernels_group.cu:1243

  • The grouped-top-k tests in op_tests/test_moeTopkSoftmax.py:293-307,403-416 only pass float32 weights and int32 IDs, so the newly added failure behavior for both grouped entry points is untested. Add explicit invalid-dtype tests, including the large-token public biased_grouped_topk fallback route, to verify every dispatch path rejects before launch.
    AITER_CHECK(topk_weights.dtype() == AITER_DTYPE_fp32,
                "topk_weights must be float32");
    AITER_CHECK(topk_ids.dtype() == AITER_DTYPE_i32,
                "topk_ids must be int32");
  • Files reviewed: 2/2 changed files
  • Comments generated: 3
  • Review effort level: Balanced

Comment thread csrc/kernels/topk_softmax_kernels.cu
Comment thread csrc/kernels/topk_softmax_kernels.cu
Comment thread csrc/kernels/topk_softmax_kernels_group.cu
The first pass covered topk_softmax, grouped_topk and biased_grouped_topk, but
an int64 id buffer is still accepted by three other reachable entry points,
each of which writes 4 bytes per element and leaves the rest of every slot
holding whatever the caller allocated:

  * topk_sigmoid    - forwards topk_indices.data_ptr() unchecked
  * topk_softmax_asm - hard-codes out_stride * 4
  * moe_fused_gate  - reinterpret_cast<int32_t*>, and biased_grouped_topk
                      re-dispatches to it purely on token count, so the public
                      call that is guarded on one path is unguarded on the other

Add a negative test per entry point. They run in subprocesses: AITER_CHECK in
these translation units resolves to check_fail, which prints to stderr and then
aborts unless g_aiter_can_throw is set, and none of these files opt into the
ctypes error translation that sets it. That is pre-existing behaviour shared
with the ~34 AITER_CHECKs already in these files, but it does mean
pytest.raises cannot observe them; asserting on the message the process died
with is correct either way.
Copilot AI review requested due to automatic review settings September 3, 2026 17:41
@github-actions github-actions Bot changed the title [HIP] [MoE] Reject non-int32 index buffers in the topk kernels [HIP] [CK] [MoE] Reject non-int32 index buffers in the topk kernels Sep 3, 2026
@github-actions github-actions Bot added the CK label Sep 3, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The new test file lacks an entry point, so the standard CI invocation executes none of its tests.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread op_tests/test_topk_index_dtype_contract.py Outdated
aiter_test.sh runs op_tests files as `python3 <file>`, so a module containing
only pytest functions defines nine tests, runs none, and exits 0 -- reported as
a pass. Which is precisely the failure mode this file exists to rule out.
Copilot AI review requested due to automatic review settings September 3, 2026 17:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The new sigmoid fp32 restriction rejects fp16 and bf16 weight outputs that its existing CK trait dispatch explicitly supports.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

csrc/py_itfs_ck/topk_sigmoid_kernels.cu:25

  • This rejects output dtypes that this CK wrapper explicitly supports: lines 43-66 map topk_weights.dtype() across fp16/bf16/fp32 and pass it as weight_prec so the kernel writes with the selected width. Consequently, existing topk_sigmoid callers using fp16 or bf16 weights now fail even though only the index buffer has the fixed 32-bit contract. Keep the int32 index check, but remove this fp32-only restriction and update the comment accordingly.
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

…check

Unlike the other three entry points, this wrapper does not hard-code the weight
width: weight_prec is derived from topk_weights.dtype() and mapped across
fp16/bf16/fp32, and the kernel writes at the selected width. Requiring fp32
would have broken existing callers. Only topk_indices has the fixed 32-bit
contract here.
Copilot AI review requested due to automatic review settings September 3, 2026 18:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The guards span multiple GPU backends and the updated branch lacks complete head-matched runtime validation.

Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Comment thread op_tests/test_topk_index_dtype_contract.py Outdated
Kept the AITER_CHECK/TORCH_CHECK additions across the six entry points --
those are the actual fix. Dropped op_tests/test_topk_index_dtype_contract.py
per @valarLip's review (op_tests/test_topk_index_dtype_contract.py, request:
'please remove this one').
Copilot AI review requested due to automatic review settings September 4, 2026 09:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The ASM path unnecessarily rejects non-int32 token_expert_indices despite never consuming that buffer.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread csrc/py_itfs_cu/asm_topksoftmax.cu Outdated
KernelArgs only forwards ptr_T (topk_indices), ptr_W (topk_weights) and ptr_A
(gating_output); token_expert_indices is accepted as a parameter and never
assigned into args, so this kernel neither reads nor writes it. The check
therefore rejected callers on a buffer the partial-write fix has nothing to
do with -- confirmed against op_tests/test_moeTopkSoftmax.py's test_asm,
which discards that argument with 'Not used. Will be used in the future.'
Copilot AI review requested due to automatic review settings September 4, 2026 09:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The current tree lacks regression tests exercising the newly added rejection paths.

Review details

Suppressed comments (1)

csrc/kernels/topk_softmax_kernels.cu:804

  • The promised regression test is absent from the current tree: op_tests/test_topk_index_dtype_contract.py no longer exists, and the existing top-k tests only allocate int32 outputs. Consequently none of the new rejection paths—including the ASM, CK sigmoid, grouped, and fused-gate routes—is exercised, so these guards could regress while the positive tests remain green. Restore the subprocess-based negative cases for every changed entry point (including the fused-gate redispatch route).
    AITER_CHECK(topk_indices.dtype() == AITER_DTYPE_i32,
                "topk_indices must be int32");
    AITER_CHECK(token_expert_indices.dtype() == AITER_DTYPE_i32,
                "token_expert_indices must be int32");
  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@i-kosarev
i-kosarev requested a review from valarLip September 4, 2026 09:56
@zufayu
zufayu requested a review from junhaha666 September 4, 2026 23:44
@valarLip
valarLip merged commit e8eac17 into ROCm:main Sep 7, 2026
56 checks passed
@i-kosarev

i-kosarev commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

@valarLip @zufayu @junhaha666 any chance you could also look at #5256 ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants