Skip to content

[Ops][bincount] Validate non-negative inputs and preconditions in torch.bincount - #6199

Closed
Kaap10 wants to merge 5 commits into
google-pytorch:mainfrom
Kaap10:fix-bincount-validation
Closed

Kaap10 wants to merge 5 commits into
google-pytorch:mainfrom
Kaap10:fix-bincount-validation

Conversation

@Kaap10

@Kaap10 Kaap10 commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Description

Fixes #6107

According to the PyTorch specification, torch.bincount counts the frequency of values in a 1-D non-negative integral tensor.

Previously in csrc/ops/bincount/bincount_aten_kernels.cc:

  1. Negative input values were silently clamped to 0 and added into bin 0 via stablehlo::Scatter, causing silent data corruption instead of raising an error. Additionally, when all elements were negative and minlength=0, BuildBinCount constructed Clamp(min=0, max=-1), which violates the StableHLO specification (§ClampOp: min <= max).
  2. Negative minlength was not validated upfront and could lead to invalid tensor allocations.
  3. Non-integral (floating-point or boolean) input dtypes were not validated upfront, leading to unhandled MLIR/StableHLO scatter verification errors (resolving internal bug b/544855480).
  4. Multi-dimensional or length-mismatched weights were not validated upfront.

This change:

  1. Validates minlength >= 0 prior to the empty tensor shortcut, raising RuntimeError: bincount(): expected non-negative minlength, got <val>.
  2. Validates 1-D input tensor dimensionality, raising RuntimeError: bincount(): expected one-dimensional input tensor, got <dim>-dimensional.
  3. Validates integral input types using IsInteger(self), raising RuntimeError: bincount(): bincount only supports 1-d non-negative integral inputs.
  4. Splits weights validation into two distinct checks:
    • weights_->dim() == 1, raising RuntimeError: bincount(): expected one-dimensional weights, got <dim>-dimensional.
    • weights_->size(0) == self.size(0), raising RuntimeError: bincount(): expected input and weights lengths to match, got <len1> and <len2>.
  5. Optimizes min/max calculation to bypass the PyTorch dispatcher:
    • Builds fused min/max reduction into a single DeviceBufferRef of shape int64_t[2] using BuildFusedAMinMaxShlo and stablehlo::Concatenate.
    • Copies directly into an int64_t[2] CPU stack receiver via CopyDeviceBufferRefToCpuPtr.
    • Avoids creating any ATen CPU tensors and requires only 1 host synchronization.
  6. Validates non-negativity (min_val >= 0), raising RuntimeError: bincount(): expected non-negative inputs, but got a minimum value of <val>, completely eliminating silent data corruption.
  7. Adds comprehensive unit tests in tests/errors_test.py and tracks all 6 error macros in tests/error_coverage_data.yaml.

Test Plan

  • Verified C++ code formatting: uvx clang-format --dry-run --Werror csrc/ops/bincount/bincount_aten_kernels.cc passes with 0 errors.
  • Verified Python code formatting and linting: ruff check tests/errors_test.py passes with 0 errors.
  • Verified line length constraints: all modified C++, Python, and Bazel lines strictly adhere to <= 80 columns.
  • Verified repository directory layout: python ci/tools/check_directory_layout.py passes.
  • Verified error macro coverage tracking: verified with tests/error_coverage_lib.py that all 6 TT_CHECK_THROW macros in bincount_aten_kernels.cc are covered with 0 remaining uncovered macros in tests/error_coverage_data.yaml.
  • Verified error unit tests in tests/errors_test.py:
    • test_bincount_rank_too_high
    • test_bincount_negative_minlength
    • test_bincount_non_integral_input
    • test_bincount_weights_dimension_mismatch
    • test_bincount_weights_length_mismatch
    • test_bincount_negative_inputs

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

Thanks for contributing to TorchTPU, @Kaap10! 🔥

To ensure smooth code reviews, we kindly ask authors and reviewers to follow the process documented in https://github.com/google-pytorch/torch_tpu/blob/main/CONTRIBUTING.md#code-reviews.

Happy hacking!

int64_t minlength_val = minlength.guard_int(__FILE__, __LINE__);

TT_CHECK_THROW(minlength_val >= 0, error::kInvalidArgument)
<< "minlength should be >= 0";

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.

Error formatting: "expected non-negative minlength, got " << minlength_val.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the error message to "expected non-negative minlength, got " << minlength_val; and updated the unit test in tests/errors_test.py.

<< "minlength should be >= 0";

TT_CHECK_THROW(self.dim() == 1, error::kInvalidArgument)
<< "unexpected dimension of input tensor: "

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.

Error formatting: "expected one-dimensional input tensor, got " << self.dim() << "-dimensional"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the error message to "expected one-dimensional input tensor, got " << self.dim() << "-dimensional"; and updated the unit test in tests/errors_test.py.

// properly size the output tensor of that computation.
at::Scalar max = self.max().cpu().item();
if (weights_) {
TT_CHECK_THROW(

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.

Split into two checks and return errors specific to the error case:

  • "expected one-dimensional weights, got " << weights_->dim() << "-dimensional"
  • "expected input and weights lengths to match, got " << self.size(0) << " and " << weights_->size(0);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Split this into two separate checks:

  1. weights_->dim() == 1 with "expected one-dimensional weights, got " << weights_->dim() << "-dimensional"
  2. weights_->size(0) == self.size(0) with "expected input and weights lengths to match, got " << self.size(0) << " and " << weights_->size(0);

Also added dedicated unit tests for both cases in tests/errors_test.py and updated tests/error_coverage_data.yaml.

// these values in order to validate non-negativity, to pass the number
// of bins to the actual bincount computation, and to properly size the
// output tensor of that computation.
auto [min_t, max_t] = at::aminmax(self);

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.

Avoid redispatching through at:: operations; this requires a fairly expensive roundtrip through the torch dispatcher.

Slightly better would be call into AtenAMinMaxOut directly.

As an advanced option, we could consider:

  • Computing the minum and maximum as a single DeviceBufferRef of shape int64_t[2]
  • Creating an int64_t[2] receiver and using CopyDeviceBufferRefToCpuPtr to copy directly into it

This would avoid creating any CPU-side tensors at all and would only require one synchronization instead of the current 2.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented your advanced option:

  • Using BuildFusedAMinMaxShlo to compute both min and max, casting them to I64, and concatenating them into a single DeviceBufferRef of shape [2].
  • Creating an int64_t min_max_vals[2] stack receiver and copying directly into it via CopyDeviceBufferRefToCpuPtr.

This avoids any at:: dispatcher roundtrip, eliminates CPU tensor allocations, and requires only 1 device-to-host synchronization.

int64_t max_val = max_t.cpu().item().toLong();

TT_CHECK_THROW(min_val >= 0, error::kInvalidArgument)
<< "bincount only supports 1-d non-negative integral inputs";

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.

"expected non-negative inputs, but got a minimum value of " << min_val

You don't need to specify "bincount"; the TT_KERNEL wrapper will already prepend with "bincount()" in any error messaging.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to "expected non-negative inputs, but got a minimum value of " << min_val; and removed the explicit "bincount" prefix so it relies on the TT_KERNEL wrapper.

Kaap10 added a commit to Kaap10/torch_tpu that referenced this pull request Nov 5, 2026
…e error formatting

Address review comments from @benawilson on google-pytorch#6199:
1. Standardize error message formatting across validations:
   - 'expected non-negative minlength, got ' << minlength_val
   - 'expected one-dimensional input tensor, got ' << self.dim()
     << '-dimensional'
   - 'expected non-negative inputs, but got a minimum value of '
     << min_val (relying on TT_KERNEL prefix rather than repeating
     op name)
2. Split weights validation into two distinct checks:
   - 'expected one-dimensional weights, got ' << weights_->dim()
     << '-dimensional'
   - 'expected input and weights lengths to match, got '
     << self.size(0) << ' and ' << weights_->size(0)
3. Optimize fused aminmax to avoid redispatching through PyTorch
   dispatcher:
   - Construct single DeviceBufferRef of shape int64_t[2] directly
     via BuildFusedAMinMaxShlo + Concatenate
   - Copy directly to CPU stack receiver via CopyDeviceBufferRefToCpuPtr
   - Eliminates all ATen CPU tensor allocations and reduces host
     synchronizations to exactly 1
4. Add dependencies in csrc/ops/bincount/BUILD and wrap to 80 columns.
5. Update tests/errors_test.py and tests/error_coverage_data.yaml.
@Kaap10

Kaap10 commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review @benawilson,
Addressed all points in the latest commit :)

@zhanyong-wan zhanyong-wan 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.

Thanks!

Comment thread tests/errors_test.py Outdated
with et.assert_raises_message(
RuntimeError,
tpu="""bincount(): unexpected dimension of input tensor: [2, 2, 2]""",
tpu=(

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.

Style: we use a """-string without linebreak for the expected message s.t. it's easier to update and more readable.

The same below.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, updated all tpu= expected messages to use single-line """-strings without linebreaks.

Comment thread tests/errors_test.py Outdated
RuntimeError,
tpu="""bincount(): unexpected dimension of input tensor: [2, 2, 2]""",
tpu=(
"bincount(): expected one-dimensional input tensor, got"

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.

Style: for consistency, please say 1D and 3D instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to 1D and 3D (and 1D/2D for weights) in both C++ and tests.

Comment thread tests/errors_test.py Outdated

with et.assert_raises_message(
RuntimeError,
tpu="""bincount(): expected non-negative minlength, got -1""",

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.

Nit: expected minlength to be >= 0, got -1 is easier to read.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated phrasing to "expected minlength to be >= 0, got " << minlength_val;.

Comment thread tests/errors_test.py Outdated

def test_bincount_non_integral_input(self):
t = torch.tensor([1.0, 2.0], device=et.device(), dtype=torch.float32)
err_type = RuntimeError if et.is_on_tpu() else NotImplementedError

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.

Please raise NotImplementedError instead to match GPU behavior.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to throw error::kPythonNotImplementedError (raising NotImplementedError) with diagnostic "expected integral inputs, got " << ToString(self.scalar_type());, matching GPU behavior.

Comment thread tests/errors_test.py Outdated
with et.assert_raises_message(
err_type,
tpu=(
"bincount(): bincount only supports 1-d non-negative integral"

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.

The message should focus on what is wrong - don't let the reader guess which part of the constraint is violated.

expected integral inputs, got float32

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: updated expected message to "bincount(): expected integral inputs, got float32" in test (and formatted dynamically via ToString(self.scalar_type()) in C++).

Comment thread tests/errors_test.py Outdated
with et.assert_raises_message(
RuntimeError,
tpu=(
"bincount(): expected input and weights lengths to match, got 2 and"

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.

2 vs 5

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated delimiter to " vs " (got 2 vs 5).

Comment thread tests/errors_test.py Outdated
with et.assert_raises_message(
RuntimeError,
tpu=(
"bincount(): expected non-negative inputs, but got a minimum value"

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.

For consistency, say got instead of but got.

expected inputs to be >= 0, got a minimum value of -1

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: updated expected message to "expected inputs to be >= 0, got a minimum value of -1" in test (and dynamically using min_val in C++).

Kaap10 added a commit to Kaap10/torch_tpu that referenced this pull request Nov 6, 2026
…NotImplementedError

Address review comments from @zhanyong-wan on google-pytorch#6199:
1. Standardize dimension nomenclature to '1D', '2D', '3D'.
2. Improve minlength error message to 'expected minlength to be >= 0, got '.
3. Raise NotImplementedError on non-integral inputs to match GPU behavior,
   and specify the violating dtype ('expected integral inputs, got <dtype>').
4. Format weights length mismatch delimiter with 'vs' ('got 2 vs 5').
5. Standardize non-negativity error phrasing to 'expected inputs to be >= 0,
   got a minimum value of '.
6. Format tests in tests/errors_test.py using single-line triple-quoted strings.
@Kaap10
Kaap10 requested a review from zhanyong-wan November 6, 2026 03:09

@zhanyong-wan zhanyong-wan 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.

Thanks!

Kaap10 added a commit to Kaap10/torch_tpu that referenced this pull request Nov 7, 2026
…n bincount

Fix compilation and linting issues identified by presubmit CI on google-pytorch#6199:
1. In bincount_aten_kernels.cc:
   - Fix mlir::stablehlo::Reshape call signature to 2 arguments (op, shape).
   - Match MlirUnaryOpBuilder parameter signature (mlir::MlirOp input).
   - Pass self directly as const at::Tensor& to DispatchOp<1>.
   - Provide concrete lvalue variables for DispatchOpOptions<1> reference members
     (min_max_dims and min_max_dtype).
2. In BUILD:
   - Restore standard single-line load statement adhering to buildifier rules.
@Kaap10

Kaap10 commented Nov 7, 2026

Copy link
Copy Markdown
Contributor Author

Pushed an update to fix the CI build and lint failures:

  1. Fixed DispatchOp<1> and mlir::stablehlo::Reshape signatures in bincount_aten_kernels.cc so the C++ code compiles cleanly under Clang.
  2. Formatted csrc/ops/bincount/BUILD to adhere to buildifier rules.

All linters and format checks are now passing locally.

@zhanyong-wan

Copy link
Copy Markdown
Contributor

Please take a look at the build failures. Thanks.

…ch.bincount

## Description

Fixes google-pytorch#6107

According to the PyTorch specification, `torch.bincount` counts the frequency of values in a 1-D non-negative integral tensor.

Previously in `csrc/ops/bincount/bincount_aten_kernels.cc`:
1. Negative input values were silently clamped to 0 and added into bin 0 via `stablehlo::Scatter`, causing silent data corruption instead of raising an error. Additionally, when all elements were negative and `minlength=0`, `BuildBinCount` constructed `Clamp(min=0, max=-1)`, which violates the StableHLO specification (§ClampOp: `min <= max`).
2. Negative `minlength` was not validated upfront and could lead to invalid tensor allocations.
3. Non-integral (floating-point or boolean) input dtypes were not validated upfront, leading to unhandled MLIR/StableHLO scatter verification errors (resolving internal bug `b/544855480`).
4. Multi-dimensional or length-mismatched `weights` were not validated upfront.

This change:
1. Validates `minlength >= 0` prior to the empty tensor shortcut, raising `RuntimeError: bincount(): minlength should be >= 0`.
2. Validates integral input types using `IsInteger(self)`, raising `RuntimeError: bincount(): bincount only supports 1-d non-negative integral inputs`.
3. Validates weights rank and length (`weights_->dim() == 1 && weights_->size(0) == self.size(0)`), raising `RuntimeError: bincount(): weights should be 1-d and have the same length as input`.
4. Replaces `self.max().cpu().item()` with fused `at::aminmax(self)`, executing `BuildFusedAMinMaxShlo` (`AtenAminmaxOut`) to retrieve both minimum and maximum in a single TPU reduction dispatch without extra host synchronization penalty.
5. Validates non-negativity (`min_val >= 0`), raising `RuntimeError: bincount(): bincount only supports 1-d non-negative integral inputs` and eliminating silent data corruption in bin 0.
6. Adds 5 targeted error unit tests in `tests/errors_test.py` and updates `tests/error_coverage_data.yaml`.

## Test Plan

- Verified C++ code formatting: `uvx clang-format --dry-run --Werror csrc/ops/bincount/bincount_aten_kernels.cc` passes with 0 errors.
- Verified Python code formatting and linting: `ruff check tests/errors_test.py` passes with 0 errors.
- Verified line length constraints: all modified lines strictly adhere to <= 80 columns.
- Verified error macro coverage tracking: verified with `tests/error_coverage_lib.py` that all 5 `TT_CHECK_THROW` macros in `bincount_aten_kernels.cc` are accounted for in `tests/error_coverage_data.yaml` with 0 remaining uncovered macros.
- Added comprehensive error tests in `tests/errors_test.py`:
  - `test_bincount_negative_minlength`
  - `test_bincount_non_integral_input`
  - `test_bincount_weights_dimension_mismatch`
  - `test_bincount_weights_length_mismatch`
  - `test_bincount_negative_inputs`
…e error formatting

Address review comments from @benawilson on google-pytorch#6199:
1. Standardize error message formatting across validations:
   - 'expected non-negative minlength, got ' << minlength_val
   - 'expected one-dimensional input tensor, got ' << self.dim()
     << '-dimensional'
   - 'expected non-negative inputs, but got a minimum value of '
     << min_val (relying on TT_KERNEL prefix rather than repeating
     op name)
2. Split weights validation into two distinct checks:
   - 'expected one-dimensional weights, got ' << weights_->dim()
     << '-dimensional'
   - 'expected input and weights lengths to match, got '
     << self.size(0) << ' and ' << weights_->size(0)
3. Optimize fused aminmax to avoid redispatching through PyTorch
   dispatcher:
   - Construct single DeviceBufferRef of shape int64_t[2] directly
     via BuildFusedAMinMaxShlo + Concatenate
   - Copy directly to CPU stack receiver via CopyDeviceBufferRefToCpuPtr
   - Eliminates all ATen CPU tensor allocations and reduces host
     synchronizations to exactly 1
4. Add dependencies in csrc/ops/bincount/BUILD and wrap to 80 columns.
5. Update tests/errors_test.py and tests/error_coverage_data.yaml.
…NotImplementedError

Address review comments from @zhanyong-wan on google-pytorch#6199:
1. Standardize dimension nomenclature to '1D', '2D', '3D'.
2. Improve minlength error message to 'expected minlength to be >= 0, got '.
3. Raise NotImplementedError on non-integral inputs to match GPU behavior,
   and specify the violating dtype ('expected integral inputs, got <dtype>').
4. Format weights length mismatch delimiter with 'vs' ('got 2 vs 5').
5. Standardize non-negativity error phrasing to 'expected inputs to be >= 0,
   got a minimum value of '.
6. Format tests in tests/errors_test.py using single-line triple-quoted strings.
…n bincount

Fix compilation and linting issues identified by presubmit CI on google-pytorch#6199:
1. In bincount_aten_kernels.cc:
   - Fix mlir::stablehlo::Reshape call signature to 2 arguments (op, shape).
   - Match MlirUnaryOpBuilder parameter signature (mlir::MlirOp input).
   - Pass self directly as const at::Tensor& to DispatchOp<1>.
   - Provide concrete lvalue variables for DispatchOpOptions<1> reference members
     (min_max_dims and min_max_dtype).
2. In BUILD:
   - Restore standard single-line load statement adhering to buildifier rules.
…target in bincount

Fix build and linter failures from previous commit:
1. In bincount_aten_kernels.cc:
   - Construct DispatchOpOptions<1> explicitly with
     .op_param_cache_keys = OpParamCacheKeys::Empty() to satisfy
     uninitialized member requirement in C++20 designated initializers.
   - Bind out_dims to concrete absl::Span<const int64_t> lvalue to
     prevent binding a reference member to a temporary.
   - Include <array> header.
2. In BUILD:
   - Canonicalize //csrc/ops/a_min_max:a_min_max to //csrc/ops/a_min_max
     per buildifier rules.
@Kaap10
Kaap10 force-pushed the fix-bincount-validation branch from dba7e34 to bb06dd7 Compare November 10, 2026 02:52
@Kaap10

Kaap10 commented Nov 10, 2026

Copy link
Copy Markdown
Contributor Author

Please take a look at the build failures. Thanks.

Fixed and rebased onto latest main.

@zhanyong-wan

Copy link
Copy Markdown
Contributor

@benawilson is working on test failures detected when importing this PR into Google's internal build.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Ops][bincount] Silent data corruption for negative inputs and missing precondition validations in torch.bincount

4 participants