Skip to content

Fix XLA DenseBincount negative input validation for runtime tensors - #122706

Merged
copybara-service[bot] merged 1 commit into
tensorflow:masterfrom
davidnichols-ops:fix-bincount-xla-runtime-negatives
Jul 27, 2026
Merged

copybara-service[bot] merged 1 commit into
tensorflow:masterfrom
davidnichols-ops:fix-bincount-xla-runtime-negatives

Conversation

@davidnichols-ops

@davidnichols-ops davidnichols-ops commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

PR #121118 added compile-time constant validation for negative inputs in the XLA DenseBincount kernel. However, it only handles the case where the input is a compile-time constant (e.g., tf.constant([0, -1, 2])). For runtime (non-constant) tensors — the common case in @tf.function(jit_compile=True) where inputs come from placeholders, variables, or upstream ops — negative values are still passed directly to xla::Scatter, silently producing wrong results instead of raising InvalidArgumentError as eager mode does.

This PR fixes the runtime case that #121118 left unfixed.

Approach

For runtime (non-constant) inputs, since XLA cannot raise runtime errors from within a compiled computation, we:

  1. Build a mask nonneg_mask = xla::Ge(input, 0) — true where input >= 0
  2. Set negative indices to output_size (out of bounds) via xla::Select so that XLA Scatter silently ignores them (verified: XLA Scatter drops out-of-bounds updates)

Using OOB indices instead of clamping to 0 avoids a subtle bug with binary_output=True: when scatter assigns (rather than adds), clamping negative values to index 0 would overwrite legitimate bin 0 values with 0. OOB indices are simply dropped by XLA Scatter, preserving correct results.

Note: An earlier revision of this PR also zeroed the corresponding updates via xla::Mul(updates, ConvertElementType(mask, dtype)) as a belt-and-suspenders measure. This was removed in commit 3560814 per review feedback — it is redundant because XLA Scatter already drops out-of-bounds updates, and zeroing could actually make a hypothetical backend OOB-clamping bug worse for ScatterAssign (assigning 0 over valid bin-0 data). The OOB-index approach alone is sufficient and safer.

Compile-time constant inputs

The existing validation from #121118 is preserved unchanged — OP_REQUIRES raises InvalidArgumentError at compile time for constant negative inputs.

Test plan

  • testNegativeInputRuntimeIgnored: 1D runtime input with negatives → negative values ignored, correct counts for non-negative values
  • testNegativeInputRuntimeIgnoredBinary: binary_output=True with runtime negatives → negative values ignored, no bin 0 overwrite
  • testNegativeInputRuntime2d: 2D runtime input with negatives → per-row bincount correct, negatives ignored
python tensorflow/compiler/tests/bincount_op_test.py

Closes #117960

@google-ml-butler google-ml-butler Bot added the size:M CL Change Size: Medium label Jul 6, 2026
@google-cla

google-cla Bot commented Jul 6, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

Gemini encountered an error creating the review. You can try again by commenting /gemini review.

@davidnichols-ops

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request adds support for handling runtime negative inputs in the XLA dense_bincount operator by mapping negative indices to an out-of-bounds value (output_size) so they are silently ignored by XLA Scatter, along with adding corresponding unit tests. The review feedback points out that also zeroing out the updates for these negative values is redundant and introduces unnecessary performance overhead, suggesting the removal of these redundant operations and updating the comments accordingly.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@keerthanakadiri
keerthanakadiri requested a review from a team July 7, 2026 10:57
@google-ml-butler google-ml-butler Bot added the awaiting review Pull request awaiting review label Jul 7, 2026
@github-project-automation github-project-automation Bot moved this to Assigned Reviewer in PR Queue Jul 7, 2026
@keerthanakadiri keerthanakadiri added the prtype:bugfix PR to fix a bug label Jul 7, 2026
@davidnichols-ops

Copy link
Copy Markdown
Contributor Author

Hi @gbaned, gentle nudge on this one. The CLA check is passing and the branch is mergeable with no conflicts. This fixes a real bug where DenseBincount silently produces wrong results for runtime (non-constant) tensors under jit_compile=True — the existing validation from PR #121118 only covers compile-time constants. Happy to make any changes if needed.

@dmiltr3 dmiltr3 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.

Correctness & Safety

The approach of using output_size (Out of Bounds - OOB) instead of clamping to 0 is excellent. It correctly avoids overwriting valid data (e.g., bin 0) in cases where binary_output=True (Scatter Assign semantics). Since XLA Scatter drops OOB updates, setting negative indices to a positive OOB value like output_size is a standard and robust way to ignore them.

Redundancy in Updates Zeroing

The PR description notes step 3 (zeroing updates) as a "belt-and-suspenders safety measure".

  • If Scatter strictly adheres to XLA semantics and drops OOB indices (which includes output_size), zeroing updates is redundant because the update value is never applied.
  • Zeroing updates introduces minor dynamic overhead (Mul, ConvertElementType).
  • Caution: If there were a backend bug where OOB indices were accidentally clamped or routed to a valid bin (e.g., bin 0), zeroing out updates could actually make the bug worse for ScatterAssign (Assign 0 overwriting valid data) than leaving it as-is (Assign 1 or weight).
  • Recommendation: Consider removing the updates zeroing step if performance is critical or if you are confident in standard OOB dropping behavior. The explicit conversion of negative indices to positive OOB (output_size) is sufficient and safer than leaving them negative (which might cause UB or buffer overflow on non-compliant backends).

Consistent Positive OOB

Using output_size is preferred over relying on negative indices being treated as OOB, as some backends or low-level arithmetic might handle signed negative indices inconsistently or interpret them as huge positive indices wrapping around.


Unit Tests

To verify this behavior, the following tests can be added to standard suite (e.g., bincount_op_test.py). These tests verify that runtime negative inputs are ignored gracefully.

  def testNegativeInputRuntimeIgnored(self):
    with self.session():
      with self.test_scope():

        @def_function.function(jit_compile=True)
        def f(arr):
          return gen_math_ops.dense_bincount(
              input=arr, size=3, weights=[]
          )

        # Pass as argument to make it a runtime tensor
        arr = array_ops.constant([0, -1, 2])
        result = f(arr)
        # Expected: 0 -> bin 0, 2 -> bin 2. -1 ignored.
        self.assertAllEqual([1, 0, 1], result)

  def testNegativeInputRuntimeIgnoredBinary(self):
    with self.session():
      with self.test_scope():

        @def_function.function(jit_compile=True)
        def f(arr):
          return gen_math_ops.dense_bincount(
              input=arr, size=3, weights=[], binary_output=True
          )

        arr = array_ops.constant([0, -1, 2])
        result = f(arr)
        self.assertAllEqual([1, 0, 1], result)

  def testNegativeInputRuntime2d(self):
    with self.session():
      with self.test_scope():

        @def_function.function(jit_compile=True)
        def f(arr):
          return gen_math_ops.dense_bincount(
              input=arr, size=3, weights=[]
          )

        # 2D input: [[0, -1, 2], [1, 2, -1]]
        arr = array_ops.constant([[0, -1, 2], [1, 2, -1]])
        result = f(arr)
        # Expected output shape: [2, 3]
        # Row 0: 0 -> bin 0, 2 -> bin 2. -1 ignored. -> [1, 0, 1]
        # Row 1: 1 -> bin 1, 2 -> bin 2. -1 ignored. -> [0, 1, 1]
        self.assertAllEqual([[1, 0, 1], [0, 1, 1]], result)

@github-project-automation github-project-automation Bot moved this from Assigned Reviewer to Reviewer Requested Changes in PR Queue Jul 15, 2026
@davidnichols-ops

Copy link
Copy Markdown
Contributor Author

Thanks @dmiltr3 for the thorough review. You're right — the updates zeroing is redundant since XLA Scatter drops OOB indices, and it adds unnecessary dynamic overhead. Removed both zeroing blocks (rank-1 and rank-2 paths) in the latest push. The OOB approach (output_size) is sufficient on its own. The unit tests you suggested were already in place from an earlier commit.

@nithyak0204
nithyak0204 requested a review from dmiltr3 July 16, 2026 10:10

@dmiltr3 dmiltr3 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.

We have tested the changes internally and they look good.

Verification Results

  • CPU Tests: Passed.
  • GPU Tests: Passed (verified with CUDA backend).
  • A100 Tests: Passed.

Logic Review

The approach of mapping negative indices in runtime tensors to output_size (out of bounds) using xla::Select successfully prevents corruption and ensures that negative values are ignored, matching the behavior of the non-XLA kernels.

Removing the redundant updates zeroing for negative indices (in your 4th commit) is also correct as XLA Scatter handles out-of-bounds updates silently.

@github-project-automation github-project-automation Bot moved this from Reviewer Requested Changes to Approved by Reviewer in PR Queue Jul 16, 2026
@google-ml-butler google-ml-butler Bot added kokoro:force-run Tests on submitted change ready to pull PR ready for merge process labels Jul 16, 2026
@kokoro-team kokoro-team removed the kokoro:force-run Tests on submitted change label Jul 16, 2026
@davidnichols-ops

davidnichols-ops commented Jul 16, 2026 •

Copy link
Copy Markdown
Contributor Author

Heads up on the build-windows-x86 / build-and-test failure — it's not related to this PR. The Windows pipeline is currently failing across multiple unrelated PRs on master.

Update (Jul 20): the GetPythonAPIMaxIndex link regression noted below has since been fixed on master. The current Windows failure on this PR is a different, narrower issue — a pre-existing flaky test.

Current failure (run 29689318004)

  • Exactly 1 of 1713 test targets fails: //tensorflow/python/framework:convert_to_constants_test
  • Failing case: ConvertVariablesToConstantsV2SessionTest.testDynamicRnn
  • Cause: float32 precision flake — Max absolute difference: 1.7881393e-07 (one ULP) in a legacy dynamic_rnn output array. The build itself completes fine (46,093/47,086 actions).

All bincount targets pass on Windows in the same run:

//tensorflow/core/kernels:bincount_op_test_cpu                  PASSED in 0.4s
//tensorflow/python/kernel_tests/math_ops:bincount_op_test_cpu  PASSED in 13.4s
//tensorflow/python/ops:bincount_ops_test_cpu                   PASSED in 11.4s
//tensorflow/python/ops:sparse_bincount_ops_test_cpu            PASSED in 16.5s
//tensorflow/python/ops/ragged:ragged_bincount_ops_test_cpu     PASSED in 13.7s

Repo-wide reproduction

Every other recent PR I checked also fails build-windows-x86, with at least two distinct root causes (this precision flake, plus a TPU compile error in sparse_core_ops_utils_test.cc on #123627) — neither related to bincount:

PR Windows Failure
#123628 fail —
#123627 fail TPU compile error (sparse_core_ops_utils_test.cc)
#123626 in progress —
#123624 fail —
#123615 fail —

This PR's diff only touches bincount_op.cc and its test.


Original note (Jul 16, since fixed on master): the failure at that time was a master-side link regression:

lld-link: error: undefined symbol: ?GetPythonAPIMaxIndex@tensorflow@@YAHAEBVPythonAPIInfo@1@@Z

GetPythonAPIMaxIndex was defined in tensorflow/python/framework/python_api_parameter_converter.cc (introduced in 85913fffdcf, "Fix various OOB access across TensorFlow") but not declared in the header, so it didn't cross the Windows DLL boundary. This broke __pywrap_tensorflow_64_shared_object.dll and cascaded to ~952 downstream test build failures. Two other unrelated PRs (#123394, #123398) hit the identical error at the time.

@davidnichols-ops

Copy link
Copy Markdown
Contributor Author

@dmiltr3 Following up on the approval — would it be worth adding a weighted-bincount runtime-negative test (non-empty weights with a negative index) to cover the weights branch under the new OOB path? The current tests all pass weights=[]. Happy to add one if you think it's useful, otherwise I'll leave the branch as-is.

@google-ml-butler google-ml-butler Bot removed the ready to pull PR ready for merge process label Jul 19, 2026
@dmiltr3
dmiltr3 self-requested a review July 20, 2026 14:50

@dmiltr3 dmiltr3 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.

Code Quality & Style

Please ensure your code follows standard formatting rules. Running clang-format on your C++ changes (bincount_op.cc) will help maintain consistency.

Implementation Details

Your approach of masking negative indices to output_size (out of bounds) so that XLA Scatter silently ignores them is valid and works correctly for both 1D and 2D inputs.

However, running these tests exposed a couple of issues that were addressed:

  1. DenseBincount Shape Inference: The shape function in tensorflow/core/ops/math_ops.cc was missing a fallback for unknown rank inputs. This caused a crash (Check failed: output(i).IsSet()) during tracing of your new tests. Added a fallback to set output to UnknownShape() for unknown rank inputs.

    // In tensorflow/core/ops/math_ops.cc under REGISTER_OP("DenseBincount")
    // ...
          if (c->Rank(c->input(0)) == 1 || c->Rank(c->input(0)) == 0) {
            c->set_output(0, c->MakeShape({size_val}));
          } else if (c->Rank(c->input(0)) == 2) {
            c->set_output(0, c->MakeShape({c->Dim(c->input(0), 0), size_val}));
    +     } else {
    +       c->set_output(0, c->UnknownShape());
          }
          return absl::OkStatus();
        });
  2. Test Wrappers: In bincount_op_test.py, the new tests required with self.session(): wrappers to avoid test_session not supported errors under XLATestCase.

    # In tensorflow/compiler/tests/bincount_op_test.py
    # ...
      def testNegativeInputRuntimeIgnored(self):
    -    with self.test_scope():
    +    with self.session():
    +      with self.test_scope():
            @def_function.function(
                jit_compile=True,
                input_signature=[tensor_spec.TensorSpec([5], dtypes.int32)])
            def f(arr):
              return gen_math_ops.dense_bincount(
                  input=arr, size=5, weights=[]
              )

    (Please apply this with self.session(): wrapper to all three new tests: testNegativeInputRuntimeIgnored, testNegativeInputRuntimeIgnoredBinary, and testNegativeInputRuntime2d)

Please include these fixes in your GitHub PR to ensure tests pass successfully.

@github-project-automation github-project-automation Bot moved this from Approved by Reviewer to Reviewer Requested Changes in PR Queue Jul 20, 2026
@davidnichols-ops
davidnichols-ops requested a review from dmiltr3 July 20, 2026 16:26
PR #121118 added compile-time constant validation for negative inputs in
the XLA DenseBincount kernel, but only for the case where the input is a
compile-time constant. For runtime (non-constant) tensors -- the common
case in @tf.function(jit_compile=True) where inputs come from
placeholders, variables, or upstream ops -- negative values were passed
directly to xla::Scatter, silently producing wrong results instead of
raising InvalidArgumentError as eager mode does.

Since XLA cannot raise runtime errors from within a compiled computation,
map negative indices to output_size (out of bounds) via xla::Select so
that XLA Scatter silently ignores them. Using OOB indices instead of
clamping to 0 avoids a subtle bug with binary_output=True: when scatter
assigns (rather than adds), clamping negative values to index 0 would
overwrite legitimate bin 0 values with 0. OOB indices are simply dropped
by XLA Scatter, preserving correct results.

The existing compile-time constant validation from #121118 is preserved
unchanged -- OP_REQUIRES raises InvalidArgumentError at compile time for
constant negative inputs.

Also add an UnknownShape() fallback to the DenseBincount shape function
in math_ops.cc for unknown-rank inputs, which previously crashed with
'Check failed: output(i).IsSet()' when tracing the new runtime-negative
tests.

Adds three XLA tests covering runtime negative inputs (1D, 1D with
binary_output, and 2D), all wrapped in with self.session(): per
XLATestCase requirements.

Closes #117960
@davidnichols-ops

Copy link
Copy Markdown
Contributor Author

Thanks for the review. I addressed all requested changes and rebased/squashed into a single clean commit:

4155e484519

Changes:

  • math_ops.cc — Added else { c->set_output(0, c->UnknownShape()); } fallback for unknown-rank inputs during DenseBincount shape inference.
  • Tests — Wrapped the three new runtime-negative tests with self.session() and self.test_scope().
  • Style — Ran clang-format --style=file on bincount_op.cc.

I also ran additional cross-architecture validation in Colab (TF 2.20.0), including weighted-path coverage, since existing in-repo tests primarily exercise weights=[].

Case Input Weights Expected CPU T4 GPU
weighted_1d [0,-1,2,-3] [1.5,99,2.5,88] [1.5,0,2.5,0] ✅ ✅
binary_1d [0,-1,0,-2,3] — [1,0,0,1] ✅ ✅
unweighted_2d [[0,-1,2],[1,-3,3]] — [[1,0,1,0],[0,1,0,1]] ✅ ✅
weighted_2d [[0,-1,2],[1,-3,3]] [[10,20,30],[40,50,60]] [[10,0,30,0],[0,40,0,60]] ✅ ✅
all_negative [-1,-2,-3] — [0,0,0,0] ✅ ✅
weighted_all_negative [-1,-2,-3] [10,20,30] [0,0,0,0] ✅ ✅

The OOB-index behavior correctly drops negative indices and their corresponding weights, so no additional masking logic is required.

This closes the runtime negative-input gap identified in #121118 and resolves #117960.

Ready for merge once CI is green. (The Windows failure appears to be an unrelated master branch issue: .)

@github-project-automation github-project-automation Bot moved this from Reviewer Requested Changes to Approved by Reviewer in PR Queue Jul 20, 2026
@google-ml-butler google-ml-butler Bot added kokoro:force-run Tests on submitted change ready to pull PR ready for merge process labels Jul 20, 2026
@kokoro-team kokoro-team removed the kokoro:force-run Tests on submitted change label Jul 20, 2026
@nithyak0204 nithyak0204 added kokoro:force-run Tests on submitted change and removed ready to pull PR ready for merge process labels Jul 24, 2026
@kokoro-team kokoro-team removed the kokoro:force-run Tests on submitted change label Jul 24, 2026
@nithyak0204 nithyak0204 added the ready to pull PR ready for merge process label Jul 24, 2026
@davidnichols-ops

Copy link
Copy Markdown
Contributor Author

Thanks @dmiltr3 for the thorough reviews and approval. The PR is rebased, squashed to a single commit, and all checks are green (the Windows x86 failure is a known infra issue unrelated to this change). Ready to merge when you have a moment.

copybara-service Bot pushed a commit that referenced this pull request Jul 27, 2026
…ion flag

And its corresponding field in DebugOptions proto. The flag defaults to false and does nothing for now.

FUTURE_COPYBARA_INTEGRATE_REVIEW=#122706 from davidnichols-ops:fix-bincount-xla-runtime-negatives 4155e48
PiperOrigin-RevId: 951298781
copybara-service Bot pushed a commit that referenced this pull request Jul 27, 2026
FUTURE_COPYBARA_INTEGRATE_REVIEW=#122706 from davidnichols-ops:fix-bincount-xla-runtime-negatives 4155e48
PiperOrigin-RevId: 933745241
@copybara-service
copybara-service Bot merged commit 468162d into tensorflow:master Jul 27, 2026
20 checks passed
@github-project-automation github-project-automation Bot moved this from Approved by Reviewer to Merged in PR Queue Jul 27, 2026
@nithyak0204 nithyak0204 removed the awaiting review Pull request awaiting review label Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

prtype:bugfix PR to fix a bug ready to pull PR ready for merge process size:M CL Change Size: Medium

Projects

Status: Merged

Development

Successfully merging this pull request may close these issues.

tf.math.bincount with negative values raises in eager but silently returns an empty tensor with jit_compile=True

6 participants