ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

RISC-V端侧推理能效优化:从调度、量化到空闲态

RISC-V端侧推理能效优化:从调度、量化到空闲态 前阵子我把一块RISC-V八核开发板拿来跑端侧推理最开始的心态其实有点“佛系”反正板子功耗低性能弱一点就弱一点降降频、关关核、多等一会儿总能把活干完。结果一测傻了眼——YOLOv5s跑起来不到3FPS整板平均功耗却飙到快7W待机功耗也有3W多。这哪里是“低功耗AI”分明是“低性能电暖器”。后来我花了两周时间从调度、量化、空闲态优化三个方向做了一轮系统性的“能效治理”实测能效比翻了好几倍。这篇就来复盘整个过程聊聊为什么端侧推理不能靠被动节流而要走向主动治理。如果你也在RISC-V或类似资源受限的平台上部署视觉模型、LLM这篇文章里的思路、命令和坑应该能帮你少走不少弯路。1. 先把瓶颈找出来端侧推理到底慢在哪1.1 算力不是唯一瓶颈数据搬运才是做端侧推理的人第一直觉都是算算力主频多少、TOPS多少、FPGA/NPU算力多大。但这些纸面数字在真实板上往往对不上帧率。原因很简单一个模型跑得慢卡的时间大头通常不在乘加运算而在访存。拿我手头这块板子来说八核RISC-V主频1.8GHz带RVV向量扩展理论算力不算难看。但它的L2总共只有2MB上下内存是单通道DDR4带宽大约12.8GB/s。跑YOLOv5s时按FP32口径模型计算量大约4.6 GFLOPs按算力推算每帧应该几十毫秒搞定可实际上单帧耗时400多毫秒。差距去哪儿了全耗在访存上。一个卷积层输入Activation、权重、偏置、中间结果都要从DDR搬进L1/L2算完再写回DDR。每个算子的访存量往往是计算量的好几倍。更别说RISC-V这类平台上的cache miss成本一次DDR访问的延迟够CPU执行上百条指令。所以我的第一个结论很明确在端侧推理里内存带宽和cache miss比TOPS数字更能决定实际吞吐。借用《计算机体系结构量化研究方法》里那句经典的话性能瓶颈一定在你实际测量的地方而不是你理论推导的地方。1.2 被动节流和主动能效治理差在哪想清楚瓶颈之后我意识到之前“降频等一等”的思路本身就是错的。降频省的是峰值功耗但它解决不了访存瓶颈还把性能一起砍了关核就更粗暴直接牺牲并行度。我把这种思路叫“被动节流”出了发热、功耗超限、性能不达标的问题才用一刀切的手段去压。真正应该做的是“主动能效治理”在任务还没开始跑之前就通过调度、量化、空闲态三件事把能量规划好。调度负责让对的核心在对的时间干对的活量化负责让模型更小、访存量更低算同样的活只花一小半能量空闲态负责让不干活的核心真正睡死而不是挂着高频空转。这两个思路的差别可以整理成一张表维度被动节流主动能效治理目标降温、保护硬件、临时降载提升单位能量的有效产出FPS/W、token/s/W手段降频、关核、强制休眠负载感知调度、模型量化、空闲态精细管理时机温度超限后 / 系统空闲后被动响应任务启动前预估、运行中动态调整副作用性能抖动明显、响应变慢延迟可预测、能效稳定提升后文所有优化动作本质上都是在回答三个问题让哪个核心干活怎么让活变得更轻怎么让没事的核心快点睡。2. 调度优化把每一核算力花在刀刃上2.1 异构核心怎么分活别让通用调度器替你拍板RISC-V端侧SoC的CPU拓扑往往是几簇核心每簇共用一个频率域和电压域。有的簇偏性能有的簇偏能效这跟手机上的大小核结构类似但Linux通用调度器默认只会根据负载均衡来做决策它不会主动识别“你这个推理线程放错簇了”。我实测下来一个INT8卷积推理线程放在性能簇和能效簇帧率能差20%到30%。原因不光是主频差异还有cache共享和内存延迟的不对称。通用调度器不知道这些它只知道“哪个CPU现在空闲”。所以第一步就是把关键推理线程绑到正确的核上。我用的方法很直接# 把推理进程绑定到CPU2和CPU3 taskset -c 2,3 ./infer_process # 如果有多个线程可以按线程粒度绑 taskset -p -c 2,3 $PID # 隔离出几个核给实时任务用 # 内核cmdline加: isolcpus4-7 nohz_full4-7 rcu_nocbs4-7如果你用cgroup管理也可以用cpusetmkdir -p /sys/fs/cgroup/cpuset/infer echo 2-3 /sys/fs/cgroup/cpuset/infer/cpuset.cpus echo 0 /sys/fs/cgroup/cpuset/infer/cpuset.mems echo $PID /sys/fs/cgroup/cpuset/infer/tasks这里有个前提你最好先摸清板子上的CPU拓扑和频率域划分再看lscpu -e的输出确认哪些核可以绑在一起。别一上来就绑核先实测再动手。绑完核之后记得用perf stat对比一下cache miss率你会看到立竿见影的变化。2.2 如何看时序调度和系统延时从trace到Momentic说完了核怎么分下一个问题是怎么验证调度质量。我见过不少朋友优化调度全靠“感觉”帧率上去了就认为没问题掉帧了就开始美团式重启。这样不行你得会看时序和延时。Linux下最常用的手段是trace-cmd抓内核调度事件。我一般这么抓trace-cmd record -e sched_switch -e sched_wakeup -e irq_handler_entry -e power:cpu_idle sleep 10 trace-cmd report trace.txt这个trace里你能看到每个线程什么时候被唤醒、什么时候真正切换上CPU、中间隔了多少微秒这个间隔就是调度延迟。端侧推理里调度延迟超过5ms就会明显影响帧率稳定性。如果你用的是QNX这类RTOS或者板子的SDK里自带像Momentic这样的时序分析工具它会直接给出调度反转、优先级抢占、中断延迟这些指标。一套好的时序分析工具能帮你快速定位“是调度器在等锁还是中断风暴在抢CPU”。我自己的习惯是先看调度延迟的P95再看中断频次最后才去看负载。2.3 cpuload数据分析先会读数据再谈调优很多人把cpuload和CPU占用率混为一谈。load average高不代表CPU跑满了它统计的是R状态和D状态的进程数一个进程在等DDR返回数据时也算load。所以只看load会误判。正确的做法是组合看几组数据vmstat 1看r运行队列、b阻塞、cs上下文切换三列上下文切换每秒超过几千次说明调度太碎。pidstat -t -p PID 1看进程内各线程的CPU占用分布找出谁在偷跑。perf stat -e task-clock,context-switches,cache-misses ./infer看推理进程的访存和切换指标。我遇到过最典型的场景推理进程CPU占用率50%但帧率只有预期的一半。看vmstat发现cs列狂跳再看pidstat发现进程里有个后台线程在周期性做日志刷新每秒把推理线程打断几十次。这就是典型的“cpuload看起来不高实际性能被切换吃掉了”。这类问题不靠数据分析是找不到的。2.4 关键任务的实时属性SCHED_FIFO与优先级控制绑核只能解决“在哪跑”决定不了“抢不抢得到CPU”。端侧推理如果跟其他业务线程混跑交互式线程、后台服务都可能抢占CPU。对推理任务我推荐把它的一部分关键线程设为实时调度。#include sched.h #include pthread.h struct sched_param param { .sched_priority 80 }; pthread_setschedparam(pthread_self(), SCHED_FIFO, param);注意SCHED_FIFO优先级不是越高越好。如果优先级设到90以上你的中断线程和内核关键线程可能被堵住反而带来更大的延时抖动。我自己一般控制在70到80之间。另外设置了实时优先级之后线程里的循环里如果自旋等锁很容易把整个核吃死所以代码里务必避免while(!flag);这种忙等写法。调度层优化做完之后我测了一次帧率从2.4FPS涨到3.4FPS功耗几乎没有变化能效比提升了差不多50%。但这还远远不够因为模型本身还是那个又大又慢的FP32模型真正的大头在下一层。3. 量化把模型体积和访存量一起砍掉3.1 别把模型量化跟量化交易搞混端侧量化的真实收益先说点题外话常有人一听“量化”就以为是金融那套K线、夏普比率。这里说的量化是把模型权重和激活从FP32变成INT8甚至更低比特的整数表示纯粹是端侧部署的事。别搞混。量化在端侧推理里的收益不只是模型体积缩小四倍这么简单。它最实在的好处是让同样的DDR带宽能搬运更多的有效数据。FP32的权重一次DDR读32位只能读一个参数INT8的权重同样32位能读四个参数。访存是端侧推理的命门量化直接打在命门上。当然量化是有代价的。不同数据类型的精度损失和硬件友好度差别很大对RISC-V这类对工具链要求高的平台尤其明显。我用过的方案整理成一张表数据类型位宽典型适用层精度损失端侧部署注意点FP3232各种基线模型大、访存开销最大FP16/BF1616各种很小硬件没有Zfh扩展时只能软件模拟反而更慢INT88Conv、FC、MatMul较小部署最成熟RVV向量化友好INT4/NF4等4LLM线性层中等需要反量化端侧收益主要在内存带宽FP88部分新模型中等工具链不成熟暂不建议现在连DeepSeek、GLM这些开源模型都有量化版本地部署的玩法端侧重构的基本路线是能INT8就不INT16能极低比特就不普通FP16前提是精度还能用。3.2 INT8量化方案怎么选从PTQ到QAT对大部分端侧视觉模型推荐从PTQ训练后量化入手。流程不复杂准备几百张有代表性的输入图片跑一遍推理统计每层激活的数值范围然后算出scale和zero_point。快、不需要重新训练是首选。但PTQ翻车也很常见尤其是小模型或者深层特征图分布不均匀的时候。如果你发现量化后精度掉得离谱先别急着换QAT把校准集换一下再试。校准集一定要覆盖真实场景光照变化、分辨率变化、遮挡都要有。只拿几张测试图做校准大概率过拟合到那几张图上。如果换了校准集还不行再考虑逐通道per-channel量化尤其对卷积层和FC层per-channel比per-tensor精度好很多代价是推理引擎的多一点计算量。最后没办法才上QAT量化感知训练。QAT要重新训练模型成本高但精度确实最稳。3.3 算子级的坑为什么量化后反而慢、为什么shape对不上量化不是导出一个INT8模型就能直接跑这里坑特别多。我踩得最深的几个第一个坑是算子fallback。ONNX里的某些算子比如动态Shape的Resize、Gather、NonMaxSuppression量化工具可能没有实现INT8版本推理引擎就只能把这一块放回FP32执行。如果FP32算子占的推理时间比例高量化后的加速效果会被严重稀释甚至出现“量化了但没完全量化”的尴尬情况。所以导出前一定要看算子支持表。第二个坑是Shape不匹配。社区里常有人问“为什么我量化版的某个模型导出来后在引擎里报错两个维度对不上”比如中间特征从5120变成4096。我遇到过类似的情况十有八九是导出脚本里某处Reshape硬编码了未量化时的维度或者模型源码和导出工具版本不一致。排查方法很简单用ONNX Runtime跑一遍原始FP32模型把每层的输入输出Shape打印出来再跟量化导出的模型逐层比对。import onnx model onnx.load(model.onnx) for node in model.graph.node: for idx, out in enumerate(node.output): print(node.op_type, out)第三个坑是量化后反而变慢。这通常是因为动态反量化被放在了热点路径上或者INT8算子内部还在做不必要的数据格式转换。RISC-V平台上尤其明显因为很多推理引擎的RVV向量化路径还没把INT8卷积写透反而不如FP32标量路径。解决办法是打开引擎的profiling看到底是哪个算子慢再针对性替换后端。3.4 RVV指令与推理引擎适配最后一步的加速RISC-V的RVV向量扩展对INT8推理很友好。数据是8位整型一条向量指令可以一次搬几十个字节。但编译器自动向量化的效果取决于循环结构规整不规整。很多量化后的卷积、Depthwise层循环边界参差不齐GCC/Clang经常生成很保守的代码。这时候需要手写RVV intrinsic或者利用推理引擎已经封装好的RVV后端。比如加载一个INT8向量#include riscv_vector.h size_t vl vsetvl_e8m1(n); vint8m1_t vec vle8_v_i8m1(ptr, vl);实际项目中我更推荐先用NCNN、TEngine这类原生支持RVV的推理框架而不是自己一行行写汇编。但框架选型能决定你能不能吃到RVV红利。我实测下来同一个INT8 YOLOv5s开启RVV后端的推理时间大概能再降30%以上。像在RK3568这类Arm平台上跑YOLOv5 INT8的朋友流程其实类似只是把NEON换成RVV思路互通。量化这层做完我的推理帧率从3.4FPS干到了7.1FPS模型体积从14MB缩到3.8MB平均功耗反而从6.5W降到5.2W。能效比已经翻倍还不止。但这还不够因为我发现板子待机功耗高得离谱问题出在“不干活的时候”。4. 空闲态优化别让CPU假装下班4.1 为什么推理服务空闲时待机功耗还那么高WFI与cpuidle跑推理的板子不可能每时每刻都在满负荷推理更多时候是“来一帧处理一帧不来就等着”。我最初测待机功耗发现一个诡异现象推理进程完全空闲板子功耗还有3.1W。这不正常低功耗板子待机应该不到1W才对。问题出在CPU没有真正进WFI。RISC-V CPU执行WFI指令后会进入低功耗等待状态中断来了才会醒来。Linux的cpuidle子系统会根据预测的idle时长选择合适的idle state。查看当前支持的状态cat /sys/devices/system/cpu/cpu0/cpuidle/state*/name如果系统频繁被定时器唤醒比如有线程每秒醒来几十次cpuidle governor会认为“CPU很快就会有事干”于是选择浅睡甚至不睡。最终结果就是CPU一直在高频空转只是没有执行用户代码功耗自然下不去。4.2 DVFS与调度联动别用一套governor走天下除了idle频率也是大头。Linux的cpufreq governor有很多从performance到powersave到schedutil。问题在于很多人一套governor用到底用performance推理是快了但空闲不降频用powersave空闲省电了推理又变慢。我现在的做法是动态切换推理任务开始前切成performance确保前几帧不会因为升频慢而掉帧推理任务结束后切成schedutil让调度器根据实时负载自动调频。schedutil的优势是它直接和调度器联动利用率高才升频利用率低立即降频比单纯的按时间轮询的conservative响应快得多。# 查看当前governor cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor # 推理开始前 echo performance /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor # 推理结束后 echo schedutil /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor注意不是所有RISC-V板子的cpufreq驱动都支持每个核独立调频很多SoC是几个核共享一个频率域切换governor的时候就是一整簇一起变。实测下来这种共享频率域的平台上schedutil比独立调频的平台抖动更明显更需要在应用层做任务合并。4.3 从被动到主动的配置实例一个最小推理服务的能效策略把上面这些组合起来我给推理服务写了一个简单的能效管理脚本。逻辑很朴素有活的时候用performance加绑核没活的时候用schedutil加WFI同时强制后台日志进程和大核隔离。#!/bin/sh # 推理主进程 INFER_PID$(pidof infer_process) # 有活切performance 绑核 echo performance /sys/devices/system/cpu/cpu2/cpufreq/scaling_governor taskset -p -c 2,3 $INFER_PID # 没活后台任务绑到小核主核切schedutil echo schedutil /sys/devices/system/cpu/cpu2/cpufreq/scaling_governor echo 1 /sys/devices/system/cpu/cpu6/cpufreq/scaling_governor这套策略的精髓不是“省电”而是“该快时快该睡时睡”。推理延迟要从几百毫秒级别降到几十毫秒你必须在任务到来前把频率拉起来而任务间隔长的时候又得让CPU尽快沉到深睡。这就是主动治理和被动节流最大的区别它知道你什么时候需要性能什么时候需要省电而不是一刀切。做完空闲态优化待机功耗从3.1W降到了0.8W推理过程中的平均功耗也降了一点因为帧间空闲的CPU真正睡过去了。5. 实测复盘三轮优化后的能效账单5.1 实验环境与基线数据简单交代一下环境方便你对比自己的板卡。硬件是某八核RISC-V开发板带RVV 1.0主频1.8GHz2GB内存Linux 6.x内核推理框架为NCNN自编译RVV后端。模型是YOLOv5s先跑FP32基线再对比INT8量化后的版本。基线数据什么优化都没做FP32模型默认调度器cpuidle全开但没做任何策略场景帧率(FPS)平均功耗(W)能效(FPS/W)连续推理2.46.80.35空闲待机—3.1—这里有个细节要解释一下能效是帧率除以平均功耗单位FPS/W意思是每瓦每秒能处理多少张图。这个指标比只说帧率或者只说功耗都更能反映“主动能效治理”的成效。5.2 三轮优化后的数据对比我按顺序做了三轮优化先调调度再上量化最后做空闲态。每轮都在前一轮基础上叠加最终数据如下优化阶段帧率(FPS)平均功耗(W)能效(FPS/W)待机功耗(W)基线2.46.80.353.1调度绑核实时优先级3.46.50.523.0加INT8量化RVV后端7.15.21.372.7加空闲态动态governor7.14.51.580.8从2.4FPS到7.1FPS帧率提升了大约两倍待机功耗从3.1W降到0.8W能效比从0.35FPS/W涨到1.58FPS/W提升接近4.5倍。这个结果比我预想的要好关键是每一层优化都在“做减法”调度层减少了无效切换量化层减少了访存量空闲态减少了空转能量。5.3 现场案例一个后台线程让功耗翻倍这轮优化过程中最典型的排查案例是一个后台线程。当时我发现待机功耗在优化后依然偶发跳高从0.8W跳到2.5W。一开始以为是中断问题但抓了几次cpuload数据和trace发现规律很清晰每200毫秒左右CPU会从idle状态被唤醒一次像是某个周期性任务在执行。后来用trace-cmd抓sched_wakeup事件追到唤醒源是一个日志组件的flush线程。它每200ms醒一次每次只跑不到1ms平时看着CPU占用率很低但它把CPU从深睡状态硬拉起来每次都付一次唤醒功耗和升频功耗累计下来待机功耗直接翻倍。解决方案很简单把flush周期从200ms放宽到2秒同时对日志线程做cpuset隔离让它只在小核上跑。就这么一处改动待机功耗又降了1.4W。这类问题不看trace是根本不可能定位到的只看top会以为CPU很闲实际能量全耗在“醒来—干活—再睡”的路上了。我个人在这轮优化中最深的体会是端侧推理的能效问题从来不是某一个环节的锅而是调度、模型、空闲态三个层面共同作用的结果。只调模型不调调度帧率上去了功耗下不来只调度不量化功耗降了性能还在原地只抠空闲态待机是省了但一来推理任务照样卡顿。只有把这三级都打通才能真正实现从被动节流到主动能效治理的转变。另外多说一句类似的方法不只能用在RISC-V上。只要你手里的平台是异构多核、资源受限、还要跑AI推理这套“先量化减负、再调度分活、最后管空闲”的思路都可以直接搬过去。区别只是指令集和工具链不同干活的路数是一样的。
返回列表