Repository navigation
Keep int64 precision in portable div trunc mode and fmod - #24170
Conversation
Values above 2^53 must come back exact from div with rounding_mode="trunc" (Tensor and Scalar) and from fmod (Tensor and Scalar), and fmod by -1 must give 0 for the most negative int64. See pytorch#24169.
div.out_mode and div.Scalar_mode_out called std::trunc on the integer quotient, which converts it to double and back. Integer division already truncates, so trunc now only applies to floating-point types. fmod.Tensor_out and fmod.Scalar_out computed every integer type as double; integer types now use % through utils::integer_fmod, which returns 0 for a divisor of -1 so the most negative value does not overflow. Floating-point types are unchanged. Fixes pytorch#24169
🔗 Helpful Links🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/24170
Note: Links to docs will display an error until the docs builds have been completed. ❌ 1 New FailureAs of commit cea6e90 with merge base 77f5323 ( NEW FAILURE - The following job has failed:
This comment was automatically generated by Dr. CI and updates every 15 minutes. |
|
@claude please review |
|
Claude finished @JakeStevens's task in 3m 14s —— View job Claude finished @JakeStevens's task —— The int64 fix is right, but there's a regression to fix first: integer Detailed reviewPR Review: #24170SummaryThe PR keeps int64 precision in portable Code Quality
Testing
RecommendationRequest Changes The integer |
Like ATen, fmod.Scalar_out and div.Scalar_mode_out cast the scalar to the tensor's integer dtype, so 256 is 0 for an int8 tensor and must fail as a division by zero, while 255 (-1) and 257 (1) give ATen's results. Also cover every integer dtype and negative values in fmod, integer division by zero in both fmod variants, and move the Scalar half of the int64 trunc test into OpDivScalarOutTest.
fmod.Scalar_out and div.Scalar_mode_out checked the scalar for zero as a double and then cast it to the compute dtype, so a scalar such as 256 for an int8 tensor reached a % 0 or a / 0. Check the cast value instead. fmod now uses a fmod_override overload pair, like remainder_override, so fmod.Tensor_out needs only one switch.
|
Pushed two commits:
All 1702 portable kernel tests pass. |
With if constexpr, the float instantiations of the lambda do not use div_by_zero_error, which clang reports as an unused lambda capture under -Werror.
JakeStevens
left a comment
There was a problem hiding this comment.
lets fix the comment and then can merge thanks!
It had split the integer and floating-point floor_divide overloads.
|
@usmanmateen updating branch with main should enable running lint checks. Do update and give us a ping |
|
@nil-is-all thanks, I've merged main into the branch (d4b3559). It merged cleanly and all 1745 portable kernel tests pass. Ready for lint and CI. |
could you try rebasing again? Not sure why it still says branch out of date. |
|
@nil-is-all main had moved 3 commits since then, so I've merged it again (5752ff0) and the branch is 0 commits behind now. I merged rather than rebased so Jake's approval and the review history stay attached; happy to rebase if you'd prefer. Portable kernel tests still pass (1745). If main moves again before the checks run, the "Update branch" button on the PR should also work. |
|
sorry there was a CI failure over the weekend, update one more time and it should kick off properly |
|
@JakeStevens done, merged main again (cea6e90). The portable kernel tests still pass (1785). |
### Summary `remainder.Scalar_out` checked the scalar for zero as a `double` and then cast it to the compute dtype, so a scalar such as 256 for an int8 tensor became 0 and reached `a % 0` (SIGFPE on x86). The check now runs on the cast value, so like ATen, 256 raises a division-by-zero error while 255 and 257 wrap to -1 and 1. Empty inputs skip the check and return an empty tensor, also matching ATen. The integer `remainder_override` computed `INT_MIN % -1`, which overflows. Like ATen's `remainder_kernel`, a divisor of -1 now returns 0. These are the same issues #24170 fixes for `fmod` and `div`, found while reviewing that PR. ### Test plan Added `MostNegativeByMinusOneIsZero` (int32 and int64), `TensorIntegerDivisionByZeroFails`, `ScalarIntegerDivisionByZeroFails`, `ScalarIsCastToIntegerDtype`, `ScalarIsCastToUnsignedDtype`, and `EmptyInputWithZeroScalarDivisor` to `op_remainder_test.cpp`; expected values match eager PyTorch 2.14. Built `portable_kernels_test` with CMake (Release, `EXECUTORCH_BUILD_TESTS=ON`) on macOS arm64: all 1743 tests pass. With the kernel's scalar zero check removed, `ScalarIntegerDivisionByZeroFails`, `ScalarIsCastToIntegerDtype`, and `ScalarIsCastToUnsignedDtype` fail. `MostNegativeByMinusOneIsZero` passes on arm64 either way because `sdiv` does not trap; it guards the x86 crash. This PR was authored with AI assistance (Claude Code), with an additional review pass by Codex.
…h#24349) Summary: The Cadence HiFi and Fusion G3 operators are forks of the portable kernels and carry the int64 precision bug from pytorch#24169, which pytorch#24170 fixes in portable. HiFi and G3 `div_out_mode` (and G3 `div_scalar_mode_out`) called `std::trunc` on the integer quotient, rounding int64 values above 2^53 through `double`. Integer division already truncates, so `trunc` now only runs for floating-point types. HiFi `fmod.Tensor_out` and `fmod.Scalar_out` forced every integer compute type to `Double`. Integers are now computed with `%`, returning 0 for a divisor of -1 like ATen. `fmod.Scalar_out` checks for a zero divisor after casting the scalar to the compute dtype, so 256 for an int8 tensor fails instead of reaching `a % 0`; empty inputs skip the check like ATen. The local `fmod_value` helper can switch to `utils::fmod_override` once pytorch#24170 lands. HiFi `fmod.Tensor_out` also had an inverted NNLib fast-path condition: Float inputs always took the `std::fmod` fallback, and every other dtype with rank > 0 was sent to `xa_nn_elm_fmod_f32xf32_f32`, which read integer data as float. The fast path is removed rather than fixed: the NNLib kernel computes `a - trunc(a / b) * b` in float32, which is not exact fmod (1.0f and 0.1f give 0 instead of ~0.1), so enabling it would change today's Float results. Float behavior is unchanged by this PR. Test Plan: I could not build or run the Xtensa targets locally. All three changed files pass a host `clang++ -std=c++17 -fsyntax-only -Wall` check against stub NNLib headers, which instantiates every dtype in the switches. The integer fmod and divisor logic matches the portable kernels tested in pytorch#24170 and the portable remainder fix. There is no existing HiFi fmod test target to extend; relying on Cadence CI for the build. This PR was authored with AI assistance (Claude Code), with an additional review pass by Codex. Reviewed By: rascani Differential Revision: D129098757 Pulled By: JakeStevens
Summary: The Cadence HiFi and Fusion G3 operators are forks of the portable kernels and carry the int64 precision bug from #24169, which #24170 fixes in portable. HiFi and G3 `div_out_mode` (and G3 `div_scalar_mode_out`) called `std::trunc` on the integer quotient, rounding int64 values above 2^53 through `double`. Integer division already truncates, so `trunc` now only runs for floating-point types. HiFi `fmod.Tensor_out` and `fmod.Scalar_out` forced every integer compute type to `Double`. Integers are now computed with `%`, returning 0 for a divisor of -1 like ATen. `fmod.Scalar_out` checks for a zero divisor after casting the scalar to the compute dtype, so 256 for an int8 tensor fails instead of reaching `a % 0`; empty inputs skip the check like ATen. The local `fmod_value` helper can switch to `utils::fmod_override` once #24170 lands. HiFi `fmod.Tensor_out` also had an inverted NNLib fast-path condition: Float inputs always took the `std::fmod` fallback, and every other dtype with rank > 0 was sent to `xa_nn_elm_fmod_f32xf32_f32`, which read integer data as float. The fast path is removed rather than fixed: the NNLib kernel computes `a - trunc(a / b) * b` in float32, which is not exact fmod (1.0f and 0.1f give 0 instead of ~0.1), so enabling it would change today's Float results. Float behavior is unchanged by this PR. Test Plan: I could not build or run the Xtensa targets locally. All three changed files pass a host `clang++ -std=c++17 -fsyntax-only -Wall` check against stub NNLib headers, which instantiates every dtype in the switches. The integer fmod and divisor logic matches the portable kernels tested in #24170 and the portable remainder fix. There is no existing HiFi fmod test target to extend; relying on Cadence CI for the build. This PR was authored with AI assistance (Claude Code), with an additional review pass by Codex. Reviewed By: rascani Pulled By: JakeStevens Differential Revision: D129098757 Pull Request resolved: #24349
Summary
Fixes #24169.
The portable
div.out_modeanddiv.Scalar_mode_outkernels compute the integer quotient with/and then callstd::truncon it, which converts it todoubleand back. Integer division already truncates, sostd::truncnow only runs for floating-point types. For int64 values above 2^53,div(a, 3, rounding_mode="trunc")gave1537228672809129216instead of1537228672809129302.fmod.Tensor_outandfmod.Scalar_outswitched every integer compute type toDouble. Integer types now use%through a newutils::integer_fmodinmath_util.h, which returns 0 for a divisor of -1 so the most negative value can't overflow. Float types keep the existingstd::fmodpath, including the vectorized one for the Scalar variant.Test plan
Added
OpDivOutTest.TruncModeKeepsInt64Precision(Tensor and Scalar divisor) andOpFmodTest.Int64KeepsFullPrecision(Tensor, Scalar, and a divisor of -1 withINT64_MIN). The expected values come from eager PyTorch.I built
portable_kernels_testwith CMake (EXECUTORCH_BUILD_TESTS=ON, Release) on Linux x86_64 (WSL2) at 5b14a2f. Before this change both new tests fail; after it, the fullportable_kernels_testrun passes (1696 tests).clang-format18.1.3 reports no changes on the changed files.This PR was authored with AI assistance (Claude Code).
cc @nil-is-all @larryliu0820 @manuelcandales @JakeStevens