Skip to content

Add decimal support to VARIANT casting - #23858

Open
vuule wants to merge 14 commits into
NVIDIA:mainfrom
vuule:variant-decimal-cast
Open

Add decimal support to VARIANT casting#23858
vuule wants to merge 14 commits into
NVIDIA:mainfrom
vuule:variant-decimal-cast

Conversation

@vuule

@vuule vuule commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Description

closes #23817

cast_variant and extract_variant_field now accept DECIMAL32/DECIMAL64/DECIMAL128 target types, decoding the DECIMAL4/8/16 VARIANT primitives.

The encoding stores a scale per value while a cuDF column carries a single scale, so each value is rescaled to desired_type.scale(). A value that does not fit the target after rescaling is nulled and reported as variant_operation_status::OVERFLOW. An out-of-range scale byte or a truncated payload reports MALFORMED_VARIANT, and a non-decimal encoding reports TYPE_MISMATCH.

Also added test and benchmark coverage.

Checklist

  • I am familiar with the Contributing Guidelines.
  • New or existing tests cover these changes.
  • The documentation is up to date with these changes.

cast_variant and extract_variant_field now accept DECIMAL32/64/128 targets.
The VARIANT encoding scales every value individually while a cuDF column
carries a single scale, so each value is rescaled to the requested scale,
truncating toward zero, and a value that no longer fits the target
representation is nulled with the OVERFLOW status.
@github-actions github-actions Bot added the libcudf Affects libcudf (C++/CUDA) code. label Aug 27, 2026
vuule added 3 commits August 27, 2026 02:03
…hmark decimals

Adds a DECIMAL16 test at the int128 limits, which the previous cases left the
high half of the payload zeroed for, and a sliced 512-row case so the decimal
kernel's grid-stride loop and slice offset are covered.

Factors the incoming-status and null-bit preamble the cast paths share into
should_decode_row, so the protocol lives in one place instead of three, and
extends the variant nvbench with decimal32 and decimal128 cases.
@vuule vuule added feature request New feature or request non-breaking Non-breaking change labels Aug 27, 2026
vuule added 3 commits August 27, 2026 23:19
Adds a DECIMAL64 arm to the overflow test, the only place the int64_t
range check is reachable, and a decimal64 case to the cast benchmark's
type axis.
# Conflicts:
#	cpp/tests/io/experimental/variant_extract_test.cpp
@GregoryKimball GregoryKimball moved this to Burndown in libcudf Aug 31, 2026
The cast target scale and the expected column scale must agree for these
tests to mean anything, so route both through one named constant instead
of repeating the literal.
Derive the overflow bounds from the target type's limits instead of
literals a reviewer has to count digits in, fold the empty-input loops
together, and make the interchangeable-widths case a typed test over the
three fixed-point types.
d_output[row] = T{};
continue;
}
if (!should_decode_row(row, d_null_mask, d_status)) {

@vuule vuule Sep 1, 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.

No behavioral changes, just using the new helper

The per-digit loop paid a full 128-bit software division for every digit
of rescale distance. Computing the divisor with ipow and dividing once,
narrowed to 64 bits when both operands fit, cuts a two-digit decimal32
rescale from 168 to 107 us on 2M rows, against a 102 us baseline for a
cast that needs no rescale.

Also tightens a few comments in the shared row helper.
Give width 16 its own case so an unexpected width yields zero instead of
reading 16 bytes, and fix a comment indent in the cast matrix test.
@NVIDIA NVIDIA deleted a comment from copy-pr-bot Bot Sep 1, 2026
@vuule

vuule commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test e818b78

@vuule
vuule marked this pull request as ready for review September 1, 2026 22:20
@vuule
vuule requested a review from a team as a code owner September 1, 2026 22:20
@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 12aa6e41-1460-4d42-8f71-1e90694224df

📥 Commits

Reviewing files that changed from the base of the PR and between 81e6804 and e818b78.

📒 Files selected for processing (4)
  • cpp/benchmarks/io/parquet/experimental/variant/extract.cpp
  • cpp/include/cudf/io/experimental/variant.hpp
  • cpp/src/io/parquet/experimental/variant_extract.cu
  • cpp/tests/io/experimental/variant_extract_test.cpp

Included review availability: Your plan provides up to 12 included reviews per hour; 11 remain after this review.


📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added support for extracting and casting VARIANT decimal values as DECIMAL32, DECIMAL64, and DECIMAL128.
    • Decimal values are rescaled to requested precision, with truncation toward zero.
  • Bug Fixes
    • Decimal overflow now returns null rows with an overflow status.
    • Improved handling of malformed inputs and mixed-width decimal values.
  • Tests
    • Expanded coverage for decimal extraction, rescaling, overflow, empty inputs, sliced data, and multi-row scenarios.

Walkthrough

Changes

The PR adds DECIMAL32, DECIMAL64, and DECIMAL128 decoding for Parquet VARIANT extraction and casting. It handles source-scale conversion, truncation, overflow, malformed payloads, operation statuses, and expanded benchmark and test coverage.

VARIANT decimal casting

Layer / File(s) Summary
Decimal contracts and benchmark support
cpp/include/cudf/io/experimental/variant.hpp, cpp/benchmarks/io/parquet/experimental/variant/extract.cpp
Documentation lists decimal targets and rescaling behavior. Benchmarks encode decimal payloads and run decimal target cases.
Decimal decoding and row filtering
cpp/src/io/parquet/experimental/variant_extract.cu
Decimal widths and scales are decoded. Rescaling, malformed input, overflow, null masks, and operation statuses are handled. Shared row filtering is used by primitive and boolean casts.
Fixed-point dispatch and output handling
cpp/src/io/parquet/experimental/variant_extract.cu
Fixed-point targets are accepted. The dispatch path allocates representations and launches the decimal casting kernel.
Decimal extraction and casting validation
cpp/tests/io/experimental/variant_extract_test.cpp
Tests cover decimal encoding, extraction, rescaling, width interoperability, boundaries, malformed payloads, overflow, slices, statuses, and cast matrices.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: ⚪ Minimal · up to e818b

This change adds decimal VARIANT casting with documented overflow and malformed-input handling; no actionable merge-blocking risk remains after normal checks and review.

Suggested reviewers: davidwendt, simoneves, abigalekim, vyasr

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.50% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 40 functions across 4 files. 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 clearly summarizes the main change: adding decimal support to VARIANT casting.
Description check ✅ Passed The description directly covers decimal target support, rescaling, overflow, malformed inputs, type mismatches, tests, and benchmarks.
Linked Issues check ✅ Passed The changes address issue #23817 by supporting DECIMAL4/8/16 decoding into DECIMAL32/64/128 targets, per-value scale conversion, truncation, overflow and malformed-input statuses, null handling, and b…
Out of Scope Changes check ✅ Passed The implementation, documentation, tests, and benchmarks are directly related to decimal VARIANT casting and the requirements in issue #23817. No unrelated changes are identified.
Full details: Linked Issues check

Explanation

The changes address issue #23817 by supporting DECIMAL4/8/16 decoding into DECIMAL32/64/128 targets, per-value scale conversion, truncation, overflow and malformed-input statuses, null handling, and broad test coverage.

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

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

Couldn't find anything worth requesting changes except some of the constexprs / magic numbers used here could use a one liner comment.

LGTM with some optional comments that you can take liberty in addressing

Comment on lines +118 to +119
* `FLOAT32`/`FLOAT64`, `BOOL8`, or `DECIMAL32`/`DECIMAL64`/`DECIMAL128` (see `cast_variant`
* for decimal rescaling)

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.

very optional nit: just reads better

Suggested change
* `FLOAT32`/`FLOAT64`, `BOOL8`, or `DECIMAL32`/`DECIMAL64`/`DECIMAL128` (see `cast_variant`
* for decimal rescaling)
* `FLOAT32`/`FLOAT64`, `BOOL8`, or `DECIMAL32`/`DECIMAL64`/`DECIMAL128`
* (see `cast_variant` for decimal rescaling)

Comment on lines +809 to +815
constexpr __int128_t max_over_10 = cuda::std::numeric_limits<__int128_t>::max() / 10;
constexpr __int128_t min_over_10 = cuda::std::numeric_limits<__int128_t>::min() / 10;
for (int i = 0; i < exp && value != 0; ++i) {
if (value > max_over_10 || value < min_over_10) { return cuda::std::nullopt; }
value *= 10;
}
return value;

@mhaseeb123 mhaseeb123 Sep 1, 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.

We can use cuda::mul_overflow here Something like the following but it requires __int128_t to satisfy the integer concept in concepts.cuh. Please check if this works and ignore if it errors out.

__device__ cuda::std::optional<__int128_t> constexpr multiply_pow10(__int128_t value, int exp)
{
  for (int i = 0; i < exp && value != 0; ++i) {
    auto r = ops::mul_overflow<__int128_t>(value, __int128_t{10});
    if (!r) { return cuda::std::nullopt; }
    value = *r;
  }
  return value;
}

return value / ipow<__int128_t, numeric::Radix::BASE_10>(exp);
}

__device__ int variant_decimal_unscaled_width(primitive_type ptype)

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.

Suggested change
__device__ int variant_decimal_unscaled_width(primitive_type ptype)
__device__ int constexpr variant_decimal_unscaled_width(primitive_type ptype)

}

// Divide `value` by 10^exp, truncating toward zero.
__device__ __int128_t divide_pow10(__int128_t value, int exp)

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.

Suggested change
__device__ __int128_t divide_pow10(__int128_t value, int exp)
__device__ __int128_t constexpr divide_pow10(__int128_t value, int exp)

constexpr int variant_decimal_max_scale = 38;

// Multiply `value` by 10^exp, or return nullopt if the result does not fit in `__int128_t`.
__device__ cuda::std::optional<__int128_t> multiply_pow10(__int128_t value, int exp)

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.

Suggested change
__device__ cuda::std::optional<__int128_t> multiply_pow10(__int128_t value, int exp)
__device__ cuda::std::optional<__int128_t> constexpr multiply_pow10(__int128_t value, int exp)

Comment on lines +979 to +1011
/**
* @brief Per-row kernel: decode each VARIANT decimal value blob into a fixed-point representation
* of type `Rep`, rescaled to `desired_scale`. Same null and status protocol as
* `cast_variant_primitive_kernel`.
*/
template <typename Rep>
CUDF_KERNEL __launch_bounds__(block_size) void cast_variant_decimal_kernel(
cudf::lists_column_device_view values,
device_span<Rep> d_output,
int desired_scale,
bitmask_type* d_null_mask,
op_status* d_status) // nullptr when no status was requested
{
auto const num_rows = static_cast<size_type>(d_output.size());
auto const tid = cudf::detail::grid_1d::global_thread_id<block_size>();
auto const stride = cudf::detail::grid_1d::grid_stride<block_size>();

for (auto row = tid; row < num_rows; row += stride) {
if (!should_decode_row(row, d_null_mask, d_status)) {
d_output[row] = Rep{};
continue;
}

auto const [value, status] = decode_decimal<Rep>(list_row_span(values, row), desired_scale);
if (status == op_status::SUCCESS) {
d_output[row] = value;
} else {
d_output[row] = Rep{};
cudf::clear_bit(d_null_mask, row);
}
if (d_status != nullptr) { d_status[row] = status; }
}
}

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.

Optional: Since this kernel processes one element per thread, we could make this a functor and launch via thrust::transform

{
constexpr __int128_t max_over_10 = cuda::std::numeric_limits<__int128_t>::max() / 10;
constexpr __int128_t min_over_10 = cuda::std::numeric_limits<__int128_t>::min() / 10;
for (int i = 0; i < exp && value != 0; ++i) {

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.

Exit early once exp exceeds the max spec scale similar to divide_pow10, so a large user-supplied desired_scale can't force many redundant iterations after overflow is inevitable.

Suggested change
for (int i = 0; i < exp && value != 0; ++i) {
if (exp > variant_decimal_max_scale) { return cuda::std::nullopt; }
for (int i = 0; i < exp && value != 0; ++i) {

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

Labels

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

Projects

Status: Burndown

Development

Successfully merging this pull request may close these issues.

[FEA] Add DECIMAL decoding for Parquet VARIANT values

3 participants