Skip to content

Order NaN as the largest value in the CPU TopK kernel - #33543

Merged
mirounga merged 1 commit into
microsoft:mainfrom
HuzaifaChaudary:fix/cpu-topk-nan-ordering
Oct 29, 2026
Merged

mirounga merged 1 commit into
microsoft:mainfrom
HuzaifaChaudary:fix/cpu-topk-nan-ordering

Conversation

@HuzaifaChaudary

@HuzaifaChaudary Huzaifa Iftikhar (HuzaifaChaudary) commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Description

the CPU TopK comparators use > / < and == on the values , so with a NaN in the input they are not a strict weak ordering : a NaN is neither greater , smaller nor equal to anything . the result depends on where the NaN sits and which path runs , and std::nth_element / std::sort are handed a comparator they do not support .

input k largest before after
[1, NaN, 3, 2] 2 1 [1, NaN] [NaN, 3]
[1, NaN, 3, 2, NaN, -1, 5, 0] 1 1 5 at 6 NaN at 1

NaN now ranks above every other value and equal values , NaN included , keep the lower index first . that is what the CUDA kernel already does , HybridNaNUsesStablePackedOrdering checks it , so CPU and CUDA agree . integer types compile to the same comparisons as before .

the k == 1 scan keeps its plain comparison , which the compiler vectorizes , and folds a NaN check into the same loop . only a row that holds a NaN is scanned again with the NaN aware comparison . a standalone copy of the loops on 4M floats without NaN , clang -O2 on arm64 :

path before after
k == 1 scan 1.97 ms 1.96 ms
full index sort 324 ms 323 ms

putting the NaN check straight into the k == 1 comparison instead made that loop about 6x slower , which is why it has the second pass .

TopKOperator.NaNIsLargest covers the scan ( k = 1 ) , the heap ( k = 3 ) and nth_element ( k = 6 and 8 ) for largest and smallest , float and double , opset 11 and 24 , on the CPU EP . it fails before the change . onnxruntime_provider_test --gtest_filter='*TopK*:*BeamSearch*:*GreedySearch*:*Sampling*' , 100 tests , passes on macOS arm64 , and lintrunner is clean .

Motivation and Context

found by running single node models through the ONNX reference evaluator and onnxruntime 1.31 side by side . TopK of [1, NaN, 3, 2] with k = 2 gave [1, NaN] here , 1 being the smallest value in the input . the spec does not define a NaN order , so this follows the CUDA EP . no existing issue that I could find .

With a NaN in the input, the CPU comparators were not a strict weak
ordering, so the selected elements were arbitrary: TopK of [1, NaN, 3, 2]
with k = 2 returned [1, NaN]. NaN now ranks above every other value, as it
does in the CUDA kernel, and ties keep the lower index first. The k = 1 scan
keeps its plain comparison and only rescans rows that contain a NaN.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Copilot AI balanced review requested due to automatic review settings October 22, 2026 19:15
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@mirounga
mirounga requested review from mirounga and a balanced review from Copilot October 28, 2026 15:26

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.

🟢 Approval recommended

No active review issues are reported for the CPU TopK NaN-ordering update.

No open issues 🎉

Review effort: Balanced

@mirounga
mirounga merged commit d80d43c into microsoft:main Oct 29, 2026
98 of 99 checks passed
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