Skip to content

Opt-in overflow tracking for decimal fixed-point arithmetic - #22356

Closed
Avinash-Raj wants to merge 22 commits into
NVIDIA:mainfrom
Avinash-Raj:avi/decimal-overflow
Closed

Opt-in overflow tracking for decimal fixed-point arithmetic#22356
Avinash-Raj wants to merge 22 commits into
NVIDIA:mainfrom
Avinash-Raj:avi/decimal-overflow

Conversation

@Avinash-Raj

@Avinash-Raj Avinash-Raj commented May 2, 2026

Copy link
Copy Markdown
Contributor

Description

Adds an opt-in path for detecting overflow in decimal fixed-point arithmetic. The default binary_operation / fixed_point code paths are unchanged; callers that want overflow tracking get it through a new set of value-level free functions and a new binary_operation_safe public API, all from the same libcudf build (no CMake flag, no new type id).

The change lands in three layers.

1. Value-level overflow-aware free functions

New header cpp/include/cudf/fixed_point/detail/safe_arithmetic.hpp (namespace numeric::detail) exposes:

  • safe_result<Rep, Rad> — pair of fixed_point value + bool overflow.
  • safe_add / safe_sub / safe_mul / safe_div / safe_mod / safe_pmod / safe_pymod — overflow-checked binary operators on fixed_point<Rep, Rad>. Rescale-induced and op-induced overflows are OR'd into the returned flag.
  • safe_rescaledx.rescaled(s) that surfaces the shift overflow.
  • safe_convert_floating_to_fixed<Fixed>(floating, scale) — overflow-aware floating→decimal conversion built on the conversion primitives below.

The numeric::fixed_point class itself is not modified; these are free functions that take regular fixed_point operands and compose the per-call overflow flags explicitly. Downstream code paths (the binary_operation_safe kernel today, future overflow-aware reduce / groupby aggregations) consume these primitives directly.

2. Column / binaryop level

New public API in cpp/include/cudf/binaryop.hpp:

std::pair<std::unique_ptr<column>, std::unique_ptr<column>>
binary_operation_safe(
  /* scalar | column_view */ lhs,
  /* scalar | column_view */ rhs,
  binary_operator        op,
  data_type              output_type,
  rmm::cuda_stream_view  stream = cudf::get_default_stream(),
  rmm::device_async_resource_ref mr =
    cudf::get_current_device_resource_ref());

Returns a (result, overflow) pair where:

  • result is the decimal arithmetic result column (same semantics as binary_operation).
  • overflow is a BOOL8 column of the same length. Element i is true iff row i is an active (non-null on both sides) row whose arithmetic or rescale to output_type overflowed. Null rows and clean rows hold false. The column has no null mask.

Supported operators: ADD, SUB, MUL, DIV, MOD, PMOD, PYMOD. All three operand shapes (column,column / scalar,column / column,scalar) are supported. The kernel (cpp/src/binaryop/compiled/binary_ops_safe.cu) writes the per-row overflow bit with a plain store (no atomics), one byte per row, coalesced with the result write. Callers that only need a column-wide yes/no can reduce the overflow column with cudf::reduce(overflow, make_any_aggregation(), ...).

3. Overflow-aware float → decimal conversion primitives

The conversion code in cpp/include/cudf/fixed_point/detail/floating_conversion.hpp grew a single bool CheckOverflow = false template parameter (rather than duplicating into *_checked siblings) on:

  • checked_left_shift<CheckOverflow>(value, bit_shift)
  • checked_narrow_cast<To, CheckOverflow>(value)
  • multiply_power10_saturating<T, CheckOverflow>(value, pow10) (also made O(1) by precomputing the overflow threshold)
  • shift_to_decimal_pospow<Rep, FloatingType, CheckOverflow>(...)
  • shift_to_decimal_negpow<Rep, FloatingType, CheckOverflow>(...)
  • convert_floating_to_integral_shifting<Rep, FloatingType, CheckOverflow>(...)
  • convert_floating_to_integral<Rep, CheckOverflow, FloatingType>(...) (top-level entry consumed by safe_convert_floating_to_fixed)

When CheckOverflow = false the generated code is unchanged from main. When true, every primitive returns {value, overflow} with saturating semantics on overflow. The sign-reapply path in convert_floating_to_integral was also fixed to handle the -INT_MIN boundary without invoking signed UB on the unchecked code path.

Motivation

decimal32 / decimal64 / decimal128 silently wrap on overflow today. Downstream engines (velox-cudf, Spark RAPIDS, ANSI-mode pipelines) need a way to detect overflow without rolling their own checked arithmetic or shipping a forked libcudf. This PR:

  • keeps the default unchecked behavior fully unchanged,
  • compiles overflow-aware paths from the same libcudf build (no CMake flag, no opt-in type id),
  • composes at the free-function level so future overflow-aware reductions and groupby aggregations can re-use the same primitives,
  • surfaces overflow per row at the column API so callers can choose between "fail fast" (reduce to any) and "carry NULL / saturate per row".

Tests

New test target SAFE_ARITHMETIC_TEST (cpp/tests/fixed_point/safe_arithmetic_tests.cpp, 50+ tests):

  • safe_* operators on decimal32 / decimal64 / decimal128: happy path + boundary-overflow on each width (AddOverflowSetsFlag, Mul32OverflowSetsFlag, Mul128OverflowSetsFlag, Div{32,64,128}IntMinByNegativeOneOverflows, …).
  • safe_rescaled no-op + shift-overflow.
  • safe_convert_floating_to_fixed for each width: float / double inputs, positive / negative scale, 0.0 / -0.0, positive- and negative-overflow branches, scale-induced overflow.
  • Compile-time static_assert checks that safe_* preserve the operand Rep (don't silently upcast).
  • Caller-composed multi-step overflow propagation.
  • Direct tests of the conversion-overflow primitives in floating_conversion.hpp (new FloatingConversionOverflowTest fixture): checked_left_shift<true>, multiply_power10_saturating<true>, checked_narrow_cast<true>, and the entry point convert_floating_to_integral<Rep, true> — including the INT_MIN-exact boundary that the wrapper-level tests don't reach, and a regression check that the checked path's value matches the unchecked path on non-overflowing inputs.

New BinaryOperationSafeDecimal tests in cpp/tests/binaryop/binop-compiled-fixed_point-test.cpp cover the column-level API: a no-overflow case (asserts an all-false BOOL8 overflow column) and an overflow case (asserts the offending row's slot is true).

@Avinash-Raj
Avinash-Raj requested review from a team as code owners May 2, 2026 07:58
@github-actions github-actions Bot added libcudf Affects libcudf (C++/CUDA) code. CMake CMake build issue labels May 2, 2026
@Avinash-Raj Avinash-Raj changed the title Add optional sticky fixed-point overflow tracking (CUDF_TRACK_FIXED_POINT_OVERFLOW) [Draft] Add optional sticky fixed-point overflow tracking (CUDF_TRACK_FIXED_POINT_OVERFLOW) May 2, 2026
@Avinash-Raj
Avinash-Raj marked this pull request as draft May 2, 2026 07:59
@copy-pr-bot

copy-pr-bot Bot commented May 2, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@Avinash-Raj Avinash-Raj changed the title [Draft] Add optional sticky fixed-point overflow tracking (CUDF_TRACK_FIXED_POINT_OVERFLOW) Add optional sticky fixed-point overflow tracking (CUDF_TRACK_FIXED_POINT_OVERFLOW) May 2, 2026
@bdice

bdice commented May 2, 2026

Copy link
Copy Markdown
Contributor

I would favor adding new operators/functions rather than a compile-time flag — we want to be able to ship one build of cuDF that works universally.

@devavret

devavret commented May 4, 2026

Copy link
Copy Markdown
Contributor

@bdice I'd imagine this is not limited to scalar functions like binaryops and such. We'd also need to detect overflow in aggregations like groupby and reduce. And those would require overloading the existing +,* operators.

What's your opinion on adding a new type maybe called safe_decimal.

Spark should also have a concept of arithmetic exception for overflows. How do they support this with libcudf decimal?

@mattgara

mattgara commented May 4, 2026

Copy link
Copy Markdown

What's your opinion on adding a new type maybe called safe_decimal.

Inline with this thinking, we could extend cudf's fixed_point to implement this:

enum class overflow_tracking { off, on };

template <typename Rep, Radix Rad,
          overflow_tracking Track = overflow_tracking::off>
class fixed_point{ ...

and use if constexpr (Track == overflow_tracking::on) to branch the implementation based on this configuration. Effectively, this should be compile-time semantically equivalent to having a safe_decimal, but with less code duplication.

@ttnghia

ttnghia commented May 4, 2026

Copy link
Copy Markdown
Contributor

This is related to ANSI support that I've recently filed: #21676
We can use some kind of input enum like mentioned above, and custom return type as my suggestion.

@Avinash-Raj

Copy link
Copy Markdown
Contributor Author

Ok, replaced the CMake CUDF_TRACK_FIXED_POINT_OVERFLOW flag with an opt-in overflow_tracking NTTP on numeric::fixed_point and add decimal*_safe / type_id::DECIMAL*_SAFE so velox-cudf gets per-element overflow detection without requiring a separate libcudf build.

@pmattione-nvidia

Copy link
Copy Markdown
Contributor

You might want to put overflow tracking into the floating <--> decimal conversion code as well (include/cudf/fixed_point/detail/floating_conversion.hpp). E.g. there are the guarded_left_shift() and guarded_right_shift() functions, and there are potential overflows in convert_floating_to_integral_shifting(), shift_to_decimal_pospow(), and shift_to_decimal_negpow(). All potential overflow sites are mentioned explicitly in the code comments in these functions.

@Avinash-Raj Avinash-Raj changed the title Add optional sticky fixed-point overflow tracking (CUDF_TRACK_FIXED_POINT_OVERFLOW) Opt-in overflow tracking for decimal fixed-point arithmetic May 11, 2026
@davidwendt

Copy link
Copy Markdown
Contributor

Could this be done without modifying the fixed-point class? The operator overloads are not really required for this to work since the dispatcher logic creates specialized code paths automatically. And somehow the overflow-awareness is communicated through to the appropriate operation/function and would generally require a special code path either way.

It seems the operators could be just free functions and not member functions or member operators. The return type would include an overflow flag along with the result. Similar to how the https://github.com/rapidsai/cudf/blob/main/cpp/include/cudf/fixed_point/detail/floating_conversion.hpp is a separate file handling specific operations for fixed-point.

The new free functions would be called as part of the specialized overflow-aware aggregation types for reduce, groupby, etc.
This would better isolate the specialized operation functions away from the current fixed-point implementations.

@Avinash-Raj

Copy link
Copy Markdown
Contributor Author

@davidwendt I have addressed your comment by adding free functions that return the operation result and has_overflow_occurred as a tuple, without making or reverting any changes to the fixed_point class.

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

Seems the gtests only execute on decimal64.

* @return true if the shift would overflow `Rep`, false otherwise
*/
template <typename Rep, Radix Rad, typename T>
CUDF_HOST_DEVICE inline constexpr bool shift_overflows(T const& val, scale_type const& scale)

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.

Can this function be moved to safe_arithmetic.hpp? Or does it need to be public?
Perhaps move it or create a new header file.

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.

Yes, we could move the shift_overflows function to safe_arithmetic.hpp since that is the only place where it is called.

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! the relevant function gets moved to safe_arithmetic.hpp and also added safe arithmetic tests to deal with decimal64 overflows.

@mhaseeb123

Copy link
Copy Markdown
Contributor

Is this PR and #22261 related or overlapping?

return guarded_left_shift(static_cast<UnsignedRep>(shifting_rep), pow2);
}

/**

@pmattione-nvidia pmattione-nvidia May 18, 2026

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.

duplicating the functionality by adding new high level functions for all of this code is brutal; we shouldn't have to duplicate all of this code logic. It's too easy for one function to get updated but not the other, etc. I would instead add a template parameter bool for whether to check or not. Then that bool can be passed to e.g. checked_narrow_cast() (potentially renaming) and if it's constexpr-false we just do the straight up cast.

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.

In addition I feel the overflow bool should be returned and not passed in.

@Avinash-Raj Avinash-Raj May 21, 2026

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.

Like you suggested, instead of duplicating the high-level functions, I used compile-time branching inside each function based on the CheckOverflow template parameter. For example,

safe_arithmetic.hpp

template <typename Fixed,
          typename Floating,
          CUDF_ENABLE_IF(cuda::std::is_floating_point_v<Floating> &&
                         cudf::is_fixed_point<Fixed>())>
CUDF_HOST_DEVICE inline safe_result<typename Fixed::rep, Fixed::rad>
safe_convert_floating_to_fixed(Floating floating, scale_type scale)
{
  using Rep = typename Fixed::rep;
  if constexpr (Fixed::rad == Radix::BASE_10) {
    auto const [value, overflow] = convert_floating_to_integral<Rep, true>(floating, scale);
    return safe_result<Rep, Fixed::rad>{Fixed{scaled_integer<Rep>{value, scale}}, overflow};
  } else {
    Rep const value = static_cast<Rep>(shift<Rep, Fixed::rad>(floating, scale));
    return safe_result<Rep, Fixed::rad>{Fixed{scaled_integer<Rep>{value, scale}}, false};
  }
}

floating_conversion.hpp

/**
 * @brief Perform floating-point -> integer decimal conversion
 *
 * @tparam Rep The type of integer we are converting to, to store the decimal value
 * @tparam CheckOverflow Whether to return overflow detection with the converted value
 * @tparam FloatingType The type of floating-point object we are converting from
 * @param floating The floating point value to convert
 * @param scale The desired base-10 scale factor: decimal value = returned value * 10^scale
 * @return Integer representation of the floating-point value, or `{value, overflow}` when
 * `CheckOverflow` is true
 */
template <typename Rep,
          bool CheckOverflow = false,
          typename FloatingType,
          CUDF_ENABLE_IF(cuda::std::is_floating_point_v<FloatingType>)>
CUDF_HOST_DEVICE inline auto convert_floating_to_integral(FloatingType const& floating,
                                                          scale_type const& scale)
{
  // Extract components of the floating point number
  using converter        = floating_converter<FloatingType>;
  auto const integer_rep = converter::bit_cast_to_integer(floating);
  if (converter::is_zero(integer_rep)) { return maybe_with_overflow<CheckOverflow>(Rep{0}, false); }

  // Note that the significand here is an unsigned integer with sizeof(FloatingType)
  auto const is_negative                  = converter::get_is_negative(integer_rep);
  auto const [significand, floating_pow2] = converter::get_significand_and_pow2(integer_rep);

  // Add half a bit if truncating to yield expected value, see function for discussion.
  auto const pow10 = static_cast<int>(scale);
  auto const [base2_value, pow2] =
    add_half_if_truncates(floating, significand, floating_pow2, pow10);

  // Apply the powers of 2 and 10 to convert to decimal.
  auto const [magnitude_u, overflow] =
    convert_floating_to_integral_shifting<Rep, FloatingType, CheckOverflow>(
      base2_value, pow10, pow2);

  if constexpr (!CheckOverflow) {
    // Reapply the sign and return
    // NOTE: Cast can overflow!
    auto const signed_magnitude = static_cast<Rep>(magnitude_u);
    return is_negative ? -signed_magnitude : signed_magnitude;
  } else {
    // Reapply sign with saturation on representational overflow.
    using UnsignedRep = cuda::std::make_unsigned_t<Rep>;
    auto const umax   = static_cast<UnsignedRep>(cuda::std::numeric_limits<Rep>::max());

    if (!is_negative) {
      if (magnitude_u > umax) {
        return cuda::std::pair<Rep, bool>{cuda::std::numeric_limits<Rep>::max(), true};
      }
      return cuda::std::pair<Rep, bool>{static_cast<Rep>(magnitude_u), overflow};
    }

    // Negative range has one extra representable value for two's complement.
    // magnitude == max+1 maps to min.
    auto const umin_mag = umax + UnsignedRep{1};
    if (magnitude_u > umin_mag) {
      return cuda::std::pair<Rep, bool>{cuda::std::numeric_limits<Rep>::min(), true};
    }
    if (magnitude_u == umin_mag) {
      return cuda::std::pair<Rep, bool>{cuda::std::numeric_limits<Rep>::min(), overflow};
    }

    return cuda::std::pair<Rep, bool>{static_cast<Rep>(-static_cast<Rep>(magnitude_u)), overflow};
  }
}

I hope this is the right way, correct me if I'm wrong.

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.

My point is we should not have both shift_to_decimal_pospow_checked() and shift_to_decimal_pospow(). It should just be one function with template bool on whether to check or not. Same for all of the others. Duplicating all of this code will just cause maintenance nightmares.

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.

Agreed and it got fixed on floating_conversion.hpp.

@Avinash-Raj Avinash-Raj May 22, 2026

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.

@davidwendt “In addition, I feel the overflow bool should be returned and not passed in.” If my understanding is correct, you don’t want to pass a sticky overflow flag, rmm::device_scalar<std::uint32_t>& overflow_flag, to the high-level safe functions (refer below). Instead, the corresponding overflow value (bool) should be returned as the second output parameter. Is that correct?

template <typename LhsType, typename RhsType>
std::unique_ptr<column> binary_operation_safe(LhsType const& lhs,
                                              RhsType const& rhs,
                                              binary_operator op,
                                              data_type output_type,
                                              rmm::device_scalar<std::uint32_t>& overflow_flag,
                                              rmm::cuda_stream_view stream,
                                              rmm::device_async_resource_ref mr)
{

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.

Yes. Should this not return a bool per row?
I would've expected the result to be 2 columns. One for the fixed-point value and one of bool values.

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.

I have made changes as suggested in your comment: "Now the binary_operation_safe function should return two columns (result, overflow_per_row)."

@davidwendt davidwendt added feature request New feature or request non-breaking Non-breaking change labels May 27, 2026
@Avinash-Raj

Copy link
Copy Markdown
Contributor Author

@davidwendt Updated PR description.

@davidwendt

Copy link
Copy Markdown
Contributor

Great. You have some style issues to resolve: https://results.pre-commit.ci/run/github/90506918/1779899179.fLA-VsUFQTW7brv_xDSJaw

@Avinash-Raj
Avinash-Raj marked this pull request as ready for review May 27, 2026 17:56
@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds overflow-aware fixed-point arithmetic helpers and checked floating-to-fixed conversion primitives; declares and implements binary_operation_safe (returns result + per-row BOOL8 overflow), provides compiled/kernel implementations for DECIMAL32/64/128, and adds extensive unit tests and CMake updates.

Changes

Safe decimal arithmetic and overflow detection

Layer / File(s) Summary
Safe arithmetic data structures and operations
cpp/include/cudf/fixed_point/detail/safe_arithmetic.hpp
New safe_result<Rep,Rad> and overflow-aware fixed_point helpers (safe_rescaled, safe_add, safe_sub, safe_mul, safe_div, safe_mod, safe_pmod, safe_pymod, safe_convert_floating_to_fixed) returning value+overflow.
Floating-point conversion overflow detection
cpp/include/cudf/fixed_point/detail/floating_conversion.hpp
Added checked conversion primitives (checked_left_shift, checked_narrow_cast, multiply_power10_saturating) and threaded overflow reporting through shift_to_decimal_pospow, shift_to_decimal_negpow, convert_floating_to_integral_shifting, and convert_floating_to_integral.
Public binary_operation_safe API
cpp/include/cudf/binaryop.hpp, cpp/src/binaryop/binaryop.cpp
Declared and implemented three binary_operation_safe overloads (scalar/column, column/scalar, column/column) that validate inputs, allocate result and BOOL8 overflow column, and invoke the compiled safe kernel.
Compiled interface and scalar wrappers
cpp/src/binaryop/compiled/binary_ops.hpp, cpp/src/binaryop/compiled/binary_ops.cu
Declared binary_operation_safe overloads and apply_binary_op_safe; implemented scalar→column wrappers and forwarding into the safe kernel with scalar flags and overflow buffer.
CUDA kernel for safe operations
cpp/src/binaryop/compiled/binary_ops_safe.cu
Implements device functors and per-row kernel that executes safe fixed-point ops, rescales outputs, writes payload and per-row overflow flags; dispatcher and apply_binary_op_safe validate and route by decimal width.
Safe arithmetic operation tests
cpp/tests/fixed_point/safe_arithmetic_tests.cpp
Extensive unit tests for safe arithmetic primitives and safe floating-to-fixed conversion across decimal32/64/128 including edge and composability cases.
Floating conversion primitive tests
cpp/tests/fixed_point/safe_arithmetic_tests.cpp
Boundary and saturation tests for checked_left_shift, multiply_power10_saturating, checked_narrow_cast, and checked convert_floating_to_integral.
Binary operation safe decimal tests
cpp/tests/binaryop/binop-compiled-fixed_point-test.cpp
Tests for binary_operation_safe covering multiplication/division overflow detection, empty/null/sliced inputs, large multi-block patterns, and divide-by-zero handling.
Build configuration and test registration
cpp/CMakeLists.txt, cpp/tests/CMakeLists.txt
Added src/binaryop/compiled/binary_ops_safe.cu to cudf sources and registered SAFE_ARITHMETIC_TEST target.
Formatting and copyright updates
cpp/include/cudf/fixed_point/fixed_point.hpp
Minor formatting of debug asserts and updated SPDX year to 2026 with no API change.

🎯 4 (Complex) | ⏱️ ~45 minutes


Suggested reviewers

  • pmattione-nvidia
  • davidwendt
  • vuule
  • bdice
  • ttnghia
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Opt-in overflow tracking for decimal fixed-point arithmetic' accurately and concisely describes the main feature added: a new optional mechanism for detecting overflow in decimal fixed-point operations.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the three-layer implementation (value-level functions, column API, conversion primitives), motivation, and test coverage for the opt-in overflow tracking feature.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (1)
cpp/tests/fixed_point/safe_arithmetic_tests.cpp (1)

301-329: ⚡ Quick win

Move test helper out of anonymous namespace to match test-style rule.

Please keep this helper/static_assert block in the global namespace to align with the project’s test namespace rule.

As per coding guidelines, "cpp/**/*test*.{cu,cpp}: Test code must be in the global namespace, not in custom namespaces."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/tests/fixed_point/safe_arithmetic_tests.cpp` around lines 301 - 329, The
helper function safe_ops_preserve_rep and its three static_asserts must be moved
out of the anonymous namespace into the global namespace; locate the block
declaring template<typename Fixed> constexpr bool safe_ops_preserve_rep() (which
references Rep, Rad, numeric::detail::safe_result and uses
decltype(detail::safe_add/sub/mul/div)) and the following
static_assert(safe_ops_preserve_rep<decimal32/64/128>()) and remove the
surrounding anonymous namespace so the template and asserts live at global scope
to comply with the test namespace rule.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cpp/include/cudf/binaryop.hpp`:
- Around line 226-264: Add full Doxygen for each binary_operation_safe overload
(the scalar-column, column-scalar, and column-column signatures) mirroring the
top-level brief: include `@brief`, an explicit `@param` for lhs, rhs, op,
output_type, stream, and mr (document types: scalar vs column_view where
appropriate), an `@return` describing the pair {result, overflow} and the per-row
BOOL8 overflow semantics, and an `@throw` describing exceptions (e.g., when
operand storage types differ or when overflow/errors occur). Also explicitly
document the implementation constraint that both operands must have the same
underlying storage type (same-storage-type requirement) and note that the
overflow column has no null mask. Ensure the tags and wording are consistent
across all three overload declarations.

In `@cpp/include/cudf/fixed_point/detail/floating_conversion.hpp`:
- Around line 533-535: The unchecked branch of checked_left_shift forwards a
possibly-negative bit_shift to guarded_left_shift, allowing undefined behavior;
fix by adding a guard in the !CheckOverflow branch: if bit_shift < 0 return an
overflow result instead of calling guarded_left_shift (e.g., return {value,
true}), otherwise call guarded_left_shift(value, bit_shift); reference symbols:
checked_left_shift, CheckOverflow, bit_shift, guarded_left_shift.

In `@cpp/include/cudf/fixed_point/detail/safe_arithmetic.hpp`:
- Around line 187-193: safe_mod currently computes `Rep const remainder =
lhs_r.value.value() % rhs_r.value.value();` without guarding a zero divisor;
modify `safe_mod` (and the analogous `safe_pmod`/`safe_pymod` code paths) to
first check whether `rhs_r.value.value()` is zero and short-circuit to a safe
return instead of performing `%`; specifically, if the rescaled RHS is zero,
construct and return a `safe_result<Rep, Rad>` containing a `fixed_point<Rep,
Rad>{ scaled_integer<Rep>{ /* zero remainder */, common_scale } }` (or other
agreed sentinel) and set the overflow flag to `lhs_r.overflow || rhs_r.overflow
|| true` (i.e., mark it as an invalid/overflow case), otherwise proceed to
compute `remainder` with `%` as before; apply the same zero-check and
short-circuit logic in the blocks that reference `lhs_r`, `rhs_r`,
`safe_rescaled`, `safe_result`, `fixed_point`, and `scaled_integer` at the other
locations mentioned (lines around the `safe_pmod`/`safe_pymod` implementations).
- Around line 168-174: The division implementation in safe_div computes quot =
lv / rv even when rv == 0; move the integer division after guarding the zero
divisor: use division_overflow<Rep>(lv, rv) and/or an explicit check for rv == 0
to set op_overflow (or an appropriate error flag) and only perform lv / rv when
rv != 0, then construct fixed_point<Rep, Rad>{scaled_integer<Rep>{quot,
out_scale}} and return safe_result<Rep, Rad>{... , op_overflow}; ensure symbols
referenced are Rep, lv, rv, division_overflow<Rep>, quot, out_scale,
fixed_point, scaled_integer, and safe_result so the zero-divisor is
short-circuited before any division occurs.

In `@cpp/src/binaryop/compiled/binary_ops_safe.cu`:
- Around line 105-118: The code currently reads fixed-width decimal elements via
lhs.element<Rep>(li) and rhs.element<Rep>(ri) even when the row is inactive;
change the flow to short-circuit before any element loads: compute row_active =
lhs.is_valid(li) && rhs.is_valid(ri) first, then if row_active is true load
lhs.element<Rep>(li) and rhs.element<Rep>(ri), perform SafeOp{} and
numeric::detail::safe_rescaled on the result and write d_overflow_per_row[i] and
out.data<Rep>()[i]; if row_active is false avoid calling
lhs.element/rhs.element, set d_overflow_per_row[i] = false and set
out.data<Rep>()[i] to a defined neutral value (e.g., zero) to avoid reading null
payloads.

In `@cpp/tests/binaryop/binop-compiled-fixed_point-test.cpp`:
- Around line 894-940: Add additional unit tests for binary_operation_safe to
cover required edge cases: implement TESTs (e.g.,
BinaryOperationSafeDecimal.mulEmptyInput, .mulNullsKeepFalse, .mulSlicedColumns,
.mulMultiBlockSizes) that call cudf::binary_operation_safe with decimal inputs
and use cudf::binary_operation_fixed_point_output_type to build the output type;
verify for empty inputs that result size is zero and overflow column is empty,
for inputs with nulls that inactive/null rows produce overflow==false, for
sliced column inputs that slicing preserves correct results and overflow flags,
and for large/multi-block sized inputs that overflow flags are correctly set
per-row. Locate behaviour around binary_operation_safe and reuse the pattern
from existing tests (stream/mr creation, CUDF_TEST_EXPECT_COLUMNS_EQUAL /
EXPECT_EQ) when asserting results.

---

Nitpick comments:
In `@cpp/tests/fixed_point/safe_arithmetic_tests.cpp`:
- Around line 301-329: The helper function safe_ops_preserve_rep and its three
static_asserts must be moved out of the anonymous namespace into the global
namespace; locate the block declaring template<typename Fixed> constexpr bool
safe_ops_preserve_rep() (which references Rep, Rad, numeric::detail::safe_result
and uses decltype(detail::safe_add/sub/mul/div)) and the following
static_assert(safe_ops_preserve_rep<decimal32/64/128>()) and remove the
surrounding anonymous namespace so the template and asserts live at global scope
to comply with the test namespace rule.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 73057fd4-1323-41df-8a0e-dea63e4fcded

📥 Commits

Reviewing files that changed from the base of the PR and between a14650d and 2cb1ace.

📒 Files selected for processing (13)
  • cpp/CMakeLists.txt
  • cpp/include/cudf/binaryop.hpp
  • cpp/include/cudf/fixed_point/detail/floating_conversion.hpp
  • cpp/include/cudf/fixed_point/detail/safe_arithmetic.hpp
  • cpp/include/cudf/fixed_point/fixed_point.hpp
  • cpp/include/cudf/utilities/type_dispatcher.hpp
  • cpp/src/binaryop/binaryop.cpp
  • cpp/src/binaryop/compiled/binary_ops.cu
  • cpp/src/binaryop/compiled/binary_ops.hpp
  • cpp/src/binaryop/compiled/binary_ops_safe.cu
  • cpp/tests/CMakeLists.txt
  • cpp/tests/binaryop/binop-compiled-fixed_point-test.cpp
  • cpp/tests/fixed_point/safe_arithmetic_tests.cpp

Comment thread cpp/include/cudf/binaryop.hpp
Comment thread cpp/include/cudf/fixed_point/detail/floating_conversion.hpp
Comment thread cpp/include/cudf/fixed_point/detail/safe_arithmetic.hpp Outdated
Comment thread cpp/include/cudf/fixed_point/detail/safe_arithmetic.hpp Outdated
Comment thread cpp/src/binaryop/compiled/binary_ops_safe.cu Outdated
Comment thread cpp/tests/binaryop/binop-compiled-fixed_point-test.cpp

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cpp/src/binaryop/compiled/binary_ops.cu (1)

386-417: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add Doxygen documentation for new public-facing functions.

The three new binary_operation_safe overloads lack Doxygen documentation. While there is a brief comment on lines 383-385, proper Doxygen-style documentation is required for C++/CUDA code. As per coding guidelines, C++/CUDA code must include proper Doxygen documentation.

📝 Example Doxygen documentation structure
+/**
+ * `@brief` Performs safe binary operation between two columns with overflow detection.
+ *
+ * `@param` out Output mutable column view to store the result
+ * `@param` lhs Left-hand side column view
+ * `@param` rhs Right-hand side column view
+ * `@param` op Binary operator to apply
+ * `@param` d_overflow_per_row Device pointer to bool array for per-row overflow flags
+ * `@param` stream CUDA stream used for device memory operations and kernel launches
+ * `@param` mr Device memory resource used for temporary allocations
+ */
 void binary_operation_safe(mutable_column_view& out,
                            column_view const& lhs,
                            column_view const& rhs,
                            binary_operator op,
                            bool* d_overflow_per_row,
                            rmm::cuda_stream_view stream,
                            rmm::device_async_resource_ref mr)

Similar documentation should be added for the scalar/column and column/scalar overloads.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/src/binaryop/compiled/binary_ops.cu` around lines 386 - 417, Add
Doxygen-style comments for the three public overloads of binary_operation_safe
so they follow project guidelines: document purpose, parameters (out, lhs, rhs,
op, d_overflow_per_row, stream, mr), return/behavior (how overflow is reported
via d_overflow_per_row), and any special cases (scalar vs column overloads and
ownership of temporary column_views from scalar_to_column_view). Place the
Doxygen block immediately above each function signature (the column/column,
scalar/column, and column/scalar overloads) and mirror the wording/format used
by existing documented binary ops in the file for consistency.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@cpp/src/binaryop/compiled/binary_ops.cu`:
- Around line 386-417: Add Doxygen-style comments for the three public overloads
of binary_operation_safe so they follow project guidelines: document purpose,
parameters (out, lhs, rhs, op, d_overflow_per_row, stream, mr), return/behavior
(how overflow is reported via d_overflow_per_row), and any special cases (scalar
vs column overloads and ownership of temporary column_views from
scalar_to_column_view). Place the Doxygen block immediately above each function
signature (the column/column, scalar/column, and column/scalar overloads) and
mirror the wording/format used by existing documented binary ops in the file for
consistency.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 50a77704-f1cc-4824-a22e-3c63a2ee44a4

📥 Commits

Reviewing files that changed from the base of the PR and between 2cb1ace and 73b68f8.

📒 Files selected for processing (8)
  • cpp/CMakeLists.txt
  • cpp/include/cudf/fixed_point/detail/floating_conversion.hpp
  • cpp/include/cudf/fixed_point/fixed_point.hpp
  • cpp/include/cudf/utilities/type_dispatcher.hpp
  • cpp/src/binaryop/binaryop.cpp
  • cpp/src/binaryop/compiled/binary_ops.cu
  • cpp/src/binaryop/compiled/binary_ops.hpp
  • cpp/src/binaryop/compiled/binary_ops_safe.cu
✅ Files skipped from review due to trivial changes (2)
  • cpp/include/cudf/utilities/type_dispatcher.hpp
  • cpp/include/cudf/fixed_point/fixed_point.hpp
🚧 Files skipped from review as they are similar to previous changes (5)
  • cpp/src/binaryop/compiled/binary_ops.hpp
  • cpp/CMakeLists.txt
  • cpp/src/binaryop/binaryop.cpp
  • cpp/src/binaryop/compiled/binary_ops_safe.cu
  • cpp/include/cudf/fixed_point/detail/floating_conversion.hpp

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
cpp/tests/binaryop/binop-compiled-fixed_point-test.cpp (1)

945-1084: ⚡ Quick win

Edge-case coverage now in place; consider adding a decimal128 overflow case.

These new tests resolve the earlier edge-case gap (empty input, null rows kept false, sliced columns, multi-block sizes, div-by-zero). Slice/sparse-overflow math all checks out.

One coverage note: every BinaryOperationSafeDecimal test uses decimal64, so the boundary value (int64::max()/2)+1 only exercises the int64 storage path. The decimal128 (__int128) rescale/overflow path is untested. A single added case mirroring mulOverflowSetsGlobalFlag with a decimal128 near-__int128-max operand would cover it cheaply.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/tests/binaryop/binop-compiled-fixed_point-test.cpp` around lines 945 -
1084, Add a decimal128 overflow test analogous to the existing decimal64 case:
create a new TEST(BinaryOperationSafeDecimal, mulOverflowDecimal128) that uses
Rep = cudf::device_storage_type_t<numeric::decimal128>, pick a "big" operand
near the __int128 max (mirror how big is computed for decimal64), build lhs/rhs
vectors to force multiplication overflow on selected rows (similar pattern to
mulOverflowSetsGlobalFlag), call cudf::binary_operation_fixed_point_output_type
and cudf::binary_operation_safe with cudf::binary_operator::MUL, and assert the
result size and that the overflow column (from overflow->view()) matches the
expected boolean mask marking overflows.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@cpp/tests/binaryop/binop-compiled-fixed_point-test.cpp`:
- Around line 945-1084: Add a decimal128 overflow test analogous to the existing
decimal64 case: create a new TEST(BinaryOperationSafeDecimal,
mulOverflowDecimal128) that uses Rep =
cudf::device_storage_type_t<numeric::decimal128>, pick a "big" operand near the
__int128 max (mirror how big is computed for decimal64), build lhs/rhs vectors
to force multiplication overflow on selected rows (similar pattern to
mulOverflowSetsGlobalFlag), call cudf::binary_operation_fixed_point_output_type
and cudf::binary_operation_safe with cudf::binary_operator::MUL, and assert the
result size and that the overflow column (from overflow->view()) matches the
expected boolean mask marking overflows.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: ac07494e-0ef2-43e7-a932-2633d3b25af0

📥 Commits

Reviewing files that changed from the base of the PR and between 73b68f8 and 72b132a.

📒 Files selected for processing (5)
  • cpp/include/cudf/binaryop.hpp
  • cpp/include/cudf/fixed_point/detail/floating_conversion.hpp
  • cpp/include/cudf/fixed_point/detail/safe_arithmetic.hpp
  • cpp/src/binaryop/compiled/binary_ops_safe.cu
  • cpp/tests/binaryop/binop-compiled-fixed_point-test.cpp
🚧 Files skipped from review as they are similar to previous changes (4)
  • cpp/include/cudf/binaryop.hpp
  • cpp/src/binaryop/compiled/binary_ops_safe.cu
  • cpp/include/cudf/fixed_point/detail/safe_arithmetic.hpp
  • cpp/include/cudf/fixed_point/detail/floating_conversion.hpp

@Avinash-Raj

Avinash-Raj commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

@pmattione-nvidia https://github.com/rapidsai/cudf/actions/runs/26745355782/job/78819432616?pr=22356
On spark-rapids-jni build job, decimal_utils.cu calls convert_floating_to_integral_shifting<IntType, FT> and assigns the result to a plain unsigned integer. Before refactor it returned a plain integer; after the refactor it returns a pair. The fix: make the default CheckOverflow=false keep the legacy plain-integer return, while CheckOverflow=true returns the pair. Shall I proceed with this approach?

@Avinash-Raj

Copy link
Copy Markdown
Contributor Author

@davidwendt All the style issues have been resolved now.

Comment thread cpp/include/cudf/fixed_point/detail/safe_arithmetic.hpp
* @throw cudf::data_type_error if the operation is not supported for the types of
* @p lhs and @p rhs
*/
std::pair<std::unique_ptr<column>, std::unique_ptr<column>> binary_operation_safe(

@PointKernel PointKernel Jun 2, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Compared to ansi, I do like the safe naming though it's not perfect.

Actually, safe is probably not a great choice either as we already have existing uses of unsafe to denote an operation is not thread safe, e.g. https://github.com/rapidsai/cudf/blob/4162d61633c332be91851126df388fb6489aac9c/cpp/include/cudf/utilities/bit.hpp#L73

AFAIK, we currently have three different naming schemes in libcudf related to overflow checking:

  1. For aggregation, we use _WITH_OVERFLOW suffix to denote it has overflow check https://github.com/rapidsai/cudf/blob/aa3cdee199d636d0075b6ae165d8ed09aff0b92a/cpp/include/cudf/aggregation.hpp#L81
  2. For JIT operators, we use the SQL term ansi, e.g. https://github.com/rapidsai/cudf/blob/6f8c429d0a532bdfd5d5e4057643addec6f5f486/cpp/include/cudf/detail/operators/ansi_arithmetic.cuh#L32
  3. The current effort with safe prefix/suffix

We should really converge on a consistent naming scheme going forward to avoid further fragmentation and branching.

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.

Yes, we've agreed on using an "_overflow" suffix

* @throw cudf::data_type_error if the operation is not supported for the types of
* @p lhs and @p rhs
*/
std::pair<std::unique_ptr<column>, std::unique_ptr<column>> binary_operation_safe(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Another minor difference I wanted to mention: in the SUM_WITH_OVERFLOW aggregation, both reduce and groupby currently return a single STRUCT column, where the first field is the sum and the second is the overflow flag.

It would be good to align with the existing pattern, or at least converge on a consistent one. That said, it’s not immediately obvious how to adjust the groupby or reduce code paths, since the input-output mapping assumes one input column projects to one output column, and there’s no straightforward way to change that.

Changing the output here from two columns to a single struct column seems relatively straightforward, but I’m not sure how important this consistency is in practice, or whether it’s worth enforcing it across the board.

Comment on lines +43 to +299
* @brief Result of an overflow-checked `fixed_point` operation.
*
* @tparam Rep Storage type of the wrapped `fixed_point` value
* @tparam Rad Radix of the wrapped `fixed_point` value
*/
template <typename Rep, Radix Rad>
struct safe_result {
fixed_point<Rep, Rad> value; ///< Computed `fixed_point` value
bool overflow; ///< Whether the producing operation overflowed
};

/**
* @brief Whether `shift<Rep, Rad>(val, scale)` would incur signed-integer overflow
*
* Mirrors the overflow conditions of `multiplication_overflow` /
* `division_overflow` on the intermediate scale factor.
*
* @tparam Rep Representation type
* @tparam Rad Radix
* @tparam T Type of the value being shifted (typically `Rep`)
* @param val The value being shifted
* @param scale The amount to shift the value by
* @return true if the shift would overflow `Rep`, false otherwise
*/
template <typename Rep, Radix Rad, typename T>
CUDF_HOST_DEVICE inline constexpr bool shift_overflows(T const& val, scale_type const& scale)
{
auto const v = static_cast<Rep>(val);
if (scale == 0) { return false; }
if (scale > 0) {
Rep const divisor = ipow<Rep, Rad>(static_cast<int32_t>(scale));
return division_overflow<Rep>(v, divisor);
}
Rep const multiplier = ipow<Rep, Rad>(static_cast<int32_t>(-scale));
return multiplication_overflow<Rep>(v, multiplier);
}

/**
* @brief Rescale a `fixed_point` value, reporting whether the underlying shift overflows
*
* Equivalent to `x.rescaled(new_scale)` but surfaces the shift overflow flag
* instead of dropping it.
*
* @tparam Rep Storage type
* @tparam Rad Radix
* @param x The value to rescale
* @param new_scale The target scale
* @return `{rescaled_value, overflow}`; `rescaled_value` is zero on overflow
*/
template <typename Rep, Radix Rad>
CUDF_HOST_DEVICE inline safe_result<Rep, Rad> safe_rescaled(fixed_point<Rep, Rad> x,
scale_type new_scale)
{
if (new_scale == x.scale()) { return safe_result<Rep, Rad>{x, false}; }
auto const scale_delta = scale_type{new_scale - x.scale()};
// Skip the shift when it would overflow: the rescaled value is meaningless
// and performing the multiply would be signed-integer-overflow UB.
if (shift_overflows<Rep, Rad>(x.value(), scale_delta)) {
return safe_result<Rep, Rad>{fixed_point<Rep, Rad>{scaled_integer<Rep>{Rep{0}, new_scale}},
true};
}
Rep const value = shift<Rep, Rad>(x.value(), scale_delta);
return safe_result<Rep, Rad>{fixed_point<Rep, Rad>{scaled_integer<Rep>{value, new_scale}}, false};
}

/**
* @brief Overflow-checked addition of two `fixed_point` values
*
* Rescales both operands to the smaller of their two scales (matching
* `operator+`), then performs the add. The returned `overflow` flag is the
* disjunction of any rescale overflow and the integer add overflow. When
* overflow is detected the add is skipped and `value` is zero.
*/
template <typename Rep, Radix Rad>
CUDF_HOST_DEVICE inline safe_result<Rep, Rad> safe_add(fixed_point<Rep, Rad> lhs,
fixed_point<Rep, Rad> rhs)
{
auto const common_scale = cuda::std::min(lhs.scale(), rhs.scale());
auto const lhs_r = safe_rescaled(lhs, common_scale);
auto const rhs_r = safe_rescaled(rhs, common_scale);
Rep const lv = lhs_r.value.value();
Rep const rv = rhs_r.value.value();
bool const overflow = lhs_r.overflow || rhs_r.overflow || addition_overflow<Rep>(lv, rv);
Rep const sum = overflow ? Rep{0} : lv + rv;
return safe_result<Rep, Rad>{fixed_point<Rep, Rad>{scaled_integer<Rep>{sum, common_scale}},
overflow};
}

/**
* @brief Overflow-checked subtraction of two `fixed_point` values
*
* When overflow is detected the subtract is skipped and `value` is zero.
*/
template <typename Rep, Radix Rad>
CUDF_HOST_DEVICE inline safe_result<Rep, Rad> safe_sub(fixed_point<Rep, Rad> lhs,
fixed_point<Rep, Rad> rhs)
{
auto const common_scale = cuda::std::min(lhs.scale(), rhs.scale());
auto const lhs_r = safe_rescaled(lhs, common_scale);
auto const rhs_r = safe_rescaled(rhs, common_scale);
Rep const lv = lhs_r.value.value();
Rep const rv = rhs_r.value.value();
bool const overflow = lhs_r.overflow || rhs_r.overflow || subtraction_overflow<Rep>(lv, rv);
Rep const diff = overflow ? Rep{0} : lv - rv;
return safe_result<Rep, Rad>{fixed_point<Rep, Rad>{scaled_integer<Rep>{diff, common_scale}},
overflow};
}

/**
* @brief Overflow-checked multiplication of two `fixed_point` values
*
* No rescale is needed -- the result scale is `lhs.scale() + rhs.scale()`.
* When overflow is detected the multiply is skipped and `value` is zero.
*/
template <typename Rep, Radix Rad>
CUDF_HOST_DEVICE inline safe_result<Rep, Rad> safe_mul(fixed_point<Rep, Rad> lhs,
fixed_point<Rep, Rad> rhs)
{
Rep const lv = lhs.value();
Rep const rv = rhs.value();
bool const overflow = multiplication_overflow<Rep>(lv, rv);
Rep const prod = overflow ? Rep{0} : lv * rv;
scale_type const out_scale{lhs.scale() + rhs.scale()};
return safe_result<Rep, Rad>{fixed_point<Rep, Rad>{scaled_integer<Rep>{prod, out_scale}},
overflow};
}

/**
* @brief Overflow-checked division of two `fixed_point` values
*
* Two failure modes are reported as overflow: `INT_MIN / -1` (caught by
* `division_overflow`) and division by zero. In both cases the divide is
* skipped -- so we never invoke signed-integer divide overflow or
* divide-by-zero UB -- and a zero-valued result is returned.
*/
template <typename Rep, Radix Rad>
CUDF_HOST_DEVICE inline safe_result<Rep, Rad> safe_div(fixed_point<Rep, Rad> lhs,
fixed_point<Rep, Rad> rhs)
{
Rep const lv = lhs.value();
Rep const rv = rhs.value();
scale_type const out_scale{lhs.scale() - rhs.scale()};
// Short-circuit on a zero divisor before touching `division_overflow` (which
// would itself divide) or the divide below.
bool const overflow = (rv == Rep{0}) || division_overflow<Rep>(lv, rv);
Rep const quot = overflow ? Rep{0} : lv / rv;
return safe_result<Rep, Rad>{fixed_point<Rep, Rad>{scaled_integer<Rep>{quot, out_scale}},
overflow};
}

/**
* @brief Overflow-checked modulo of two `fixed_point` values
*
* The op-level failure modes are divide-by-zero and the `INT_MIN % -1`
* signed-overflow boundary; the rescale to the common scale can also overflow.
* All are OR'd into the returned flag, and whenever any of them is set the `%`
* is skipped and a zero-valued result is returned (so we never invoke
* `%`-by-zero or signed-overflow UB).
*/
template <typename Rep, Radix Rad>
CUDF_HOST_DEVICE inline safe_result<Rep, Rad> safe_mod(fixed_point<Rep, Rad> lhs,
fixed_point<Rep, Rad> rhs)
{
auto const common_scale = cuda::std::min(lhs.scale(), rhs.scale());
auto const lhs_r = safe_rescaled(lhs, common_scale);
auto const rhs_r = safe_rescaled(rhs, common_scale);
Rep const lv = lhs_r.value.value();
Rep const rv = rhs_r.value.value();
bool const overflow =
lhs_r.overflow || rhs_r.overflow || (rv == Rep{0}) || division_overflow<Rep>(lv, rv);
Rep const remainder = overflow ? Rep{0} : lv % rv;
return safe_result<Rep, Rad>{fixed_point<Rep, Rad>{scaled_integer<Rep>{remainder, common_scale}},
overflow};
}

/**
* @brief Overflow-checked positive modulo, matching `ops::PMod` semantics for decimals.
*
* Implements `rem = x % y; (rem < 0) ? (rem + y) % y : rem`. The intermediate
* `rem + y` can overflow even though `%` itself cannot. If the base modulo
* already overflowed (incl. divide-by-zero), or the correcting add overflows,
* the correction is skipped and a zero-valued result is returned.
*/
template <typename Rep, Radix Rad>
CUDF_HOST_DEVICE inline safe_result<Rep, Rad> safe_pmod(fixed_point<Rep, Rad> lhs,
fixed_point<Rep, Rad> rhs)
{
auto const m = safe_mod(lhs, rhs);
if (m.overflow || !(m.value.value() < Rep{0})) { return m; }

// `m.overflow` is false here, so `safe_mod` saw a non-zero divisor: `rv != 0`.
auto const rhs_r = safe_rescaled(rhs, m.value.scale());
Rep const mv = m.value.value();
Rep const rv = rhs_r.value.value();
bool const overflow = rhs_r.overflow || addition_overflow<Rep>(mv, rv);
Rep const corrected = overflow ? Rep{0} : (mv + rv) % rv;
return safe_result<Rep, Rad>{
fixed_point<Rep, Rad>{scaled_integer<Rep>{corrected, m.value.scale()}}, overflow};
}

/**
* @brief Overflow-checked Python-style modulo: `((x % y) + y) % y`.
*
* The intermediate add can overflow; the final `%` cannot. If the base modulo
* already overflowed (incl. divide-by-zero), or the correcting add overflows,
* the correction is skipped and a zero-valued result is returned, so we never
* invoke `%`-by-zero or signed-overflow UB.
*/
template <typename Rep, Radix Rad>
CUDF_HOST_DEVICE inline safe_result<Rep, Rad> safe_pymod(fixed_point<Rep, Rad> lhs,
fixed_point<Rep, Rad> rhs)
{
auto const m = safe_mod(lhs, rhs);
if (m.overflow) { return m; }

// `m.overflow` is false here, so `safe_mod` saw a non-zero divisor: `rv != 0`.
auto const rhs_r = safe_rescaled(rhs, m.value.scale());
Rep const mv = m.value.value();
Rep const rv = rhs_r.value.value();
bool const overflow = rhs_r.overflow || addition_overflow<Rep>(mv, rv);
Rep const corrected = overflow ? Rep{0} : (mv + rv) % rv;
return safe_result<Rep, Rad>{
fixed_point<Rep, Rad>{scaled_integer<Rep>{corrected, m.value.scale()}}, overflow};
}

/**
* @brief Overflow-checked floating-point -> `fixed_point` conversion
*
* Uses `convert_floating_to_integral<Rep, true>` (in
* `floating_conversion.hpp`) for base-10 decimals, which saturates and reports
* overflow. For base-2 radixes there is no checked path today, so `overflow`
* is always `false`.
*
* @tparam Fixed Target `fixed_point` instantiation
* @tparam Floating Source floating-point type
* @param floating The floating-point value to convert
* @param scale The desired scale of the result
* @return `{fixed_point_value, overflow}`
*/
template <typename Fixed,
typename Floating,
CUDF_ENABLE_IF(cuda::std::is_floating_point_v<Floating>&& cudf::is_fixed_point<Fixed>())>
CUDF_HOST_DEVICE inline safe_result<typename Fixed::rep, Fixed::rad> safe_convert_floating_to_fixed(
Floating floating, scale_type scale)
{
using Rep = typename Fixed::rep;
if constexpr (Fixed::rad == Radix::BASE_10) {
auto const [value, overflow] = convert_floating_to_integral<Rep, true>(floating, scale);
return safe_result<Rep, Fixed::rad>{Fixed{scaled_integer<Rep>{value, scale}}, overflow};
} else {
Rep const value = static_cast<Rep>(shift<Rep, Fixed::rad>(floating, scale));
return safe_result<Rep, Fixed::rad>{Fixed{scaled_integer<Rep>{value, scale}}, false};
}
}

} // namespace detail
} // namespace CUDF_EXPORT numeric

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.

can you please confirm each of these operators hasn't been implemented in cudf/detail/operators/*?

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 also merge these into cudf/detail/operators/ansi_arithmetic.cuh or cudf/detail/operators/ansi_casts.cuh.

@Avinash-Raj Avinash-Raj Jun 5, 2026

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.

ansi_arithmetic.cuh already provides overflow-checked add/sub/mul/div/mod (and abs/neg/precision_check), so it overlaps with safe_add/safe_sub/safe_mul/safe_div.

However, it doesn't detect rescale-induced overflow in add/sub
( Given the overlap, we should converge the two — e.g. have one delegate to the other ie, _safe kernel delegating to ansi_* (after closing ansi_*'s coverage gaps), not changing binary_operation itself).

The main reason for this implementation is that, by default, Velox GPU/Velox-cuDF silently wraps the result on decimal arithmetic overflow instead of throwing an error.

@lamarrr lamarrr Jun 5, 2026

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.

I'm not convinced scale-induced overflow/underflow needs to be checked in the device code, whilst it is useful to have.
It is a metadata that should be checked before computing the operators.
Can you please move it outside of the operators and add it as a pre-kernel-launch error check instead.

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.

It might be worth doing benchmark before trying this. A single kernel to do both seems like it would be faster than 2 separate kernels but a benchmark would certainly show the impact.

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 scale is the same for all the rows of a column. So it is trivial to compute and check before launching the binary operation (2 scalars). AST has a similar setup. It's what Spark RAPIDS does.

@Avinash-Raj Avinash-Raj Jun 12, 2026

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.

Thanks @lamarrr. Two separate things here:

Scale/type validity -> agreed, I'll add a pre-kernel check. Scales are column metadata (same for all rows), so I can add a host-side CUDF_EXPECTS guard before launch that rejects invalid scale/output-type combos and fails fast without launching the kernel.

Rescale overflow -> has to stay per-row. Whether a rescale overflows depends on each row's value, not just the scale:

DECIMAL64, common rescale ×10^-3
row 0: rep = 5      → 5000        (Pass)
row 1: rep = 9.2e15 → overflows   (Fail)

Same scale metadata, different outcome-> so a scalar pre-check can't produce the per-row BOOL8 this API exposes .

So the metadata check is additive. A cheap early guard but doesn't remove the kernel's per-row checks.

@lamarrr lamarrr Jun 22, 2026

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.

can you instead implement overflow checks into "checked_arithmetic.cuh" header, using #22836 as base?
Otherwise, reuse the equivalent functions in "checked_arithmetic.cuh". We'd like to reduce code duplication

* @throw cudf::data_type_error if the operation is not supported for the types of
* @p lhs and @p rhs
*/
std::pair<std::unique_ptr<column>, std::unique_ptr<column>> binary_operation_safe(

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.

can you make this behaviour part of binary_operation instead?

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.

What behavior are you referring to?

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 error checking. rather than having binary_operation and binary_operation_safe, we should have a single binary_operation with enums to specify whether to check for errors.

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.

I disagree with this suggestion.

Comment on lines +48 to +52
template <typename Rep, Radix Rad>
struct safe_result {
fixed_point<Rep, Rad> value; ///< Computed `fixed_point` value
bool overflow; ///< Whether the producing operation overflowed
};

@lamarrr lamarrr Jun 5, 2026

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.

can you replace this and all occurrences with cuda::std::expected<decimal, errc>? to be consistent with CUDF's operator library

@GregoryKimball

Copy link
Copy Markdown
Contributor

@Avinash-Raj are you planning to revisit this?

@Avinash-Raj

Avinash-Raj commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

@GregoryKimball No, because the pinned ANSI/checked arithmetic mode already provides what I need for Velox-cuDF, throwing an error on arithmetic overflow: facebookincubator/velox#17924

@Avinash-Raj

Copy link
Copy Markdown
Contributor Author

Closing this PR since it has been superseded by the ANSI mode PRs.

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

Labels

CMake CMake build issue feature request New feature or request libcudf Affects libcudf (C++/CUDA) code. non-breaking Non-breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.