Skip to content

Fix Where discarding the sign of a selected -0.0 - #32192

Open
Kevin Xu (keevin-xu) wants to merge 2 commits into
microsoft:mainfrom
keevin-xu:fix-where-signed-zero
Open

Fix Where discarding the sign of a selected -0.0#32192
Kevin Xu (keevin-xu) wants to merge 2 commits into
microsoft:mainfrom
keevin-xu:fix-where-signed-zero

Conversation

@keevin-xu

Copy link
Copy Markdown

Description

Replaces the two != T{} comparisons in Where's merge step with a WasSelected helper that also
admits negative zero, and adds four regression tests to WhereOpTest.

Where<T>::Compute builds an X_selection and a Y_selection, each holding the default T{}
where the element was not selected, then merges them with
(X_selection != default value) ? X_selection : Y_selection. For floats the default is +0.0, and
-0.0 == 0.0. So a -0.0 selected from X compares equal to the default, the merge treats it as
unselected, and takes Y_selection, which holds +0.0 at that position. At equal shapes
Y_selection is the untested fall-through of that merge, which is why a -0.0 from Y survives
there but not when Y is the broadcast scalar operand of MergeScalarAndVector.

A negative zero can only be present in a selection tensor if it was selected, since the default has
its sign bit clear, so admitting it is sufficient. Selecting +0.0 from X still falls through to
Y_selection, which holds +0.0 at that position, so nothing else changes.

For the CPU kernel the affected registered floating-point types are float and double.
MLFloat16 and BFloat16 are present in the registration block but commented out, and the
std::string specialization uses a separate overload where the empty string is the default and is
indistinguishable between the two selection arms, so it is unaffected.

OpTester compares floating-point outputs numerically and -0.0f == 0.0f, so a lost sign is
invisible to it; the new tests check the sign bit through SetCustomOutputVerifier, as in #31477.
Three of the four fail before this change and all pass after, with onnxruntime_provider_test green
on macOS arm64 (5553 passed, 0 failed).

Motivation and Context

Where is elementwise selection rather than arithmetic, so the selected element should be
preserved, and +0.0 and -0.0 are distinct IEEE-754 values with distinct bit patterns.
onnx.reference preserves the negative zero in both cases; ONNX Runtime does not.

Fixes #32191.

Where merges its X_selection and Y_selection with
(X_selection != default value) ? X_selection : Y_selection, using
"differs from the default" as a proxy for "was selected". For floats the
default is +0.0 and -0.0 != 0.0 is false, so a genuinely selected negative
zero was discarded and the other operand's default +0.0 used instead.

A negative zero can only be present in a selection tensor if it was
selected, since the default has its sign bit clear, so admit it explicitly.
MSG
Copilot AI balanced review requested due to automatic review settings August 20, 2026 23:24
@azure-pipelines

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

@keevin-xu

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

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.

Pull request overview

Fixes CPU Where so selected negative-zero values retain their sign.

Changes:

  • Adds signed-zero-aware selection detection.
  • Adds regression tests for equal-shape and broadcast paths.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
onnxruntime/core/providers/cpu/tensor/where_op.cc Preserves selected negative zero during merging.
onnxruntime/test/providers/cpu/tensor/where_op_test.cc Adds float signed-zero regression coverage.

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

// Where is selection, so a selected -0.0 must come back as -0.0. OpTester compares floating
// point outputs numerically and -0.0f == 0.0f, so a lost sign is invisible to it; check the
// sign bit directly instead.
void ExpectSignBits(const std::vector<OrtValue>& fetches, const std::vector<float>& expected) {
The fix is templated and double is a registered Where kernel, but the
tests only exercised float. Template the verifier and the four cases and
instantiate each for float and double, matching how WhereBasicNumericTest
and WhereBroadcastTest already cover both.

On the unpatched kernel each of the three affected cases now reports two
sign-bit failures, one per instantiation, confirming the double path is
exercised rather than shadowed by the float call.
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.

Where returns +0.0 for a selected -0.0, on both the X and Y branches (CPU EP)

2 participants