Skip to content
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

Fallback to self = np.ndarray when necessary #431

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cunumeric/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from legate.core import Array

from .config import FFTDirection, FFTNormalization, UnaryOpCode, UnaryRedCode
from .coverage import clone_class
from .coverage import clone_np_ndarray
from .runtime import runtime
from .utils import dot_modes

Expand Down Expand Up @@ -148,7 +148,7 @@ def _convert_all_to_numpy(obj):
return obj


@clone_class(np.ndarray)
@clone_np_ndarray
class ndarray:
def __init__(
self,
Expand Down
99 changes: 51 additions & 48 deletions cunumeric/coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@
from dataclasses import dataclass
from functools import wraps
from types import FunctionType, MethodDescriptorType, MethodType, ModuleType
from typing import Any, Callable, Container, Mapping, Optional, TypeVar, cast
from typing import Any, Container, Mapping, Optional, cast

import numpy as np
from typing_extensions import Protocol

from .runtime import runtime
from .utils import find_last_user_frames, find_last_user_stacklevel

__all__ = ("clone_class", "clone_module")
__all__ = ("clone_module", "clone_np_ndarray")

FALLBACK_WARNING = (
"cuNumeric has not implemented {name} "
Expand Down Expand Up @@ -118,7 +119,11 @@ def wrapper(*args: Any, **kwargs: Any) -> Any:


def unimplemented(
func: AnyCallable, prefix: str, name: str, reporting: bool = True
func: AnyCallable,
prefix: str,
name: str,
reporting: bool = True,
self_fallback: Optional[str] = None,
) -> CuWrapped:
name = f"{prefix}.{name}"

Expand Down Expand Up @@ -150,6 +155,9 @@ def wrapper(*args: Any, **kwargs: Any) -> Any:
location=location,
implemented=False,
)
if self_fallback:
self_value = getattr(args[0], self_fallback)()
args = (self_value,) + args[1:]
return func(*args, **kwargs)

else:
Expand All @@ -162,6 +170,9 @@ def wrapper(*args: Any, **kwargs: Any) -> Any:
stacklevel=stacklevel,
category=RuntimeWarning,
)
if self_fallback:
self_value = getattr(args[0], self_fallback)()
args = (self_value,) + args[1:]
return func(*args, **kwargs)

wrapper._cunumeric = CuWrapperMetadata(implemented=False)
Expand Down Expand Up @@ -219,59 +230,51 @@ def clone_module(
new_globals[attr] = value


C = TypeVar("C", bound=type)
def should_wrap(obj: object) -> bool:
return isinstance(obj, (FunctionType, MethodType, MethodDescriptorType))


def clone_class(origin_class: type) -> Callable[[C], C]:
"""Copy attributes from one class to another
def clone_np_ndarray(cls: type) -> type:
"""Copy attributes from np.ndarray to cunumeric.ndarray

Method types are wrapped with a decorator to report API calls. All
other values are copied as-is.

Parameters
----------
origin_class : type
Existing class type to clone attributes from

"""

def should_wrap(obj: object) -> bool:
return isinstance(
obj, (FunctionType, MethodType, MethodDescriptorType)
)

def decorator(cls: C) -> C:
class_name = f"{origin_class.__module__}.{origin_class.__name__}"

missing = filter_namespace(
origin_class.__dict__,
# this simply omits ndarray internal methods for any class. If
# we ever need to wrap more classes we may need to generalize to
# per-class specification of internal names to skip
omit_names=set(cls.__dict__).union(NDARRAY_INTERNAL),
)

reporting = runtime.args.report_coverage

for attr, value in cls.__dict__.items():
if should_wrap(value):
wrapped = implemented(
value, class_name, attr, reporting=reporting
)
setattr(cls, attr, wrapped)

for attr, value in missing.items():
if should_wrap(value):
wrapped = unimplemented(
value, class_name, attr, reporting=reporting
)
setattr(cls, attr, wrapped)
else:
setattr(cls, attr, value)

return cls

return decorator
origin_class = np.ndarray

class_name = f"{origin_class.__module__}.{origin_class.__name__}"

missing = filter_namespace(
origin_class.__dict__,
# this simply omits ndarray internal methods for any class. If
# we ever need to wrap more classes we may need to generalize to
# per-class specification of internal names to skip
omit_names=set(cls.__dict__).union(NDARRAY_INTERNAL),
)

reporting = runtime.args.report_coverage

for attr, value in cls.__dict__.items():
if should_wrap(value):
wrapped = implemented(value, class_name, attr, reporting=reporting)
setattr(cls, attr, wrapped)

for attr, value in missing.items():
if should_wrap(value):
wrapped = unimplemented(
value,
class_name,
attr,
reporting=reporting,
self_fallback="__array__",
)
setattr(cls, attr, wrapped)
else:
setattr(cls, attr, value)

return cls


def is_implemented(obj: Any) -> bool:
Expand Down
39 changes: 39 additions & 0 deletions tests/integration/test_array_fallback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Copyright 2022 NVIDIA Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

import pytest

import cunumeric as num


# ref: https://github.com/nv-legate/cunumeric/pull/430
def test_unimplemented_method_self_fallback():

ones = num.ones((10,))
ones.mean()

# This test uses std because it is currently unimplemented, and we want
# to verify a behaviour of unimplemented ndarray method wrappers. If std
# becomes implemeneted in the future, this assertion will start to fail,
# and a new (unimplemented) ndarray method should be found to replace it
assert not ones.std._cunumeric.implemented
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@magnatelee I don't love this but I am not sure what else we can really do.


ones.std()


if __name__ == "__main__":
import sys

sys.exit(pytest.main(sys.argv))
94 changes: 34 additions & 60 deletions tests/unit/cunumeric/test_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from types import ModuleType

import cunumeric.coverage as m # module under test
import numpy as np
import pytest
from mock import MagicMock, patch

Expand Down Expand Up @@ -365,90 +366,63 @@ def test_report_coverage_False(self) -> None:
assert _Dest.function1.__wrapped__ is _OriginMod.function1
assert not _Dest.function1._cunumeric.implemented

assert _Dest.function2 is _Dest.function2


class _OriginClass:
def __array_finalize__(self) -> None:
pass

def __array_function__(self) -> None:
pass

def __array_interface__(self) -> None:
pass

def __array_prepare__(self) -> None:
pass

def __array_priority__(self) -> None:
pass

def __array_struct__(self) -> None:
pass

def __array_ufunc__(self) -> None:
pass
assert _Dest.function2.__wrapped__
assert _Dest.function2._cunumeric.implemented

def __array_wrap__(self) -> None:
pass

def method1(self) -> None:
pass
@m.clone_np_ndarray
class _Test_ndarray:
def __array__(self):
return "__array__"

def method2(self) -> None:
pass
def conjugate(self):
return self, "conjugate"

attr1 = 10

attr2 = 20
attr2 = 30


class Test_clone_class:
class Test_clone_np_ndarray:
@patch.object(cunumeric.runtime.args, "report_coverage", True)
def test_report_coverage_True(self) -> None:
assert cunumeric.runtime.args.report_coverage

@m.clone_class(_OriginClass)
class _Dest:
def method2(self) -> None:
pass

attr2 = 30

for name in m.NDARRAY_INTERNAL:
assert name not in _Dest.__dict__
assert name not in _Test_ndarray.__dict__

assert _Dest.attr1 == 10
assert _Dest.attr2 == 30
assert _Test_ndarray.attr1 == 10
assert _Test_ndarray.attr2 == 30

assert _Dest.method1.__wrapped__ is _OriginClass.method1
assert not _Dest.method1._cunumeric.implemented
assert _Test_ndarray.conj.__wrapped__ is np.ndarray.conj
assert not _Test_ndarray.conj._cunumeric.implemented

assert _Dest.method2.__wrapped__
assert _Dest.method2._cunumeric.implemented
assert _Test_ndarray.conjugate.__wrapped__
assert _Test_ndarray.conjugate._cunumeric.implemented

@patch.object(cunumeric.runtime.args, "report_coverage", False)
def test_report_coverage_False(self) -> None:
assert not cunumeric.runtime.args.report_coverage

@m.clone_class(_OriginClass)
class _Dest:
def method2(self) -> None:
pass

attr2 = 30

for name in m.NDARRAY_INTERNAL:
assert name not in _Dest.__dict__
assert name not in _Test_ndarray.__dict__

assert _Dest.attr1 == 10
assert _Dest.attr2 == 30
assert _Test_ndarray.attr1 == 10
assert _Test_ndarray.attr2 == 30

assert _Dest.method1.__wrapped__ is _OriginClass.method1
assert not _Dest.method1._cunumeric.implemented
assert _Test_ndarray.conj.__wrapped__ is np.ndarray.conj
assert not _Test_ndarray.conj._cunumeric.implemented

assert _Dest.method2 is _Dest.method2
assert _Test_ndarray.conjugate.__wrapped__
assert _Test_ndarray.conjugate._cunumeric.implemented

# TODO (bev) Not sure how to unit test this. Try to use a toy ndarray class
# (as above) for testing, and numpy gets unhappy. On the other hand, if we
# pick some arbitrary currently unimplemented method to test with, then it
# could become implemented in the future, silently making the test a noop
# For now, will test with real code in a separate integration test, and
# assert the unimplemented-ness so that future changes will draw attention
def test_self_fallback(self):
pass


if __name__ == "__main__":
Expand Down