Skip to content

Apply new validation to cuml.manifold - #8042

Merged
rapids-bot[bot] merged 10 commits into
NVIDIA:mainfrom
jinsolp:validation-manifold
May 7, 2026
Merged

Apply new validation to cuml.manifold#8042
rapids-bot[bot] merged 10 commits into
NVIDIA:mainfrom
jinsolp:validation-manifold

Conversation

@jinsolp

@jinsolp jinsolp commented May 1, 2026

Copy link
Copy Markdown
Contributor

Closes #7997

@jinsolp jinsolp self-assigned this May 1, 2026
@jinsolp
jinsolp requested a review from a team as a code owner May 1, 2026 17:54
@jinsolp
jinsolp requested a review from viclafargue May 1, 2026 17:54
@jinsolp jinsolp added Cython / Python Cython or Python issue improvement Improvement / enhancement to an existing function non-breaking Non-breaking change labels May 1, 2026
@coderabbitai

coderabbitai Bot commented May 1, 2026

Copy link
Copy Markdown

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

Three manifold estimators (SpectralEmbedding, TSNE, UMAP) migrate bespoke input conversion to centralized validation (check_inputs/check_array with return_index), adjust device-buffer usage and CuPy allocations, and change @reflect(reset=True) to @reflect(reset="type").

Changes

Manifold Input Validation Migration

Layer / File(s) Summary
Input validation / Data shape
python/cuml/cuml/manifold/spectral_embedding.pyx, python/cuml/cuml/manifold/t_sne.pyx, python/cuml/cuml/manifold/umap/umap.pyx
Replace input_to_cuml_array/manual conversions with check_inputs/check_array (force float32, F-order where required, allow/limit sparse forms, return_index=True) and obtain index for downstream wrapping.
Core pointer/buffer handling
python/cuml/cuml/manifold/t_sne.pyx, python/cuml/cuml/manifold/umap/umap.pyx
Derive raw C++ pointers from validated CuPy-backed buffers (.data.ptr), convert KNN distances/indices to CuPy (ensure int32 for sparse indices), and switch some internal allocations to cupy.zeros.
Model outputs / wrapping
python/cuml/cuml/manifold/umap/umap.pyx, python/cuml/cuml/manifold/t_sne.pyx, python/cuml/cuml/manifold/spectral_embedding.pyx
Store outputs (embedding, _sigmas, _rhos) as CumlArray(data=..., index=index) using the validated index (or wrap CuPy results into CumlArray with returned index).
Transform / inverse paths
python/cuml/cuml/manifold/umap/umap.pyx
transform and inverse_transform now use check_inputs/check_array with return_index=True, remove explicit n_features_in_ checks, allocate outputs with CuPy, and return CumlArray(data=..., index=index).
Wiring / Imports & decorators
python/cuml/cuml/manifold/..., python/cuml/tests/test_sklearn_compatibility.py
Add check_inputs/check_array imports and remove legacy conversion helpers; update @reflect on fit methods from reset=Truereset="type"; adjust XFAILs for TSNE/UMAP in sklearn compatibility tests.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • rapidsai/cuml#8023 — Migrates other components to check_inputs and updates @reflect reset semantics.
  • rapidsai/cuml#7894 — Extends reflect behavior to accept reset="type" and adjusts runtime semantics.
  • rapidsai/cuml#7768 — Updates XFAIL mappings in sklearn compatibility tests for manifold estimators.

Suggested labels

sklearn-api-compat

Suggested reviewers

  • viclafargue
  • betatim
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Apply new validation to cuml.manifold' directly and clearly summarizes the main change across all modified files in the changeset.
Description check ✅ Passed The description references issue #7997 'Update cuml.manifold to new input validation', which is directly related to the validation changes made in the PR.
Linked Issues check ✅ Passed The PR implements validation updates to SpectralEmbedding, TSNE, and UMAP classes by adding check_inputs validation and updating reflect decorators, aligning with the linked issue objective.
Out of Scope Changes check ✅ Passed All changes are focused on applying new validation to manifold classes and updating test expectations, directly related to the linked issue scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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 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: 2

🤖 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/manifold/spectral_embedding.pyx`:
- Around line 423-429: The call to check_inputs(...) in fit currently ignores
its return value so subsequent code still uses the original X; update fit to
capture the validated/coerced array (e.g., X = check_inputs(self, X, ...)) and
then use that returned X for any further operations (reading X.shape and passing
into spectral_embedding(...)). Make sure the call-sites referencing X.shape and
the spectral_embedding(...) invocation use the validated X (and preserve
accept_sparse/self.affinity behavior).

In `@python/cuml/cuml/manifold/umap/umap.pyx`:
- Around line 1210-1240: check_inputs() can return y as a host (NumPy) array so
directly using y.data.ptr will fail; modify the y handling so when y is not None
you detect whether y has a .data.ptr (CuPy) and otherwise wrap it with CumlArray
to obtain a pointer. Concretely, in the block that sets y_ptr (currently using
y.data.ptr), replace that single access with logic like: if hasattr(y, "data")
and hasattr(y.data, "ptr") then set y_ptr = <uintptr_t>y.data.ptr else create
y_m = CumlArray(data=y, index=index_if_needed) and set y_ptr = y_m.ptr (and
ensure y_m is kept in scope similar to X_m). This uses the existing CumlArray
symbol and aligns with how X is handled so supervised fit works for both host
and device y.
🪄 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: 4648bec4-721d-4ec3-a8cb-7756417c55f3

📥 Commits

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

📒 Files selected for processing (3)
  • python/cuml/cuml/manifold/spectral_embedding.pyx
  • python/cuml/cuml/manifold/t_sne.pyx
  • python/cuml/cuml/manifold/umap/umap.pyx

Comment thread python/cuml/cuml/manifold/spectral_embedding.pyx Outdated
Comment thread python/cuml/cuml/manifold/umap/umap.pyx
@jinsolp
jinsolp force-pushed the validation-manifold branch from ff9bad1 to b332651 Compare May 1, 2026 20:38
@NVIDIA NVIDIA deleted a comment from copy-pr-bot Bot May 1, 2026

@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

♻️ Duplicate comments (1)
python/cuml/cuml/manifold/umap/umap.pyx (1)

1239-1243: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep y device-backed before passing its pointer to native fit.

Line 1242 avoids the NumPy attribute error, but it still leaves supervised fit() passing a host pointer whenever check_inputs() returns host-backed y (the "host" / None mem-type paths above). X is normalized through CumlArray first; y should be treated the same way or the native call can read the wrong memory.

Proposed fix
         cdef uintptr_t y_ptr = 0
+        y_m = None
         if y is not None:
-            y_ptr = <uintptr_t>(
-                y.data.ptr if isinstance(y, cp.ndarray) else y.ctypes.data
-            )
+            y_m = CumlArray(data=y)
+            y_ptr = y_m.ptr
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/cuml/cuml/manifold/umap/umap.pyx` around lines 1239 - 1243, The
supervised fit path currently takes a host pointer for y (setting y_ptr from
y.ctypes.data) which can mismatch X's device memory; ensure y is converted to a
device-backed CumlArray before extracting its pointer (mirror how X is
normalized). Concretely, in the fit flow around check_inputs() replace direct
use of y with a device-normalized object (e.g., create a CumlArray or call the
same input-normalization helper used for X), then set y_ptr from that
device-backed object's data.ptr (and still handle cp.ndarray vs CumlArray
appropriately) so the native fit always receives a device pointer.
🤖 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/manifold/umap/umap.pyx`:
- Around line 1239-1243: The supervised fit path currently takes a host pointer
for y (setting y_ptr from y.ctypes.data) which can mismatch X's device memory;
ensure y is converted to a device-backed CumlArray before extracting its pointer
(mirror how X is normalized). Concretely, in the fit flow around check_inputs()
replace direct use of y with a device-normalized object (e.g., create a
CumlArray or call the same input-normalization helper used for X), then set
y_ptr from that device-backed object's data.ptr (and still handle cp.ndarray vs
CumlArray appropriately) so the native fit always receives a device pointer.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 36bec911-7a0f-4ece-b7e4-2df68d22e310

📥 Commits

Reviewing files that changed from the base of the PR and between ff9bad1 and 7ede7f3.

📒 Files selected for processing (2)
  • python/cuml/cuml/manifold/spectral_embedding.pyx
  • python/cuml/cuml/manifold/umap/umap.pyx
🚧 Files skipped from review as they are similar to previous changes (1)
  • python/cuml/cuml/manifold/spectral_embedding.pyx

Comment on lines +1450 to +1456
if self.hash_input:
if X_input_sparse:
X_for_hash = X.toarray().get()
else:
X_for_hash = cp.asnumpy(X)
if _joblib_hash(X_for_hash) == self._input_hash:
return self.embedding_

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Preserve the current input index on the hash fast path.

Line 1456 returns self.embedding_ directly, so when hash_input=True and the same values are transformed with a different dataframe index, the output keeps the training index instead of index from this call.

Proposed fix
         if self.hash_input:
             if X_input_sparse:
                 X_for_hash = X.toarray().get()
             else:
                 X_for_hash = cp.asnumpy(X)
             if _joblib_hash(X_for_hash) == self._input_hash:
-                return self.embedding_
+                return CumlArray(
+                    data=self.embedding_.to_output("cupy"),
+                    index=index,
+                )
📝 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 self.hash_input:
if X_input_sparse:
X_for_hash = X.toarray().get()
else:
X_for_hash = cp.asnumpy(X)
if _joblib_hash(X_for_hash) == self._input_hash:
return self.embedding_
if self.hash_input:
if X_input_sparse:
X_for_hash = X.toarray().get()
else:
X_for_hash = cp.asnumpy(X)
if _joblib_hash(X_for_hash) == self._input_hash:
return CumlArray(
data=self.embedding_.to_output("cupy"),
index=index,
)

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

🤖 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/cuml/cuml/manifold/umap/umap.pyx`:
- Around line 1198-1209: The current branching can set mem_type="host" for dense
training data (when params.build_algo is NN_DESCENT or clustered
BRUTE_FORCE_KNN), which leaves CumlArray as NumPy-backed and causes
self._raw_data.ptr to be an invalid GPU pointer in lib.transform(...); change
the condition so mem_type="host" is only chosen for sparse inputs—i.e., require
X_is_sparse in the branch that sets mem_type="host" (update the block using
X_is_sparse, params.build_algo, lib.graph_build_algo.NN_DESCENT,
lib.graph_build_algo.BRUTE_FORCE_KNN, and params.build_params.n_clusters) so
dense data uses base_mem_type (keeps a device copy) for later transform() calls.
- Around line 1211-1243: check_inputs can leave y as integer dtype, but
lib.fit[_sparse] expects float*; before computing y_ptr in the umap.fit path
coerce y to float32 and C-contiguous: if y is a cupy ndarray use y =
cp.ascontiguousarray(y, dtype=cp.float32) else use y = np.ascontiguousarray(y,
dtype=np.float32) (or .astype(np.float32, copy=False) with order='C'), then take
the pointer as done now (y.data.ptr for cupy or y.ctypes.data for numpy); update
the code around the y_ptr assignment (referencing check_inputs, the local
variable y and the y_ptr calculation) so the buffer passed to lib.fit is always
float32 and contiguous.
🪄 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: 4205292e-400b-4732-bae5-9ec547e4982e

📥 Commits

Reviewing files that changed from the base of the PR and between 7ede7f3 and 3801565.

📒 Files selected for processing (1)
  • python/cuml/cuml/manifold/umap/umap.pyx

Comment thread python/cuml/cuml/manifold/umap/umap.pyx
Comment thread python/cuml/cuml/manifold/umap/umap.pyx Outdated

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

Thanks for working on this!

Pre-approving to speed up things as most comments are minor things. However, importantly the y array should systematically be located on device memory.

Comment thread python/cuml/cuml/manifold/umap/umap.pyx Outdated
cdef bool X_is_sparse = is_sparse(X)

cdef lib.UMAPParams params
init_params(self, params, n_rows=n_rows, is_sparse=X_is_sparse)

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.

More of a detail, but in principle we would like to leave the estimator in an clean state before input validation. Here init_params sets self._a, self._b, self._n_neighbors before input validation had the opportunity to fail with an exception.

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.

Should we reorder check_inputs before init_params? The issue when we do this is that we need to duplicate some code, becausemem_type for check_inputs depends on build_algo and knn_n_clusters, which are resolved inside init_params.

Do you have any other suggestions?

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.

It is probably not worth changing then.

Comment thread python/cuml/cuml/manifold/umap/umap.pyx
Comment thread python/cuml/cuml/manifold/umap/umap.pyx Outdated
@csadorf

csadorf commented May 7, 2026

Copy link
Copy Markdown
Contributor

/merge

@rapids-bot
rapids-bot Bot merged commit 53b7c04 into NVIDIA:main May 7, 2026
93 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request May 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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.manifold to new input validation

5 participants