Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
91b9144
align 3 Tensor api and PReLU
Manfredss Jul 27, 2026
3451263
Merge branch 'develop' of https://github.com/paddlepaddle/paddle into…
Manfredss Jul 27, 2026
d5154f3
fix PReLU, fix enable_compat restore after using guard
Manfredss Jul 27, 2026
4865ecc
add test coverage
Manfredss Jul 28, 2026
8aa81ef
fix per bot feedback
Manfredss Jul 28, 2026
7f2684a
fix typo
Manfredss Jul 28, 2026
7eda286
also fix paddle.distributions.categorical.Categorical
Manfredss Jul 28, 2026
ec4686b
fix
Manfredss Jul 29, 2026
cf76ec4
fix
Manfredss Jul 30, 2026
f668adb
Refine compat levels and guard state restoration
Manfredss Jul 30, 2026
111a665
Merge branch 'develop' of https://github.com/paddlepaddle/paddle into…
Manfredss Jul 30, 2026
b3bb47c
remove assertion
Manfredss Jul 30, 2026
2f3cc36
fix fleet tests failure
Manfredss Jul 31, 2026
ec1295c
Merge branch 'develop' of https://github.com/paddlepaddle/paddle into…
Manfredss Jul 31, 2026
f514e71
staged
Manfredss Jul 31, 2026
a721161
Merge branch 'develop' of https://github.com/paddlepaddle/paddle into…
Manfredss Aug 3, 2026
c64978f
reframe use_compat_guard
Manfredss Aug 3, 2026
826269a
remove unused methods
Manfredss Aug 3, 2026
a119dbe
fix counter; add dispatch_property
Manfredss Aug 3, 2026
5a5b7b3
fix
Manfredss Aug 3, 2026
219307c
fix tests
Manfredss Aug 3, 2026
fde90cf
Merge branch 'develop' of https://github.com/paddlepaddle/paddle into…
Manfredss Aug 4, 2026
b0217d4
Merge branch 'develop' of https://github.com/paddlepaddle/paddle into…
Manfredss Aug 5, 2026
82f8ebf
fix
Manfredss Aug 6, 2026
ce136ec
Merge branch 'develop' of https://github.com/paddlepaddle/paddle into…
Manfredss Aug 6, 2026
3eeb401
improve code per review suggestions
Manfredss Aug 6, 2026
43c95f1
fix
Manfredss Aug 7, 2026
8c035b1
refine
Manfredss Aug 7, 2026
0488bce
fix Tensor.type so that paconvert can pass without check_value=False
Manfredss Aug 10, 2026
2921bbb
fix factory device missed
Manfredss Aug 11, 2026
6132cef
Merge branch 'develop' of https://github.com/paddlepaddle/paddle into…
Manfredss Aug 11, 2026
c995fa1
fix
Manfredss Aug 11, 2026
9c9fe9b
fix device convert to cpu silently
Manfredss Aug 11, 2026
61b6e77
add tests
Manfredss Aug 12, 2026
25748d7
Merge branch 'develop' of https://github.com/paddlepaddle/paddle into…
Manfredss Aug 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 173 additions & 0 deletions python/paddle/compat/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
from collections.abc import Sequence

from paddle import Tensor
from paddle._typing import DTypeLike

__all__ = [
'allclose',
Expand All @@ -56,6 +57,26 @@
]


_TENSOR_TYPE_DTYPES = {
'HalfTensor': 'float16',
'FloatTensor': 'float32',
'DoubleTensor': 'float64',
'Float8_e4m3fnTensor': 'float8_e4m3fn',
'Float8_e5m2Tensor': 'float8_e5m2',
'BFloat16Tensor': 'bfloat16',
'ByteTensor': 'uint8',
'CharTensor': 'int8',
'ShortTensor': 'int16',
'IntTensor': 'int32',
'LongTensor': 'int64',
'BoolTensor': 'bool',
'ComplexFloatTensor': 'complex64',
'ComplexDoubleTensor': 'complex128',
}

_DTYPE_TENSOR_TYPES = {v: k for k, v in _TENSOR_TYPE_DTYPES.items()}


def __getattr__(name):
if name == "paddle_triton":
return paddle_triton_fun()
Expand All @@ -66,6 +87,140 @@ def __getattr__(name):
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


def _tensor_numel(input: Tensor) -> int:
"""
Returns the total number of elements in the tensor.

Args:
input (Tensor): The input tensor.

Returns:
int: The number of elements in ``input``.
"""
return int(input.size)


def _tensor_type_name(input: Tensor) -> str:
"""The tensor type name of ``input``, e.g. ``'torch.cuda.sparse.FloatTensor'``,
following ``torch.Tensor.type``. The device segment is ``'cuda'`` for GPU,
``'xpu'`` for XPU and the device type itself for custom devices, e.g.
``'torch.npu.FloatTensor'``. Dtypes without a tensor type name fall back to
the dtype string."""
dtype_name = str(input.dtype).removeprefix("paddle.")
tensor_type = _DTYPE_TENSOR_TYPES.get(dtype_name)
if tensor_type is None:
return str(input.dtype)
segments = ["torch"]
place = input.place
if place.is_gpu_place():
segments.append("cuda")

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 这里开始生成 torch.xpu.*/custom-device 类型名,但 _tensor_type() 解析字符串时只把 torch.cuda./paddle.cuda. 映射到 GPU,其余合法 torch.* 名称都落到 device="cpu"。因此 t.type("torch.xpu.FloatTensor")(以及 custom-device 对应名称)会把 Tensor 从 XPU/自定义设备错误迁移到 CPU;当前测试只覆盖名称生成,没有覆盖反向转换。请补齐 XPU/custom 的设备解析与 round-trip 测试。

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.

@risemeup1111 现在的改动可以吗

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.

XPU/custom 的解析已补齐,但当前修改仍不完整:torch.FloatTensor/paddle.FloatTensor 这类无设备段的 tensor type 现在返回 device=None,GPU Tensor 调用后会保留在 GPU;PyTorch 中这些名称表示 CPU tensor type,应转到 CPU。请让已识别的 tensor-type 字符串在无设备段时默认 cpu,同时保留 torch.float32 这类 dtype-only 字符串的 device=None,并补 GPU→torch.FloatTensor 回归测试。

@Manfredss Manfredss Aug 12, 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.

@risemeup1111 我觉得像 torch.FloatTensor/paddle.FloatTensor 这样的调用,本身就不应该涉及设备的转换,所以 device=None 保持设备不变是对的;即参数中有设备段或者显式表明了类(比如 paddle.cuda.DoubleTensor)才要管设备,否则保持不变

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.

这里的 device=None 只适用于 dtype-only 参数(例如 torch.float32),不适用于 tensor type factory。PyTorch 中 torch.FloatTensor 是 CPU tensor class,torch.cuda.FloatTensor 才是 CUDA class;本 PR 的 _tensor_type_name() 也明确把 CPU Tensor 编码成 torch.FloatTensor、GPU Tensor 编码成 torch.cuda.FloatTensor。因此 t.type(t.type()) 必须保持 round-trip,GPU Tensor 的 t.type("torch.FloatTensor") 应转换到 CPU;否则返回的类型名不能表达并恢复 Tensor 的 place。请将无设备段的 tensor-type 字符串区分为 device="cpu",同时继续让 torch.float32 保持 device=None

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.

@risemeup1111 现在可以没 审查一下

elif place.is_xpu_place():
segments.append("xpu")
elif place.is_custom_place():
segments.append(place.custom_device_type().lower())
if input.is_sparse_coo():
segments.append("sparse")
segments.append(tensor_type)
return ".".join(segments)


def _tensor_type(
input: Tensor,
dtype: DTypeLike | str | type | None = None,
non_blocking: bool = False,
**kwargs: Any,
) -> str | Tensor:
"""
Returns the tensor type when ``dtype`` is not specified, otherwise casts
the tensor to the requested type.

Args:
input (Tensor): The input tensor.
dtype (DTypeLike|str|type|None, optional): The target tensor type or
data type. Qualified ``torch.*`` and ``paddle.*`` dtype or tensor
type strings are supported. When it is ``None``, returns the tensor
type name. Default: ``None``.
non_blocking (bool, optional): Whether the conversion may occur
asynchronously. Default: ``False``.

Returns:
str|Tensor: The tensor type name when ``dtype`` is ``None``, e.g.
``'torch.FloatTensor'``, ``'torch.cuda.FloatTensor'`` or
``'torch.cuda.sparse.FloatTensor'``, encoding the dtype, the place
and the sparse COO layout as ``torch.Tensor.type`` does; otherwise,
a tensor with the requested type.
"""
if "async" in kwargs:
non_blocking = kwargs.pop("async")
if kwargs:
key = next(iter(kwargs))
raise TypeError(f"type() got an unexpected keyword argument {key!r}")

if dtype is None:
return _tensor_type_name(input)

device = None
if getattr(dtype, "__name__", None) in _TENSOR_TYPE_DTYPES:

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 通过类名识别会丢失工厂类的限定设备:paddle.cuda.DoubleTensorpaddle.DoubleTensor 实际导入的是同一类对象,经 torch proxy 调用 t.type(torch.cuda.DoubleTensor) 也会命中这里并被强制设为 device="cpu",把本应转到 CUDA 的 Tensor 移到 CPU。请保留限定模块的设备信息;若类对象无法区分来源,应拒绝该形式而不是静默迁移到 CPU,并补 CUDA 工厂类测试。

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.

当前 head 已改为带设备标记的独立 CUDA factory,并加入 CUDA 构造覆盖测试,原问题已修复。

# tensor factory classes, e.g. paddle.DoubleTensor
device = getattr(dtype, "_device", None) or "cpu"
dtype = getattr(paddle, _TENSOR_TYPE_DTYPES[dtype.__name__])
elif isinstance(dtype, str):
dtype_string = dtype
tensor_type = dtype_string.rsplit(".", 1)[-1]
if not dtype_string.startswith(("torch.", "paddle.")):
raise ValueError(f"invalid type: {dtype_string!r}")
if tensor_type in _TENSOR_TYPE_DTYPES:
dtype = getattr(paddle, _TENSOR_TYPE_DTYPES[tensor_type])
middle = dtype_string.split(".")[1:-1]
device_parts = [s for s in middle if s != "sparse"]
if not device_parts:
device = "cpu"
elif device_parts[0] == "cuda":
device = "gpu"
else:
device = device_parts[0]
elif tensor_type in _TENSOR_TYPE_DTYPES.values():
dtype = getattr(paddle, tensor_type)
else:
raise ValueError(f"invalid type: {dtype_string!r}")

if device is None:
same_device = True
elif device == "cpu":
same_device = input.place.is_cpu_place()
elif device == "gpu":
same_device = input.place.is_gpu_place()
elif device == "xpu":
same_device = input.place.is_xpu_place()
else:
same_device = (
input.place.is_custom_place()
and input.place.custom_device_type().lower() == device
)
if input.dtype == dtype and same_device:
return input

return input.to(
device=device,
dtype=dtype,
blocking=not non_blocking,
)


@property
def _tensor_is_sparse(input: Tensor) -> bool:
"""
Whether the tensor uses the sparse COO layout.

Args:
input (Tensor): The input tensor.

Returns:
bool: ``True`` for a sparse COO tensor, otherwise ``False``.
"""
return input.is_sparse_coo()


def allclose(
input: Tensor,
other: Tensor,
Expand Down Expand Up @@ -1132,3 +1287,21 @@ def GetShapeOnDimInRange(shape, dim: int) -> int:
split_size_or_sections
)
return tuple(_C_ops.split(tensor, split_size_or_sections, dim))


# ``paddle.Tensor`` APIs routed to their ``paddle.compat`` implementations
_TENSOR_API_OVERRIDES = {
'allclose': allclose,
'equal': equal,
'slogdet': slogdet,
'sort': sort,
'split': split,
'min': min,
'max': max,
'unique': unique,
'median': median,
'nanmedian': nanmedian,
'numel': _tensor_numel,
'type': _tensor_type,
'is_sparse': _tensor_is_sparse,
}
89 changes: 50 additions & 39 deletions python/paddle/compat/api_dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,29 +51,19 @@ def _caller_is_paddle_internal() -> bool:
return name == "paddle" or name.startswith("paddle.")


def dispatch_function(compat_fn: Any) -> Any:
"""Wrap a native ``paddle`` callable to route external callers to
``compat_fn`` while compat is enabled; paddle-internal callers and the
disabled state get the native callable. Installed 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)
def dispatch_function(native_fn: Any, compat_fn: Any) -> Any:
"""Wrap a native ``paddle`` callable for caller-aware dispatch."""

dispatcher.__compat_fn__ = compat_fn
dispatcher.__native_fn__ = native_fn
dispatcher.__signature__ = inspect.signature(compat_fn)
return dispatcher
@wraps(native_fn)
def dispatcher(*args: Any, **kwargs: Any) -> Any:
if _caller_is_paddle_internal():
return native_fn(*args, **kwargs)
return compat_fn(*args, **kwargs)

return decorator
dispatcher.__compat_fn__ = compat_fn
dispatcher.__native_fn__ = native_fn
dispatcher.__signature__ = inspect.signature(compat_fn)
return dispatcher


def _iter_compat_modules() -> Generator[types.ModuleType, None, None]:
Expand Down Expand Up @@ -123,28 +113,49 @@ def __call__(cls, *args: Any, **kwargs: Any) -> Any:
return proxy


def dispatch_property(
native_attr: Any,
compat_attr: Any,
) -> Any:
"""Route a Tensor API when either side uses the property protocol."""
compat_fn = (
compat_attr.fget if isinstance(compat_attr, property) else compat_attr
)

class _PropertyDispatcher:
def __get__(self, instance: Any, owner: type | None = None) -> Any:
if _caller_is_paddle_internal():
attr = native_attr
else:
attr = compat_attr
return attr.__get__(instance, owner)

dispatcher = _PropertyDispatcher()
dispatcher.__native_fn__ = native_attr
dispatcher.__compat_fn__ = compat_fn
dispatcher.__doc__ = compat_fn.__doc__
dispatcher.__name__ = compat_fn.__name__
dispatcher.__signature__ = inspect.signature(compat_fn)
return dispatcher


def _patch_tensor_methods() -> None:
"""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.
"""
"""Route ``paddle.Tensor`` APIs to their root compat implementations."""
import paddle
import paddle.compat as compat_root

for attr_name in getattr(compat_root, "__all__", ()):
native_method = getattr(paddle.Tensor, attr_name, None)
if native_method is None:
for attr_name, compat_attr in compat_root._TENSOR_API_OVERRIDES.items():
native_attr = inspect.getattr_static(paddle.Tensor, attr_name, None)
if native_attr is None:
continue
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),
)
_PADDLE_NAMESPACE_SAVED[(paddle.Tensor, attr_name)] = native_attr
if inspect.isdatadescriptor(native_attr) or isinstance(
compat_attr, property
):
dispatcher = dispatch_property(native_attr, compat_attr)
else:
dispatcher = dispatch_function(native_attr, compat_attr)
setattr(paddle.Tensor, attr_name, dispatcher)


def _apply_paddle_namespace_aliases() -> None:
Expand Down Expand Up @@ -177,7 +188,7 @@ def _apply_paddle_namespace_aliases() -> None:
setattr(
target_module,
attr_name,
dispatch_function(compat_attr)(current),
dispatch_function(current, compat_attr),
)
_patch_tensor_methods()

Expand Down
16 changes: 16 additions & 0 deletions python/paddle/compat/distributions/categorical.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

from ..utils import _CompatClassMeta

__all__ = ["Categorical"]


class Categorical(distribution.Distribution, metaclass=_CompatClassMeta):
arg_constraints = {
Expand Down Expand Up @@ -66,6 +68,20 @@ def __init__(
distribution.Distribution.__init__(
self, batch_shape, validate_args=validate_args
)
if self._validate_args_enabled and paddle.in_dynamic_mode():
if probs is not None:
param_name = "probs"
valid = paddle.all(self.probs >= 0, axis=-1) & (
(self.probs.sum(-1) - 1).abs() < 1e-6
)
else:
param_name = "logits"
valid = constraint.real_vector.check(self.logits)
if not bool(valid.all()):
raise ValueError(
f'Expected parameter {param_name} of distribution '
'Categorical to satisfy its constraint'
)

def expand(self, batch_shape, _instance=None):
new = (
Expand Down
Loading
Loading