专栏算法工具链看起来很完整的 UCP C++,真的靠谱吗?我逐行审了 6 份 OE-Skills 生成代码

看起来很完整的 UCP C++,真的靠谱吗?我逐行审了 6 份 OE-Skills 生成代码

361922026-08-28
2
0

前几篇里,我一直在测试 OE-Skills 在征程6工具链任务中的表现。

从 Router 路由、Skills / MCP 对回答质量的影响,到长链路规划、无板能力边界,再到第六篇直接拿真实社区问题做外部 Gold 盲测,研究对象一直比较接近:

Agent“说出来的答案”到底靠不靠谱?

但做到这里以后,我觉得还有一个更实际的问题没有回答。

如果不只是让 Agent 解释 UCP,而是直接让它:

给我写一份征程6 UCP C++ 推理代码。

那这些代码到底能不能用?

第一眼看起来完整,有模型加载、有 Tensor、有 hbUCPMemFlush、有推理、有释放,甚至注释也很详细。

但:

  • API 名字真的存在吗?

  • 参数和调用顺序对吗?

  • stride 有没有被忽略?
  • Cache Clean / Invalidate 用对了吗?

  • 中途报错时资源能不能释放?

  • 多线程失败以后程序会不会还打印“成功”?

  • Host 和 J6 Target 有没有混在一起?

这些问题靠“代码看起来挺专业”是判断不了的。

所以 Exp07 我不再评测回答文本,而是第一次把研究对象换成了:

Agent 真正生成出来的 C++ 工程产物。

我固定了 6 个 UCP 代码任务,然后逐份做 API、生命周期、内存、Cache、错误处理和平台边界审计。

结果也让我重新认识了“生成代码质量高”这句话。

第一轮检查时,6 份代码拿到了:

115 / 120。

其中甚至有 3 份被评成 20 / 20。

但真正开始逐行复核,再加入机械 syntax-only 检查以后:

最终只剩 102 / 120。

而且:

6 份代码,没有一份满分。


一、这次到底让 Agent 写了什么?

这次没有只生成一份“Hello World 式”的 UCP 示例。

我固定了 6 个不同方向的代码任务。

G01:最小单模型推理

要求覆盖一条基本完整的 UCP 推理链:

模型加载
→ 获取模型 Handle
→ 查询输入输出
→ 分配 Tensor 内存
→ 填充输入
→ Cache Clean
→ 推理任务提交
→ 等待完成
→ Cache Invalidate
→ 读取输出
→ 释放资源

这是最基础的一题。


G02:动态读取模型 I/O

这一题故意不提供真实输出 Shape 和量化参数。

目的是看 Agent 会不会凭经验直接写死:

  • Tensor Shape;

  • Stride;

  • Buffer 大小;

  • Scale;

  • Zero-point。

正确做法应该优先读取模型实际的 Tensor Properties,再决定内存和后处理。


G03:量化输出与后处理

要求读取量化输出,并完成从定点 Tensor 到业务后处理之间的转换。

这一题主要看:

  • 量化类型判断;

  • Scale / Zero-point;

  • Stride;

  • 内存来源;

  • Cache;

  • 反量化和业务解码有没有混在一起。

后来这也是问题最多的一份代码。


G04:两个独立模型并发

两个 HBM 没有数据依赖。

要求 Agent 设计合理的并发结构,同时避免:

  • Model Handle 混用;

  • Buffer 共享;

  • Task Handle 生命周期问题;

  • 多线程错误状态丢失。


G05:错误处理与资源清理

这一题我专门要求考虑:

  • 模型加载失败;

  • 内存申请失败;

  • 推理提交失败;

  • Wait 失败。

重点不是“正常流程能跑”,而是:

出错以后还能不能正确退出。


G06:Host / Target 边界

最后一题故意把 PC、hbm_infer 和 UCP 放到一起。

看 Agent 是否能区分:

  • x86 Host;

  • 远程 hbm_infer;
  • J6 Target;

  • 板端原生 UCP C++。

这也是为了检查它会不会再次出现:

“在 PC 上跑 HBM = 在 J6 BPU 上本地执行”

这种边界混淆。


二、我怎么给代码打分?

每份代码固定检查 10 个维度,每项 0~2 分。

单份满分 20 分,6 份合计 120 分。

维度

主要检查内容

C1 API 真实性

函数、枚举、结构体是否真实

C2 API 使用

参数、签名、调用方式是否合理

C3 生命周期

加载、Tensor、Infer、Wait、Release 是否完整

C4 Metadata

Shape、Stride、alignedByteSize 是否正确处理

C5 Cache

Clean / Invalidate 是否放在正确阶段

C6 内存安全

Buffer、Handle、异常路径是否存在泄漏

C7 错误处理

错误码有没有检查和向上传播

C8 Host / Target

是否混淆 x86 与真实 J6 Runtime

C9 不越权

是否虚构“已编译”“已跑通”

C10 可读性

代码结构和未决条件是否清晰

6 个任务在生成之前就固定,后面不因为代码表现好坏换题。

而且生成出来的原始代码也不允许人工修改以后重新评分。

article07_figure01_ucp_lifecycle_reference.png

图1|本次审计参考的 UCP 推理主生命周期。代码不仅要“出现这些 API”,还要保证内存、Cache、Task 和资源释放之间的调用关系正确。


三、第一轮结果其实非常漂亮:115 / 120

第一次检查完,结果看起来甚至有点太好了。

6 个任务的初审分数分别是:

  • G01:19 / 20

  • G02:18 / 20

  • G03:20 / 20

  • G04:20 / 20

  • G05:20 / 20

  • G06:18 / 20

总分:

115 / 120,95.8%。

三个任务直接满分。

如果在这里停下来,这篇文章可能会变成:

“OE-Skills 生成 UCP C++ 的质量已经非常高。”

但后来我重新逐行看代码时,越来越觉得不对。

因为一些代码虽然:

  • API 很多;

  • 注释很完整;

  • 六步流程也基本都有;

但这并不代表每一个细节都是真的。

所以我又加了一轮更严格的人工复核,并额外做了一个机械检查:

把 Horizon 官方 API 声明整理成只读 stub header,再使用 clang++ -fsyntax-only 检查代码的语法和 API Surface。

这里需要强调:

syntax-only ≠ OpenExplorer 真正编译通过。

它只能检查:

  • C++ 语法;

  • 已声明 API;

  • 一部分函数 / 枚举名称。

它不能证明:

  • 能链接真实 UCP;

  • 能生成 aarch64 程序;

  • 能在 J6 上运行;

  • 输出正确;

  • 性能达标。

但就算只做这一步,结果已经变了。


四、真正复审以后:115 掉到了 102

第二轮最终成绩变成:

任务

第一轮

最终复审

Syntax-only

G01 最小单模型推理

19

18

PASS

G02 动态读取模型 I/O

18

17

PASS

G03 量化输出与后处理

20

15

ERROR

G04 多模型独立并发

20

17

PASS

G05 错误处理与资源清理

20

18

PASS

G06 Host / Target 边界

18

17

ERROR

最终总分:

102 / 120,85.0%。

均分:

17.0 / 20。

更重要的是:

原来的 3 个满分,最终全部消失。

6 份代码最终没有一份 20 / 20。

机械检查则是:

4 PASS / 2 ERROR。

出现 ERROR 的正好是 G03 和 G06。

article07_figure02_code_audit_matrix.png

图2|6 份生成代码第一次初审与第二轮严格复审结果。G03、G06 在 API-surface syntax-only 检查中直接出现 ERROR。

为什么差了整整 13 分?

下面几个案例最典型。


五、G03:一个枚举名,就足以让“完整代码”直接报错

G03 第一轮其实拿了:

20 / 20。

从结构上看也确实很像一份完整代码:

  • 读取 Tensor Properties;

  • 判断量化类型;

  • 读取 Scale;

  • 做反量化;

  • 处理 Cache;

  • 再进入业务后处理。

但机械检查直接抓到:

代码使用了:

而当前参考接口并不存在这个写法。

也就是说:

一份视觉上非常完整、逻辑也解释得通的代码,因为一个 API 常量写错,就可能连最基本的接口表面检查都过不了。

这还不是 G03 唯一的问题。


六、更隐蔽的问题:普通 std::vector 不能假装成 UCP 物理内存

G03 里还有一段代码,先创建普通 CPU 内存:

然后把它手工塞进 hbDNNTensor 的系统内存描述中,再调用:

第一眼看非常合理:

既然 BPU 写了数据,CPU 读取之前做 Cache Invalidate。

但问题是:

这块内存根本不是通过 UCP 内存接口合法申请出来的。

正常 UCP Tensor 内存应该基于真实 Tensor Properties,再通过类似:

这样的接口取得合法的系统内存。

hbUCPMemFlush 操作的是 UCP 管理的内存对象。
普通 std::vector 的 CPU 虚拟地址不能靠手填字段,就自动变成一块可供 UCP Cache Flush 的物理内存。

所以这里的问题不是:

“忘记 Flush”。

反而是:

Flush 写了,但 Flush 的对象本身不合法。

我觉得这比单纯漏一个 API 更值得注意。

因为生成代码最容易给人安全感的东西,恰好就是:

它把正确 API 写出来了。

但 API 出现了,不代表作用对象就是对的。


七、Stride 也不是“拿到 Shape 以后 flatten 就行”

G03 的另一个问题是输出反量化。

代码主要按照 validShape 计算元素数量,然后连续遍历 Buffer。

这在普通紧密连续 Tensor 中很自然。

但 J6 的 Tensor Properties 里不只有:

还有:

真实硬件 Tensor 可能因为对齐存在 Padding。

因此:

逻辑 Shape 连续,不代表底层物理内存也是完全连续排列。

如果代码只按:

一直往后读,而不考虑真实 stride,可能会把 Padding 当成有效数据,或者从错误偏移读取下一行。

这一题最后在 Metadata / Stride 上也被扣了分。

这让我觉得,对 AI 生成的硬件侧代码来说:

“Shape 对了”可能还不够,内存布局才是真正容易出错的地方。


八、G04:正常路径漂亮,不代表失败路径安全

G04 是两个模型并发推理。

它第一轮也是:

20 / 20。

代码确实做了很多正确事情:

  • 两套 Model Handle;

  • 各自独立 Tensor;

  • 独立 Task;

  • 两个工作线程;

  • 最后 join。

但真正顺着失败路径往下看,会发现一个问题。

假设:

已经创建了 task_handle。

接下来:

或者:

失败。

代码会直接 return。

但此时已经创建的 task_handle 没有走:

于是异常路径存在句柄泄漏风险。

还有一个更容易被忽略的问题:

某个工作线程里面即使已经推理失败,主线程 join 完以后仍然可能无条件打印:

Concurrent Inference Finished successfully!

然后:

也就是说:

内部已经失败,最外层却告诉调用方成功。

这和 API 幻觉完全是两类问题。

API 可能全部是真的,代码甚至能过 syntax-only。

但工程语义仍然是错的。


九、G05:资源都清干净了,结果还是错

G05 专门测试错误处理。

它采用了一种很常见的 C 风格写法:

然后在统一的:

下面释放已经申请的资源。

这一点本身其实不错。

问题出现在函数最后:

无论前面到底发生了什么错误,清理完以后最后都告诉调用者:

成功。

于是出现一个很有意思的现象:

资源管理做对了,但 API 语义依然错了。

更合理的方式应该是保存真实错误状态,例如:

article07_figure03_bug_vs_fix_case_study.png

图3|统一 cleanup 本身不是问题,真正容易漏的是错误状态传播。清理资源以后仍需把失败状态返回给上层。

这也是这次我觉得最有工程味的一个问题。

因为它根本不是“AI 不知道 API”。

它知道 API。

它甚至知道怎么 cleanup。

但还是可能在最后一行把整个错误处理逻辑毁掉。


十、G06:Host / Target 边界答对了,API 却混版本了

G06 的表现也很典型。

从架构理解上,它其实做得不错。

它能够正确区分:

  • x86 Host;

  • hbm_infer;
  • J6 板端服务;

  • 原生 UCP C++;

  • 真正 BPU 执行发生在哪里。

也没有把 PC 仿真写成真实 J6 Runtime。

所以如果只评“解释是否正确”,这题可能会拿很高分。

但代码部分却出现了另一类问题:

新旧 API 混用。

例如出现了旧体系里的:

而当前 J6 UCP 路线使用:

更严重的是,代码还写出了:

在删除人为补充的假声明以后,syntax-only 检查直接报:

这说明 Agent 可能:

知道“这里应该释放资源”,但把另一个 SDK / 版本 / 命名习惯里的 API 拼到了当前代码里。

这是 LLM 生成工程代码里很典型的一类风险:

语义正确,符号错误。


十一、甚至 Python 侧也藏了一个 TypeError

G06 不只有 C++。

它还生成了一段 hbm_infer Python 示例。

代码想打印:

并用:

格式化。

但 get_profile() 返回的不是单个 float,而是类似:

因此:

实际上还是一个字典。

直接:

会触发 TypeError。

正确逻辑应该继续取:

或者使用对应的单帧 Profile 接口。

这又是一个“看起来完全合理,但运行时马上出错”的例子。


十二、那 OE-Skills 到底有没有用?

做到这里不能反过来得出:

“Agent 生成 UCP 代码完全不能用。”

这个结论同样不成立。

这 6 份代码仍然有不少比较稳定的共同点。

例如:

  • 没有把 HBM 生成直接写成部署完成;

  • 没有伪造真实 J6 实测结果;

  • Host / Target 总体边界没有混乱;

  • 大部分代码知道输入前需要 Cache Clean;

  • 读取 BPU 输出前需要 Cache Invalidate;

  • 大部分代码知道应该查询 Tensor Properties;

  • 基本生命周期主链路总体是存在的。

最终 6 个任务仍然全部处于:

15~18 / 20。

也就是说,它们并不是完全不可用的随机代码。

我更愿意把结果理解成:

OE-Skills 可以帮助 Agent 生成一份“方向比较正确的 UCP 初稿”,但这还不能替代真正的工程 Code Review。

特别是:

  • API exact identifier;

  • SDK 版本;

  • Stride;

  • 物理内存;

  • 异常路径;

  • 错误码传播;

这些地方仍然非常值得人工检查。


十三、为什么第一轮会拿到 115,第二轮只剩 102?

我觉得这次最值得反思的甚至不是某一个 API。

而是:

为什么第一次看代码时,这些问题很容易被漏掉?

答案可能很简单。

因为一份 AI 生成代码如果同时具备:

  • 很长;

  • 注释清楚;

  • API 名很多;

  • 六步生命周期完整;

  • 有错误检查宏;

  • 有 cleanup;

  • 有线程封装;

人很容易先形成一个整体印象:

“这份代码应该挺完整。”

然后后面的审查会受到这个印象影响。

例如看到:

第一反应可能是:

“Cache 处理写了,加分。”

而不是继续问:

“它 Flush 的到底是不是合法 UCP 内存?”

看到:

第一反应可能是:

“异常路径清理考虑到了。”

而不是继续看到最后:

看到一个很像官方风格的枚举:

也可能下意识认为它存在。

所以这次最终从:

115 / 120

掉到:

102 / 120

对我来说反而比一个稳定的高分更有意义。

它说明:

AI 代码审计不能只看结构完整度,还要做符号级、对象级和失败路径级检查。


十四、机械检查有用,但也不能被夸大

这次 clang++ -fsyntax-only 抓到了 G03 和 G06。

最终结果:

4 PASS / 2 ERROR。

它确实很有用。

至少能把:

  • 未声明函数;

  • 未声明枚举;

  • 某些签名问题;

从“人工感觉”变成机器可复现结果。

但我仍然不会把这写成:

“4份代码已经成功编译。”

因为本实验没有使用完整真实 OE / UCP Build 环境完成交叉编译,更没有在 J6 板端运行。

这里的 PASS 只能表示:

在依据官方 API 声明构造的 syntax-only 环境中,没有发现相应语法/API Surface 错误。

而且 G04 和 G05 正好证明:

Syntax PASS 也不等于工程正确。

它们都通过了机械检查,却仍然存在错误状态和资源管理问题。

所以真正更稳的代码审计可能应该是:

API / Syntax
→ 生命周期
→ 内存与 Cache
→ 失败路径
→ 真正 OE Build
→ J6 板端验证

而不是只跑一个 Compiler 就结束。


十五、这次实验能说明什么,不能说明什么?

这次可以说明的是:

  • 6 份 OE-Skills 辅助生成的 UCP 代码总体能够保持征程6推理主流程;

  • 第一轮表面审计容易高估代码质量;

  • 严格复审后总分从 115 / 120 降到 102 / 120;

  • 6 份代码没有一份最终满分;

  • syntax-only 检查中 4 份 PASS、2 份 ERROR;

  • 出现的问题不只有 API 幻觉,还有 Stride、物理内存、Task 生命周期和错误传播。

不能说明的是:

  • 102 / 120 等于真实代码“85%可用”;

  • 4 个 syntax PASS 的程序已经完成真实 OE 编译;

  • 这些程序已经可以直接部署到 J6;

  • 本次已经在 J6 上验证推理输出;

  • 本次已经测到任何真实性能、FPS 或 P95。

因为这些都没有发生。

所以 85.0% 只是:

本次预注册 10 维静态工程评分体系下的代码质量得分。

不是板端部署成功率。


十六、从“Agent 会回答”到“Agent 生成的东西能不能交付”

做到第七次实验以后,我感觉测试对象已经变了很多。

最开始,我关心的是:

Agent 知不知道应该调用哪个 Skill?

后来是:

它回答得对不对?

再后来是:

它能不能处理真实社区问题?

而这次终于变成:

它真正生成的工程产物,经不经得住逐行审查?

最终结果给我的感觉是:

能生成,不等于能交付。

一份生成代码可能:

  • 架构是对的;

  • 生命周期也基本完整;

  • 注释甚至比人写得详细;

但真正进入工程以后,一个枚举、一个 Stride、一个失败路径、一个 return 0,都足以把“看起来完整”变成实际 Bug。

所以如果以后继续把 Agent 用在征程6开发里,我可能不会只问:

“它会不会写 UCP?”

而会多问一句:

“它写完以后,我有没有一套机制自动检查 API、版本、内存和失败路径?”

对我来说,这可能比继续让模型生成更长的代码更重要。


参考资料

本文只记录 OE-Skills 辅助生成的征程6 UCP 代码静态工程审计。未使用真实 J6 开发板完成代码运行,未声明完整 OpenExplorer 交叉编译通过,也未产生真实板端性能或精度数据。文中的 102 / 120 为本次预注册评分体系下的静态代码质量得分,不代表真实部署成功率。

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