Skip to content
189 changes: 189 additions & 0 deletions python/paddle/compat/api_dispatch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
# 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 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


# (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] = {}


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_function(compat_fn: Any) -> Any:
"""Wrap a native ``paddle`` callable to route external callers to
``compat_fn`` while compat is enabled; paddle-internal callers and the
disabled state get the native callable. Installed only under
``enable_compat(level=2)``; ``disable_compat`` restores the originals,
so the default hot path is untouched."""

def decorator(native_fn: Any) -> Any:
@wraps(native_fn)
def dispatcher(*args: Any, **kwargs: Any) -> Any:
if (
len(_PADDLE_NAMESPACE_SAVED) > 0
and not _caller_is_paddle_internal()
):
return compat_fn(*args, **kwargs)
return native_fn(*args, **kwargs)

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 ``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__"):
yield paddle.compat
for module_info in pkgutil.walk_packages(
paddle.compat.__path__,
paddle.compat.__name__ + ".",
):
compat_module = importlib.import_module(module_info.name)
if not hasattr(compat_module, "__all__"):
continue
yield compat_module


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)):
def __call__(cls, *args: Any, **kwargs: Any) -> Any:
if cls is proxy:
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)
Comment on lines +103 to +111

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 优先级:P1
处理要求:请针对该评论修复并提交新的 commit。

这里删掉 proxy 自身的 __instancecheck__ / __subclasscheck__ 后,level=2 下的 class alias 会丢掉基本类型判定语义:paddle.nn.Linear 被替换成这个 proxy,但外部调用 paddle.nn.Linear(...) 实际返回的是 compat_cls(...),也就是 paddle.compat.nn.Linear 实例,而不是 proxy 实例。因此 isinstance(paddle.nn.Linear(2, 2), paddle.nn.Linear) 会变成 Falseissubclass(paddle.compat.nn.Linear, paddle.nn.Linear) 也会变成 False。这和 PR 描述里“Class APIs retain ... type-checking semantics”的目标不一致,也会影响依赖 isinstance(module, paddle.nn.Linear) 的迁移代码。

可以把类型判定保留在 proxy 这一层,但只对 cls is proxy 生效,避免影响用户从 proxy 派生出来的子类:

Suggested change
def __call__(cls, *args: Any, **kwargs: Any) -> Any:
if cls is proxy:
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)
def __call__(cls, *args: Any, **kwargs: Any) -> Any:
if cls is proxy:
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)
def __instancecheck__(cls, instance: Any) -> bool:
if cls is proxy:
return isinstance(instance, (native_cls, compat_cls))
return super().__instancecheck__(instance)
def __subclasscheck__(cls, subclass: Any) -> bool:
if cls is proxy:
return issubclass(subclass, native_cls) or issubclass(
subclass, compat_cls
)
return super().__subclasscheck__(subclass)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已在 /python/paddle/compat/utils.py 中使用 metaclass 的方式实现了

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

已确认,dispatch_class / _CompatClassMeta 已把 class 语义移到 python/paddle/compat/utils.py,并且新增的 test_class_type_compatibility_without_alias 也补了未启用 enable_compat(2) 时的 isinstance / issubclass 覆盖,这条 concern 现在看起来已经被当前提交覆盖。


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.<m>`` 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__", ()):
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),
)


def _apply_paddle_namespace_aliases() -> None:
"""Install caller-aware dispatchers/proxies for every public ``paddle.compat.*``
symbol that has a ``paddle.*`` counterpart, plus the Tensor methods."""
if _PADDLE_NAMESPACE_SAVED:
return

for compat_module in _iter_compat_modules():
target_name = compat_module.__name__.replace(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

如果没有__all__,直接continue,不要浪费时间import_module

"paddle.compat", "paddle", 1
)
try:
target_module = importlib.import_module(target_name)
except ModuleNotFoundError:
continue
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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 优先级:P1
处理要求:请针对该评论修复并提交新的 commit。

这里把 paddle.compat root 里没有 native paddle.* 对应项的符号跳过后,torch proxy 没有其它路径再暴露这些 root compat API。_register_compat_override() 目前只 walk_packages(paddle.compat.__path__),不会处理 root paddle.compat.__all__;而当前分支又不再创建 paddle.slogdet。结果是 paddle.compat.slogdet 仍存在,但 enable_compat(level=2); import torch; torch.slogdet 会继续从 root paddle proxy 查找,找不到对应属性,PyTorch 迁移代码里直接调用 torch.slogdet(...) 会回归为 AttributeError

如果维护者期望 compat-only 符号不出现在 paddle.*,请在 torch override 注册里显式覆盖 root paddle.compat,并补 torch.slogdet 这类 root compat-only API 的回归测试,例如:

def _iter_compat_override_modules():
    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)

for module in _iter_compat_override_modules():
    torch_module_name = module.__name__.replace("paddle.compat", "torch", 1)
    for attr_name in getattr(module, "__all__", ()): 
        compat_overrides[f"{torch_module_name}.{attr_name}"] = RawOverriddenAttribute(
            getattr(module, attr_name)
        )

@Manfredss Manfredss Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

把整个 root __all__ 注册到 GLOBAL_OVERRIDES,默认 level=1 的 torch.sort/min/unique 等也会从 native Paddle API 变成 compat API,违反“level=1 保持原规则”

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

对,这里不能把整个 root all 直接塞进 GLOBAL_OVERRIDES;那样确实会让 level=1 的 torch.sort/min/unique 语义回退。我的本意不是改 level=1 行为,而是补 compat-only root 符号在 level=2 下的可达性,但这需要单独的 level=2 专用路径,不能用这个办法。这里我把前一条建议收窄,不再要求改 GLOBAL_OVERRIDES。

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

已按你的提醒改成 level=2 专用路径:torch 根模块只在 enable_compat(level=2) 下补 root compat-only API,level=1 不会把 root __all__ 塞进全局覆盖表。torch.slogdet 的回归也已补测试,这个点已处理。

continue
_PADDLE_NAMESPACE_SAVED[(target_module, attr_name)] = current
if isinstance(compat_attr, type):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

回复下,这个分支是什么case?

@Manfredss Manfredss Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

这个分支处理 paddle.compat.nn.Linear 等 class 类的 API

class 除了可调用外,还需要保留继承和 isinstance / issubclass 这样的语义,故不能使用普通函数 dispatcher;这里使用 caller-aware metaclass,在实例化时让外部调用进入 compat class、paddle 内部调用进入 native class,同时保持例如 paddle.nn.Linear 仍然是合法的类型

setattr(
target_module,
attr_name,
dispatch_class(current, compat_attr),
)
else:
setattr(
target_module,
attr_name,
dispatch_function(compat_attr)(current),
)
_patch_tensor_methods()


def _restore_paddle_namespace_aliases() -> None:
"""Undo :func:`_apply_paddle_namespace_aliases`, restoring the paddle namespace."""
for (target_module, attr_name), original in _PADDLE_NAMESPACE_SAVED.items():
setattr(target_module, attr_name, original)
_PADDLE_NAMESPACE_SAVED.clear()
12 changes: 9 additions & 3 deletions python/paddle/compat/distributions/categorical.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 = (
Expand All @@ -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

Expand Down
33 changes: 17 additions & 16 deletions python/paddle/compat/nn/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -58,7 +59,7 @@
]


class BatchNorm1D(nn.BatchNorm1D):
class BatchNorm1D(nn.BatchNorm1D, metaclass=_CompatClassMeta):
def __init__(
self,
num_features: int,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -547,7 +548,7 @@ def to_list_if_necessary(x):
)


class Linear(nn.Layer):
class Linear(nn.Layer, metaclass=_CompatClassMeta):
r"""

Python compatible fully-connected linear transformation layer. For each input :math:`X` ,
Expand Down Expand Up @@ -639,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
)
Expand Down Expand Up @@ -687,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.

Expand Down Expand Up @@ -814,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

Expand All @@ -825,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
Expand Down Expand Up @@ -890,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'
Expand Down
Loading
Loading