Skip to content

Replace rolling.apply implementation with numba-cuda-mlir - #23598

Open
mroeschke wants to merge 2 commits into
NVIDIA:mainfrom
mroeschke:cudf/ref/rolling_agg_mlir
Open

Replace rolling.apply implementation with numba-cuda-mlir#23598
mroeschke wants to merge 2 commits into
NVIDIA:mainfrom
mroeschke:cudf/ref/rolling_agg_mlir

Conversation

@mroeschke

Copy link
Copy Markdown
Contributor

Description

closes #23555

Primarily agent generated implementation of replacing the prior rolling.apply (PTX UDF aggregation via libcudf) to a pure numba cuda mlir implementation (dedicated numba cuda kernel that jits the users UDF)

For a "simple UDF" w/ a small window size e.g.

N = 5_000_000
WINDOW = 5

rng = cp.random.default_rng(0)
gsr = cudf.Series(rng.integers(0, 100, size=N, dtype="int64"))


def some_func(window):
    total = 0.0
    for value in window:
        total += value
    return total / len(window)

The new implementation is ~5.5x slower

  • 1.57 ms ± 1.31 µs (main)
  • 8.62 ms ± 13 µs (PR)

For a "complex UDF" w/ a larger window size e.g.

N = 5_000_000
WINDOW = 500


rng = cp.random.default_rng(0)
gsr = cudf.Series(rng.integers(1, 100, size=N, dtype="int64"))


def some_func(window):
    acc = 0.0
    for value in window:
        acc += math.sqrt(value) * math.log(value + 1.0)
    return acc / len(window)

The new implementation is about equivalent to the old implementation

  • 503 ms ± 22 µs (main)
  • 509 ms ± 32 µs (PR)

cc @brandon-b-miller

Checklist

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

@mroeschke mroeschke self-assigned this Aug 7, 2026
@mroeschke
mroeschke requested a review from a team as a code owner August 7, 2026 23:41
@mroeschke mroeschke added the improvement Improvement / enhancement to an existing function label Aug 7, 2026
@mroeschke
mroeschke requested a review from TomAugspurger August 7, 2026 23:41
@mroeschke mroeschke added the non-breaking Non-breaking change label Aug 7, 2026
@mroeschke
mroeschke requested a review from bdice August 7, 2026 23:41
@github-actions github-actions Bot added the Python Affects Python cuDF API. label Aug 7, 2026
@GPUtester GPUtester moved this to In Progress in cuDF Python Aug 7, 2026
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added CUDA-accelerated support for applying user-defined functions to rolling windows.
    • Rolling UDFs now support grouped data and forward-looking window operations.
    • Results from rolling UDFs are returned consistently with float64 output.
  • Bug Fixes

    • Improved handling of empty windows and minimum-period validity requirements.
    • Rolling UDF compilation failures now provide clearer errors.
  • Removed

    • Removed the legacy UDF aggregation interface.

Walkthrough

Rolling UDF execution now uses cached CUDA-MLIR kernels over precomputed window bounds. Legacy UDF aggregation compilation was removed. Rolling results are cast to float64, and grouped and forward-window cases have new pandas-based tests.

Changes

Rolling UDF migration

Layer / File(s) Summary
Remove legacy UDF aggregation path
python/cudf/cudf/core/_internals/aggregation.py
Removes legacy NumPy and Numba UDF compilation support and updates make_aggregation documentation.
Compile and execute rolling UDFs
python/cudf/cudf/core/udf/rolling_utils.py
Adds CUDA-MLIR compilation, kernel caching, indexed-window execution, min_periods validity handling, CUDA synchronization, and masked column output.
Integrate rolling dispatch and validation
python/cudf/cudf/core/window/rolling.py, python/cudf/cudf/tests/window/test_rolling.py
Computes absolute window bounds, dispatches callable aggregations through jit_rolling_apply, casts results to float64, and tests grouped and fixed-forward rolling UDFs.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Possibly related issues

Possibly related PRs

  • rapidsai/cudf#23468 — Also modifies rolling-window execution, including fixed-size rolling support.

Suggested reviewers: bdice, tomaugspurger, kylefromnvidia

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes replacing the rolling.apply implementation with a Numba-CUDA-MLIR implementation.
Description check ✅ Passed The description directly explains the rolling.apply replacement, implementation approach, performance, and test coverage.
Linked Issues check ✅ Passed The changes implement rolling.apply with precomputed windows and a dedicated Numba-CUDA-MLIR kernel, matching issue #23555.
Out of Scope Changes check ✅ Passed The changes remain focused on replacing rolling UDF execution and removing the obsolete PTX aggregation path.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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.

Actionable comments posted: 3

🤖 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/cudf/core/window/rolling.py`:
- Around line 393-399: Update the callable branch in the rolling aggregation
path to cast the result of jit_rolling_apply to float64 before returning it,
matching the dtype behavior applied later in the method. Add or update the
relevant assertion for integer-returning UDFs while preserving the existing
Python-float result expectations.
- Around line 393-399: Update the Rolling.apply docstring to document the
numba_cuda_mlir UDF execution path used by the callable branch and its currently
supported features. Remove outdated libcudf and PTX-specific limitations, and
explicitly state that inputs containing nulls and passing args or kwargs are
unsupported.
- Line 391: The default assignment in _apply_agg_column should use
self.window.window_size when self.min_periods is None, preserving explicit
min_periods values. Add a test for FixedForwardWindowIndexer(window_size=3)
without min_periods that verifies the final one- and two-row windows are null.
🪄 Autofix

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: 81b6b25c-aedd-40c7-b5aa-1dbdf34da058

📥 Commits

Reviewing files that changed from the base of the PR and between 6a22d1d and 6f80d19.

📒 Files selected for processing (4)
  • python/cudf/cudf/core/_internals/aggregation.py
  • python/cudf/cudf/core/udf/rolling_utils.py
  • python/cudf/cudf/core/window/rolling.py
  • python/cudf/cudf/tests/window/test_rolling.py

# not the codes
source_column = source_column._get_decategorized_column() # type: ignore[attr-defined]

min_periods = 1 if self.min_periods is None else self.min_periods

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 | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- candidate files ---'
fd -i 'rolling|window|indexer' python/cudf tests 2>/dev/null | head -120 || true

printf '%s\n' '--- symbols and call sites ---'
rg -n --glob '*.py' --glob '*.pyx' \
  'class (BaseIndexer|FixedForwardWindowIndexer)|FixedForwardWindowIndexer|min_periods|jit_rolling_apply|_window_start_end' \
  python/cudf tests 2>/dev/null | head -300

printf '%s\n' '--- outline ---'
ast-grep outline python/cudf/cudf/core/window/rolling.py 2>/dev/null | head -160 || true

printf '%s\n' '--- implementation context ---'
sed -n '330,455p' python/cudf/cudf/core/window/rolling.py

printf '%s\n' '--- indexer definitions ---'
rg -l --glob '*.py' --glob '*.pyx' 'class (BaseIndexer|FixedForwardWindowIndexer)' python/cudf | while read -r f; do
  echo "FILE: $f"
  sed -n '1,240p' "$f"
done

Repository: rapidsai/cudf

Length of output: 19833


🏁 Script executed:

#!/bin/bash
set -eu
sed -n '330,455p' python/cudf/cudf/core/window/rolling.py
rg -n --glob '*.py' --glob '*.pyx' 'class (BaseIndexer|FixedForwardWindowIndexer)|FixedForwardWindowIndexer|min_periods|jit_rolling_apply|_window_start_end' python/cudf tests 2>/dev/null | head -300

Repository: rapidsai/cudf

Length of output: 18510


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- rolling.py relevant lines ---'
nl -ba python/cudf/cudf/core/window/rolling.py | sed -n '300,455p'

printf '%s\n' '--- indexer and test locations ---'
rg -n --glob '*.py' --glob '*.pyx' --glob '*.md' \
  'BaseIndexer|FixedForwardWindowIndexer|min_periods' . | head -400

printf '%s\n' '--- rolling tests ---'
fd -i 'rolling' tests python | head -100

Repository: rapidsai/cudf

Length of output: 22475


🏁 Script executed:

#!/bin/bash
set -eu
nl -ba python/cudf/cudf/core/window/rolling.py | sed -n '330,455p'
rg -n --glob '*.py' --glob '*.pyx' --glob '*.md' \
  'BaseIndexer|FixedForwardWindowIndexer|min_periods|jit_rolling_apply' . | head -400
fd -i 'rolling' tests python | head -100

Repository: rapidsai/cudf

Length of output: 22655


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- documented default and normalization ---'
sed -n '60,95p;640,695p' python/cudf/cudf/core/window/rolling.py

printf '%s\n' '--- existing BaseIndexer tests ---'
sed -n '385,425p;475,545p;620,650p' python/cudf/cudf/tests/window/test_rolling.py

printf '%s\n' '--- UDF min_periods behavior ---'
sed -n '35,125p' python/cudf/cudf/core/udf/rolling_utils.py

printf '%s\n' '--- pandas behavioral probe ---'
python3 - <<'PY'
import pandas as pd

indexer = pd.api.indexers.FixedForwardWindowIndexer(window_size=3)
s = pd.Series([10, 20, 30, 40])

for operation in ("sum", "apply"):
    rolling = s.rolling(window=indexer)
    result = getattr(rolling, operation)(lambda x: x.sum() if len(x) else 0, raw=True) if operation == "apply" else rolling.sum()
    print(operation, result.tolist(), result.isna().tolist())

print("rolling_min_periods", rolling.min_periods)
PY

Repository: rapidsai/cudf

Length of output: 10542


🌐 Web query:

pandas Rolling min_periods BaseIndexer FixedForwardWindowIndexer default window_size source

💡 Result:

In pandas, the BaseIndexer and its subclass FixedForwardWindowIndexer have a default window_size of 0 [1][2][3]. This default value indicates that, by default, the indexer is configured to consider all observations within the window [4][2][3]. Regarding min_periods in the context of rolling operations: 1. For standard integer-based windows, min_periods defaults to the size of the window [5][6]. 2. For offset-based windows, min_periods defaults to 1 [5][6]. 3. When using a BaseIndexer subclass like FixedForwardWindowIndexer as the window argument in rolling(), the min_periods parameter is explicitly passed to the indexer's get_window_bounds method [5][1]. If you do not specify min_periods in the rolling() call, it defaults to None, and the behavior of the indexer depends on the implementation of its get_window_bounds method [5][1][7]. The source code for BaseIndexer and FixedForwardWindowIndexer is located in the pandas repository within pandas/core/indexers/objects.py [1][7]. The logic for how rolling() handles these indexers can be found in pandas/core/window/rolling.py [5].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from urllib.request import urlopen

urls = [
    "https://raw.githubusercontent.com/pandas-dev/pandas/main/pandas/core/window/rolling.py",
    "https://raw.githubusercontent.com/pandas-dev/pandas/main/pandas/core/indexers/objects.py",
]

for url in urls:
    print(f"--- {url} ---")
    text = urlopen(url, timeout=15).read().decode()
    lines = text.splitlines()
    needles = (
        "min_periods =",
        "class FixedForwardWindowIndexer",
        "def get_window_bounds",
        "window_size",
    )
    for i, line in enumerate(lines):
        if any(needle in line for needle in needles):
            lo = max(0, i - 5)
            hi = min(len(lines), i + 18)
            print("\n".join(f"{j + 1}: {lines[j]}" for j in range(lo, hi)))
            print()
PY

Repository: rapidsai/cudf

Length of output: 2533


🏁 Script executed:

#!/bin/bash
set -eu

for spec in \
  "https://raw.githubusercontent.com/pandas-dev/pandas/main/pandas/core/window/rolling.py|rolling.py" \
  "https://raw.githubusercontent.com/pandas-dev/pandas/main/pandas/core/indexers/objects.py|objects.py"
do
  url=${spec%%|*}
  name=${spec##*|}
  curl -fsSLk "$url" -o "/tmp/$name"
  echo "--- $name ---"
  rg -n -A16 -B6 \
    'min_periods = \(|class FixedForwardWindowIndexer|def get_window_bounds|window_size' \
    "/tmp/$name" | head -260
done

Repository: rapidsai/cudf

Length of output: 16300


Preserve the BaseIndexer window-size default for min_periods.

When min_periods is omitted for FixedForwardWindowIndexer(window_size=3), _apply_agg_column passes 1 instead of 3. The final one- and two-row windows therefore produce values instead of nulls. Derive the default from self.window.window_size and add a forward-indexer test without an explicit min_periods.

🤖 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/cudf/core/window/rolling.py` at line 391, The default assignment
in _apply_agg_column should use self.window.window_size when self.min_periods is
None, preserving explicit min_periods values. Add a test for
FixedForwardWindowIndexer(window_size=3) without min_periods that verifies the
final one- and two-row windows are null.

Source: Coding guidelines

Comment on lines +393 to +399
if callable(agg_name):
from cudf.core.udf.rolling_utils import jit_rolling_apply

start, end = self._window_start_end()
return jit_rolling_apply(
source_column, start, end, min_periods, agg_name
)

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

Cast callable rolling results to float64.

The early return bypasses line 428. A UDF that returns an integer or float32 therefore exposes its inferred dtype, while this change requires rolling results to use float64.

Cast the jit_rolling_apply result before returning it. Add an integer-returning UDF assertion because both new tests return a Python float.

Proposed fix
-            return jit_rolling_apply(
-                source_column, start, end, min_periods, agg_name
-            )
+            return jit_rolling_apply(
+                source_column, start, end, min_periods, agg_name
+            ).astype(np.dtype("float64"))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if callable(agg_name):
from cudf.core.udf.rolling_utils import jit_rolling_apply
start, end = self._window_start_end()
return jit_rolling_apply(
source_column, start, end, min_periods, agg_name
)
if callable(agg_name):
from cudf.core.udf.rolling_utils import jit_rolling_apply
start, end = self._window_start_end()
return jit_rolling_apply(
source_column, start, end, min_periods, agg_name
).astype(np.dtype("float64"))
🤖 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/cudf/core/window/rolling.py` around lines 393 - 399, Update the
callable branch in the rolling aggregation path to cast the result of
jit_rolling_apply to float64 before returning it, matching the dtype behavior
applied later in the method. Add or update the relevant assertion for
integer-returning UDFs while preserving the existing Python-float result
expectations.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- rolling.py outline ---'
ast-grep outline python/cudf/cudf/core/window/rolling.py
printf '%s\n' '--- relevant implementation and docstring references ---'
rg -n -C 8 'def apply|Rolling\\.apply|numba_cuda_mlir|libcudf|PTX|rolling_apply|jit_rolling_apply' python/cudf/cudf/core/window/rolling.py python/cudf/cudf/core/udf python/cudf -g '*.py' -g '*.pyx'

Repository: rapidsai/cudf

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- rolling.py outline ---'
ast-grep outline python/cudf/cudf/core/window/rolling.py
printf '%s\n' '--- relevant implementation and docstring references ---'
rg -n -C 8 'def apply|Rolling\.apply|numba_cuda_mlir|libcudf|PTX|rolling_apply|jit_rolling_apply' \
  python/cudf/cudf/core/window/rolling.py \
  python/cudf/cudf/core/udf \
  python/cudf -g '*.py' -g '*.pyx'

Repository: rapidsai/cudf

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- Rolling.apply docstring ---'
sed -n '560,635p' python/cudf/cudf/core/window/rolling.py
printf '%s\n' '--- rolling UDF implementation ---'
wc -l python/cudf/cudf/core/udf/rolling_utils.py
ast-grep outline python/cudf/cudf/core/udf/rolling_utils.py
sed -n '1,260p' python/cudf/cudf/core/udf/rolling_utils.py
printf '%s\n' '--- UDF backend and feature documentation references ---'
rg -n -C 4 'numba_cuda_mlir|supported.*feature|unsupported|Rolling\.apply|rolling.*apply|Masked|NA' \
  python/cudf/cudf/core/udf \
  python/cudf/cudf/core/window/rolling.py \
  docs -g '*.py' -g '*.rst' -g '*.md' -g '*.yaml' 2>/dev/null | head -n 500

Repository: rapidsai/cudf

Length of output: 44606


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- remainder of Rolling.apply ---'
sed -n '630,700p' python/cudf/cudf/core/window/rolling.py
printf '%s\n' '--- rolling apply tests and documentation ---'
rg -n -C 6 'rolling.*apply|apply\(.*count_if|jit_rolling_apply|cmath|math\.sin|global variables|PTX|numba_cuda_mlir' \
  python/cudf tests docs -g '*.py' -g '*.rst' -g '*.md' 2>/dev/null | head -n 800
printf '%s\n' '--- repository changes touching the relevant files ---'
git diff --stat -- python/cudf/cudf/core/window/rolling.py python/cudf/cudf/core/udf/rolling_utils.py
git status --short -- python/cudf/cudf/core/window/rolling.py python/cudf/cudf/core/udf/rolling_utils.py

Repository: rapidsai/cudf

Length of output: 50369


Update the Rolling.apply docstring. Describe the numba_cuda_mlir UDF path and its current supported features. Remove stale libcudf and PTX-specific restrictions, and document that null-containing inputs and args/kwargs are unsupported.

🤖 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/cudf/core/window/rolling.py` around lines 393 - 399, Update the
Rolling.apply docstring to document the numba_cuda_mlir UDF execution path used
by the callable branch and its currently supported features. Remove outdated
libcudf and PTX-specific limitations, and explicitly state that inputs
containing nulls and passing args or kwargs are unsupported.

Source: Coding guidelines

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

Labels

improvement Improvement / enhancement to an existing function non-breaking Non-breaking change Python Affects Python cuDF API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEA] Reimplement rolling().apply() in Numba-CUDA-MLIR

2 participants