「消失的两个TA」闭源算子库迎来了一次 ABI 小手术

一次常规的 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 C1libapiinfer.so无(仅 C2)可 shim
gated_delta_netlibxops_blocks.so30 参不可 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 排查范式

这次排障可以固化为四步:

  1. 列需求:readelf/nm 列出 UND 符号,确定"谁缺什么";
  2. 找缺失:批量脚本扫描现有 .so 的符号表,与需求集做差,得到缺失集;
  3. 判 shim:缺失符号若为纯转发或位操作,可写 shim;若为签名演进,不可 shim;
  4. 配版本:不可 shim 时停止打补丁,按目录日期规律与 MD5 校验找配套包。
步骤工具判定要点
列 UND 符号readelf -Ws、nm -D需求方、符号名
扫符号表批量脚本 + c++filt缺失集、C1/C2 差异
shim 可行性签名对比转发/位操作可,签名演进不可
配套版本目录日期规律 + MD5符号与签名双重校验

判断 shim 可行性的分界线在于"差异是否可翻译"。C1 转 C2 是等价转发,bf16 转 float 是位操作,都属于可翻译差异。30 参变 31 参是接口演进,跨签名翻译没有可靠边界,属于不可翻译差异。越过这条线继续打补丁,收益会迅速归零。

结论

两个"消失的 TA"最终都不是消失,而是版本没有配对。闭源算子库的版本配对像侦探工作:先看符号,再看签名,最后对版本。shim 是过渡手段,解决的是可翻译的差异;签名演进只能靠配套版本收场。

标签:昆仑芯, ABI, 算子库