-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Replace rolling.apply implementation with numba-cuda-mlir #23598
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
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
02c1f3c
Replace rolling.apply implementation with numba cuda mlir implementation
mroeschke 6f80d19
pre-commit
mroeschke 9015e63
Merge remote-tracking branch 'upstream/main' into cudf/ref/rolling_ag…
mroeschke a33e371
Merge remote-tracking branch 'upstream/main' into cudf/ref/rolling_ag…
mroeschke c3e51b9
Merge remote-tracking branch 'upstream/main' into cudf/ref/rolling_ag…
mroeschke 791a392
Address coderabbit review
mroeschke 5ebca60
Address review in jit_rolling_apply
mroeschke c9caf83
Add test for raising with nulls
mroeschke 339e16f
Merge remote-tracking branch 'upstream/main' into cudf/ref/rolling_ag…
mroeschke b8af893
Merge remote-tracking branch 'upstream/main' into cudf/ref/rolling_ag…
mroeschke File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| from __future__ import annotations | ||
|
|
||
| import functools | ||
| from typing import TYPE_CHECKING | ||
|
|
||
| import cupy as cp | ||
| import numpy as np | ||
| from numba_cuda_mlir import compiler, cuda | ||
| from numba_cuda_mlir.numba_cuda.core import config as _mlir_config | ||
| from numba_cuda_mlir.numba_cuda.np import numpy_support | ||
|
|
||
| from cudf.core.column.column import ColumnBase, as_column | ||
| from cudf.core.udf.utils import UDFError | ||
| from cudf.utils.performance_tracking import _performance_tracking | ||
|
|
||
| if TYPE_CHECKING: | ||
| from collections.abc import Callable | ||
|
|
||
|
|
||
| class _MLIRNumbaCudaConfig: | ||
| """Silence numba_cuda_mlir low-occupancy warnings during launch.""" | ||
|
|
||
| def __enter__(self) -> None: | ||
| self._low_occupancy_warnings = _mlir_config.CUDA_LOW_OCCUPANCY_WARNINGS | ||
| _mlir_config.CUDA_LOW_OCCUPANCY_WARNINGS = 0 | ||
|
|
||
| def __exit__(self, exc_type, exc_value, traceback) -> None: | ||
| _mlir_config.CUDA_LOW_OCCUPANCY_WARNINGS = self._low_occupancy_warnings | ||
|
|
||
|
|
||
| def _get_udf_return_type(func: Callable, value_dtype: np.dtype) -> np.dtype: | ||
| """Compile ``func`` for a 1D window of ``value_dtype`` to infer its | ||
| output dtype. | ||
| """ | ||
| nb_value_type = numpy_support.from_dtype(value_dtype) | ||
| signature = (nb_value_type[::1],) | ||
| try: | ||
| _, return_type = compiler.compile( | ||
| func, signature, device=True, output="ptx" | ||
| ) | ||
| except Exception as e: | ||
| raise UDFError(str(e)) from e | ||
| return np.dtype(numpy_support.as_dtype(return_type)) | ||
|
|
||
|
|
||
| def _make_rolling_kernel(device_func): | ||
| @cuda.jit | ||
| def _kernel(data, start, end, out, valid, min_periods): | ||
| i = cuda.grid(1) | ||
| if i < out.size: | ||
| begin = start[i] | ||
| stop = end[i] | ||
| count = stop - begin | ||
| if count >= min_periods: | ||
| out[i] = device_func(data[begin:stop]) | ||
| valid[i] = True | ||
| else: | ||
| valid[i] = False | ||
|
|
||
| return _kernel | ||
|
|
||
|
|
||
| @functools.lru_cache(maxsize=32) | ||
| def _compile_or_get_kernel(func: Callable, value_dtype: np.dtype): | ||
| return_dtype = _get_udf_return_type(func, value_dtype) | ||
| device_func = cuda.jit(device=True)(func) | ||
| kernel = _make_rolling_kernel(device_func) | ||
| return kernel, return_dtype | ||
|
|
||
|
|
||
| @_performance_tracking | ||
| def jit_rolling_apply( | ||
| source_column: ColumnBase, | ||
| start: cp.ndarray, | ||
| end: cp.ndarray, | ||
| min_periods: int, | ||
| func: Callable, | ||
| ) -> ColumnBase: | ||
| """Apply a user-defined function to each rolling window using a custom | ||
| CUDA kernel compiled with ``numba_cuda_mlir``. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| source_column : ColumnBase | ||
| The (non-null) numeric column the windows are drawn from. | ||
| start, end : cupy.ndarray | ||
| ``size_type`` arrays giving the absolute ``[start, end)`` row | ||
| indices of each row's window. | ||
| min_periods : int | ||
| Minimum number of observations in a window required to produce a | ||
| non-null result. | ||
| func : callable | ||
| The user-defined function. Receives a 1D array (the window) and | ||
| returns a scalar. | ||
| """ | ||
| value_dtype = source_column.dtype | ||
| kernel, return_dtype = _compile_or_get_kernel(func, value_dtype) | ||
|
|
||
| n = len(source_column) | ||
| if n == 0: | ||
| return as_column(cp.empty(0, dtype=return_dtype)) | ||
|
|
||
| data = source_column.values | ||
| out = cp.empty(n, dtype=return_dtype) | ||
| valid = cp.zeros(n, dtype=np.bool_) | ||
|
|
||
| with _MLIRNumbaCudaConfig(): | ||
| kernel.forall(n)(data, start, end, out, valid, min_periods) | ||
|
|
||
| result = as_column(out) | ||
| valid_col = as_column(valid) | ||
| mask, null_count = valid_col.as_mask() | ||
| return result.set_mask(mask, null_count) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -356,24 +356,55 @@ def _plc_windows(self) -> WindowTypePair: | |
| f"not {type(self.window).__name__}" | ||
| ) | ||
|
|
||
| def _window_start_end(self) -> tuple[cupy.ndarray, cupy.ndarray]: | ||
| """ | ||
| Return the absolute ``[start, end)`` row indices of each row's window | ||
| as ``size_type`` cupy arrays, used by the UDF (``apply``) kernel path. | ||
| """ | ||
| n = len(self.obj) | ||
| idx = cupy.arange(n, dtype=SIZE_TYPE_DTYPE) | ||
| pre, fwd = self._plc_windows | ||
| if isinstance(pre, int): | ||
| start = idx - (pre - 1) | ||
| end = idx + (fwd + 1) | ||
| else: | ||
| preceding = cupy.asarray( | ||
| ColumnBase.from_pylibcudf(pre).astype(SIZE_TYPE_DTYPE).values | ||
| ) | ||
| following = cupy.asarray( | ||
| ColumnBase.from_pylibcudf(fwd).astype(SIZE_TYPE_DTYPE).values | ||
| ) | ||
| start = idx - preceding + np.int32(1) | ||
| end = idx + following + np.int32(1) | ||
| start = cupy.clip(start, 0, n).astype(SIZE_TYPE_DTYPE) | ||
| end = cupy.clip(end, 0, n).astype(SIZE_TYPE_DTYPE) | ||
|
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. This might be a source of some of the bottlenecks. There's no action item from me here, but if we ever want to push perf we should see if we can fold this logic somehow into the main numba-cuda-mlir kernel that also contains the UDF logic. |
||
| return start, end | ||
|
|
||
| def _apply_agg_column( | ||
| self, source_column: ColumnBase, agg_name: str | Callable, **agg_kwargs | ||
| ) -> ColumnBase: | ||
| if isinstance(source_column.dtype, CategoricalDtype): | ||
| # pandas window aggregations operate on the category values, | ||
| # 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 | ||
|
mroeschke marked this conversation as resolved.
|
||
|
|
||
| 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 | ||
| ) | ||
|
mroeschke marked this conversation as resolved.
|
||
|
|
||
| pre, fwd = self._plc_windows | ||
|
|
||
| rolling_agg = aggregation.make_aggregation( | ||
| agg_name, | ||
| {"dtype": source_column.dtype} | ||
| if callable(agg_name) | ||
| else agg_kwargs, | ||
| agg_name, agg_kwargs | ||
| ).plc_obj | ||
|
|
||
| min_periods = 1 if self.min_periods is None else self.min_periods | ||
| if self.min_periods == 0 and isinstance(agg_name, str): | ||
| if self.min_periods == 0: | ||
| # libcudf supports min_periods=0 and returns identity values for windows with | ||
| # insufficient observations: SUM and COUNT return 0, MIN returns the maximum | ||
| # value for the type, MAX returns the minimum value for the type. Only SUM and | ||
|
|
@@ -394,9 +425,7 @@ def _apply_agg_column( | |
| plc_result, dtype_from_pylibcudf_column(plc_result) | ||
| ) | ||
|
|
||
| if isinstance(agg_name, str): | ||
| return col.astype(np.dtype("float64")) | ||
| return col | ||
| return col.astype(np.dtype("float64")) | ||
|
|
||
| def _reduce( | ||
| self, | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should consider a hard error when the user passes a column with a null mask
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, this is already done higher up in
applythat eventually calls thiscudf/python/cudf/cudf/core/window/rolling.py
Line 605 in bf87e06
We didn't have a unit test though, so added in c9caf83