Skip to content

Commit b5888e8

Browse files
authored
Remove module class wrapping and coverage tool (#1573)
Addresses issue nv-legate/cupynumeric.internal#1572
1 parent 9939b51 commit b5888e8

44 files changed

Lines changed: 189 additions & 2022 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cupynumeric/__init__.py

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,54 @@
2525

2626
from __future__ import annotations
2727

28-
import numpy as _np
2928

30-
from . import linalg, random, fft, ma # noqa: F401
29+
from . import linalg, random, fft # noqa: F401
3130
from ._array.array import ndarray # noqa: F401
32-
from ._array.util import maybe_convert_to_np_ndarray
3331
from ._dlpack import from_dlpack # noqa: F401
3432
from ._module import * # noqa: F403
3533
from ._ufunc import * # noqa: F403
3634
from ._utils.array import is_supported_dtype, local_task_array # noqa: F401
37-
from ._utils.coverage import clone_module
3835

39-
clone_module(_np, globals(), maybe_convert_to_np_ndarray)
40-
41-
del maybe_convert_to_np_ndarray
42-
del clone_module
43-
del _np
36+
# ============================================================
37+
# NumPy dtypes and constants
38+
# These were previously copied by clone_module(), now explicit
39+
# ============================================================
40+
41+
from numpy import (
42+
# Commonly used dtypes
43+
bool_, # noqa: F401
44+
int8, # noqa: F401
45+
int16, # noqa: F401
46+
int32, # noqa: F401
47+
int64, # noqa: F401
48+
uint8, # noqa: F401
49+
uint16, # noqa: F401
50+
uint32, # noqa: F401
51+
uint64, # noqa: F401
52+
float16, # noqa: F401
53+
float32, # noqa: F401
54+
float64, # noqa: F401
55+
complex64, # noqa: F401
56+
complex128, # noqa: F401
57+
# Type hierarchy (abstract base classes)
58+
integer, # noqa: F401
59+
signedinteger, # noqa: F401
60+
unsignedinteger, # noqa: F401
61+
inexact, # noqa: F401
62+
floating, # noqa: F401
63+
complexfloating, # noqa: F401
64+
# Commonly used constants
65+
pi, # noqa: F401
66+
e, # noqa: F401
67+
inf, # noqa: F401
68+
nan, # noqa: F401
69+
newaxis, # noqa: F401
70+
# Dtype class
71+
dtype, # noqa: F401
72+
# Info functions
73+
iinfo, # noqa: F401
74+
finfo, # noqa: F401
75+
)
4476

4577

4678
def _fixup_version() -> str:

cupynumeric/_array/array.py

Lines changed: 6 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727

2828
from .. import _ufunc
2929
from .._utils.array import max_identity, min_identity, to_core_type
30-
from .._utils.coverage import clone_class, is_implemented
3130
from .._utils.linalg import dot_modes
3231
from ..config import (
3332
FFTDirection,
@@ -49,7 +48,6 @@
4948
broadcast_where,
5049
check_writeable,
5150
convert_to_cupynumeric_ndarray,
52-
maybe_convert_to_np_ndarray,
5351
sanitize_shape,
5452
)
5553

@@ -89,21 +87,6 @@
8987

9088
from math import prod
9189

92-
NDARRAY_INTERNAL = {
93-
"__array_finalize__",
94-
"__array_function__",
95-
"__array_interface__",
96-
"__array_prepare__",
97-
"__array_priority__",
98-
"__array_struct__",
99-
"__array_ufunc__",
100-
"__array_wrap__",
101-
# Avoid auto-wrapping Array API specifics:
102-
"__array_namespace__",
103-
"device",
104-
"to_device",
105-
}
106-
10790

10891
def _warn_and_convert(array: ndarray, dtype: np.dtype[Any]) -> ndarray:
10992
if array.dtype != dtype:
@@ -115,7 +98,6 @@ def _warn_and_convert(array: ndarray, dtype: np.dtype[Any]) -> ndarray:
11598
return array
11699

117100

118-
@clone_class(np.ndarray, NDARRAY_INTERNAL, maybe_convert_to_np_ndarray)
119101
class ndarray:
120102
_thunk: NumPyThunk
121103
_legate_data: dict[str, Any] | None
@@ -312,8 +294,6 @@ def __array_function__(
312294
) -> Any:
313295
import cupynumeric as cn
314296

315-
what = func.__name__
316-
317297
for t in types:
318298
# Be strict about which types we support. Accept superclasses
319299
# (for basic subclassing support) and NumPy.
@@ -323,29 +303,13 @@ def __array_function__(
323303
# We are wrapping all NumPy modules, so we can expect to find the implemented
324304
# NumPy API call in cuPyNumeric.
325305
module = reduce(getattr, func.__module__.split(".")[1:], cn)
326-
cn_func = getattr(module, func.__name__)
327-
328-
# We can't immediately forward to the corresponding cuPyNumeric
329-
# entrypoint. Say that we reached this point because the user code
330-
# invoked `np.foo(x, bar=True)` where `x` is a `cupynumeric.ndarray`.
331-
# If our implementation of `foo` is not complete, and cannot handle
332-
# `bar=True`, then forwarding this call to `cn.foo` would fail. This
333-
# goes against the semantics of `__array_function__`, which shouldn't
334-
# fail if the custom implementation cannot handle the provided
335-
# arguments. Conversely, if the user calls `cn.foo(x, bar=True)`
336-
# directly, that means they requested the cuPyNumeric implementation
337-
# specifically, and the `NotImplementedError` should not be hidden.
338-
if is_implemented(cn_func):
339-
try:
340-
return cn_func(*args, **kwargs)
341-
except NotImplementedError:
342-
# Inform the user that we support the requested API in general,
343-
# but not this specific combination of arguments.
344-
what = f"the requested combination of arguments to {what}"
345-
346-
# We cannot handle this call - raise an error instead of falling back
306+
cn_func = getattr(module, func.__name__, None)
307+
308+
if cn_func is not None:
309+
return cn_func(*args, **kwargs)
310+
347311
raise NotImplementedError(
348-
f"cuPyNumeric has not implemented {what}. "
312+
f"cuPyNumeric has not implemented {func.__name__}. "
349313
f"This function is not available."
350314
)
351315

cupynumeric/_array/util.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,10 +230,9 @@ def maybe_convert_to_np_ndarray(obj: Any) -> Any:
230230
"""
231231
Converts cuPyNumeric arrays into NumPy arrays, otherwise has no effect.
232232
"""
233-
from ..ma import MaskedArray
234233
from .array import ndarray
235234

236-
if isinstance(obj, (ndarray, MaskedArray)):
235+
if isinstance(obj, ndarray):
237236
return obj.__array__()
238237
return obj
239238

cupynumeric/_module/indexing.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,7 @@
2626
)
2727
from .._module.array_dimension import broadcast_arrays, broadcast_to
2828
from .._utils.array import calculate_volume
29-
from .._utils.coverage import is_implemented
3029
from ..lib.array_utils import normalize_axis_index
31-
from ..runtime import runtime
3230
from ..types import NdShape
3331
from .array_joining import hstack
3432
from .array_shape import reshape
@@ -232,12 +230,6 @@ def mask_indices(
232230
"""
233231
# this implementation is based on the Cupy
234232
a = ones((n, n), dtype=bool)
235-
if not is_implemented(mask_func):
236-
runtime.warn(
237-
"Calling non-cuPyNumeric functions in mask_func can result in bad "
238-
"performance",
239-
category=UserWarning,
240-
)
241233
return mask_func(a, k).nonzero()
242234

243235

cupynumeric/_sphinxext/_comparison_util.py

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,8 @@
1818
from types import ModuleType
1919
from typing import TYPE_CHECKING, Any, Iterable, Iterator
2020

21-
from .._utils.coverage import (
22-
GPUSupport,
23-
is_implemented,
24-
is_multi,
25-
is_single,
26-
is_wrapped,
27-
)
21+
from ._gpu_support import parse_gpu_support_from_docstring, GPUSupport
22+
2823
from ._comparison_config import MISSING_NP_REFS, SKIP
2924

3025
if TYPE_CHECKING:
@@ -95,8 +90,8 @@ def filter_wrapped_names(
9590
) -> Iterator[str]:
9691
names = (n for n in dir(obj)) # every name in the module or class
9792
names = (
98-
n for n in names if is_wrapped(getattr(obj, n))
99-
) # that is wrapped
93+
n for n in names if callable(getattr(obj, n, None))
94+
) # Exists and is callable
10095
names = (n for n in names if n not in skip) # except the ones we skip
10196
names = (n for n in names if not n.startswith("_")) # or any private names
10297
return names
@@ -113,17 +108,17 @@ def filter_type_names(obj: Any, *, skip: Iterable[str] = ()) -> Iterator[str]:
113108

114109

115110
def get_item(name: str, np_obj: Any, lg_obj: Any) -> ItemDetail:
116-
lg_attr = getattr(lg_obj, name, None)
117-
118-
# If attribute doesn't exist in cuPyNumeric, mark as not implemented
119-
if lg_attr is None:
120-
implemented = False
121-
single = multi = ""
122-
elif implemented := is_implemented(lg_attr):
123-
single = _support_symbol(is_single(lg_attr))
124-
multi = _support_symbol(is_multi(lg_attr))
111+
if (lg_attr := getattr(lg_obj, name, None)) is not None:
112+
# Parse docstring at build time
113+
single_support, multi_support = parse_gpu_support_from_docstring(
114+
lg_attr
115+
)
116+
single = single_support.value
117+
multi = multi_support.value
118+
implemented = True
125119
else:
126120
single = multi = ""
121+
implemented = False
127122

128123
return ItemDetail(
129124
name=name,
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Copyright 2026 NVIDIA Corporation
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
from __future__ import annotations
16+
17+
from enum import Enum
18+
from typing import Any
19+
20+
21+
class GPUSupport(Enum):
22+
YES = 1
23+
NO = 2
24+
PARTIAL = 3
25+
26+
27+
def parse_gpu_support_from_docstring(
28+
obj: Any,
29+
) -> tuple[GPUSupport, GPUSupport]:
30+
"""
31+
Extract GPU support from function docstring.
32+
33+
Looks for these patterns in the docstring:
34+
- "Single GPU" -> single_gpu = YES
35+
- "Multiple GPUs" -> multi_gpu = YES
36+
- "Multiple GPUs (partial)" -> multi_gpu = PARTIAL
37+
38+
Parameters
39+
----------
40+
obj : Any
41+
Function or method object
42+
43+
Returns
44+
-------
45+
single_gpu : GPUSupport
46+
Single GPU support level
47+
multi_gpu : GPUSupport
48+
Multi GPU support level
49+
"""
50+
doc = getattr(obj, "__doc__", None) or ""
51+
52+
# Parse multi-GPU support
53+
if "Multiple GPUs (partial)" in doc:
54+
multi = GPUSupport.PARTIAL
55+
elif "Multiple GPUs" in doc:
56+
multi = GPUSupport.YES
57+
else:
58+
multi = GPUSupport.NO
59+
60+
# Parse single-GPU support
61+
if "Single GPU" in doc:
62+
single = GPUSupport.YES
63+
else:
64+
# If multi-GPU works, single-GPU definitely works
65+
single = multi if multi != GPUSupport.NO else GPUSupport.NO
66+
67+
return single, multi

cupynumeric/_sphinxext/implemented_index.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,15 @@
2222

2323
import cupynumeric as cn
2424

25-
from .._utils.coverage import is_implemented
2625
from . import PARALLEL_SAFE, SphinxParallelSpec
2726
from ._cupynumeric_directive import CupynumericDirective
2827

2928
log = getLogger(__name__)
3029

3130

3231
def _filter(x: Any) -> bool:
33-
return (
34-
callable(x)
35-
and is_implemented(x)
36-
and (x.__name__.startswith("__") or not x.__name__.startswith("_"))
32+
return callable(x) and (
33+
x.__name__.startswith("__") or not x.__name__.startswith("_")
3734
)
3835

3936

0 commit comments

Comments
 (0)