搞深度学习的人大都体会过什么叫“算力焦虑”。模型从几十层卷积一路涨到几百B参数,通用芯片跑起来又热又慢,成本还压不住。于是NPU和TPU这类硬件加速单元成了绕不开的话题。我第一次接触NPU,是接手一个要把图像分类模型塞进嵌入式板卡的活儿;当时以为所谓硬件加速就是“换个更好的显卡”,真正动手才发现,NPU/TPU硬件加速的核心不是跑得更快,而是把“按指令取数、算完存回”的老规矩彻底改成了“数据流到哪,算到哪”。这篇文章以我实际迁移和调优一台带NPU的设备和一套云端加速集群的经验为底,把架构差异、底层机制、部署流程和性能调优心得完整盘一遍,适合准备做模型部署、搞边缘计算、或者想弄清楚加速器之间区别的同学参考。读完之后,你至少能判断自己的模型适不适合迁移,以及当性能不达标时该从哪里入手。
1. 为什么深度学习需要单独造一块专用芯片
1.1 CPU 的“通用”在深度学习面前是浪费还是保护
CPU是一台非常聪明的通用处理器。它要兼顾操作系统、业务逻辑、分支判断、数据库查询等五花八门的工作,所以芯片面积里很大一块给了分支预测器、乱序执行窗口、寄存器重命名、多级缓存。这些电路在跑一个普通的Web服务时非常有用,但在跑深度学习模型时基本帮不上忙——卷积和矩阵乘是高度规律、没有分支的重复计算,控制逻辑再强也无事可做。更麻烦的是,CPU的访存模式是“取指令、解码、执行、写回”,每一步都要访问片外DRAM,而内存带宽长期落后于CPU主频增长,大量时间都耗在等待数据搬移上。我在某台机器上做过一次对比实验:一个几十层的卷积模型在CPU上推理时,CPU的平均利用率只有不到20%,而内存控制器几乎满载。这说明算力根本没有成为瓶颈,数据通路才是。
从能效角度看,通用CPU的浪费更明显。一个复杂任务可能需要几百条不同指令,每条指令都要经过取指、译码、发射、执行、写回,每个环节都要消耗动态功耗。深度学习推理则是一遍又一遍执行同一批乘加,指令重复率极高。与其让一个复杂的处理器去反复做同样的动作,不如做一个专门电路来处理乘加。这也是NPU和TPU这类专用芯片存在的最底层理由:当工作量变得极其规律、极其单一的时候,通用性就成了负担,专用性反而能带来数量级的能效提升。
1.2 GPU为什么也不是终点
GPU通过成百上千个核心做并行线程,确实把矩阵运算拉快了很多。但它依然是一种“通用并行处理器”,还要兼容图形渲染、通用计算、各种线程调度。为了运行任意并行程序,它保留了复杂的线程块调度、全局内存模型和缓存一致性,这些机制在深度学习场景里同样不是全部必要。你用GPU训练和推理当然没毛病,但当功耗上限卡死、设备要长时间在线跑推理时,GPU的高功耗和高访问延迟就让人肉疼。我遇到过一台需要7×24小时做视频分析的服务,显卡功耗几乎占掉整机功率的一半,散热一跟不上就降频,最后不得不考虑专用加速方案。这说明通用并行和专用计算之间,存在一个“做得多但费得多”的灰色地带。
GPU的问题还体现在内存带宽利用方式上。它通过大量线程隐藏访存延迟,每个线程独立取数,虽然总带宽很高,但缓存行的利用率并不总是最佳。深度学习里的权重和激活值是可以被大量复用的,GPU也能利用局部性,但设计时它要考虑太多其他工作负载,没法像专用芯片那样把数据复用做到极致。因此我们看到,在同等功耗下,专门为矩阵运算设计的NPU/TPU通常能跑出比GPU更优的每瓦性能,尤其是在推理任务上。
1.3 专用芯片把矩阵计算变成硬件本身
于是就有了NPU和TPU。它们的共同点是:把深度学习里最核心的乘加运算,直接固化成硬件阵列,而不是用通用单元去模拟。芯片不再需要复杂的指令流水,只需要极简单的控制逻辑,数据从存储器读取之后,进入一排排计算单元,乘加结果在单元间直接传递,省掉大量临时寄存和内存读写。用一个生活类比:CPU像一位什么都会做的全科医生,能处理内科、外伤、慢病;GPU像一个训练有素的多科室急诊团队,应对各种并行任务;而NPU/TPU就像一条专门做CT检查的流水线,每个环节只干一件事,吞吐高、功耗低,但你不能让CT机去包伤口。这种取舍听起来很顺理成章,但真正设计时会遇到很多麻烦,比如怎么保证精度、怎么灵活响应不同形状的网络,这些后面详细说。
2. NPU和TPU的架构差异、精度策略与场景选择
2.1 两种加速器的技术路线差异
NPU,全称是神经网络处理单元,大多被集成在SoC里,和CPU、GPU、ISP共享一片存储系统,典型工作负载是手机拍照场景、端侧语音助手、摄像头里的目标识别。它讲究的是低功耗、低延迟、可嵌入,而且为了兼容五花八门的网络结构,NPU的计算阵列通常做成可配置的矩阵单元,配合稀疏权重处理、动态形状处理等功能。TPU则不一样,虽然网上已经有很多人把它泛化成“张量处理单元”,本质上是面向数据中心训练和推理的专用张量处理器。它更强调大规模矩阵乘法的吞吐、集群间的互联带宽、以及长时间稳定运行。我个人的理解:NPU是嵌入式世界里百搭的加速器,TPU是数据中心的超大规模矩阵引擎,两者设计目标不一样,导致它们在指令集、缓存设计、精度支持上都有明显差异。
这里的“指令集”差异很值得玩味。传统CPU指令集面向“数据怎么算”,每条指令做一次操作;NPU/TPU的指令更像“数据怎么流”,比如在哪个地址读入一块矩阵、送入哪个计算阵列、结果写到哪个缓冲区。一条指令往往对应数千甚至数百万次乘加。这种粗粒度指令大幅减少了取指和译码开销,但也意味着软件必须把整个网络拆成很规整的大块操作。如果你模型里有太多不规则分支,硬件这些“大炮”就很难瞄准目标,反而打得歪。
2.2 精度、量化与存储墙:为什么低精度如此重要
先说存储墙。芯片的计算能力增长一直比内存带宽快,于是很多加速器芯片看起来算力惊人,实际一跑却跑不满,原因就是权重和激活值要从DDR或高带宽内存搬进片上缓存,带宽不够,计算单元只能空转。为了解决这个问题,NPU/TPU都在片上堆了大量SRAM,设计者还使出浑身解数做数据复用——同一个权重能被相邻计算单元反复使用,就能少搬几次。更高明的解法是降低精度:同样的存储和带宽,存FP16比FP32少一半占用,换成INT8只要FP32的四分之一,单位时间内能搬运的数据条数几乎翻倍。有些云端加速器早期就把矩阵计算支持到低精度整数运算,重点提升推理性能;后来的训练版本则重新引入BF16/FP16,因为训练需要更大动态范围,不能简单砍成INT8。
量化不是一句“把小数变整数”就完事。实际部署时,你要为模型准备一个校准数据集,统计每一层激活值和权重的数值范围,再决定INT8的映射范围。动手量化的同学十有八九会踩坑:校准数据太少,范围估计不准,第一层输出就偏;校准数据分布和线上数据不一致,精度看着正常上线就崩。后面我会讲到具体的做法,这里先记住一个原则:量化本质上是损失信息,但几乎所有的深度学习推理场景都值得做,因为它换来的带宽收益实在太大了。
2.3 选NPU还是选TPU:别只看算力参数
现在我们常看到各种指标,标称算力从几个TOPS到几百TOPS,新手很容易被数字吸引。实际上,一个加速器在真实模型上的表现,由算子支持程度、内存带宽、缓存容量、编译器优化共同决定,算力只是上限。如果模型全是普通卷积和全连接,NPU通常够用;如果有大量动态shape、稀疏注意力、自定义算子,那就要确认加速器是否原生支持,否则经常被切到CPU执行,性能掉得很难看。TPU类的云端加速器更强调大规模并行和确定性性能,适合固定shape、固定batch、长时间重复推理的服务。我的习惯是先做算子映射表检查,再跑最小基准测试,用真实延迟和吞吐来决定硬件,这个阶段不需要买硬件,用模拟器或者平台上按量计费的环境就能跑通。千万不要只看宣传页上的“算力提升N倍”。
3. 核心机制拆解:矩阵阵列、数据复用与算子映射
3.1 矩阵乘法如何变成一块电路
几乎所有深度学习算法到最后都是矩阵乘法和卷积的组合,而卷积又可以被展开成矩阵乘。所以加速器的核心就是做矩阵乘。一个简单的例子:计算C = A × B,假设A和B都是4×4矩阵,最朴素的做法是一共做64次乘法和48次加法。如果在芯片里摆一堆乘加单元,每个单元负责一组乘加,数据从一个单元传到下一个单元,那么一次就能计算出一整行乘一整列的部分结果,这就是脉动阵列的基本思路。每个周期,数据沿着相邻单元流动,计算单元只需反复做本地操作,不用频繁访问全局寄存器。这种设计让功耗和面积都大幅下降。相比之下,CPU是一条指令处理一对数,GPU是一条指令处理一组线程,TPU/NPU则是一条指令把整个矩阵“推”进阵列。理解了这一点,就能明白为什么矩阵乘做得好的芯片,跑它的模型特别有优势。
做矩阵阵列时,设计者还要考虑数据摆放。矩阵A和B是有方向的,通常一个方向流入权重,另一个方向流入激活,部分和在阵列内部累加。如果让数据在阵列里走太远,连线延迟和功耗都会上升。所以很多芯片会按二维阵列划分,每个周期只和上下左右邻居通信。这也就是为什么编译器需要把大矩阵分块成合适尺寸,而不是直接把原始大矩阵塞进阵列。
3.2 分块与数据复用:大模型怎么塞进小缓存
深度模型动辄几GB权重,而片上SRAM通常只有几MB到几十MB,不可能一次把所有权重放上去,所以必须分块。工程上叫tiling,思路很像把一个大文件分片传输:把矩阵乘的三重循环按块划分,每次只加载一个块到片上SRAM,计算完后把结果写回主存。分块大小有讲究:块太大,SRAM放不下,编译器会频繁排挤数据;块太小,数据复用过少,每次加载只能对应一点计算,带宽白白浪费。实际调试时我常用一个简单的估算:假设片上SRAM有2MB,权重块和激活块各占一半,那么每次加载大约1MB权重加1MB激活,能支撑的计算量是块内矩阵乘的MAC数。如果这个MAC数除以加载字节数远小于芯片的算力带宽比,说明数据搬移成了瓶颈,要加大分块尺寸或做更激进的数据复用。这些听着数学味很重,但落到编译器优化上,就是帮你自动选择合适tile。
分块策略还直接影响推理延迟。我在一个模型迁移项目中观察到,同样的权重矩阵,编译器选择了不同的tile尺寸后,推理时间能差出30%以上。原因很简单:数据复用不够,带宽被白白消耗。这里也可以理解为什么很多硬件厂商强烈建议把输入分辨率固定下来,形状一旦固定,编译器就能提前做好分块规划,运行时的开销少很多。
3.3 算子融合与内存规划:编译器才是真正的大管家
有了硬件阵列,还得把神经网络的运算图翻译成加速器指令。编译器承担的活儿比想象中多:识别哪些算子可以融合、决定每个张量放到片上还是主存、按什么顺序执行指令才能避免等待。最常见的融合是卷积、批归一化、ReLU三合一。单独执行时,每次都要把中间结果写回内存再读出来,融合之后数据全部留在片上,带宽立刻省下一大截。我调试过的一个分类模型,编译器报告算子支持情况时,明确显示卷积、批归一化和激活已经融合成一个融合算子,网络里剩下的中间张量数量降到原来的三分之一,推理时间直接少了近30%。这里要提醒一句:不是所有融合都安全,涉及动态shape、尺寸变化时,编译器可能保守地把算子切回CPU,所以建模时尽量保持算子简单、形状静态,对编译器最友好。
内存规划同样重要。加速器执行运算时,输入和输出都要放在特定的缓冲区里,编译器要为每个张量分配地址空间,避免相互覆盖,同时还要考虑生命周期。如果编译器实现得不好,会出现大量无意义的搬运。我遇到过一种情况:模型里有几个并行分支,编译器没有把它们安排到不同的内存区域,导致一个分支的中间结果被另一个分支覆盖,结果只能用同步保护来解决,白白多等一个round trip。这种细节在文档里不会写,只能靠性能profile去发现。
3.4 从计算图到指令:一个推理请求的旅程
一个推理请求到达加速器,大致走这么几步:编译器和运行时先把你训练好的模型转成加速器能读的指令序列,包括每个算子的计算类型、数据地址、tile参数;运行时再把输入数据从主存拷到片上缓冲区,启动指令,等待计算完成,把结果拷回来。这个过程看起来很简单,但其中大量的数据拷贝和同步等待会吃掉性能。我第一次部署时,忽略了运行时的预热和异步拷贝,结果延迟里有一半是主机和设备之间搬数据。后来改用双缓冲——计算当前块的时候预取下一块,总算把空闲时间填满。这类问题在官方文档里往往一句话带过,实际踩坑时花了好几天。
如果计算图中存在硬件不支持的算子,运行时通常会把它切到CPU上执行,也就是 fallback 路径。此时主机和设备之间要进行上下文切换和数据回传,代价比想象中大得多。我见过一个检测模型,只是有一个自定义的RoI Align实现没被支持,导致整个模型的20多个算子全部被推回CPU,GPU和加速器几乎都在空转。所以算子支持清单的审查一定要做在前面。
4. 落地部署实操:把模型跑在NPU/TPU上的完整流程
4.1 动手前先做硬件支持审查
拿到一个新加速器,第一步并不是写代码,而是做算子支持审查。你需要导出一份目标模型的算子清单,一般用结构可视化工具,比如把常见深度学习框架训练好的模型转成中间表示后,逐个算子对照加速器的支持表。重点看三类算子:推理时可能出现的自定义op、动态shape相关的op、内部实现不透明的高层op。如果支持表里写了“降级到CPU”,不代表一定不能跑,但你要有心理准备,性能会差很多。我会先写一个脚本,把模型里每个算子在硬件模拟器上逐个跑一遍,用一个简单的特征输入,确认输出形状和数值范围正常,再进入完整转换。
这一步还要检查模型的输入和输出。有些模型在训练时喜欢用动态尺寸,部署到加速器上就会非常难受。我会提前把输入形状固定成最可能的几个档位,比如640×640、1280×720,转换成几个静态shape的变体,后续编译和内存规划都会轻松很多。
4.2 模型转换与量化校准:精度变化的神坑
模型转换尽量选中间格式,先把训练框架的权重导出为中间表示文件,再在加速器提供的转换工具里选择目标平台。转换完成后,建议在芯片上跑一下数值对比:逐层比较浮点模型和加速器输出的差异,误差通常应在可控范围内。训练好的模型直接移植,多数是FP32,推理加速器往往默认转成FP16或INT8。如果你选择INT8,就一定得做校准。校准是一个很实际的操作:收集几百到几千张和线上数据分布一致的样本,前向推理一遍,统计激活值的min/max或百分位,然后选择合适的量化参数。我在一个检测项目里用过512张样本做校准,离线精度只掉了0.3%,但上线第一天就因为光照抖动精度掉到不能接受。后来把校准样本扩到几千张、覆盖不同光照,并改用量化感知训练重新微调,才稳住。经验就一句话:宁可校准样本多一点,也不要拿一小把随机图片糊弄。
量化感知训练听起来复杂,操作起来其实也不简单。它会在训练过程中模拟量化的舍入误差,让模型权重逐步适应低精度表达。如果你只想快速部署,可以先试PTQ,也就是训练后量化;如果精度不达标,再回到训练管线做QAT。这两种方式的成本差一个数量级,所以判断标准应该是“PTQ能不能满足业务要求”,而不是“哪种方式看起来更先进”。
4.3 跑通最小推理程序:从加载到出结果
下面是规范化后的最小示例,我用的是通用加速器接口,具体平台换成你的SDK即可:
import accel_runtime as rt # 加载编译好的模型描述文件 graph = rt.load_graph("model_accel.bin") # 创建设备上下文 device = rt.Device(0) # 申请输入输出缓冲区 in_buf = device.alloc((1, 3, 224, 224), dtype="float16") out_buf = device.alloc((1, 10), dtype="float16") # 填充输入,这里省略数据预处理 in_buf.copy_from_host(input_tensor) # 执行推理 device.execute(graph, inputs=[in_buf], outputs=[out_buf]) device.sync() # 取回结果 result = out_buf.copy_to_host() print(result.shape)这里有几个容易忽略的点:alloc的缓冲区最好一次性创建,避免每次调用都分配;sync之后才能取数据,但异步模式可以配合双buffer使用;load_graph成功后不能频繁重新加载,模型加载本身很慢,我见过有同学在请求循环里每次load一次图,时延直接从3ms飙到300ms。跑通之后,先测一次warmup,再测量100次推理的平均时延,这才是真实性能。
4.4 性能指标到底看哪几个
跑通之后,就该用数据说话。我的习惯是固定记录五个指标:单次端到端延迟(从输入到输出)、吞吐(每秒处理请求数)、帧率(视觉任务常用)、算力利用率(加速器忙碌时间占比)、内存带宽利用率。这些数字合起来才能定位瓶颈。比如某一次推理延迟高,但算力利用率只有50%而且带宽利用率接近90%,说明问题不是计算不够,而是数据搬运不够快;这时候换更强的算力等于浪费钱,应该先做量化、优化数据流水线。
另一个重要指标是功耗。设备端加速器往往有功耗墙,持续高负载会触发降频,所以只看单次峰值没用。我会用压力测试把设备跑满十分钟,观察温度、频率和功耗的变化曲线。如果温度持续上升,性能就开始打折扣,这时你就需要考虑散热设计或调整批处理大小。
4.5 常见问题排查速查表
| 现象 | 可能原因 | 排查思路 |
|---|---|---|
| 模型转换失败 | 有算子不在支持清单内 | 导出中间表示后逐个算子比对支持表,替换成等价子图 |
| 转换成功但推理结果全错 | 输入数据范围与训练不一致,或量化校准没做好 | 逐层对比浮点和加速器输出,找到第一个异常层 |
| 延迟高但算力占用率低 | 主存和设备显存之间拷贝太多 | 用profiler看memory copy时间,使用双缓冲/异步拷贝 |
| 多路切片输入时吞吐上不去 | 预处理和后处理阻塞在CPU节点 | 预处理挪到硬件融合节点,后处理用独立线程 |
| 跑一段时间后开始降频 | 散热不足或功耗墙 | 查看设备温度,调整批处理大小降低瞬时功耗 |
| 更换shape后性能暴跌 | 编译器为旧shape生成了最优代码 | 尽量固定shape,或者在编译期预留常见shape池 |
这张表是我实际排障时最常用的框架,遇到问题先对号入座,能省不少时间。
5. 训练侧和推理侧加速的本质差别
5.1 训练为什么需要更高的精度和动态范围
前面说过推理可以降到INT8,训练却不行。原因是梯度更新对数值误差非常敏感,一个微小的舍入误差可能在几百步迭代里累积成模型发散。训练时常用FP16和BF16混合精度,FP16的动态范围窄,但好在有FP32主权重做累加;BF16动态范围和FP32接近,精度略低但更稳。专用推理芯片的设计目标是用最低精度跑出可接受的精度,而训练芯片需要考虑反向传播、梯度累加、优化器状态,这些都要占据额外的存储和计算。所以你不能把推理专用芯片拿去做训练,至少从精度和指令集上会碰壁。
5.2 反向传播让硬件复杂度上了一个台阶
训练不仅要做前向,还要做反向传播。反向传播需要保存每一层的中间激活值,反向计算梯度时又要重新读取这些值,所以显存/缓存占用比纯推理高好几倍。同时,反向传播的矩阵乘法和前向不是完全对称的,它需要把权重转置后参与计算,这在硬件上涉及更复杂的数据重排。如果你在训练芯片上做算子映射,会发现同一层网络在反向阶段会有好几个额外算子,比如梯度计算、权重梯度归约、偏置归约。这些都会占用计算阵列和带宽。TPU这类训练加速器在设计时要为这些额外模式留足余量,所以芯片面积更大,功耗也更高。
5.3 分布式并行:别忽略通信开销
训练一个大模型通常需要多卡并行,数据并行让每个设备算同一个子图的梯度,然后做AllReduce归约;模型并行则是把不同层切到不同设备上,层间通信更密集。在这个过程里,瓶颈经常不是单芯片算力,而是芯片之间的互联带宽和同步等待。很多加速器会提供高速互联总线,并配合集合通信库,把梯度归约直接卸载到硬件上,尽量减少CPU参与。如果你把训练任务硬搬到纯推理加速器上,就会发现既没有高速互联,也没有对应的分布式原语,整个训练根本跑不起来。从这个角度看,“训练硬件”和“推理硬件”面向的是完全不同的系统设计问题。
5.4 从训练硬件迁移到推理硬件的关键一步
实践中你通常是在训练集群上训练模型,再压缩、量化、转换后部署到推理加速器上。这一步的关键不是“复制文件”,而是重新审视模型里的每一层算子。训练阶段可能用到的Dropout、BatchNorm、动态TensorShape,在推理时都需要折叠或消除。BatchNorm在训练时是随着输入统计不断变化的,推理时要折叠进卷积权重里,否则加速器可能无法高效处理。动态shape也会阻碍编译优化,所以部署前要做“静态化”处理。我的流程一般是:先固定输入尺寸,再训练或微调一个推理友好的模型,最后才转换到NPU/TPU工具链。这样可以减少部署阶段的反复试错。
6. 性能调优心得:从数字到体感
6.1 算力不是唯一,带宽和调度才是命门
很多人在选型或者调优时,第一反应是看TOPS,似乎TOPS越高就越强。但真实系统里,加速器经常跑不满峰值算力。我见过一个极端例子,某个模型在标称算力高得吓人的芯片上跑,实际帧率只有一半,查下来问题出在权重排布不规则,编译器只能加载一小块数据算一小块,复用效率极低。反过来,一个参数更低的芯片,因为编译器做了很漂亮的分块和双缓冲,实际吞吐反而高一大截。所以调优要时刻问自己:现在到底是计算瓶颈、带宽瓶颈,还是调度等待瓶颈?用profiler看一眼,通常都能分清。
调度等待是最容易被忽视的一类。很多运行时接口是同步的,模型推理时CPU线程会阻塞等待设备返回,期间什么也干不了。如果你同时在处理多个视频流,这个等待会被放大。解决办法是异步执行:提交一批任务到设备,然后在等待的同时做下一个任务的数据预处理。这听起来是软件优化,实际上对加速器性能的影响极其显著,因为它把设备的空闲时间填满了。
6.2 一次典型的调优过程复盘
我想用一个模拟项目X来说明完整过程。项目是一个目标检测模型,原先是跑在通用并行卡上,后来为了低功耗换到带NPU的板卡。初始端到端延迟是32ms,要求是20ms以内。我先跑profiler,发现三个热点:输入图像预处理占8ms,用的是CPU上的缩放和归一化操作;模型本身在NPU上推理占16ms;后处理NMS占7ms。看起来CPU部分比NPU还重。
优化分三步:第一步,把缩放和归一化用加速器自带的图像预处理算子实现,并改成半精度输入,CPU负载降了下来,预处理从8ms降到2ms。第二步,给NPU推理加双缓冲,解码和推理并行,计算等待时间被填平,推理部分实际时间降到13ms。第三步,NMS用CPU多线程并行,同时把输出的候选框数量限制到更合理范围,后处理降到3ms。最终端到端延迟变成18ms,达到预期。这个过程的收益顺序很重要:先去掉明显浪费的主机侧等待,再优化设备侧调度,最后才考虑算法层面的剪枝和蒸馏。如果一开始就急着把模型剪小,可能把很多问题掩盖了。
6.3 我踩过的几个坑,希望你绕开
第一,不要盲目迷信“毫秒级下降”。加速器驱动和SDK经常有不同版本,相同代码换个版本性能可能差一倍。我建议每次部署都记录SDK版本、编译参数、flag开关,否则回头排查时想破头也还原不了最优配置。第二,动态shape是性能杀手。你为了省事,把输入分辨率写成了可变的,结果编译器为每种形状单独生成代码,还会在运行时反复做内存重规划,性能自然上不去。固定住shape,尽量走批量,让编译器能选到最优tile。第三,多模型并发时,显式设置设备上下文和流。我遇到过两次不同模型推理互相干扰,原因是默认上下文串在一起,后来为每个模型建独立执行流才解决。第四,别忘了warmup。很多加速器第一次调用要初始化上下文和编译缓存,没warmup直接测出来的延迟不能当依据。第五,端侧设备功耗墙和散热比云端更敏感,做长时间压测时,温度一升高频率会自动掉下来,那些“峰值功耗”指标只在刹那间有意义。
6.4 一些关于“测准数据”的体会
测性能不能只取一次时间。我通常的做法是:固定输入、固定batch、固定设备上下文,先跑几十次做warmup,然后记录100次推理的P50、P95和P99延迟。只报平均数是会骗人的,很多服务的SLA其实要求P95稳定,而P95往往比平均值高得多。另一个容易被忽视的是输入数据本身:如果输入图片全是空图或纯色图,某些模型的计算量可能波动很小,但真实场景里不同输入的计算量差异很大,所以最好用线上数据回放来做压测。单纯用随机数当输入,性能参考价值有限。我之前就吃过亏,用随机张量测出来延迟漂亮,上线后被真实视频流里的分辨率波动打得措手不及。
说回我自己,从最初以为硬件加速就是“换一张更快的卡”,到后来理解矩阵阵列、数据复用、编译器调度这些概念,中间踩过的坑远比文章里写的多。NPU/TPU硬件加速本质上是一场“资源换效率”的游戏:你用灵活性换吞吐,用精度换带宽,用编译期优化换运行期确定性。如果你此刻正准备部署,我的建议很朴素:先用手头模型跑一次最小基准,再决定卡脖子的瓶颈到底在计算、带宽还是软件生态;如果生态里算子支持很全、工具链顺手,哪怕标称算力低一点,最终体验也可能更好。反过来,一个算力很高但工具链难用的芯片,会让你在模型转换和问题排查上付出大量隐形成本。这些经验不一定适用所有项目,但大多是从真实部署里淌出来的,希望对你有用。