🏠 总目录📚 本教程 04 · ABI 与二进制兼容 ← →
📑 本页目录(点开跳转)

04 · ABI 与二进制兼容

⏱ 84 分钟 | ⭐ 「编译得过」和「装得上、跑得起来」是三道不同的关,你那个报错断在其中一道


🎯 一句话

头文件写的是「怎么调用」,ABI 定的是「调用时字节怎么摆」—— 后者不写在任何一份代码里,而两边只要理解得不一样,程序就会在三个不同的地方之一坏掉。 最后一种坏法不报错,它只是把你的 batch=128 读成了 159。

⭐ 这一章是设计成能单独读的,不需要前面三章。


🧩 一、先划界:「装不上」其实有两半

⚠️ 「我装不上这个包」是个复合症状,它的两半归两个不同的板块:

哪一半 长什么样 归哪
Python 那一半 装到哪个环境去了、import 找的是哪个路径、版本怎么解出来的、锁文件 《Python 会咬你的地方》
⭐ 二进制那一半 undefined symbol、cannot open shared object file、明明装上了却崩 这一章

这一章的立项依据是站内一句留了半边的话。《AI 全栈》14 在讲「在我机器上能跑」的三种死法时,第二种写着:

⚠️ 系统库不一样:某个 pip 包依赖 libpq / libgomp。你机器上早有(因为你装过别的东西),干净的服务器上没有 → ImportError: libxxx.so.1: cannot open shared object file

那一章给的解药是容器,并且明确写了容器不解决什么,其中第二条是「GPU 驱动 / CUDA 是宿主机的事」。⭐ 也就是说:最高频的那一类装不上,被划到了它自己的范围之外。 这一章接的就是那个位置。

📋 本章要拆的三道关:

关 什么时候坏 典型报错
① 符号对不上 链接 / 加载符号时 undefined symbol: _ZN3c10...
② 库文件找不到 加载动态库时 libxxx.so.1: cannot open shared object file
💀 ③ 布局对不上 不坏,只是数据是错的 没有报错

🧩 二、C++ 的函数名,不是你写的那个

要理解 ① 得先知道一件事:你写的函数名不会原样出现在编译产物里。

C++ 允许重载 —— 同名不同参的函数可以有好几个。但链接器只认名字,不认参数。所以编译器会把参数类型、命名空间、是不是 const 全部编码进名字里,这个过程叫 name mangling(名字修饰)。

// c04_names.cpp
// g++ -O2 -std=c++17 -c c04_names.cpp -o c04_names.o
#include <string>
#include <vector>

int  add(int a, int b)              { return a + b; }          // 重载 ①
double add(double a, double b)      { return a + b; }          // 重载 ②
int  add(int a, int b, int c)       { return a + b + c; }      // 重载 ③

namespace mylib {
    struct Engine {
        int run(const std::string& s) { return (int)s.size(); }
        int run(const std::vector<int>& v) { return (int)v.size(); }
    };
    int helper(const Engine&, double) { return 0; }
}

extern "C" {                          // ⭐ 关掉名字修饰
    int add_c(int a, int b) { return a + b; }
}

编译成目标文件,然后把里面的符号列出来(nm 是 binutils 自带的):

$ nm -g --defined-only c04_names.o | grep " T "
0000000000000010 T _Z3adddd
0000000000000000 T _Z3addii
0000000000000020 T _Z3addiii
0000000000000030 T _ZN5mylib6helperERKNS_6EngineEd
0000000000000040 T add_c

同一批符号,加个 -C 让它解回人话:

$ nm -gC --defined-only c04_names.o | grep " T "
0000000000000010 T add(double, double)
0000000000000000 T add(int, int)
0000000000000020 T add(int, int, int)
0000000000000030 T mylib::helper(mylib::Engine const&, double)
0000000000000040 T add_c

⭐ 两条要记住的:

🚦 用 ctypes 亲眼看一次

把这两种函数编进一个动态库,然后从 Python 按名字去找:

# c04_findsym.py
import ctypes, os

lib = ctypes.CDLL(os.path.abspath('c04lib.dll'))     # Linux/macOS 换成 ./c04lib.so
for name in ['add_c', 'add_cpp', '_Z7add_cppii']:
    try:
        f = lib[name]
        f.restype = ctypes.c_int
        f.argtypes = [ctypes.c_int] * 2
        print(f'  {name:16} -> 找到了, add(2,3) = {f(2,3)}')
    except AttributeError as e:
        print(f'  {name:16} -> ❌ {type(e).__name__}: {e}')

对应的库这么编:

// c04_lib.cpp
// g++ -O2 -std=c++17 -shared -static c04_lib.cpp -o c04lib.dll
int add_cpp(int a, int b) { return a + b; }            // C++ 名字会被修饰
extern "C" int add_c(int a, int b) { return a + b; }   // ⭐ 保持原名

实跑输出:

结果对照

add_c -> 找到了, add(2,3) = 5
add_cpp -> ❌ AttributeError: function 'add_cpp' not found
_Z7add_cppii -> 找到了, add(2,3) = 5

⭐ 第二行和第三行是同一个函数。 按你写的名字找 —— 找不到;按修饰后的名字找 —— 找到了。

⚠️ 所以下次看到 undefined symbol: _ZN3c10... 不要被吓到:_Z 开头就是一个被修饰过的 C++ 名字,3c10 是命名空间 c10(PyTorch 的基础库就叫这个名字)。把它丢给 c++filt 就能读:

$ echo '_ZN5mylib6helperERKNS_6EngineEd' | c++filt
mylib::helper(mylib::Engine const&, double)

🧩 三、API 和 ABI 差在哪

现在可以给 ABI 一个准确的说法了。

API ABI
规定什么 源码怎么写才能调用(函数名、参数、类型) 编译后的字节怎么摆:符号叫什么名、参数放哪个寄存器、结构体每个字段在第几个字节、异常怎么传
写在哪 头文件、文档 ⚠️ 哪儿都没写,由编译器 + 编译选项 + 标准库版本共同决定
不兼容的表现 编译报错,一眼能看见 链接报错 / 加载报错 / 或者什么都不报
谁来检查 编译器 ⚠️ 没人(链接器只对名字,不对含义)

⭐ 一句话:API 兼容 = 能重新编译;ABI 兼容 = 不用重新编译就能换。

⚠️ 能悄悄改变 ABI 的东西比你想的多:编译器版本、C++ 标准版本、标准库的实现和版本、优化选项、是否开异常、结构体里加一个字段、给函数加一个默认参数、给类加一个虚函数……其中大部分都不会让源码编译失败。


🧩 四、第一道关:符号对不上

最经典的一次 ABI 断裂,是 GCC 5 改了 std::string 的内部结构。为了让老程序还能用,libstdc++ 同时保留了两套实现,用宏 _GLIBCXX_USE_CXX11_ABI 切换。

同一份源码,只改这一个宏,看符号名会怎样:

// c04_abi.cpp
#include <string>
int consume(const std::string& s);
int consume(const std::string& s) { return (int)s.size(); }

要点

$ g++ -std=c++17 -c c04_abi.cpp -o abi1.o # 默认 (=1)

$ nm -g --defined-only abi1.o | grep consume

0000000000000000 T _Z7consumeRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE

$ g++ -std=c++17 -D_GLIBCXX_USE_CXX11_ABI=0 -c c04_abi.cpp -o abi0.o

$ nm -g --defined-only abi0.o | grep consume

0000000000000000 T _Z7consumeRKSs

💀 同一个函数、同一份源码,两个完全不同的符号名。 解回人话之后更要命,两边看起来几乎一样:

consume(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)
consume(std::string const&)

把它们拿去链接,实跑结果:

结果对照

### ① 两边 ABI 一致 -> 链接成功 ###
5
### ② 用旧 ABI 编的实现 + 默认 ABI 的调用方 -> 链接失败 ###
ld.exe: c04_use.cpp:(.text+0x3c): undefined reference to
`consume(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)'
collect2.exe: error: ld returned 1 exit status

⭐ 这就是 undefined symbol 家族的真面目:不是「函数没实现」,是两边对同一个函数名的编码方式不一致。你去源码里找,函数明明白白写在那儿。

⚠️ 看到 std::__cxx11 出现在报错里,基本可以确定是这个宏对不上。

📋 这件事在 PyTorch 生态里的具体形态:预编译的 libtorch 是用某一套设定编的,你的扩展必须用同一套。本机装的 torch 报告的是:

# c04_torch_abi.py
import torch
print("torch          =", torch.__version__)
print("_GLIBCXX_USE_CXX11_ABI =", torch._C._GLIBCXX_USE_CXX11_ABI)

实跑输出:

torch          = 2.13.0+cpu
_GLIBCXX_USE_CXX11_ABI = True

⭐ 这个值必须和你编扩展时用的一致,对不上就是链接期一堆 undefined symbol: _ZN3c10...。⚠️ 同理还有编译器版本和 CUDA 版本 —— 那些 -cu121 / -cu124 后缀的轮子就是在标这件事:它们不是「支持 CUDA 12.1」,是「用 CUDA 12.1 编的,二进制只和那一套对得上」。


🛑 读到这里可以停 —— 前半章讲完了(约 27 分钟)。 后半章还有:第二道关:库文件找不到 · 💀 第三道关:什么都不报,数据是错的 · 于是有了 wheel 标签和 manylinux · 📋 一张排查表 回来的时候不用重读,直接从下一节接着看就行。


🧩 五、第二道关:库文件找不到

符号都对,也可能在加载那一步就死了 —— 因为你的库依赖别的库,而那些库不在。

先看一个动态链接的库依赖谁:

$ objdump -p c04dyn.dll | grep "DLL Name"
    DLL Name: KERNEL32.dll
    DLL Name: msvcrt.dll
    DLL Name: libstdc++-6.dll

ldd 更进一步,把每个依赖实际解析到了哪个文件也打出来:

算一算

$ ldd c04dyn.dll

ntdll.dll => /c/WINDOWS/SYSTEM32/ntdll.dll (0x7ff9ceba0000)

KERNEL32.DLL => /c/WINDOWS/System32/KERNEL32.DLL (0x7ff9cddd0000)

KERNELBASE.dll => /c/WINDOWS/System32/KERNELBASE.dll (0x7ff9cc2e0000)

msvcrt.dll => /c/WINDOWS/System32/msvcrt.dll (0x7ff9ccb10000)

libstdc++-6.dll => /mingw64/bin/libstdc++-6.dll (0x7ff93bc50000)

libgcc_s_seh-1.dll => /mingw64/bin/libgcc_s_seh-1.dll (0x7ff9b5cc0000)

libwinpthread-1.dll => /mingw64/bin/libwinpthread-1.dll (0x7ff9a39d0000)

(后面还有几行系统库,略)

⭐ ldd 的正确读法:你要找的不是列出来的那些,是没被列出来的那一行 —— 在 Linux 上缺失的依赖会显示成 => not found。那一行就是你要装的东西。

🚦 ⚠️ 报错指着 A,缺的其实是 A 的依赖

这是这道关最误导人的地方。实跑三种情况:

# c04_load.py
import ctypes, os
p = os.path.abspath('c04dyn.dll')

print('① 直接加载(依赖 libstdc++-6.dll 不在搜索路径里):')
try:
    ctypes.CDLL(p); print('   ✅ 成功')
except OSError as e:
    print('   ❌', type(e).__name__, ':', str(e)[:110], '...')

print('② 先 os.add_dll_directory 指出依赖在哪,再加载:')
with os.add_dll_directory(r'C:\Users\manaf\mingw64\bin'):
    lib = ctypes.CDLL(p)
    lib.strlen_cpp.restype = ctypes.c_int
    lib.strlen_cpp.argtypes = [ctypes.c_char_p]
    print('   ✅ 成功,strlen_cpp(b"hello") =', lib.strlen_cpp(b'hello'))

print('③ 对照:静态链接的那个 dll,什么都不用做:')
ctypes.CDLL(os.path.abspath('c04lib.dll')); print('   ✅ 成功')

实跑输出:

操作步骤

  1. 直接加载(依赖 libstdc++-6.dll 不在搜索路径里):
  2. ❌ FileNotFoundError : Could not find module 'C:\ · \c04dyn.dll' (or one of its dependencies) ·
  3. 先 os.add_dll_directory 指出依赖在哪,再加载:
  4. ✅ 成功,strlen_cpp(b"hello") = 5
  5. 对照:静态链接的那个 dll,什么都不用做:
  6. ✅ 成功

💀 注意 ① 那句报错:它说找不到 c04dyn.dll,可那个文件明明就在那儿。 缺的是它的依赖 libstdc++-6.dll。括号里那句 or one of its dependencies 是唯一的线索,而它太容易被忽略了。

⭐ Linux 上的同一件事长这样:ImportError: libxxx.so.1: cannot open shared object file —— 报错里那个 libxxx.so.1 通常不是你 import 的那个模块,而是它依赖的系统库。这正是《AI 全栈》14 章描述的那个症状。

📋 三种解法,各有代价:

解法 做什么 代价
装上那个系统库 apt install libgomp1 之类 要有权限;换台机器要重来
⭐ 静态链接(实验 ③) 把依赖编进自己的库 产物变大;某些库不能这么做
⭐ 打包进 wheel(下一节) 把依赖的 .so 一起塞进去并改好搜索路径 标准做法,auditwheel 干这活

⚠️ 一个 Windows 专有的坑:Python 3.8 之后,ctypes / 扩展模块在 Windows 上不再用 PATH 找依赖,要用 os.add_dll_directory()。所以「我把它加进 PATH 了啊」在新版本 Python 上是无效操作 —— 上面实验 ② 用的就是正确写法。


🧩 六、💀 第三道关:什么都不报,数据是错的

前两道关至少会告诉你出事了。这一道不会。

场景很常见:库升级了,加了个字段;而你这边还在用旧头文件编译。

// lib_v2.cpp —— 「库」这一侧(新版:中间插了一个字段)
#include <cstdio>
struct Config {
    int       version;
    long long lr_scale;      // ⭐ v2 新加的,插在中间
    long long batch;
};
void lib_show(const Config& c) {
    std::printf("  [库里读到的] sizeof=%zu  version=%d  lr_scale=%lld  batch=%lld\n",
                sizeof(Config), c.version, c.lr_scale, c.batch);
}
// app_v1.cpp —— 「调用方」这一侧(还在用旧头文件)
#include <cstdio>
struct Config {
    int       version;
    long long batch;
};
void lib_show(const Config& c);
int main() {
    Config c{7, 128};
    std::printf("  [我传进去的] sizeof=%zu  version=%d  batch=%lld\n",
                sizeof(Config), c.version, c.batch);
    lib_show(c);
    return 0;
}

实跑(连跑三次,结果完全一致):

操作步骤

  1. 编译:两边都通过
  2. 链接:成功,零警告
  3. 运行:
  4. [我传进去的] sizeof=16 version=7 batch=128
  5. [库里读到的] sizeof=24 version=7 lr_scale=128 batch=159
  6. 退出码 = 0

💀💀 batch 传进去是 128,库里读到 159。没有报错,没有警告,退出码 0。

⭐ 机制:调用方认为 Config 是 16 字节(version 在偏移 0,batch 在偏移 8);库认为是 24 字节(batch 在偏移 16)。于是库从偏移 16 开始读 —— 那个位置已经在这个对象之外了,读到的是碰巧躺在那里的东西。而 lr_scale 读到 128,正是调用方本来放 batch 的位置。

⚠️ 两个额外的观察,都是我在做这个实验时撞出来的:

📋 这就是为什么生态里有那么多看起来很啰嗦的规矩:

规矩 挡的是什么
头文件和二进制必须同一个版本,不许混用 就是上面这件事
库的接口只用不透明指针 + extern "C" 函数,不在接口上暴露结构体 结构体布局变了也不影响调用方
PyTorch 扩展要求 torch 版本、编译器、CUDA、ABI 宏全部对齐 三道关一起挡
升级依赖之后重新编译所有扩展,而不是只换一个 避免混用

🧩 七、于是有了 wheel 标签和 manylinux

上面三道关合起来说明一件事:一个编译好的二进制,不是「哪都能跑」的。 那 pip 装轮子的时候是怎么知道该给你哪一个的?

答案是文件名里写着。本机的规则可以直接问出来:

# c04_tags.py
import sysconfig, sys
print("EXT_SUFFIX =", sysconfig.get_config_var('EXT_SUFFIX'))
print("SOABI      =", sysconfig.get_config_var('SOABI'))
print("version    =", sys.version.split()[0])

实跑输出:

对照

EXT_SUFFIX = .cp313-win_amd64.pyd

SOABI = cp313-win_amd64

version = 3.13.14

看一眼装好的 numpy,每个编译产物都带着这个后缀:

要点

_pocketfft_umath.cp313-win_amd64.pyd

lapack_lite.cp313-win_amd64.pyd

_umath_linalg.cp313-win_amd64.pyd

bit_generator.cp313-win_amd64.pyd

mtrand.cp313-win_amd64.pyd

共 19 个

pip 认得的完整标签列表也能直接问:

对照

$ python -m pip debug --verbose

Compatible tags: 45

cp313-cp313-win_amd64

cp313-abi3-win_amd64

cp313-none-win_amd64

cp312-abi3-win_amd64

cp311-abi3-win_amd64

·

⭐ 标签有三段:cp313(哪个 Python)· cp313 / abi3 / none(对 ABI 的要求)· win_amd64(哪个平台)。

中间那段 意思
cp313 只和 CPython 3.13 的 ABI 对得上,换个小版本就得重编
⭐ abi3 只用了 Python 的稳定 ABI 子集,一次编译能在多个版本上用(表里 cp34-abi3 到 cp313-abi3 都在,所以本机能装很老的 abi3 轮子)
none 不含扩展模块,纯 Python

⚠️ win_amd64 这一段在 Linux 上要难得多:Windows 和 macOS 各自的系统库比较统一,而 Linux 发行版之间 glibc 版本天差地别 —— 在新系统上编的轮子,拿到老系统上就是「找不到符号」(第①道关(符号对不上)的另一种形态:glibc 的符号是带版本号的)。

⭐ manylinux 就是为这件事发明的标签,思路只有两句:

  1. 在足够老的 glibc 上编译,这样新系统也能用(向后兼容是单向的)。
  2. 只许依赖一个很小的白名单里的系统库,白名单之外的依赖必须打包进 wheel 自己带着。

标签的命名换过一次:早期是 manylinux1 / manylinux2010 / manylinux2014 这种按「年份」的写法,后来改成直接写 glibc 版本的 manylinux_x_y 形式(例如 manylinux_2_17、manylinux_2_28),因为按年份根本看不出兼容边界在哪。

⭐ 两套是对应的,manylinux2014 就是 manylinux_2_17。

做这件事的工具叫 auditwheel:它检查你的 wheel 依赖了哪些库、把不在白名单里的复制进去、再把搜索路径改好。

🗓️ 这一小段没法在本机演示 —— 上面的标签输出是 Windows 的,manylinux 是 Linux 侧的规范。⚠️ 规范本身会变(新的 glibc 基线会不断加进来),用之前查一下当前状态。

📋 回到最开头那几个「老是装不上」的包:

现象 哪一道关
pip install 直接开始编译源码(然后失败) 没有匹配你这套标签的轮子,只能退回源码编译
装上了,import 时 undefined symbol: _ZN3c10... ① 符号对不上(torch 版本 / ABI 宏 / 编译器不一致)
装上了,import 时 libcudart.so.12: cannot open... ② 库找不到(CUDA 运行时不在,或版本号不是 12)
装上了,能 import,结果不对或偶发崩溃 💀 ③ 布局对不上

⭐ 绝大多数情况下最省事的解法是:不要自己编,去找和你这套(Python 版本 + torch 版本 + CUDA 版本 + 平台)完全匹配的预编译轮子。 那些项目通常有一个很长的下载表,⚠️ 表里那些后缀不是装饰,是上面三道关的编号。


🛑 读到这里可以停 —— 已经读了约 57 分钟。 最后一段还有(约 22 分钟):📋 一张排查表 · 检查点与走神救援 回来的时候不用重读,直接从下一节接着看就行。


🧩 八、📋 一张排查表

拿到一个二进制层面的报错,按这个顺序走:

症状里有 断在 先做什么
undefined symbol: _Z... / undefined reference to ① 符号 把符号丢给 c++filt 读出来;查 torch 版本、编译器版本、_GLIBCXX_USE_CXX11_ABI 是否一致
报错里出现 std::__cxx11 ① 符号 基本可以断定是 _GLIBCXX_USE_CXX11_ABI 对不上
cannot open shared object file / Could not find module ② 库文件 ldd 看哪一行是 not found;⚠️ 缺的通常不是报错里那个名字,是它的依赖
Windows 上「明明加进 PATH 了」 ② 库文件 用 os.add_dll_directory(),3.8 之后 PATH 不管用了
pip install 开始编译源码 标签 没有匹配的轮子;先找预编译版而不是硬编
没有报错,但结果不对 / 偶发崩溃 💀 ③ 布局 检查头文件和二进制是不是同一个版本;重新编译所有扩展

🔗 这一章连到哪里

相关的地方 为什么
《AI 全栈》14 ⭐ 本章的立项依据:它描述了 libxxx.so.1: cannot open shared object file 这个症状、给了容器这个解药、又明写容器不解决 GPU/CUDA。本章拆的就是它留下的那半边
《Python 会咬你的地方》 「装不上」的另一半:装到哪个环境、import 找哪个路径、版本怎么解、锁文件。⭐ 本章一个字都不碰那一半
01 章 那一章的示例代码里写着 extern "C" 并注了一句「关掉名字修饰,ctypes 才找得到」—— 第二节是这句话的完整解释
03 章 结构体的对齐空洞是那一章讲的;⭐ 第六节那个「新字段正好填进空洞、于是碰巧全对」的坑,就是它在这一章的化身
05 章 写扩展会真的撞上本章三道关;那一章讲注册机制,本章讲为什么编出来的东西装不上
AI 基础设施 00 它的「前置知识」清单里把 CUDA 编程列为「❌ 不需要」,而 -cu121 这类轮子的装不上问题落在本章第七节

✅ 检查点

  1. 「我装不上这个包」的两半分别是什么?各归哪个板块?
  2. 本章拆的三道关分别在什么时候坏?第三道关的报错长什么样?
  3. 为什么 C++ 的函数名会被改?c04_names.cpp 里三个 add 编出来的符号分别叫什么?
  4. ctypes 那个实验里,add_c / add_cpp / _Z7add_cppii 三个名字分别是什么结果?说明了什么?
  5. API 兼容和 ABI 兼容各是什么意思?ABI 写在哪份文件里?谁负责检查它?
  6. 同一份 consume(const std::string&),两种 _GLIBCXX_USE_CXX11_ABI 下的符号名分别是什么?链接失败时报错长什么样?看到哪个字符串基本能断定是这个问题?
  7. 本机 torch 的 _GLIBCXX_USE_CXX11_ABI 是多少?-cu121 这种后缀到底在标什么?
  8. ldd 的正确读法是什么?为什么说「报错指着 A,缺的其实是 A 的依赖」?举出实跑里那句误导人的报错。
  9. Windows 上 Python 3.8 之后为什么「加进 PATH」不管用了?该用什么?
  10. 结构体布局不一致那个实验:传进去的 batch 是多少,库里读到多少?编译、链接、退出码分别是什么情况?
  11. 为什么说「测了一下没问题」在第三道关上不能当证据?(提示:作者第一版实验发生了什么)
  12. wheel 标签的三段各是什么?abi3 和 cp313 差在哪?
  13. manylinux 的两句核心思路是什么?它的命名为什么从 manylinux2014 改成了 manylinux_2_17 这种写法?
👀 答案
  1. Python 那一半(装到哪个环境、import 找哪个路径、版本解析、锁文件)→ 《Python 会咬你的地方》;二进制那一半(undefined symbol、cannot open shared object file、装上却崩)→ 本章。
  2. ① 符号对不上,在链接 / 加载符号时坏;② 库文件找不到,在加载动态库时坏;③ 布局对不上,⚠️ 不坏,没有任何报错 —— 只是数据是错的。
  3. 因为 C++ 允许重载(同名不同参),而链接器只认名字不认参数,所以编译器把参数类型、命名空间、是否 const 全编码进名字。三个 add 分别是 _Z3addii(int,int)、_Z3adddd(double,double)、_Z3addiii(int,int,int)。
  4. add_c 找到了(extern "C",名字没变);add_cpp 找不到(AttributeError: function 'add_cpp' not found);_Z7add_cppii 找到了。后两个是同一个函数 —— 按源码里的名字找不到,按修饰后的名字才找得到。
  5. API 兼容 = 能重新编译(规定源码怎么写);ABI 兼容 = 不用重新编译就能换(规定编译后字节怎么摆:符号名、参数放哪、结构体字段偏移、异常怎么传)。⚠️ ABI 哪份文件里都没写,由编译器 + 编译选项 + 标准库版本共同决定;没人检查它,链接器只对名字不对含义。
  6. 默认(=1)是 _Z7consumeRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE,=0 是 _Z7consumeRKSs。链接失败报 undefined reference to consume(std::__cxx11::basic_string<...> const&)。看到 std::__cxx11 基本可以断定是这个宏对不上。
  7. 本机 torch 2.13.0+cpu,_GLIBCXX_USE_CXX11_ABI = True。-cu121 不是「支持 CUDA 12.1」,是「用 CUDA 12.1 编的,二进制只和那一套对得上」。
  8. 要找的不是列出来的那些,是没被列出来的那一行 —— Linux 上缺失的依赖显示成 => not found。实跑那句误导的报错是 Could not find module 'C:\...\c04dyn.dll' (or one of its dependencies) —— 它点名的那个文件明明存在,缺的是它的依赖 libstdc++-6.dll,唯一线索是括号里那句 or one of its dependencies。Linux 上同理:libxxx.so.1 通常不是你 import 的模块本身。
  9. 因为 Python 3.8 之后,Windows 上加载扩展/动态库不再用 PATH 解析依赖。要用 os.add_dll_directory()(实验 ② 的写法)。
  10. 传进去 batch=128,库里读到 batch=159;lr_scale 读到 128(调用方本来放 batch 的位置)。编译两边都通过、链接成功且零警告、退出码 0 —— 三次跑结果完全一致。调用方认为 sizeof=16(batch 在偏移 8),库认为 sizeof=24(batch 在偏移 16,已经在对象之外)。
  11. 因为作者第一版实验里新加的字段是 int,正好填进了旧结构体因对齐留下的 4 字节空洞,于是三个字段读出来全对。⚠️ 换个字段类型、换个平台、换个编译器就变成错的 —— 所以「测了一下没问题」完全不能作为证据。另外 排在新字段前面的字段全都对、后面的全错,症状像「就某几个参数不对」。
  12. 三段:哪个 Python(cp313)· 对 ABI 的要求(cp313 / abi3 / none)· 哪个平台(win_amd64)。cp313 只和 CPython 3.13 的 ABI 对得上,换小版本要重编;abi3 只用稳定 ABI 子集,一次编译能在多个版本上用(本机的兼容标签里 cp34-abi3 到 cp313-abi3 都在)。
  13. 两句:① 在足够老的 glibc 上编译(向后兼容是单向的);② 只许依赖一个很小的白名单里的系统库,白名单外的必须打包进 wheel 自己带着(auditwheel 干这活)。改名是因为按年份的写法根本看不出兼容边界在哪,直接写 glibc 版本才看得出来;两套是对应的,manylinux2014 就是 manylinux_2_17。

🛑 可以停在这里

⚡ 走神救援

🚪 「编译得过」和「装得上、跑得起来」是三道不同的关:① 符号对不上(链接/加载时报错)· ② 库文件找不到(加载时报错)· 💀 ③ 布局对不上——不报错,数据是错的。

🏷️ 先理解名字:C++ 允许重载而链接器只认名字,所以编译器把参数类型和命名空间全编码进符号里。⭐ 所以 undefined symbol: _ZN3c10... 里的 _Z 就是修饰过的名字,丢给 c++filt 就能读。

⭐ API 兼容 = 能重新编译;ABI 兼容 = 不用重编就能换——而 ABI 哪儿都没写、没人检查。⚠️ 看到 std::__cxx11 基本能断定是那个老新 ABI 开关不一致;-cu121 不是「支持 CUDA 12.1」而是「用 CUDA 12.1 编的」。

第二道关用 ldd 查,⭐ 要找的是没列出来的那一行。💀 实测那句「找不到某个 dll」而文件明明就在那儿——缺的是它的依赖,而线索只有报错里那半句「or one of its dependencies」。⚠️ Windows 上 Python 3.8 之后 PATH 不管用,要用专门的 API 加目录。

💀💀 第三道关最贵:库的结构体中间插了个字段,调用方还用旧头文件——编译通过、链接零警告、退出码 0,但传进去的参数被读成完全不相干的数。⚠️ 排在新字段前面的字段全对、后面的全错,看起来像「就某几个参数不对」。

⭐ 而且它有时碰巧正确:新字段如果正好填进旧结构体的对齐空洞,读出来全对——所以「测了一下没问题」在这道关上完全不能当证据。

下一节 👉 05-算子怎么注册进PyTorch.md

打卡记录保存在你的浏览器里,首页能看到总进度