一次常规的 kunlun_ops 版本升级,在 import 阶段就停住了。报错只有一行:
ImportError: libapiinfer.so: undefined symbol: _ZN9xbfloat16C1Ef这行错误指向第一个"消失的 TA"。随后的排查又牵出第二个"消失的 TA"。两个符号的追踪最终指向同一个结论:符号没有消失,是版本没有配对。
背景
故障环境是昆仑芯 P800(8×96G)推理节点,运行 vllm-kunlun 与 vLLM,承载 Qwen3.8-27B 等在线服务。kunlun_ops 是独立于官方、使用汇编编写的算子库和运行库,以预编译 whl 形式分发。0.1.122 对应 torch25 编译环境,与 xmlir、torch 的版本强绑定。这类库以闭源二进制交付,符号依赖在构建期固化,版本配对是运行的第一前提。
本次升级的路径是 kunlun_ops 0.1.122(torch25 版)。升级完成后,import 直接命中 libapiinfer.so 的未定义符号。问题从一开始就不是逻辑错误,而是 ABI 层面的缺失。
第一个 TA:xbfloat16 的 C1 构造函数
先用 c++filt 还原符号名:
c++filt _ZN9xbfloat16C1Ef
# xbfloat16::xbfloat16(float)_ZN9xbfloat16C1Ef 是 xbfloat16::xbfloat16(float) 的 C1 完整构造函数。Itanium ABI 把构造函数拆成 C1(complete object constructor)与 C2(base object constructor)。对没有虚函数的类,C1 与 C2 行为等价,差异只在于虚表初始化。
对系统内全部 .so 做符号表扫描,结果很明确:现有 torch_xmlir 只提供 C2(_ZN9xbfloat16C2Ef),没有 C1。缺失集只有一个符号,且属于"可转发"类型。
过渡方案是写一个约 30 行的 C++ shim,导出 C1 符号并转调 C2,同时实现 _ZNK9xbfloat16cvfEv(bf16→float 转换,位操作左移 16 位)。精简后的核心如下:
// abi_shim.cpp
#include <cstdint>
#include <cstring>
extern "C" {
void _ZN9xbfloat16C2Ef(void*, float); // 借用已有 C2 实现
void _ZN9xbfloat16C1Ef(void* self, float v) { // 导出 C1,转调 C2
_ZN9xbfloat16C2Ef(self, v);
}
float _ZNK9xbfloat16cvfEv(const void* self) { // bf16 → float
uint16_t bits = *reinterpret_cast<const uint16_t*>(self);
uint32_t f = static_cast<uint32_t>(bits) << 16;
float out; std::memcpy(&out, &f, 4); return out;
}
}编译并注入:
g++ -shared -fPIC abi_shim.cpp -o abi_shim.so
LD_PRELOAD=$(pwd)/abi_shim.so python -c "import kunlun_ops"import 通过。这次 ABI 小手术临时生效,但马上暴露了下一个缺失符号。
第二个 TA:31 参数的 gated_delta_net
libxops_blocks.so 需要 baidu::xpu::xfa::gated_delta_net 的 31 参数版本。而 xmlir 1.0.0.1 的 20260409 版本只提供 30 参数版本。用 c++filt 对比两版签名,31 参版本在末尾多一个 float* 输出参数。
| 符号 | 需求方 | 0409 xmlir | 判定 |
|---|---|---|---|
| xbfloat16 C1 | libapiinfer.so | 无(仅 C2) | 可 shim |
| gated_delta_net | libxops_blocks.so | 30 参 | 不可 shim |
参数数量变化意味着调用约定与栈布局变化,shim 无法在 30 参与 31 参之间做翻译。这一发现同时说明 0.1.122 的编译环境比 0409 的 xmlir 更新。第一个 TA 可以被补丁按住,第二个 TA 拒绝配合。
破局:找配套版本
顺着 BOS 目录的日期规律排查,定位到 20260428/torch25 的 xpytorch 包(1.48GB)。下载后 MD5 校验通过,安装。该包携带的 xmlir 1.0.0.1(2026-04-22 build)同时提供 31 参 gated_delta_net 与 xbfloat16 C1。两个符号都在,shim 不再需要,LD_PRELOAD 从启动脚本中移除。
两个附带坑
0.1.122 的 Python 扩展 xpu_kunlun_ops、xpu_flash_ops 位于 whl 根目录(site-packages 顶层),不在包目录内,安装时容易漏。xmlir 升级后必须设置 XMLIR_DYNAMO_WORKAROUND=1,否则 torch.compile 在 make_tensors_stateful 上报 Unsupported。
方法论:闭源预编译库的 ABI 排查范式
这次排障可以固化为四步:
- 列需求:readelf/nm 列出 UND 符号,确定"谁缺什么";
- 找缺失:批量脚本扫描现有 .so 的符号表,与需求集做差,得到缺失集;
- 判 shim:缺失符号若为纯转发或位操作,可写 shim;若为签名演进,不可 shim;
- 配版本:不可 shim 时停止打补丁,按目录日期规律与 MD5 校验找配套包。
| 步骤 | 工具 | 判定要点 |
|---|---|---|
| 列 UND 符号 | readelf -Ws、nm -D | 需求方、符号名 |
| 扫符号表 | 批量脚本 + c++filt | 缺失集、C1/C2 差异 |
| shim 可行性 | 签名对比 | 转发/位操作可,签名演进不可 |
| 配套版本 | 目录日期规律 + MD5 | 符号与签名双重校验 |
判断 shim 可行性的分界线在于"差异是否可翻译"。C1 转 C2 是等价转发,bf16 转 float 是位操作,都属于可翻译差异。30 参变 31 参是接口演进,跨签名翻译没有可靠边界,属于不可翻译差异。越过这条线继续打补丁,收益会迅速归零。
结论
两个"消失的 TA"最终都不是消失,而是版本没有配对。闭源算子库的版本配对像侦探工作:先看符号,再看签名,最后对版本。shim 是过渡手段,解决的是可翻译的差异;签名演进只能靠配套版本收场。