Skip to content

Apply new validation to cuml.explainer - #8043

Merged
rapids-bot[bot] merged 6 commits into
NVIDIA:mainfrom
jcrist:explainer-cleanup
May 5, 2026
Merged

Apply new validation to cuml.explainer#8043
rapids-bot[bot] merged 6 commits into
NVIDIA:mainfrom
jcrist:explainer-cleanup

Conversation

@jcrist

@jcrist jcrist commented May 1, 2026

Copy link
Copy Markdown
Member

This:

  • Fixes all tests to pass (at least on my machine). Some of our tests haven't been running in CI since optional test dependencies weren't installed. I've attempted to fix a few, but xfailed those related to treelite not support xgboost categoricals.
  • Adds shap and lightgbm to our test dependencies. Without these, a few of the explainer tests don't run.
  • Updates cuml.explainer to use the new validation and ingest routines. I did the bare minimum here to get things working, functionally things should still run mostly the same as before (just take a different pipeline).
  • Fixes a bug in TreeExplainer.shap_interaction_values and TreeExplainer.shap_values. These routines always return either a cupy.ndarray or numpy.ndarray, depending on the memory type of the input. However, previously there was a bug where pandas inputs were incorrectly treated as cuda inputs (and cupy returned). This is now fixed and matches the docstrings.

Fixes #7993.

@jcrist jcrist self-assigned this May 1, 2026
@jcrist jcrist added the improvement Improvement / enhancement to an existing function label May 1, 2026
@jcrist
jcrist requested a review from a team as a code owner May 1, 2026 18:25
@jcrist jcrist added the non-breaking Non-breaking change label May 1, 2026
@jcrist
jcrist requested a review from a team as a code owner May 1, 2026 18:25
@jcrist
jcrist requested review from AyodeAwe and betatim May 1, 2026 18:25
@github-actions github-actions Bot added conda conda issue Cython / Python Cython or Python issue labels May 1, 2026
@coderabbitai

coderabbitai Bot commented May 1, 2026

Copy link
Copy Markdown
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added LightGBM and SHAP to development, test, and environment dependencies.
  • Bug Fixes

    • Improved explainer input/output validation and host/GPU return consistency for SHAP outputs.
    • Made SHAP plotting deterministic for reproducible results.
  • Tests

    • Marked several XGBoost categorical tests as expected to fail; updated tests to use the new SHAP retrieval behavior and reuse a pre-fit example model.

Walkthrough

Adds lightgbm and shap to environment and test dependency manifests; refactors cuML explainer modules to use check_array for input validation and changes TreeExplainer I/O and GPU/host return behavior; updates tests (xfails, deterministic plotting, reuse of example RF).

Changes

Cohort / File(s) Summary
Conda Environment Updates
conda/environments/all_cuda-129_arch-aarch64.yaml, conda/environments/all_cuda-129_arch-x86_64.yaml, conda/environments/all_cuda-131_arch-aarch64.yaml, conda/environments/all_cuda-131_arch-x86_64.yaml
Added lightgbm and shap to dependencies in CUDA/arch environment YAMLs.
Dependency manifests
conda/dependencies.yaml, python/cuml/pyproject.toml
Added lightgbm and shap to common/test dependency lists and the test optional extras.
Explainer: core validation refactor
python/cuml/cuml/explainer/base.pyx, python/cuml/cuml/explainer/common.py, python/cuml/cuml/explainer/kernel_shap.pyx, python/cuml/cuml/explainer/sampling.py
Replaced legacy input_to_* conversions with check_array-based validation; updated dtype/shape handling, indexing, return wrapping, and removal of some sparse/type-specific branches.
TreeExplainer refactor
python/cuml/cuml/explainer/tree_shap.pyx
Reworked input/output handling: validate/coerce background with check_array, allocate GPU outputs with cupy, use .data.ptr for pointers, and select host-vs-GPU return (transfer .get() only for host inputs); removed prior output-typing helpers.
Tests
python/cuml/tests/explainer/test_gpu_treeshap.py, python/cuml/tests/explainer/test_shap_plotting.py
Added xfail markers for specific XGBoost categorical tests, changed SHAP value handling to use returned objects directly, made plotting deterministic via fixed RNG, and introduced module-level pre-fitted example RF for reuse.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • dantegd
  • csadorf
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% 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 'Apply new validation to cuml.explainer' accurately reflects the main change: updating the explainer module to use new validation and ingest routines.
Description check ✅ Passed The description is directly related to the changeset, explaining the key motivations: fixing tests, adding dependencies, updating validation routines, and fixing a bug in TreeExplainer.
Linked Issues check ✅ Passed The PR addresses the primary requirement from issue #7993 to update cuml.explainer to new input validation. Code changes across multiple explainer files (base.pyx, common.py, kernel_shap.pyx, sampling.py, tree_shap.pyx) migrate from old conversion functions to the new check_array validation approach. Additionally, the PR fixes a documented bug in TreeExplainer return types for pandas inputs. Test dependencies (shap, lightgbm) are added to support the explainer tests.
Out of Scope Changes check ✅ Passed All changes are within scope: dependency additions directly support explainer testing, validation updates are applied only to explainer modules, SHAP plotting test fixes are necessary for the explainer validation migration, and TreeExplainer bug fixes directly address the issue being resolved.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Review rate limit: 9/10 reviews remaining, refill in 6 minutes.

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

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
python/cuml/cuml/explainer/common.py (1)

35-40: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update model_func_call docstring to match current return behavior.

Line 39 still states this returns CuPy arrays, but the new path at Line 52 can return NumPy or CuPy depending on the model output/memory type.

Suggested docstring tweak
-    Returns the results as CuPy arrays.
+    Returns validated model outputs as array-like values (NumPy/CuPy),
+    preserving host/device type where possible.
As per coding guidelines "Missing docstrings for public methods ... must be addressed".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/cuml/cuml/explainer/common.py` around lines 35 - 40, The docstring for
model_func_call is outdated (it claims the function always returns CuPy arrays)
but the implementation may return NumPy or CuPy depending on the model
output/memory type; update the docstring for model_func_call to clearly describe
input expectations (X and gpu_model), the two code paths (converting
NumPy->model input when gpu_model is False vs passing X directly when gpu_model
is True) and the actual return behavior (may return NumPy arrays or CuPy arrays
depending on the model's output/memory type), and include the return type and
any side-effects so the public method documentation matches current behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@python/cuml/cuml/explainer/sampling.py`:
- Around line 54-75: The code currently coerces all inputs to GPU/CuPy and
rejects sparse inputs; update the input handling to preserve caller memory type
by using input_to_cuml_array(...) instead of check_array, record the original
input_type (numpy/pandas -> output_type="numpy", cudf/cupy -> "cupy", sparse ->
keep sparse and accept_sparse=True), pass that output_type into SimpleImputer
and KMeans, and set check/accept_sparse=True so the is_sparse branch can run;
ensure input_to_cuml_array also preserves row/column major order so subsequent
indexing (used in round_values and is_sparse checks) behaves the same.

---

Outside diff comments:
In `@python/cuml/cuml/explainer/common.py`:
- Around line 35-40: The docstring for model_func_call is outdated (it claims
the function always returns CuPy arrays) but the implementation may return NumPy
or CuPy depending on the model output/memory type; update the docstring for
model_func_call to clearly describe input expectations (X and gpu_model), the
two code paths (converting NumPy->model input when gpu_model is False vs passing
X directly when gpu_model is True) and the actual return behavior (may return
NumPy arrays or CuPy arrays depending on the model's output/memory type), and
include the return type and any side-effects so the public method documentation
matches current behavior.
🪄 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: 987b923e-9239-4b18-ac78-b499b64c926c

📥 Commits

Reviewing files that changed from the base of the PR and between c840432 and c4a5945.

📒 Files selected for processing (13)
  • conda/environments/all_cuda-129_arch-aarch64.yaml
  • conda/environments/all_cuda-129_arch-x86_64.yaml
  • conda/environments/all_cuda-131_arch-aarch64.yaml
  • conda/environments/all_cuda-131_arch-x86_64.yaml
  • dependencies.yaml
  • python/cuml/cuml/explainer/base.pyx
  • python/cuml/cuml/explainer/common.py
  • python/cuml/cuml/explainer/kernel_shap.pyx
  • python/cuml/cuml/explainer/sampling.py
  • python/cuml/cuml/explainer/tree_shap.pyx
  • python/cuml/pyproject.toml
  • python/cuml/tests/explainer/test_gpu_treeshap.py
  • python/cuml/tests/explainer/test_shap_plotting.py

Comment thread python/cuml/cuml/explainer/sampling.py Outdated

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

♻️ Duplicate comments (1)
python/cuml/cuml/explainer/sampling.py (1)

54-56: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve the caller’s memory type through validation and clustering.

check_array defaults to mem_type="device", and both SimpleImputer and KMeans are then forced to output_type="cupy". For pandas/NumPy inputs this now moves the data to device, returns device-backed summaries/labels, and even converts the returned row index to cudf.Index when detailed=True. That’s a behavior regression in exactly the input-preservation path this PR is touching.

Possible localized fix
-    X, index = check_array(
-        X, ensure_2d=False, ensure_all_finite=False, return_index=True
-    )
+    X, index = check_array(
+        X,
+        mem_type=None,
+        ensure_2d=False,
+        ensure_all_finite=False,
+        return_index=True,
+    )
     if X.ndim == 1:
         X = X.reshape(-1, 1)

+    output_type = "cupy" if isinstance(X, cp.ndarray) else "numpy"
+
     # in case there are any missing values in data impute them
     imp = SimpleImputer(
-        missing_values=cp.nan, strategy="mean", output_type="cupy"
+        missing_values=cp.nan, strategy="mean", output_type=output_type
     )
     X = imp.fit_transform(X)

     kmeans = KMeans(
         n_clusters=k,
         random_state=random_state,
-        output_type="cupy",
+        output_type=output_type,
         n_init="auto",
     ).fit(X)

As per coding guidelines, "Correctly handle cuDF, pandas, and NumPy inputs using input_to_cuml_array() for consistent conversion; preserve input type in output where sensible; handle both row-major (C) and column-major (F) memory order".

Also applies to: 61-69, 82-89

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/cuml/cuml/explainer/sampling.py` around lines 54 - 56, The
validation/clustering path currently forces device memory by calling check_array
with the default mem_type and then using SimpleImputer/KMeans with
output_type="cupy", which moves pandas/NumPy inputs to device and converts
indexes to cudf; change this to use input_to_cuml_array() to detect and preserve
the caller's input type and memory order (C/F), pass the detected mem_type into
check_array and downstream SimpleImputer/KMeans instead of hardcoding
output_type="cupy", and ensure any returned summaries/labels and the row index
(when detailed=True) are converted back to the original input type
(pandas/NumPy/cuDF) so the function preserves caller memory type and layout;
update the logic around X, index = check_array(...) and the code paths using
SimpleImputer and KMeans to follow this pattern.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@python/cuml/cuml/explainer/sampling.py`:
- Around line 54-56: The validation/clustering path currently forces device
memory by calling check_array with the default mem_type and then using
SimpleImputer/KMeans with output_type="cupy", which moves pandas/NumPy inputs to
device and converts indexes to cudf; change this to use input_to_cuml_array() to
detect and preserve the caller's input type and memory order (C/F), pass the
detected mem_type into check_array and downstream SimpleImputer/KMeans instead
of hardcoding output_type="cupy", and ensure any returned summaries/labels and
the row index (when detailed=True) are converted back to the original input type
(pandas/NumPy/cuDF) so the function preserves caller memory type and layout;
update the logic around X, index = check_array(...) and the code paths using
SimpleImputer and KMeans to follow this pattern.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: c8ea67bf-244b-4659-9d1c-58f773ffb3c4

📥 Commits

Reviewing files that changed from the base of the PR and between c4a5945 and 096550a.

📒 Files selected for processing (1)
  • python/cuml/cuml/explainer/sampling.py

These tests were skipped in CI before. Unskipping them has turned up a
few lingering tweaks needed to get cudf.pandas working.

@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 the current code and only fix it if needed.

Inline comments:
In `@python/cuml/cuml/explainer/sampling.py`:
- Around line 54-56: Update the check_array call that assigns X and index (the
line "X, index = check_array(...)" in sampling.py) to validate numeric dtypes up
front by passing dtype=np.number (or dtype="numeric") so
non-numeric/object/complex inputs raise a clear validation error before reaching
SimpleImputer or KMeans; ensure numpy is imported if not already and keep the
existing ensure_2d/ensure_all_finite/return_index flags.
🪄 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: e6aa9a93-0568-4fe7-bbb8-a2dd1baea471

📥 Commits

Reviewing files that changed from the base of the PR and between 096550a and 8af207b.

📒 Files selected for processing (3)
  • python/cuml/cuml/explainer/sampling.py
  • python/cuml/cuml/explainer/tree_shap.pyx
  • python/cuml/tests/explainer/test_gpu_treeshap.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • python/cuml/tests/explainer/test_gpu_treeshap.py

Comment thread python/cuml/cuml/explainer/sampling.py

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

One question, but otherwise LGTM.

Comment thread python/cuml/cuml/explainer/tree_shap.pyx
@jcrist

jcrist commented May 4, 2026

Copy link
Copy Markdown
Member Author

/merge

@rapids-bot
rapids-bot Bot merged commit f39dde1 into NVIDIA:main May 5, 2026
101 checks passed
@jcrist
jcrist deleted the explainer-cleanup branch May 5, 2026 17:09
rapids-bot Bot pushed a commit that referenced this pull request May 6, 2026
This test was skipped in CI for years (due to missing dependencies). When I readded them (#8043), I must've forgot to stress test the hypothesis tests, leading to failures in nightlies.

I can confirm that the failures happened the same way before recent changes to explainer, so this isn't a new bug. Three things needed to be fixed:

- A small tweak to the hypothesis generation to normalize `preds` to `numpy`
- Fixup the indexing for 3d shap values in a test (the old method was incorrect, again, this is not a new bug)
- Drop xgboost from the test. The xgboost test runs error due to missing categorical support, (same as other xfails we added). If you drop categorical generation for xgboost, you then get incorrect results. I'm just dropping the test for now.

Authors:
  - Jim Crist-Harif (https://github.com/jcrist)

Approvers:
  - Simon Adorf (https://github.com/csadorf)

URL: #8054
@coderabbitai coderabbitai Bot mentioned this pull request Jun 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

conda conda issue Cython / Python Cython or Python issue improvement Improvement / enhancement to an existing function non-breaking Non-breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Update cuml.explainer to new input validation

5 participants