From e68e20652b7a74da1b53f05c1f106c2e9677b9b1 Mon Sep 17 00:00:00 2001 From: manfredss Date: Fri, 17 Jul 2026 10:49:55 +0000 Subject: [PATCH 1/9] new compat mechnism --- python/paddle/compat/api_dispatch.py | 286 +++++++++ python/paddle/compat/nn/__init__.py | 16 +- python/paddle/compat/proxy.py | 23 + .../torch_proxy_root_api_module.py | 22 + .../test_compat_level2_internal_composites.py | 135 +++++ test/compat/test_compat_namespace_aliased.py | 573 ++++++++++++++++++ 6 files changed, 1054 insertions(+), 1 deletion(-) create mode 100644 python/paddle/compat/api_dispatch.py create mode 100644 test/compat/fake_modules/torch_proxy_root_api_module.py create mode 100644 test/compat/test_compat_level2_internal_composites.py create mode 100644 test/compat/test_compat_namespace_aliased.py diff --git a/python/paddle/compat/api_dispatch.py b/python/paddle/compat/api_dispatch.py new file mode 100644 index 0000000000000..704d3e5526a84 --- /dev/null +++ b/python/paddle/compat/api_dispatch.py @@ -0,0 +1,286 @@ +# 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. + +"""``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. +""" + +from __future__ import annotations + +import contextvars +import importlib +import inspect +import pkgutil +import sys +from functools import wraps +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + import types + from collections.abc import Generator + + +class _MissingType: + """Sentinel marking a paddle attribute that did not exist before aliasing.""" + + +_MISSING = _MissingType() + +# (live module, attr name) -> original value (or ``_MISSING``) for every +# ``paddle.*`` symbol aliased to its ``paddle.compat.*`` counterpart, so the +# paddle namespace can be restored exactly on ``disable_compat()``. +_PADDLE_NAMESPACE_SAVED: dict[tuple[types.ModuleType, str], Any] = {} + +# Dispatch state: _COMPAT_ENABLED is the process-wide on/off; _COMPAT_SUSPENDED is +# a per-thread/async-task override so a running compat API uses native internals. +_COMPAT_ENABLED = False +_COMPAT_SUSPENDED: contextvars.ContextVar[bool] = contextvars.ContextVar( + "paddle_compat_suspended", default=False +) + + +def is_compat_api_enabled() -> bool: + """Whether ``paddle.*`` currently dispatches to the torch-aligned compat APIs + in this thread / async task.""" + return _COMPAT_ENABLED and not _COMPAT_SUSPENDED.get() + + +def _is_paddle_namespace_aliased() -> bool: + """Whether level-2 aliases are currently installed process-wide.""" + return bool(_PADDLE_NAMESPACE_SAVED) + + +class compat_api_guard: + """Suspend/restore compat dispatch for the current thread / async task. + The level-2 dispatcher suspends aliases while a compat implementation runs, + so its internal ``paddle.*`` calls hit native. Context manager and decorator; + the decorator preserves ``__signature__``.""" + + def __init__(self, enable: bool = True) -> None: + self._enable = enable + self._token = None + + def __enter__(self) -> None: + self._token = _COMPAT_SUSPENDED.set(not self._enable) + + def __exit__(self, *exc: object) -> bool: + _COMPAT_SUSPENDED.reset(self._token) + self._token = None + return False + + def __call__(self, func: Any) -> Any: + enable = self._enable + + @wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> Any: + # A fresh guard per call keeps re-entrant / concurrent use correct. + with compat_api_guard(enable): + return func(*args, **kwargs) + + # Every decorated callable is a pure-Python compat API, so its + # signature is always introspectable. + wrapper.__signature__ = inspect.signature(func) + return wrapper + + +def _caller_is_paddle_internal() -> bool: + """True when the caller of the dispatched ``paddle.*`` API is a ``paddle`` + module. Paddle's own internals call these APIs with native kwargs/defaults, so + they (and the compat impls) get native; only external callers see compat.""" + # _getframe(1) = our caller (the dispatch site); its f_back = who called paddle.X. + frame = sys._getframe(1).f_back + if frame is None: + return False + name = frame.f_globals.get("__name__") or "" + return name == "paddle" or name.startswith("paddle.") + + +def dispatch_compat_api(compat_fn: Any) -> Any: + """Wrap a native ``paddle`` callable to route external callers to + ``compat_fn`` while compat is enabled; paddle-internal callers and the + disabled state get the native callable. Installed only under + ``enable_compat(level=2)``; ``disable_compat`` restores the originals, + so the default hot path is untouched.""" + + def decorator(native_fn: Any) -> Any: + getframe = sys._getframe + suspended = _COMPAT_SUSPENDED.get + + @wraps(native_fn) + def dispatcher(*args: Any, **kwargs: Any) -> Any: + if _COMPAT_ENABLED and not suspended(): + caller = getframe().f_back + name = ( + "" if caller is None else caller.f_globals.get("__name__") + ) or "" + if name != "paddle" and not name.startswith("paddle."): + with compat_api_guard(enable=False): + return compat_fn(*args, **kwargs) + return native_fn(*args, **kwargs) + + dispatcher.__compat_fn__ = compat_fn + dispatcher.__native_fn__ = native_fn + dispatcher.__signature__ = inspect.signature(compat_fn) + return dispatcher + + return decorator + + +def _iter_compat_modules() -> Generator[types.ModuleType, None, None]: + """Yield every ``paddle.compat`` (sub)module, root package included. + + ``pkgutil.walk_packages`` skips the starting package, so the root + ``paddle.compat`` (holding the top-level functions) is yielded explicitly. + """ + import paddle.compat + + yield paddle.compat + for module_info in pkgutil.walk_packages( + paddle.compat.__path__, + paddle.compat.__name__ + ".", + ): + yield importlib.import_module(module_info.name) + + +def _make_caller_aware_class_proxy(native_cls: type, compat_cls: type) -> type: + """Caller-aware ``paddle.X`` class: instantiates ``compat_cls`` for external + callers, ``native_cls`` for paddle-internal ones; ``isinstance``/``issubclass`` + accept either. Subclasses ``compat_cls`` so a user subclass derived under + level=2 keeps the torch-style (compat) constructor/methods.""" + + class _CompatAwareMeta(type(compat_cls)): + def __call__(cls, *args: Any, **kwargs: Any) -> Any: + if cls is proxy: + if is_compat_api_enabled() and not _caller_is_paddle_internal(): + return compat_cls(*args, **kwargs) + return native_cls(*args, **kwargs) + return super().__call__(*args, **kwargs) + + def __instancecheck__(cls, instance: Any) -> bool: + return isinstance(instance, (native_cls, compat_cls)) + + def __subclasscheck__(cls, subclass: Any) -> bool: + return issubclass(subclass, native_cls) or issubclass( + subclass, compat_cls + ) + + proxy = _CompatAwareMeta( + native_cls.__name__, + (compat_cls,), + { + "__module__": native_cls.__module__, + "__compat_cls__": compat_cls, + "__native_cls__": native_cls, + }, + ) + proxy.__signature__ = inspect.signature(compat_cls) + return proxy + + +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. + """ + import paddle + import paddle.compat as compat_root + + for attr_name in getattr(compat_root, "__all__", ()): + if attr_name.startswith("_"): + continue + native_method = getattr(paddle.Tensor, attr_name, _MISSING) + if native_method is _MISSING or not callable(native_method): + continue + compat_fn = getattr(compat_root, attr_name) + _PADDLE_NAMESPACE_SAVED[(paddle.Tensor, attr_name)] = native_method + setattr( + paddle.Tensor, + attr_name, + dispatch_compat_api(compat_fn)(native_method), + ) + + +def _apply_paddle_namespace_aliases() -> None: + """Install caller-aware dispatchers/proxies for every public ``paddle.compat.*`` + symbol onto ``paddle.*`` (functions -> dispatcher, classes -> proxy, torch-only + symbols -> direct alias) plus the Tensor methods. Idempotent; ``enable_compat`` + calls this only for ``level=2``.""" + if _PADDLE_NAMESPACE_SAVED: + return + + global _COMPAT_ENABLED + COMPAT_PREFIX = "paddle.compat" + PADDLE_PREFIX = "paddle" + PUBLIC_ATTR_DECLARATION = "__all__" + + for compat_module in _iter_compat_modules(): + if not hasattr(compat_module, PUBLIC_ATTR_DECLARATION): + continue + # paddle.compat -> paddle ; paddle.compat.nn.functional -> paddle.nn.functional + target_name = compat_module.__name__.replace( + COMPAT_PREFIX, PADDLE_PREFIX, 1 + ) + try: + target_module = importlib.import_module(target_name) + except ModuleNotFoundError: + # compat-only subpackage with no paddle counterpart: nothing to alias. + continue + for attr_name in getattr(compat_module, PUBLIC_ATTR_DECLARATION): + if attr_name.startswith("_"): + continue + compat_attr = getattr(compat_module, attr_name) + current = getattr(target_module, attr_name, _MISSING) + if current is compat_attr: + # Already the same object: skip so restore has nothing to undo. + continue + _PADDLE_NAMESPACE_SAVED[(target_module, attr_name)] = current + if current is _MISSING: + # torch-only symbol: alias directly. + setattr(target_module, attr_name, compat_attr) + elif isinstance(compat_attr, type): + # existing class: caller-aware proxy. + setattr( + target_module, + attr_name, + _make_caller_aware_class_proxy(current, compat_attr), + ) + else: + # existing function: caller-aware dispatcher. + setattr( + target_module, + attr_name, + dispatch_compat_api(compat_attr)(current), + ) + _patch_tensor_methods() + _COMPAT_ENABLED = True + + +def _restore_paddle_namespace_aliases() -> None: + """Undo :func:`_apply_paddle_namespace_aliases`, restoring the paddle namespace.""" + global _COMPAT_ENABLED + for (target_module, attr_name), original in _PADDLE_NAMESPACE_SAVED.items(): + if original is _MISSING: + if hasattr(target_module, attr_name): + delattr(target_module, attr_name) + else: + setattr(target_module, attr_name, original) + _PADDLE_NAMESPACE_SAVED.clear() + _COMPAT_ENABLED = False diff --git a/python/paddle/compat/nn/__init__.py b/python/paddle/compat/nn/__init__.py index 2b8e797e6dd3d..c2b42414adc42 100644 --- a/python/paddle/compat/nn/__init__.py +++ b/python/paddle/compat/nn/__init__.py @@ -14,13 +14,14 @@ from __future__ import annotations +import collections import warnings +from itertools import repeat from math import sqrt from typing import TYPE_CHECKING import paddle from paddle import nn -from paddle.nn.modules.utils import _single from paddle.utils.decorator_utils import ForbidKeywordsDecorator from . import functional @@ -58,6 +59,19 @@ ] +def _ntuple(n, name="parse"): + def parse(x): + if isinstance(x, collections.abc.Iterable): + return tuple(x) + return tuple(repeat(x, n)) + + parse.__name__ = name + return parse + + +_single = _ntuple(1, "_single") + + class BatchNorm1D(nn.BatchNorm1D): def __init__( self, diff --git a/python/paddle/compat/proxy.py b/python/paddle/compat/proxy.py index c9f7f5488bd3c..e147895211850 100644 --- a/python/paddle/compat/proxy.py +++ b/python/paddle/compat/proxy.py @@ -26,6 +26,12 @@ from functools import cache from typing import TYPE_CHECKING, Any, Literal +from .api_dispatch import ( + _apply_paddle_namespace_aliases, + _is_paddle_namespace_aliased, + _restore_paddle_namespace_aliases, +) + if TYPE_CHECKING: from collections.abc import Generator, Iterable from typing import TypeAlias @@ -479,6 +485,7 @@ def enable_compat( blocked_modules: _ScopeType = None, backend: Literal["torch"] = "torch", silent: bool = False, + level: int = 1, ) -> None: """ Enable the PyTorch compat by adding the TorchProxyMetaFinder to sys.meta_path. @@ -493,6 +500,9 @@ def enable_compat( "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. Example: .. code-block:: pycon @@ -517,6 +527,10 @@ def enable_compat( >>> paddle.disable_compat() # Disable torch 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) @@ -526,6 +540,9 @@ def enable_compat( _modify_scope_of_torch_proxy(scope, silent=silent) sys.meta_path.insert(0, TORCH_PROXY_FINDER) + if level == 2: + _apply_paddle_namespace_aliases() + def disable_compat() -> None: """ @@ -547,6 +564,7 @@ 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 @@ -601,6 +619,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 + restore_namespace_aliases = _is_paddle_namespace_aliased() if enable == already_has_torch_proxy and ( (original_globally_enabled and scope is None) or (original_local_enabled_scope == (scope or set())) @@ -617,6 +636,8 @@ def use_compat_guard( ) TORCH_PROXY_FINDER._globally_enabled = original_globally_enabled disable_compat() + if restore_namespace_aliases: + _apply_paddle_namespace_aliases() else: disable_compat() try: @@ -627,6 +648,8 @@ def use_compat_guard( original_local_enabled_scope ) TORCH_PROXY_FINDER._globally_enabled = original_globally_enabled + if restore_namespace_aliases: + _apply_paddle_namespace_aliases() def extend_torch_proxy_blocked_modules(modules: Iterable[str]) -> None: diff --git a/test/compat/fake_modules/torch_proxy_root_api_module.py b/test/compat/fake_modules/torch_proxy_root_api_module.py new file mode 100644 index 0000000000000..9ee874e019637 --- /dev/null +++ b/test/compat/fake_modules/torch_proxy_root_api_module.py @@ -0,0 +1,22 @@ +# 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 capture_root_apis(): + import torch + + return {"sort": torch.sort, "min": torch.min, "unique": torch.unique} + + +captured = capture_root_apis() diff --git a/test/compat/test_compat_level2_internal_composites.py b/test/compat/test_compat_level2_internal_composites.py new file mode 100644 index 0000000000000..b847477b47c50 --- /dev/null +++ b/test/compat/test_compat_level2_internal_composites.py @@ -0,0 +1,135 @@ +# 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. + +"""Verify ``enable_compat(level=2)`` keeps paddle-internal composites native. + +- vsplit / hsplit / dsplit / tensor_split / chunk -> split +- quantile -> paddle.sort(x, axis) +- nan_to_num -> paddle.equal -> paddle.where +- histogram_bin_edges -> paddle.min / paddle.max +- F.nll_loss (ignore_index, mean) -> paddle.equal +""" + +import unittest + +import numpy as np + +import paddle +import paddle.nn.functional as F + + +def setUpModule(): + paddle.enable_compat(level=2) + + +def tearDownModule(): + paddle.disable_compat() + + +class TestCompatIsActuallyOn(unittest.TestCase): + def test_external_surface_is_torch_style(self): + t = paddle.to_tensor([[3.0, 1.0, 2.0]]) + self.assertIsInstance(paddle.split(t, 1, dim=0), tuple) + self.assertTrue(hasattr(paddle.sort(t, dim=-1), "values")) + with self.assertRaises(TypeError): + paddle.max(t, axis=1) + + +class TestSplitFamilyStaysNative(unittest.TestCase): + def test_vsplit(self): + x = np.arange(48, dtype="float32").reshape([4, 4, 3]) + outs = paddle.vsplit(paddle.to_tensor(x), 2) + for o, r in zip(outs, np.array_split(x, 2, axis=0)): + np.testing.assert_array_equal(o.numpy(), r) + + def test_hsplit(self): + x = np.arange(24, dtype="float32").reshape([4, 6]) + outs = paddle.hsplit(paddle.to_tensor(x), 3) + for o, r in zip(outs, np.array_split(x, 3, axis=1)): + np.testing.assert_array_equal(o.numpy(), r) + + def test_dsplit(self): + x = np.arange(48, dtype="float32").reshape([2, 4, 6]) + outs = paddle.dsplit(paddle.to_tensor(x), 2) + for o, r in zip(outs, np.array_split(x, 2, axis=2)): + np.testing.assert_array_equal(o.numpy(), r) + + def test_tensor_split(self): + x = np.arange(7, dtype="float32") + outs = paddle.tensor_split(paddle.to_tensor(x), 3) + self.assertEqual(len(outs), 3) + for o, r in zip(outs, np.array_split(x, 3)): + np.testing.assert_array_equal(o.numpy(), r) + + def test_chunk(self): + x = np.arange(18, dtype="float32").reshape([6, 3]) + outs = paddle.chunk(paddle.to_tensor(x), 3, axis=0) + self.assertEqual(len(outs), 3) + for o, r in zip(outs, np.split(x, 3, axis=0)): + np.testing.assert_array_equal(o.numpy(), r) + + +class TestReduceAndCompareStayNative(unittest.TestCase): + def test_quantile_uses_native_sort(self): + x = np.array( + [ + [0.2, 0.7, 0.1, 0.4], + [1.0, 0.3, 0.8, 0.5], + [0.6, 0.9, 0.0, 0.25], + ], + dtype="float32", + ) + got = paddle.quantile(paddle.to_tensor(x), 0.35, axis=1) + np.testing.assert_allclose( + got.numpy(), np.quantile(x, 0.35, axis=1), rtol=1e-5 + ) + + def test_nan_to_num_uses_native_equal(self): + x = np.array([1.0, np.nan, np.inf, -np.inf, -2.5], dtype="float32") + got = paddle.nan_to_num(paddle.to_tensor(x), nan=0.5) + np.testing.assert_allclose(got.numpy(), np.nan_to_num(x, nan=0.5)) + + def test_histogram_bin_edges_uses_native_min_max(self): + x = np.array([0.0, 1.5, 3.0, 4.5, 6.0], dtype="float32") + got = paddle.histogram_bin_edges(paddle.to_tensor(x), bins=4) + np.testing.assert_allclose( + got.numpy(), np.histogram_bin_edges(x, bins=4), rtol=1e-6 + ) + + def test_nll_loss_uses_native_equal(self): + prob = np.array( + [ + [0.70, 0.10, 0.10, 0.10], + [0.20, 0.50, 0.20, 0.10], + [0.10, 0.20, 0.60, 0.10], + [0.25, 0.25, 0.25, 0.25], + [0.10, 0.20, 0.20, 0.50], + ], + dtype="float32", + ) + logp = np.log(prob) + label = np.array([0, 1, 2, 1, 3], dtype="int64") + got = F.nll_loss( + paddle.to_tensor(logp), + paddle.to_tensor(label), + ignore_index=1, + reduction="mean", + ) + keep = label != 1 + ref = -logp[np.arange(5)[keep], label[keep]].sum() / keep.sum() + np.testing.assert_allclose(got.item(), ref, rtol=1e-5) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/compat/test_compat_namespace_aliased.py b/test/compat/test_compat_namespace_aliased.py new file mode 100644 index 0000000000000..fa5c7da052574 --- /dev/null +++ b/test/compat/test_compat_namespace_aliased.py @@ -0,0 +1,573 @@ +# 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. + +import inspect +import pathlib +import sys +import unittest +from contextlib import contextmanager +from functools import wraps + +import paddle +from paddle.compat.api_dispatch import _PADDLE_NAMESPACE_SAVED +from paddle.compat.proxy import TORCH_PROXY_FINDER + +sys.path.append(str(pathlib.Path(__file__).parent / "fake_modules")) + + +@contextmanager +def level2_guard(): + paddle.enable_compat(level=2) + try: + yield + finally: + paddle.disable_compat() + + +def with_level2(func): + @wraps(func) + def wrapper(*args, **kwargs): + with level2_guard(): + return func(*args, **kwargs) + + return wrapper + + +class CompatNamespaceAliasBase(unittest.TestCase): + """Snapshot the relevant native symbols and the proxy-finder state so every + test reverts the global namespace/finder no matter how it ends.""" + + # (module, attr) pairs that should be perfectly restored after disable. + EXISTING = [ + (paddle, "sort"), + (paddle, "split"), + (paddle, "min"), + (paddle, "max"), + (paddle, "unique"), + (paddle, "median"), + (paddle, "nanmedian"), + (paddle, "allclose"), + (paddle, "equal"), + (paddle, "seed"), + ] + # symbols that do NOT exist natively and are created by aliasing. + NEW = [ + (paddle, "slogdet"), + (paddle.nn, "AvgPool1d"), + (paddle.nn, "MultiheadAttention"), + ] + + def setUp(self): + # Dispatch is device-agnostic; run on CPU to dodge a DCU GPU-op hang + # (attention/sdpa). Original device restored in tearDown. + self._device = paddle.get_device() + paddle.set_device('cpu') + while TORCH_PROXY_FINDER in sys.meta_path: + 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: + paddle.disable_compat() + TORCH_PROXY_FINDER._local_enabled_scope = set(self._scope) + TORCH_PROXY_FINDER._globally_enabled = self._global + paddle.set_device(self._device) + + def assertNativeRestored(self): + for (m, a), native in self._native.items(): + self.assertIs( + getattr(m, a), native, f"{m.__name__}.{a} not restored" + ) + for m, a in self.NEW: + self.assertFalse( + hasattr(m, a), f"{m.__name__}.{a} should be gone after disable" + ) + + def assertAliased(self, paddle_attr, compat_attr): + """``paddle.*`` resolves to ``paddle.compat.*``: an existing function is a + dispatcher with ``__compat_fn__``; an existing class is a caller-aware + proxy with ``__compat_cls__``; a new symbol is the compat object itself.""" + target = getattr( + paddle_attr, + "__compat_fn__", + getattr(paddle_attr, "__compat_cls__", paddle_attr), + ) + self.assertIs(target, compat_attr) + + +class TestTopLevelAlias(CompatNamespaceAliasBase): + def test_public_level_parameter_is_minimal(self): + enable_parameters = list( + inspect.signature(paddle.enable_compat).parameters + ) + self.assertEqual(enable_parameters[-1], "level") + self.assertEqual( + inspect.signature(paddle.enable_compat).parameters["level"].default, + 1, + ) + 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: 3"): + paddle.enable_compat(level=3) + self.assertNotIn(TORCH_PROXY_FINDER, sys.meta_path) + self.assertNativeRestored() + + def test_top_level_symbols_aliased_on_enable(self): + paddle.enable_compat(level=2) + self.assertAliased(paddle.sort, paddle.compat.sort) + self.assertAliased(paddle.split, paddle.compat.split) + self.assertAliased(paddle.min, paddle.compat.min) + self.assertAliased(paddle.max, paddle.compat.max) + self.assertAliased(paddle.unique, paddle.compat.unique) + self.assertAliased(paddle.median, paddle.compat.median) + self.assertAliased(paddle.nanmedian, paddle.compat.nanmedian) + self.assertAliased(paddle.allclose, paddle.compat.allclose) + self.assertAliased(paddle.equal, paddle.compat.equal) + self.assertAliased(paddle.seed, paddle.compat.seed) + paddle.disable_compat() + self.assertNativeRestored() + + def test_level1_default_does_not_alias(self): + """level=1 (the default) installs only the torch proxy and must NOT alias + paddle.*; the namespace dispatch is opt-in via level=2.""" + paddle.enable_compat() # default level=1 + try: + self.assertIs(paddle.sort, self._native[(paddle, "sort")]) + self.assertFalse(hasattr(paddle.sort, "__compat_fn__")) + with self.assertRaises(TypeError): + paddle.sort( + paddle.to_tensor([3.0, 1.0]), dim=-1 + ) # native: no dim= + finally: + paddle.disable_compat() + self.assertNativeRestored() + + @with_level2 + def test_submodule_symbols_aliased(self): + self.assertAliased(paddle.nn.Linear, paddle.compat.nn.Linear) + self.assertAliased(paddle.nn.Softmax, paddle.compat.nn.Softmax) + self.assertAliased(paddle.nn.Unfold, paddle.compat.nn.Unfold) + self.assertAliased( + paddle.nn.functional.pad, paddle.compat.nn.functional.pad + ) + self.assertAliased( + paddle.nn.functional.linear, paddle.compat.nn.functional.linear + ) + + @with_level2 + def test_aliased_signatures_are_torch_style(self): + self.assertEqual( + inspect.signature(paddle.sort), + inspect.signature(paddle.compat.sort), + ) + self.assertEqual( + inspect.signature(paddle.nn.Linear), + inspect.signature(paddle.compat.nn.Linear), + ) + + def test_new_symbols_created_then_removed(self): + for m, a in self.NEW: + self.assertFalse(hasattr(m, a)) + paddle.enable_compat(level=2) + self.assertIs(paddle.slogdet, paddle.compat.slogdet) + self.assertIs(paddle.nn.AvgPool1d, paddle.compat.nn.AvgPool1d) + self.assertIs( + paddle.nn.MultiheadAttention, paddle.compat.nn.MultiheadAttention + ) + paddle.disable_compat() + self.assertNativeRestored() + + def test_all_avgpool_aliases(self): + """Every AvgPool alias is aliased; the lowercase variants are new symbols + that appear on enable and are removed on disable.""" + for a in ("AvgPool1d", "AvgPool2d", "AvgPool3d"): + self.assertFalse(hasattr(paddle.nn, a)) + paddle.enable_compat(level=2) + try: + for a in ( + "AvgPool1D", + "AvgPool2D", + "AvgPool3D", + "AvgPool1d", + "AvgPool2d", + "AvgPool3d", + ): + self.assertAliased( + getattr(paddle.nn, a), getattr(paddle.compat.nn, a) + ) + finally: + paddle.disable_compat() + for a in ("AvgPool1d", "AvgPool2d", "AvgPool3d"): + self.assertFalse(hasattr(paddle.nn, a)) + + @with_level2 + def test_all_nn_functional_aliases(self): + """Every public compat nn.functional symbol overrides its paddle target, + including softmax/log_softmax/scaled_dot_product_attention (NOT no-ops).""" + F, cF = paddle.nn.functional, paddle.compat.nn.functional + for a in ( + "pad", + "softmax", + "log_softmax", + "linear", + "scaled_dot_product_attention", + "unfold", + ): + self.assertAliased(getattr(F, a), getattr(cF, a)) + + +class TestAliasBehavior(CompatNamespaceAliasBase): + def test_sort_returns_namedtuple(self): + t = paddle.to_tensor([[3.0, 1.0, 2.0], [9.0, 7.0, 8.0]]) + with level2_guard(): + out = paddle.sort(t, dim=-1) + self.assertTrue(hasattr(out, "values")) + self.assertTrue(hasattr(out, "indices")) + self.assertEqual(out.values[0].tolist(), [1.0, 2.0, 3.0]) + # native again: returns a plain Tensor, not the compat namedtuple + self.assertIs(paddle.sort, self._native[(paddle, "sort")]) + self.assertNotIsInstance(paddle.sort(t), tuple) + + @with_level2 + def test_min_max_no_recursion(self): + """Reduce-all min/max self-call the native impl; must not RecursionError.""" + t = paddle.to_tensor([3.0, 1.0, 2.0]) + self.assertEqual(paddle.min(t).item(), 1.0) + self.assertEqual(paddle.max(t).item(), 3.0) + # dim form returns a namedtuple (values, indices) + r = paddle.min(t, dim=0) + self.assertEqual(r.values.item(), 1.0) + self.assertEqual(r.indices.item(), 1) + + @with_level2 + def test_median_unique_allclose_no_recursion(self): + t = paddle.to_tensor([[1.0, 2.0, 3.0], [6.0, 5.0, 4.0]]) + med = paddle.median(t, dim=1) + self.assertTrue(hasattr(med, "values")) + u = paddle.unique(paddle.to_tensor([2, 3, 3, 1])) + self.assertEqual(u.tolist(), [1, 2, 3]) + # compat.allclose returns a python bool + res = paddle.allclose(t, t) + self.assertIsInstance(res, bool) + self.assertTrue(res) + + @with_level2 + def test_seed_no_recursion(self): + s = paddle.seed() # compat.seed takes no arg and returns an int + self.assertIsInstance(s, int) + + @with_level2 + def test_multihead_attention_forward_under_compat(self): + """MHA forward chains F.linear, sdpa and F.softmax without recursion.""" + mha = paddle.nn.MultiheadAttention(8, 2, batch_first=True) + x = paddle.randn([2, 5, 8]) + out, weights = mha(x, x, x, need_weights=False) # sdpa path + self.assertEqual(out.shape, [2, 5, 8]) + self.assertIsNone(weights) + out2, weights2 = mha(x, x, x, need_weights=True) # softmax path + self.assertEqual(out2.shape, [2, 5, 8]) + self.assertIsNotNone(weights2) + + @with_level2 + def test_aliased_wrappers_execute(self): + """Broader behavioral coverage of the self-ref-fixed / aliased wrappers + that were previously only checked by identity.""" + t = paddle.to_tensor([[1.0, 2.0, 3.0], [6.0, 5.0, 4.0]]) + # nanmedian dim-path reaches the native paddle.nanmedian internally + nm = paddle.nanmedian( + paddle.to_tensor([[1.0, float("nan")], [3.0, 4.0]]), dim=0 + ) + self.assertTrue(hasattr(nm, "values")) + # split: torch semantics (per-chunk size), returns a tuple + parts = paddle.split(t, 1, dim=0) + self.assertIsInstance(parts, tuple) + self.assertEqual(len(parts), 2) + # equal returns a python bool (like allclose) + self.assertIsInstance(paddle.equal(t, t), bool) + # slogdet returns a (sign, logabsdet) namedtuple + sd = paddle.slogdet(paddle.to_tensor([[1.0, 0.0], [0.0, 2.0]])) + self.assertTrue(hasattr(sd, "sign")) + # nn.functional: pad / linear / unfold + self.assertEqual(paddle.nn.functional.pad(t, [1, 1]).shape, [2, 5]) + y = paddle.nn.functional.linear( + t, paddle.ones([4, 3]), paddle.zeros([4]) + ) + self.assertEqual(y.shape, [2, 4]) + img = paddle.randn([1, 1, 4, 4]) + self.assertEqual(paddle.nn.functional.unfold(img, 2).shape[:2], [1, 4]) + # nn.Unfold layer forward routes nn.functional.unfold to native + self.assertEqual(paddle.nn.Unfold(kernel_size=2)(img).shape[0], 1) + + @with_level2 + def test_nn_functional_softmax_logsoftmax_sdpa_execute(self): + """Direct execution of the aliased softmax/log_softmax/sdpa (these are + genuinely aliased to torch-style compat APIs, not no-ops). After enable, + paddle.nn.functional.softmax is the torch-style compat API, so it takes + `dim=` and REJECTS the paddle-native `axis=` via ForbidKeywords.""" + x = paddle.randn([2, 4]) + self.assertEqual(paddle.nn.functional.softmax(x, dim=-1).shape, [2, 4]) + self.assertEqual( + paddle.nn.functional.log_softmax(x, dim=-1).shape, [2, 4] + ) + # paddle-native axis= is rejected once aliased (torch-style contract) + with self.assertRaises(TypeError): + paddle.nn.functional.softmax(x, axis=-1) + q = paddle.randn([2, 2, 5, 4]) + out = paddle.nn.functional.scaled_dot_product_attention(q, q, q) + self.assertEqual(out.shape, [2, 2, 5, 4]) + + def test_sdpa_numeric_correct_under_compat(self): + """Regression for caller-aware dispatch: at level=2 paddle.nn.functional.softmax + dispatches to the torch-style compat softmax for *external* code, but paddle's + own sdpa math backend (``_math_attention``) calls F.softmax internally and must + get the NATIVE softmax (last/key axis). fp32 forces the math backend, so a + wrong-axis softmax would diverge from this hand-computed softmax(QK^T/sqrt(d))V + reference.""" + import numpy as np + + rng = np.random.RandomState(0) + x = rng.rand(2, 4, 8, 16).astype("float32") + d = x.shape[-1] + s = (x @ np.swapaxes(x, -1, -2)) / np.sqrt(d) + s = s - s.max(-1, keepdims=True) + e = np.exp(s) + ref = (e / e.sum(-1, keepdims=True)) @ x + with level2_guard(): + q = paddle.to_tensor(x) + out = paddle.nn.functional.scaled_dot_product_attention(q, q, q) + np.testing.assert_allclose(out.numpy(), ref, rtol=1e-4, atol=1e-4) + + +class TestScopeAndLifecycle(CompatNamespaceAliasBase): + def test_scoped_level2_enable_aliases(self): + paddle.enable_compat(scope={"triton"}, level=2, silent=True) + try: + self.assertAliased(paddle.sort, paddle.compat.sort) + self.assertIs(paddle.slogdet, paddle.compat.slogdet) + finally: + paddle.disable_compat() + self.assertNativeRestored() + + def test_level1_repeated_enable_preserves_legacy_finders(self): + paddle.enable_compat() + paddle.enable_compat(scope={"triton"}, silent=True) + self.assertEqual(sys.meta_path.count(TORCH_PROXY_FINDER), 2) + paddle.disable_compat() + self.assertEqual(sys.meta_path.count(TORCH_PROXY_FINDER), 1) + paddle.disable_compat() + self.assertNotIn(TORCH_PROXY_FINDER, sys.meta_path) + self.assertNativeRestored() + + def test_registry_empty_after_disable(self): + self.assertEqual(len(_PADDLE_NAMESPACE_SAVED), 0) + paddle.enable_compat(level=2) + self.assertGreater(len(_PADDLE_NAMESPACE_SAVED), 0) + paddle.disable_compat() + self.assertEqual(len(_PADDLE_NAMESPACE_SAVED), 0) + + def test_use_compat_guard_nested(self): + native_sort = self._native[(paddle, "sort")] + paddle.enable_compat(level=2) + try: + self.assertAliased(paddle.sort, paddle.compat.sort) + with paddle.use_compat_guard(enable=False): + self.assertIs(paddle.sort, native_sort) + self.assertAliased(paddle.sort, paddle.compat.sort) + finally: + paddle.disable_compat() + self.assertIs(paddle.sort, native_sort) + + def test_bare_guard_keeps_level2_alias(self): + t = paddle.to_tensor([[3.0, 1.0, 2.0]]) + paddle.enable_compat(level=2) + try: + with paddle.use_compat_guard(): + self.assertAliased(paddle.sort, paddle.compat.sort) + self.assertTrue(hasattr(paddle.sort(t, dim=-1), "values")) + # the level in effect survives the guard + self.assertAliased(paddle.sort, paddle.compat.sort) + self.assertTrue(hasattr(paddle.sort(t, dim=-1), "values")) + finally: + paddle.disable_compat() + self.assertNativeRestored() + + def test_scoped_guard_restores_level2_alias(self): + native_sort = self._native[(paddle, "sort")] + paddle.enable_compat(scope={"triton"}, level=2, silent=True) + try: + self.assertAliased(paddle.sort, paddle.compat.sort) + with paddle.use_compat_guard(enable=False): + self.assertIs(paddle.sort, native_sort) + self.assertFalse(TORCH_PROXY_FINDER._globally_enabled) + self.assertAliased(paddle.sort, paddle.compat.sort) + self.assertGreater(len(_PADDLE_NAMESPACE_SAVED), 0) + finally: + paddle.disable_compat() + self.assertNativeRestored() + + +class TestTorchSurfaceUnderCompat(CompatNamespaceAliasBase): + """torch.* still reaches the compat implementations: at level=2 the paddle.* + alias carries through the torch proxy (torch.sort -> paddle.sort -> + paddle.compat.sort).""" + + @staticmethod + def _drop_torch_modules(): + for name in [ + m + for m in list(sys.modules) + if m == "torch" or m.startswith("torch.") + ]: + del sys.modules[name] + + def test_level1_root_torch_apis_keep_native_mapping(self): + paddle.enable_compat() + try: + self._drop_torch_modules() + import torch + + self.assertIs(torch.sort, self._native[(paddle, "sort")]) + self.assertIs(torch.min, self._native[(paddle, "min")]) + self.assertIs(torch.unique, self._native[(paddle, "unique")]) + finally: + self._drop_torch_modules() + paddle.disable_compat() + + def test_root_torch_apis_resolve_to_compat_at_level2(self): + paddle.enable_compat(level=2) + try: + self._drop_torch_modules() + import torch + + # Root functions have no dedicated torch override; at level=2 the + # paddle.* alias carries them through: torch.sort -> paddle.sort -> + # paddle.compat.sort. + self.assertAliased(torch.sort, paddle.compat.sort) + self.assertAliased(torch.min, paddle.compat.min) + self.assertAliased(torch.unique, paddle.compat.unique) + # nn.* overrides reach compat regardless of the alias. + self.assertIs(torch.nn.Linear, paddle.compat.nn.Linear) + finally: + self._drop_torch_modules() + paddle.disable_compat() + + def test_scoped_enable_survives_scoped_module_import(self): + """A scoped torch proxy and the level-2 paddle aliases are independent.""" + mod = "torch_proxy_root_api_module" + sys.modules.pop(mod, None) + self._drop_torch_modules() + paddle.enable_compat(scope=mod, level=2, silent=True) + try: + import torch_proxy_root_api_module # noqa: F401 (imports torch inside) + + self.assertAliased(paddle.sort, paddle.compat.sort) + self.assertIn(TORCH_PROXY_FINDER, sys.meta_path) + self.assertFalse(TORCH_PROXY_FINDER._globally_enabled) + self.assertGreater(len(_PADDLE_NAMESPACE_SAVED), 0) + sys.modules.pop(mod, None) + self._drop_torch_modules() + __import__(mod) + finally: + sys.modules.pop(mod, None) + self._drop_torch_modules() + TORCH_PROXY_FINDER._globally_enabled = False + TORCH_PROXY_FINDER._local_enabled_scope = set() + paddle.disable_compat() + + +class TestLevel2InternalCallersUseNative(CompatNamespaceAliasBase): + """level=2 redirects ``paddle.*`` to the torch-aligned compat APIs for the + USER's code, but paddle's own internals call these same APIs (F.softmax / + F.linear / paddle.max / ...) with native ``axis=``/``name=`` kwargs and native + defaults. Caller-aware dispatch must route those internal calls to native, or + composite native layers break under level=2.""" + + @with_level2 + def test_native_composite_layers_run_under_level2(self): + # Each of these calls F.linear / F.softmax / paddle.* internally with + # native kwargs; under level=2 they must still work (internal -> native). + x = paddle.randn([2, 5, 8]) + self.assertEqual( + paddle.nn.TransformerEncoderLayer(8, 2, 16)(x).shape, [2, 5, 8] + ) + mha = paddle.nn.MultiHeadAttention( + 8, 2 + ) # native (capital H), not compat + self.assertEqual(mha(x, x, x).shape, [2, 5, 8]) + self.assertEqual(paddle.nn.LayerNorm(8)(x).shape, [2, 5, 8]) + + @with_level2 + def test_external_caller_still_gets_compat(self): + # This test module is not a ``paddle.*`` module, so it still sees the + # torch-aligned compat API: torch-style ``dim=`` + namedtuple return, and + # the native ``axis=`` kwarg is rejected by the torch-style contract. + t = paddle.to_tensor([[3.0, 1.0, 2.0]]) + self.assertTrue(hasattr(paddle.sort(t, dim=-1), "values")) + with self.assertRaises(TypeError): + paddle.max(t, axis=1) + + 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.""" + native_max = paddle.Tensor.max + 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 + self.assertTrue(hasattr(r, "values")) + self.assertEqual(r.values.tolist(), [3.0, 6.0]) + self.assertIsInstance(t.split(1, dim=0), tuple) + # paddle-internal native-style call (simulated) stays native + ns = {"__name__": "paddle.fake_internal", "t": t} + exec("internal = t.max(axis=1)", ns) # native accepts axis= + self.assertIs(paddle.Tensor.max, native_max) # restored on disable + + @with_level2 + def test_aliased_class_caller_aware(self): + """Existing classes (Linear/...) become caller-aware proxies: external + callers get the torch-aligned compat class, paddle-internal callers get + native; isinstance/issubclass accept either form.""" + # external (this module) -> compat class (torch-style) + self.assertIs(type(paddle.nn.Linear(2, 2)), paddle.compat.nn.Linear) + with self.assertRaises(TypeError): + paddle.nn.Linear(2, 2, weight_attr=False) # native kwarg rejected + # paddle-internal caller (simulated) -> native class, accepts native kwarg + ns = {"__name__": "paddle.fake_internal", "paddle": paddle} + exec("obj = paddle.nn.Linear(2, 2, weight_attr=False)", ns) + self.assertIsNot(type(ns["obj"]), paddle.compat.nn.Linear) + # isinstance / issubclass accept both forms + self.assertIsInstance(paddle.nn.Linear(2, 2), paddle.nn.Linear) + self.assertTrue(issubclass(paddle.compat.nn.Linear, paddle.nn.Linear)) + + @with_level2 + def test_aliased_class_subclassing_is_torch_style(self): + """A user subclass derived from the alias class under level=2 uses the + torch-style (compat) constructor, not a silent fallback to native.""" + + class MyLinear(paddle.nn.Linear): + pass + + m = MyLinear(3, 4, bias=False) # torch-style ``bias=`` must be accepted + self.assertIsInstance(m, MyLinear) + self.assertIsInstance(m, paddle.nn.Linear) + self.assertTrue(issubclass(MyLinear, paddle.nn.Linear)) + + +if __name__ == "__main__": + unittest.main() From 2cea4601f27165261b0d07c944c536128141fb0e Mon Sep 17 00:00:00 2001 From: manfredss Date: Fri, 17 Jul 2026 10:58:23 +0000 Subject: [PATCH 2/9] fix --- python/paddle/compat/nn/__init__.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/python/paddle/compat/nn/__init__.py b/python/paddle/compat/nn/__init__.py index c2b42414adc42..2b8e797e6dd3d 100644 --- a/python/paddle/compat/nn/__init__.py +++ b/python/paddle/compat/nn/__init__.py @@ -14,14 +14,13 @@ from __future__ import annotations -import collections import warnings -from itertools import repeat from math import sqrt from typing import TYPE_CHECKING import paddle from paddle import nn +from paddle.nn.modules.utils import _single from paddle.utils.decorator_utils import ForbidKeywordsDecorator from . import functional @@ -59,19 +58,6 @@ ] -def _ntuple(n, name="parse"): - def parse(x): - if isinstance(x, collections.abc.Iterable): - return tuple(x) - return tuple(repeat(x, n)) - - parse.__name__ = name - return parse - - -_single = _ntuple(1, "_single") - - class BatchNorm1D(nn.BatchNorm1D): def __init__( self, From 7ee7eea58d6ad4289dff3ca19e4975e9245ee309 Mon Sep 17 00:00:00 2001 From: manfredss Date: Fri, 17 Jul 2026 15:57:03 +0000 Subject: [PATCH 3/9] re-use func, simplify the logic --- python/paddle/compat/api_dispatch.py | 119 +++---------------- python/paddle/compat/proxy.py | 4 +- test/compat/test_compat_namespace_aliased.py | 64 +++------- 3 files changed, 35 insertions(+), 152 deletions(-) diff --git a/python/paddle/compat/api_dispatch.py b/python/paddle/compat/api_dispatch.py index 704d3e5526a84..3c42a9ed6b7c2 100644 --- a/python/paddle/compat/api_dispatch.py +++ b/python/paddle/compat/api_dispatch.py @@ -22,7 +22,6 @@ from __future__ import annotations -import contextvars import importlib import inspect import pkgutil @@ -35,67 +34,16 @@ from collections.abc import Generator -class _MissingType: - """Sentinel marking a paddle attribute that did not exist before aliasing.""" - - -_MISSING = _MissingType() - -# (live module, attr name) -> original value (or ``_MISSING``) for every -# ``paddle.*`` symbol aliased to its ``paddle.compat.*`` counterpart, so the -# paddle namespace can be restored exactly on ``disable_compat()``. +# (live module, attr name) -> original value for every patched ``paddle.*`` +# symbol, so ``disable_compat()`` can restore the paddle namespace. _PADDLE_NAMESPACE_SAVED: dict[tuple[types.ModuleType, str], Any] = {} -# Dispatch state: _COMPAT_ENABLED is the process-wide on/off; _COMPAT_SUSPENDED is -# a per-thread/async-task override so a running compat API uses native internals. _COMPAT_ENABLED = False -_COMPAT_SUSPENDED: contextvars.ContextVar[bool] = contextvars.ContextVar( - "paddle_compat_suspended", default=False -) def is_compat_api_enabled() -> bool: - """Whether ``paddle.*`` currently dispatches to the torch-aligned compat APIs - in this thread / async task.""" - return _COMPAT_ENABLED and not _COMPAT_SUSPENDED.get() - - -def _is_paddle_namespace_aliased() -> bool: - """Whether level-2 aliases are currently installed process-wide.""" - return bool(_PADDLE_NAMESPACE_SAVED) - - -class compat_api_guard: - """Suspend/restore compat dispatch for the current thread / async task. - The level-2 dispatcher suspends aliases while a compat implementation runs, - so its internal ``paddle.*`` calls hit native. Context manager and decorator; - the decorator preserves ``__signature__``.""" - - def __init__(self, enable: bool = True) -> None: - self._enable = enable - self._token = None - - def __enter__(self) -> None: - self._token = _COMPAT_SUSPENDED.set(not self._enable) - - def __exit__(self, *exc: object) -> bool: - _COMPAT_SUSPENDED.reset(self._token) - self._token = None - return False - - def __call__(self, func: Any) -> Any: - enable = self._enable - - @wraps(func) - def wrapper(*args: Any, **kwargs: Any) -> Any: - # A fresh guard per call keeps re-entrant / concurrent use correct. - with compat_api_guard(enable): - return func(*args, **kwargs) - - # Every decorated callable is a pure-Python compat API, so its - # signature is always introspectable. - wrapper.__signature__ = inspect.signature(func) - return wrapper + """Whether ``paddle.*`` currently dispatches to compat APIs.""" + return _COMPAT_ENABLED def _caller_is_paddle_internal() -> bool: @@ -118,19 +66,10 @@ def dispatch_compat_api(compat_fn: Any) -> Any: so the default hot path is untouched.""" def decorator(native_fn: Any) -> Any: - getframe = sys._getframe - suspended = _COMPAT_SUSPENDED.get - @wraps(native_fn) def dispatcher(*args: Any, **kwargs: Any) -> Any: - if _COMPAT_ENABLED and not suspended(): - caller = getframe().f_back - name = ( - "" if caller is None else caller.f_globals.get("__name__") - ) or "" - if name != "paddle" and not name.startswith("paddle."): - with compat_api_guard(enable=False): - return compat_fn(*args, **kwargs) + if is_compat_api_enabled() and not _caller_is_paddle_internal(): + return compat_fn(*args, **kwargs) return native_fn(*args, **kwargs) dispatcher.__compat_fn__ = compat_fn @@ -158,10 +97,7 @@ def _iter_compat_modules() -> Generator[types.ModuleType, None, None]: def _make_caller_aware_class_proxy(native_cls: type, compat_cls: type) -> type: - """Caller-aware ``paddle.X`` class: instantiates ``compat_cls`` for external - callers, ``native_cls`` for paddle-internal ones; ``isinstance``/``issubclass`` - accept either. Subclasses ``compat_cls`` so a user subclass derived under - level=2 keeps the torch-style (compat) constructor/methods.""" + """Create a class proxy that selects compat only for external callers.""" class _CompatAwareMeta(type(compat_cls)): def __call__(cls, *args: Any, **kwargs: Any) -> Any: @@ -204,10 +140,8 @@ def _patch_tensor_methods() -> None: import paddle.compat as compat_root for attr_name in getattr(compat_root, "__all__", ()): - if attr_name.startswith("_"): - continue - native_method = getattr(paddle.Tensor, attr_name, _MISSING) - if native_method is _MISSING or not callable(native_method): + native_method = getattr(paddle.Tensor, attr_name, None) + if native_method is None or not callable(native_method): continue compat_fn = getattr(compat_root, attr_name) _PADDLE_NAMESPACE_SAVED[(paddle.Tensor, attr_name)] = native_method @@ -220,50 +154,33 @@ def _patch_tensor_methods() -> None: def _apply_paddle_namespace_aliases() -> None: """Install caller-aware dispatchers/proxies for every public ``paddle.compat.*`` - symbol onto ``paddle.*`` (functions -> dispatcher, classes -> proxy, torch-only - symbols -> direct alias) plus the Tensor methods. Idempotent; ``enable_compat`` - calls this only for ``level=2``.""" + symbol that has a ``paddle.*`` counterpart, plus the Tensor methods.""" if _PADDLE_NAMESPACE_SAVED: return global _COMPAT_ENABLED - COMPAT_PREFIX = "paddle.compat" - PADDLE_PREFIX = "paddle" - PUBLIC_ATTR_DECLARATION = "__all__" for compat_module in _iter_compat_modules(): - if not hasattr(compat_module, PUBLIC_ATTR_DECLARATION): - continue - # paddle.compat -> paddle ; paddle.compat.nn.functional -> paddle.nn.functional target_name = compat_module.__name__.replace( - COMPAT_PREFIX, PADDLE_PREFIX, 1 + "paddle.compat", "paddle", 1 ) try: target_module = importlib.import_module(target_name) except ModuleNotFoundError: - # compat-only subpackage with no paddle counterpart: nothing to alias. continue - for attr_name in getattr(compat_module, PUBLIC_ATTR_DECLARATION): - if attr_name.startswith("_"): - continue + for attr_name in getattr(compat_module, "__all__", ()): compat_attr = getattr(compat_module, attr_name) - current = getattr(target_module, attr_name, _MISSING) - if current is compat_attr: - # Already the same object: skip so restore has nothing to undo. + current = getattr(target_module, attr_name, None) + if current is None or current is compat_attr: continue _PADDLE_NAMESPACE_SAVED[(target_module, attr_name)] = current - if current is _MISSING: - # torch-only symbol: alias directly. - setattr(target_module, attr_name, compat_attr) - elif isinstance(compat_attr, type): - # existing class: caller-aware proxy. + if isinstance(compat_attr, type): setattr( target_module, attr_name, _make_caller_aware_class_proxy(current, compat_attr), ) else: - # existing function: caller-aware dispatcher. setattr( target_module, attr_name, @@ -277,10 +194,6 @@ def _restore_paddle_namespace_aliases() -> None: """Undo :func:`_apply_paddle_namespace_aliases`, restoring the paddle namespace.""" global _COMPAT_ENABLED for (target_module, attr_name), original in _PADDLE_NAMESPACE_SAVED.items(): - if original is _MISSING: - if hasattr(target_module, attr_name): - delattr(target_module, attr_name) - else: - setattr(target_module, attr_name, original) + setattr(target_module, attr_name, original) _PADDLE_NAMESPACE_SAVED.clear() _COMPAT_ENABLED = False diff --git a/python/paddle/compat/proxy.py b/python/paddle/compat/proxy.py index e147895211850..e9e3c940b580e 100644 --- a/python/paddle/compat/proxy.py +++ b/python/paddle/compat/proxy.py @@ -27,8 +27,8 @@ from typing import TYPE_CHECKING, Any, Literal from .api_dispatch import ( + _PADDLE_NAMESPACE_SAVED, _apply_paddle_namespace_aliases, - _is_paddle_namespace_aliased, _restore_paddle_namespace_aliases, ) @@ -619,7 +619,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 - restore_namespace_aliases = _is_paddle_namespace_aliased() + restore_namespace_aliases = bool(_PADDLE_NAMESPACE_SAVED) if enable == already_has_torch_proxy and ( (original_globally_enabled and scope is None) or (original_local_enabled_scope == (scope or set())) diff --git a/test/compat/test_compat_namespace_aliased.py b/test/compat/test_compat_namespace_aliased.py index fa5c7da052574..236b900d9f0b7 100644 --- a/test/compat/test_compat_namespace_aliased.py +++ b/test/compat/test_compat_namespace_aliased.py @@ -61,10 +61,15 @@ class CompatNamespaceAliasBase(unittest.TestCase): (paddle, "equal"), (paddle, "seed"), ] - # symbols that do NOT exist natively and are created by aliasing. - NEW = [ + # Compat-only symbols must not be added to the paddle namespace. + COMPAT_ONLY = [ (paddle, "slogdet"), (paddle.nn, "AvgPool1d"), + (paddle.nn, "AvgPool2d"), + (paddle.nn, "AvgPool3d"), + (paddle.nn, "BatchNorm1d"), + (paddle.nn, "BatchNorm2d"), + (paddle.nn, "BatchNorm3d"), (paddle.nn, "MultiheadAttention"), ] @@ -91,15 +96,13 @@ def assertNativeRestored(self): self.assertIs( getattr(m, a), native, f"{m.__name__}.{a} not restored" ) - for m, a in self.NEW: + for m, a in self.COMPAT_ONLY: self.assertFalse( - hasattr(m, a), f"{m.__name__}.{a} should be gone after disable" + hasattr(m, a), f"{m.__name__}.{a} should not be added" ) def assertAliased(self, paddle_attr, compat_attr): - """``paddle.*`` resolves to ``paddle.compat.*``: an existing function is a - dispatcher with ``__compat_fn__``; an existing class is a caller-aware - proxy with ``__compat_cls__``; a new symbol is the compat object itself.""" + """Check the dispatcher or class proxy points to the compat API.""" target = getattr( paddle_attr, "__compat_fn__", @@ -181,40 +184,23 @@ def test_aliased_signatures_are_torch_style(self): inspect.signature(paddle.compat.nn.Linear), ) - def test_new_symbols_created_then_removed(self): - for m, a in self.NEW: - self.assertFalse(hasattr(m, a)) + def test_compat_only_symbols_are_not_added(self): paddle.enable_compat(level=2) - self.assertIs(paddle.slogdet, paddle.compat.slogdet) - self.assertIs(paddle.nn.AvgPool1d, paddle.compat.nn.AvgPool1d) - self.assertIs( - paddle.nn.MultiheadAttention, paddle.compat.nn.MultiheadAttention - ) - paddle.disable_compat() - self.assertNativeRestored() + try: + for module, name in self.COMPAT_ONLY: + self.assertFalse(hasattr(module, name)) + finally: + paddle.disable_compat() def test_all_avgpool_aliases(self): - """Every AvgPool alias is aliased; the lowercase variants are new symbols - that appear on enable and are removed on disable.""" - for a in ("AvgPool1d", "AvgPool2d", "AvgPool3d"): - self.assertFalse(hasattr(paddle.nn, a)) paddle.enable_compat(level=2) try: - for a in ( - "AvgPool1D", - "AvgPool2D", - "AvgPool3D", - "AvgPool1d", - "AvgPool2d", - "AvgPool3d", - ): + for a in ("AvgPool1D", "AvgPool2D", "AvgPool3D"): self.assertAliased( getattr(paddle.nn, a), getattr(paddle.compat.nn, a) ) finally: paddle.disable_compat() - for a in ("AvgPool1d", "AvgPool2d", "AvgPool3d"): - self.assertFalse(hasattr(paddle.nn, a)) @with_level2 def test_all_nn_functional_aliases(self): @@ -272,18 +258,6 @@ def test_seed_no_recursion(self): s = paddle.seed() # compat.seed takes no arg and returns an int self.assertIsInstance(s, int) - @with_level2 - def test_multihead_attention_forward_under_compat(self): - """MHA forward chains F.linear, sdpa and F.softmax without recursion.""" - mha = paddle.nn.MultiheadAttention(8, 2, batch_first=True) - x = paddle.randn([2, 5, 8]) - out, weights = mha(x, x, x, need_weights=False) # sdpa path - self.assertEqual(out.shape, [2, 5, 8]) - self.assertIsNone(weights) - out2, weights2 = mha(x, x, x, need_weights=True) # softmax path - self.assertEqual(out2.shape, [2, 5, 8]) - self.assertIsNotNone(weights2) - @with_level2 def test_aliased_wrappers_execute(self): """Broader behavioral coverage of the self-ref-fixed / aliased wrappers @@ -300,9 +274,6 @@ def test_aliased_wrappers_execute(self): self.assertEqual(len(parts), 2) # equal returns a python bool (like allclose) self.assertIsInstance(paddle.equal(t, t), bool) - # slogdet returns a (sign, logabsdet) namedtuple - sd = paddle.slogdet(paddle.to_tensor([[1.0, 0.0], [0.0, 2.0]])) - self.assertTrue(hasattr(sd, "sign")) # nn.functional: pad / linear / unfold self.assertEqual(paddle.nn.functional.pad(t, [1, 1]).shape, [2, 5]) y = paddle.nn.functional.linear( @@ -359,7 +330,6 @@ def test_scoped_level2_enable_aliases(self): paddle.enable_compat(scope={"triton"}, level=2, silent=True) try: self.assertAliased(paddle.sort, paddle.compat.sort) - self.assertIs(paddle.slogdet, paddle.compat.slogdet) finally: paddle.disable_compat() self.assertNativeRestored() From 22f97e4b3d4204cc9fd17a5765b5a142ed43ca45 Mon Sep 17 00:00:00 2001 From: manfredss Date: Sat, 18 Jul 2026 16:04:51 +0000 Subject: [PATCH 4/9] fix slogdet been skipped convert --- python/paddle/compat/proxy.py | 12 ++++++++ test/compat/test_compat_namespace_aliased.py | 30 ++++++++++++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/python/paddle/compat/proxy.py b/python/paddle/compat/proxy.py index e9e3c940b580e..586eb4e12deac 100644 --- a/python/paddle/compat/proxy.py +++ b/python/paddle/compat/proxy.py @@ -30,6 +30,7 @@ _PADDLE_NAMESPACE_SAVED, _apply_paddle_namespace_aliases, _restore_paddle_namespace_aliases, + is_compat_api_enabled, ) if TYPE_CHECKING: @@ -360,6 +361,17 @@ def _find_spec_for_torch_module(self, fullname: str): for k, v in GLOBAL_OVERRIDES.items() if k.startswith(f"{fullname}.") } + if fullname == "torch" and is_compat_api_enabled(): + compat_root = importlib.import_module("paddle.compat") + overrides.update( + { + attr_name: RawOverriddenAttribute( + getattr(compat_root, attr_name) + ) + for attr_name in getattr(compat_root, "__all__", ()) + if not hasattr(source_module, attr_name) + } + ) is_pkg = hasattr(source_module, "__path__") diff --git a/test/compat/test_compat_namespace_aliased.py b/test/compat/test_compat_namespace_aliased.py index 236b900d9f0b7..15bc1f9fbc90c 100644 --- a/test/compat/test_compat_namespace_aliased.py +++ b/test/compat/test_compat_namespace_aliased.py @@ -425,9 +425,8 @@ def test_root_torch_apis_resolve_to_compat_at_level2(self): self._drop_torch_modules() import torch - # Root functions have no dedicated torch override; at level=2 the - # paddle.* alias carries them through: torch.sort -> paddle.sort -> - # paddle.compat.sort. + # Root functions with native Paddle counterparts carry through the + # paddle.* aliases: torch.sort -> paddle.sort -> paddle.compat.sort. self.assertAliased(torch.sort, paddle.compat.sort) self.assertAliased(torch.min, paddle.compat.min) self.assertAliased(torch.unique, paddle.compat.unique) @@ -437,6 +436,31 @@ 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_level2_only(self): + paddle.enable_compat(level=2) + try: + self._drop_torch_modules() + import torch + + self.assertFalse(hasattr(paddle, "slogdet")) + self.assertIs(torch.slogdet, paddle.compat.slogdet) + result = torch.slogdet(paddle.eye(2)) + self.assertEqual(result.sign.item(), 1.0) + self.assertEqual(result.logabsdet.item(), 0.0) + finally: + self._drop_torch_modules() + paddle.disable_compat() + + paddle.enable_compat() + try: + self._drop_torch_modules() + import torch + + self.assertFalse(hasattr(torch, "slogdet")) + finally: + self._drop_torch_modules() + paddle.disable_compat() + def test_scoped_enable_survives_scoped_module_import(self): """A scoped torch proxy and the level-2 paddle aliases are independent.""" mod = "torch_proxy_root_api_module" From 8f0cb7686319436453a1c4c89e55848f270cf51d Mon Sep 17 00:00:00 2001 From: manfredss Date: Sun, 19 Jul 2026 16:24:25 +0000 Subject: [PATCH 5/9] further simplify the logic --- python/paddle/compat/api_dispatch.py | 36 +++++++------- python/paddle/compat/proxy.py | 8 +--- test/compat/test_compat_namespace_aliased.py | 49 -------------------- 3 files changed, 18 insertions(+), 75 deletions(-) diff --git a/python/paddle/compat/api_dispatch.py b/python/paddle/compat/api_dispatch.py index 3c42a9ed6b7c2..5751a6e09d0d8 100644 --- a/python/paddle/compat/api_dispatch.py +++ b/python/paddle/compat/api_dispatch.py @@ -38,13 +38,6 @@ # symbol, so ``disable_compat()`` can restore the paddle namespace. _PADDLE_NAMESPACE_SAVED: dict[tuple[types.ModuleType, str], Any] = {} -_COMPAT_ENABLED = False - - -def is_compat_api_enabled() -> bool: - """Whether ``paddle.*`` currently dispatches to compat APIs.""" - return _COMPAT_ENABLED - def _caller_is_paddle_internal() -> bool: """True when the caller of the dispatched ``paddle.*`` API is a ``paddle`` @@ -68,7 +61,10 @@ def dispatch_compat_api(compat_fn: Any) -> Any: def decorator(native_fn: Any) -> Any: @wraps(native_fn) def dispatcher(*args: Any, **kwargs: Any) -> Any: - if is_compat_api_enabled() and not _caller_is_paddle_internal(): + if ( + len(_PADDLE_NAMESPACE_SAVED) > 0 + and not _caller_is_paddle_internal() + ): return compat_fn(*args, **kwargs) return native_fn(*args, **kwargs) @@ -81,19 +77,23 @@ def dispatcher(*args: Any, **kwargs: Any) -> Any: def _iter_compat_modules() -> Generator[types.ModuleType, None, None]: - """Yield every ``paddle.compat`` (sub)module, root package included. + """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 - yield paddle.compat + if hasattr(paddle.compat, "__all__"): + yield paddle.compat for module_info in pkgutil.walk_packages( paddle.compat.__path__, paddle.compat.__name__ + ".", ): - yield importlib.import_module(module_info.name) + compat_module = importlib.import_module(module_info.name) + if not hasattr(compat_module, "__all__"): + continue + yield compat_module def _make_caller_aware_class_proxy(native_cls: type, compat_cls: type) -> type: @@ -102,7 +102,10 @@ def _make_caller_aware_class_proxy(native_cls: type, compat_cls: type) -> type: class _CompatAwareMeta(type(compat_cls)): def __call__(cls, *args: Any, **kwargs: Any) -> Any: if cls is proxy: - if is_compat_api_enabled() and not _caller_is_paddle_internal(): + if ( + len(_PADDLE_NAMESPACE_SAVED) > 0 + and not _caller_is_paddle_internal() + ): return compat_cls(*args, **kwargs) return native_cls(*args, **kwargs) return super().__call__(*args, **kwargs) @@ -141,7 +144,7 @@ def _patch_tensor_methods() -> None: for attr_name in getattr(compat_root, "__all__", ()): native_method = getattr(paddle.Tensor, attr_name, None) - if native_method is None or not callable(native_method): + if native_method is None: continue compat_fn = getattr(compat_root, attr_name) _PADDLE_NAMESPACE_SAVED[(paddle.Tensor, attr_name)] = native_method @@ -158,8 +161,6 @@ def _apply_paddle_namespace_aliases() -> None: if _PADDLE_NAMESPACE_SAVED: return - global _COMPAT_ENABLED - for compat_module in _iter_compat_modules(): target_name = compat_module.__name__.replace( "paddle.compat", "paddle", 1 @@ -168,7 +169,7 @@ def _apply_paddle_namespace_aliases() -> None: target_module = importlib.import_module(target_name) except ModuleNotFoundError: continue - for attr_name in getattr(compat_module, "__all__", ()): + for attr_name in compat_module.__all__: compat_attr = getattr(compat_module, attr_name) current = getattr(target_module, attr_name, None) if current is None or current is compat_attr: @@ -187,13 +188,10 @@ def _apply_paddle_namespace_aliases() -> None: dispatch_compat_api(compat_attr)(current), ) _patch_tensor_methods() - _COMPAT_ENABLED = True def _restore_paddle_namespace_aliases() -> None: """Undo :func:`_apply_paddle_namespace_aliases`, restoring the paddle namespace.""" - global _COMPAT_ENABLED for (target_module, attr_name), original in _PADDLE_NAMESPACE_SAVED.items(): setattr(target_module, attr_name, original) _PADDLE_NAMESPACE_SAVED.clear() - _COMPAT_ENABLED = False diff --git a/python/paddle/compat/proxy.py b/python/paddle/compat/proxy.py index 586eb4e12deac..1fcc4e69fe9a9 100644 --- a/python/paddle/compat/proxy.py +++ b/python/paddle/compat/proxy.py @@ -30,7 +30,6 @@ _PADDLE_NAMESPACE_SAVED, _apply_paddle_namespace_aliases, _restore_paddle_namespace_aliases, - is_compat_api_enabled, ) if TYPE_CHECKING: @@ -361,7 +360,7 @@ def _find_spec_for_torch_module(self, fullname: str): for k, v in GLOBAL_OVERRIDES.items() if k.startswith(f"{fullname}.") } - if fullname == "torch" and is_compat_api_enabled(): + if fullname == "torch" and len(_PADDLE_NAMESPACE_SAVED) > 0: compat_root = importlib.import_module("paddle.compat") overrides.update( { @@ -631,7 +630,6 @@ 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 - restore_namespace_aliases = bool(_PADDLE_NAMESPACE_SAVED) if enable == already_has_torch_proxy and ( (original_globally_enabled and scope is None) or (original_local_enabled_scope == (scope or set())) @@ -648,8 +646,6 @@ def use_compat_guard( ) TORCH_PROXY_FINDER._globally_enabled = original_globally_enabled disable_compat() - if restore_namespace_aliases: - _apply_paddle_namespace_aliases() else: disable_compat() try: @@ -660,8 +656,6 @@ def use_compat_guard( original_local_enabled_scope ) TORCH_PROXY_FINDER._globally_enabled = original_globally_enabled - if restore_namespace_aliases: - _apply_paddle_namespace_aliases() def extend_torch_proxy_blocked_modules(modules: Iterable[str]) -> None: diff --git a/test/compat/test_compat_namespace_aliased.py b/test/compat/test_compat_namespace_aliased.py index 15bc1f9fbc90c..43f455536b092 100644 --- a/test/compat/test_compat_namespace_aliased.py +++ b/test/compat/test_compat_namespace_aliased.py @@ -351,18 +351,6 @@ def test_registry_empty_after_disable(self): paddle.disable_compat() self.assertEqual(len(_PADDLE_NAMESPACE_SAVED), 0) - def test_use_compat_guard_nested(self): - native_sort = self._native[(paddle, "sort")] - paddle.enable_compat(level=2) - try: - self.assertAliased(paddle.sort, paddle.compat.sort) - with paddle.use_compat_guard(enable=False): - self.assertIs(paddle.sort, native_sort) - self.assertAliased(paddle.sort, paddle.compat.sort) - finally: - paddle.disable_compat() - self.assertIs(paddle.sort, native_sort) - def test_bare_guard_keeps_level2_alias(self): t = paddle.to_tensor([[3.0, 1.0, 2.0]]) paddle.enable_compat(level=2) @@ -377,20 +365,6 @@ def test_bare_guard_keeps_level2_alias(self): paddle.disable_compat() self.assertNativeRestored() - def test_scoped_guard_restores_level2_alias(self): - native_sort = self._native[(paddle, "sort")] - paddle.enable_compat(scope={"triton"}, level=2, silent=True) - try: - self.assertAliased(paddle.sort, paddle.compat.sort) - with paddle.use_compat_guard(enable=False): - self.assertIs(paddle.sort, native_sort) - self.assertFalse(TORCH_PROXY_FINDER._globally_enabled) - self.assertAliased(paddle.sort, paddle.compat.sort) - self.assertGreater(len(_PADDLE_NAMESPACE_SAVED), 0) - finally: - paddle.disable_compat() - self.assertNativeRestored() - class TestTorchSurfaceUnderCompat(CompatNamespaceAliasBase): """torch.* still reaches the compat implementations: at level=2 the paddle.* @@ -461,29 +435,6 @@ def test_root_compat_only_api_is_level2_only(self): self._drop_torch_modules() paddle.disable_compat() - def test_scoped_enable_survives_scoped_module_import(self): - """A scoped torch proxy and the level-2 paddle aliases are independent.""" - mod = "torch_proxy_root_api_module" - sys.modules.pop(mod, None) - self._drop_torch_modules() - paddle.enable_compat(scope=mod, level=2, silent=True) - try: - import torch_proxy_root_api_module # noqa: F401 (imports torch inside) - - self.assertAliased(paddle.sort, paddle.compat.sort) - self.assertIn(TORCH_PROXY_FINDER, sys.meta_path) - self.assertFalse(TORCH_PROXY_FINDER._globally_enabled) - self.assertGreater(len(_PADDLE_NAMESPACE_SAVED), 0) - sys.modules.pop(mod, None) - self._drop_torch_modules() - __import__(mod) - finally: - sys.modules.pop(mod, None) - self._drop_torch_modules() - TORCH_PROXY_FINDER._globally_enabled = False - TORCH_PROXY_FINDER._local_enabled_scope = set() - paddle.disable_compat() - class TestLevel2InternalCallersUseNative(CompatNamespaceAliasBase): """level=2 redirects ``paddle.*`` to the torch-aligned compat APIs for the From ae3ae4a817d6fb37c431cbc19457c9aa044c7818 Mon Sep 17 00:00:00 2001 From: manfredss Date: Mon, 20 Jul 2026 07:43:49 +0000 Subject: [PATCH 6/9] add metaclass for class Linear; remove the __instancecheck__ and __subclasscheck__ in api_dispatch.py --- python/paddle/compat/api_dispatch.py | 8 -------- python/paddle/compat/nn/__init__.py | 17 ++++++++++++++++- test/compat/test_compat_namespace_aliased.py | 10 ++++++---- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/python/paddle/compat/api_dispatch.py b/python/paddle/compat/api_dispatch.py index 5751a6e09d0d8..2378fcd24dc3a 100644 --- a/python/paddle/compat/api_dispatch.py +++ b/python/paddle/compat/api_dispatch.py @@ -110,14 +110,6 @@ def __call__(cls, *args: Any, **kwargs: Any) -> Any: return native_cls(*args, **kwargs) return super().__call__(*args, **kwargs) - def __instancecheck__(cls, instance: Any) -> bool: - return isinstance(instance, (native_cls, compat_cls)) - - def __subclasscheck__(cls, subclass: Any) -> bool: - return issubclass(subclass, native_cls) or issubclass( - subclass, compat_cls - ) - proxy = _CompatAwareMeta( native_cls.__name__, (compat_cls,), diff --git a/python/paddle/compat/nn/__init__.py b/python/paddle/compat/nn/__init__.py index 2b8e797e6dd3d..4ccff2590cbc8 100644 --- a/python/paddle/compat/nn/__init__.py +++ b/python/paddle/compat/nn/__init__.py @@ -547,7 +547,22 @@ def to_list_if_necessary(x): ) -class Linear(nn.Layer): +_NATIVE_LINEAR = nn.Linear + + +class _LinearMeta(type): + def __instancecheck__(cls, instance: object) -> bool: + if cls is Linear and isinstance(instance, _NATIVE_LINEAR): + return True + return super().__instancecheck__(instance) + + def __subclasscheck__(cls, subclass: type) -> bool: + if cls is Linear and issubclass(subclass, _NATIVE_LINEAR): + return True + return super().__subclasscheck__(subclass) + + +class Linear(nn.Layer, metaclass=_LinearMeta): r""" Python compatible fully-connected linear transformation layer. For each input :math:`X` , diff --git a/test/compat/test_compat_namespace_aliased.py b/test/compat/test_compat_namespace_aliased.py index 43f455536b092..6b34b18aa0838 100644 --- a/test/compat/test_compat_namespace_aliased.py +++ b/test/compat/test_compat_namespace_aliased.py @@ -161,6 +161,11 @@ def test_level1_default_does_not_alias(self): paddle.disable_compat() self.assertNativeRestored() + def test_linear_type_compatibility_without_alias(self): + native = paddle.nn.Linear(2, 2) + self.assertIsInstance(native, paddle.compat.nn.Linear) + self.assertTrue(issubclass(paddle.nn.Linear, paddle.compat.nn.Linear)) + @with_level2 def test_submodule_symbols_aliased(self): self.assertAliased(paddle.nn.Linear, paddle.compat.nn.Linear) @@ -487,7 +492,7 @@ def test_tensor_methods_caller_aware(self): def test_aliased_class_caller_aware(self): """Existing classes (Linear/...) become caller-aware proxies: external callers get the torch-aligned compat class, paddle-internal callers get - native; isinstance/issubclass accept either form.""" + native.""" # external (this module) -> compat class (torch-style) self.assertIs(type(paddle.nn.Linear(2, 2)), paddle.compat.nn.Linear) with self.assertRaises(TypeError): @@ -496,9 +501,6 @@ def test_aliased_class_caller_aware(self): ns = {"__name__": "paddle.fake_internal", "paddle": paddle} exec("obj = paddle.nn.Linear(2, 2, weight_attr=False)", ns) self.assertIsNot(type(ns["obj"]), paddle.compat.nn.Linear) - # isinstance / issubclass accept both forms - self.assertIsInstance(paddle.nn.Linear(2, 2), paddle.nn.Linear) - self.assertTrue(issubclass(paddle.compat.nn.Linear, paddle.nn.Linear)) @with_level2 def test_aliased_class_subclassing_is_torch_style(self): From 10bd361e7e7e7b0851ee261f2d861852aaaf1b94 Mon Sep 17 00:00:00 2001 From: manfredss Date: Mon, 20 Jul 2026 12:10:33 +0000 Subject: [PATCH 7/9] implement general metaclass --- .../compat/distributions/categorical.py | 12 +++- python/paddle/compat/nn/__init__.py | 48 ++++++---------- python/paddle/compat/nn/transformer.py | 12 +++- python/paddle/compat/utils.py | 34 ++++++++++++ test/compat/test_compat_namespace_aliased.py | 55 +++++++++++++++++-- 5 files changed, 120 insertions(+), 41 deletions(-) diff --git a/python/paddle/compat/distributions/categorical.py b/python/paddle/compat/distributions/categorical.py index 7836595d1326d..910aba15f3134 100644 --- a/python/paddle/compat/distributions/categorical.py +++ b/python/paddle/compat/distributions/categorical.py @@ -20,8 +20,10 @@ from paddle.distribution import constraint, distribution from paddle.tensor import multinomial +from ..utils import _CompatClassMeta -class Categorical(distribution.Distribution): + +class Categorical(distribution.Distribution, metaclass=_CompatClassMeta): arg_constraints = { "probs": constraint.simplex, "logits": constraint.real_vector, @@ -61,7 +63,9 @@ def __init__( batch_shape = ( tuple(self._param.shape[:-1]) if self._param.dim() > 1 else () ) - super().__init__(batch_shape, validate_args=validate_args) + distribution.Distribution.__init__( + self, batch_shape, validate_args=validate_args + ) def expand(self, batch_shape, _instance=None): new = ( @@ -81,7 +85,9 @@ def expand(self, batch_shape, _instance=None): ) new._param = new._logits if new._logits is not None else new._probs new._num_events = self._num_events - super(Categorical, new).__init__(batch_shape, validate_args=False) + distribution.Distribution.__init__( + new, batch_shape, validate_args=False + ) new._validate_args_enabled = self._validate_args_enabled return new diff --git a/python/paddle/compat/nn/__init__.py b/python/paddle/compat/nn/__init__.py index 4ccff2590cbc8..e401845d96c7b 100644 --- a/python/paddle/compat/nn/__init__.py +++ b/python/paddle/compat/nn/__init__.py @@ -23,6 +23,7 @@ from paddle.nn.modules.utils import _single from paddle.utils.decorator_utils import ForbidKeywordsDecorator +from ..utils import _CompatClassMeta from . import functional from .transformer import MultiheadAttention @@ -58,7 +59,7 @@ ] -class BatchNorm1D(nn.BatchNorm1D): +class BatchNorm1D(nn.BatchNorm1D, metaclass=_CompatClassMeta): def __init__( self, num_features: int, @@ -85,7 +86,7 @@ def __init__( self.momentum = momentum -class BatchNorm2D(nn.BatchNorm2D): +class BatchNorm2D(nn.BatchNorm2D, metaclass=_CompatClassMeta): def __init__( self, num_features: int, @@ -112,7 +113,7 @@ def __init__( self.momentum = momentum -class BatchNorm3D(nn.BatchNorm3D): +class BatchNorm3D(nn.BatchNorm3D, metaclass=_CompatClassMeta): def __init__( self, num_features: int, @@ -144,7 +145,7 @@ def __init__( BatchNorm3d = BatchNorm3D -class AvgPool1D(nn.Layer): +class AvgPool1D(nn.Layer, metaclass=_CompatClassMeta): r""" This operation applies a 1D average pooling over an input signal composed of several input planes, based on the input, output_size, return_mask parameters. @@ -227,7 +228,7 @@ def __init__( ceil_mode: bool = False, count_include_pad: bool = True, ) -> None: - super().__init__() + nn.Layer.__init__(self) self.kernel_size = _single(kernel_size) self.stride = _single(stride if stride is not None else kernel_size) self.padding = _single(padding) @@ -248,7 +249,7 @@ def extra_repr(self) -> str: return f"kernel_size={self.kernel_size}, stride={self.stride}, padding={self.padding}" -class AvgPool2D(nn.Layer): +class AvgPool2D(nn.Layer, metaclass=_CompatClassMeta): r""" This operation applies 2D average pooling over input features based on the input, and kernel_size, stride, padding parameters. Input(X) and Output(Out) are @@ -345,7 +346,7 @@ def __init__( count_include_pad: bool = True, divisor_override: int | None = None, ): - super().__init__() + nn.Layer.__init__(self) self.kernel_size = kernel_size self.stride = stride if (stride is not None) else kernel_size self.padding = padding @@ -368,7 +369,7 @@ def extra_repr(self) -> str: return f"kernel_size={self.kernel_size}, stride={self.stride}, padding={self.padding}" -class AvgPool3D(nn.Layer): +class AvgPool3D(nn.Layer, metaclass=_CompatClassMeta): """ This operation applies 3D max pooling over input features based on the input, @@ -452,7 +453,7 @@ def __init__( count_include_pad: bool = True, divisor_override: int | None = None, ) -> None: - super().__init__() + nn.Layer.__init__(self) self.kernel_size = kernel_size self.stride = stride if (stride is not None) else kernel_size self.padding = padding @@ -481,7 +482,7 @@ def __setstate__(self, state): self.__dict__.setdefault("count_include_pad", True) -class Unfold(nn.Unfold): +class Unfold(nn.Unfold, metaclass=_CompatClassMeta): """ A compatible version of paddle.nn.Unfold: @@ -547,22 +548,7 @@ def to_list_if_necessary(x): ) -_NATIVE_LINEAR = nn.Linear - - -class _LinearMeta(type): - def __instancecheck__(cls, instance: object) -> bool: - if cls is Linear and isinstance(instance, _NATIVE_LINEAR): - return True - return super().__instancecheck__(instance) - - def __subclasscheck__(cls, subclass: type) -> bool: - if cls is Linear and issubclass(subclass, _NATIVE_LINEAR): - return True - return super().__subclasscheck__(subclass) - - -class Linear(nn.Layer, metaclass=_LinearMeta): +class Linear(nn.Layer, metaclass=_CompatClassMeta): r""" Python compatible fully-connected linear transformation layer. For each input :math:`X` , @@ -654,7 +640,7 @@ def __init__( device: PlaceLike | None = None, dtype: DTypeLike | None = None, ) -> None: - super().__init__() + nn.Layer.__init__(self) self._dtype = ( self._helper.get_default_dtype() if dtype is None else dtype ) @@ -702,7 +688,7 @@ def reset_parameters(self) -> None: nn.init.uniform_(self.bias, -bound, bound) -class Softmax(nn.Layer): +class Softmax(nn.Layer, metaclass=_CompatClassMeta): r""" Softmax Activation. @@ -829,7 +815,7 @@ class Softmax(nn.Layer): correct_name="paddle.nn.Softmax", ) def __init__(self, dim: int | None = None) -> None: - super().__init__() + nn.Layer.__init__(self) self._dim = dim self._dtype = None @@ -840,7 +826,7 @@ def extra_repr(self) -> str: return f"dim={self._dim}" -class SmoothL1Loss(nn.Layer): +class SmoothL1Loss(nn.Layer, metaclass=_CompatClassMeta): r""" PyTorch compatible version of :ref:`api_paddle_nn_SmoothL1Loss`, aligned with @@ -905,7 +891,7 @@ def __init__( reduction: str = 'mean', beta: float = 1.0, ) -> None: - super().__init__() + nn.Layer.__init__(self) if size_average is not None or reduce is not None: reduction = ( 'none' diff --git a/python/paddle/compat/nn/transformer.py b/python/paddle/compat/nn/transformer.py index 222a954e761eb..61576dc06fd9e 100644 --- a/python/paddle/compat/nn/transformer.py +++ b/python/paddle/compat/nn/transformer.py @@ -21,12 +21,18 @@ from paddle import nn from paddle.nn.initializer import XavierNormal, XavierUniform +from ..utils import _CompatClassMeta + if TYPE_CHECKING: from paddle import Tensor from paddle._typing import DTypeLike, PlaceLike -class MultiheadAttention(nn.Layer): +class MultiheadAttention( + nn.Layer, + metaclass=_CompatClassMeta, + native_cls=nn.MultiHeadAttention, +): r""" Allows the model to jointly attend to information from different representation subspaces. @@ -100,9 +106,9 @@ def __init__( dtype: DTypeLike | None = None, ) -> None: if dtype: - super().__init__(dtype=dtype) + nn.Layer.__init__(self, dtype=dtype) else: - super().__init__() + nn.Layer.__init__(self) self.embed_dim = embed_dim self.kdim = kdim if kdim is not None else embed_dim self.vdim = vdim if vdim is not None else embed_dim diff --git a/python/paddle/compat/utils.py b/python/paddle/compat/utils.py index e909473a9ef54..2b68aeb789120 100644 --- a/python/paddle/compat/utils.py +++ b/python/paddle/compat/utils.py @@ -21,6 +21,40 @@ ) +class _CompatClassMeta(type): + """Keep compat classes recognizable as native classes across dispatch.""" + + def __new__(mcls, name, bases, namespace, *, native_cls=None, **kwargs): + if native_cls is None: + module_parts = namespace.get('__module__', '').split('.') + if ( + module_parts[:2] == ['paddle', 'compat'] + and len(module_parts) > 2 + ): + native_module = getattr(paddle, module_parts[2], None) + native_cls = getattr(native_module, name, None) + if isinstance(native_cls, type) and not any( + issubclass(base, native_cls) for base in bases + ): + bases = tuple( + native_cls if issubclass(native_cls, base) else base + for base in bases + ) + return super().__new__(mcls, name, bases, namespace, **kwargs) + + def __instancecheck__(cls, instance: object) -> bool: + native_cls = cls.__dict__.get('__native_cls__') + if native_cls is not None: + return isinstance(instance, native_cls) + return super().__instancecheck__(instance) + + def __subclasscheck__(cls, subclass: type) -> bool: + native_cls = cls.__dict__.get('__native_cls__') + if native_cls is not None: + return issubclass(subclass, native_cls) + return super().__subclasscheck__(subclass) + + def _check_out_status( out: Tensor | tuple[Tensor, Tensor] | list[Tensor], expect_multiple: bool = False, diff --git a/test/compat/test_compat_namespace_aliased.py b/test/compat/test_compat_namespace_aliased.py index 6b34b18aa0838..de1dee346d917 100644 --- a/test/compat/test_compat_namespace_aliased.py +++ b/test/compat/test_compat_namespace_aliased.py @@ -161,10 +161,54 @@ def test_level1_default_does_not_alias(self): paddle.disable_compat() self.assertNativeRestored() - def test_linear_type_compatibility_without_alias(self): - native = paddle.nn.Linear(2, 2) - self.assertIsInstance(native, paddle.compat.nn.Linear) - self.assertTrue(issubclass(paddle.nn.Linear, paddle.compat.nn.Linear)) + def test_class_type_compatibility_without_alias(self): + constructor_args = { + "Unfold": (1,), + "Linear": (2, 2), + "Softmax": (), + "AvgPool1D": (1,), + "AvgPool2D": (1,), + "AvgPool3D": (1,), + "BatchNorm1D": (2,), + "BatchNorm2D": (2,), + "BatchNorm3D": (2,), + "SmoothL1Loss": (), + "MultiheadAttention": (4, 1), + "Categorical": (paddle.to_tensor([0.5, 0.5]),), + } + native_classes = { + "Unfold": paddle.nn.Unfold, + "Linear": paddle.nn.Linear, + "Softmax": paddle.nn.Softmax, + "AvgPool1D": paddle.nn.AvgPool1D, + "AvgPool2D": paddle.nn.AvgPool2D, + "AvgPool3D": paddle.nn.AvgPool3D, + "BatchNorm1D": paddle.nn.BatchNorm1D, + "BatchNorm2D": paddle.nn.BatchNorm2D, + "BatchNorm3D": paddle.nn.BatchNorm3D, + "SmoothL1Loss": paddle.nn.SmoothL1Loss, + "MultiheadAttention": paddle.nn.MultiHeadAttention, + "Categorical": paddle.distributions.Categorical, + } + compat_modules = (paddle.compat.nn, paddle.compat.distributions) + compat_classes = { + getattr(module, name) + for module in compat_modules + for name in module.__all__ + if isinstance(getattr(module, name), type) + } + self.assertEqual( + {compat_cls.__name__ for compat_cls in compat_classes}, + set(native_classes), + ) + + for compat_cls in compat_classes: + name = compat_cls.__name__ + with self.subTest(name=name): + native_cls = native_classes[name] + compat = compat_cls(*constructor_args[name]) + self.assertIsInstance(compat, native_cls) + self.assertTrue(issubclass(compat_cls, native_cls)) @with_level2 def test_submodule_symbols_aliased(self): @@ -501,6 +545,9 @@ def test_aliased_class_caller_aware(self): ns = {"__name__": "paddle.fake_internal", "paddle": paddle} exec("obj = paddle.nn.Linear(2, 2, weight_attr=False)", ns) self.assertIsNot(type(ns["obj"]), paddle.compat.nn.Linear) + # isinstance / issubclass accept both forms + self.assertIsInstance(paddle.nn.Linear(2, 2), paddle.nn.Linear) + self.assertTrue(issubclass(paddle.compat.nn.Linear, paddle.nn.Linear)) @with_level2 def test_aliased_class_subclassing_is_torch_style(self): From 99007704ed5359ac15a1d341434109f14fcf3c27 Mon Sep 17 00:00:00 2001 From: manfredss Date: Tue, 21 Jul 2026 02:43:17 +0000 Subject: [PATCH 8/9] use a clearer naming, add test coverage --- python/paddle/compat/api_dispatch.py | 10 +++++----- test/compat/test_compat_namespace_aliased.py | 2 ++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/python/paddle/compat/api_dispatch.py b/python/paddle/compat/api_dispatch.py index 2378fcd24dc3a..848506dadbdd4 100644 --- a/python/paddle/compat/api_dispatch.py +++ b/python/paddle/compat/api_dispatch.py @@ -51,7 +51,7 @@ def _caller_is_paddle_internal() -> bool: return name == "paddle" or name.startswith("paddle.") -def dispatch_compat_api(compat_fn: Any) -> Any: +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 @@ -96,7 +96,7 @@ def _iter_compat_modules() -> Generator[types.ModuleType, None, None]: yield compat_module -def _make_caller_aware_class_proxy(native_cls: type, compat_cls: type) -> type: +def dispatch_class(native_cls: type, compat_cls: type) -> type: """Create a class proxy that selects compat only for external callers.""" class _CompatAwareMeta(type(compat_cls)): @@ -143,7 +143,7 @@ def _patch_tensor_methods() -> None: setattr( paddle.Tensor, attr_name, - dispatch_compat_api(compat_fn)(native_method), + dispatch_function(compat_fn)(native_method), ) @@ -171,13 +171,13 @@ def _apply_paddle_namespace_aliases() -> None: setattr( target_module, attr_name, - _make_caller_aware_class_proxy(current, compat_attr), + dispatch_class(current, compat_attr), ) else: setattr( target_module, attr_name, - dispatch_compat_api(compat_attr)(current), + dispatch_function(compat_attr)(current), ) _patch_tensor_methods() diff --git a/test/compat/test_compat_namespace_aliased.py b/test/compat/test_compat_namespace_aliased.py index de1dee346d917..173291e9d2e1d 100644 --- a/test/compat/test_compat_namespace_aliased.py +++ b/test/compat/test_compat_namespace_aliased.py @@ -209,6 +209,8 @@ def test_class_type_compatibility_without_alias(self): compat = compat_cls(*constructor_args[name]) self.assertIsInstance(compat, native_cls) self.assertTrue(issubclass(compat_cls, native_cls)) + native = native_cls(*constructor_args[name]) + self.assertNotIsInstance(native, compat_cls) @with_level2 def test_submodule_symbols_aliased(self): From 4e6b097953b7c50713a90d3eb01810be27b0a066 Mon Sep 17 00:00:00 2001 From: manfredss Date: Tue, 21 Jul 2026 14:04:40 +0000 Subject: [PATCH 9/9] root include paddle.compat --- python/paddle/compat/proxy.py | 46 ++++----------- test/compat/test_compat_namespace_aliased.py | 60 ++++++++------------ 2 files changed, 34 insertions(+), 72 deletions(-) diff --git a/python/paddle/compat/proxy.py b/python/paddle/compat/proxy.py index 1fcc4e69fe9a9..22b54cc51ca47 100644 --- a/python/paddle/compat/proxy.py +++ b/python/paddle/compat/proxy.py @@ -18,7 +18,6 @@ import importlib.abc import importlib.util import inspect -import pkgutil import sys import types import warnings @@ -27,8 +26,8 @@ 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, ) @@ -153,31 +152,17 @@ def _extend_torch_proxy_overrides( @cache def _register_compat_override(): - import paddle.compat - - PADDLE_PREFIX = "paddle.compat" - TORCH_PREFIX = "torch" - PUBLIC_ATTR_DECLARATION = "__all__" - compat_overrides = {} - for module_info in pkgutil.walk_packages( - paddle.compat.__path__, - paddle.compat.__name__ + ".", - ): - module = importlib.import_module(module_info.name) - if hasattr(module, PUBLIC_ATTR_DECLARATION): - public_attrs = getattr(module, PUBLIC_ATTR_DECLARATION) - torch_module_name = module_info.name.replace( - PADDLE_PREFIX, TORCH_PREFIX, 1 + for module in _iter_compat_modules(): + torch_module_name = module.__name__.replace("paddle.compat", "torch", 1) + for attr_name in module.__all__: + if attr_name.startswith("_"): + continue + paddle_attr = getattr(module, attr_name) + torch_attr_name = f"{torch_module_name}.{attr_name}" + compat_overrides[torch_attr_name] = RawOverriddenAttribute( + paddle_attr ) - for attr_name in public_attrs: - if attr_name.startswith("_"): - continue - paddle_attr = getattr(module, attr_name) - torch_attr_name = f"{torch_module_name}.{attr_name}" - compat_overrides[torch_attr_name] = RawOverriddenAttribute( - paddle_attr - ) _extend_torch_proxy_overrides(compat_overrides) @@ -360,17 +345,6 @@ def _find_spec_for_torch_module(self, fullname: str): for k, v in GLOBAL_OVERRIDES.items() if k.startswith(f"{fullname}.") } - if fullname == "torch" and len(_PADDLE_NAMESPACE_SAVED) > 0: - compat_root = importlib.import_module("paddle.compat") - overrides.update( - { - attr_name: RawOverriddenAttribute( - getattr(compat_root, attr_name) - ) - for attr_name in getattr(compat_root, "__all__", ()) - if not hasattr(source_module, attr_name) - } - ) is_pkg = hasattr(source_module, "__path__") diff --git a/test/compat/test_compat_namespace_aliased.py b/test/compat/test_compat_namespace_aliased.py index 173291e9d2e1d..f60cc1cb86950 100644 --- a/test/compat/test_compat_namespace_aliased.py +++ b/test/compat/test_compat_namespace_aliased.py @@ -418,9 +418,7 @@ def test_bare_guard_keeps_level2_alias(self): class TestTorchSurfaceUnderCompat(CompatNamespaceAliasBase): - """torch.* still reaches the compat implementations: at level=2 the paddle.* - alias carries through the torch proxy (torch.sort -> paddle.sort -> - paddle.compat.sort).""" + """torch.* reaches the public compat implementations at both levels.""" @staticmethod def _drop_torch_modules(): @@ -431,15 +429,15 @@ def _drop_torch_modules(): ]: del sys.modules[name] - def test_level1_root_torch_apis_keep_native_mapping(self): + def test_level1_root_torch_apis_resolve_to_compat(self): paddle.enable_compat() try: self._drop_torch_modules() import torch - self.assertIs(torch.sort, self._native[(paddle, "sort")]) - self.assertIs(torch.min, self._native[(paddle, "min")]) - self.assertIs(torch.unique, self._native[(paddle, "unique")]) + self.assertIs(torch.sort, paddle.compat.sort) + self.assertIs(torch.min, paddle.compat.min) + self.assertIs(torch.unique, paddle.compat.unique) finally: self._drop_torch_modules() paddle.disable_compat() @@ -450,41 +448,31 @@ def test_root_torch_apis_resolve_to_compat_at_level2(self): self._drop_torch_modules() import torch - # Root functions with native Paddle counterparts carry through the - # paddle.* aliases: torch.sort -> paddle.sort -> paddle.compat.sort. - self.assertAliased(torch.sort, paddle.compat.sort) - self.assertAliased(torch.min, paddle.compat.min) - self.assertAliased(torch.unique, paddle.compat.unique) + self.assertIs(torch.sort, paddle.compat.sort) + self.assertIs(torch.min, paddle.compat.min) + self.assertIs(torch.unique, paddle.compat.unique) # nn.* overrides reach compat regardless of the alias. self.assertIs(torch.nn.Linear, paddle.compat.nn.Linear) finally: self._drop_torch_modules() paddle.disable_compat() - def test_root_compat_only_api_is_level2_only(self): - paddle.enable_compat(level=2) - try: - self._drop_torch_modules() - import torch - - self.assertFalse(hasattr(paddle, "slogdet")) - self.assertIs(torch.slogdet, paddle.compat.slogdet) - result = torch.slogdet(paddle.eye(2)) - self.assertEqual(result.sign.item(), 1.0) - self.assertEqual(result.logabsdet.item(), 0.0) - finally: - self._drop_torch_modules() - paddle.disable_compat() - - paddle.enable_compat() - try: - self._drop_torch_modules() - import torch - - self.assertFalse(hasattr(torch, "slogdet")) - finally: - self._drop_torch_modules() - paddle.disable_compat() + def test_root_compat_only_api_is_registered_at_both_levels(self): + for level in (1, 2): + with self.subTest(level=level): + paddle.enable_compat(level=level) + try: + self._drop_torch_modules() + import torch + + self.assertFalse(hasattr(paddle, "slogdet")) + self.assertIs(torch.slogdet, paddle.compat.slogdet) + result = torch.slogdet(paddle.eye(2)) + self.assertEqual(result.sign.item(), 1.0) + self.assertEqual(result.logabsdet.item(), 0.0) + finally: + self._drop_torch_modules() + paddle.disable_compat() class TestLevel2InternalCallersUseNative(CompatNamespaceAliasBase):