[CUDA] Parallelize ArgMax/ArgMin over the reduction axis - #31694
Closed
Tianlei Wu (tianleiwu) wants to merge 1 commit into
Closed
Tianlei Wu (tianleiwu) wants to merge 1 commit into
Tianlei Wu (tianleiwu) wants to merge 1 commit into
Conversation
`arg_min_max_last_axis_kernel` parallelizes only across rows, so a single thread walks the entire reduction axis. When the axis is long and there are few rows the GPU is almost entirely idle: an ArgMax over a 129280-wide axis with one row launches `grid=(1,1,1)`, `block=(256,1,1)` and leaves exactly one active thread issuing 129280 dependent global loads. That shape is common at decode time, where a model takes an ArgMax over the vocabulary. Add `arg_min_max_last_axis_block_kernel`, which assigns one block per row, strides the reduction axis across the block so the loads coalesce, and combines the per-thread candidates with a shared-memory tree reduction. The launcher selects it once the axis is at least a full block wide and keeps the thread-per-row kernel for short axes, where that mapping is still better. Ties continue to resolve to the lowest index, matching ONNX's first-occurrence rule; the CUDA EP already rejects `select_last_index == 1`. Measured on H200 (sm90) with one row and a 129280-wide axis: 3098 us -> 46 us, a 67x improvement. Tested by comparing the CUDA EP against the CPU EP over 396 configurations (float/MLFloat16/double x ArgMax/ArgMin x keepdims x random, tied and all-equal inputs), all matching. Adds ArgMax/ArgMin cases over an axis that is wider than the block and not a multiple of it, covering the strided-loop remainder, multiple rows, and the lowest-index tie-break.
Contributor
|
Superseded by #32092. Thanks for reviewing! |
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.
Description
arg_min_max_last_axis_kernelparallelizes only across rows, so a single thread walks the entire reduction axis. When the axis is long and there are few rows, the GPU is almost entirely idle.This adds
arg_min_max_last_axis_block_kernel, which assigns one block per row, strides the reduction axis across the block so the loads coalesce, and combines the per-thread candidates with a shared-memory tree reduction. The launcher picks it once the axis is at least a full block wide (256) and keeps the existing thread-per-row kernel for short axes, where that mapping is still the better one.Motivation and Context
An
ArgMaxover a 129280-wide axis with a single row currently launches:with exactly one active thread, issuing 129280 dependent global loads — about 3.10 ms per call, roughly 0.17 GB/s of achieved bandwidth.
That shape is common at decode time, where a model takes an
ArgMaxover the vocabulary to pick the next token. It was found while profiling a speculative-decoding workload on 8×H200, where five suchArgMaxcalls per step accounted for 90.4% of the drafter's GPU kernel time.Performance
Measured on H200 (sm90), one row, 129280-wide axis:
In the speculative-decoding workload that motivated the change, the affected model's kernel time dropped from 448.71 ms to 6.73 ms and end-to-end decode throughput improved by 52.6%, with unchanged output.
Semantics
Ties continue to resolve to the lowest index, matching ONNX's first-occurrence rule. Each thread seeds with element 0, which is always a valid candidate and can never displace a strictly better one, so rows shorter than the block need no separate guard. The CUDA EP already rejects
select_last_index == 1, so only first-occurrence behavior has to be preserved.Short axes (
n < 256) keep the original kernel, since one thread per row is the better mapping when there are many rows and little to reduce.Testing
keepdims0/1 × random, tied, and all-equal inputs, across axis lengths spanning both sides of the 256 threshold (1, 2, 255, 256, 257, 1000, 129280) and row counts 1–300. All matched.ArgMax_float_long_axis_multiple_rowsandArgMin_float_long_axis_multiple_rowstoreduction_ops_test.cc. They use a 300-wide axis — wider than the block and not a multiple of it — over 4 rows, covering the strided-loop remainder, the grid mapping across rows, a maximum in the first position, a maximum in the remainder region, duplicate extrema (must return the lower index), and a fully constant row.ArgMax_float_first_index_randomtest already exercises this path (1 row, 65536-wide axis, duplicate+infvalues expecting the lowest index) and continues to pass.