From 91b9144538a1a0b9b6ddad09021c6ede5ebc9c67 Mon Sep 17 00:00:00 2001 From: manfredss Date: Mon, 27 Jul 2026 07:51:54 +0000 Subject: [PATCH 01/21] align 3 Tensor api and PReLU --- python/paddle/compat/__init__.py | 91 +++++++++++++++++++ python/paddle/compat/api_dispatch.py | 45 +++++++++ python/paddle/nn/layer/activation.py | 11 +++ test/compat/test_compat_namespace_aliased.py | 57 +++++++++++- .../test_api_compatibility_part2.py | 23 ++++- 5 files changed, 222 insertions(+), 5 deletions(-) diff --git a/python/paddle/compat/__init__.py b/python/paddle/compat/__init__.py index f22d335c8bdb7..b4cd75a479cba 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', @@ -55,6 +56,26 @@ 'seed', ] +_TENSOR_TYPE_NAMES = { + 'float16': 'HalfTensor', + 'float32': 'FloatTensor', + 'float64': 'DoubleTensor', + 'float8_e4m3fn': 'Float8_e4m3fnTensor', + 'float8_e5m2': 'Float8_e5m2Tensor', + 'bfloat16': 'BFloat16Tensor', + 'uint8': 'ByteTensor', + 'int8': 'CharTensor', + 'int16': 'ShortTensor', + 'int32': 'IntTensor', + 'int64': 'LongTensor', + 'bool': 'BoolTensor', + 'complex64': 'ComplexFloatTensor', + 'complex128': 'ComplexDoubleTensor', +} +_TENSOR_TYPE_DTYPES = { + tensor_type: dtype for dtype, tensor_type in _TENSOR_TYPE_NAMES.items() +} + def __getattr__(name): if name == "paddle_triton": @@ -66,6 +87,76 @@ def __getattr__(name): raise AttributeError(f"module {__name__!r} has no attribute {name!r}") +def _tensor_numel(input: Tensor) -> int: + return int(input.size) + + +def _tensor_type( + input: Tensor, + dtype: DTypeLike | str | type | None = None, + non_blocking: bool = False, + **kwargs: Any, +) -> str | Tensor: + 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: + dtype_name = str(input.dtype).removeprefix("paddle.") + tensor_type = _TENSOR_TYPE_NAMES[dtype_name] + prefix = "torch.cuda" if input.place.is_gpu_place() else "torch" + if input.is_sparse_coo(): + prefix += ".sparse" + return f"{prefix}.{tensor_type}" + + device = None + if isinstance(dtype, type): + tensor_type = dtype.__name__ + if tensor_type not in _TENSOR_TYPE_DTYPES: + raise ValueError(f"invalid type: {tensor_type!r}") + dtype = _TENSOR_TYPE_DTYPES[tensor_type] + device = "cpu" + elif isinstance(dtype, str): + dtype_string = dtype + tensor_type = dtype_string.rsplit(".", 1)[-1] + if ( + not dtype_string.startswith("torch.") + or tensor_type not in _TENSOR_TYPE_DTYPES + ): + raise ValueError(f"invalid type: {dtype_string!r}") + dtype = _TENSOR_TYPE_DTYPES[tensor_type] + device = "gpu" if dtype_string.startswith("torch.cuda.") else "cpu" + + dtype_name = str(input.dtype).removeprefix("paddle.") + target_dtype_name = str(dtype).removeprefix("paddle.") + same_device = ( + device is None + or (device == "cpu" and input.place.is_cpu_place()) + or (device == "gpu" and input.place.is_gpu_place()) + ) + if dtype_name == target_dtype_name and same_device: + return input + + return input.to( + device=device, + dtype=dtype, + blocking=not non_blocking, + ) + + +def _tensor_is_sparse(input: Tensor) -> bool: + return input.is_sparse_coo() + + +_TENSOR_API_OVERRIDES = { + 'numel': (_tensor_numel, False), + 'type': (_tensor_type, False), + 'is_sparse': (_tensor_is_sparse, True), +} + + def allclose( input: Tensor, other: Tensor, diff --git a/python/paddle/compat/api_dispatch.py b/python/paddle/compat/api_dispatch.py index 848506dadbdd4..03741070f60e4 100644 --- a/python/paddle/compat/api_dispatch.py +++ b/python/paddle/compat/api_dispatch.py @@ -123,6 +123,37 @@ def __call__(cls, *args: Any, **kwargs: Any) -> Any: return proxy +class _TensorCompatDescriptor: + """Caller-aware adapter for Tensor method/property shape differences.""" + + def __init__( + self, + native_attr: Any, + compat_fn: Any, + as_property: bool, + ) -> None: + self.__native_fn__ = native_attr + self.__compat_fn__ = compat_fn + self._as_property = as_property + self.__doc__ = compat_fn.__doc__ + self.__name__ = compat_fn.__name__ + self.__signature__ = inspect.signature(compat_fn) + + def __get__(self, instance: Any, owner: type | None = None) -> Any: + if instance is None: + if _caller_is_paddle_internal(): + return self.__native_fn__ + return self if self._as_property else self.__compat_fn__ + if ( + len(_PADDLE_NAMESPACE_SAVED) > 0 + and not _caller_is_paddle_internal() + ): + if self._as_property: + return self.__compat_fn__(instance) + return self.__compat_fn__.__get__(instance, owner) + return self.__native_fn__.__get__(instance, owner) + + 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 @@ -146,6 +177,20 @@ def _patch_tensor_methods() -> None: dispatch_function(compat_fn)(native_method), ) + for attr_name, ( + compat_fn, + as_property, + ) in compat_root._TENSOR_API_OVERRIDES.items(): + native_attr = inspect.getattr_static(paddle.Tensor, attr_name, None) + if native_attr is None: + continue + _PADDLE_NAMESPACE_SAVED[(paddle.Tensor, attr_name)] = native_attr + setattr( + paddle.Tensor, + attr_name, + _TensorCompatDescriptor(native_attr, compat_fn, as_property), + ) + def _apply_paddle_namespace_aliases() -> None: """Install caller-aware dispatchers/proxies for every public ``paddle.compat.*`` diff --git a/python/paddle/nn/layer/activation.py b/python/paddle/nn/layer/activation.py index 06bffdad4e822..fa63214516f19 100644 --- a/python/paddle/nn/layer/activation.py +++ b/python/paddle/nn/layer/activation.py @@ -586,6 +586,17 @@ def __init__( device: PlaceLike | None = None, dtype: DTypeLike | None = None, ) -> None: + is_torch_device = weight_attr is None or ( + isinstance(weight_attr, str) + and weight_attr.split(':', 1)[0].lower() + in {'cpu', 'cuda', 'gpu', 'xpu', 'mps', 'meta'} + ) + if is_torch_device: + if not isinstance(data_format, str): + device, dtype = weight_attr, data_format + weight_attr, data_format = None, "NCHW" + elif weight_attr is not None and data_format == "NCHW": + device, weight_attr = weight_attr, None super().__init__() self._num_parameters = num_parameters self._init = init diff --git a/test/compat/test_compat_namespace_aliased.py b/test/compat/test_compat_namespace_aliased.py index 70448c9e7f632..825700b21ae77 100644 --- a/test/compat/test_compat_namespace_aliased.py +++ b/test/compat/test_compat_namespace_aliased.py @@ -60,6 +60,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"), @@ -152,6 +155,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( @@ -539,6 +552,9 @@ def test_tensor_methods_caller_aware(self): for paddle-internal ``x.max(axis=1)``; restored on disable.""" 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 +567,54 @@ 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.type(), "torch.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.assertIs(t.type(paddle.float32), t) + self.assertIs(t.type("torch.FloatTensor"), t) + with self.assertRaises(ValueError): + t.type("float64") + self.assertEqual( + paddle.ones([1], dtype="int64").type(), "torch.LongTensor" + ) + self.assertEqual( + paddle.ones([1], dtype="float8_e4m3fn").type(), + "torch.Float8_e4m3fnTensor", + ) + self.assertEqual( + paddle.ones([1], dtype="float8_e5m2").type(), + "torch.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) 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) @with_level2 def test_aliased_class_caller_aware(self): diff --git a/test/legacy_test/test_api_compatibility_part2.py b/test/legacy_test/test_api_compatibility_part2.py index 59c6dc7c259e9..ec96b6730bcba 100644 --- a/test/legacy_test/test_api_compatibility_part2.py +++ b/test/legacy_test/test_api_compatibility_part2.py @@ -3176,17 +3176,30 @@ 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 device without dtype + out6 = paddle.nn.PReLU(2, 0.5, "cpu")(x) + # 7. PyTorch positional dtype without device + out7 = 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]: 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) + out8 = layer64(input=x64) self.assertEqual(layer64._weight.dtype, paddle.float64) np.testing.assert_allclose( - out5.numpy(), self._expected(self.np_x64), rtol=1e-6 + out8.numpy(), self._expected(self.np_x64), rtol=1e-6 + ) + + layer64_positional = paddle.nn.PReLU(2, 0.5, None, paddle.float64) + out9 = layer64_positional(x64) + self.assertEqual(layer64_positional._weight.dtype, paddle.float64) + np.testing.assert_allclose( + out9.numpy(), self._expected(self.np_x64), rtol=1e-6 ) paddle.enable_static() @@ -3212,13 +3225,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) From d5154f313be2964a4375de74be241493ee2c25cc Mon Sep 17 00:00:00 2001 From: manfredss Date: Mon, 27 Jul 2026 12:56:13 +0000 Subject: [PATCH 02/21] fix PReLU, fix enable_compat restore after using guard --- python/paddle/compat/nn/__init__.py | 17 +++++++++++ python/paddle/compat/proxy.py | 4 ++- python/paddle/nn/layer/activation.py | 14 ++------- test/compat/test_compat_namespace_aliased.py | 29 +++++++++++++++++++ .../test_api_compatibility_part2.py | 6 ++-- 5 files changed, 56 insertions(+), 14 deletions(-) diff --git a/python/paddle/compat/nn/__init__.py b/python/paddle/compat/nn/__init__.py index 2ca059b159872..dc79467092719 100644 --- a/python/paddle/compat/nn/__init__.py +++ b/python/paddle/compat/nn/__init__.py @@ -40,6 +40,7 @@ __all__ = [ 'Unfold', 'Linear', + 'PReLU', 'Softmax', 'AvgPool1D', 'AvgPool2D', @@ -662,6 +663,22 @@ def reset_parameters(self) -> None: nn.init.uniform_(self.bias, -bound, bound) +class PReLU(nn.PReLU, metaclass=_CompatClassMeta): + def __init__( + self, + num_parameters: int = 1, + init: float = 0.25, + device: PlaceLike | None = None, + dtype: DTypeLike | None = None, + ) -> None: + super().__init__( + num_parameters=num_parameters, + init=init, + device=device, + dtype=dtype, + ) + + class Softmax(nn.Layer, metaclass=_CompatClassMeta): r""" Softmax Activation. diff --git a/python/paddle/compat/proxy.py b/python/paddle/compat/proxy.py index 22b54cc51ca47..579f57656d175 100644 --- a/python/paddle/compat/proxy.py +++ b/python/paddle/compat/proxy.py @@ -26,6 +26,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, @@ -604,6 +605,7 @@ def use_compat_guard( already_has_torch_proxy = TORCH_PROXY_FINDER in sys.meta_path original_local_enabled_scope = set(TORCH_PROXY_FINDER._local_enabled_scope) original_globally_enabled = TORCH_PROXY_FINDER._globally_enabled + original_level = 2 if _PADDLE_NAMESPACE_SAVED else 1 if enable == already_has_torch_proxy and ( (original_globally_enabled and scope is None) or (original_local_enabled_scope == (scope or set())) @@ -625,7 +627,7 @@ def use_compat_guard( try: yield finally: - enable_compat(scope=None, silent=True) + enable_compat(scope=None, silent=True, level=original_level) TORCH_PROXY_FINDER._local_enabled_scope = ( original_local_enabled_scope ) diff --git a/python/paddle/nn/layer/activation.py b/python/paddle/nn/layer/activation.py index fa63214516f19..2f8863fbd1633 100644 --- a/python/paddle/nn/layer/activation.py +++ b/python/paddle/nn/layer/activation.py @@ -586,17 +586,9 @@ def __init__( device: PlaceLike | None = None, dtype: DTypeLike | None = None, ) -> None: - is_torch_device = weight_attr is None or ( - isinstance(weight_attr, str) - and weight_attr.split(':', 1)[0].lower() - in {'cpu', 'cuda', 'gpu', 'xpu', 'mps', 'meta'} - ) - if is_torch_device: - if not isinstance(data_format, str): - device, dtype = weight_attr, data_format - weight_attr, data_format = None, "NCHW" - elif weight_attr is not None and data_format == "NCHW": - device, weight_attr = weight_attr, None + if not isinstance(data_format, str): + device, dtype = weight_attr, data_format + weight_attr, data_format = None, "NCHW" super().__init__() self._num_parameters = num_parameters self._init = init diff --git a/test/compat/test_compat_namespace_aliased.py b/test/compat/test_compat_namespace_aliased.py index 825700b21ae77..5111b603df90c 100644 --- a/test/compat/test_compat_namespace_aliased.py +++ b/test/compat/test_compat_namespace_aliased.py @@ -70,6 +70,7 @@ class CompatNamespaceAliasBase(unittest.TestCase): (paddle.nn, "BatchNorm2d"), (paddle.nn, "BatchNorm3d"), (paddle.nn, "MultiheadAttention"), + (paddle.nn, "PReLU"), ] # Compat-only symbols must not be added to the paddle namespace. COMPAT_ONLY = [ @@ -187,6 +188,7 @@ def test_class_type_compatibility_without_alias(self): "BatchNorm3D": (2,), "SmoothL1Loss": (), "MultiheadAttention": (4, 1), + "PReLU": (), "Categorical": (paddle.to_tensor([0.5, 0.5]),), } native_classes = { @@ -201,6 +203,7 @@ def test_class_type_compatibility_without_alias(self): "BatchNorm3D": paddle.nn.BatchNorm3D, "SmoothL1Loss": paddle.nn.SmoothL1Loss, "MultiheadAttention": paddle.nn.MultiHeadAttention, + "PReLU": paddle.nn.PReLU, "Categorical": paddle.distributions.Categorical, } compat_modules = (paddle.compat.nn, paddle.compat.distributions) @@ -228,6 +231,7 @@ def test_class_type_compatibility_without_alias(self): @with_level2 def test_submodule_symbols_aliased(self): self.assertAliased(paddle.nn.Linear, paddle.compat.nn.Linear) + self.assertAliased(paddle.nn.PReLU, paddle.compat.nn.PReLU) self.assertAliased(paddle.nn.Softmax, paddle.compat.nn.Softmax) self.assertAliased(paddle.nn.Unfold, paddle.compat.nn.Unfold) self.assertAliased( @@ -247,6 +251,10 @@ def test_aliased_signatures_are_torch_style(self): inspect.signature(paddle.nn.Linear), inspect.signature(paddle.compat.nn.Linear), ) + self.assertEqual( + inspect.signature(paddle.nn.PReLU), + inspect.signature(paddle.compat.nn.PReLU), + ) def test_compat_only_symbols_are_not_added(self): paddle.enable_compat(level=2) @@ -456,6 +464,21 @@ def test_bare_guard_keeps_level2_alias(self): paddle.disable_compat() self.assertNativeRestored() + def test_disabled_guard_restores_level2_aliases(self): + t = paddle.to_tensor([[3.0, 1.0, 2.0]]) + paddle.enable_compat(level=2) + try: + with paddle.use_compat_guard(enable=False): + self.assertNotIn(TORCH_PROXY_FINDER, sys.meta_path) + self.assertIs(paddle.sort, self._native[(paddle, "sort")]) + + self.assertIn(TORCH_PROXY_FINDER, sys.meta_path) + 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.""" @@ -633,6 +656,12 @@ def test_aliased_class_caller_aware(self): self.assertIsInstance(paddle.nn.Linear(2, 2), paddle.nn.Linear) self.assertTrue(issubclass(paddle.compat.nn.Linear, paddle.nn.Linear)) + @with_level2 + def test_prelu_positional_device_is_compat_only(self): + layer = paddle.nn.PReLU(2, 0.5, "cpu") + self.assertIs(type(layer), paddle.compat.nn.PReLU) + self.assertTrue(layer._weight.place.is_cpu_place()) + @with_level2 def test_aliased_class_subclassing_is_torch_style(self): """A user subclass derived from the alias class under level=2 uses the diff --git a/test/legacy_test/test_api_compatibility_part2.py b/test/legacy_test/test_api_compatibility_part2.py index ec96b6730bcba..1b5ce632029c1 100644 --- a/test/legacy_test/test_api_compatibility_part2.py +++ b/test/legacy_test/test_api_compatibility_part2.py @@ -3178,8 +3178,10 @@ def test_dygraph_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) - # 6. PyTorch positional device without dtype - out6 = paddle.nn.PReLU(2, 0.5, "cpu")(x) + # 6. Paddle string weight_attr keeps its original meaning + layer6 = paddle.nn.PReLU(2, 0.5, "cpu") + self.assertEqual(layer6._weight.name, "cpu") + out6 = layer6(x) # 7. PyTorch positional dtype without device out7 = paddle.nn.PReLU(2, 0.5, None, paddle.float32)(x) From 4865eccfc22f7c44585e8b4b2244ce4ab73470b9 Mon Sep 17 00:00:00 2001 From: manfredss Date: Tue, 28 Jul 2026 02:31:02 +0000 Subject: [PATCH 03/21] add test coverage --- test/compat/test_compat_namespace_aliased.py | 67 ++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/test/compat/test_compat_namespace_aliased.py b/test/compat/test_compat_namespace_aliased.py index 5111b603df90c..38b143424e5ac 100644 --- a/test/compat/test_compat_namespace_aliased.py +++ b/test/compat/test_compat_namespace_aliased.py @@ -18,8 +18,10 @@ import unittest from contextlib import contextmanager from functools import wraps +from unittest import mock 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 @@ -639,6 +641,71 @@ def test_tensor_methods_caller_aware(self): self.assertIs(paddle.Tensor.type, native_type) self.assertIs(paddle.Tensor.is_sparse, native_is_sparse) + @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) + + coo = paddle.sparse.sparse_coo_tensor([[0], [0]], [1.0], [1, 1]) + self.assertEqual(coo.type(), "torch.sparse.FloatTensor") + + class InvalidTensor: + pass + + with self.assertRaisesRegex( + ValueError, "invalid type: 'InvalidTensor'" + ): + t.type(InvalidTensor) + + @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.assertIs(paddle.Tensor.is_sparse, sparse_descriptor) + + 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_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, False)}, + 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): """Existing classes (Linear/...) become caller-aware proxies: external From 8aa81efd247a86979cf504199cae5df16bce58f2 Mon Sep 17 00:00:00 2001 From: manfredss Date: Tue, 28 Jul 2026 02:47:32 +0000 Subject: [PATCH 04/21] fix per bot feedback --- python/paddle/compat/__init__.py | 4 +--- test/compat/test_compat_namespace_aliased.py | 11 +++-------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/python/paddle/compat/__init__.py b/python/paddle/compat/__init__.py index b4cd75a479cba..a947eebb7eaf6 100644 --- a/python/paddle/compat/__init__.py +++ b/python/paddle/compat/__init__.py @@ -112,10 +112,8 @@ def _tensor_type( return f"{prefix}.{tensor_type}" device = None - if isinstance(dtype, type): + if isinstance(dtype, type) and dtype.__name__ in _TENSOR_TYPE_NAMES: tensor_type = dtype.__name__ - if tensor_type not in _TENSOR_TYPE_DTYPES: - raise ValueError(f"invalid type: {tensor_type!r}") dtype = _TENSOR_TYPE_DTYPES[tensor_type] device = "cpu" elif isinstance(dtype, str): diff --git a/test/compat/test_compat_namespace_aliased.py b/test/compat/test_compat_namespace_aliased.py index 38b143424e5ac..f4eb4f57e7ab0 100644 --- a/test/compat/test_compat_namespace_aliased.py +++ b/test/compat/test_compat_namespace_aliased.py @@ -20,6 +20,8 @@ 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 @@ -664,14 +666,7 @@ def test_tensor_type_edge_cases(self): coo = paddle.sparse.sparse_coo_tensor([[0], [0]], [1.0], [1, 1]) self.assertEqual(coo.type(), "torch.sparse.FloatTensor") - - class InvalidTensor: - pass - - with self.assertRaisesRegex( - ValueError, "invalid type: 'InvalidTensor'" - ): - t.type(InvalidTensor) + self.assertEqual(t.type(np.float64).dtype, paddle.float64) @with_level2 def test_tensor_descriptor_class_access(self): From 7f2684a92df9ba3ec1096ffaa30c8f060839e3e8 Mon Sep 17 00:00:00 2001 From: manfredss Date: Tue, 28 Jul 2026 03:10:17 +0000 Subject: [PATCH 05/21] fix typo --- python/paddle/compat/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/paddle/compat/__init__.py b/python/paddle/compat/__init__.py index a947eebb7eaf6..929f9c778f76f 100644 --- a/python/paddle/compat/__init__.py +++ b/python/paddle/compat/__init__.py @@ -112,7 +112,7 @@ def _tensor_type( return f"{prefix}.{tensor_type}" device = None - if isinstance(dtype, type) and dtype.__name__ in _TENSOR_TYPE_NAMES: + if isinstance(dtype, type) and dtype.__name__ in _TENSOR_TYPE_DTYPES: tensor_type = dtype.__name__ dtype = _TENSOR_TYPE_DTYPES[tensor_type] device = "cpu" From 7eda286c8be5e4e289e5f2657468ee2417f88818 Mon Sep 17 00:00:00 2001 From: manfredss Date: Tue, 28 Jul 2026 13:54:28 +0000 Subject: [PATCH 06/21] also fix paddle.distributions.categorical.Categorical --- .../paddle/compat/distributions/categorical.py | 16 ++++++++++++++++ test/compat/test_compat_namespace_aliased.py | 4 ++++ .../test_api_compatibility_part3.py | 18 ++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/python/paddle/compat/distributions/categorical.py b/python/paddle/compat/distributions/categorical.py index 910aba15f3134..b3a9e667cc2f6 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/test/compat/test_compat_namespace_aliased.py b/test/compat/test_compat_namespace_aliased.py index f4eb4f57e7ab0..3c8e86933dc4e 100644 --- a/test/compat/test_compat_namespace_aliased.py +++ b/test/compat/test_compat_namespace_aliased.py @@ -244,6 +244,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): diff --git a/test/legacy_test/test_api_compatibility_part3.py b/test/legacy_test/test_api_compatibility_part3.py index 16f3fda922bab..b5966e3d6beb4 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 From ec4686bbe0466620ebd8d814b29d2b5dd171a0e4 Mon Sep 17 00:00:00 2001 From: manfredss Date: Wed, 29 Jul 2026 13:48:57 +0000 Subject: [PATCH 07/21] fix --- python/paddle/compat/__init__.py | 41 +++++++++++++++-- python/paddle/compat/api_dispatch.py | 46 +++++++------------ python/paddle/compat/nn/__init__.py | 17 ------- python/paddle/nn/layer/activation.py | 35 ++++++++++++-- python/paddle/utils/decorator_utils.py | 45 ++++++++++++++++++ test/compat/test_compat_namespace_aliased.py | 16 +------ .../test_api_compatibility_part2.py | 28 +++++++---- 7 files changed, 150 insertions(+), 78 deletions(-) diff --git a/python/paddle/compat/__init__.py b/python/paddle/compat/__init__.py index 929f9c778f76f..159e6fe71e9ad 100644 --- a/python/paddle/compat/__init__.py +++ b/python/paddle/compat/__init__.py @@ -88,6 +88,15 @@ def __getattr__(name): 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) @@ -97,6 +106,22 @@ def _tensor_type( 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. When it is ``None``, returns a PyTorch-style tensor type + string. Default: ``None``. + non_blocking (bool, optional): Whether the conversion may occur + asynchronously. Default: ``False``. + + Returns: + str|Tensor: A tensor type string when ``dtype`` is ``None``; otherwise, + a tensor with the requested type. + """ if "async" in kwargs: non_blocking = kwargs.pop("async") if kwargs: @@ -144,14 +169,24 @@ def _tensor_type( ) +@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() _TENSOR_API_OVERRIDES = { - 'numel': (_tensor_numel, False), - 'type': (_tensor_type, False), - 'is_sparse': (_tensor_is_sparse, True), + '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 03741070f60e4..007f215e5a82a 100644 --- a/python/paddle/compat/api_dispatch.py +++ b/python/paddle/compat/api_dispatch.py @@ -124,17 +124,19 @@ def __call__(cls, *args: Any, **kwargs: Any) -> Any: class _TensorCompatDescriptor: - """Caller-aware adapter for Tensor method/property shape differences.""" + """Caller-aware dispatcher for a ``paddle.Tensor`` API.""" def __init__( self, native_attr: Any, - compat_fn: Any, - as_property: bool, + compat_attr: Any, ) -> None: + self._compat_is_property = isinstance(compat_attr, property) + compat_fn = ( + compat_attr.fget if self._compat_is_property else compat_attr + ) self.__native_fn__ = native_attr self.__compat_fn__ = compat_fn - self._as_property = as_property self.__doc__ = compat_fn.__doc__ self.__name__ = compat_fn.__name__ self.__signature__ = inspect.signature(compat_fn) @@ -143,44 +145,28 @@ def __get__(self, instance: Any, owner: type | None = None) -> Any: if instance is None: if _caller_is_paddle_internal(): return self.__native_fn__ - return self if self._as_property else self.__compat_fn__ + return self if self._compat_is_property else self.__compat_fn__ if ( len(_PADDLE_NAMESPACE_SAVED) > 0 and not _caller_is_paddle_internal() ): - if self._as_property: + if self._compat_is_property: return self.__compat_fn__(instance) return self.__compat_fn__.__get__(instance, owner) return self.__native_fn__.__get__(instance, owner) 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: - 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), - ) - - for attr_name, ( - compat_fn, - as_property, - ) in compat_root._TENSOR_API_OVERRIDES.items(): + tensor_apis = { + attr_name: getattr(compat_root, attr_name) + for attr_name in getattr(compat_root, "__all__", ()) + } + tensor_apis.update(compat_root._TENSOR_API_OVERRIDES) + for attr_name, compat_attr in tensor_apis.items(): native_attr = inspect.getattr_static(paddle.Tensor, attr_name, None) if native_attr is None: continue @@ -188,7 +174,7 @@ def _patch_tensor_methods() -> None: setattr( paddle.Tensor, attr_name, - _TensorCompatDescriptor(native_attr, compat_fn, as_property), + _TensorCompatDescriptor(native_attr, compat_attr), ) diff --git a/python/paddle/compat/nn/__init__.py b/python/paddle/compat/nn/__init__.py index dc79467092719..2ca059b159872 100644 --- a/python/paddle/compat/nn/__init__.py +++ b/python/paddle/compat/nn/__init__.py @@ -40,7 +40,6 @@ __all__ = [ 'Unfold', 'Linear', - 'PReLU', 'Softmax', 'AvgPool1D', 'AvgPool2D', @@ -663,22 +662,6 @@ def reset_parameters(self) -> None: nn.init.uniform_(self.bias, -bound, bound) -class PReLU(nn.PReLU, metaclass=_CompatClassMeta): - def __init__( - self, - num_parameters: int = 1, - init: float = 0.25, - device: PlaceLike | None = None, - dtype: DTypeLike | None = None, - ) -> None: - super().__init__( - num_parameters=num_parameters, - init=init, - device=device, - dtype=dtype, - ) - - class Softmax(nn.Layer, metaclass=_CompatClassMeta): r""" Softmax Activation. diff --git a/python/paddle/nn/layer/activation.py b/python/paddle/nn/layer/activation.py index 2f8863fbd1633..7f92d0bc9dcd6 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, @@ -586,9 +616,6 @@ def __init__( device: PlaceLike | None = None, dtype: DTypeLike | None = None, ) -> None: - if not isinstance(data_format, str): - device, dtype = weight_attr, data_format - weight_attr, data_format = None, "NCHW" super().__init__() self._num_parameters = num_parameters self._init = init diff --git a/python/paddle/utils/decorator_utils.py b/python/paddle/utils/decorator_utils.py index 5dd2f9a321672..bd7199221e736 100644 --- a/python/paddle/utils/decorator_utils.py +++ b/python/paddle/utils/decorator_utils.py @@ -240,6 +240,51 @@ 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: + third_arg = args[3] + device_types = { + "cpu", + "cuda", + "gpu", + "dcu", + "xpu", + "ipu", + *(paddle.device.get_all_custom_device_type() or ()), + } + is_device_string = ( + isinstance(third_arg, str) + and third_arg.lower().split(":", 1)[0] in device_types + ) + is_torch_call = ( + isinstance(third_arg, paddle.base.libpaddle.Place) + or is_device_string + or (len(args) == 5 and not isinstance(args[4], str)) + ) + if is_torch_call: + 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/test_compat_namespace_aliased.py b/test/compat/test_compat_namespace_aliased.py index 3c8e86933dc4e..e14b594dd1937 100644 --- a/test/compat/test_compat_namespace_aliased.py +++ b/test/compat/test_compat_namespace_aliased.py @@ -74,7 +74,6 @@ class CompatNamespaceAliasBase(unittest.TestCase): (paddle.nn, "BatchNorm2d"), (paddle.nn, "BatchNorm3d"), (paddle.nn, "MultiheadAttention"), - (paddle.nn, "PReLU"), ] # Compat-only symbols must not be added to the paddle namespace. COMPAT_ONLY = [ @@ -192,7 +191,6 @@ def test_class_type_compatibility_without_alias(self): "BatchNorm3D": (2,), "SmoothL1Loss": (), "MultiheadAttention": (4, 1), - "PReLU": (), "Categorical": (paddle.to_tensor([0.5, 0.5]),), } native_classes = { @@ -207,7 +205,6 @@ def test_class_type_compatibility_without_alias(self): "BatchNorm3D": paddle.nn.BatchNorm3D, "SmoothL1Loss": paddle.nn.SmoothL1Loss, "MultiheadAttention": paddle.nn.MultiHeadAttention, - "PReLU": paddle.nn.PReLU, "Categorical": paddle.distributions.Categorical, } compat_modules = (paddle.compat.nn, paddle.compat.distributions) @@ -235,7 +232,6 @@ def test_class_type_compatibility_without_alias(self): @with_level2 def test_submodule_symbols_aliased(self): self.assertAliased(paddle.nn.Linear, paddle.compat.nn.Linear) - self.assertAliased(paddle.nn.PReLU, paddle.compat.nn.PReLU) self.assertAliased(paddle.nn.Softmax, paddle.compat.nn.Softmax) self.assertAliased(paddle.nn.Unfold, paddle.compat.nn.Unfold) self.assertAliased( @@ -259,10 +255,6 @@ def test_aliased_signatures_are_torch_style(self): inspect.signature(paddle.nn.Linear), inspect.signature(paddle.compat.nn.Linear), ) - self.assertEqual( - inspect.signature(paddle.nn.PReLU), - inspect.signature(paddle.compat.nn.PReLU), - ) def test_compat_only_symbols_are_not_added(self): paddle.enable_compat(level=2) @@ -698,7 +690,7 @@ def test_missing_tensor_override_is_skipped(self): mock.patch.object(paddle.compat, "__all__", ()), mock.patch.dict( paddle.compat._TENSOR_API_OVERRIDES, - {missing_attr: (mock.sentinel.compat_fn, False)}, + {missing_attr: mock.sentinel.compat_fn}, clear=True, ), ): @@ -722,12 +714,6 @@ def test_aliased_class_caller_aware(self): self.assertIsInstance(paddle.nn.Linear(2, 2), paddle.nn.Linear) self.assertTrue(issubclass(paddle.compat.nn.Linear, paddle.nn.Linear)) - @with_level2 - def test_prelu_positional_device_is_compat_only(self): - layer = paddle.nn.PReLU(2, 0.5, "cpu") - self.assertIs(type(layer), paddle.compat.nn.PReLU) - self.assertTrue(layer._weight.place.is_cpu_place()) - @with_level2 def test_aliased_class_subclassing_is_torch_style(self): """A user subclass derived from the alias class under level=2 uses the diff --git a/test/legacy_test/test_api_compatibility_part2.py b/test/legacy_test/test_api_compatibility_part2.py index 1b5ce632029c1..69b33ec736cc5 100644 --- a/test/legacy_test/test_api_compatibility_part2.py +++ b/test/legacy_test/test_api_compatibility_part2.py @@ -3178,32 +3178,42 @@ def test_dygraph_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) - # 6. Paddle string weight_attr keeps its original meaning + # 6. PyTorch positional string device without dtype layer6 = paddle.nn.PReLU(2, 0.5, "cpu") - self.assertEqual(layer6._weight.name, "cpu") + paddle.nn.PReLU(2, 0.5, "cpu") + self.assertTrue(layer6._weight.place.is_cpu_place()) out6 = layer6(x) - # 7. PyTorch positional dtype without device - out7 = paddle.nn.PReLU(2, 0.5, None, paddle.float32)(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) + # 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, out5, out6, out7]: + 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") - out8 = layer64(input=x64) + out9 = layer64(input=x64) self.assertEqual(layer64._weight.dtype, paddle.float64) np.testing.assert_allclose( - out8.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) - out9 = layer64_positional(x64) + out10 = layer64_positional(x64) self.assertEqual(layer64_positional._weight.dtype, paddle.float64) np.testing.assert_allclose( - out9.numpy(), self._expected(self.np_x64), rtol=1e-6 + 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_static() def test_static_Compatibility(self): From cf76ec48f370d6a927dbd17d327b162847d4161d Mon Sep 17 00:00:00 2001 From: manfredss Date: Thu, 30 Jul 2026 02:48:18 +0000 Subject: [PATCH 08/21] fix --- python/paddle/utils/decorator_utils.py | 3 +++ test/legacy_test/test_api_compatibility_part2.py | 13 ++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/python/paddle/utils/decorator_utils.py b/python/paddle/utils/decorator_utils.py index bd7199221e736..149d1f62d9ecb 100644 --- a/python/paddle/utils/decorator_utils.py +++ b/python/paddle/utils/decorator_utils.py @@ -262,8 +262,11 @@ def wrapper(*args: _InputT.args, **kwargs: _InputT.kwargs) -> _RetT: "ipu", *(paddle.device.get_all_custom_device_type() or ()), } + from paddle.compat.api_dispatch import _PADDLE_NAMESPACE_SAVED + is_device_string = ( isinstance(third_arg, str) + and bool(_PADDLE_NAMESPACE_SAVED) and third_arg.lower().split(":", 1)[0] in device_types ) is_torch_call = ( diff --git a/test/legacy_test/test_api_compatibility_part2.py b/test/legacy_test/test_api_compatibility_part2.py index 69b33ec736cc5..06b33874e6e06 100644 --- a/test/legacy_test/test_api_compatibility_part2.py +++ b/test/legacy_test/test_api_compatibility_part2.py @@ -3178,10 +3178,9 @@ def test_dygraph_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) - # 6. PyTorch positional string device without dtype + # 6. Paddle string weight_attr keeps its original meaning layer6 = paddle.nn.PReLU(2, 0.5, "cpu") - paddle.nn.PReLU(2, 0.5, "cpu") - self.assertTrue(layer6._weight.place.is_cpu_place()) + self.assertEqual(layer6._weight.name, "cpu") out6 = layer6(x) # 7. Paddle string weight_attr keeps its original meaning layer7 = paddle.nn.PReLU(2, 0.5, "prelu_weight") @@ -3214,6 +3213,14 @@ def test_dygraph_Compatibility(self): 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): From f668adbf2a010e2d3f2aa26ad8246b87ab3eadc2 Mon Sep 17 00:00:00 2001 From: manfredss Date: Thu, 30 Jul 2026 08:03:28 +0000 Subject: [PATCH 09/21] Refine compat levels and guard state restoration --- python/paddle/compat/proxy.py | 174 +++++++++++++------ test/compat/test_compat_namespace_aliased.py | 164 +++++++++++++++-- test/compat/test_torch_proxy_mixed.py | 21 +++ 3 files changed, 290 insertions(+), 69 deletions(-) diff --git a/python/paddle/compat/proxy.py b/python/paddle/compat/proxy.py index 579f57656d175..d7d9c3096df98 100644 --- a/python/paddle/compat/proxy.py +++ b/python/paddle/compat/proxy.py @@ -465,6 +465,25 @@ def _parse_scope(scope: str | Iterable[str] | None) -> set[str] | None: return set(scope) +def _get_compat_level() -> int | None: + level = 0 + if TORCH_PROXY_FINDER in sys.meta_path: + level |= 1 + if _PADDLE_NAMESPACE_SAVED: + level |= 2 + return level or None + + +def _clear_compat_state() -> None: + had_torch_proxy = TORCH_PROXY_FINDER in sys.meta_path + while TORCH_PROXY_FINDER in sys.meta_path: + sys.meta_path.remove(TORCH_PROXY_FINDER) + _restore_paddle_namespace_aliases() + if had_torch_proxy: + _clear_torch_proxy_modules() + _copy_torch_modules_from_cache() + + def enable_compat( *, scope: _ScopeType = None, @@ -474,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 @@ -514,26 +534,28 @@ 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: + if level in {1, 3}: + 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 in {2, 3}: _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 @@ -548,13 +570,15 @@ 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 @@ -563,22 +587,28 @@ def use_compat_guard( enable: bool = True, scope: _ScopeType = None, silent: bool = False, + level: int | None = None, ) -> Generator[None, None, None]: """ - Context manager to temporarily enable or disable the PyTorch compat. + Context manager to temporarily enable or disable PyTorch compatibility. - 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. + When `enable` is True (default), compat is enabled for the duration of the + context and restored to its previous state afterwards. When `enable` 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 + enable (bool, optional): Whether to enable or disable compatibility within the context. Defaults to True. scope (str or Iterable[str], optional): Specific module or modules to enable - PyTorch compat for. If None, uses the global scope. Defaults to None. + the torch proxy for at level 1 or 3. If None, uses the global scope. + Defaults to None. silent (bool, optional): If True, suppresses warnings about scope changes. Defaults to False. + level (int|None, optional): The compatibility level to use in the context. + ``1`` enables the torch proxy, ``2`` enables Paddle namespace aliases, + and ``3`` enables both. If None, preserves the active level or uses + level 1 when compat is disabled. Defaults to None. Example: .. code-block:: pycon @@ -601,33 +631,71 @@ def use_compat_guard( ... ... assert torch.sin is paddle.sin """ + if level is not None and level not in {1, 2, 3}: + raise ValueError( + f"Unsupported level: {level}. It should be 1, 2, or 3." + ) + scope = _parse_scope(scope) - already_has_torch_proxy = TORCH_PROXY_FINDER in sys.meta_path + original_level = _get_compat_level() + original_torch_proxy_count = sys.meta_path.count(TORCH_PROXY_FINDER) original_local_enabled_scope = set(TORCH_PROXY_FINDER._local_enabled_scope) original_globally_enabled = TORCH_PROXY_FINDER._globally_enabled - original_level = 2 if _PADDLE_NAMESPACE_SAVED else 1 - if enable == already_has_torch_proxy and ( - (original_globally_enabled and scope is None) - or (original_local_enabled_scope == (scope or set())) + target_level = (level or original_level or 1) if enable else None + target_scope = scope + if ( + original_level in {1, 3} + and target_level in {1, 3} + and not original_globally_enabled + and scope is not None ): + target_scope = original_local_enabled_scope | scope + scope_matches = target_level not in {1, 3} or ( + (original_globally_enabled and target_scope is None) + or ( + not original_globally_enabled + and original_local_enabled_scope == (target_scope or set()) + ) + ) + + try: + if original_level != target_level or not scope_matches: + _clear_compat_state() + if target_level is not None: + enable_compat( + scope=target_scope, + silent=silent, + level=target_level, + ) yield - return - if enable: - enable_compat(scope=scope, silent=silent) - try: - yield - finally: - TORCH_PROXY_FINDER._local_enabled_scope = ( - original_local_enabled_scope - ) - TORCH_PROXY_FINDER._globally_enabled = original_globally_enabled - disable_compat() - else: - disable_compat() - try: - yield - finally: - enable_compat(scope=None, silent=True, level=original_level) + finally: + state_changed = ( + _get_compat_level() != original_level + or sys.meta_path.count(TORCH_PROXY_FINDER) + != original_torch_proxy_count + or TORCH_PROXY_FINDER._local_enabled_scope + != original_local_enabled_scope + or TORCH_PROXY_FINDER._globally_enabled != original_globally_enabled + ) + if state_changed: + _clear_compat_state() + if original_level is not None: + original_scope = ( + None + if original_globally_enabled + else original_local_enabled_scope + ) + enable_compat( + scope=original_scope, + silent=True, + level=original_level, + ) + for _ in range(1, original_torch_proxy_count): + enable_compat( + scope=original_scope, + silent=True, + level=1, + ) TORCH_PROXY_FINDER._local_enabled_scope = ( original_local_enabled_scope ) diff --git a/test/compat/test_compat_namespace_aliased.py b/test/compat/test_compat_namespace_aliased.py index e14b594dd1937..3f424ddc19e5f 100644 --- a/test/compat/test_compat_namespace_aliased.py +++ b/test/compat/test_compat_namespace_aliased.py @@ -32,11 +32,8 @@ @contextmanager def level2_guard(): - paddle.enable_compat(level=2) - try: + with paddle.use_compat_guard(level=2): yield - finally: - paddle.disable_compat() def with_level2(func): @@ -85,14 +82,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 @@ -117,6 +114,16 @@ def assertAliased(self, paddle_attr, compat_attr): ) self.assertIs(target, compat_attr) + def assertCompatLevel(self, level): + self.assertEqual( + TORCH_PROXY_FINDER in sys.meta_path, + level in {1, 3}, + ) + self.assertEqual( + bool(_PADDLE_NAMESPACE_SAVED), + level in {2, 3}, + ) + class TestTopLevelAlias(CompatNamespaceAliasBase): def test_public_level_parameter_is_minimal(self): @@ -128,13 +135,20 @@ def test_public_level_parameter_is_minimal(self): inspect.signature(paddle.enable_compat).parameters["level"].default, 1, ) - self.assertNotIn( - "level", inspect.signature(paddle.use_compat_guard).parameters + self.assertIsNone( + inspect.signature(paddle.use_compat_guard) + .parameters["level"] + .default ) 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) + with ( + self.assertRaisesRegex(ValueError, "Unsupported level: 4"), + paddle.use_compat_guard(level=4), + ): + pass self.assertNotIn(TORCH_PROXY_FINDER, sys.meta_path) self.assertNativeRestored() @@ -428,6 +442,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() @@ -455,6 +470,7 @@ def test_bare_guard_keeps_level2_alias(self): paddle.enable_compat(level=2) try: with paddle.use_compat_guard(): + self.assertNotIn(TORCH_PROXY_FINDER, sys.meta_path) self.assertAliased(paddle.sort, paddle.compat.sort) self.assertTrue(hasattr(paddle.sort(t, dim=-1), "values")) # the level in effect survives the guard @@ -472,16 +488,132 @@ def test_disabled_guard_restores_level2_aliases(self): self.assertNotIn(TORCH_PROXY_FINDER, sys.meta_path) self.assertIs(paddle.sort, self._native[(paddle, "sort")]) - self.assertIn(TORCH_PROXY_FINDER, sys.meta_path) + self.assertNotIn(TORCH_PROXY_FINDER, sys.meta_path) self.assertAliased(paddle.sort, paddle.compat.sort) self.assertTrue(hasattr(paddle.sort(t, dim=-1), "values")) finally: paddle.disable_compat() self.assertNativeRestored() + def test_disabled_guard_contains_body_level2_enable(self): + native_softmax = paddle.nn.Softmax + x = paddle.to_tensor([[1.0, 2.0, 3.0]]) + + with paddle.use_compat_guard(enable=False): + + class NativeSoftmaxSubclass(paddle.nn.Softmax): + pass + + self.assertIs(NativeSoftmaxSubclass.__mro__[1], native_softmax) + paddle.enable_compat(level=2) + self.assertCompatLevel(2) + self.assertEqual( + NativeSoftmaxSubclass(axis=-1)(x).shape, + x.shape, + ) + with self.assertRaises(TypeError): + paddle.nn.Softmax(axis=-1) + self.assertEqual(paddle.nn.Softmax(dim=-1)(x).shape, x.shape) + + self.assertCompatLevel(None) + self.assertIs(paddle.nn.Softmax, native_softmax) + self.assertNativeRestored() + + def test_guard_levels_restore_disabled_state(self): + for level in (1, 2, 3): + with self.subTest(level=level): + with paddle.use_compat_guard(level=level): + self.assertCompatLevel(level) + self.assertCompatLevel(None) + self.assertNativeRestored() + + def test_nested_guard_restores_each_outer_level(self): + for outer_level, inner_level in ((1, 2), (2, 3), (3, 1)): + with self.subTest( + outer_level=outer_level, + inner_level=inner_level, + ): + with paddle.use_compat_guard(level=outer_level): + self.assertCompatLevel(outer_level) + with paddle.use_compat_guard(level=inner_level): + self.assertCompatLevel(inner_level) + self.assertCompatLevel(outer_level) + with paddle.use_compat_guard(enable=False): + self.assertCompatLevel(None) + self.assertCompatLevel(outer_level) + self.assertCompatLevel(None) + self.assertNativeRestored() + + def test_same_level_guard_restores_body_changes(self): + paddle.enable_compat(level=2) + try: + with paddle.use_compat_guard(level=2): + paddle.disable_compat() + self.assertCompatLevel(None) + self.assertCompatLevel(2) + finally: + paddle.disable_compat() + self.assertNativeRestored() + + def test_guard_restores_after_exception(self): + with ( + self.assertRaisesRegex(RuntimeError, "expected"), + paddle.use_compat_guard(level=2), + ): + self.assertCompatLevel(2) + raise RuntimeError("expected") + self.assertCompatLevel(None) + self.assertNativeRestored() + + def test_guard_restores_repeated_proxy_and_scope(self): + scope = {"torch_proxy_local_enabled_module"} + paddle.enable_compat(scope=scope) + paddle.enable_compat(scope=scope, silent=True) + self.assertEqual(sys.meta_path.count(TORCH_PROXY_FINDER), 2) + try: + with paddle.use_compat_guard(level=2): + self.assertCompatLevel(2) + self.assertCompatLevel(1) + self.assertEqual(sys.meta_path.count(TORCH_PROXY_FINDER), 2) + self.assertFalse(TORCH_PROXY_FINDER._globally_enabled) + self.assertEqual(TORCH_PROXY_FINDER._local_enabled_scope, scope) + finally: + paddle.disable_compat() + paddle.disable_compat() + self.assertNativeRestored() + + def test_nested_guard_extends_local_scope(self): + outer_scope = {"outer_local_module"} + inner_scope = {"inner_local_module"} + with paddle.use_compat_guard(level=1, scope=outer_scope): + self.assertFalse(TORCH_PROXY_FINDER._globally_enabled) + self.assertEqual( + TORCH_PROXY_FINDER._local_enabled_scope, + outer_scope, + ) + with paddle.use_compat_guard(scope=inner_scope): + self.assertFalse(TORCH_PROXY_FINDER._globally_enabled) + self.assertEqual( + TORCH_PROXY_FINDER._local_enabled_scope, + outer_scope | inner_scope, + ) + self.assertEqual( + TORCH_PROXY_FINDER._local_enabled_scope, + outer_scope, + ) + self.assertCompatLevel(None) + self.assertNativeRestored() + + def test_guard_contains_repeated_level2_enable(self): + with paddle.use_compat_guard(level=2): + paddle.enable_compat(level=2) + self.assertCompatLevel(2) + self.assertCompatLevel(None) + 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(): @@ -505,8 +637,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 @@ -520,8 +652,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: diff --git a/test/compat/test_torch_proxy_mixed.py b/test/compat/test_torch_proxy_mixed.py index a558a8cc3d8d8..119c8df9a74bd 100644 --- a/test/compat/test_torch_proxy_mixed.py +++ b/test/compat/test_torch_proxy_mixed.py @@ -61,6 +61,27 @@ def test_nested_torch_proxy(self): self.check_is_not_proxy() + def test_level2_does_not_proxy_torch(self): + self.check_is_not_proxy() + with paddle.use_compat_guard(level=2): + self.check_is_not_proxy() + import torch + + self.assertTrue(hasattr(torch, "SymInt")) + self.check_is_not_proxy() + + def test_level2_restores_real_torch_inside_proxy_guard(self): + self.check_is_not_proxy() + with paddle.use_compat_guard(level=1): + self.check_is_proxy() + with paddle.use_compat_guard(level=2): + self.check_is_not_proxy() + import torch + + self.assertTrue(hasattr(torch, "SymInt")) + self.check_is_proxy() + self.check_is_not_proxy() + def test_local_enabled_module_import(self): self.check_is_not_proxy() with paddle.use_compat_guard( From b3bb47c1815bfbdbe1700dbd46b1ac63e226b029 Mon Sep 17 00:00:00 2001 From: manfredss Date: Thu, 30 Jul 2026 11:38:11 +0000 Subject: [PATCH 10/21] remove assertion --- test/compat/test_torch_proxy_mixed.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/compat/test_torch_proxy_mixed.py b/test/compat/test_torch_proxy_mixed.py index 119c8df9a74bd..541394388aa10 100644 --- a/test/compat/test_torch_proxy_mixed.py +++ b/test/compat/test_torch_proxy_mixed.py @@ -65,9 +65,6 @@ def test_level2_does_not_proxy_torch(self): self.check_is_not_proxy() with paddle.use_compat_guard(level=2): self.check_is_not_proxy() - import torch - - self.assertTrue(hasattr(torch, "SymInt")) self.check_is_not_proxy() def test_level2_restores_real_torch_inside_proxy_guard(self): @@ -76,9 +73,6 @@ def test_level2_restores_real_torch_inside_proxy_guard(self): self.check_is_proxy() with paddle.use_compat_guard(level=2): self.check_is_not_proxy() - import torch - - self.assertTrue(hasattr(torch, "SymInt")) self.check_is_proxy() self.check_is_not_proxy() From 2f3cc3617d0bcbc68da4ae8afac9621b7dfc733a Mon Sep 17 00:00:00 2001 From: manfredss Date: Fri, 31 Jul 2026 03:11:35 +0000 Subject: [PATCH 11/21] fix fleet tests failure --- python/paddle/compat/proxy.py | 15 +++++++++++++++ test/compat/test_torch_proxy.py | 6 ++++++ 2 files changed, 21 insertions(+) diff --git a/python/paddle/compat/proxy.py b/python/paddle/compat/proxy.py index d7d9c3096df98..e9a399df8f35b 100644 --- a/python/paddle/compat/proxy.py +++ b/python/paddle/compat/proxy.py @@ -667,6 +667,21 @@ def use_compat_guard( silent=silent, level=target_level, ) + for _ in range(1, target_level or 1): + enable_compat( + scope=target_scope, + silent=silent, + level=1, + ) + if ( + original_level in {1, 3} + and target_level in {1, 3} + and target_scope is None + and not original_globally_enabled + ): + TORCH_PROXY_FINDER._local_enabled_scope = ( + original_local_enabled_scope + ) yield finally: state_changed = ( diff --git a/test/compat/test_torch_proxy.py b/test/compat/test_torch_proxy.py index 57e607c945db7..d623e121a83e5 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): From f514e71f82a434e7286ea57be67ceed91b8f7833 Mon Sep 17 00:00:00 2001 From: manfredss Date: Fri, 31 Jul 2026 10:54:37 +0000 Subject: [PATCH 12/21] staged --- python/paddle/compat/proxy.py | 9 +++------ python/paddle/utils/decorator_utils.py | 18 +++++++++--------- .../__init__.py | 17 +++++++++++++++++ .../submodule.py | 19 +++++++++++++++++++ test/compat/test_torch_proxy_mixed.py | 3 +++ .../test_api_compatibility_part2.py | 4 ++-- 6 files changed, 53 insertions(+), 17 deletions(-) create mode 100644 test/compat/fake_modules/torch_proxy_local_enabled_package/__init__.py create mode 100644 test/compat/fake_modules/torch_proxy_local_enabled_package/submodule.py diff --git a/python/paddle/compat/proxy.py b/python/paddle/compat/proxy.py index e9a399df8f35b..9ec2e7044c7f7 100644 --- a/python/paddle/compat/proxy.py +++ b/python/paddle/compat/proxy.py @@ -576,8 +576,11 @@ def disable_compat() -> None: 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 + _restore_paddle_namespace_aliases() @@ -667,12 +670,6 @@ def use_compat_guard( silent=silent, level=target_level, ) - for _ in range(1, target_level or 1): - enable_compat( - scope=target_scope, - silent=silent, - level=1, - ) if ( original_level in {1, 3} and target_level in {1, 3} diff --git a/python/paddle/utils/decorator_utils.py b/python/paddle/utils/decorator_utils.py index 149d1f62d9ecb..d412904ca902b 100644 --- a/python/paddle/utils/decorator_utils.py +++ b/python/paddle/utils/decorator_utils.py @@ -262,17 +262,17 @@ def wrapper(*args: _InputT.args, **kwargs: _InputT.kwargs) -> _RetT: "ipu", *(paddle.device.get_all_custom_device_type() or ()), } - from paddle.compat.api_dispatch import _PADDLE_NAMESPACE_SAVED - - is_device_string = ( - isinstance(third_arg, str) - and bool(_PADDLE_NAMESPACE_SAVED) - and third_arg.lower().split(":", 1)[0] in device_types - ) is_torch_call = ( isinstance(third_arg, paddle.base.libpaddle.Place) - or is_device_string - or (len(args) == 5 and not isinstance(args[4], str)) + or ( + isinstance(third_arg, str) + and third_arg.lower().split(":", 1)[0] in device_types + ) + or ( + third_arg is None + and len(args) == 5 + and not isinstance(args[4], str) + ) ) if is_torch_call: for name, value in zip(("device", "dtype"), args[3:]): 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 0000000000000..5573bb57ca61a --- /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 0000000000000..c26e86582932c --- /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_torch_proxy_mixed.py b/test/compat/test_torch_proxy_mixed.py index 541394388aa10..09bc470db17e3 100644 --- a/test/compat/test_torch_proxy_mixed.py +++ b/test/compat/test_torch_proxy_mixed.py @@ -19,6 +19,7 @@ import paddle from paddle.compat.proxy import ( ProxyModule, + _get_compat_level, ) sys.path.append(str(pathlib.Path(__file__).parent / "fake_modules")) @@ -64,6 +65,7 @@ def test_nested_torch_proxy(self): def test_level2_does_not_proxy_torch(self): self.check_is_not_proxy() with paddle.use_compat_guard(level=2): + self.assertEqual(_get_compat_level(), 2) self.check_is_not_proxy() self.check_is_not_proxy() @@ -72,6 +74,7 @@ def test_level2_restores_real_torch_inside_proxy_guard(self): with paddle.use_compat_guard(level=1): self.check_is_proxy() with paddle.use_compat_guard(level=2): + self.assertEqual(_get_compat_level(), 2) self.check_is_not_proxy() self.check_is_proxy() self.check_is_not_proxy() diff --git a/test/legacy_test/test_api_compatibility_part2.py b/test/legacy_test/test_api_compatibility_part2.py index 06b33874e6e06..ead7d46cbc021 100644 --- a/test/legacy_test/test_api_compatibility_part2.py +++ b/test/legacy_test/test_api_compatibility_part2.py @@ -3178,9 +3178,9 @@ def test_dygraph_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) - # 6. Paddle string weight_attr keeps its original meaning + # 6. PyTorch positional string device without dtype layer6 = paddle.nn.PReLU(2, 0.5, "cpu") - self.assertEqual(layer6._weight.name, "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") From c64978fe198612b838ea58059c5db5487882ff74 Mon Sep 17 00:00:00 2001 From: manfredss Date: Mon, 3 Aug 2026 06:39:35 +0000 Subject: [PATCH 13/21] reframe use_compat_guard --- python/paddle/compat/proxy.py | 135 +++++++---------- test/compat/test_compat_namespace_aliased.py | 143 ++----------------- test/compat/test_torch_proxy_mixed.py | 43 ++++-- 3 files changed, 93 insertions(+), 228 deletions(-) diff --git a/python/paddle/compat/proxy.py b/python/paddle/compat/proxy.py index 9ec2e7044c7f7..e87bed0e280e9 100644 --- a/python/paddle/compat/proxy.py +++ b/python/paddle/compat/proxy.py @@ -548,8 +548,9 @@ def enable_compat( _swap_torch_modules_to_cache() _modify_scope_of_torch_proxy(scope, silent=silent) sys.meta_path.insert(0, TORCH_PROXY_FINDER) - - if level in {2, 3}: + if level == 3: + _apply_paddle_namespace_aliases() + else: _apply_paddle_namespace_aliases() @@ -576,10 +577,8 @@ def disable_compat() -> None: 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 _restore_paddle_namespace_aliases() @@ -590,28 +589,22 @@ def use_compat_guard( enable: bool = True, scope: _ScopeType = None, silent: bool = False, - level: int | None = None, ) -> Generator[None, None, None]: """ - Context manager to temporarily enable or disable PyTorch compatibility. + Context manager to temporarily enable or disable the PyTorch compat. - When `enable` is True (default), compat is enabled for the duration of the - context and restored to its previous state afterwards. When `enable` is - False, compat is disabled for the duration of the context and restored + 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, compat is disabled for the duration of the context and restored afterwards. Args: - enable (bool, optional): Whether to enable or disable compatibility + enable (bool, optional): Whether to enable or disable the PyTorch compat within the context. Defaults to True. scope (str or Iterable[str], optional): Specific module or modules to enable - the torch proxy for at level 1 or 3. If None, uses the global scope. - Defaults to None. + PyTorch compat for. If None, uses the global scope. Defaults to None. silent (bool, optional): If True, suppresses warnings about scope changes. Defaults to False. - level (int|None, optional): The compatibility level to use in the context. - ``1`` enables the torch proxy, ``2`` enables Paddle namespace aliases, - and ``3`` enables both. If None, preserves the active level or uses - level 1 when compat is disabled. Defaults to None. Example: .. code-block:: pycon @@ -634,80 +627,56 @@ def use_compat_guard( ... ... assert torch.sin is paddle.sin """ - if level is not None and level not in {1, 2, 3}: - raise ValueError( - f"Unsupported level: {level}. It should be 1, 2, or 3." - ) - scope = _parse_scope(scope) - original_level = _get_compat_level() - original_torch_proxy_count = sys.meta_path.count(TORCH_PROXY_FINDER) + already_has_torch_proxy = TORCH_PROXY_FINDER in sys.meta_path + has_paddle_aliases = bool(_PADDLE_NAMESPACE_SAVED) original_local_enabled_scope = set(TORCH_PROXY_FINDER._local_enabled_scope) original_globally_enabled = TORCH_PROXY_FINDER._globally_enabled - target_level = (level or original_level or 1) if enable else None - target_scope = scope - if ( - original_level in {1, 3} - and target_level in {1, 3} - and not original_globally_enabled - and scope is not None - ): - target_scope = original_local_enabled_scope | scope - scope_matches = target_level not in {1, 3} or ( - (original_globally_enabled and target_scope is None) + already_has_compat = already_has_torch_proxy or has_paddle_aliases + if not enable and not already_has_compat: + yield + return + if enable and ( + (has_paddle_aliases and not already_has_torch_proxy and scope is None) or ( - not original_globally_enabled - and original_local_enabled_scope == (target_scope or set()) + already_has_torch_proxy + and ( + (original_globally_enabled and scope is None) + or original_local_enabled_scope == (scope or set()) + ) ) - ) - - try: - if original_level != target_level or not scope_matches: - _clear_compat_state() - if target_level is not None: - enable_compat( - scope=target_scope, - silent=silent, - level=target_level, - ) - if ( - original_level in {1, 3} - and target_level in {1, 3} - and target_scope is None - and not original_globally_enabled - ): - TORCH_PROXY_FINDER._local_enabled_scope = ( - original_local_enabled_scope - ) + ): yield - finally: - state_changed = ( - _get_compat_level() != original_level - or sys.meta_path.count(TORCH_PROXY_FINDER) - != original_torch_proxy_count - or TORCH_PROXY_FINDER._local_enabled_scope - != original_local_enabled_scope - or TORCH_PROXY_FINDER._globally_enabled != original_globally_enabled + return + if enable: + enable_compat( + scope=scope, + silent=silent, + level=3 if has_paddle_aliases else 1, ) - if state_changed: - _clear_compat_state() - if original_level is not None: - original_scope = ( - None - if original_globally_enabled - else original_local_enabled_scope - ) - enable_compat( - scope=original_scope, - silent=True, - level=original_level, - ) - for _ in range(1, original_torch_proxy_count): - enable_compat( - scope=original_scope, - silent=True, - level=1, - ) + try: + yield + finally: + TORCH_PROXY_FINDER._local_enabled_scope = ( + original_local_enabled_scope + ) + TORCH_PROXY_FINDER._globally_enabled = original_globally_enabled + disable_compat() + if has_paddle_aliases: + _apply_paddle_namespace_aliases() + else: + disable_compat() + try: + yield + finally: + level = ( + 3 + if already_has_torch_proxy and has_paddle_aliases + else 2 + if has_paddle_aliases + else 1 + ) + enable_compat(scope=None, silent=True, level=level) TORCH_PROXY_FINDER._local_enabled_scope = ( original_local_enabled_scope ) diff --git a/test/compat/test_compat_namespace_aliased.py b/test/compat/test_compat_namespace_aliased.py index 3f424ddc19e5f..ac35714b564d7 100644 --- a/test/compat/test_compat_namespace_aliased.py +++ b/test/compat/test_compat_namespace_aliased.py @@ -32,8 +32,11 @@ @contextmanager def level2_guard(): - with paddle.use_compat_guard(level=2): + paddle.enable_compat(level=2) + try: yield + finally: + paddle.disable_compat() def with_level2(func): @@ -114,16 +117,6 @@ def assertAliased(self, paddle_attr, compat_attr): ) self.assertIs(target, compat_attr) - def assertCompatLevel(self, level): - self.assertEqual( - TORCH_PROXY_FINDER in sys.meta_path, - level in {1, 3}, - ) - self.assertEqual( - bool(_PADDLE_NAMESPACE_SAVED), - level in {2, 3}, - ) - class TestTopLevelAlias(CompatNamespaceAliasBase): def test_public_level_parameter_is_minimal(self): @@ -135,20 +128,13 @@ def test_public_level_parameter_is_minimal(self): inspect.signature(paddle.enable_compat).parameters["level"].default, 1, ) - self.assertIsNone( - inspect.signature(paddle.use_compat_guard) - .parameters["level"] - .default + self.assertNotIn( + "level", inspect.signature(paddle.use_compat_guard).parameters ) def test_invalid_level_has_no_side_effect(self): with self.assertRaisesRegex(ValueError, "Unsupported level: 4"): paddle.enable_compat(level=4) - with ( - self.assertRaisesRegex(ValueError, "Unsupported level: 4"), - paddle.use_compat_guard(level=4), - ): - pass self.assertNotIn(TORCH_PROXY_FINDER, sys.meta_path) self.assertNativeRestored() @@ -495,120 +481,17 @@ def test_disabled_guard_restores_level2_aliases(self): paddle.disable_compat() self.assertNativeRestored() - def test_disabled_guard_contains_body_level2_enable(self): - native_softmax = paddle.nn.Softmax - x = paddle.to_tensor([[1.0, 2.0, 3.0]]) - - with paddle.use_compat_guard(enable=False): - - class NativeSoftmaxSubclass(paddle.nn.Softmax): - pass - - self.assertIs(NativeSoftmaxSubclass.__mro__[1], native_softmax) - paddle.enable_compat(level=2) - self.assertCompatLevel(2) - self.assertEqual( - NativeSoftmaxSubclass(axis=-1)(x).shape, - x.shape, - ) - with self.assertRaises(TypeError): - paddle.nn.Softmax(axis=-1) - self.assertEqual(paddle.nn.Softmax(dim=-1)(x).shape, x.shape) - - self.assertCompatLevel(None) - self.assertIs(paddle.nn.Softmax, native_softmax) - self.assertNativeRestored() - - def test_guard_levels_restore_disabled_state(self): - for level in (1, 2, 3): - with self.subTest(level=level): - with paddle.use_compat_guard(level=level): - self.assertCompatLevel(level) - self.assertCompatLevel(None) - self.assertNativeRestored() - - def test_nested_guard_restores_each_outer_level(self): - for outer_level, inner_level in ((1, 2), (2, 3), (3, 1)): - with self.subTest( - outer_level=outer_level, - inner_level=inner_level, - ): - with paddle.use_compat_guard(level=outer_level): - self.assertCompatLevel(outer_level) - with paddle.use_compat_guard(level=inner_level): - self.assertCompatLevel(inner_level) - self.assertCompatLevel(outer_level) - with paddle.use_compat_guard(enable=False): - self.assertCompatLevel(None) - self.assertCompatLevel(outer_level) - self.assertCompatLevel(None) - self.assertNativeRestored() - - def test_same_level_guard_restores_body_changes(self): - paddle.enable_compat(level=2) + def test_disabled_guard_restores_level3_aliases(self): + paddle.enable_compat(level=3) try: - with paddle.use_compat_guard(level=2): - paddle.disable_compat() - self.assertCompatLevel(None) - self.assertCompatLevel(2) - finally: - paddle.disable_compat() - self.assertNativeRestored() - - def test_guard_restores_after_exception(self): - with ( - self.assertRaisesRegex(RuntimeError, "expected"), - paddle.use_compat_guard(level=2), - ): - self.assertCompatLevel(2) - raise RuntimeError("expected") - self.assertCompatLevel(None) - self.assertNativeRestored() + with paddle.use_compat_guard(enable=False): + self.assertNotIn(TORCH_PROXY_FINDER, sys.meta_path) + self.assertIs(paddle.sort, self._native[(paddle, "sort")]) - def test_guard_restores_repeated_proxy_and_scope(self): - scope = {"torch_proxy_local_enabled_module"} - paddle.enable_compat(scope=scope) - paddle.enable_compat(scope=scope, silent=True) - self.assertEqual(sys.meta_path.count(TORCH_PROXY_FINDER), 2) - try: - with paddle.use_compat_guard(level=2): - self.assertCompatLevel(2) - self.assertCompatLevel(1) - self.assertEqual(sys.meta_path.count(TORCH_PROXY_FINDER), 2) - self.assertFalse(TORCH_PROXY_FINDER._globally_enabled) - self.assertEqual(TORCH_PROXY_FINDER._local_enabled_scope, scope) + self.assertIn(TORCH_PROXY_FINDER, sys.meta_path) + self.assertAliased(paddle.sort, paddle.compat.sort) finally: paddle.disable_compat() - paddle.disable_compat() - self.assertNativeRestored() - - def test_nested_guard_extends_local_scope(self): - outer_scope = {"outer_local_module"} - inner_scope = {"inner_local_module"} - with paddle.use_compat_guard(level=1, scope=outer_scope): - self.assertFalse(TORCH_PROXY_FINDER._globally_enabled) - self.assertEqual( - TORCH_PROXY_FINDER._local_enabled_scope, - outer_scope, - ) - with paddle.use_compat_guard(scope=inner_scope): - self.assertFalse(TORCH_PROXY_FINDER._globally_enabled) - self.assertEqual( - TORCH_PROXY_FINDER._local_enabled_scope, - outer_scope | inner_scope, - ) - self.assertEqual( - TORCH_PROXY_FINDER._local_enabled_scope, - outer_scope, - ) - self.assertCompatLevel(None) - self.assertNativeRestored() - - def test_guard_contains_repeated_level2_enable(self): - with paddle.use_compat_guard(level=2): - paddle.enable_compat(level=2) - self.assertCompatLevel(2) - self.assertCompatLevel(None) self.assertNativeRestored() diff --git a/test/compat/test_torch_proxy_mixed.py b/test/compat/test_torch_proxy_mixed.py index 09bc470db17e3..b60255f9b89d5 100644 --- a/test/compat/test_torch_proxy_mixed.py +++ b/test/compat/test_torch_proxy_mixed.py @@ -17,10 +17,7 @@ import unittest import paddle -from paddle.compat.proxy import ( - ProxyModule, - _get_compat_level, -) +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")) @@ -63,22 +60,38 @@ 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() - with paddle.use_compat_guard(level=2): - self.assertEqual(_get_compat_level(), 2) + paddle.enable_compat(level=2) + try: self.check_is_not_proxy() - self.check_is_not_proxy() + import torch + from torch.nn.functional import relu - def test_level2_restores_real_torch_inside_proxy_guard(self): - self.check_is_not_proxy() - with paddle.use_compat_guard(level=1): - self.check_is_proxy() - with paddle.use_compat_guard(level=2): - self.assertEqual(_get_compat_level(), 2) - self.check_is_not_proxy() - self.check_is_proxy() + self.assertIs(torch, original_torch) + self.assertIs(relu, original_relu) + finally: + paddle.disable_compat() self.check_is_not_proxy() + def test_disabled_guard_keeps_compat_disabled(self): + with paddle.use_compat_guard( + enable=False, + scope={"torch_proxy_local_enabled_module"}, + ): + self.assertNotIn( + paddle.compat.proxy.TORCH_PROXY_FINDER, + sys.meta_path, + ) + self.assertNotIn( + paddle.compat.proxy.TORCH_PROXY_FINDER, + sys.meta_path, + ) + def test_local_enabled_module_import(self): self.check_is_not_proxy() with paddle.use_compat_guard( From 826269a123ffb4a2213aadc04bbfa9c601eb0fc6 Mon Sep 17 00:00:00 2001 From: manfredss Date: Mon, 3 Aug 2026 08:26:28 +0000 Subject: [PATCH 14/21] remove unused methods --- python/paddle/compat/proxy.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/python/paddle/compat/proxy.py b/python/paddle/compat/proxy.py index e87bed0e280e9..ef51c7d71bebf 100644 --- a/python/paddle/compat/proxy.py +++ b/python/paddle/compat/proxy.py @@ -465,25 +465,6 @@ def _parse_scope(scope: str | Iterable[str] | None) -> set[str] | None: return set(scope) -def _get_compat_level() -> int | None: - level = 0 - if TORCH_PROXY_FINDER in sys.meta_path: - level |= 1 - if _PADDLE_NAMESPACE_SAVED: - level |= 2 - return level or None - - -def _clear_compat_state() -> None: - had_torch_proxy = TORCH_PROXY_FINDER in sys.meta_path - while TORCH_PROXY_FINDER in sys.meta_path: - sys.meta_path.remove(TORCH_PROXY_FINDER) - _restore_paddle_namespace_aliases() - if had_torch_proxy: - _clear_torch_proxy_modules() - _copy_torch_modules_from_cache() - - def enable_compat( *, scope: _ScopeType = None, From a119dbe30c88d1b2b12e8c616d0caa6370763811 Mon Sep 17 00:00:00 2001 From: manfredss Date: Mon, 3 Aug 2026 10:41:12 +0000 Subject: [PATCH 15/21] fix counter; add dispatch_property --- python/paddle/compat/api_dispatch.py | 93 +++++++++++++++------------- python/paddle/compat/proxy.py | 59 +++++++++--------- 2 files changed, 81 insertions(+), 71 deletions(-) diff --git a/python/paddle/compat/api_dispatch.py b/python/paddle/compat/api_dispatch.py index 007f215e5a82a..e8a813173f32f 100644 --- a/python/paddle/compat/api_dispatch.py +++ b/python/paddle/compat/api_dispatch.py @@ -12,9 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""``enable_compat(level=2)`` API dispatch: route ``paddle.*`` (and -``paddle.Tensor`` methods) to the torch-aligned ``paddle.compat.*`` for -external callers while paddle-internal callers keep native semantics. +"""``enable_compat(level=2)`` and ``enable_compat(level=3)`` API dispatch: +route ``paddle.*`` (and ``paddle.Tensor`` methods) to the torch-aligned +``paddle.compat.*`` for external callers while paddle-internal callers keep +native semantics. The enable/disable/level lifecycle lives in ``paddle.compat.proxy``; this module only installs/removes the dispatchers and holds the dispatch state. @@ -54,9 +55,10 @@ def _caller_is_paddle_internal() -> bool: 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.""" + disabled state get the native callable. Installed under + ``enable_compat(level=2)`` or ``enable_compat(level=3)``; + ``disable_compat`` restores the originals, so the default hot path is + untouched.""" def decorator(native_fn: Any) -> Any: @wraps(native_fn) @@ -77,11 +79,12 @@ def dispatcher(*args: Any, **kwargs: Any) -> Any: def _iter_compat_modules() -> Generator[types.ModuleType, None, None]: - """Yield ``paddle.compat`` modules that declare ``__all__``. - - ``pkgutil.walk_packages`` skips the starting package, so the root - ``paddle.compat`` (holding the top-level functions) is yielded explicitly. - """ + """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 under + ``enable_compat(level=2)`` or ``enable_compat(level=3)``; + ``disable_compat`` restores the originals, so the default hot path is + untouched.""" import paddle.compat if hasattr(paddle.compat, "__all__"): @@ -123,37 +126,37 @@ def __call__(cls, *args: Any, **kwargs: Any) -> Any: return proxy -class _TensorCompatDescriptor: - """Caller-aware dispatcher for a ``paddle.Tensor`` API.""" +def dispatch_property(compat_attr: Any) -> Any: + """Route a Tensor API when either side uses the property protocol.""" - def __init__( - self, - native_attr: Any, - compat_attr: Any, - ) -> None: - self._compat_is_property = isinstance(compat_attr, property) - compat_fn = ( - compat_attr.fget if self._compat_is_property else compat_attr - ) - self.__native_fn__ = native_attr - self.__compat_fn__ = compat_fn - self.__doc__ = compat_fn.__doc__ - self.__name__ = compat_fn.__name__ - self.__signature__ = inspect.signature(compat_fn) - - def __get__(self, instance: Any, owner: type | None = None) -> Any: - if instance is None: - if _caller_is_paddle_internal(): - return self.__native_fn__ - return self if self._compat_is_property else self.__compat_fn__ - if ( - len(_PADDLE_NAMESPACE_SAVED) > 0 - and not _caller_is_paddle_internal() - ): - if self._compat_is_property: - return self.__compat_fn__(instance) - return self.__compat_fn__.__get__(instance, owner) - return self.__native_fn__.__get__(instance, owner) + def decorator(native_attr: Any) -> Any: + compat_is_property = isinstance(compat_attr, property) + compat_fn = compat_attr.fget if compat_is_property else compat_attr + + class _PropertyDispatcher: + def __get__(self, instance: Any, owner: type | None = None) -> Any: + if instance is None: + if _caller_is_paddle_internal(): + return native_attr + return self if compat_is_property else compat_fn + if ( + len(_PADDLE_NAMESPACE_SAVED) > 0 + and not _caller_is_paddle_internal() + ): + if compat_is_property: + return compat_fn(instance) + return compat_fn.__get__(instance, owner) + return native_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 + + return decorator def _patch_tensor_methods() -> None: @@ -171,10 +174,16 @@ def _patch_tensor_methods() -> None: if native_attr is None: continue _PADDLE_NAMESPACE_SAVED[(paddle.Tensor, attr_name)] = native_attr + if isinstance(compat_attr, property) or inspect.isdatadescriptor( + native_attr + ): + dispatcher = dispatch_property + else: + dispatcher = dispatch_function setattr( paddle.Tensor, attr_name, - _TensorCompatDescriptor(native_attr, compat_attr), + dispatcher(compat_attr)(native_attr), ) diff --git a/python/paddle/compat/proxy.py b/python/paddle/compat/proxy.py index ef51c7d71bebf..3db8a073ecf27 100644 --- a/python/paddle/compat/proxy.py +++ b/python/paddle/compat/proxy.py @@ -465,6 +465,16 @@ def _parse_scope(scope: str | Iterable[str] | None) -> set[str] | None: return set(scope) +def _clear_compat_state() -> None: + had_torch_proxy = TORCH_PROXY_FINDER in sys.meta_path + while TORCH_PROXY_FINDER in sys.meta_path: + sys.meta_path.remove(TORCH_PROXY_FINDER) + _restore_paddle_namespace_aliases() + if had_torch_proxy: + _clear_torch_proxy_modules() + _copy_torch_modules_from_cache() + + def enable_compat( *, scope: _ScopeType = None, @@ -609,18 +619,19 @@ def use_compat_guard( ... assert torch.sin is paddle.sin """ scope = _parse_scope(scope) - already_has_torch_proxy = TORCH_PROXY_FINDER in sys.meta_path + original_proxy_count = sys.meta_path.count(TORCH_PROXY_FINDER) has_paddle_aliases = bool(_PADDLE_NAMESPACE_SAVED) original_local_enabled_scope = set(TORCH_PROXY_FINDER._local_enabled_scope) original_globally_enabled = TORCH_PROXY_FINDER._globally_enabled - already_has_compat = already_has_torch_proxy or has_paddle_aliases + already_has_compat = original_proxy_count > 0 or has_paddle_aliases + if not enable and not already_has_compat: yield return if enable and ( - (has_paddle_aliases and not already_has_torch_proxy and scope is None) + (has_paddle_aliases and original_proxy_count == 0 and scope is None) or ( - already_has_torch_proxy + original_proxy_count > 0 and ( (original_globally_enabled and scope is None) or original_local_enabled_scope == (scope or set()) @@ -629,39 +640,29 @@ def use_compat_guard( ): yield return + if enable: enable_compat( scope=scope, silent=silent, level=3 if has_paddle_aliases else 1, ) - try: - yield - finally: - TORCH_PROXY_FINDER._local_enabled_scope = ( - original_local_enabled_scope - ) - TORCH_PROXY_FINDER._globally_enabled = original_globally_enabled - disable_compat() - if has_paddle_aliases: - _apply_paddle_namespace_aliases() else: - disable_compat() - try: - yield - finally: - level = ( - 3 - if already_has_torch_proxy and has_paddle_aliases - else 2 - if has_paddle_aliases - else 1 - ) + _clear_compat_state() + try: + yield + finally: + _clear_compat_state() + if original_proxy_count or has_paddle_aliases: + if original_proxy_count and has_paddle_aliases: + level = 3 + else: + level = 2 if has_paddle_aliases else 1 enable_compat(scope=None, silent=True, level=level) - TORCH_PROXY_FINDER._local_enabled_scope = ( - original_local_enabled_scope - ) - TORCH_PROXY_FINDER._globally_enabled = original_globally_enabled + for _ in range(1, original_proxy_count): + enable_compat(scope=None, silent=True, level=1) + TORCH_PROXY_FINDER._local_enabled_scope = original_local_enabled_scope + TORCH_PROXY_FINDER._globally_enabled = original_globally_enabled def extend_torch_proxy_blocked_modules(modules: Iterable[str]) -> None: From 5a5b7b3d2b3051b51b93bce92b92ec07a13f4e75 Mon Sep 17 00:00:00 2001 From: manfredss Date: Mon, 3 Aug 2026 12:00:46 +0000 Subject: [PATCH 16/21] fix --- python/paddle/compat/__init__.py | 38 ++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/python/paddle/compat/__init__.py b/python/paddle/compat/__init__.py index 159e6fe71e9ad..730f8ff5d3d4e 100644 --- a/python/paddle/compat/__init__.py +++ b/python/paddle/compat/__init__.py @@ -107,20 +107,21 @@ def _tensor_type( **kwargs: Any, ) -> str | Tensor: """ - Returns the tensor type when ``dtype`` is not specified, otherwise casts + Returns the tensor dtype 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. When it is ``None``, returns a PyTorch-style tensor type - string. Default: ``None``. + data type. Qualified ``torch.*`` and ``paddle.*`` dtype or tensor + type strings are supported. When it is ``None``, returns a Paddle + dtype string. Default: ``None``. non_blocking (bool, optional): Whether the conversion may occur asynchronously. Default: ``False``. Returns: - str|Tensor: A tensor type string when ``dtype`` is ``None``; otherwise, - a tensor with the requested type. + str|Tensor: A Paddle dtype string when ``dtype`` is ``None``; + otherwise, a tensor with the requested type. """ if "async" in kwargs: non_blocking = kwargs.pop("async") @@ -129,12 +130,7 @@ def _tensor_type( raise TypeError(f"type() got an unexpected keyword argument {key!r}") if dtype is None: - dtype_name = str(input.dtype).removeprefix("paddle.") - tensor_type = _TENSOR_TYPE_NAMES[dtype_name] - prefix = "torch.cuda" if input.place.is_gpu_place() else "torch" - if input.is_sparse_coo(): - prefix += ".sparse" - return f"{prefix}.{tensor_type}" + return str(input.dtype) device = None if isinstance(dtype, type) and dtype.__name__ in _TENSOR_TYPE_DTYPES: @@ -144,13 +140,21 @@ def _tensor_type( elif isinstance(dtype, str): dtype_string = dtype tensor_type = dtype_string.rsplit(".", 1)[-1] - if ( - not dtype_string.startswith("torch.") - or tensor_type not in _TENSOR_TYPE_DTYPES - ): + 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] + device = ( + "gpu" + if dtype_string.startswith(("torch.cuda.", "paddle.cuda.")) + else "cpu" + ) + elif tensor_type in _TENSOR_TYPE_NAMES: + dtype = tensor_type + if dtype_string.startswith(("torch.cuda.", "paddle.cuda.")): + device = "gpu" + else: raise ValueError(f"invalid type: {dtype_string!r}") - dtype = _TENSOR_TYPE_DTYPES[tensor_type] - device = "gpu" if dtype_string.startswith("torch.cuda.") else "cpu" dtype_name = str(input.dtype).removeprefix("paddle.") target_dtype_name = str(dtype).removeprefix("paddle.") From 219307cf468efe58a068870ec2eae18df94de7fa Mon Sep 17 00:00:00 2001 From: manfredss Date: Mon, 3 Aug 2026 12:59:27 +0000 Subject: [PATCH 17/21] fix tests --- test/compat/test_compat_namespace_aliased.py | 37 +++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/test/compat/test_compat_namespace_aliased.py b/test/compat/test_compat_namespace_aliased.py index ac35714b564d7..f1c1654e1d5e5 100644 --- a/test/compat/test_compat_namespace_aliased.py +++ b/test/compat/test_compat_namespace_aliased.py @@ -609,24 +609,31 @@ def test_tensor_methods_caller_aware(self): self.assertIs(type(t.numel()), int) self.assertIsInstance(paddle.numel(t), paddle.Tensor) self.assertEqual(paddle.empty([0, 3]).numel(), 0) - self.assertEqual(t.type(), "torch.FloatTensor") + self.assertEqual(t.type(), "paddle.float32") 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").type(), "torch.LongTensor" + paddle.ones([1], dtype="int64").type(), "paddle.int64" ) self.assertEqual( paddle.ones([1], dtype="float8_e4m3fn").type(), - "torch.Float8_e4m3fnTensor", + "paddle.float8_e4m3fn", ) self.assertEqual( paddle.ones([1], dtype="float8_e5m2").type(), - "torch.Float8_e5m2Tensor", + "paddle.float8_e5m2", ) self.assertIs(t.is_sparse, False) coo = paddle.sparse.sparse_coo_tensor([[0], [1]], [1.0], [2, 2]) @@ -648,11 +655,18 @@ def test_tensor_methods_caller_aware(self): 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.assertIsInstance(cached_numel(), paddle.Tensor) + self.assertIsInstance(cached_max(axis=1), paddle.Tensor) @with_level2 def test_tensor_type_edge_cases(self): @@ -674,9 +688,22 @@ def test_tensor_type_edge_cases(self): 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]) - self.assertEqual(coo.type(), "torch.sparse.FloatTensor") + self.assertEqual(coo.type(), "paddle.float32") self.assertEqual(t.type(np.float64).dtype, paddle.float64) @with_level2 From 82f8ebf1a8832526b423944ecf9bf26cf72cac70 Mon Sep 17 00:00:00 2001 From: manfredss Date: Thu, 6 Aug 2026 07:52:36 +0000 Subject: [PATCH 18/21] fix --- python/paddle/compat/__init__.py | 34 +++-- python/paddle/compat/api_dispatch.py | 129 +++++++++---------- python/paddle/compat/proxy.py | 78 +++++------ python/paddle/utils/decorator_utils.py | 32 +++-- test/compat/test_compat_namespace_aliased.py | 85 ++++++------ test/compat/test_torch_proxy_mixed.py | 14 -- 6 files changed, 164 insertions(+), 208 deletions(-) diff --git a/python/paddle/compat/__init__.py b/python/paddle/compat/__init__.py index 730f8ff5d3d4e..481d862aacf55 100644 --- a/python/paddle/compat/__init__.py +++ b/python/paddle/compat/__init__.py @@ -56,24 +56,22 @@ 'seed', ] -_TENSOR_TYPE_NAMES = { - 'float16': 'HalfTensor', - 'float32': 'FloatTensor', - 'float64': 'DoubleTensor', - 'float8_e4m3fn': 'Float8_e4m3fnTensor', - 'float8_e5m2': 'Float8_e5m2Tensor', - 'bfloat16': 'BFloat16Tensor', - 'uint8': 'ByteTensor', - 'int8': 'CharTensor', - 'int16': 'ShortTensor', - 'int32': 'IntTensor', - 'int64': 'LongTensor', - 'bool': 'BoolTensor', - 'complex64': 'ComplexFloatTensor', - 'complex128': 'ComplexDoubleTensor', -} + _TENSOR_TYPE_DTYPES = { - tensor_type: dtype for dtype, tensor_type in _TENSOR_TYPE_NAMES.items() + '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', } @@ -149,7 +147,7 @@ def _tensor_type( if dtype_string.startswith(("torch.cuda.", "paddle.cuda.")) else "cpu" ) - elif tensor_type in _TENSOR_TYPE_NAMES: + elif tensor_type in _TENSOR_TYPE_DTYPES.values(): dtype = tensor_type if dtype_string.startswith(("torch.cuda.", "paddle.cuda.")): device = "gpu" diff --git a/python/paddle/compat/api_dispatch.py b/python/paddle/compat/api_dispatch.py index e8a813173f32f..d2ba2e000d35c 100644 --- a/python/paddle/compat/api_dispatch.py +++ b/python/paddle/compat/api_dispatch.py @@ -12,10 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""``enable_compat(level=2)`` and ``enable_compat(level=3)`` API dispatch: -route ``paddle.*`` (and ``paddle.Tensor`` methods) to the torch-aligned -``paddle.compat.*`` for external callers while paddle-internal callers keep -native semantics. +"""``enable_compat(level=2)`` API dispatch: route ``paddle.*`` (and +``paddle.Tensor`` methods) to the torch-aligned ``paddle.compat.*`` for +external callers while paddle-internal callers keep native semantics. The enable/disable/level lifecycle lives in ``paddle.compat.proxy``; this module only installs/removes the dispatchers and holds the dispatch state. @@ -52,39 +51,27 @@ 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 under - ``enable_compat(level=2)`` or ``enable_compat(level=3)``; - ``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]: - """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 under - ``enable_compat(level=2)`` or ``enable_compat(level=3)``; - ``disable_compat`` restores the originals, so the default hot path is - untouched.""" + """Yield ``paddle.compat`` modules that declare ``__all__``. + + ``pkgutil.walk_packages`` skips the starting package, so the root + ``paddle.compat`` (holding the top-level functions) is yielded explicitly. + """ import paddle.compat if hasattr(paddle.compat, "__all__"): @@ -126,37 +113,30 @@ def __call__(cls, *args: Any, **kwargs: Any) -> Any: return proxy -def dispatch_property(compat_attr: Any) -> Any: +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 + ) - def decorator(native_attr: Any) -> Any: - compat_is_property = isinstance(compat_attr, property) - compat_fn = compat_attr.fget if compat_is_property else compat_attr - - class _PropertyDispatcher: - def __get__(self, instance: Any, owner: type | None = None) -> Any: - if instance is None: - if _caller_is_paddle_internal(): - return native_attr - return self if compat_is_property else compat_fn - if ( - len(_PADDLE_NAMESPACE_SAVED) > 0 - and not _caller_is_paddle_internal() - ): - if compat_is_property: - return compat_fn(instance) - return compat_fn.__get__(instance, owner) - return native_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 + 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) - return decorator + 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: @@ -174,17 +154,24 @@ def _patch_tensor_methods() -> None: if native_attr is None: continue _PADDLE_NAMESPACE_SAVED[(paddle.Tensor, attr_name)] = native_attr - if isinstance(compat_attr, property) or inspect.isdatadescriptor( - native_attr - ): - dispatcher = dispatch_property + native_is_property = inspect.isdatadescriptor(native_attr) + compat_is_property = isinstance(compat_attr, property) + # Select once for all four descriptor combinations. The installed + # dispatcher only needs to distinguish the caller at runtime. + if compat_is_property: + if native_is_property: + # native property -> compat property + dispatcher = dispatch_property(native_attr, compat_attr) + else: + # native function -> compat property + dispatcher = dispatch_property(native_attr, compat_attr) + elif native_is_property: + # native property -> compat function + dispatcher = dispatch_property(native_attr, compat_attr) else: - dispatcher = dispatch_function - setattr( - paddle.Tensor, - attr_name, - dispatcher(compat_attr)(native_attr), - ) + # native function -> compat function + dispatcher = dispatch_function(native_attr, compat_attr) + setattr(paddle.Tensor, attr_name, dispatcher) def _apply_paddle_namespace_aliases() -> None: @@ -217,7 +204,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/proxy.py b/python/paddle/compat/proxy.py index 3db8a073ecf27..6cd475ed0e72b 100644 --- a/python/paddle/compat/proxy.py +++ b/python/paddle/compat/proxy.py @@ -465,14 +465,14 @@ def _parse_scope(scope: str | Iterable[str] | None) -> set[str] | None: return set(scope) -def _clear_compat_state() -> None: - had_torch_proxy = TORCH_PROXY_FINDER in sys.meta_path - while TORCH_PROXY_FINDER in sys.meta_path: - sys.meta_path.remove(TORCH_PROXY_FINDER) - _restore_paddle_namespace_aliases() - if had_torch_proxy: - _clear_torch_proxy_modules() - _copy_torch_modules_from_cache() +def _current_compat_level() -> int: + """0 = disabled, 1 = torch proxy only, 2 = paddle aliases only, 3 = both.""" + level = 0 + if TORCH_PROXY_FINDER in sys.meta_path: + level |= 1 + if _PADDLE_NAMESPACE_SAVED: + level |= 2 + return level def enable_compat( @@ -619,50 +619,40 @@ def use_compat_guard( ... assert torch.sin is paddle.sin """ scope = _parse_scope(scope) - original_proxy_count = sys.meta_path.count(TORCH_PROXY_FINDER) - has_paddle_aliases = bool(_PADDLE_NAMESPACE_SAVED) original_local_enabled_scope = set(TORCH_PROXY_FINDER._local_enabled_scope) original_globally_enabled = TORCH_PROXY_FINDER._globally_enabled - already_has_compat = original_proxy_count > 0 or has_paddle_aliases + original_level = _current_compat_level() - if not enable and not already_has_compat: - yield - return - if enable and ( - (has_paddle_aliases and original_proxy_count == 0 and scope is None) - or ( - original_proxy_count > 0 - and ( - (original_globally_enabled and scope is None) - or original_local_enabled_scope == (scope or set()) - ) - ) + if enable == bool(original_level) and ( + (original_globally_enabled and scope is None) + or (original_local_enabled_scope == (scope or set())) ): yield return - if enable: - enable_compat( - scope=scope, - silent=silent, - level=3 if has_paddle_aliases else 1, - ) + enable_compat(scope=scope, silent=silent) + try: + yield + finally: + disable_compat() + if original_level: + enable_compat(scope=None, silent=True, level=original_level) + TORCH_PROXY_FINDER._local_enabled_scope = ( + original_local_enabled_scope + ) + TORCH_PROXY_FINDER._globally_enabled = original_globally_enabled else: - _clear_compat_state() - try: - yield - finally: - _clear_compat_state() - if original_proxy_count or has_paddle_aliases: - if original_proxy_count and has_paddle_aliases: - level = 3 - else: - level = 2 if has_paddle_aliases else 1 - enable_compat(scope=None, silent=True, level=level) - for _ in range(1, original_proxy_count): - enable_compat(scope=None, silent=True, level=1) - TORCH_PROXY_FINDER._local_enabled_scope = original_local_enabled_scope - TORCH_PROXY_FINDER._globally_enabled = original_globally_enabled + if original_level: + disable_compat() + try: + yield + finally: + if original_level: + enable_compat(scope=None, silent=True, level=original_level) + TORCH_PROXY_FINDER._local_enabled_scope = ( + original_local_enabled_scope + ) + TORCH_PROXY_FINDER._globally_enabled = original_globally_enabled def extend_torch_proxy_blocked_modules(modules: Iterable[str]) -> None: diff --git a/python/paddle/utils/decorator_utils.py b/python/paddle/utils/decorator_utils.py index d412904ca902b..5b906ccead324 100644 --- a/python/paddle/utils/decorator_utils.py +++ b/python/paddle/utils/decorator_utils.py @@ -252,7 +252,6 @@ def prelu_decorator( @functools.wraps(func) def wrapper(*args: _InputT.args, **kwargs: _InputT.kwargs) -> _RetT: if 4 <= len(args) <= 5: - third_arg = args[3] device_types = { "cpu", "cuda", @@ -262,19 +261,26 @@ def wrapper(*args: _InputT.args, **kwargs: _InputT.kwargs) -> _RetT: "ipu", *(paddle.device.get_all_custom_device_type() or ()), } - is_torch_call = ( - isinstance(third_arg, paddle.base.libpaddle.Place) - or ( - isinstance(third_arg, str) - and third_arg.lower().split(":", 1)[0] in device_types - ) - or ( - third_arg is None - and len(args) == 5 - and not isinstance(args[4], str) - ) + data_formats = { + "NC", + "NCL", + "NCHW", + "NCDHW", + "NLC", + "NHWC", + "NDHWC", + } + is_paddle_place = isinstance(args[3], paddle.base.libpaddle.Place) + is_device = ( + isinstance(args[3], str) + and args[3].lower().split(":", 1)[0] in device_types ) - if is_torch_call: + is_dtype = ( + args[3] is None + and len(args) == 5 + and args[4] not in data_formats + ) + if is_paddle_place or is_device or is_dtype: for name, value in zip(("device", "dtype"), args[3:]): if name in kwargs: raise TypeError( diff --git a/test/compat/test_compat_namespace_aliased.py b/test/compat/test_compat_namespace_aliased.py index f1c1654e1d5e5..477e0ab3add90 100644 --- a/test/compat/test_compat_namespace_aliased.py +++ b/test/compat/test_compat_namespace_aliased.py @@ -451,49 +451,6 @@ 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.assertNotIn(TORCH_PROXY_FINDER, sys.meta_path) - 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() - - def test_disabled_guard_restores_level2_aliases(self): - t = paddle.to_tensor([[3.0, 1.0, 2.0]]) - paddle.enable_compat(level=2) - try: - with paddle.use_compat_guard(enable=False): - self.assertNotIn(TORCH_PROXY_FINDER, sys.meta_path) - self.assertIs(paddle.sort, self._native[(paddle, "sort")]) - - self.assertNotIn(TORCH_PROXY_FINDER, sys.meta_path) - self.assertAliased(paddle.sort, paddle.compat.sort) - self.assertTrue(hasattr(paddle.sort(t, dim=-1), "values")) - finally: - paddle.disable_compat() - self.assertNativeRestored() - - def test_disabled_guard_restores_level3_aliases(self): - paddle.enable_compat(level=3) - try: - with paddle.use_compat_guard(enable=False): - self.assertNotIn(TORCH_PROXY_FINDER, sys.meta_path) - self.assertIs(paddle.sort, self._native[(paddle, "sort")]) - - self.assertIn(TORCH_PROXY_FINDER, sys.meta_path) - self.assertAliased(paddle.sort, paddle.compat.sort) - finally: - paddle.disable_compat() - self.assertNativeRestored() - class TestTorchSurfaceUnderCompat(CompatNamespaceAliasBase): """torch.* reaches public compat APIs only at proxy-enabled levels.""" @@ -587,7 +544,7 @@ 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 @@ -665,8 +622,8 @@ def test_tensor_methods_caller_aware(self): self.assertIs(paddle.Tensor.numel, native_numel) self.assertIs(paddle.Tensor.type, native_type) self.assertIs(paddle.Tensor.is_sparse, native_is_sparse) - self.assertIsInstance(cached_numel(), paddle.Tensor) - self.assertIsInstance(cached_max(axis=1), paddle.Tensor) + self.assertEqual(cached_numel(), 6) + self.assertTrue(hasattr(cached_max(dim=1), "values")) @with_level2 def test_tensor_type_edge_cases(self): @@ -698,7 +655,7 @@ def test_tensor_type_edge_cases(self): tensor_to.assert_called_once_with( t, device="gpu", - dtype="float64", + dtype='float64', blocking=True, ) @@ -712,7 +669,11 @@ def test_tensor_descriptor_class_access(self): sparse_descriptor = inspect.getattr_static(paddle.Tensor, "is_sparse") self.assertIs(paddle.Tensor.type, type_descriptor.__compat_fn__) - self.assertIs(paddle.Tensor.is_sparse, sparse_descriptor) + 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( @@ -723,6 +684,34 @@ def test_tensor_descriptor_class_access(self): 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( diff --git a/test/compat/test_torch_proxy_mixed.py b/test/compat/test_torch_proxy_mixed.py index b60255f9b89d5..63b4f26df3f3a 100644 --- a/test/compat/test_torch_proxy_mixed.py +++ b/test/compat/test_torch_proxy_mixed.py @@ -78,20 +78,6 @@ def test_level2_does_not_proxy_torch(self): paddle.disable_compat() self.check_is_not_proxy() - def test_disabled_guard_keeps_compat_disabled(self): - with paddle.use_compat_guard( - enable=False, - scope={"torch_proxy_local_enabled_module"}, - ): - self.assertNotIn( - paddle.compat.proxy.TORCH_PROXY_FINDER, - sys.meta_path, - ) - self.assertNotIn( - paddle.compat.proxy.TORCH_PROXY_FINDER, - sys.meta_path, - ) - def test_local_enabled_module_import(self): self.check_is_not_proxy() with paddle.use_compat_guard( From 3eeb4013f92afdcdc9125654a449dc0d3547843f Mon Sep 17 00:00:00 2001 From: manfredss Date: Thu, 6 Aug 2026 14:10:53 +0000 Subject: [PATCH 19/21] improve code per review suggestions --- python/paddle/compat/__init__.py | 15 +++++++++++++ python/paddle/compat/api_dispatch.py | 21 +++++------------- python/paddle/compat/proxy.py | 32 ++++++++++++++++++++-------- 3 files changed, 43 insertions(+), 25 deletions(-) diff --git a/python/paddle/compat/__init__.py b/python/paddle/compat/__init__.py index 481d862aacf55..43369e97dc4a9 100644 --- a/python/paddle/compat/__init__.py +++ b/python/paddle/compat/__init__.py @@ -57,6 +57,21 @@ ] +# root compat APIs that torch also exposes as Tensor methods +_TENSOR_API_NAMES = ( + 'allclose', + 'equal', + 'slogdet', + 'sort', + 'split', + 'min', + 'max', + 'unique', + 'median', + 'nanmedian', +) + + _TENSOR_TYPE_DTYPES = { 'HalfTensor': 'float16', 'FloatTensor': 'float32', diff --git a/python/paddle/compat/api_dispatch.py b/python/paddle/compat/api_dispatch.py index d2ba2e000d35c..e497da95d65da 100644 --- a/python/paddle/compat/api_dispatch.py +++ b/python/paddle/compat/api_dispatch.py @@ -145,8 +145,8 @@ def _patch_tensor_methods() -> None: import paddle.compat as compat_root tensor_apis = { - attr_name: getattr(compat_root, attr_name) - for attr_name in getattr(compat_root, "__all__", ()) + name: getattr(compat_root, name) + for name in compat_root._TENSOR_API_NAMES } tensor_apis.update(compat_root._TENSOR_API_OVERRIDES) for attr_name, compat_attr in tensor_apis.items(): @@ -154,22 +154,11 @@ def _patch_tensor_methods() -> None: if native_attr is None: continue _PADDLE_NAMESPACE_SAVED[(paddle.Tensor, attr_name)] = native_attr - native_is_property = inspect.isdatadescriptor(native_attr) - compat_is_property = isinstance(compat_attr, property) - # Select once for all four descriptor combinations. The installed - # dispatcher only needs to distinguish the caller at runtime. - if compat_is_property: - if native_is_property: - # native property -> compat property - dispatcher = dispatch_property(native_attr, compat_attr) - else: - # native function -> compat property - dispatcher = dispatch_property(native_attr, compat_attr) - elif native_is_property: - # native property -> compat function + if inspect.isdatadescriptor(native_attr) or isinstance( + compat_attr, property + ): dispatcher = dispatch_property(native_attr, compat_attr) else: - # native function -> compat function dispatcher = dispatch_function(native_attr, compat_attr) setattr(paddle.Tensor, attr_name, dispatcher) diff --git a/python/paddle/compat/proxy.py b/python/paddle/compat/proxy.py index 6cd475ed0e72b..efec66da8e040 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 @@ -465,13 +466,21 @@ def _parse_scope(scope: str | Iterable[str] | None) -> set[str] | None: return set(scope) -def _current_compat_level() -> int: - """0 = disabled, 1 = torch proxy only, 2 = paddle aliases only, 3 = both.""" - level = 0 +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 |= 1 + level |= _CompatLevel.MODULE_PROXY if _PADDLE_NAMESPACE_SAVED: - level |= 2 + level |= _CompatLevel.API_ALIAS return level @@ -530,7 +539,8 @@ def enable_compat( f"Unsupported level: {level}. It should be 1, 2, or 3." ) - if level in {1, 3}: + 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) @@ -539,7 +549,7 @@ def enable_compat( _swap_torch_modules_to_cache() _modify_scope_of_torch_proxy(scope, silent=silent) sys.meta_path.insert(0, TORCH_PROXY_FINDER) - if level == 3: + if _CompatLevel.API_ALIAS in compat_level: _apply_paddle_namespace_aliases() else: _apply_paddle_namespace_aliases() @@ -636,7 +646,9 @@ def use_compat_guard( finally: disable_compat() if original_level: - enable_compat(scope=None, silent=True, level=original_level) + enable_compat( + scope=None, silent=True, level=original_level.value + ) TORCH_PROXY_FINDER._local_enabled_scope = ( original_local_enabled_scope ) @@ -648,7 +660,9 @@ def use_compat_guard( yield finally: if original_level: - enable_compat(scope=None, silent=True, level=original_level) + enable_compat( + scope=None, silent=True, level=original_level.value + ) TORCH_PROXY_FINDER._local_enabled_scope = ( original_local_enabled_scope ) From 43c95f13b2bc65c9e7014a87fbce243611c6d59b Mon Sep 17 00:00:00 2001 From: manfredss Date: Fri, 7 Aug 2026 07:04:37 +0000 Subject: [PATCH 20/21] fix --- python/paddle/compat/__init__.py | 48 ++++++++++++---------------- python/paddle/compat/api_dispatch.py | 7 +--- 2 files changed, 22 insertions(+), 33 deletions(-) diff --git a/python/paddle/compat/__init__.py b/python/paddle/compat/__init__.py index 43369e97dc4a9..2725e600bb58c 100644 --- a/python/paddle/compat/__init__.py +++ b/python/paddle/compat/__init__.py @@ -57,21 +57,6 @@ ] -# root compat APIs that torch also exposes as Tensor methods -_TENSOR_API_NAMES = ( - 'allclose', - 'equal', - 'slogdet', - 'sort', - 'split', - 'min', - 'max', - 'unique', - 'median', - 'nanmedian', -) - - _TENSOR_TYPE_DTYPES = { 'HalfTensor': 'float16', 'FloatTensor': 'float32', @@ -146,9 +131,9 @@ def _tensor_type( return str(input.dtype) device = None - if isinstance(dtype, type) and dtype.__name__ in _TENSOR_TYPE_DTYPES: - tensor_type = dtype.__name__ - dtype = _TENSOR_TYPE_DTYPES[tensor_type] + 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 @@ -164,8 +149,6 @@ def _tensor_type( ) elif tensor_type in _TENSOR_TYPE_DTYPES.values(): dtype = tensor_type - if dtype_string.startswith(("torch.cuda.", "paddle.cuda.")): - device = "gpu" else: raise ValueError(f"invalid type: {dtype_string!r}") @@ -200,13 +183,6 @@ def _tensor_is_sparse(input: Tensor) -> bool: return input.is_sparse_coo() -_TENSOR_API_OVERRIDES = { - 'numel': _tensor_numel, - 'type': _tensor_type, - 'is_sparse': _tensor_is_sparse, -} - - def allclose( input: Tensor, other: Tensor, @@ -1273,3 +1249,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 e497da95d65da..08651ba70bb8c 100644 --- a/python/paddle/compat/api_dispatch.py +++ b/python/paddle/compat/api_dispatch.py @@ -144,12 +144,7 @@ def _patch_tensor_methods() -> None: import paddle import paddle.compat as compat_root - tensor_apis = { - name: getattr(compat_root, name) - for name in compat_root._TENSOR_API_NAMES - } - tensor_apis.update(compat_root._TENSOR_API_OVERRIDES) - for attr_name, compat_attr in tensor_apis.items(): + 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 From 8c035b1a3f31506b6d16859bc1824b40b8c2f0ac Mon Sep 17 00:00:00 2001 From: manfredss Date: Fri, 7 Aug 2026 09:22:32 +0000 Subject: [PATCH 21/21] refine --- python/paddle/utils/decorator_utils.py | 14 +++++++------- test/legacy_test/test_api_compatibility_part2.py | 4 ++++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/python/paddle/utils/decorator_utils.py b/python/paddle/utils/decorator_utils.py index 5b906ccead324..3287dcd1d624f 100644 --- a/python/paddle/utils/decorator_utils.py +++ b/python/paddle/utils/decorator_utils.py @@ -270,17 +270,17 @@ def wrapper(*args: _InputT.args, **kwargs: _InputT.kwargs) -> _RetT: "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 = ( + is_device = args[3] is None or ( isinstance(args[3], str) and args[3].lower().split(":", 1)[0] in device_types ) - is_dtype = ( - args[3] is None - and len(args) == 5 - and args[4] not in data_formats - ) - if is_paddle_place or is_device or is_dtype: + 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( diff --git a/test/legacy_test/test_api_compatibility_part2.py b/test/legacy_test/test_api_compatibility_part2.py index ead7d46cbc021..8c1813c512124 100644 --- a/test/legacy_test/test_api_compatibility_part2.py +++ b/test/legacy_test/test_api_compatibility_part2.py @@ -3186,6 +3186,10 @@ def test_dygraph_Compatibility(self): 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)