Skip to content

Support cudf-polars total_xxx datetime extraction methods - #18171

Merged
rapids-bot[bot] merged 28 commits into
NVIDIA:mainfrom
brandon-b-miller:fea-cudf-polars-more-dt-extracts
Jun 26, 2026
Merged

Support cudf-polars total_xxx datetime extraction methods#18171
rapids-bot[bot] merged 28 commits into
NVIDIA:mainfrom
brandon-b-miller:fea-cudf-polars-more-dt-extracts

Conversation

@brandon-b-miller

@brandon-b-miller brandon-b-miller commented Mar 5, 2025

Copy link
Copy Markdown
Contributor

Part of #16481

Since theres two consumers of this API now (pandas and polars), I am wondering if we might take another look at adding libcudf APIs here. xref #16802 and cc @bdice @mroeschke @galipremsagar .

WIP

@brandon-b-miller
brandon-b-miller requested a review from a team as a code owner March 5, 2025 14:41
@github-actions github-actions Bot added Python Affects Python cuDF API. cudf-polars Issues specific to cudf-polars labels Mar 5, 2025
@brandon-b-miller brandon-b-miller added feature request New feature or request 2 - In Progress Currently a work in progress non-breaking Non-breaking change and removed Python Affects Python cuDF API. labels Mar 5, 2025
@github-actions github-actions Bot added the Python Affects Python cuDF API. label Mar 10, 2025
Comment on lines +279 to +312
millis = plc.datetime.extract_datetime_component(
column.obj, plc.datetime.DatetimeComponent.MILLISECOND
)
micros = plc.datetime.extract_datetime_component(
column.obj, plc.datetime.DatetimeComponent.MICROSECOND
)
nanos = plc.datetime.extract_datetime_component(
column.obj, plc.datetime.DatetimeComponent.NANOSECOND
)
millis_as_nanos = plc.binaryop.binary_operation(
millis,
plc.interop.from_arrow(pa.scalar(1_000_000, type=pa.int32())),
plc.binaryop.BinaryOperator.MUL,
plc.types.DataType(plc.types.TypeId.INT32),
)
micros_as_nanos = plc.binaryop.binary_operation(
micros,
plc.interop.from_arrow(pa.scalar(1_000, type=pa.int32())),
plc.binaryop.BinaryOperator.MUL,
plc.types.DataType(plc.types.TypeId.INT32),
)
total_nanos = plc.binaryop.binary_operation(
nanos,
millis_as_nanos,
plc.binaryop.BinaryOperator.ADD,
plc.types.DataType(plc.types.TypeId.INT32),
)
total_nanos = plc.binaryop.binary_operation(
total_nanos,
micros_as_nanos,
plc.binaryop.BinaryOperator.ADD,
plc.types.DataType(plc.types.TypeId.INT32),
)
return Column(total_nanos)

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.

Yeah, I definitely think we need kernels for this and similar things.

Suppose the datetime column is length N. This reads the input column three times, for 3 * N * 8 bytes, writes millis/micros/nanos for 3 * N * 4 bytes, then converts millis to nanos (N * 4 bytes read, N * 4 bytes written), and similarly micros to nanos. Then we add nanos and millis (2 * N * 4 bytes read, N * 4 bytes written) and then add that to micros (another 2 * N * 4 bytes read, N * 4 bytes written).
So we read (6 + 1 + 1 + 2 + 2) * N * 4 = 48 * N bytes and write (3 + 1 + 1 + 1 + 1) * N * 4 = 28 * N bytes.

In contrast, an optimal algorithm reads 8 N bytes and writes 4 N bytes. So we move approximately six times more data than necessary. Given we have perhaps 5-10x memory bandwidth difference compared to a good CPU implementation we kind of can't afford this cost.

I've also ignored the overhead due to chained dependent kernel launch latency.

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.

This code isn't technically part of this PR but I'm wondering if perhaps some kernels might be fusable here using the libcudf ast machinery, looking into this.

@brandon-b-miller
brandon-b-miller changed the base branch from branch-25.04 to branch-25.06 March 21, 2025 13:28
@vyasr

vyasr commented Apr 2, 2025

Copy link
Copy Markdown
Contributor

What is the plan on this PR? I think that we decided to go ahead and get this merged and then put up an issue requesting the libcudf kernel right?

@brandon-b-miller

Copy link
Copy Markdown
Contributor Author

What is the plan on this PR? I think that we decided to go ahead and get this merged and then put up an issue requesting the libcudf kernel right?

This one is WIP still, pending some efficiency improvements

@brandon-b-miller

Copy link
Copy Markdown
Contributor Author

@vyasr the major issue with the libcudf kernels in this case is that they would be single consumer - polars. Both polars and pandas support the singular total_seconds API, but I've found cases where the outputs are different for the same data and I'm not sure they are intended to do the same thing. This has led me to rethink what the best way to push this one forward is.

@coderabbitai

coderabbitai Bot commented Jun 22, 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

TemporalFunction now supports total_* duration extraction through nanosecond-based rescaling, with tests for Duration ms/us/ns values. Column.astype also adds a path for casting integral columns to duration types, with a corresponding unit test.

Changes

total_* Duration Component Extraction

Layer / File(s) Summary
Unit conversion constants and class-variable registration
python/cudf_polars/cudf_polars/dsl/expressions/datetime.py
Adds _unit_to_nanoseconds_conversion, defines _TOTAL_COMPONENT_NANOSECONDS for total_* operations, and extends _valid_ops.
do_evaluate branch for total_* conversion
python/cudf_polars/cudf_polars/dsl/expressions/datetime.py
Adds an early do_evaluate branch that rescales duration values with integer multiply or divide based on source and target nanosecond scales, returns the input when the factor is 1, and preserves existing temporal branches.
Parametrized test for total_* duration fields
python/cudf_polars/tests/expressions/test_datetime_basic.py
Adds duration_extract_fields and a parametrized GPU test covering total_* accessors across Duration ms/us/ns values.

Integral-to-Duration Type Casting

Layer / File(s) Summary
Integral-to-duration casting in Column.astype
python/cudf_polars/cudf_polars/containers/column.py
Adds a cast path from integral columns to duration dtypes by selecting an intermediate integer representation, relabeling the column with the duration dtype, conditionally casting when needed, and preserving sortedness.
Test for integral-to-duration casting
python/cudf_polars/tests/containers/test_column.py
Adds a test that casts a Duration column to Int32 and checks the resulting Polars dtype and underlying pylibcudf type id.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • rapidsai/cudf#18443: Also extends TemporalFunction in python/cudf_polars/.../dsl/expressions/datetime.py with new temporal operation handling and _valid_ops updates.

Suggested reviewers

  • vyasr
  • mroeschke
  • Matt711
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% 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 clearly matches the main change: adding support for cudf-polars total_xxx datetime extraction methods.
Description check ✅ Passed The description is related to the PR and discusses the same API area, even though it is brief and marked WIP.
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

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

@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)
python/cudf_polars/tests/expressions/test_datetime_basic.py (1)

184-213: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consider adding null value coverage.

The test has good numeric coverage including edge cases, but doesn't test null handling. Duration columns can contain nulls, and verifying correct null propagation through total_* operations would strengthen coverage.

pl.Series(
    [
        0,
        None,  # add null values
        1500,
        None,
        -134234534,
    ],
    dtype=dtype,
),
🤖 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 `@python/cudf_polars/tests/expressions/test_datetime_basic.py` around lines 184
- 213, The test_duration_total_component_extract function currently lacks null
value coverage in the durations Series test data. To improve test coverage and
verify correct null propagation through the duration total component extraction
operations, add null values (None) interspersed throughout the list of values in
the pl.Series construction for the "durations" field. This will ensure the test
validates both numeric edge cases and null handling behavior.
🤖 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 `@python/cudf_polars/tests/expressions/test_datetime_basic.py`:
- Around line 184-213: The test_duration_total_component_extract function
currently lacks null value coverage in the durations Series test data. To
improve test coverage and verify correct null propagation through the duration
total component extraction operations, add null values (None) interspersed
throughout the list of values in the pl.Series construction for the "durations"
field. This will ensure the test validates both numeric edge cases and null
handling behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 449c1992-f3f5-4ce7-b9ac-9efc950c60da

📥 Commits

Reviewing files that changed from the base of the PR and between 96896b1 and 0ce3e8e.

📒 Files selected for processing (2)
  • python/cudf_polars/cudf_polars/dsl/expressions/datetime.py
  • python/cudf_polars/tests/expressions/test_datetime_basic.py

result = plc.binaryop.binary_operation(
casted,
plc.Scalar.from_py(
factor, plc.DataType(plc.TypeId.INT64), stream=df.stream

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.

suggestion: if factor is ever 1, we could avoid this plc.binaryop.binary_operation call all together and return Column(casted, dtype=self.dtype)

Comment thread python/cudf_polars/cudf_polars/dsl/expressions/datetime.py Outdated
Comment thread python/cudf_polars/cudf_polars/dsl/expressions/datetime.py
Comment thread python/cudf_polars/cudf_polars/dsl/expressions/datetime.py Outdated
Comment thread python/cudf_polars/cudf_polars/dsl/expressions/datetime.py

@Matt711 Matt711 left a comment

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.

Thanks @brandon-b-miller nothing else blocking from me. If you don't mind can you do a refresh of the table in #16481?

Comment thread python/cudf_polars/cudf_polars/containers/column.py

@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: 1

🤖 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 `@python/cudf_polars/tests/containers/test_column.py`:
- Around line 318-333: The test_astype_duration_to_narrower_integer function
only validates the dtype and type-id of the result but does not verify the
actual converted values, which means data corruption could go undetected. Add
assertions that check the actual values in result.obj match the expected
converted output from the original input [1, 2, -3]. Additionally, enhance the
test with edge case coverage by adding separate test cases or parameterization
for empty columns, null/missing values, and single-element columns to ensure
robustness per the coding guidelines. Consider also adding a complementary test
that covers the integral-to-duration conversion direction to provide complete
bidirectional coverage for the astype functionality.
🪄 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: 7e27609d-7ee7-4776-ae4a-4cd577a3f764

📥 Commits

Reviewing files that changed from the base of the PR and between 3a8a4e2 and 7a25dbb.

📒 Files selected for processing (1)
  • python/cudf_polars/tests/containers/test_column.py

Comment on lines +318 to +333
def test_astype_duration_to_narrower_integer():
stream = get_cuda_stream()
col = Column(
plc.unary.cast(
plc.Column.from_iterable_of_py(
[1, 2, -3], plc.DataType(plc.TypeId.INT64), stream=stream
),
plc.DataType(plc.TypeId.DURATION_MICROSECONDS),
stream=stream,
),
dtype=DataType(pl.Duration(time_unit="us")),
)
target_dtype = DataType(pl.Int32())
result = col.astype(target_dtype, stream=stream)
assert result.dtype == target_dtype
assert result.obj.type().id() == plc.TypeId.INT32

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Test currently verifies metadata but not cast correctness of values

At Line 332–333, the test only checks dtype/type-id. Please also assert the converted values (and add at least one edge case like null/empty/single-element), otherwise value corruption/regressions in astype can pass unnoticed. Also, this test direction (duration→int) does not directly cover the new integral→duration path called out in this PR cohort.

As per coding guidelines: python/**/test_*.py: “Ensure test files provide comprehensive edge case coverage (empty, all-null, single-element, mixed types)”.

🤖 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 `@python/cudf_polars/tests/containers/test_column.py` around lines 318 - 333,
The test_astype_duration_to_narrower_integer function only validates the dtype
and type-id of the result but does not verify the actual converted values, which
means data corruption could go undetected. Add assertions that check the actual
values in result.obj match the expected converted output from the original input
[1, 2, -3]. Additionally, enhance the test with edge case coverage by adding
separate test cases or parameterization for empty columns, null/missing values,
and single-element columns to ensure robustness per the coding guidelines.
Consider also adding a complementary test that covers the integral-to-duration
conversion direction to provide complete bidirectional coverage for the astype
functionality.

Source: Coding guidelines

@brandon-b-miller

Copy link
Copy Markdown
Contributor Author

@Matt711 I'm seeing a coverage miss that I'm not sure is related to this change. It's a line that's involved in _drop_dyn_pred_hints and appears to be uncovered on the earliest version of cudf polars being tested. Does this ring any bells for you?

@Matt711

Matt711 commented Jun 24, 2026

Copy link
Copy Markdown
Member

@Matt711 I'm seeing a coverage miss that I'm not sure is related to this change. It's a line that's involved in _drop_dyn_pred_hints and appears to be uncovered on the earliest version of cudf polars being tested. Does this ring any bells for you?

We only check code coverage on the latest polars version being tested. Looking at the CI logs...

2026-06-23T23:06:13.7595360Z =================================== XPASSES ====================================
2026-06-23T23:06:13.7598961Z -- generated xml file: /__w/cudf/cudf/test-results/junit-cudf-polars-1.41.xml --
2026-06-23T23:06:13.7600945Z ================================ tests coverage ================================
2026-06-23T23:06:13.7602663Z _______________ coverage: platform linux, python 3.14.6-final-0 ________________
2026-06-23T23:06:13.7603956Z 
2026-06-23T23:06:13.7604909Z Name                           Stmts   Miss  Cover   Missing
2026-06-23T23:06:13.7609488Z ------------------------------------------------------------
2026-06-23T23:06:13.7611435Z cudf_polars/dsl/translate.py     468      1    99%   305
2026-06-23T23:06:13.7613033Z ------------------------------------------------------------
2026-06-23T23:06:13.7614625Z TOTAL                           4956      1    99%

But on line 305 we explicitly skip code coverage.

        if left is None:
            return right  # pragma: no cover <-- 305

But your branch (brandon-b-miller:fea-cudf-polars-more-dt-extracts) doesn't seem to skip code coverage on that line. Could you try pulling in main again (it should have already picked up this change which is weird 😕 ) ? And just check that line is skipped (it should be).

Edit:

Interesting, so that skip was not added by me in #22820. I remeber now adding a test specifically for that test. It was added in #22909 which is a commit your branch does not have. So

  1. Yes pulling in main should unblock you
  2. I don't think we should have skipped that line in refactor(streaming): flatten namespaces and rename to snake_case #22909. I'll investigate.

@brandon-b-miller

Copy link
Copy Markdown
Contributor Author

Ah this might just be being behind trunk, lets see if that fixes it 👍

@brandon-b-miller

Copy link
Copy Markdown
Contributor Author

/merge

@rapids-bot
rapids-bot Bot merged commit 38d968a into NVIDIA:main Jun 26, 2026
200 of 203 checks passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in cuDF Python Jun 26, 2026
@brandon-b-miller
brandon-b-miller deleted the fea-cudf-polars-more-dt-extracts branch June 26, 2026 01:30
@Matt711

Matt711 commented Jun 26, 2026

Copy link
Copy Markdown
Member

This PR got in?! 😂🎉

copy-pr-bot Bot pushed a commit that referenced this pull request Jun 29, 2026
Part of #16481

Since theres two consumers of this API now (pandas and polars), I am wondering if we might take another look at adding libcudf APIs here. xref #16802 and cc @bdice @mroeschke @galipremsagar . 

WIP

Authors:
  - https://github.com/brandon-b-miller
  - Vyas Ramasubramani (https://github.com/vyasr)

Approvers:
  - Matthew Murray (https://github.com/Matt711)
  - Matthew Roeschke (https://github.com/mroeschke)

URL: #18171
@coderabbitai coderabbitai Bot mentioned this pull request Jul 8, 2026
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

2 - In Progress Currently a work in progress CMake CMake build issue cudf-polars Issues specific to cudf-polars feature request New feature or request libcudf Affects libcudf (C++/CUDA) code. non-breaking Non-breaking change Python Affects Python cuDF API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants