这个内测模型帮我把端侧推理速度飙升了37倍!记录Agent工程师的第一次昇腾生态算子优化经历
这个内测模型帮我把端侧推理速度飙升了37倍!记录Agent工程师的第一次昇腾生态算子优化经历
你好,我是司沐。
写在前面:本文只是笔者作为一个Agent工程师,第一次体验AI跑算子优化的过程记录与感想,并不是严谨的教学或者科普,因为笔者并非此领域的专家,过程中肯定会有一些事实性错误。如果您发现了哪里有问题,欢迎评论区指出与讨论,言辞激烈一些也没有关系,这样反而能帮助新人理解,与加深印象。
本文提到的代码已经同步上传Github仓库:minicpmv-ascend-orangepi。代码纯Vibe,仅供交流学习,也希望能帮上同样想在香橙派AI Pro上跑VLM的小伙伴~
上个月 WAIC 给我们团队送的香橙派 AIpro 8T 终于到货了,它有 8GB 内存,4 核 CPU,板载一块昇腾 310B4 NPU。到货之后,我左看看右看看,想知道这块 NPU 性能到底怎么样,能不能推一个端侧的小VLM,接一些传感器和外设,让它在阳台帮我种番茄。
如你所见,我的本职工作是 Agent 工程师,虽然平时也用过 torch、vllm,但对 NPU 算子与推理优化可以说是几乎一窍不通。模型这一层倒是熟,原始权重下过不少,safetensors、bin、gguf、onnx 的区别清楚,量化版本挑哪个心里有数,各种推理框架都是常用工具,但再往下一层就断了。
模型到底怎么在一块芯片上被执行起来,为什么每出一个新模型都要重新适配一遍,为什么同样的硬件换一套算子性能能差出几十倍,这些问题我一直没有时间弄明白。有时候看别人的优化博客,名词有点眼熟,连起来不知道在讲什么。
正巧,那几天赶上阶跃星辰给我发了新模型 step-explore 的内测权限,说是提升很大。于是我就抱着试试看的想法,把它接进 Claude Code,让它从零开始,试试能不能在这块新生态的昇腾 NPU 上把 MiniCPM-V 4.6 跑起来。
动手之前,我真不觉得这事能成。这种算子优化的任务,我以为只有 A\ 它家的强模型才做得动,其他模型来了都得铩羽而归。何况昇腾这套生态还新,网上能抄的东西不多,适配过的视觉模型更是只有 DeepSeek 去年出的那个OCR模型,所以很多坑得自己一个一个踩。
7 月 27 日早上七点四十开工,中间几次停顿,8 月 1 日凌晨五点多收工。
结果,家人们,它真做到了?!
| 阶段 | 解码速度 |
|---|---|
| ollama 跑 4-bit 量化版,纯 CPU | 0.15 字每秒 |
| 第一版跑上 NPU | 1.19 |
| 凭直觉合并了一批算子调用 | 1.19,没动 |
| 绕开一个病态的卷积 | 4.94 |
| 去掉最后一处会触发重编译的写法 | 5.46 |
| 给递归步单独开一条路径 | 5.65 |
| 这块板子的物理上限 | 7.8 |
最后一行是它自己实测标定出来的天花板,5.65 已经是这块板子物理极限的 72%。生成一百个字,CPU 版要 653 秒,NPU 版 17.7 秒,快了 37 倍。

图 1:解码速度单位为字每秒;5.65 已达到这块板子物理上限 7.8 的约 72%。
到最后那天,网页界面做出来以后,我传了一张我的鱼缸照片上去,问他看到了什么。一秒多的首字延迟后,token以每秒五个的速度往外冒,汩汩的,有种异样的舒畅感。
那一下我是真有点激动。这块板子三天前都不认识这个模型。
经历了这些惊喜后,很难不生出分享的念头。一开始我想把整个流程梳理好,然后把代码po出去,但后来细细一想,像我这样只做Agent,但想了解算子优化这种新领域的工程师应该还不少。如果我也写一篇那样的技术博客,既比不上专业人士的专业度,又让有兴趣的人难看懂。
与其这样,不如就以我这个几乎不懂模型推理优化的人的视角,分享我在AI完成这些任务过程中的震撼,思考,以及带领一些同样不熟悉这个领域,却怀有好奇心的读者,一起走一下全流程,看看模型推理优化写算子,和我们平时做的写Agent、微调模型有什么区别,应该如何理解这个过程。
我们开始吧!
为什么不能拿现成的框架跑
我一开始的设想很省事:华为这么重视昇腾生态,网上应该有开源解决方案吧?慢就慢点,先跑起来。
explore 花了小半天翻代码,回来说这条路是堵的。310B4 这块NPU在生态内有点不上不下,已经停止维护很久了不说,性能也不是非常哇塞,所以很少有人用它跑大模型。
那直接从权重运行呗?香橙派上有带自己的 torch 和 transformer 库的。explore 又摆摆手,说问题不在权重。2.6GB 的 safetensors 没问题,问题在于跑这个模型需要的模型代码,仓库里没带。MiniCPM-V 4.6 的实现被合进了 transformers 5.7,仓库里既没有 modeling 文件也没有 auto_map,trust_remote_code 那条路走不通。
而 transformers 5.7 要求 Python 3.10 以上、torch 2.4 以上。板子上那套昇腾的 torch 适配是出厂预装的,编译的时候就绑死在 Python 3.9 和 torch 2.1,换不了。两头凑不到一起。explore 也看过 MindSpore,那边压根没有这个模型的支持,用它等于全量重写。
explore 选的路是照着权重文件里的 key,把这个模型在板子上重新实现一遍。二十七层视觉、二十四层语言,每一层的算法都得跟官方对上,然后把参数一个不差地灌进去。
我当时没意识到这句话的分量,于是就很甩手掌柜,留下一句:你看着办,尽力完成任务。
explore 对于这条指令,足足跑了5个小时。这五个小时是我最忐忑的时候,因为我也看不懂他在干什么(其实就没看),不知道几个小时之后,他会给我端上来惊喜还是屎山。
后来,我看了它的工作轨迹。explore 在开始前,先是顺手做了另一件事:在板子上另开了一个纯 CPU 环境,装新版 Python 和 transformers,用官方代码把模型正确跑一遍,把中间每一步的结果全存下来。图片预处理出来的张量、分词结果、每一层的输出、最后生成的一百个字,都存成文件。
这套东西成了整个项目的 baseline。它自己那份实现每改一次,就跟这个结果对一次,差在哪儿一眼能看见。后面每一个结论、每一次"这个改动不影响正确性"的说法,靠的都是它。

图 2:这里的 baseline 是用于逐层核对正确性的 CPU 参考结果,不是性能对比基准。
我现在觉得这是全程最要紧的决定,而它是在还没写第一行推理代码的时候就想到的。
这也顺带回答了我那个"为什么每个新模型都要单独适配"。要适配的东西落在模型里每一个具体的算法结构上。这个模型的语言部分是混合注意力,二十四层里有十八层是 Gated DeltaNet,这种结构去年才流行起来,昇腾这边的老库没有现成的模型结构可用。结构一新,底下那套跑它的东西就得重新长一遍。

图 3:概念示意:当后端没有现成支持时,适配工作需要覆盖模型权重结构中的每一层,并为其建立对应的执行路径。
通顺的乱码
7 月 27 日下午,模型第一次在 NPU 上跑通,输出是一段乱码。
不是那种一看就崩了的乱码。有一点点语法,词之间有一点点关系,中间还夹着几种语言,读起来像一个伪人学说话。它贴给我看的时候,我第一反应是权重哪里算错了。
可所有能查的地方都是好的。参数一个不多一个不少地对上了,视觉那部分跟baseline比对相似度 0.999925,每个环节单独拿出来测都正常。
它找这个 bug 的办法是把范围一层层收紧。先拿 CPU 上那把尺子,看 NPU 这边从第几层开始跟标准答案对不上,锁到语言模型里的 Gated DeltaNet。然后让 CPU 和 NPU 在同一个进程里并排跑这一个模块,每算完一小步就打印数字的最大值,看哪一步的量级突然跳变。
跳的地方是一个补零操作。
这里插一句算子是什么,我自己也是这时候才彻底清楚。模型跑起来的时候,芯片上执行的是一个一个很小的动作,比如两个矩阵相乘,比如取最大值,比如把一段数据后面补几个零凑够长度、。每个动作背后都有一段专门写给这块芯片的程序。一个模型跑一次,就是这些小程序按固定顺序被调用几百上千遍。
补零是最不起眼的那种。Gated DeltaNet 要求序列长度是 64 的倍数,实际长度 91,所以要往后补 37 个零。
结果那块地方补出来的不是零,是没被清理过的内存,绝对值最大到 1e37。不报错,不崩溃,就这么混进后面的运算。原本 0.27 的中间结果变成 21008,原本 4.26 的状态变成 43 亿。数字全崩,模型照常吐字,只是吐出来的话没有意义。
它讲清楚这件事的时候,我第一句话是,这不是最基础的函数吗,怎么会连这个都错。
它的回答大意是,这个函数在英伟达卡上没问题,在这块昇腾芯片配 CANN 7.0 上有问题,而这个组合用的人不多。改法很简单,换成拼接一段确定是零的数据上去。
改完之后,prefill 的输出跟标准答案相似度 0.999855,生成的一百个字逐字一致。

图 4:CPU 参考结果作为 baseline;NPU 在“补零”步骤产生未清理过的内存,替换为显式零拼接后,生成的一百个字逐字一致。
看到 explore 做到这一步,我有种很奇妙的感觉:它可能真的能完成任务。
一个函数怎么可能慢一千倍
跑对了,但慢。每秒 1.19 个字。
7 月 28 日我给它派了个活,先跟 ollama 上那个 4-bit 版本比一下速度,再看看算子还有多大提升空间,顺便估一下 500 字输入、200 字输出的时候每个环节要多久。我想知道的就一件事,这东西能不能用。
它先按直觉改了一版。判断是模型每吐一个字要触发大概 300 次算子调用,瓶颈肯定在调用次数上,于是把 Gated DeltaNet 的四个输入投影合成一次矩阵乘,每个字少发 60 次调用。
改完测,1.1906 变成 1.1887。只快了一点点,误差级别。
它自己在报告里写,“我的直觉是错的”。然后它不猜了,写了个脚本把每一处单独掐表。
| 掐表结果 | 耗时 | 占一步 840 毫秒的比例 |
|---|---|---|
| 300 次算子调用的开销总和 | 12 毫秒 | 1.4% |
| 18 个线性注意力层 | 755 毫秒 | 89% |
| 其中那一个卷积(18 层各一次) | 671 毫秒 | 80% |
| 6 个全注意力层 | 37 毫秒 | 4% |
单个算子的调用开销只有 27 到 45 微秒,它之前改的方向从根上就不对。真正吃掉时间的是一个卷积,单次 37.30 毫秒。
我看到这儿的疑问是,一个函数怎么可能慢成这样。
它的解释我理解成这样。这些算子都是针对具体硬件、具体数据形状(shape)专门写出来的程序。同一个功能,形状换一个,就可能落到一条没人认真优化过的路上,退化成很笨的实现。这个卷积要分成 6144 组分别算,这块芯片处理这种形状特别吃力。
接下来这一手我看得赏心悦目。explore 先回头看了这一步数学上到底在算什么,然后发现,生成的时候这个卷积的输入宽度只有 5,卷积核宽 4,而且只取最后一个输出。展开来就是 4 个数乘 4 个权重加起来,别的什么都没有。剩下那些复杂度全是通用函数自带的。于是它把这个函数整个绕开,换成一次乘法加一次求和,然后在 CPU 上跟原函数逐元素比对,差异 1e-6,只有浮点舍入的量级。
37.30 毫秒变成 1.86 毫秒,快了 20 倍。整体 1.19 变成 4.94。这中间隔了一个晚上,28 日凌晨三点改完的。

图 5:逐算子计时后发现,真正瓶颈是单次 37.30 毫秒的卷积;将其替换为等价的乘加后,整体速度由 1.19 提升至 4.94,约为 4.2 倍。
我睡醒起来看到这个数字的时候是懵的。前一天晚上我还在琢磨这东西是不是就这样了,一秒钟一个字,拿来看看图凑合,别的干不了。
这就是我那第三个问题的答案里最实的一块。差别不在硬件,在于你调用的那段程序有没有落在被人优化过的路上。落不上,它照样把结果算对,只是慢一千倍,而且不会有任何东西提醒你。
同样的剧本后来又演了两次。做多轮对话的时候,追问一句要等 9 到 10 秒,它先猜是块宽设大了,改完只快 1.2 倍;再去掐表,发现真凶是一行取长度的代码逼着芯片停下来等主机回话,而这一行在 18 个层里各跑一遍,把整个队列排空。改掉之后 9.5 秒降到 1.42 秒。
三次都一样。凭直觉改,零收益或者 1.2 倍;掐完表再改,20 倍、4.2 倍、5.8 倍。
占用才 5%,你怎么好意思说榨干了
7 月 28 日下午,我提了个要求,以后跑分的时候顺手把 CPU 和 NPU 的占用率一起记下来,我想看看到底榨出了多少性能。
数字出来我就懵了。NPU 的核心占用率只有 5% 左右。而它在报告里写的是,单序列解码基本榨干了。
基于自己早期调显卡集群推理的经验,我猜可能是通信带宽受限。于是我发过去一条消息:“那我想知道,为什么NPU占用这么低,你却说性能全榨干了?是带宽限制吗?解释一下”。
它没有嘴硬,也没有绕。上来先说那个表述是错的、容易误导人,然后专门写了一整节来讲清楚。这一节我来回读了三遍,读明白之后我觉得这几天就值了,光这一节就值。
它先实测了两个上限。这块芯片一秒能做 2.48 万亿次浮点运算,一秒能从内存里读进 13.5 GB 数据。两个数一除得到 184,意思是要让计算单元不闲着,每读进 1 个字节就得配上 184 次运算。
再看模型生成文字的时候在干什么。每吐一个字,要把全部权重从内存过一遍。一个 fp16 权重占 2 字节,参与一次乘加,也就是 2 次运算。折下来每读 1 字节只做 1 次运算。
1 和 184。差了 184 倍!
所以哪怕代码写得再完美,计算单元最多只能忙 0.54%,实测 0.34%。而内存带宽那头用掉了 73%。核心不是在偷懒,它是发出取数指令之后卡在那儿干等。至于工具为什么显示 5% 而不是 0.5%,因为那个数统计的是核心处不处在占用状态,一个卡着等内存的核心也算忙。
它还给了个反证,这个反证特别有说服力。
| 同一块板子,同一份代码 | 每字节配多少次运算 | 核心占用率 |
|---|---|---|
| 生成文字(单人) | 1 | 5% |
| 生成文字(8 人合批) | 3.07 | 36% |
| 编码图片 | 454 | 86% 到 87% |
编码图片这部分远超过 184,属于被算力卡住,所以能跑满。如果是代码把硬件浪费了,它不可能跑满。
结论它拆成两句。对"一个人自己聊天"这种用法,已经贴着这块板子的内存速度上限,继续抠算子只剩一点余量。对"整块板子",算力几乎全空着。

图 6:图中“1”与“184”表示每读取1字节权重所配的运算次数;实际负载低于机器平衡点,因此低核心占用率不代表内存带宽没有成为瓶颈。
空着的算力可以拿去换别的。八个人同时问问题,权重照样只搬一遍,总吞吐能到 21 字每秒。
漂亮,非常清晰!很直观的展示出了带宽与算力之间的关系。
第一次跑 53 分钟,第二次 49 秒
这块板子上跑推理时,有个我挺困扰的现象:每次改算子之后,第一次跑模型极慢,之后就正常。最夸张的一次,一个新算子跑一次请求花了 53 分钟。
| 同一件事 | 第一次 | 之后 |
|---|---|---|
| 编码一张图 | 13.3 到 14.5 秒 | 1.34 秒 |
| 首字延迟 | 138.8 秒 | 1.12 秒 |
| 生成 100 个字 | 1419 秒 | 20.2 秒 |
| 长 prompt 那次完整请求 | 53 分钟 | 49 秒 |
它给的解释是,这块芯片上的算子遇到没见过的输入形状,第一次执行要现场编译一个专用的小程序出来,一次十几秒起,编完存硬盘,下次直接用。
我听到存硬盘就顺口问了一句,那预热的结果是一个持久化文件吗,是 .om 文件吗。
.om 这个词我是从昇腾文档里瞄到的,听起来类似 .guff 之类的预编译二进制格式。
它说不是。.om 是把整个模型当成一整张图离线编译出来的东西,这个项目没走那条路,因为模型里有递归状态和动态形状,转不过去。落在硬盘上的是一个一个算子各自的二进制,当时一共 3804 个,34MB。文件名是串哈希,由算子类型、输入形状、数据类型和一堆属性算出来。
这句话读懂以后,前面所有奇怪的现象一下全串上了。
形状变了哈希就变,哈希变了就是另一个文件,另一个文件就要重新编译。而用户随便换个问题,输入长度就变了。所以我每问一个新长度的问题,它都要在那儿重编一整套,实测一次 609 秒。
explore 现在也察觉到了这个策略的不合理,于是做了一长串改造,把输入长度补齐到固定档位,把缓存换成预先分配好的固定大小,还有好几处取数据的写法也全改了,因为原来那些写法切出来的长度会跟着问题变。
| 改动 | 换个长度提问的等待时间 |
|---|---|
| 最初 | 609 秒 |
| 长度补齐到固定档位 | 78 秒 |
| 改掉取末位结果的写法 | 6.5 秒 |
| 缓存尾部不再裁剪 | 2.3 秒 |

图 7:右侧的2.3秒表示长度与缓存相关写法全部固定后,换一个输入长度提问时的等待时间。
每一步都拿 CPU 那把尺子重新对过,生成的每个字都跟标准答案一致。
还有个坑更阴。这块板子上内存不够的时候进程是被悄悄杀掉的,没有报错没有堆栈,日志里只剩几行不相干的东西。查到最后发现编译器默认要开 8 个进程并行编译,每个都是当前这个 2GB 进程的完整副本,7.4GB 的板子必死。改成 1 个,再把预编译放进一个不加载模型的干净子进程里一个一个做,子进程被杀也不影响主进程。113 个组合全部编译成功。
不开并发的话还是五点几吗
批处理做完那天,它报了一串数字,同时处理 8 个请求,总吞吐 21 字每秒。
我想起来两年前测试ollama和vllm并发时候的经历,如果开很多并发推理请求,总速度会变多,但每个请求速度会变慢,而且影响首字延迟。(btw,那会ollama的并发做的非常差,20并发以上能和vllm效率差出一个量级)
于是为了确认这点,我问:这个批量推理只对多人同时用有效吗,如果就我一个人用,还是那个五点几?
答案是,对,还是五点几。
explore 还解释了批处理为什么几乎免费。生成一个字的绝大部分时间花在把权重从内存搬进计算单元,搬进来之后真正的计算量小得可怜。八个人的活凑一起算,权重只搬一遍,多出来的那点计算不要钱。
| 同时处理几个请求 | 总吞吐 | 每个人感觉到的速度 | 首字延迟 |
|---|---|---|---|
| 1 | 5.66 | 5.66 | 1.19 秒 |
| 2 | 9.97 | 4.99 | 2.21 秒 |
| 4 | 15.25 | 3.81 | 4.36 秒 |
| 8 | 21.05 | 2.63 | 8.68 秒 |

图 8:图中每一行代表相同硬件和代码下的一种批处理规模,权重只搬运一次,随后被该批次的多个请求共同复用。
这也是为什么线上服务的吞吐量看着吓人,但并不是说多个用户就要多加一份卡。
我拍的一次板,和它推翻自己
有一条我一直觉得别扭。服务端每来一张新图片,都要把 1.1GB 的视觉部分搬进 NPU,编码完再释放。我在网页上换一张图,要等半分钟起步。
早先的结论是这两部分不能同时待在板子上,因为实测峰值内存会顶到 6.9GB,而总共 7.4GB。这个结论写在报告里,有数据支撑。
7 月 31 日下午我跟它说,第一点,视觉这部分肯定要常驻。
它没拿旧结论顶回来,重新测了一遍,然后发现当初那个 6.9GB 的峰值来自算子现场编译时抢的内存。而这些算子现在已经全部预热完了,那个峰值本身不存在了。也就是说,那个结论依赖的前提早就没了,只是没人回头看一眼。两部分可以同时待着,稳态占 4041MB,还剩 609MB。
改完之后,换一张新图的加载从 28 到 120 秒变成 0,编码本身只要 1.17 秒。也就是说我之前每次换图等的那半分钟,九成以上是在搬数据,不是在算。

图 9:左侧表示改造前每次换图都要重新加载视觉部分;右侧表示算子预热完成后的稳态。
它在笔记里写了一句话我很喜欢,大意是当造成某个结论的前提发生变化之后,要回头重新检查那些"我们已经否掉了"的结论。代价它也如实写了,批处理吞吐掉 8%,峰值内存多占 656MB,余量只剩 624MB,并发再往上加会顶到板子上限,所以它在启动时加了条告警。
这几天我到底干了什么
我把三个会话的记录导出来数了一下,数完有点想笑。
| 我 | 它 | |
|---|---|---|
| 消息条数 | 67 | 2130 |
| 其中"请继续" | 38(因为测试版模型不太稳,加上我没有压缩上下文,断了很多次) | 0 |
| 消息长度中位数 | 3 个字 | 巨多 |
| 工具调用 | 0 | 1190 次 |
| 在板子上跑命令 | 0 | 797 次 |
| 改文件 | 0 | 260 次 |
除了"请继续",我说过的有用的话大概就这些。让它跟 ollama 比一比速度。问这块板子的上限到底是多少。要求跑分的时候把 CPU 和 NPU 占用一起记下来。质疑那个 5%。拍板视觉部分常驻。它想去做量化的时候我拦了一下,说量化先不做,先把流式输出做了,我想先看见字一个一个往外蹦。
也有我判断错的时候。我报过一个 bug,说模型好像每轮对话都会忘记前面说的话。它跑了四组实验,日志显示缓存一直在正常复用,没复现。但它顺手加了几样东西,对话被清掉或者服务重启之后,页面上会跳一条橙色提醒,告诉你这一轮是从零开始的。因为这种两边都不报错的失忆确实是真实存在的一类问题,只是那次不是。
它也漏过我的话。我说"第一点,视觉塔肯定要常驻",后面还跟了一句让它顺便查 bug,它只做了常驻那一半。过了半天我问,现在前端还是必须传图片,你改了吗。它回,没有,我没改,这是我的疏漏,现在就改。
但是整体来看,explore 全程的注意力与理解力始终在线,长期注意力超强,从来没有发现任务遗失在中间,思路也一直很顺。就是有时候偶尔会走一些弯路,忘记自己之前写的代码是什么逻辑(当前这里可能也是因为cc的上下文压缩压的不是时候),不过最终都能自己把自己纠正。
当然,因为不知道阶跃的训练数据里面有没有很多写算子的数据,如果没有很多的话,那explore的表现就真的十分亮眼了,体感上我甚至愿意用它做Claude的平替。
和 ollama 的对比
跑完之后拿 ollama 的 4-bit 量化版做了个横向,同一张图,同一个问题,同样一百个字。
| ollama 4-bit,纯 CPU | 本项目 NPU fp16 | |
|---|---|---|
| 模型体积 | 1.6GB | 2.6GB |
| 预填充 | 4.67 秒 | 1.12 秒 |
| 解码 | 0.15 字每秒 | 4.94 |
| 生成 100 个字 | 653 秒 | 20.2 秒 |
| CPU 占用均值 | 73.9% | 32.1% |
权重体积是对方 1.6 倍、完全没量化的前提下,解码快 33 倍,同时把 CPU 让了出来。这台板子还得留着 CPU 去读传感器。
“写算子”"推理优化"到底是在干什么
这是我这三天最想弄明白的一件事,现在能答上来了。
在这之前,我其实无法想象"推理优化"到底是在干什么。但现在我通过学习explore的行为思路,已经有一个大概的认知了。我把它拆成了五件事:
第一件,把这个模型的数学在目标硬件上重新实现一遍,并且证明它跟原版逐位对得上。
这是最容易被忽略、工作量却最大的一块。框架里那份实现是给 CUDA 写的,换到昇腾上有些算子没有、有些算子行为不一样、有些库根本没编进去。所以它照着权重 key 把二十七层视觉和二十四层语言重写了一遍,然后拿 CPU 参考环境产出的金标张量一层一层对。视觉 0.999925,prefill 0.999855,生成的一百个字逐字一致。
这一步没有任何"性能"可言,但它是后面一切的地基。没有这把尺子,你连"我到底改坏了没有"都不知道。这个项目里最凶的那个 bug,一个补零函数返回垃圾数据,就是靠这把尺子一层层夹出来的。
第二件,找到真正的瓶颈,而不是你以为的那个。
这个项目里发生了三次,每次都是同一个剧本。凭直觉猜一个瓶颈,改完拿到零收益或者 1.2 倍;掐表量一遍,找到真的那个,拿到 20 倍、4.2 倍、5.8 倍。
| 猜的 | 收益 | 量出来的真凶 | 收益 |
|---|---|---|---|
| 算子调用次数太多 | 1.00× | 一个 6144 分组的卷积,单次 37 毫秒 | 20× |
| 块宽设得太大 | 1.2× | 每层一次设备等主机的同步 | 5.8× |
| 递归状态的临时张量太大 | 0.93×,更慢了 | 包装层里三十来个零碎算子 | 提了 3% |
所谓 profile,就是拿 torch.npu.synchronize() 把每个算子、每个层单独夹住计时。听起来朴素得不行,但它是唯一靠谱的办法。这块板子上直觉全是错的,因为从 GPU 那边带过来的经验在这儿不成立。
第三件,针对这块硬件的脾气,把热点重写掉。
这才是狭义上的"写算子"。做法是先看清这一步数学上在算什么,再找一条这块芯片走得顺的路,把同样的数算出来。跟把代码写精巧没什么关系。
那个 37 毫秒的卷积,数学上就是 4 个数的加权和,换成一次乘法一次求和,20 倍。这个项目里还没走到最深那一层,真正的重写是用 AscendC 手写核函数,把好几步融进一个 kernel。想做量化就必须走到那一步,因为板子上这套 torch_npu 里没有融合反量化的矩阵乘,先反量化再相乘反而慢 3.7 倍。
第四件,把形状钉死,让编译出来的东西能复用。
这一条是我之前完全没预料到的,我之前只知道用transformer直推模型会比用推理框架慢很多,因为推理框架推的权重格式是编译过的,有优化。但这块板子上还不太一样:
这块芯片上,每一个算子碰到一个没见过的输入形状,第一次执行都要现场编译一个专用的小程序,一次十几秒起。所以你的代码里只要有一处形状会随着用户的问题长度变化,用户每换一个长度就要重编一整套。实测最惨一次,一个没预热过的组合,一次请求 53 分钟。
优化的内容因此变成了一件很反直觉的事,把所有跟真实长度有关的形状全部消灭掉。长度补齐到固定档位,缓存改成预分配的固定大小,取末位结果不能用切片要用 index_select,构造 mask 不能用变长切片要用比较加 where。全改完,换个长度提问从 609 秒降到 2.3 秒。
第五件,把闲着的资源换成你想要的东西。
前面第四节讲的那个 184 就是干这个用的。先量出这块板子的算力上限和带宽上限,算出机器平衡点,再算出你这个负载的算术强度,两边一比就知道你卡在哪、还剩多少余量、以及该往哪个方向使劲。
单人聊天卡在带宽上,算力空着 99.66%。那就拿空着的算力换吞吐,八个人合批,权重只搬一遍,总吞吐从 5.66 变成 21.05。想换单人的延迟,那是投机解码要干的事,这次没做。想减少搬运的字节数,那是量化,被软件栈挡住了。
这五件事里没有一件是玄学,每一件背后都有一个数字盯着。而且这五件事之间是有顺序的,第一件不做完,后面全是在给一个错误答案加速。

图 10:五个步骤是有先后依赖的:先证明结果正确,再谈性能;后一步建立在前一步完成的基础上。
至于 explore,说实话,我一开始真的没有寄予任何希望,因为上一代 step-3.7-flash 的代码能力并不出色,能力主要集中在Agent工具调用上了,社区里的伙伴们都说,只要不写代码,step就很好用,比如各种AI自动化,小龙虾,工作流等等场景。
但是没有想到的是,这个新的 step-explore 模型效果好的不像是 step3.x 系列!三天下来,技术路线是它自己选的,那把用来判对错的尺子也是它自己想到要先搭的,那个连报错都没有的底层 bug 是它一层一层比对量出来的,它还三次推翻过自己先前的判断。这些环节我一次都没能给出方案,我只是在旁边问为什么。期间几乎全是代码工作,大量的推倒重来,各种方案对比测试,各种实验验证。我的感受是阶跃模型终于在 Coding 场景站起来了。
尾声
先说学习这件事,这才是我这几天真正被震到的地方。
三天前我连"为什么解码会卡在内存带宽上"都讲不清楚。现在我能跟人讲清楚机器平衡点是怎么算的,为什么低占用率不代表硬件被浪费,为什么这块芯片上换个提问长度就要重新编译一整套东西。而这三天里我一行推理代码都没写。
以前挡着我的是这么一件事。资料其实一直都在,昇腾的文档、roofline 的论文、别人的优化博客,全都搜得到,可那些资料后面全是名词,而我身边没有一个能随时打断、随时追问的人。看两页,问题攒了七八个,没处问,就放下了。这种事我干过很多次。
现在这道障碍基本没了。除掉那些真正人迹罕至的领域,前沿到全世界只有几十个人在做、连模型都没见过多少语料的那种,剩下的绝大部分领域,学不学得会已经只取决于你愿不愿意动手。你可以随时打断它问一句这是什么意思,可以让它把同一件事换三种说法讲到你听懂,还可以让它当场跑个实验证明给你看。我这三天问出去的那些问题,搁在以前,是要占用一个真专家很长时间的。
更要紧的是,它不是给你讲一遍就完了,它是拉着你把这件事真做出来。我学到的每一个概念背后都挂着一个具体的数字和一次具体的失败,这种记忆和看完一篇博客完全是两码事。
再说 explore 这个模型本身。在我感受下来,explore 的手感已经相当逼近 Claude 了,保守估计能暴打sonnet4.6,与opus4.8扳手腕。我原本以为这类活只有 A\ 能干,现在看起来也并非如此。推翻A\暴政,世界属于开源!!!(当然,这个手感只是纯主观感受,且只在当前这个算子优化任务上感受得到,其他任务暂时没有大量体验,可能会与其他人的感受有些出入)
根据手感经验猜一下,从模型的专注力和生成速度上来看,我感觉 step-explore 的总参数量应该在 1.5T 以上,激活参数 40B 左右。很期待发布,不知道估的准不准。
希望本文可以帮到大家,我去研究怎么接外设了~
更多推荐


所有评论(0)