博客算法工具链把六份 UCP 代码交给真实 SDK:一次编译与链接对照

把六份 UCP 代码交给真实 SDK:一次编译与链接对照

Sam Hui2026-10-09
10
0

把六份 UCP 代码交给真实 SDK:一次编译与链接对照

上次vol4做 OE-Skills 测评时,我让 Agent 生成了六份征程6 UCP C++ 代码,再检查它们有没有写对接口名称、内存处理和错误退出流程。那次还做了一项机器检查:用自己整理的简化头文件,让编译器检查代码的语法和接口引用,结果是四份通过、两份报错。

这次我把同样六份代码交给真实 SDK,分别执行编译和链接。原版结果变成了三份通过、三份失败。

多出来的失败是 G02。它使用的一个量化枚举名,在真实 SDK 里不存在,却被我之前的简化头文件定义了出来。代码和检查工具用了同一个错误名字,于是旧检查没有把它拦住。

我意识到,除了检查生成代码,还需要检查自己拿来判断代码的依据。下面就从这六份代码开始,把复验过程和结果展开。

一、为什么还要检查这六份代码?

UCP 是应用程序使用地平线推理能力的一组接口。应用通过这些接口加载模型、查询输入输出要求、分配内存、提交推理任务,再取得结果和释放资源。

在前面里,我审查的是 Agent 生成的六份代码,包括最小单模型推理、动态读取模型输入输出、量化输出处理、两模型并发、错误处理,以及 Host/Target 场景下的 C++ 示例。代码里有完整的流程和大量注释,但实际使用时,每一个函数名、参数和数据类型都要符合项目所用的开发包。

SDK 可以理解为官方提供的开发工具和接口实现。这里最直接用到的是两部分:头文件告诉编译器有哪些类型和函数、怎样调用;库文件提供这些函数的实现。

上一轮没有使用真实 SDK 完成构建,而是自己整理了一组简化头文件,也就是 stub。它方便检查代码能否按照这些声明被解析,但这些声明是否与官方接口一致,还需要核实。

因此,这轮保留原代码,直接使用随包头文件和库。研究的问题是:这六份代码在 OE 3.9.1 的真实接口下,哪些能编译、哪些能链接,失败又发生在哪里?

二、这次做到了哪一步?

先把编译和链接分开理解。

编译会检查 C++ 源代码的语法、类型和接口引用,并生成目标文件。例如,代码使用一个从未声明过的枚举名称,就会在这一步报错。
链接则把目标文件和它依赖的库组合起来,生成可执行文件或共享模块。某个函数即使已经声明,如果没有找到它的实现,也可能在链接时失败。

这轮流程是:

实验在本地官方 OE CPU 容器内执行。此次只构建代码,没有运行生成的推理程序。本次复验使用 AI Agent Codex 辅助完成,辅助工作包括构建脚本编写、接口核对与修复、日志分析、图表生成。

项目

实验设置

开发包与镜像

OE 3.9.1,openexplorer/ai_toolchain_ubuntu_22_j6_cpu:v3.9.1

编译器与架构

GCC 12.2.0,x86_64

头文件与库

随包 samples/ucp_tutorial/deps_x86/ucp/

代码标准

六份原代码使用 C++17,官方样例使用 C++11

构建方法

直接调用 GCC,分别保存编译与链接日志

容器里没有 CMake。我按照官方样例 CMakeLists.txt 中列出的头文件路径、库路径和依赖,直接调用 GCC。这是一次 x86 SDK 构建检查,没有执行完整 CMake 工程构建或 ARM 交叉编译。
正式检查前,我先构建两个未修改的官方样例:model_example.cc 和 resnet_rgb/src/main.cc。两者都通过了编译和严格链接检查,说明当前路径和依赖能够完成这两个样例的构建。随后再把同样的 SDK 用于六份生成代码。
原代码单独保留。每份 CPP 都与原回答中的代码块核对,并保存文件指纹 SHA-256,修改只发生在另外建立的副本中。G02、G05 本来没有程序入口 main,我把它们作为函数模块检查,链接成共享模块;其他四份代码则按有入口的程序处理。

三、原版结果:三份通过,三份在编译阶段失败

代码

主要内容

旧 stub 语法检查

真实 SDK 编译

原版严格链接

G01

最小单模型推理

通过

通过

通过,可执行文件

G02

动态查询模型输入输出、分配内存

通过

失败

未进入

G03

反量化函数与模拟输出示例

失败

失败

未进入

G04

两模型并发

通过

通过

通过,可执行文件

G05

错误处理与资源清理

通过

通过

通过,共享模块

G06

Host/Target 回答中的 C++ 代码块

失败

失败

未进入

图1-六份代码构建对照.png

图1|六份固定代码的逐例结果。原版与修复副本分别记录;G02、G05 的链接对象是共享模块。图中结果来自保存的构建日志。

“未进入”表示编译已经失败,所以没有继续链接。原版通过编译和链接的是 G01、G04、G05。

这张表比较的是旧 stub 检查与新的 SDK 构建,编译器和检查方式也有变化。下面对 G02 的判断,会同时结合真实头文件、旧 stub、报错和补丁来说明原因。G06 本轮只检查 C++ 代码块,没有把原回答中的 Python 示例算进来。

四、G02:旧检查为什么放过了错误名称?

G02 会查询模型的张量属性,包括数据形状、内存步长、缓冲区大小和量化信息。它在判断量化类型时写了:

这段代码想表达的是:如果当前张量使用 scale 量化,就打印量化参数的数量。思路可以理解,问题出在用来表示量化类型的名称。

真实编译日志在这一行报错:

意思是:编译器在当前声明中找不到 HB_DNN_QUANTI_TYPE_SCALE。
我查看本次 SDK 的 hobot/dnn/hb_dnn.h,实际枚举是:
其中 NONE 表示不量化,SCALE 表示按 scale 量化。在这份 SDK 下,代码应该比较 SCALE:

只改这一处后,G02 副本就通过了编译和严格链接。

回头看旧 stub,原因也很清楚。它的定义是:

我给编译器提供的头文件里,确实有代码使用的那个名字。因此旧编译器接受了它。生成代码的错误与 stub 的错误恰好吻合,检查就出现了漏检。

这还影响了旧检查给出的修复建议。G03 当时使用了另一个名称 HB_DNN_QUANTI_SCALE,旧编译器提示可以改成 HB_DNN_QUANTI_TYPE_SCALE。建议来自 stub 中已有的声明,而那个声明本身就是错的。

这次需要更正的是这部分检查依据:旧 PASS 是代码符合当时的简化声明;真实 SDK 则确认了 G02 使用的名称不兼容。核对具体接口时,实际开发包中的声明比根据命名习惯整理出来的名称更可靠。

五、G03 和 G06:接口修复分别改了什么?

G03 的错误同样来自量化枚举名。它在反量化函数和模拟输出初始化中,分别使用了 HB_DNN_QUANTI_SCALE。将这两处替换为 SCALE 后,副本通过了编译和严格链接。

G06 则先停在头文件引用上:

这表示当前 SDK 的 include 路径里没有代码引用的那个文件。继续对照源码和真实声明,可以看到它在当前 UCP 流程中混入了旧式 hbSys 调用,还使用了两个不符合当前 SDK 的释放函数名。

修复副本的主要变化如下。表中的写法针对本次 OE 3.9.1 SDK。

处理环节

原写法

修复后的写法

引用系统内存接口

hobot/hb_sys.h

hobot/hb_ucp_sys.h

分配可缓存内存

hbSysAllocCachedMem(&mem, size)

hbUCPMallocCached(&mem, size, 0)

维护缓存

hbSysFlushMemCache(&mem, flag)

hbUCPMemFlush(&mem, flag)

释放内存

hbSysFreeMem(&mem)

hbUCPFree(&mem)

释放模型资源

分别调用 hbDNNReleaseModelHandle 和 hbDNNReleasePackedHandle
调用 hbDNNRelease(packed_handle)

修改后的 G06 也通过了编译和严格链接。原版首轮日志停在缺失头文件处;后续接口修改及其结果,分别保存在补丁和修复日志中。

至此,三个失败案例都取得了接口修复后的构建结果:G02 改一处枚举名,G03 改两处枚举名,G06 调整头文件和相关接口调用。原版三份通过与三个修复副本通过分别记录,便于看清问题来自哪里、修改影响了什么。

六、一个链接选项,怎样让缺失函数的代码也返回成功?

检查官方样例的 CMake 配置时,我注意到一个选项:

-Wl, 表示把后面的选项传给链接器,ignore-all 则要求忽略未解析符号。所谓未解析符号,可以用一个简单情况理解:代码调用了某个函数,声明也有了,但链接时找不到实现。
这种情况下,编译和链接会出现不同结果。编译器知道函数怎样调用,就可以先生成目标文件;链接器随后才需要找出函数实现。GNU ld 官方文档说明,ignore-all 会使链接器不报告这些未解析符号。

为了观察该选项的影响,我构造了下面这个小反例。它不依赖 UCP,只声明并调用一个没有实现的函数:

将它保存为 link_policy_control.cpp,先编译,再分别用两种策略链接:

编译返回 0,宽松链接也返回 0;严格链接返回 1,并报告:

这里 0 表示命令报告成功,1 表示这次命令失败。函数实现始终没有提供,改变的是链接器处理缺失符号的策略。

我还让两个未修改的官方样例分别使用宽松与严格策略链接,两种情况下都通过。完整对照如下:

图2-链接策略对照.png

图2|同一环境下的编译与链接退出码。两个官方样例在两种策略下都通过;缺失符号反例只在严格策略下报错。所有产物均未执行。

因此,记录“链接成功”时,还要记录采用的链接选项。本轮六份代码及修复副本的正式链接没有使用 ignore-all。对没有 main 的共享模块,还加入了 --no-undefined 检查普通目标文件中的未解析引用,并使用 --no-allow-shlib-undefined 检查所链接共享库中的未解析引用。

七、通过构建的代码,仍有哪些问题留在源码里?

G05 就是一个很具体的例子。它负责错误处理和资源清理,在真实 SDK 下通过了编译和链接。可是沿源码检查,错误检查宏在失败时会跳到清理路径,局部错误码没有保存到最终返回值,函数末尾固定写着:

如果走到这些失败分支,函数清理完资源后,仍然会向调用者返回成功。保存错误状态,再在清理结束后返回,是这个函数需要另外解决的逻辑问题。

编译器接受它并不奇怪:这样的控制流在语法上成立,函数引用也能解析。构建工具检查了代码如何组成程序,而代码在出错时应当向上层报告什么,还需要沿控制流继续审查。

G03 的接口修复也没有处理全部逻辑。它的示例仍把普通 vector 地址和手填物理地址放进系统内存描述,再调用缓存维护接口;反量化函数也没有按张量 stride,也就是实际内存步长,定位元素。这些来自源码的风险没有被枚举名补丁消除。

所以我保留两类记录:编译与链接日志定位当前 SDK 下的名称、签名和符号问题;源码审查继续追踪数据布局、内存来源和失败路径。本次三个修复副本通过的是接口构建检查。

八、这次复验得到的结果

原版与修复版都在新的隔离容器和新输出目录中重复检查过。原版仍然是 G01、G04、G05 通过,G02、G03、G06 编译失败;三个修复副本仍全部编译、严格链接通过。输入文件未改动,逐例结果一致。每一步的参数、退出码、原始日志和补丁都保存下来,便于复查。

回到文章开头的问题:为什么旧检查通过的代码,换成真实 SDK 后会报错?G02 给出了具体答案——旧 stub 把一个并不存在于本次 SDK 的名称写进了头文件,检查因此放过了同样使用这个名字的代码。

这轮实验也把原来停在源码里的判断落实到了构建结果上:哪些原代码可以被当前 SDK 接受,哪些需要修改,修改后是否通过,以及链接选项怎样影响检查结论,都有对应记录。

参考资料

算法工具链
社区征文征程6
评论0
0/600