-
Notifications
You must be signed in to change notification settings - Fork 6k
[API Compatibility] enhance paddle.enable_compat -part
#79391
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e68e206
2cea460
7ee7eea
22f97e4
8f0cb76
ae3ae4a
10bd361
5c4fde7
9900770
4e6b097
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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) | ||
|
|
||
| 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( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
这里把 如果维护者期望 compat-only 符号不出现在 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)
)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 保持原规则”
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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。
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 已按你的提醒改成 level=2 专用路径: |
||
| continue | ||
| _PADDLE_NAMESPACE_SAVED[(target_module, attr_name)] = current | ||
| if isinstance(compat_attr, type): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 回复下,这个分支是什么case?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 这个分支处理 class 除了可调用外,还需要保留继承和 |
||
| 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() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
处理要求:请针对该评论修复并提交新的 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)会变成False,issubclass(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 派生出来的子类:There was a problem hiding this comment.
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 的方式实现了
There was a problem hiding this comment.
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 现在看起来已经被当前提交覆盖。