diff --git a/python/paddle/compat/__init__.py b/python/paddle/compat/__init__.py index f22d335c8bdb74..7138ba15ecd9c2 100644 --- a/python/paddle/compat/__init__.py +++ b/python/paddle/compat/__init__.py @@ -40,6 +40,7 @@ from collections.abc import Sequence from paddle import Tensor + from paddle._typing import DTypeLike __all__ = [ 'allclose', @@ -56,6 +57,26 @@ ] +_TENSOR_TYPE_DTYPES = { + 'HalfTensor': 'float16', + 'FloatTensor': 'float32', + 'DoubleTensor': 'float64', + 'Float8_e4m3fnTensor': 'float8_e4m3fn', + 'Float8_e5m2Tensor': 'float8_e5m2', + 'BFloat16Tensor': 'bfloat16', + 'ByteTensor': 'uint8', + 'CharTensor': 'int8', + 'ShortTensor': 'int16', + 'IntTensor': 'int32', + 'LongTensor': 'int64', + 'BoolTensor': 'bool', + 'ComplexFloatTensor': 'complex64', + 'ComplexDoubleTensor': 'complex128', +} + +_DTYPE_TENSOR_TYPES = {v: k for k, v in _TENSOR_TYPE_DTYPES.items()} + + def __getattr__(name): if name == "paddle_triton": return paddle_triton_fun() @@ -66,6 +87,141 @@ def __getattr__(name): raise AttributeError(f"module {__name__!r} has no attribute {name!r}") +def _tensor_numel(input: Tensor) -> int: + """ + Returns the total number of elements in the tensor. + + Args: + input (Tensor): The input tensor. + + Returns: + int: The number of elements in ``input``. + """ + return int(input.size) + + +def _place_device(place) -> str: + """The paddle device name of ``place``""" + if place.is_gpu_place(): + return "gpu" + if place.is_xpu_place(): + return "xpu" + if place.is_custom_place(): + return place.custom_device_type().lower() + return "cpu" + + +def _tensor_type_devices() -> set[str]: + """Paddle device names that may appear in a tensor type string.""" + customs = paddle.device.get_all_custom_device_type() or [] + return {"cpu", "gpu", "xpu", *(custom.lower() for custom in customs)} + + +def _tensor_type_name(input: Tensor) -> str: + """``torch.Tensor.type``-style name with a paddle prefix, e.g. + ``'paddle.cuda.sparse.FloatTensor'``. Dtypes without a tensor type name + fall back to the dtype string.""" + dtype_name = str(input.dtype).removeprefix("paddle.") + tensor_type = _DTYPE_TENSOR_TYPES.get(dtype_name) + if tensor_type is None: + return str(input.dtype) + segments = ["paddle"] + device = _place_device(input.place) + if device != "cpu": + segments.append("cuda" if device == "gpu" else device) + if input.is_sparse_coo(): + segments.append("sparse") + segments.append(tensor_type) + return ".".join(segments) + + +def _tensor_type( + input: Tensor, + dtype: DTypeLike | str | type | None = None, + non_blocking: bool = False, + **kwargs: Any, +) -> str | Tensor: + """ + Returns the tensor type when ``dtype`` is not specified, otherwise casts + the tensor to the requested type. + + Args: + input (Tensor): The input tensor. + dtype (DTypeLike|str|type|None, optional): The target tensor type or + data type. Qualified ``torch.*`` and ``paddle.*`` dtype or tensor + type strings are supported. When it is ``None``, returns the tensor + type name. Default: ``None``. + non_blocking (bool, optional): Whether the conversion may occur + asynchronously. Default: ``False``. + + Returns: + str|Tensor: The tensor type name when ``dtype`` is ``None``, e.g. + ``'paddle.FloatTensor'``, ``'paddle.cuda.FloatTensor'`` or + ``'paddle.cuda.sparse.FloatTensor'``, encoding the dtype, the place + and the sparse COO layout as ``torch.Tensor.type`` does; otherwise, + a tensor with the requested type. + """ + if "async" in kwargs: + non_blocking = kwargs.pop("async") + if kwargs: + key = next(iter(kwargs)) + raise TypeError(f"type() got an unexpected keyword argument {key!r}") + + if dtype is None: + return _tensor_type_name(input) + + device = None + if getattr(dtype, "__name__", None) in _TENSOR_TYPE_DTYPES: + # tensor factory classes, e.g. paddle.DoubleTensor + dtype = _TENSOR_TYPE_DTYPES[dtype.__name__] + device = "cpu" + elif isinstance(dtype, str): + dtype_string = dtype + tensor_type = dtype_string.rsplit(".", 1)[-1] + if not dtype_string.startswith(("torch.", "paddle.")): + raise ValueError(f"invalid type: {dtype_string!r}") + if tensor_type in _TENSOR_TYPE_DTYPES: + dtype = _TENSOR_TYPE_DTYPES[tensor_type] + # the segments between the prefix and the tensor type name are the + # device, optionally followed by 'sparse'; no segment means cpu + middle = [s for s in dtype_string.split(".")[1:-1] if s != "sparse"] + device = middle[0] if middle else "cpu" + if device == "cuda": + device = "gpu" + if len(middle) > 1 or device not in _tensor_type_devices(): + raise ValueError(f"invalid type: {dtype_string!r}") + elif tensor_type in _TENSOR_TYPE_DTYPES.values(): + dtype = tensor_type + else: + raise ValueError(f"invalid type: {dtype_string!r}") + + dtype_name = str(input.dtype).removeprefix("paddle.") + target_dtype_name = str(dtype).removeprefix("paddle.") + same_device = device is None or device == _place_device(input.place) + if dtype_name == target_dtype_name and same_device: + return input + + return input.to( + device=device, + dtype=dtype, + blocking=not non_blocking, + ) + + +@property +def _tensor_is_sparse(input: Tensor) -> bool: + """ + Whether the tensor uses the sparse COO layout. + + Args: + input (Tensor): The input tensor. + + Returns: + bool: ``True`` for a sparse COO tensor, otherwise ``False``. + """ + return input.is_sparse_coo() + + def allclose( input: Tensor, other: Tensor, @@ -1132,3 +1288,21 @@ def GetShapeOnDimInRange(shape, dim: int) -> int: split_size_or_sections ) return tuple(_C_ops.split(tensor, split_size_or_sections, dim)) + + +# ``paddle.Tensor`` APIs routed to their ``paddle.compat`` implementations +_TENSOR_API_OVERRIDES = { + 'allclose': allclose, + 'equal': equal, + 'slogdet': slogdet, + 'sort': sort, + 'split': split, + 'min': min, + 'max': max, + 'unique': unique, + 'median': median, + 'nanmedian': nanmedian, + 'numel': _tensor_numel, + 'type': _tensor_type, + 'is_sparse': _tensor_is_sparse, +} diff --git a/python/paddle/compat/api_dispatch.py b/python/paddle/compat/api_dispatch.py index 848506dadbdd43..08651ba70bb8ce 100644 --- a/python/paddle/compat/api_dispatch.py +++ b/python/paddle/compat/api_dispatch.py @@ -51,29 +51,19 @@ def _caller_is_paddle_internal() -> bool: return name == "paddle" or name.startswith("paddle.") -def dispatch_function(compat_fn: Any) -> Any: - """Wrap a native ``paddle`` callable to route external callers to - ``compat_fn`` while compat is enabled; paddle-internal callers and the - disabled state get the native callable. Installed only under - ``enable_compat(level=2)``; ``disable_compat`` restores the originals, - so the default hot path is untouched.""" - - def decorator(native_fn: Any) -> Any: - @wraps(native_fn) - def dispatcher(*args: Any, **kwargs: Any) -> Any: - if ( - len(_PADDLE_NAMESPACE_SAVED) > 0 - and not _caller_is_paddle_internal() - ): - return compat_fn(*args, **kwargs) - return native_fn(*args, **kwargs) +def dispatch_function(native_fn: Any, compat_fn: Any) -> Any: + """Wrap a native ``paddle`` callable for caller-aware dispatch.""" - dispatcher.__compat_fn__ = compat_fn - dispatcher.__native_fn__ = native_fn - dispatcher.__signature__ = inspect.signature(compat_fn) - return dispatcher + @wraps(native_fn) + def dispatcher(*args: Any, **kwargs: Any) -> Any: + if _caller_is_paddle_internal(): + return native_fn(*args, **kwargs) + return compat_fn(*args, **kwargs) - return decorator + dispatcher.__compat_fn__ = compat_fn + dispatcher.__native_fn__ = native_fn + dispatcher.__signature__ = inspect.signature(compat_fn) + return dispatcher def _iter_compat_modules() -> Generator[types.ModuleType, None, None]: @@ -123,28 +113,49 @@ def __call__(cls, *args: Any, **kwargs: Any) -> Any: return proxy +def dispatch_property( + native_attr: Any, + compat_attr: Any, +) -> Any: + """Route a Tensor API when either side uses the property protocol.""" + compat_fn = ( + compat_attr.fget if isinstance(compat_attr, property) else compat_attr + ) + + class _PropertyDispatcher: + def __get__(self, instance: Any, owner: type | None = None) -> Any: + if _caller_is_paddle_internal(): + attr = native_attr + else: + attr = compat_attr + return attr.__get__(instance, owner) + + dispatcher = _PropertyDispatcher() + dispatcher.__native_fn__ = native_attr + dispatcher.__compat_fn__ = compat_fn + dispatcher.__doc__ = compat_fn.__doc__ + dispatcher.__name__ = compat_fn.__name__ + dispatcher.__signature__ = inspect.signature(compat_fn) + return dispatcher + + def _patch_tensor_methods() -> None: - """Route ``paddle.Tensor.`` to the compat function for the root compat APIs - that torch also exposes as Tensor methods (max/min/sort/split/unique/...), so - ``x.max(dim=1)`` works torch-style for external callers (native for internal). - The dispatcher is patched directly like any paddle Tensor method: the - descriptor protocol forwards the tensor as the first positional argument, - which is exactly the compat function's ``input`` parameter. - """ + """Route ``paddle.Tensor`` APIs to their root compat implementations.""" import paddle import paddle.compat as compat_root - for attr_name in getattr(compat_root, "__all__", ()): - native_method = getattr(paddle.Tensor, attr_name, None) - if native_method is None: + for attr_name, compat_attr in compat_root._TENSOR_API_OVERRIDES.items(): + native_attr = inspect.getattr_static(paddle.Tensor, attr_name, None) + if native_attr is None: continue - compat_fn = getattr(compat_root, attr_name) - _PADDLE_NAMESPACE_SAVED[(paddle.Tensor, attr_name)] = native_method - setattr( - paddle.Tensor, - attr_name, - dispatch_function(compat_fn)(native_method), - ) + _PADDLE_NAMESPACE_SAVED[(paddle.Tensor, attr_name)] = native_attr + if inspect.isdatadescriptor(native_attr) or isinstance( + compat_attr, property + ): + dispatcher = dispatch_property(native_attr, compat_attr) + else: + dispatcher = dispatch_function(native_attr, compat_attr) + setattr(paddle.Tensor, attr_name, dispatcher) def _apply_paddle_namespace_aliases() -> None: @@ -177,7 +188,7 @@ def _apply_paddle_namespace_aliases() -> None: setattr( target_module, attr_name, - dispatch_function(compat_attr)(current), + dispatch_function(current, compat_attr), ) _patch_tensor_methods() diff --git a/python/paddle/compat/distributions/categorical.py b/python/paddle/compat/distributions/categorical.py index 910aba15f31343..b3a9e667cc2f67 100644 --- a/python/paddle/compat/distributions/categorical.py +++ b/python/paddle/compat/distributions/categorical.py @@ -22,6 +22,8 @@ from ..utils import _CompatClassMeta +__all__ = ["Categorical"] + class Categorical(distribution.Distribution, metaclass=_CompatClassMeta): arg_constraints = { @@ -66,6 +68,20 @@ def __init__( distribution.Distribution.__init__( self, batch_shape, validate_args=validate_args ) + if self._validate_args_enabled and paddle.in_dynamic_mode(): + if probs is not None: + param_name = "probs" + valid = paddle.all(self.probs >= 0, axis=-1) & ( + (self.probs.sum(-1) - 1).abs() < 1e-6 + ) + else: + param_name = "logits" + valid = constraint.real_vector.check(self.logits) + if not bool(valid.all()): + raise ValueError( + f'Expected parameter {param_name} of distribution ' + 'Categorical to satisfy its constraint' + ) def expand(self, batch_shape, _instance=None): new = ( diff --git a/python/paddle/compat/proxy.py b/python/paddle/compat/proxy.py index 22b54cc51ca470..a6d772fb47b96d 100644 --- a/python/paddle/compat/proxy.py +++ b/python/paddle/compat/proxy.py @@ -14,6 +14,7 @@ from __future__ import annotations +import enum import importlib import importlib.abc import importlib.util @@ -26,6 +27,7 @@ from typing import TYPE_CHECKING, Any, Literal from .api_dispatch import ( + _PADDLE_NAMESPACE_SAVED, _apply_paddle_namespace_aliases, _iter_compat_modules, _restore_paddle_namespace_aliases, @@ -464,6 +466,24 @@ def _parse_scope(scope: str | Iterable[str] | None) -> set[str] | None: return set(scope) +class _CompatLevel(enum.Flag): + """The mechanisms behind the numeric ``level`` of ``enable_compat``: + ``1`` = module proxy, ``2`` = API alias, ``3`` = both.""" + + MODULE_PROXY = 1 + API_ALIAS = 2 + + +def _current_compat_level() -> _CompatLevel: + """The mechanisms that are currently installed.""" + level = _CompatLevel(0) + if TORCH_PROXY_FINDER in sys.meta_path: + level |= _CompatLevel.MODULE_PROXY + if _PADDLE_NAMESPACE_SAVED: + level |= _CompatLevel.API_ALIAS + return level + + def enable_compat( *, scope: _ScopeType = None, @@ -473,21 +493,22 @@ def enable_compat( level: int = 1, ) -> None: """ - Enable the PyTorch compat by adding the TorchProxyMetaFinder to sys.meta_path. - This allows importing 'torch' modules that are actually proxies to PaddlePaddle. + Enable the requested PyTorch compatibility mechanisms. Args: scope (str or Iterable[str], optional): Specific module or modules to enable - PyTorch compat for. If None, enables PyTorch compat globally. Defaults to None. + the torch proxy for at level 1 or 3. If None, enables the torch proxy + globally. Defaults to None. blocked_modules (str or Iterable[str], optional): Specific module or modules to - exclude from PyTorch compat. Defaults to None. + exclude from the torch proxy at level 1 or 3. Defaults to None. backend (str, optional): The backend to enable compat for. Currently only "torch" is supported. Defaults to "torch". silent (bool, optional): If True, suppresses warnings about scope changes. Defaults to False. - level (int, optional): The compatibility level. ``1`` (default) preserves the - original ``torch -> paddle`` proxy behavior. ``2`` aliases the torch-aligned - ``paddle.compat.*`` APIs onto ``paddle.*`` and ``paddle.Tensor```. Defaults to 1. + level (int, optional): The compatibility level. ``1`` (default) enables + the ``torch -> paddle`` proxy, ``2`` aliases the torch-aligned + ``paddle.compat.*`` APIs onto ``paddle.*`` and ``paddle.Tensor``, + and ``3`` enables both. Defaults to 1. Example: .. code-block:: pycon @@ -513,26 +534,30 @@ def enable_compat( """ assert backend == "torch", f"Unsupported backend: {backend}" - if level not in {1, 2}: - raise ValueError(f"Unsupported level: {level}. It should be 1 or 2.") - - blocked_modules = _parse_scope(blocked_modules) - if blocked_modules is not None: - extend_torch_proxy_blocked_modules(blocked_modules) - scope = _parse_scope(scope) - _register_compat_override() - _swap_torch_modules_to_cache() - _modify_scope_of_torch_proxy(scope, silent=silent) - sys.meta_path.insert(0, TORCH_PROXY_FINDER) + if level not in {1, 2, 3}: + raise ValueError( + f"Unsupported level: {level}. It should be 1, 2, or 3." + ) - if level == 2: + compat_level = _CompatLevel(level) + if _CompatLevel.MODULE_PROXY in compat_level: + blocked_modules = _parse_scope(blocked_modules) + if blocked_modules is not None: + extend_torch_proxy_blocked_modules(blocked_modules) + scope = _parse_scope(scope) + _register_compat_override() + _swap_torch_modules_to_cache() + _modify_scope_of_torch_proxy(scope, silent=silent) + sys.meta_path.insert(0, TORCH_PROXY_FINDER) + if _CompatLevel.API_ALIAS in compat_level: + _apply_paddle_namespace_aliases() + else: _apply_paddle_namespace_aliases() def disable_compat() -> None: """ - Disable the PyTorch proxy by removing the TorchProxyMetaFinder from sys.meta_path. - This prevents 'torch' imports from being proxied to PaddlePaddle. + Disable the active compatibility mechanisms. Example: .. code-block:: pycon @@ -547,13 +572,16 @@ def disable_compat() -> None: ... except ModuleNotFoundError: ... print("PyTorch compat is disabled.") """ + if TORCH_PROXY_FINDER not in sys.meta_path and not _PADDLE_NAMESPACE_SAVED: + warnings.warn("torch compat is not installed.") + return + if TORCH_PROXY_FINDER in sys.meta_path: sys.meta_path.remove(TORCH_PROXY_FINDER) - _restore_paddle_namespace_aliases() _clear_torch_proxy_modules() _copy_torch_modules_from_cache() - return - warnings.warn("torch compat is not installed.") + + _restore_paddle_namespace_aliases() @contextmanager @@ -568,8 +596,8 @@ def use_compat_guard( When `enable` is True (default), the PyTorch compat is enabled for the duration of the context and restored to its previous state afterwards. When `enable` - is False, the PyTorch compat is disabled for the duration of the context and - restored afterwards. + is False, compat is disabled for the duration of the context and restored + afterwards. Args: enable (bool, optional): Whether to enable or disable the PyTorch compat diff --git a/python/paddle/nn/layer/activation.py b/python/paddle/nn/layer/activation.py index 06bffdad4e8229..7f92d0bc9dcd6b 100644 --- a/python/paddle/nn/layer/activation.py +++ b/python/paddle/nn/layer/activation.py @@ -17,8 +17,10 @@ from typing import TYPE_CHECKING, Literal +from typing_extensions import overload + from paddle.framework import get_default_dtype -from paddle.utils.decorator_utils import param_one_alias +from paddle.utils.decorator_utils import param_one_alias, prelu_decorator from .. import functional as F from ..initializer import Constant @@ -516,6 +518,12 @@ class PReLU(Layer): """ PReLU Activation. The calculation formula is follows: + This API has two signatures: + + 1. ``PReLU(num_parameters=1, init=0.25, weight_attr=None, data_format="NCHW", name=None, device=None, dtype=None)`` (Paddle-style). + + 2. ``PReLU(num_parameters=1, init=0.25, device=None, dtype=None)`` (PyTorch-style). + If approximate calculation is used: .. math:: @@ -576,6 +584,28 @@ class PReLU(Layer): [ 6. , 7. , 8. , 9. ]]]]) """ + @overload + def __init__( + self, + num_parameters: int = 1, + init: float = 0.25, + weight_attr: ParamAttrLike | None = None, + data_format: DataLayoutND = "NCHW", + name: str | None = None, + device: PlaceLike | None = None, + dtype: DTypeLike | None = None, + ) -> None: ... + + @overload + def __init__( + self, + num_parameters: int = 1, + init: float = 0.25, + device: PlaceLike | None = None, + dtype: DTypeLike | None = None, + ) -> None: ... + + @prelu_decorator def __init__( self, num_parameters: int = 1, diff --git a/python/paddle/utils/decorator_utils.py b/python/paddle/utils/decorator_utils.py index a238c66fc5bc40..33e40a3aa03c0c 100644 --- a/python/paddle/utils/decorator_utils.py +++ b/python/paddle/utils/decorator_utils.py @@ -299,6 +299,60 @@ def wrapper(*args: _InputT.args, **kwargs: _InputT.kwargs) -> _RetT: return wrapper +def prelu_decorator( + func: Callable[_InputT, _RetT], +) -> Callable[_InputT, _RetT]: + """Dispatch between the Paddle and PyTorch ``PReLU`` signatures. + + Paddle: ``PReLU(num_parameters, init, weight_attr, data_format, name, device, dtype)`` + PyTorch: ``PReLU(num_parameters, init, device, dtype)`` + """ + + @functools.wraps(func) + def wrapper(*args: _InputT.args, **kwargs: _InputT.kwargs) -> _RetT: + if 4 <= len(args) <= 5: + device_types = { + "cpu", + "cuda", + "gpu", + "dcu", + "xpu", + "ipu", + *(paddle.device.get_all_custom_device_type() or ()), + } + data_formats = { + "NC", + "NCL", + "NCHW", + "NCDHW", + "NLC", + "NHWC", + "NDHWC", + } + is_paddle_form = ( + len(args) == 5 + and isinstance(args[4], str) + and args[4] in data_formats + ) + is_paddle_place = isinstance(args[3], paddle.base.libpaddle.Place) + is_device = args[3] is None or ( + isinstance(args[3], str) + and args[3].lower().split(":", 1)[0] in device_types + ) + if not is_paddle_form and (is_paddle_place or is_device): + for name, value in zip(("device", "dtype"), args[3:]): + if name in kwargs: + raise TypeError( + f"__init__() got multiple values for argument '{name}'" + ) + kwargs[name] = value + args = args[:3] + return func(*args, **kwargs) + + wrapper.__signature__ = inspect.signature(func) + return wrapper + + def lp_pool_function_decorator( func: Callable[_InputT, _RetT], ) -> Callable[_InputT, _RetT]: diff --git a/test/compat/fake_modules/torch_proxy_local_enabled_package/__init__.py b/test/compat/fake_modules/torch_proxy_local_enabled_package/__init__.py new file mode 100644 index 00000000000000..5573bb57ca61a8 --- /dev/null +++ b/test/compat/fake_modules/torch_proxy_local_enabled_package/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved. +# +# 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. + +from . import submodule + +__all__ = ["submodule"] diff --git a/test/compat/fake_modules/torch_proxy_local_enabled_package/submodule.py b/test/compat/fake_modules/torch_proxy_local_enabled_package/submodule.py new file mode 100644 index 00000000000000..c26e86582932c2 --- /dev/null +++ b/test/compat/fake_modules/torch_proxy_local_enabled_package/submodule.py @@ -0,0 +1,19 @@ +# Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved. +# +# 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. + + +def use_torch_compat_api(): + import torch + + return torch.randn diff --git a/test/compat/test_compat_namespace_aliased.py b/test/compat/test_compat_namespace_aliased.py index 70448c9e7f6327..89eee04e43a894 100644 --- a/test/compat/test_compat_namespace_aliased.py +++ b/test/compat/test_compat_namespace_aliased.py @@ -18,8 +18,12 @@ import unittest from contextlib import contextmanager from functools import wraps +from unittest import mock + +import numpy as np import paddle +from paddle.compat import api_dispatch from paddle.compat.api_dispatch import _PADDLE_NAMESPACE_SAVED from paddle.compat.proxy import TORCH_PROXY_FINDER @@ -60,6 +64,9 @@ class CompatNamespaceAliasBase(unittest.TestCase): (paddle, "allclose"), (paddle, "equal"), (paddle, "seed"), + (paddle.Tensor, "numel"), + (paddle.Tensor, "type"), + (paddle.Tensor, "is_sparse"), (paddle.nn, "AvgPool1d"), (paddle.nn, "AvgPool2d"), (paddle.nn, "AvgPool3d"), @@ -78,14 +85,14 @@ def setUp(self): # (attention/sdpa). Original device restored in tearDown. self._device = paddle.get_device() paddle.set_device('cpu') - while TORCH_PROXY_FINDER in sys.meta_path: + while TORCH_PROXY_FINDER in sys.meta_path or _PADDLE_NAMESPACE_SAVED: paddle.disable_compat() self._native = {(m, a): getattr(m, a) for (m, a) in self.EXISTING} self._scope = set(TORCH_PROXY_FINDER._local_enabled_scope) self._global = TORCH_PROXY_FINDER._globally_enabled def tearDown(self): - while TORCH_PROXY_FINDER in sys.meta_path: + while TORCH_PROXY_FINDER in sys.meta_path or _PADDLE_NAMESPACE_SAVED: paddle.disable_compat() TORCH_PROXY_FINDER._local_enabled_scope = set(self._scope) TORCH_PROXY_FINDER._globally_enabled = self._global @@ -126,8 +133,8 @@ def test_public_level_parameter_is_minimal(self): ) def test_invalid_level_has_no_side_effect(self): - with self.assertRaisesRegex(ValueError, "Unsupported level: 3"): - paddle.enable_compat(level=3) + with self.assertRaisesRegex(ValueError, "Unsupported level: 4"): + paddle.enable_compat(level=4) self.assertNotIn(TORCH_PROXY_FINDER, sys.meta_path) self.assertNativeRestored() @@ -152,6 +159,16 @@ def test_level1_default_does_not_alias(self): paddle.enable_compat() # default level=1 try: self.assertIs(paddle.sort, self._native[(paddle, "sort")]) + self.assertIs( + paddle.Tensor.numel, self._native[(paddle.Tensor, "numel")] + ) + self.assertIs( + paddle.Tensor.type, self._native[(paddle.Tensor, "type")] + ) + self.assertIs( + paddle.Tensor.is_sparse, + self._native[(paddle.Tensor, "is_sparse")], + ) self.assertFalse(hasattr(paddle.sort, "__compat_fn__")) with self.assertRaises(TypeError): paddle.sort( @@ -223,6 +240,10 @@ def test_submodule_symbols_aliased(self): self.assertAliased( paddle.nn.functional.linear, paddle.compat.nn.functional.linear ) + self.assertAliased( + paddle.distributions.categorical.Categorical, + paddle.compat.distributions.categorical.Categorical, + ) @with_level2 def test_aliased_signatures_are_torch_style(self): @@ -407,6 +428,7 @@ class TestScopeAndLifecycle(CompatNamespaceAliasBase): def test_scoped_level2_enable_aliases(self): paddle.enable_compat(scope={"triton"}, level=2, silent=True) try: + self.assertNotIn(TORCH_PROXY_FINDER, sys.meta_path) self.assertAliased(paddle.sort, paddle.compat.sort) finally: paddle.disable_compat() @@ -429,23 +451,9 @@ def test_registry_empty_after_disable(self): paddle.disable_compat() self.assertEqual(len(_PADDLE_NAMESPACE_SAVED), 0) - def test_bare_guard_keeps_level2_alias(self): - t = paddle.to_tensor([[3.0, 1.0, 2.0]]) - paddle.enable_compat(level=2) - try: - with paddle.use_compat_guard(): - self.assertAliased(paddle.sort, paddle.compat.sort) - self.assertTrue(hasattr(paddle.sort(t, dim=-1), "values")) - # the level in effect survives the guard - self.assertAliased(paddle.sort, paddle.compat.sort) - self.assertTrue(hasattr(paddle.sort(t, dim=-1), "values")) - finally: - paddle.disable_compat() - self.assertNativeRestored() - class TestTorchSurfaceUnderCompat(CompatNamespaceAliasBase): - """torch.* reaches the public compat implementations at both levels.""" + """torch.* reaches public compat APIs only at proxy-enabled levels.""" @staticmethod def _drop_torch_modules(): @@ -469,8 +477,8 @@ def test_level1_root_torch_apis_resolve_to_compat(self): self._drop_torch_modules() paddle.disable_compat() - def test_root_torch_apis_resolve_to_compat_at_level2(self): - paddle.enable_compat(level=2) + def test_root_torch_apis_resolve_to_compat_at_level3(self): + paddle.enable_compat(level=3) try: self._drop_torch_modules() import torch @@ -484,8 +492,8 @@ def test_root_torch_apis_resolve_to_compat_at_level2(self): self._drop_torch_modules() paddle.disable_compat() - def test_root_compat_only_api_is_registered_at_both_levels(self): - for level in (1, 2): + def test_root_compat_only_api_is_registered_at_proxy_levels(self): + for level in (1, 3): with self.subTest(level=level): paddle.enable_compat(level=level) try: @@ -536,9 +544,12 @@ def test_external_caller_still_gets_compat(self): def test_tensor_methods_caller_aware(self): """torch exposes max/min/sort/split/... as Tensor methods too; under level=2 ``x.max(dim=1)`` is torch-style for external callers and native - for paddle-internal ``x.max(axis=1)``; restored on disable.""" + for paddle-internal ``x.max(axis=1)``; disable restores the namespace.""" native_max = paddle.Tensor.max native_split = paddle.Tensor.split + native_numel = paddle.Tensor.numel + native_type = paddle.Tensor.type + native_is_sparse = paddle.Tensor.is_sparse t = paddle.to_tensor([[3.0, 1.0, 2.0], [6.0, 5.0, 4.0]]) with level2_guard(): r = t.max(dim=1) # external -> compat namedtuple @@ -551,15 +562,227 @@ def test_tensor_methods_caller_aware(self): 2, ) self.assertEqual(len(paddle.split(t, split_size=1, dim=0)), 2) + self.assertEqual(t.numel(), 6) + self.assertIs(type(t.numel()), int) + self.assertIsInstance(paddle.numel(t), paddle.Tensor) + self.assertEqual(paddle.empty([0, 3]).numel(), 0) + self.assertEqual(t.cpu().type(), "paddle.FloatTensor") + self.assertEqual(t.type(paddle.float64).dtype, paddle.float64) + self.assertEqual(t.type(paddle.DoubleTensor).dtype, paddle.float64) + self.assertEqual(t.type("torch.DoubleTensor").dtype, paddle.float64) + self.assertEqual( + t.type("paddle.DoubleTensor").dtype, paddle.float64 + ) + self.assertEqual(t.type("torch.float64").dtype, paddle.float64) + self.assertEqual(t.type("paddle.float64").dtype, paddle.float64) + self.assertIs(t.type(paddle.float32), t) + self.assertIs(t.type("torch.FloatTensor"), t) + self.assertIs(t.type("paddle.FloatTensor"), t) + self.assertIs(t.type(t.type()), t) + with self.assertRaises(ValueError): + t.type("float64") + self.assertEqual( + paddle.ones([1], dtype="int64").cpu().type(), + "paddle.LongTensor", + ) + self.assertEqual( + paddle.ones([1], dtype="float8_e4m3fn").cpu().type(), + "paddle.Float8_e4m3fnTensor", + ) + self.assertEqual( + paddle.ones([1], dtype="float8_e5m2").cpu().type(), + "paddle.Float8_e5m2Tensor", + ) + self.assertIs(t.is_sparse, False) + coo = paddle.sparse.sparse_coo_tensor([[0], [1]], [1.0], [2, 2]) + csr = paddle.sparse.sparse_csr_tensor([0, 1, 1], [0], [1.0], [2, 2]) + self.assertIs(coo.is_sparse, True) + self.assertIs(csr.is_sparse, False) # paddle-internal native-style call (simulated) stays native ns = {"__name__": "paddle.fake_internal", "t": t} exec( "internal_max = t.max(axis=1)\n" - "internal_split = t.split(num_or_sections=2, axis=0)", + "internal_split = t.split(num_or_sections=2, axis=0)\n" + "internal_numel = t.numel()\n" + "internal_type = t.type\n" + "internal_is_sparse = t.is_sparse()", ns, ) + self.assertIsInstance(ns["internal_numel"], paddle.Tensor) + self.assertEqual( + ns["internal_type"], native_type.__get__(t, paddle.Tensor) + ) + self.assertIs(ns["internal_is_sparse"], False) + cached_numel = t.numel + cached_max = t.max + ns["cached_max"] = cached_max + exec("internal_cached_max = cached_max(axis=1)", ns) + self.assertIsInstance(ns["internal_cached_max"], paddle.Tensor) self.assertIs(paddle.Tensor.max, native_max) # restored on disable self.assertIs(paddle.Tensor.split, native_split) + self.assertIs(paddle.Tensor.numel, native_numel) + self.assertIs(paddle.Tensor.type, native_type) + self.assertIs(paddle.Tensor.is_sparse, native_is_sparse) + self.assertEqual(cached_numel(), 6) + self.assertTrue(hasattr(cached_max(dim=1), "values")) + + @with_level2 + def test_tensor_type_edge_cases(self): + t = paddle.ones([1]) + with mock.patch.object( + paddle.Tensor, "to", autospec=True, return_value=t + ) as tensor_to: + self.assertIs( + t.type(paddle.float64, **{"async": True}), + t, + ) + tensor_to.assert_called_once_with( + t, + device=None, + dtype=paddle.float64, + blocking=False, + ) + with self.assertRaisesRegex( + TypeError, "unexpected keyword argument 'invalid'" + ): + t.type(invalid=True) + with self.assertRaisesRegex(ValueError, "invalid type"): + t.type("paddle.UnknownTensor") + + with mock.patch.object( + paddle.Tensor, "to", autospec=True, return_value=t + ) as tensor_to: + self.assertIs(t.type("paddle.cuda.DoubleTensor"), t) + tensor_to.assert_called_once_with( + t, + device="gpu", + dtype='float64', + blocking=True, + ) + + coo = paddle.sparse.sparse_coo_tensor( + [[0], [0]], [1.0], [1, 1], place='cpu' + ) + self.assertEqual(coo.type(), "paddle.sparse.FloatTensor") + self.assertEqual(t.type(np.float64).dtype, paddle.float64) + + # paddle spells bfloat16 as uint16, which still has a tensor type name + self.assertEqual( + paddle.ones([1], dtype="uint16").cpu().type(), + "paddle.BFloat16Tensor", + ) + if paddle.device.is_compiled_with_cuda(): + self.assertEqual( + paddle.ones([1]).cuda().type(), "paddle.cuda.FloatTensor" + ) + + # XPU and custom devices keep their own device segment, and the name a + # tensor reports must round-trip back to that very tensor + for device, expected in ( + ("gpu", "paddle.cuda.FloatTensor"), + ("xpu", "paddle.xpu.FloatTensor"), + ("npu", "paddle.npu.FloatTensor"), + ): + with ( + mock.patch.object( + paddle.compat, "_place_device", return_value=device + ), + mock.patch.object( + paddle.compat, + "_tensor_type_devices", + return_value={"cpu", "gpu", "xpu", "npu"}, + ), + mock.patch.object( + paddle.Tensor, "to", autospec=True, return_value=t + ) as tensor_to, + ): + self.assertEqual(t.type(), expected) + self.assertIs(t.type(expected), t) + tensor_to.assert_not_called() + + # a device segment naming another device does move the tensor + with mock.patch.object( + paddle.Tensor, "to", autospec=True, return_value=t + ) as tensor_to: + self.assertIs(t.type("paddle.xpu.FloatTensor"), t) + tensor_to.assert_called_once_with( + t, + device="xpu", + dtype='float32', + blocking=True, + ) + + for invalid in ( + "paddle.tpu.FloatTensor", # unknown device + "paddle.cuda.xpu.FloatTensor", # two device segments + ): + with self.assertRaisesRegex(ValueError, "invalid type"): + t.type(invalid) + + @with_level2 + def test_tensor_descriptor_class_access(self): + type_descriptor = inspect.getattr_static(paddle.Tensor, "type") + sparse_descriptor = inspect.getattr_static(paddle.Tensor, "is_sparse") + + self.assertIs(paddle.Tensor.type, type_descriptor.__compat_fn__) + self.assertIsInstance(paddle.Tensor.is_sparse, property) + self.assertIs( + paddle.Tensor.is_sparse.fget, + sparse_descriptor.__compat_fn__, + ) + + ns = {"__name__": "paddle.fake_internal", "paddle": paddle} + exec( + "internal_type = paddle.Tensor.type\n" + "internal_is_sparse = paddle.Tensor.is_sparse", + ns, + ) + self.assertIs(ns["internal_type"], type_descriptor.__native_fn__) + self.assertIs(ns["internal_is_sparse"], sparse_descriptor.__native_fn__) + + def test_property_to_property_dispatch(self): + class Native: + @property + def attr(self): + return "native" + + class Compat: + @property + def attr(self): + return "compat" + + native_attr = inspect.getattr_static(Native, "attr") + compat_attr = inspect.getattr_static(Compat, "attr") + Native.attr = api_dispatch.dispatch_property(native_attr, compat_attr) + instance = Native() + + self.assertEqual(instance.attr, "compat") + self.assertIs(Native.attr, compat_attr) + + ns = { + "__name__": "paddle.fake_internal", + "Native": Native, + "x": instance, + } + exec("value = x.attr\nattr = Native.attr", ns) + self.assertEqual(ns["value"], "native") + self.assertIs(ns["attr"], native_attr) + + def test_missing_tensor_override_is_skipped(self): + missing_attr = "__missing_tensor_compat_override__" + self.assertIsNone( + inspect.getattr_static(paddle.Tensor, missing_attr, None) + ) + with ( + mock.patch.object(paddle.compat, "__all__", ()), + mock.patch.dict( + paddle.compat._TENSOR_API_OVERRIDES, + {missing_attr: mock.sentinel.compat_fn}, + clear=True, + ), + ): + api_dispatch._patch_tensor_methods() + self.assertNotIn((paddle.Tensor, missing_attr), _PADDLE_NAMESPACE_SAVED) @with_level2 def test_aliased_class_caller_aware(self): diff --git a/test/compat/test_torch_proxy.py b/test/compat/test_torch_proxy.py index 57e607c945db79..d623e121a83e5d 100644 --- a/test/compat/test_torch_proxy.py +++ b/test/compat/test_torch_proxy.py @@ -127,6 +127,12 @@ def test_local_enabled_module(self): paddle.compat.proxy.TORCH_PROXY_FINDER._local_enabled_scope = set() paddle.disable_compat() + def test_local_enabled_package_submodule(self): + with paddle.use_compat_guard(scope="torch_proxy_local_enabled_package"): + from torch_proxy_local_enabled_package import submodule + + self.assertIs(submodule.use_torch_compat_api(), paddle.randn) + class TestTorchProxyUseMockedModule(unittest.TestCase): def test_use_mocked_module(self): diff --git a/test/compat/test_torch_proxy_mixed.py b/test/compat/test_torch_proxy_mixed.py index a558a8cc3d8d82..63b4f26df3f3a3 100644 --- a/test/compat/test_torch_proxy_mixed.py +++ b/test/compat/test_torch_proxy_mixed.py @@ -17,9 +17,7 @@ import unittest import paddle -from paddle.compat.proxy import ( - ProxyModule, -) +from paddle.compat.proxy import ProxyModule sys.path.append(str(pathlib.Path(__file__).parent / "fake_modules")) sys.path.append(str(pathlib.Path(__file__).parent / "fake_torch_modules")) @@ -61,6 +59,25 @@ def test_nested_torch_proxy(self): self.check_is_not_proxy() + def test_level2_does_not_proxy_torch(self): + import torch + from torch.nn.functional import relu + + original_torch = torch + original_relu = relu + self.check_is_not_proxy() + paddle.enable_compat(level=2) + try: + self.check_is_not_proxy() + import torch + from torch.nn.functional import relu + + self.assertIs(torch, original_torch) + self.assertIs(relu, original_relu) + finally: + paddle.disable_compat() + self.check_is_not_proxy() + def test_local_enabled_module_import(self): self.check_is_not_proxy() with paddle.use_compat_guard( diff --git a/test/legacy_test/test_api_compatibility_part2.py b/test/legacy_test/test_api_compatibility_part2.py index 59c6dc7c259e95..8c1813c5121242 100644 --- a/test/legacy_test/test_api_compatibility_part2.py +++ b/test/legacy_test/test_api_compatibility_part2.py @@ -3176,19 +3176,55 @@ def test_dygraph_Compatibility(self): )(input=x) # 4. Mixed arguments out4 = paddle.nn.PReLU(2, init=0.5, device="cpu", dtype="float32")(x) + # 5. PyTorch positional arguments + out5 = paddle.nn.PReLU(2, 0.5, "cpu", paddle.float32)(x) + # 6. PyTorch positional string device without dtype + layer6 = paddle.nn.PReLU(2, 0.5, "cpu") + self.assertTrue(layer6._weight.place.is_cpu_place()) + out6 = layer6(x) + # 7. Paddle string weight_attr keeps its original meaning + layer7 = paddle.nn.PReLU(2, 0.5, "prelu_weight") + self.assertEqual(layer7._weight.name, "prelu_weight") + out7 = layer7(x) + # 7.1 a positional data_format rules out the PyTorch signature + layer7_1 = paddle.nn.PReLU(2, 0.5, "prelu_weight_1", "NCHW") + self.assertEqual(layer7_1._weight.name, "prelu_weight_1") + self.assertEqual(layer7_1._data_format, "NCHW") + # 8. PyTorch positional dtype without device + out8 = paddle.nn.PReLU(2, 0.5, None, paddle.float32)(x) expected = self._expected(self.np_x) - for out in [out1, out2, out3, out4]: + for out in [out1, out2, out3, out4, out5, out6, out7, out8]: np.testing.assert_allclose(out.numpy(), expected, rtol=1e-6) x64 = paddle.to_tensor(self.np_x64) layer64 = paddle.nn.PReLU(2, 0.5, device="cpu", dtype="float64") - out5 = layer64(input=x64) + out9 = layer64(input=x64) self.assertEqual(layer64._weight.dtype, paddle.float64) np.testing.assert_allclose( - out5.numpy(), self._expected(self.np_x64), rtol=1e-6 + out9.numpy(), self._expected(self.np_x64), rtol=1e-6 + ) + + layer64_positional = paddle.nn.PReLU(2, 0.5, None, paddle.float64) + out10 = layer64_positional(x64) + self.assertEqual(layer64_positional._weight.dtype, paddle.float64) + np.testing.assert_allclose( + out10.numpy(), self._expected(self.np_x64), rtol=1e-6 ) + layer_place = paddle.nn.PReLU(2, 0.5, paddle.CPUPlace()) + out11 = layer_place(x) + self.assertTrue(layer_place._weight.place.is_cpu_place()) + np.testing.assert_allclose(out11.numpy(), expected, rtol=1e-6) + + paddle.enable_compat(level=2) + try: + layer_compat = paddle.nn.PReLU(2, 0.5, "cpu") + paddle.nn.PReLU(2, 0.5, "cpu") + self.assertTrue(layer_compat._weight.place.is_cpu_place()) + finally: + paddle.disable_compat() + paddle.enable_static() def test_static_Compatibility(self): @@ -3212,13 +3248,15 @@ def test_static_Compatibility(self): out4 = paddle.nn.PReLU(2, init=0.5, device="cpu", dtype="float32")( x ) + # 5. PyTorch positional arguments + out5 = paddle.nn.PReLU(2, 0.5, "cpu", paddle.float32)(x) exe = paddle.static.Executor() exe.run(startup) fetches = exe.run( main, feed={"x": self.np_x}, - fetch_list=[out1, out2, out3, out4], + fetch_list=[out1, out2, out3, out4, out5], ) expected = self._expected(self.np_x) diff --git a/test/legacy_test/test_api_compatibility_part3.py b/test/legacy_test/test_api_compatibility_part3.py index 16f3fda922bab9..b5966e3d6beb44 100644 --- a/test/legacy_test/test_api_compatibility_part3.py +++ b/test/legacy_test/test_api_compatibility_part3.py @@ -207,6 +207,24 @@ def test_dygraph_validate_args(self): ) with self.assertRaises(ValueError): batched_dist.log_prob(paddle.to_tensor([0, 1, 2], place=self.place)) + with self.assertRaises(ValueError): + categorical.Categorical( + probs=paddle.to_tensor([-0.1, 1.1], place=self.place), + validate_args=True, + ) + with self.assertRaises(ValueError): + categorical.Categorical( + logits=paddle.to_tensor([float("nan"), 0.0], place=self.place), + validate_args=True, + ) + with self.assertRaises(ValueError): + categorical.Categorical( + paddle.to_tensor([], dtype="float32", place=self.place) + ) + categorical.Categorical( + probs=paddle.to_tensor([], dtype="float32", place=self.place), + validate_args=False, + ) def test_dygraph_enumerate_support(self): import importlib