Step Edge 端侧推理发热掉速 / 先分清算力瓶颈还是散热降频

文章导读
端侧跑起来前几分钟速度正常、之后明显变慢,最省事的排查不是换模型,而是把「生成速度」和「设备频率、温度」放在同一条时间轴上。速度曲线和频率下降、温度上升同步,多半是散热降频;频率没掉、CPU 也没打满而单 token 耗时逐步升高,才更像算力或内存带宽瓶颈。判断顺序建议固定为:先确认掉速可复现,再对齐频率温度,再排除线程争抢,最后用长短输入区分持续负载与长上下文压力。
📋 目录
  1. 壹 连续跑固定输入,记录生成速度随时间的变化
  2. 贰 同步读取设备频率与温度
  3. 叁 调整推理线程数与调度优先级后再测一次
  4. 肆 对比短输入与长输入下的掉速差异
  5. 伍 按结论选择减档、限线程或分片加载
A A

端侧跑起来前几分钟速度正常、之后明显变慢,最省事的排查不是换模型,而是把「生成速度」和「设备频率、温度」放在同一条时间轴上。速度曲线和频率下降、温度上升同步,多半是散热降频;频率没掉、CPU 也没打满而单 token 耗时逐步升高,才更像算力或内存带宽瓶颈。判断顺序建议固定为:先确认掉速可复现,再对齐频率温度,再排除线程争抢,最后用长短输入区分持续负载与长上下文压力。

先连续跑同一组固定输入,按秒记录每秒生成 token 数,确认掉速能复现;采样窗口内同步读取 CPU 当前频率和热区温度,看掉速起始点是否与降频、升温重合。重合时优先按散热/调度处理,不重合时下调线程数与批大小再测一次,排除线程争抢造成的假降频。边界:不同机型的温控阈值和调频策略差别较大,任何结论都应以本机重复采样为准,单次一路曲线不足以下判断。

连续跑固定输入,记录生成速度随时间的变化

这一步只解决一个问题:掉速是真的、且可复现。把提示词固定成一段文字,输出长度固定(例如固定生成 128 个 token),中间不要改输入、不要切前后台、不要插其他任务,连续跑 3 到 5 分钟。采样的关键不是记录总耗时,而是记录每个时间窗口内新增了多少 token。

下面是一个通用采样骨架,其中 generate_stream 请替换为 Step Edge 自己的流式回调或逐 token 回调,其余计数逻辑可直接复用。窗口用真实耗时计算,不要写死 1.0 秒。

import time

prompt = '固定的测试提示词,前后两次保持一致'
max_new_tokens = 128

start = time.time()
last_t, last_n, acc = start, 0, 0

for tok in generate_stream(prompt, max_new_tokens):  # 换成实际流式接口
    acc += 1
    now = time.time()
    if now - last_t >= 1.0:
        n = acc - last_n
        print('t=%.2f tokens=%d tok_s=%.2f' % (last_t - start, n, n / (now - last_t)))
        last_t, last_n = now, acc
print('total=%.2f' % (time.time() - start))

记录表按下面的字段填写,一行一个采样窗口,后续和第 2、3 节的读数直接拼在一起:

字段含义填写方式
ts_s相对开始的秒数0、1、2…用真实时间差
tokens本窗口新增 token 数累计计数差值
window_s本窗口实际时长实测,别写死
tok_per_s生成速度tokens / window_s
cpu_pct进程 CPU 占用top、任务管理器或系统监视器
freq_mhz当期频率见第 2 节
temp_c当期温度见第 2 节
note备注首次预热、切换输入、开始掉速

判断标准很朴素:如果前两三个窗口明显快、后面稳定在一个更低的水平并且不再回升,说明掉速存在;如果整条曲线本来就是平的,那读者遇到的多半是「一开始就觉得慢」,属于另一类问题。

同步读取设备频率与温度

只在应用层看速度看不出原因,必须把频率和温度读到同一个采样窗口里。Android 上常见节点如下,读不到说明权限或机型限制,换下一行那种遍历方式再试。注意温度节点单位常为毫摄氏度,读到数值明显偏大时先按 1000 换算核对,不要直接当成摄氏度下结论。

# Android:各核当前频率(kHz)
adb shell 'for c in /sys/devices/system/cpu/cpu[0-9]*/cpufreq/scaling_cur_freq; do echo -n "$c "; cat $c; done'

# Android:热区类型与当前温度
adb shell 'for z in /sys/class/thermal/thermal_zone*; do echo -n "$z "; cat $z/type; cat $z/temp; done'

# 通用 Linux:频率与温度
cat /proc/cpuinfo | grep -i 'cpu MHz'
for z in /sys/class/thermal/thermal_zone*; do echo -n "$z "; cat $z/type; cat $z/temp; done

桌面上 powermetrics、sensors 之类工具需要额外权限,桌面端更省事的做法是同时开系统监视器看频率/温度曲线,并把第 1 节的 tok_per_s 输出打印在同一个终端里,靠时间戳对齐。iOS 侧通常不能直接读这些节点,本机不可读时的替代方式是:用框架自带的 metrics 或耗时日志,加上系统的热状态回调、进程 CPU 占用,作为间接指标参与对齐。

对齐方法:在采样循环里顺手把频率和温度读出来,同一个 print 里输出四列,即时间、tok_per_s、freq、temp。曲线画出来后只看一个点——速度开始下降的那个窗口,频率是否已经在下跌、温度是否已经在高位。两者同时出现,基本可以按散热降频处理;频率在掉速期间基本持平甚至更高,就要往负载、内存、线程方向查。

调整推理线程数与调度优先级后再测一次

多线程推理在端侧很容易被前台 UI 线程、系统后台任务抢 CPU,表现出来和降频很像:速度掉,但频率没掉。所以调频结论出来前,先做一次线程数与调度优先级的对照实验。

Step Edge 端侧推理发热掉速 / 先分清算力瓶颈还是散热降频

两个可调项的位置:线程数通常落在引擎初始化或 session 参数里,批大小通常落在推理请求参数或引擎配置里,字段名按 Step Edge 实际配置替换,下面是通用骨架。

# 通用接入骨架,字段名以实际配置为准
runtime:
  threads: 4        # 先按大核数量试,再上下各试一档
  batch_size: 1     # 单次送入的序列数
  max_batch_tokens: 512
scheduler:
  priority: normal  # 观察是否与前台线程争抢

Linux 侧还可以用 taskset 绑核、nice 调优先级,Android 侧用 top -H 看单线程占用、用 renice 做对照。改动后不要只看平均速度,要把新的 tok_per_s 曲线和旧曲线并排比对,重点看三个值:起始速度、开始掉速的时间点、掉速后的稳定平台。整体变快或掉速起点明显推后,说明之前存在线程争抢;起始速度下降但曲线更平,说明是负载与降频叠加,需要回到第 2 节的频率结论继续判断。

对比短输入与长输入下的掉速差异

这一步区分「持续算力负载」和「单次长上下文压力」。准备两组输入,输出长度保持一致:A 组短提示词(几十个 token),B 组长提示词(例如几 k token 的同一段文本),其余参数完全相同,各自单独跑一轮并各自采样。

每组都要记录:首 token 延迟、每秒生成 token 数随时间的变化、进程内存占用、掉速起始时间。判定方向比较明确:短输入也随时间掉速,指向持续算力负载或散热降频,回第 2 节看频率;短输入平稳、只有长输入一直慢但曲线平坦,指向长上下文本身的算力与内存带宽成本,不是降频;长输入前快后慢且内存持续上涨,指向缓存增长、内存压力或换页,这在端侧比降频更常见。

按结论选择减档、限线程或分片加载

测量结果要能落到动作上,三种动作对应三种不同结论,不要混用。

减档

适用条件:频率与温度同步下降,属于典型散热或功耗约束。动作是降低模型档位或降低量化精度,缩短单次输出长度,减少并行请求。验证方式是重跑第 1 节完全相同的输入,对比速度曲线和输出完成度,确认减档后掉速起点推后、输出内容仍然完整可读。风险边界是精度下降带来的质量变化,需要用同一批输入人工核对。

限线程

适用条件:频率没掉,但单线程占用高、速度随线程数波动,属于线程争抢或调度问题。动作是把线程数降到接近物理大核数量,固定绑核,降低后台任务干扰。验证方式是同一输入跑前后对比,确认平均速度和曲线抖动幅度都收敛。风险边界是线程过少会让单次响应变慢,需要按实际等待时间取舍。

分片加载

适用条件:短输入稳定、长输入才掉速,或内存占用随上下文持续上涨。动作是把长文本切成前后有重叠的分段依次推理,限制单次上下文上限,把缓存控制在可预期范围内。验证方式是同一份长文本按分片与不分片各跑一次,对比每秒 token 数曲线是否变平、首尾内容是否衔接。风险边界是分片会带来跨段语义丢失,需要在业务侧确认可接受。