做端侧推理的人恐怕都逃不过同一个场景模型在开发板上能跑算子也对得上可一上整机功耗曲线就像过山车摸板子都能感觉到热量往上涌。我在不少RISC-V平台项目里见过团队在这个问题上反复折腾早期思路基本都是“压频率、缩分辨率、加散热片”能省一点是一点属于典型被动节流。这条路走到尽头往往是性能和体验一起妥协散热成本还上去了。后来我逐渐把注意力从“省着用”转成“算着用”也就是把调度、量化、空闲态优化这三件过去分开做、甚至只在出事时才做的事情整合成了一套主动能效治理流程。这篇文章就是这套思路的完整记录写给正在RISC-V端侧做推理部署的工程师不管你是刚拿到一块开发板还是已经跑到调优阶段应该都能从中找到能直接落地的做法。1. 被动节流的死胡同先认清功耗在端侧推理里怎么来的1.1 从压频率到加散热问题为什么越解决越难先说一个反直觉的结论盲目降频很多时候不但不省电反而会更费电。原因不复杂推理是一个有时间约束的任务你把频率从1.5GHz压到1.0GHz假设计算量不变耗时就会拉长约50%。动态功耗和频率基本成正比但应用程序在单位工作上的能量消耗等于功耗乘时间换算下来大概是 0.67 * 1.5 1.0也就是说这个算子的能耗几乎没有下降如果芯片在低频下电压不能同步降下来能耗反而会上升。早期我就是这么踩坑的。为了控制发热我们把RISC-V主核频率固定压低一档结果推理一帧的延迟从40毫秒涨到65毫秒板子温度是没继续飙但整个流水线的乘积——功耗乘时间——并没有明显改善反而因为延时变长芯片有更长的时间停留在不完全休眠的活跃状态外围器件也跟着多耗电。后来才意识到问题不在“要降多少频率”而是“什么时候该降、什么时候该升”。也就是需要有主动的决策机制而不是一套参数走天下。1.2 动态功耗、静态功耗与DDR带宽的真实关系做能效治理得先理解端侧推理中功耗的三个主要来源计算部分的动态功耗公式是P_dynamic C × V² × f这是最容易理解的能耗大头。注意电压是平方项频率降下来以后如果电压没有联动降收益会被吃掉很大一部分。静态漏电功耗工艺节点越小越明显即使核心完全空闲漏电也在消耗能量。这决定了“空闲时必须真正进入低功耗状态”这件事不可跳过。存储器与DDR带宽的功耗在端侧推理里权重搬运、中间激活值读写往往比计算本身更贵。一次DDR访问消耗的能量可能是ALU一次乘加运算的几倍到十几倍。这也是为什么量化能把能效拉上去——它不只是减少计算位宽更重要的是把内存访问量砍下来了。这三个来源对应三类手段调度解决的是“什么时候算、算多快”的决策问题量化解决的是“用多少位去算、搬多少数据”的资源问题空闲态优化解决的是“不算的时候能不能真停下来”的问题。三者分开看都只是单点优化合起来才叫能效治理。2. 调度层让推理任务先于功耗曲线做出反应2.1 端侧推理调度与通用CPU调度的本质差异通用操作系统的调度器按优先级和时间片工作任务之间的依赖关系是运行起来以后才知道的调度器只能被动响应。端侧推理不一样模型结构在加载的时候就固定了每个算子的计算量、访存量、依赖关系都是静态可分析的。这意味着我们完全可以把“边跑边看”变成“跑之前先规划好”。我常用的做法是在推理任务真正下发到RISC-V核心和NPU之前先对模型做一次静态分析得到一张算子级别的执行图。不需要很复杂对于每个算子记录五件事算子类型、输入输出张量形状、预估计算量MACs、预估访存量bytes、依赖哪些前置算子。有了这张表调度器就可以回答三个其他调度器回答不了的问题哪些算子可以并行哪些算子必须串行某个算子在当前硬件上放哪个执行单元最划算。这里有个端侧特定的约束RISC-V SoC通常不是单一计算资源而是“通用核 向量单元 NPU/DSP”的组合。调度决策的本质是在不同硬件单元之间分配算子同时避免总线冲突。比如一个3x3卷积数据量大丢给NPU紧跟着一个元素级的ReLU数据量小但必须等卷积算完如果也丢给NPU就白白浪费了NPU的排队时间。把这个ReLU留在主核上等NPU通过中断把结果写回内存时主核立即处理流水就好很多。2.2 算子特征识别与异构资源分配实际分配时我一般遵循几个经验原则写出来供参考计算密集、访存比高的大算子优先放NPU或者带向量扩展的RISC-V核因为它们能持续计算外部带宽压力小。访存密集、计算简单的小算子放通用核避免频繁跨总线搬运数据减少中断和数据拷贝开销。对于必须往返调度的算子组检查它们之间的中间张量是否可以被“融合”比如卷积后面紧跟激活函数可以在输出阶段直接完成激活省一次内存写和读。带宽冲突其实是调度里最容易忽略的坑。两个大算子如果同时启动都疯狂读DDR实际吞吐会互相拖累跑出来的时间比串行还差。调度器最好能插入一定的错峰让一个算子跑计算、另一个算子走DMA搬运交替使用带宽。另外一个容易被忽略的调度入口是链接脚本。RISC-V工程里通常有link.ld来定义内存布局我见过不少团队只拿它做启动地址管理忽略了它对调度的影响。比如把常驻的权重段放到TCM或者紧耦合SRAM区域和频繁访问的代码段分开可以减少cache miss和总线争用。把热路径算子库放到ICCM区域配合锁缓存效果接近为这些算子单独开了一条“快车道”调度器再配合预取能进一步压低峰值功耗。2.3 调度参数的落地与调优在没有现成调度框架的情况下我自己常用一个极简但又有效的实现在模型加载阶段生成调度计划运行时只执行这个计划。调度计划是一组“硬件单元 算子ID 启动条件”的条目启动条件就是依赖计数归零。主核上的调度循环负责扫描可执行算子下发给对应单元NPU完成一个算子后触发中断调度循环更新依赖计数把等待它的下游算子解锁。这个结构的好处是决策成本被提前支付了运行时只需要做依赖判定开销可以控制在微秒级以下不会成为推理延迟的瓶颈。这么做牺牲的是应对输入动态变化的灵活性比如输入分辨率变化、batch大小变化时算子时间会变调度计划的适应力会下降。解决办法是把调度计划按“配置档位”打散比如低分辨率一档、高分辨率一档运行时根据实际输入切换。调频率本身也要嵌入调度逻辑而不是单独在sysfs里手动改。我习惯的做法是在调度计划里允许给每个硬件单元打上一个“预期运行频率”当推理进入某个高计算阶段时把NPU频率拉高进入调度空隙时主动降低主核频率。这样做之后频率的变化不再是拍脑袋而是跟着工作负载的形状走。3. 量化层主动分配精度预算而不是一刀切int83.1 全局int8的局限与精度损失成因量化是端侧能效治理里收益最直接的一环因为它同时砍了计算位宽和内存搬运量。大部分团队上来就做全局int8量化工具也方便跑一遍校准集就完事。但如果你追求的是“能效和质量同时可接受”全局int8其实很不聪明的做法。原因在于不同层对量化误差的敏感度差异极大。举个例子模型输入层直接面对原始像素分布一旦量化范围没选准误差会直接传导到整个网络输出层贴近最终预测结果它对量化噪声的容忍度也很低normalization层、softmax之前的logits这些位置一旦被截断精度掉起来特别明显。反过来中间层的卷积、深度可分离卷积因为网络本身有冗余很多情况下int8甚至4bit影响都很小。我在一个内部视觉模型上做过对比全局int8量化后mAP掉了2.1个百分点排查下来80%的精度损失来自前两层和一个输出分支。把那两层保持fp16其他保持int8mAP回升到只掉0.3个百分点而推理耗时只增加了约4%。这就是主动分配精度预算的意思精度是一种资源你把它花在最需要的地方而不是平均分配。3.2 按层/按通道分配位宽的实操方法主动量化在实操中是个迭代过程我的做法是分四步走先用一个较小的校准集对每一层逐个做仿真量化单独记录该层量化后整个模型的精度变化。这一步是建立“敏感度清单”。根据敏感度清单把层分成几档低敏感层可以用int4中敏感层用int8高敏感层保持fp16或者用混合精度里较安全的组合。校准量化参数时优先处理激活值的动态范围。权重比较容易量化因为它是静态的你可以做逐通道量化激活值每次推理都不一样容易出现长尾分布需要配合EMA指数滑动平均统计运行中的min/max或者在工程上直接裁剪掉极端值。量化感知训练能做就做。哪怕只是在关键层插入伪量化节点跑几个epoch也能让模型权重主动适应量化噪声后面的精度损失会明显变小。这里面最花时间的是第一步但这一步的信息密度最高值得做。一次性全局int8省下来的时间后面都要用更多的调参时间还回去。3.3 量化与向量指令协同的落地细节RISC-V做量化推理有一个比ARM平台更值得关注的点V扩展向量指令的利用效率。int8量化后的kernel如果能用vle8.v加载、vwmacc这类指令做乘累加计算密度非常可观。但这里有个隐藏问题——如果你在runtime里做的是实时反量化或实时requant这些额外的scale、shift计算会成为新的热点反而拖慢整体。所以我在代码设计上有一个硬性要求能预计算的参数全部离线算好。权重在编译阶段完成量化并按向量寄存器的排列格式重新排布scale、zero point也直接编入权重参数运行时只做整数乘加和位移不出现浮点运算。RISC-V的向量长度是软件可查的代码里最好按vlenb动态分块避免为特定VLEN写死这样换芯片时不用重写内核。另外一个关于4bit量化的提醒int4在存储上很省但在计算上需要额外的解包和位操作。RISC-V向量指令集里没有原生4bit乘加指令你要么自己处理打包要么接受额外的unpack开销。我实测下来除非内存带宽是绝对瓶颈否则4bit带来的能效增益不一定有想象中大经常是存储省了计算单元闲不下来功耗没降多少。所以选int4之前先量一下模型的访存强度再做决定别只看存储占用数字好看。4. 空闲态推理流水里那些没人管的功耗黑洞4.1 空闲窗口的常见来源同步、DMA、并发等待调度做完了量化也做完了很多人会觉得能效优化到头了。其实还有一块常常被忽略的肥肉推理流水里的空闲窗口。从任务时间线上看一次推理并不是均匀占用硬件的。典型的场景是CPU把输入数据准备好交给DMA搬运到NPU内存CPU进入等待NPU计算完写回结果CPU拿到中断后开始处理下一个算子。这个过程中CPU在等DMANPU在等CPU两边都可能处于“运行但不干活”的低效状态。更麻烦的是板子上还有其他外设和中断随时把核心唤醒核心即使调用了低功耗指令也可能因为频繁中断而根本没有真正睡下去。这些空闲窗口单次只有几十到几百微秒看起来不起眼但累积起来非常可观。我在一个双核RISC-V平台上测过一个40ms的推理循环里CPU核有接近20%的时间处于既不执行有效计算、也没进入真正休眠的模糊状态。这块优化出来等于白捡五分之一的时间窗口。4.2 用指令和电源域主动进入低功耗状态主动治理的做法是把空闲窗口利用起来而不是听任它们存在。具体分两个方向一是“让窗口消失”二是“让窗口变便宜”。让窗口消失主要靠异步化和流水线重叠。核心思路是一个经典的双缓冲机制当前算子还在NPU上跑的时候CPU不要闲着等待而是去准备下一个算子的输入把数据搬到另一块缓冲区里。这样DMA、NPU、CPU整个链路上每个单元都有活干。这个改动我不止一次看到项目延迟直接缩短10%到15%而且能耗也随之下降因为每个单元干活的时间更集中空闲的时间变长且更连续。让窗口变便宜则是利用RISC-V和SoC本身的低功耗能力。RISC-V架构提供了WFI这类等待指令可以让核心停住等中断Linux内核会把它映射到cpuidle状态。但实际工程里要注意不是所有空闲等待都适合WFI如果等待时间只有几微秒执行WFI的进入/退出开销比忙等还高不如让核心忙等。判断标准很简单拿trace测量出等待周期的分布把超过50微秒的空闲块统一进入低功耗状态短块保持忙等或干脆调度其他轻量任务填充。电源域方面RISC-V SoC通常有多个电源域可以把推理用到的NPU、向量核和主核分开管理。NPU算完一批任务后如果没有下一批直接关闭NPU电源域而不是只停时钟。时钟门控省的是动态功耗电源门控省的是静态漏电两者不是一个量级。4.3 从数据上看空闲态优化的收益我在自己的项目里做过一次完整的空闲态改造数据上更容易说明问题。改造前NPU空闲等待CPU准备数据的时间占总时间的25%CPU空闲等待NPU写回的时间占15%两个核加起来有接近一半的时间处于“空闲但没睡”的状态。改造后先用双缓冲消除大部分同步等待再用WFI和电源域管理填充剩余窗口整机平均功耗下降了约18%峰值功耗也略有下降因为多个单元不再同时抢带宽和时钟。需要特别注意的是空闲态优化不能只盯着平均功耗。对很多产品来说决定散热方案规格的是峰值功耗不是平均功耗。峰值通常出现在多个硬件单元同时启动、同时抢DDR带宽的时刻。调度层的错峰策略和这里的电源域管理要配合着用错峰能压低峰值电源域管理能压低平均值两个合起来才能既好谈散热又能延长电池续航或降低整机发热。5. 把调度、量化、空闲态串成一条能效治理闭环5.1 一条完整的优化闭环怎么跑调度、量化、空闲态三者如果各自为政效果会大打折扣。比如你辛辛苦苦做了流水线重叠但因为量化后的算子变快太多各硬件单元之间原本设计好的节奏全乱了新的空闲窗口又出现了。所以必须把它们放进一个闭环里一起调。我一般这样组织和迭代第一步用profiling工具采集当前版本的执行时间线标出每个算子、每次等待、每段空闲的具体耗时同时记录整板功耗和DDR带宽利用率。第二步按三个维度做瓶颈分类空闲多不多、带宽有没有被压满、计算单元利用率高不高。哪个维度最失衡就优先动哪个。第三步实施对应的改动注意这里永远只改一个变量比如这次只调调度计划不动量化配置避免一次改多个因素导致定位不了问题。第四步重新做profiling对比能效指标如果收益为正保留并进入下一轮如果收益为负回滚并记录原因。这个循环看起来朴素胜在能给出一个可复现、可对照的记录。没有这个过程优化很容易变成碰运气。5.2 反馈机制与自适应闭环迭代解决了“开发阶段如何调优”的问题但产品实际运行时的负载是动态的。输入分辨率会变batch大小会变甚至同一个模型在不同输入上算子耗时也有波动。要真正实现标题里说的“主动能效治理”在运行时需要一点轻量级的反馈机制。我的做法是在调度循环里挂一个轻量监控点每隔一小段周期读取当前推理阶段的执行进度并估算当前算子的完成时间。用这个估算值和预设的目标帧间隔做比较动态调整两类参数频率电压、数据预取的提前量。如果当前阶段计算压力大、剩余预算紧张就把频率拉高一档同时加深预取深度如果明显超前于目标就把频率降下来让硬件单元快点进入空闲状态。这个反馈控制器不需要很复杂一个简单的比例控制甚至查表就够了。真正复杂的是选择合适的控制变量和采样周期。采样周期太短会被算子耗时的正常波动干扰太长又跟不上负载变化。我用的经验值是控制在单个算子执行时间的5到10倍量级这样既平滑了抖动又保持了响应能力。5.3 工程实践中需要避开的协调陷阱在实际把三者捏合成一个系统时有几个容易出问题的点提前说一下能少走弯路。反馈控制器和调度计划不能互相打架。调度计划是静态的反馈控制器是动态的如果控制器为了省电把频率降了但调度计划里某个硬件单元的启动条件却要求它立刻响应就会出现性能抖动。解决办法是让反馈控制器把决策结果写回到调度计划的频率配置里调度器执行时直接读取而不是另起一套逻辑。量化与调度顺序也会互相影响。量化后算子耗时变短原来为它规划的DMA预取时间可能就来不及了。这类问题只能通过完整回归测试发现所以每次改完量化配置必须重新采集时间线不能只看精度指标。另一个容易被忽视的是中断和操作系统带来的干扰。RISC-V平台在Linux环境下做推理如果不断开不必要的CPU affinity限制进程可能被调度器迁移到不同的核上之前规划的缓存亲和和电源域配置就失效了。建议把推理进程pin到固定的核心组并把这个作为能效治理流程里的一步常规操作能省掉很多莫名其妙的功耗波动。6. 落地实测测量工具、对比指标与踩坑记录6.1 能效测量与延时分析方法任何能效优化没有测量就等于没有做。我自己的最低配置是一套“软硬结合”的测量方案。硬件上在电源路径里串一块带I2C接口的功率监控芯片常见的像INA226这一类用一颗MCU或直接在RISC-V主核上以低频率采样记录整个推理过程中的电流电压软件上在推理框架的每个关键节点打上时间戳并把时间戳与功率采样点做对齐。对齐这一步最容易出问题。功率采样和推理时间戳如果不在同一个时间基准上你很难说清某个功耗尖峰到底是哪个算子引起的。我的做法是在推理启动时通过GPIO输出一个脉冲同时记录软件时间戳和功率监控的采样点这样两边就有了一个共同的基准点后面的对齐就简单多了。对于延时分析除了看端到端耗时我还会跟踪每个硬件单元的忙时间占总时间的比例以及DDR带宽的峰值利用率。这两个指标比单一功耗数值更能暴露问题。比如功耗高但利用率也高那是正常的计算能耗功耗高但利用率低说明有大块的能量被浪费在了空闲和等待上。6.2 我在实际项目中踩过的几个坑第一个坑只看功耗不看峰值。有一轮优化做完平均功耗很好看但整机的瞬时电流尖峰反而变大了原因是多个硬件单元被安排得过于紧凑同一时刻出现了访存爆发。后来把调度计划里两个大算子错开尖峰就压下去了。从那以后我每次改完调度都会同时看平均功耗和峰值功耗两个指标差了超过两倍就要警惕。第二个坑WFI没有真正生效。代码里调用了WFI以为核心进入了休眠实际用trace一看中断风暴让核心每秒醒来几千次每次醒来都只做一点零碎工作就继续睡。这种情况下WFI的进入退出开销比省下的能量还多。解决办法是排查外设中断源把不必要的中断全部mask掉再统计真正的空闲块长度确认超过进入低功耗状态的阈值后才允许WFI。第三个坑量化校准集的分布和真实场景不一致。校准集选的都是“好数据”模型量化后精度指标全绿一到真机实拍光线差一点的场景就开始乱输出。根本原因是校准时的激活值min/max统计太乐观真实输入的动态范围更大。现在我会特意在校准集里加入低光、过曝、模糊这类边缘样本宁可校准出来的量化范围宽一点也不能让真实数据落到范围外被截断。6.3 一些可复用的经验原则最后整理几条自己目前还在用的原则不一定对所有平台都成立但大概率能帮上忙优化前先量化量化后再优化。意思是所有改动都要对应到可测量的能耗数字上别凭感觉。每个优化动作做完跑同一组测试用例记录功耗和延迟哪怕收益只有1%也要记下来后面做决策时这些都是依据。能卸载就卸载能合并就合并。调度时优先考虑异构卸载量化时优先考虑算子融合这两条原则比单纯的频率调节带来的收益大得多。低功耗状态一定要量化评估收益。进入低功耗和退出低功耗都有能量开销短窗口进入休眠反而不划算。先用trace统计空闲分布再决定阈值。模型大了以后DDR带宽消耗往往比计算更值得优化。调度和量化都要优先考虑减少数据搬运的次数和总量。我在实际项目的体会是RISC-V端侧推理的能效优化最难的从来不是某一个单项技术而是调度、量化、空闲态这三件事怎么配合成一套系统。单看每一项调度不如降低分辨率和裁剪模型来得立竿见影量化不如直接换轻量模型省事空闲态优化更是常常排在项目最后一两周才能排上。但如果只看短期收益最后只会陷入“模型越来越小、效果越来越差、功耗却不达标”的循环。把这套闭环跑顺了以后你再面对“这个模型能不能上这颗芯片”的问题时就容易判断得多了先静态分析算子图再量化敏感度扫描再调度规划最后测量空闲窗口和功耗曲线一条链路走下来心里就有数了。