Repository navigation
[HIP] [Bugfix] Keep MX quant-sort and multi-phase MoE sort in bounds under num_local_tokens - #7085
Conversation
…E sort Sorted with a real-row count (num_local_tokens), a block's padding slots hold the sentinel `topk << 24 | count`: a real token id with an out-of-range slot. fused_mx_quant_moe_sort_kernel only checks the token, so the sentinel passes; its weight is 0, so it takes the zero-weight branch, and #6916 dropped the `topk_id < topk` guard from store_zero_mx_quant_moe_sort_row. With count the last real row, `count * topk + topk` is the row past `out`: a stage-2 call zeroes one row of whatever follows `out` in device memory. Since ROCm/ATOM#2725 passes num_local_tokens on every MXFP4 fused_moe call, this fires on DeepSeek-V4.1-Flash TP2 (a8w4, topk 6): AgentX c16+ dies with an intermittent illegal memory access, which surfaces in later kernels. - A route is valid when topk == 1 or topk_id < topk. An invalid one zeroes its scale slot (indexed by its sorted row, always in range) and neither reads its input row nor writes an out row. - op_tests/test_moe_sorting_mxfp4.py: padding sentinels under a count must leave rows past `out` untouched and the real routes byte-identical to an uncounted sort (fp8 / fp4x2, bounded and unbounded launchers). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…_tokens With a device-side token count (LocalToken), the multi-phase sort kernels run their vector loops to the count rounded up to the vector width. When the count is the tensor's height and not a multiple of that width, the last vector loads past the end of the [tokens, topk] ids. The ids are dropped by the token check, so results are right, but the load faults when the tensor ends right before an unmapped page -- intermittently, by allocator layout. ROCm/ATOM#2725 passes num_local_tokens on every fused_moe call, so plain prefill (count == tokens) hits it: DeepSeek-V4.1-Flash TP2 AgentX c64 died about once per 17k requests after the quant-sort fix. - The P0 v1 / v2 and P01 kernels read a vector that would pass the end id by id, ids past the end -1 (no expert); every other vector loads as before. - Counted and uncounted sorts of the same real rows give the same routes for M in 1..16384 (checked as (expert, token, slot) multisets). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🏷️ CI GuideRuns automatically on every PR:
Extended tests (opt-in via labels):
One backend per PR: PR title tags & labels: |
Take main's csrc/include/moe_sorting_opus.h as-is: #7085 fixed the same three multi-phase kernels (impl::moe_sorting_load_topk_vec, bounded by the topk_ids tensor height), so this branch's kernel change is dropped. The branch now only adds the reproducer test_moe_sorting_local_tokens_tail_read to op_tests/test_moe_sorting.py.
…under num_local_tokens (#7085) * fix(quant): never write a sorting sentinel's row in the MX quant + MoE sort Sorted with a real-row count (num_local_tokens), a block's padding slots hold the sentinel `topk << 24 | count`: a real token id with an out-of-range slot. fused_mx_quant_moe_sort_kernel only checks the token, so the sentinel passes; its weight is 0, so it takes the zero-weight branch, and #6916 dropped the `topk_id < topk` guard from store_zero_mx_quant_moe_sort_row. With count the last real row, `count * topk + topk` is the row past `out`: a stage-2 call zeroes one row of whatever follows `out` in device memory. Since ROCm/ATOM#2725 passes num_local_tokens on every MXFP4 fused_moe call, this fires on DeepSeek-V4.1-Flash TP2 (a8w4, topk 6): AgentX c16+ dies with an intermittent illegal memory access, which surfaces in later kernels. - A route is valid when topk == 1 or topk_id < topk. An invalid one zeroes its scale slot (indexed by its sorted row, always in range) and neither reads its input row nor writes an out row. - op_tests/test_moe_sorting_mxfp4.py: padding sentinels under a count must leave rows past `out` untouched and the real routes byte-identical to an uncounted sort (fp8 / fp4x2, bounded and unbounded launchers). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(moe_sorting): never load topk_ids past the tensor under num_local_tokens With a device-side token count (LocalToken), the multi-phase sort kernels run their vector loops to the count rounded up to the vector width. When the count is the tensor's height and not a multiple of that width, the last vector loads past the end of the [tokens, topk] ids. The ids are dropped by the token check, so results are right, but the load faults when the tensor ends right before an unmapped page -- intermittently, by allocator layout. ROCm/ATOM#2725 passes num_local_tokens on every fused_moe call, so plain prefill (count == tokens) hits it: DeepSeek-V4.1-Flash TP2 AgentX c64 died about once per 17k requests after the quant-sort fix. - The P0 v1 / v2 and P01 kernels read a vector that would pass the end id by id, ids past the end -1 (no expert); every other vector loads as before. - Counted and uncounted sorts of the same real rows give the same routes for M in 1..16384 (checked as (expert, token, slot) multisets). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Motivation
Since ROCm/ATOM#2725, ATOM passes
num_local_tokens(an int32[1]device tensor holding the pass's real row count) on every MXFP4fused_moecall. Two kernels on the gfx950 a8w4 path then touch memory outside their tensors. Both are intermittent: whether one faults depends on what the caching allocator placed after the tensor. On DeepSeek-V4.1-Flash plain TP2 (MI355X,--kv_cache_dtype bf16, mono decode on), InferenceX AgentX c16 / c128 / c256 died withHIP error: an illegal memory access was encountered. With the stock aiter, each c64 run on aac (4 servers per node) died after about 200 requests.1. Out-of-bounds row write in
fused_mx_quant_moe_sort_kernel(csrc/kernels/quant_kernels.cu).A counted sort fills a block's padding slots with the sentinel
topk << 24 | count, which is a real token id paired with an out-of-range slot. The kernel only checks the token (token_idx < num_tokens), so the sentinel passes. Its weight is 0, so it takes the zero-weight branch. #6916 removed thetopk_id < topkguard fromstore_zero_mx_quant_moe_sort_row, so that branch now zeroes rowcount * topk + topk. Whencountis the last real row, that is the row afterout, and the stage-2 call writes zeros into whatever tensor follows. Before ATOM#2725 the sentinel token wastokens, which is rejected by the token check, so this path was never reached.2. Out-of-bounds
topk_idsloads in the multi-phase sort (csrc/include/moe_sorting_opus.h).With
LocalToken, theP0_v1,P0_v2andP01kernels run their vector loop up to the count rounded up to the vector width. During prefill the count is usually the tensor height. When the height is not a multiple of the width, the last vector load goes past the end of[tokens, topk]. The extra ids are discarded by the token check, so the results are correct, but the load faults if the next page is unmapped. (#7062 targets the same loads with a different approach. This change was written independently while bisecting the crash above. Happy to drop this half in favour of #7062.)Technical Details
fused_mx_quant_moe_sort_kernel: a route is valid whentopk == 1 || topk_id < topk. An invalid route zeroes only its scale slot, which is indexed by its sorted row and so always in range. It no longer reads its input row or writes anoutrow.store_zero_mx_quant_moe_sort_rowgets the guard back asroute_valid. Valid routes behave exactly as before.moe_sorting_opus.h: newimpl::moe_sorting_load_topk_vec. A vector that would crosstokens * topkis read id by id, and ids past the end are set to -1 (no expert). All other vectors use the same single load as before. Herekargs.tokensis the host-side tensor height, which is the maximum inLocalTokenmode.Test Plan
op_tests/test_moe_sorting_mxfp4.py: new section "Padding sentinels under a num_local_tokens count". It sorts withnum_local_tokens = tokens - 1and the padding row set to -1, placesoutin front of sentinel bytes, and asserts that nothing pastoutis written and that the real routes match an uncounted sort byte for byte. It covers fp8 / fp4x2, tokens 2/5/7/33, and both the bounded and unbounded launchers.moe_sortingon the same real rows, compared as(expert, token, slot)multisets, for M in {1, 3, 7, 33, 257, 1023, 2090, 4097, 8191, 16383, 16384} and counts M, M-1 and M/2.op_tests/test_moe_sorting.py(including CUDA graph capture/replay).e893780f4(ATOM#2725), DeepSeek-V4.1-Flash TP2, AgentX trace replay with the InferenceX recipe, 30 min per run, four servers per node.Test Result
wrote past out; exactly one 1152-byte row afteroutis zeroed). With this PR, 16/16 cases pass.test_moe_sorting_mxfp4.pypasses (exit 0), including the 12 zero-weight cases from [ASM] [HIP] [FlyDSL] feat: pad-row routing count, custom all_gatherv, MoE quant-sort race fix #6916.test_moe_sorting.pypasses. Counted vs uncounted multisets match for every M and count, before and after this change, so routing results are unchanged.The request error rate with this PR is 0.02–0.09%, the same as a run that does not pass
num_local_tokensat all.The end-to-end and kernel tests ran on the aiter in
rocm/atom-dev:nightly_202610261558(MI355X) with these same hunks applied. This branch is that patch rebased onto currentmain, which still contains both issues.🤖 Generated with Claude Code