-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Add decimal support to VARIANT casting #23858
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 8 commits
64be3f0
1370071
b93a33f
44c8749
df1bcbc
305e6d7
79e292e
554e2a3
8327c7c
405e29d
c8c1660
3fde395
03e70e0
e818b78
b15b724
370bd42
3e5e445
d53483a
6f348af
0766f3f
ba7a2a2
3fbdfe4
e8e95d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -525,11 +525,12 @@ constexpr bool is_variant_int = | |
| template <typename T> | ||
| constexpr bool is_variant_numerical = is_variant_int<T> || cudf::is_floating_point<T>(); | ||
|
|
||
| // The output types a VARIANT value can be cast to: the fixed-width signed integers, floats, bool, | ||
| // and strings. | ||
| // The output types a VARIANT value can be cast to: the fixed-width signed integers, floats, | ||
| // decimals, bool, and strings. | ||
| template <typename T> | ||
| constexpr bool is_variant_castable = is_variant_numerical<T> || cuda::std::is_same_v<T, bool> || | ||
| cuda::std::is_same_v<T, cudf::string_view>; | ||
| constexpr bool is_variant_castable = | ||
| is_variant_numerical<T> || cudf::is_fixed_point<T>() || cuda::std::is_same_v<T, bool> || | ||
| cuda::std::is_same_v<T, cudf::string_view>; | ||
|
|
||
| // Maps a fixed-width output type to the VARIANT primitive type header id that encodes it. | ||
| template <typename T> | ||
|
|
@@ -798,6 +799,129 @@ __device__ op_status cast_status_for_primitive(device_span<uint8_t const> val) | |
| : op_status::MALFORMED_VARIANT; | ||
| } | ||
|
|
||
| // The spec allows a scale in [0, 38] for every decimal width. | ||
| 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) | ||
|
vuule marked this conversation as resolved.
Outdated
|
||
| { | ||
| 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) { | ||
|
vuule marked this conversation as resolved.
Outdated
|
||
| if (value > max_over_10 || value < min_over_10) { return cuda::std::nullopt; } | ||
| value *= 10; | ||
| } | ||
| return value; | ||
|
Comment on lines
+813
to
+819
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can use __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;
}
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It compiles but it's slower. On a three-digit scale-up over 2M rows the checked multiply costs 17% on decimal32 (114.6 → 134.0 µs) and 13% on decimal128 (123.1 → 139.3 µs). |
||
| } | ||
|
|
||
| // Divide `value` by 10^exp, truncating toward zero. Iterating keeps an `exp` whose power of ten | ||
| // would itself overflow exact, since every value truncates to zero there. | ||
| __device__ __int128_t divide_pow10(__int128_t value, int exp) | ||
|
vuule marked this conversation as resolved.
Outdated
|
||
| { | ||
| for (int i = 0; i < exp && value != 0; ++i) { | ||
| value /= 10; | ||
| } | ||
| return value; | ||
| } | ||
|
|
||
| // Returns 0 for a `ptype` that is not a decimal. | ||
| __device__ int variant_decimal_unscaled_width(primitive_type ptype) | ||
|
vuule marked this conversation as resolved.
Outdated
|
||
| { | ||
| switch (ptype) { | ||
| case primitive_type::DECIMAL4: return 4; | ||
| case primitive_type::DECIMAL8: return 8; | ||
| case primitive_type::DECIMAL16: return 16; | ||
| default: return 0; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * @brief Decode a single VARIANT decimal value blob into the representation of a cuDF fixed-point | ||
| * type, rescaled to `desired_scale`. | ||
| * | ||
| * Encoded as the value metadata byte, a one-byte scale (fractional digit count), then the | ||
| * little-endian two's-complement unscaled integer. Any of the three widths decodes into any `Rep` | ||
| * whose range fits the rescaled value; digits below `desired_scale` truncate toward zero. | ||
| * | ||
| * @return The rescaled representation, valid only when the returned status is `SUCCESS` | ||
| */ | ||
| template <typename Rep> | ||
| __device__ cuda::std::pair<Rep, op_status> decode_decimal(device_span<uint8_t const> enc, | ||
| int desired_scale) | ||
| { | ||
| auto const fail = [](op_status status) { return cuda::std::pair<Rep, op_status>{Rep{}, status}; }; | ||
|
|
||
| if (enc.empty()) { return fail(op_status::MALFORMED_VARIANT); } | ||
| if (is_variant_null(enc)) { return fail(op_status::VARIANT_NULL); } | ||
| if (decode_basic_type(enc[0]) != basic_type::PRIMITIVE) { return fail(op_status::TYPE_MISMATCH); } | ||
|
|
||
| auto const ptype = static_cast<primitive_type>(variant_value_header(enc[0])); | ||
| auto const width = variant_decimal_unscaled_width(ptype); | ||
| if (width == 0) { | ||
| return fail(is_recognized_primitive_type(ptype) ? op_status::TYPE_MISMATCH | ||
| : op_status::MALFORMED_VARIANT); | ||
| } | ||
|
|
||
| constexpr size_type scale_bytes = 1; | ||
| if (cuda::std::cmp_less(enc.size(), variant_header_bytes + scale_bytes + width)) { | ||
| return fail(op_status::MALFORMED_VARIANT); | ||
| } | ||
| int const encoded_scale = enc[variant_header_bytes]; | ||
| if (encoded_scale > variant_decimal_max_scale) { return fail(op_status::MALFORMED_VARIANT); } | ||
|
|
||
| auto const* unscaled_data = enc.data() + variant_header_bytes + scale_bytes; | ||
| auto const unscaled = [&]() -> __int128_t { | ||
| switch (width) { | ||
| case 4: return cudf::io::unaligned_load<int32_t>(unscaled_data); | ||
| case 8: return cudf::io::unaligned_load<int64_t>(unscaled_data); | ||
| default: return cudf::io::unaligned_load<__int128_t>(unscaled_data); | ||
| } | ||
| }(); | ||
|
|
||
| // The encoded value is `unscaled * 10^-encoded_scale`; the output is `rep * 10^desired_scale`. | ||
| auto const shift = -encoded_scale - desired_scale; | ||
| __int128_t rescaled{}; | ||
| if (shift >= 0) { | ||
| auto const scaled = multiply_pow10(unscaled, shift); | ||
| if (!scaled.has_value()) { return fail(op_status::OVERFLOW); } | ||
| rescaled = scaled.value(); | ||
| } else { | ||
| rescaled = divide_pow10(unscaled, -shift); | ||
| } | ||
|
|
||
| if constexpr (!cuda::std::is_same_v<Rep, __int128_t>) { | ||
| if (rescaled < static_cast<__int128_t>(cuda::std::numeric_limits<Rep>::min()) || | ||
| rescaled > static_cast<__int128_t>(cuda::std::numeric_limits<Rep>::max())) { | ||
| return fail(op_status::OVERFLOW); | ||
| } | ||
| } | ||
| return {static_cast<Rep>(rescaled), op_status::SUCCESS}; | ||
| } | ||
|
|
||
| /** | ||
| * @brief Shared per-row preamble for the cast paths: decides whether row `row` should be decoded. | ||
| * | ||
| * A skipped row has its null bit cleared and its status recorded here, but its output element is | ||
| * left to the caller, which alone knows the output type. | ||
| * | ||
| * @return True when the row's value blob should be decoded | ||
| */ | ||
| __device__ bool should_decode_row(size_type row, bitmask_type* d_null_mask, op_status* d_status) | ||
| { | ||
| if (d_status == nullptr) { return cudf::bit_is_set(d_null_mask, row); } | ||
|
|
||
| // Status column is always non-nullable; ROW_NULL replaces the null bit. | ||
| if (d_status[row] != op_status::SUCCESS) { | ||
| if (cudf::bit_is_set(d_null_mask, row)) { cudf::clear_bit(d_null_mask, row); } | ||
| return false; | ||
| } | ||
| if (!cudf::bit_is_set(d_null_mask, row)) { | ||
| d_status[row] = op_status::ROW_NULL; | ||
| return false; | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * @brief Per-row kernel: decode each VARIANT value blob into a fixed-width primitive of type `T`. | ||
| * | ||
|
|
@@ -823,24 +947,9 @@ CUDF_KERNEL __launch_bounds__(block_size) void cast_variant_primitive_kernel( | |
| auto const stride = cudf::detail::grid_1d::grid_stride<block_size>(); | ||
|
|
||
| for (auto row = tid; row < num_rows; row += stride) { | ||
| if (d_status != nullptr) { | ||
| // Status column is always non-nullable; row_null replaces the null bit. | ||
| auto const s = d_status[row]; | ||
| if (s != op_status::SUCCESS) { | ||
| d_output[row] = T{}; | ||
| if (cudf::bit_is_set(d_null_mask, row)) { cudf::clear_bit(d_null_mask, row); } | ||
| continue; | ||
| } | ||
| if (!cudf::bit_is_set(d_null_mask, row)) { | ||
| d_output[row] = T{}; | ||
| d_status[row] = op_status::ROW_NULL; | ||
| continue; | ||
| } | ||
| } else { | ||
| if (!cudf::bit_is_set(d_null_mask, row)) { | ||
| d_output[row] = T{}; | ||
| continue; | ||
| } | ||
| if (!should_decode_row(row, d_null_mask, d_status)) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No behavioral changes, just using the new helper |
||
| d_output[row] = T{}; | ||
| continue; | ||
| } | ||
|
|
||
| auto const val = list_row_span(values, row); | ||
|
|
@@ -856,6 +965,40 @@ CUDF_KERNEL __launch_bounds__(block_size) void cast_variant_primitive_kernel( | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * @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; } | ||
| } | ||
| } | ||
|
Comment on lines
+984
to
+1016
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. kernel is 30% faster, so leaving as-is |
||
|
|
||
| __device__ op_status cast_status_for_bool(device_span<uint8_t const> val) | ||
| { | ||
| if (val.empty()) { return op_status::MALFORMED_VARIANT; } | ||
|
|
@@ -997,6 +1140,28 @@ struct cast_variant_fn { | |
| null_count); | ||
| } | ||
|
|
||
| template <typename T> | ||
| std::unique_ptr<column> operator()() | ||
| requires(cudf::is_fixed_point<T>()) | ||
| { | ||
| using Rep = typename T::rep; | ||
| rmm::device_buffer data{num_rows * sizeof(Rep), stream, mr}; | ||
| auto const grid = cudf::detail::grid_1d{num_rows, block_size}; | ||
| auto const d_out = | ||
| device_span<Rep>{static_cast<Rep*>(data.data()), static_cast<std::size_t>(num_rows)}; | ||
| cast_variant_decimal_kernel<Rep><<<grid.num_blocks, block_size, 0, stream.get()>>>( | ||
| values, d_out, desired_type.scale(), d_null_mask, d_status); | ||
| CUDF_CUDA_TRY(cudaGetLastError()); | ||
|
|
||
| auto const null_count = | ||
| num_rows - cudf::detail::count_set_bits(d_null_mask, 0, num_rows, stream); | ||
| return std::make_unique<column>(desired_type, | ||
| num_rows, | ||
| std::move(data), | ||
| null_count > 0 ? std::move(null_mask) : rmm::device_buffer{}, | ||
| null_count); | ||
| } | ||
|
|
||
| template <typename T> | ||
| std::unique_ptr<column> operator()() | ||
| requires(cuda::std::is_same_v<T, bool>) | ||
|
|
@@ -1012,14 +1177,16 @@ struct cast_variant_fn { | |
| d_out = static_cast<bool*>(data.data()), | ||
| dnm = this->d_null_mask, | ||
| dp_s] __device__(size_type row) { | ||
| if (!should_decode_row(row, dnm, dp_s)) { | ||
| d_out[row] = false; | ||
| return; | ||
| } | ||
| // The row is known live here, so a decode failure always clears its bit. | ||
| auto const fail = [&](op_status s) { | ||
| d_out[row] = false; | ||
| if (cudf::bit_is_set(dnm, row)) { cudf::clear_bit(dnm, row); } | ||
| cudf::clear_bit(dnm, row); | ||
| if (dp_s) { dp_s[row] = s; } | ||
| }; | ||
| if (dp_s and dp_s[row] != op_status::SUCCESS) { return fail(dp_s[row]); } | ||
| // Status column is always non-nullable; ROW_NULL replaces the null bit. | ||
| if (!cudf::bit_is_set(dnm, row)) { return fail(op_status::ROW_NULL); } | ||
| auto const val = list_row_span(vals, row); | ||
| auto const decoded = decode_bool(val); | ||
| if (!decoded) { return fail(cast_status_for_bool(val)); } | ||
|
|
@@ -1262,7 +1429,10 @@ std::unique_ptr<column> cast_variant(column_view const& values, | |
| case type_id::FLOAT32: | ||
| case type_id::FLOAT64: | ||
| case type_id::BOOL8: | ||
| case type_id::STRING: break; | ||
| case type_id::STRING: | ||
| case type_id::DECIMAL32: | ||
| case type_id::DECIMAL64: | ||
| case type_id::DECIMAL128: break; | ||
| default: CUDF_FAIL("unsupported type for variant cast", std::invalid_argument); | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.