一键导入
python-decorator
// 负责《Paddle API 对齐 PyTorch 项目》中 Step2:API 代码修改,实施 Python 装饰器的代码开发。通过 Python 装饰器,在 Python 层为 Paddle API 提供参数别名、参数顺序、参数类型和参数用法的兼容转换,实现 PyTorch 风格的 API 调用,并保持 Paddle API 的向后兼容性。
// 负责《Paddle API 对齐 PyTorch 项目》中 Step2:API 代码修改,实施 Python 装饰器的代码开发。通过 Python 装饰器,在 Python 层为 Paddle API 提供参数别名、参数顺序、参数类型和参数用法的兼容转换,实现 PyTorch 风格的 API 调用,并保持 Paddle API 的向后兼容性。
负责《Paddle API 对齐 PyTorch 项目》中 Step1:方案决策,分析 PyTorch API 与 Paddle API 之间的差异,制定合适的 API 改动方案
开展《Paddle API 对齐 PyTorch 项目》,负责项目整体统筹规划,调用多个 skill,完成输入的 API 对齐
负责《Paddle API 对齐 PyTorch 项目》中 Step4:API 文档修改,在 API 代码修改完成后,同步更新中文 API 文档,确保文档准确反映 API 的最新行为
负责《Paddle API 对齐 PyTorch 项目》中 Step2:API 代码修改,实施 C++下沉的代码开发。通过将 Python API 下沉至 C++层,可以减少 Python 装饰器带来的性能开销,提升 API 调度效率。
负责《Paddle API 对齐 PyTorch 项目》中 Step5:代码提交,分别对 Paddle、PaConvert、Docs 三个仓库创建或更新 Pull Request
负责《Paddle API 对齐 PyTorch 项目》中 Step3:Pytorch 对齐验证,基于 PaConvert 工具验证 Paddle API 与 PyTorch API 是否用法完全对齐一致
| name | python-decorator |
| description | 负责《Paddle API 对齐 PyTorch 项目》中 Step2:API 代码修改,实施 Python 装饰器的代码开发。通过 Python 装饰器,在 Python 层为 Paddle API 提供参数别名、参数顺序、参数类型和参数用法的兼容转换,实现 PyTorch 风格的 API 调用,并保持 Paddle API 的向后兼容性。 |
| context | fork |
| background | false |
| verbose | true |
| disable-model-invocation | false |
Paddle 现有装饰器统一位于 ${ROOT_DIR}/Paddle/python/paddle/utils/decorator_utils.py,按功能分为两类:
| 装饰器 | 支持参数数量 | 性能 | 使用场景 | 优先级 |
|---|---|---|---|---|
param_one_alias | 1 个 | 高 | 单个参数别名(如 x↔input) | ⭐⭐⭐ |
param_two_alias | 2 个 | 高 | 两个参数别名(如 x↔input, axis↔dim) | ⭐⭐⭐ |
ParamAliasDecorator | 3 个及以上 | 中 | 复杂参数映射场景 | ⭐⭐ |
param_two_alias_one_default | 2 个+默认值 | 低 | 需要默认值(median 专用) | ⭐(不推荐) |
| 装饰器 | 功能描述 | 关键特性 |
|---|---|---|
index_select_decorator | 参数别名 + 参数顺序转换 | 检测第 2 个位置参数是否为 int 来判断不同参数顺序 |
index_add_decorator | 参数别名 + 参数顺序转换 | 检测第 2 个位置参数是否为 int 来判断不同参数顺序 |
transpose_decorator | 参数别名 + 参数用法转换 | 通过 dim0/dim1 两个 int 来自动构造 perm 列表 |
size_args_decorator | 参数别名 + 可变参数 | 合并全部 int 位置参数为 shape 列表 |
view_decorator | 参数别名 + 可变参数 | 合并全部 int 位置参数为 shape 列表 |
reshape_decorator | 参数别名 + 可变参数 | 第 1 个参数别名,合并后面多个 int 位置参数为 shape 列表 |
expand_decorator | 参数别名 + 可变参数 | 第 1 个参数别名,合并后面多个 int 位置参数为 shape 列表 |
legacy_reduction_decorator | 参数别名 + 报错信息增强 | 检测到 size_average/reduce 用法后,增强报错信息 |
lp_pool_function_decorator | 参数别名 + 参数顺序转换 | 检测第 5 个位置参数是否为 bool 来判断不同参数顺序 |
整体流程:Step 1 差异分析与选择装饰器 → Step 2 应用或开发装饰器 → Step 3 添加 out 参数支持 → Step 4 更新函数文档 → Step 5 添加测试用例 → Step 6 编译并运行
根据 PyTorch API 与 Paddle API 的差异分析来区分不同场景,选择合适的装饰器方案。
| 差异类型 | 参数顺序 | 参数个数 | 参数用法 | 推荐方案 |
|---|---|---|---|---|
| 仅参数名不同 | 相同 | 相同 | 相同 | ✅ 方式一(通用别名装饰器) |
| 参数名+参数顺序不同 | 不同 | 相同 | 相同 | ❌ 需要使用方式二(专用装饰器) |
| 参数名+参数个数不同 | 相同 | 不同 | 相同 | ❌ 需要使用方式二(专用装饰器) |
| 参数名+参数用法不同 | 相同 | 相同 | 不同 | ❌ 需要使用方式二(专用装饰器) |
| 其他复杂情况 | 其他 | 其他 | 其他 | ❌ 需要使用方式二(专用装饰器) |
判断方法:若上表中任何一列出现"不同",则需使用方式二开发专用装饰器。
根据需要映射的参数数量,选择对应的通用别名装饰器:
param_one_aliasparam_two_aliasParamAliasDecorator使用示例:
# 单个参数别名
## torch.deg2rad(input)
## paddle.deg2rad(x)
from paddle.utils.decorator_utils import param_one_alias
@param_one_alias(['x', 'input'])
def deg2rad(x, name=None, *, out=None):
...
# 两个参数别名
## torch.squeeze(input, dim)
## paddle.squeeze(x, axis)
from paddle.utils.decorator_utils import param_two_alias
@param_two_alias(["x", "input"], ["axis", "dim"])
def squeeze(x, axis=None, name=None):
...
# 多个参数别名
## torch.nn.functional.normalize(input, p, dim, eps)
## paddle.nn.functional.normalize(x, p, axis, epsilon)
from paddle.utils.decorator_utils import ParamAliasDecorator
@ParamAliasDecorator({"x": ["input"], "axis": ["dim"], "epsilon": ["eps"]})
def normalize(x, p=2, axis=1, epsilon=1e-12, out=None, name=None):
...
开发新的专用装饰器,可参考size_args_decorator、reshape_decorator、expand_decorator、view_decorator
使用示例:
# torch.reshape(x, 2, 5) # shape 支持可变参数用法
# paddle.reshape(x, [2, 5])
from paddle.utils.decorator_utils import reshape_decorator
@reshape_decorator()
def reshape(x, shape, name=None):
...
开发新的专用装饰器
示例:
# torch.index_select(input, dim, index)
# paddle.index_select(x, index, axis)
from paddle.utils.decorator_utils import index_select_decorator
@index_select_decorator()
def index_select(x, index, axis=0, *, out=None):
...
开发新的专用装饰器
示例:
# torch.transpose(x, dim0, dim1) # 交换两个维度,
# paddle.transpose(x, perm)
from paddle.utils.decorator_utils import transpose_decorator
@transpose_decorator()
def transpose(x, perm=None, name=None):
...
根据Step1中的不同场景:
示例:
from paddle.utils.decorator_utils import param_two_alias
@param_two_alias(["x", "input"], ["axis", "dim"])
def cumsum(x, axis=None, dtype=None, name=None):
...
${ROOT_DIR}/Paddle/python/paddle/utils/decorator_utils.py中定义新装饰器装饰器模板:
import functools
import inspect
def custom_decorator():
"""
装饰器功能说明
Usage Example:
PyTorch: torch.api(arg1, arg2)
Paddle: paddle.api(arg2, arg1) # 或其他映射关系
"""
def decorator(func):
@functools.wraps(func) # 保持原函数的__name__, __doc__等元信息
def wrapper(*args, **kwargs):
# 1. 参数别名映射
if "input" in kwargs and "x" not in kwargs:
kwargs["x"] = kwargs.pop("input")
# 2. 参数顺序转换或其他特殊处理
# 根据需要调整 args
# 3. 调用原函数
return func(*args, **kwargs)
wrapper.__signature__ = inspect.signature(func) # 保持函数签名
return wrapper
return decorator
关键实现要点:
if "input" in kwargs and "x" not in kwargs:
kwargs["x"] = kwargs.pop("input")
if len(args) >= 2 and isinstance(args[1], int):
# 检测 args 第 2 个值是否为 int,从而判断是 torch 用法还是 paddle 用法,并根据不同的 args 顺序统一匹配为 kwargs
## torch.index_select(input, dim, index)
## paddle.index_select(x, index, axis)
kwargs["x"] = args[0]
kwargs["axis"] = args[1]
args = args[2:]
# 合并多个 int 位置参数为列表
if len(args) >= 2 and all(isinstance(arg, int) for arg in args[1:]):
kwargs["shape"] = list(args[1:])
args = args[:1]
完整示例:
def index_select_decorator():
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# 1. 参数别名映射
if "input" in kwargs and "x" not in kwargs:
kwargs["x"] = kwargs.pop("input")
if "dim" in kwargs and "axis" not in kwargs:
kwargs["axis"] = kwargs.pop("dim")
# 2. PyTorch 参数顺序匹配:识别不同的 args 顺序,统一处理为 kwargs
if len(args) >= 2 and isinstance(args[1], int):
# PyTorch 顺序: (input, dim, index) → Paddle 顺序: (x, index, axis)
kwargs["x"] = args[0]
kwargs["axis"] = args[1]
if len(args) > 2:
kwargs["index"] = args[2]
args = ()
return func(*args, **kwargs)
wrapper.__signature__ = inspect.signature(func)
return wrapper
return decorator
注意事项:
${ROOT_DIR}/Paddle/python/paddle/utils/decorator_utils.py 中已有的专用装饰器来实现,在风格和逻辑上保持尽可能一致from typing import overload@overload
def gather(
x: Tensor,
index: Tensor,
axis: Tensor | int | None = None,
name: str | None = None,
out: Tensor | None = None,
) -> Tensor: ...
@overload
def gather(
input: Tensor,
dim: int,
index: Tensor,
out: Tensor | None = None,
) -> Tensor: ...
仅支持新增 out 参数,新增其他参数则需方案 3(修改 API 智能体)来开展。
适用条件:
_C_ops;情况 2:API 调用了其他 API,调用的最后一个其他 API 也支持 out示例:
# 情况 1:API 最后一个逻辑是调用`_C_ops`
@param_two_alias(["x", "input"], ["y", "other"])
def less_than(x, y, name=None, *, out=None) -> Tensor:
"""
Keyword args:
...
out (Tensor|None, optional): The output tensor. Default: None.
"""
if in_dynamic_or_pir_mode():
return _C_ops.less_than(x, y, out=out)
else:
...
# 情况 2:API 调用的最后一个其他 API 也支持 out
@param_two_alias(["x", "input"], ["axis", "dim"])
def fft(x, n=None, axis=-1, norm="backward", name=None, *, out=None) -> Tensor:
"""
Args:
...
Keyword args:
out (Tensor|None, optional): The output tensor. Default: None.
"""
if is_integer(x) or is_floating_point(x):
return fft_r2c(
x, n, axis, norm, forward=True, onesided=False, name=name, out=out
)
else:
return fft_c2c(x, n, axis, norm, forward=True, name=name, out=out)
适用条件:不符合方式一的情况
示例:
def func(x, axis=None, name=None, *, out: Tensor | None = None):
"""
Args:
...
Keyword args:
out (Tensor|None, optional): The output tensor. Default: None.
"""
# case1: 只有 1 个 out 的情况
ret = <计算逻辑>
if out is not None:
paddle.assign(ret, out)
return out
return ret
# case2: 有多个 out 的情况
ret1, ret2 = <计算逻辑>
if out is not None:
paddle.assign(ret1, out[0])
paddle.assign(ret2, out[1])
return out
return ret1, ret2
注意:
out参数需与 Pytorch 用法一致,一般情况下 out 均是 keyword-only 参数(使用*,分隔),少数情况下 out 是位置参数。如果使用的是通用别名装饰器,则在文档的 Args 部分为有别名的参数添加 Alias Support 说明,如下:
注:Alias 说明应放在该参数描述的末尾,格式为: Alias:
alias_name,多个 Alias 描述为: Alias:alias_name1oralias_name2
@param_two_alias(["x", "input"], ["axis", "dim"])
def fft(
x: Tensor,
n: int | None = None,
axis: int = -1,
norm: _NormalizeMode = "backward",
name: str | None = None,
*,
out: Tensor | None = None,
) -> Tensor:
"""
Calculate one-dimensional discrete Fourier transform.
Args:
x (Tensor): The input data. It's a Tensor type. It's a complex.
Alias: ``input``.
n (int|None, optional): The length of the output transform axis.
axis (int, optional): Axis used to calculate FFT.
Alias: ``dim``.
norm (str, optional): Indicates which direction to scale the `forward` or `backward` transform
pair and what normalization factor to use.
name (str|None, optional): The default value is None.
Keyword args:
out(Tensor, optional): The output tensor.
"""
如果使用的是专用装饰器,则表明 API 支持了签名重载,需要分别描述两种签名,可以参考代码中的@overload 注解,如下:
注:只需在文档正文中阐述两种签名(Paddle 在前,Pytorch 在后),文档其他位置如 Args/Returns 仍以 Paddle 风格签名为准
@overload
def broadcast_tensors(input: Sequence[Tensor], name: str | None = None) -> list[Tensor]: ...
@overload
def broadcast_tensors(*tensors: Tensor) -> list[Tensor]: ...
@variadic_tensor_decorator('input')
def broadcast_tensors(input: Sequence[Tensor], name: str | None = None) -> list[Tensor]:
"""
This API has two signatures:
1. ``paddle.broadcast_tensors(input, name=None)`` (Paddle-style):
Broadcast a list of tensors following broadcast semantics.
2. ``paddle.broadcast_tensors(*tensors)`` (PyTorch-style):
Broadcast variadic tensor arguments following broadcast semantics.
Args:
...
Returns:
...
"""
如果支持了 out 参数,必须在 API 文档中描述 out 参数,out 为 keyword-only 参数(*后面)时注意增加Keyword Args:,并在此部分描述。out 为位置参数时直接在 Args 部分描述。如下:
# out 为 keyword-only 参数
def func(x, name=None, *, out=None):
"""
...
Args:
...
Keyword Args:
out (Tensor|optional): The output tensor. Default: None.
Returns:
...
"""
# out 为位置参数
def func(x, out=None, name=None):
"""
...
Args:
x (Tensor): Input of Atan operator.
out (Tensor, optional): The output Tensor. Default: None.
name (str|None, optional): Name for the operation.
Returns:
...
"""
注意事项:
不要新建任何测试文件,直接在 test/legacy_test/test_api_compatibility[1-9]\.py(数字最大的) 中添加测试。严格按以下模板来编写:
测试模板:
class Test<APIName>API(unittest.TestCase):
def setUp(self):
# If not use random seed, remove setUp
np.random.seed(2025)
self.np_x = np.random.rand(...).astype(...)
def test_dygraph_Compatibility(self):
paddle.disable_static()
x = paddle.to_tensor(self.np_x)
# 1. Paddle Positional arguments
out1 = paddle.<api_name>(x, ...)
# 2. Paddle keyword arguments
out2 = paddle.<api_name>(x=x, ...)
# 3. Pytorch Positional arguments (only if order different with paddle args)
out3 = paddle.<api_name>(x, ...)
# 4. PyTorch keyword arguments (alias)
out4 = paddle.<api_name>(input=x, dim=...)
# 5. Mixed arguments
out5 = paddle.<api_name>(x, axis=...)
# 6. out parameter test (only if supported)
out6 = paddle.empty_like(x)
out7 = paddle.<api_name>(x, ..., out=out6)
# 7. Tensor method - args (only if supported)
out8 = x.<api_name>(...)
# 8. Tensor method - kwargs (only if supported)
out9 = x.<api_name>(axis=...)
# Verify all outputs
for out in [out1, out2, out3, out4, out5, out6, out7, out8, out9]:
np.testing.assert_allclose(out.numpy(), ...)
paddle.enable_static()
def test_static_Compatibility(self):
paddle.enable_static()
main = paddle.static.Program()
startup = paddle.static.Program()
with paddle.static.program_guard(main, startup):
x = paddle.static.data(name="x", shape=self.shape, dtype=self.dtype)
# Create multiple outputs
out1 = paddle.<api_name>(x, ...)
out2 = paddle.<api_name>(x=x, ...)
out3 = paddle.<api_name>(input=x, dim=...)
exe = paddle.static.Executor()
fetches = exe.run(
main,
feed={"x": self.np_x},
fetch_list=[out1, out2, out3],
)
# Verify all outputs
for out in fetches:
np.testing.assert_allclose(out, ...)
测试规范: 动态图模式:
静态图模式:(inplace 无需测)
注意:
完整测试示例,请参考 ${ROOT_DIR}/Paddle/test/legacy_test/test_api_compatibility[1-9]\.py 中已有的测试类结构。
单测编写完成后,按以下命令验证执行(不可修改):
重新编译项目:
cd ${ROOT_DIR}/Paddle/build
cmake ..
make -j$(nproc)
运行单测文件:
python <所修改的单测文件名>
问题排查:根据报错信息调整代码或测试用例,确保所有测试用例通过。注意每次修改 Paddle 源码后,必须重新编译方可生效。
编译注意事项:
Paddle API 架构(5 层):
本方案修改范围:
import functools
import inspect
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# 参数处理逻辑
return func(*args, **kwargs)
wrapper.__signature__ = inspect.signature(func)
return wrapper
关键点:
@functools.wraps(func):保持原函数的__name__、__doc__等元信息wrapper.__signature__:保持函数签名,支持 IDE 的参数提示kwargs 别名映射
if "input" in kwargs and "x" not in kwargs:
kwargs["x"] = kwargs.pop("input")
位置参数类型检测
if len(args) >= 2 and isinstance(args[1], int):
kwargs["x"] = args[0]
kwargs["shape"] = list(args[1:])
args = ()
${ROOT_DIR} 变量表示根目录,需自行替换为实际路径错误现象:
TypeError: expected Tensor as argument, got numpy.ndarray
解决方法:
# 确保输入是 Tensor 类型
tensor_input = paddle.to_tensor(numpy_input)
paddle.api(tensor_input, ...)
错误现象:装饰器内参数转换失败或逻辑异常
解决方法:
在装饰器中添加日志进行调试:
import logging
logging.basicConfig(level=logging.DEBUG)
# 在装饰器中添加日志
def wrapper(*args, **kwargs):
logging.debug(f"Before: args={args}, kwargs={kwargs}")
# 处理逻辑...
logging.debug(f"After: args={args}, kwargs={kwargs}")
return func(*args, **kwargs)
# 或使用打印调试
def wrapper(*args, **kwargs):
print(f"[DEBUG] args={args}, kwargs={kwargs}")
# ...