Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a3bb2d7
[CppExtension] 添加InstallCommand以支持setuptools>=80的共享库重命名和安装路径优化
megemini Oct 23, 2025
3ed2fdb
[CppExtension] 优化InstallCommand以规范安装布局,确保单入口包结构并兼容遗留测试
megemini Oct 23, 2025
0662252
[CppExtension] 修复InstallCommand中的代码格式问题并优化字符串拼接逻辑
megemini Oct 23, 2025
e10f18b
[CppExtension] 确保pip兼容性:添加metadata_version并优化egg-info清理逻辑
megemini Oct 27, 2025
872e846
[CppExtension] 更新测试用例以匹配新的egg生成数量预期
megemini Oct 27, 2025
3bfa9c3
Merge branch 'develop' of https://github.com/PaddlePaddle/Paddle into…
megemini Oct 27, 2025
055d1b0
[CppExtension] 添加 setuptools 版本判断逻辑并优化异常捕获
megemini Oct 30, 2025
0655f0f
[CppExtension] 修改测试用例适配setuptools版本差异
megemini Oct 30, 2025
99a4611
[CppExtension] 移除packaging依赖并简化setuptools版本判断逻辑
megemini Oct 30, 2025
2e80847
[CppExtension] 移除packaging依赖并简化setuptools版本判断逻辑
megemini Oct 30, 2025
9a94332
[CppExtension] 添加wheel支持并优化安装布局处理
megemini Nov 1, 2025
05a4114
[CppExtension] 优化导入依赖并调整模块结构
megemini Nov 1, 2025
6305767
[CppExtension] 简化安装逻辑
megemini Nov 3, 2025
fff063e
[CppExtension] 统一变量命名从custom_egg_path改为custom_install_path
megemini Nov 3, 2025
6103552
Merge branch 'develop' of https://github.com/PaddlePaddle/Paddle into…
megemini Nov 3, 2025
545b0fa
Merge branch 'develop' of https://github.com/PaddlePaddle/Paddle into…
megemini Nov 4, 2025
1dca083
Merge branch 'develop' of https://github.com/PaddlePaddle/Paddle into…
megemini Nov 6, 2025
c3fc22b
[CppExtension] 移除废弃的bootstrap_context和相关代码
megemini Nov 8, 2025
ba9349b
[CppExtension] 仅在未指定时设置install_lib等路径
megemini Nov 10, 2025
c1ebffc
[CppExtension] 修复扩展模块名称处理逻辑,支持多级包路径
megemini Nov 17, 2025
c74e819
Merge branch 'develop' of https://github.com/PaddlePaddle/Paddle into…
megemini Nov 17, 2025
8e49d98
[CppExtension] 简化扩展模块名称获取逻辑,移除冗余代码
megemini Nov 17, 2025
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
206 changes: 202 additions & 4 deletions python/paddle/utils/cpp_extension/cpp_extension.py
Copy link
Member

Choose a reason for hiding this comment

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

啊 好快的删,没事我周一问问其他人再确定,反正就是一个 revert

Copy link
Member

Choose a reason for hiding this comment

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

昨天忘记说了,昨天和 @zyfncg 讨论的结论是先不为 79-、80+ 单独添加兼容性逻辑,单独解决一下 FD 里的问题就可以了,解决完 FD 问题后如果其他场景有问题反馈再为 79-、80+ 单独添加兼容性逻辑

Copy link
Contributor Author

Choose a reason for hiding this comment

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

有个地方没想明白,fd 为啥要把安装好的包 copy 到 fd 目录里面?方便后续一并卸载?应该也不是啊,它用的是 cp 而不是 mv ... ...

fd 如果要改的话,把现在生成的两个目录一起 copy 过去应该就可以了 ~

Copy link
Member

Choose a reason for hiding this comment

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

按我理解是,这些自定义算子 fastdeploy_ops 只是中间产物(仅算子实现),最终会被打包到 fastdeploy 包(含 Python 实现)里进行发布

Copy link
Contributor Author

Choose a reason for hiding this comment

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

相当于,将这些算子统一放到 fd 的命名空间中进行管理,而不是作为一个个单独的包?

OK ~ 那咱们这里还需要做啥?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fastdeploy/model_executor/ops/gpu/init.py 里的内容变成了 stub 内容

这个?现在是需要咱们这里修改兼容 fd 目前的实现方式?

Copy link
Member

Choose a reason for hiding this comment

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

这个?现在是需要咱们这里修改兼容 fd 目前的实现方式?

不一定是框架兼容,可以修改 FD 的 build 脚本

Copy link
Contributor Author

Choose a reason for hiding this comment

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

嗯 ~ 我的意思也是,咱们这个 pr 应该不需要修改,最好是修改 fd 那边的脚本 ~ 那我给 fd 那边提个 pr ?

Copy link
Member

Choose a reason for hiding this comment

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

嗯,如果确定不是这个 PR 导致的 bug,可以提 PR 改动下,不过那边的 PR 应该兼容两种方式,因为不能保证所有人都用 develop

Copy link
Contributor Author

Choose a reason for hiding this comment

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

--install-lib 之前是有问题的,前两天最新的 commit ba9349b 已经改了 ~

fd 那边我这两天提个 pr 改一下 ~

Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,13 @@
import setuptools
import sys
import paddle
import site

from setuptools.command.easy_install import easy_install
from setuptools.command.build_ext import build_ext
from distutils.command.build import build
from setuptools.command.install import install


from .extension_utils import (
add_compile_flag,
Expand Down Expand Up @@ -55,9 +59,9 @@
)
from .extension_utils import _reset_so_rpath, clean_object_if_change_cflags
from .extension_utils import (
bootstrap_context,
get_build_directory,
add_std_without_repeat,
custom_write_stub,
)

from .extension_utils import (
Expand Down Expand Up @@ -235,6 +239,11 @@ def setup(**attr: Any) -> None:
assert 'easy_install' not in cmdclass
cmdclass['easy_install'] = EasyInstallCommand

# Compatible with wheel installation via `pip install .`
# Note: This is rarely used with modern pip, which uses bdist_wheel instead
assert 'install' not in cmdclass
cmdclass['install'] = InstallCommand

# Note(Aurelius84): Add rename build_base directory hook in build command.
# To avoid using same build directory that will lead to remove the directory
# by mistake while parallelling execute setup.py, for example on CI.
Expand All @@ -246,9 +255,7 @@ def setup(**attr: Any) -> None:
# See http://peak.telecommunity.com/DevCenter/setuptools#setting-the-zip-safe-flag
attr['zip_safe'] = False

# switch `write_stub` to inject paddle api in .egg
with bootstrap_context():
setuptools.setup(**attr)
setuptools.setup(**attr)


def CppExtension(
Expand Down Expand Up @@ -849,8 +856,36 @@ def _clean_intermediate_files(self):
os.remove(os.path.join(root, file))
print(f"Removed: {os.path.join(root, file)}")

def _generate_python_api_file(self) -> None:
"""
Generate the top-level python api file (package stub) alongside the
built shared library in build_lib. This replaces the legacy bdist_egg
write_stub mechanism that is no longer triggered in setuptools >= 80.
"""
try:
outputs = self.get_outputs()
if not outputs:
return
# We only support a single extension per setup()
so_path = os.path.abspath(outputs[0])
so_name = os.path.basename(so_path)
build_dir = os.path.dirname(so_path)
# The package name equals distribution name
pkg_name = self.distribution.get_name()
Copy link
Member

@SigureMo SigureMo Nov 16, 2025

Choose a reason for hiding this comment

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

话说这里的路径可以从 extension.name 获取么?不与包名强绑定,比如 self.get_ext_fullpath(self.extensions[0].name) 这种?因为后续可能会考虑支持 extension.name = pkg.submodule.ops 这种形式,虽然现在的话 extension.name 应该被 distribution.name 覆盖掉了 这里描述有点问题,看下面~

Copy link
Member

@SigureMo SigureMo Nov 16, 2025

Choose a reason for hiding this comment

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

具体使用场景是这样的~

# setup.py
from paddle.utils.cpp_extension import CppExtension, setup

setup(
    name='custom_setup_ops.my_ops.custom_relu',
    ext_modules=CppExtension(
        sources=['my_ops/relu_cpu.cc']
    )
)
# pyproject.toml
[project]
name = "custom-setup-ops"  # distribution.name 可能拿到 custom-setup-ops 这种没有 normalize 的 name
version = "0.0.0"

[tool.setuptools.packages.find]
exclude = []

[build-system]
requires = ["setuptools>=61.0.0,<80.0.0"]
build-backend = "setuptools.build_meta"
// my_ops/relu_cpu.cc
#include "paddle/extension.h"

#include <vector>

#define CHECK_INPUT(x) PD_CHECK(x.is_cpu(), #x " must be a CPU Tensor.")

template <typename data_t>
void relu_cpu_forward_kernel(const data_t* x_data,
                             data_t* out_data,
                             int64_t x_numel) {
  for (int64_t i = 0; i < x_numel; ++i) {
    out_data[i] = std::max(static_cast<data_t>(0.), x_data[i]);
  }
}

std::vector<paddle::Tensor> ReluCPUForward(const paddle::Tensor& x) {
  CHECK_INPUT(x);

  auto out = paddle::empty_like(x);

  PD_DISPATCH_FLOATING_TYPES(
      x.type(), "relu_cpu_forward_kernel", ([&] {
        relu_cpu_forward_kernel<data_t>(
            x.data<data_t>(), out.data<data_t>(), x.numel());
      }));

  return {out};
}

// 维度推导
std::vector<std::vector<int64_t>> ReluInferShape(std::vector<int64_t> x_shape) {
  return {x_shape};
}

// 类型推导
std::vector<paddle::DataType> ReluInferDtype(paddle::DataType x_dtype) {
  return {x_dtype};
}

PD_BUILD_OP(custom_relu)
    .Inputs({"X"})
    .Outputs({"Out"})
    .SetKernelFn(PD_KERNEL(ReluCPUForward))
    .SetInferShapeFn(PD_INFER_SHAPE(ReluInferShape))
    .SetInferDtypeFn(PD_INFER_DTYPE(ReluInferDtype));

pyproject.toml 用于 uv buildpython setup.py bdist_wheel 同样可以复现~

目前打包出来的目录结构如下

dist/custom_setup_ops/my_ops
├── custom_relu.so  # so 没有 rename
├── custom_setup_ops.py  # 生成的 stub name 应该是 ext name split(".")[-1] 而不是 distribution name
└── version.txt

预期应该是

dist/custom_setup_ops/my_ops
├── custom_relu_pd_.so
├── custom_relu.py
└── version.txt

另外注意这里可能需要一并调整下~

def custom_write_stub(resource, pyfile):
    ...
    with open(pyfile, 'w') as f:
        f.write(
            _stub_template.format(
                resource=os.path.basename(resource),  # 这里需要删除前缀
                custom_api='\n\n'.join(api_content)
            )
        )

我们有假设 assert self.extensions == 1,可以直接取 self.extensions[0],不需要考虑多个 extensions 的 case,按我理解是微调,不需要大改

虽然这里不在 Setuptools 80+ 支持的考虑范围内,不过这里的调整看起来是泛用性支持,辛苦顺师傅再稍微调整下~

cc. @zhangbo9674

pyfile = os.path.join(build_dir, f"{pkg_name}.py")
# Write stub; it will reference the _pd_ renamed resource at import time
custom_write_stub(so_name, pyfile)
except Exception as e:
raise RuntimeError(
f"Failed to generate python api file: {e}"
) from e

def run(self):
super().run()

# Compatible with wheel installation via `pip install .`
self._generate_python_api_file()

self._clean_intermediate_files()


Expand Down Expand Up @@ -926,6 +961,169 @@ def initialize_options(self) -> None:
self.build_base = self._specified_build_base


class InstallCommand(install):
"""
Extend install Command to:
1) choose an install dir that is actually importable (on sys.path)
2) ensure a single top-level entry for the package in site/dist-packages so
legacy tests that expect a sole artifact (egg/package) keep working
3) rename the compiled library to *_pd_.so to avoid shadowing the python stub
"""

def finalize_options(self) -> None:
super().finalize_options()

install_dir = (
getattr(self, 'install_lib', None)
or getattr(self, 'install_purelib', None)
or getattr(self, 'install_platlib', None)
)
if not install_dir or not os.path.isdir(install_dir):
return
pkg = self.distribution.get_name()
# Check if dist-info exists
has_dist_info = any(
name.endswith('.dist-info') and name.startswith(pkg)
for name in os.listdir(install_dir)
)
# If dist-info exists, we are installing a wheel, so we are done
if has_dist_info:
return

# Build candidate site dirs: global + user + entries already on sys.path
candidates = []
candidates.extend(site.getsitepackages())
usp = site.getusersitepackages()
if usp:
candidates.append(usp)
for sp in sys.path:
if isinstance(sp, str) and sp.endswith(
('site-packages', 'dist-packages')
):
candidates.append(sp)
# De-dup while preserving order
seen = set()
ordered = []
for c in candidates:
if c and c not in seen:
seen.add(c)
ordered.append(c)
# Prefer a candidate that is actually on sys.path
target = None
for c in ordered:
if c in sys.path and os.path.isdir(c):
target = c
break
# Fallback: pick the first existing candidate
if target is None:
for c in ordered:
if os.path.isdir(c):
target = c
break
if target:
option_dict = self.distribution.get_option_dict('install')

if 'install_lib' not in option_dict:
self.install_lib = target

if 'install_purelib' not in option_dict:
self.install_purelib = target

if 'install_platlib' not in option_dict:
self.install_platlib = target

def run(self, *args: Any, **kwargs: Any) -> None:
super().run(*args, **kwargs)

install_dir = (
getattr(self, 'install_lib', None)
or getattr(self, 'install_purelib', None)
or getattr(self, 'install_platlib', None)
)
if not install_dir or not os.path.isdir(install_dir):
return
pkg = self.distribution.get_name()
# Check if dist-info exists
has_egg_info = any(
name.endswith('.egg-info') and name.startswith(pkg)
for name in os.listdir(install_dir)
)
# If egg-info exists, we are installing a source distribution, we need to
# reorganize the files
if has_egg_info:
# First rename the shared library if present at top-level
self._rename_shared_library()
# Then canonicalize layout to a single top-level entry for this package
self._single_entry_layout()

def _rename_shared_library(self) -> None:
install_dir = (
getattr(self, 'install_lib', None)
or getattr(self, 'install_purelib', None)
or getattr(self, 'install_platlib', None)
)
if not install_dir or not os.path.isdir(install_dir):
return
pkg = self.distribution.get_name()
suffix = (
'.pyd'
if IS_WINDOWS
else ('.dylib' if OS_NAME.startswith('darwin') else '.so')
)
old = os.path.join(install_dir, f"{pkg}{suffix}")
new = os.path.join(install_dir, f"{pkg}_pd_{suffix}")
if os.path.exists(old):
if os.path.exists(new):
os.remove(new)
os.rename(old, new)

def _single_entry_layout(self) -> None:
"""
Ensure only one top-level item in install_dir contains the package name by:
- moving {pkg}.py -> {pkg}/__init__.py
- moving {pkg}_pd_.so -> {pkg}/{pkg}_pd_.so
- removing any {pkg}-*.egg-info left by setuptools install (only if dist-info exists)
This keeps legacy tests that scan os.listdir(site_dir) happy.
"""
install_dir = (
getattr(self, 'install_lib', None)
or getattr(self, 'install_purelib', None)
or getattr(self, 'install_platlib', None)
)
if not install_dir or not os.path.isdir(install_dir):
return
pkg = self.distribution.get_name()
# Prepare paths
pkg_dir = os.path.join(install_dir, pkg)
py_src = os.path.join(install_dir, f"{pkg}.py")
# Find compiled lib (renamed or not)
suf_so = (
'.pyd'
if IS_WINDOWS
else ('.dylib' if OS_NAME.startswith('darwin') else '.so')
)
so_candidates = [
os.path.join(install_dir, f"{pkg}_pd_{suf_so}"),
os.path.join(install_dir, f"{pkg}{suf_so}"),
]
so_src = next((p for p in so_candidates if os.path.exists(p)), None)
# Create package dir
if not os.path.isdir(pkg_dir):
os.makedirs(pkg_dir, exist_ok=True)
# Move python stub to package/__init__.py if exists
if os.path.exists(py_src):
py_dst = os.path.join(pkg_dir, "__init__.py")
if os.path.exists(py_dst):
os.remove(py_dst)
os.replace(py_src, py_dst)
# Move shared lib into the package dir if exists
if so_src and os.path.exists(so_src):
so_dst = os.path.join(pkg_dir, os.path.basename(so_src))
if os.path.exists(so_dst):
os.remove(so_dst)
os.replace(so_src, so_dst)


def load(
name: str,
sources: Sequence[str],
Expand Down
15 changes: 0 additions & 15 deletions python/paddle/utils/cpp_extension/extension_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,8 @@
import textwrap
import threading
import warnings
from contextlib import contextmanager
from importlib import machinery

from setuptools.command import bdist_egg

try:
from subprocess import DEVNULL # py3
except ImportError:
Expand Down Expand Up @@ -151,18 +148,6 @@
]


@contextmanager
def bootstrap_context():
"""
Context to manage how to write `__bootstrap__` code in .egg
"""
origin_write_stub = bdist_egg.write_stub
bdist_egg.write_stub = custom_write_stub
yield

bdist_egg.write_stub = origin_write_stub


def load_op_meta_info_and_register_op(lib_filename: str) -> list[str]:
new_list = core.load_op_meta_info_and_register_op(lib_filename)
proto_sync_ops = OpProtoHolder.instance().update_op_proto(new_list)
Expand Down
10 changes: 6 additions & 4 deletions test/cpp_extension/test_cpp_extension_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,15 @@ def setUp(self):
cmd += f' --install-lib={site_dir}'
run_cmd(cmd)

custom_egg_path = [
custom_install_path = [
x for x in os.listdir(site_dir) if 'custom_cpp_extension' in x
]
assert len(custom_egg_path) == 1, (
f"Matched egg number is {len(custom_egg_path)}."

assert len(custom_install_path) == 2, (
f"Matched egg number is {len(custom_install_path)}."
)
sys.path.append(os.path.join(site_dir, custom_egg_path[0]))

sys.path.append(os.path.join(site_dir, custom_install_path[0]))
#################################

# config seed
Expand Down
10 changes: 6 additions & 4 deletions test/cpp_extension/test_mixed_extension_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,15 @@ def setUp(self):
cmd += f' --install-lib={site_dir}'
run_cmd(cmd)

custom_egg_path = [
custom_install_path = [
x for x in os.listdir(site_dir) if 'mix_relu_extension' in x
]
assert len(custom_egg_path) == 1, (
f"Matched egg number is {len(custom_egg_path)}."

assert len(custom_install_path) == 2, (
f"Matched egg number is {len(custom_install_path)}."
)
sys.path.append(os.path.join(site_dir, custom_egg_path[0]))

sys.path.append(os.path.join(site_dir, custom_install_path[0]))
#################################

# config seed
Expand Down
10 changes: 6 additions & 4 deletions test/custom_op/test_custom_relu_op_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,15 @@ def setUp(self):
site_dir = site.getsitepackages()[1]
else:
site_dir = site.getsitepackages()[0]
custom_egg_path = [
custom_install_path = [
x for x in os.listdir(site_dir) if 'custom_relu_module_setup' in x
]
assert len(custom_egg_path) == 2, (
f"Matched egg number is {len(custom_egg_path)}."

assert len(custom_install_path) == 2, (
f"Matched egg number is {len(custom_install_path)}."
)
sys.path.append(os.path.join(site_dir, custom_egg_path[0]))

sys.path.append(os.path.join(site_dir, custom_install_path[0]))

# usage: import the package directly
import custom_relu_module_setup
Expand Down
10 changes: 6 additions & 4 deletions test/custom_op/test_custom_relu_op_xpu_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,17 @@ def setUp(self):
run_cmd(cmd)

site_dir = site.getsitepackages()[0]
custom_egg_path = [
custom_install_path = [
x
for x in os.listdir(site_dir)
if 'custom_relu_xpu_module_setup' in x
]
assert len(custom_egg_path) == 1, (
f"Matched egg number is {len(custom_egg_path)}."

assert len(custom_install_path) == 2, (
f"Matched egg number is {len(custom_install_path)}."
)
sys.path.append(os.path.join(site_dir, custom_egg_path[0]))

sys.path.append(os.path.join(site_dir, custom_install_path[0]))

# usage: import the package directly
import custom_relu_xpu_module_setup
Expand Down
10 changes: 6 additions & 4 deletions test/custom_op/test_inference_gap_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,15 @@ def setUp(self):

site_dir = site.getsitepackages()[0]

custom_egg_path = [
custom_install_path = [
x for x in os.listdir(site_dir) if 'gap_op_setup' in x
]
assert len(custom_egg_path) == 1, (
f"Matched egg number is {len(custom_egg_path)}."

assert len(custom_install_path) == 2, (
f"Matched egg number is {len(custom_install_path)}."
)
sys.path.append(os.path.join(site_dir, custom_egg_path[0]))

sys.path.append(os.path.join(site_dir, custom_install_path[0]))

# usage: import the package directly
import gap_op_setup
Expand Down
Loading