Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

移动端 LLM 推理的 DVFS 与能效边界

端侧 LLM 推理会同时占用 CPU、GPU 或 NPU 与内存带宽,还要消耗设备的散热余量。计算方式随阶段变化:prefill 一次处理输入上下文,decode 持续生成输出;模型加载、后端编译、采样(sampling)和 KV cache 管理又会在这两段计算之外引入额外开销。

只看平均 CPU 利用率或某一次 tokens/s,通常解释不了用户感受到的等待、速度波动和发热。本文围绕四个工程问题展开:

源码基线:平台 android-17.0.0_r1(Android 17 / API 37),内核 android17-6.18-2026-06_r6。设备可能使用其他内核分支。

  1. 首个 token 等待时间(Time to First Token,TTFT)、每个输出 token 的平均时间(Time per Output Token,TPOT)与单 token 能耗(energy per token)应该如何定义和测量;
  2. EAS、CPUFreq、GPU / 内存 DVFS、ADPF(Android Dynamic Performance Framework)和温控分别负责什么;
  3. 普通应用能调节哪些负载参数,哪些操作只属于系统、厂商或取得最高系统权限的 root 实验;
  4. 如何判断一次优化确实节能,还是只是把耗电和热限频推迟到下一轮请求。

这些机制的基础分属不同章节:EAS 见 5.1 节,CPUFreq 与温控见 5.2 节,ADPF 见 5.4 节,端侧 AI 后端见 5.5 节。本文把它们放到同一条 LLM 请求时间线上观察。

先把指标定义清楚

LLM 的性能指标只有在起止点一致时才有可比性。我们建议在应用侧为每个请求分配一个标识符(ID),并记录下列时间戳:

  • t_request:请求进入推理 API;
  • t_prefill_start、t_prefill_end:后端开始和结束处理输入;
  • t_first_token:第一个可交付 token(模型处理文本时使用的基本单位)到达应用;
  • t_i:第 i 个输出 token 到达应用;
  • t_done:生成结束、取消或报错。

本文采用的 TTFT、TPOT 和单请求每输出 token 能耗定义如下:

TTFT = t_first_token - t_request

TPOT = (t_last_token - t_first_token) / (N_output - 1)
       仅在 N_output > 1 时定义

request_energy_per_output_token
    = (E_done - E_request) / N_output
      仅在 N_output > 0 时定义;E 为同一口径的累计消耗能量

按这套定义,TTFT 覆盖请求进入 API 后到首个 token 交付前发生的工作,可能包括分词、排队和冷启动;若模型创建或分词在 t_request 之前完成,就不会计入该 TTFT。点击到首个可见结果还需另计调用前处理和 UI 呈现。如果还要分析执行后端,我们另外报告 t_prefill_end - t_prefill_start,并分别测量模型文件读取、权重映射、delegate 初始化、计算图编译和首轮 kernel 预热。预热之后量到的后端时间属于热身后的稳态数据,报告里要与冷启动 TTFT 分开列。

TPOT 是整段 decode 的平均值,容易掩盖个别 token 的停顿,工程报告还应给出相邻 token 延迟(inter-token latency)的第 50、95 百分位数(P50、P95)、最大值和随时间变化的曲线。在采用推测解码(speculative decoding)、并行解码(decode)或一次提交多个 token 的运行时里,输出回调与硬件 kernel 可能不再一一对应;这时 token 到达间隔仍能描述用户体验,要推断每个 token 在硬件上的执行周期,还需要另找依据。

TTFT 与 TPOT 的计时边界

图:TPOT 仅在输出 token 数大于 1 时定义,并配合 P95 相邻 token 延迟。API 仅交付文本分片时,应改报分片指标,不能还原逐 token 时刻。

能耗同样要写清分母:

  • prefill_energy / input_token 反映长输入 prompt 的处理成本;
  • decode_energy / output_token 适合比较连续生成;
  • request_energy / output_token 包含加载、prefill、decode 和应用开销;
  • joules / successful_request 以焦耳 / 成功请求为单位,还能反映取消、超时与失败造成的能量浪费。

这几组指标可以同时报告。横向比较之前,先确认两边用的是同一个分母,再谈“模型能效”的高下。如果 API 只交付文本分片而不暴露逐 token 时间,应报告首分片与分片间隔;按 tokenizer 统计分片里的 token 数,并不能还原各 token 的真实到达时刻。零输出的失败或取消请求保留在请求能量和成功率统计中。

一次请求经过哪些硬件

一次端侧推理请求会穿过多个执行层,每层都有一部分容易被漏看的成本;具体分工由模型格式、运行时和 delegate 决定。我们先用下表建立一个排查用的层次视图:

层级常见工作容易漏看的成本
应用与 tokenizer文本预处理、分词(tokenize)、流式回调、用户界面(UI)更新Java / Kotlin 对象分配、日志、频繁跨语言调用
CPU 运行时图调度、采样、算子补位、GPU/NPU 命令提交单线程串行段、线程迁移、锁与唤醒
GPU张量乘加、向量算子、部分注意力计算(attention)与后处理短 kernel 间隙、命令提交等待、频率从低档升到高档所需的时间
NPU支持范围内的编译图与加速算子图切分、回退、编译缓存、厂商驱动约束
动态随机存取内存(DRAM)/ 系统缓存读取权重和 KV cache,交换各后端数据带宽竞争、容量压力、数据布局转换
存储首次读取模型与编译产物尚未进入内存缓存的文件页(冷页)、解压、校验、存储空间不足
显示、网络及其他进程展示流式文本、下载上下文、系统并发任务全机功耗与模型功耗混在一起

LLM 请求的多部件成本

图:图为常见混合执行关系。模型加载还涉及存储,屏幕、网络与其他进程会进入整机能量;具体算子分工由模型、runtime 和 delegate 决定。

Prefill 与 Decode 的负载特征

prefill 和 decode 对硬件的压力形态差别很大,我们分开看这两段。

Prefill 的负载形态

Prefill 一次处理较长的输入序列,张量运算容易组织成批量工作,这是它与 decode 最直接的差别。随着 prompt 增长,计算量、权重读取、attention 中间数据和 KV cache 写入都可能增加。所以我们在 trace 里看到 GPU 或 NPU 利用率很高时,先别急着把瓶颈定在计算单元上:内存带宽、计算图分区、首次编译和 CPU 提交,都可能拖长 TTFT。

Decode 的负载形态

传统自回归 decode 每轮根据已有上下文生成后续 token,各轮之间存在数据依赖,可批量并行的工作通常少于 prefill。分工上,CPU 可能负责采样、运行时调度与命令提交,加速器则读取权重和不断增长的 KV cache。token 周期还会受采样策略、停止条件、上下文长度、计算图回退和系统抢占影响。

所以在 trace 上,decode 常呈现“CPU 短暂工作—提交—等待加速器—读取结果—采样”的交替节奏。某个硬件在一个采样窗口内利用率不高,它的运行频率仍可能影响端到端的 token 间隔。

调度与频率控制的分层

动态电压频率调节(Dynamic Voltage and Frequency Scaling,DVFS)按负载与约束调整硬件电压和频率。在这条链路上,EAS、CPUFreq governor、GPU / 内存策略和 Power HAL 各管一段,我们按层拆开看:EAS 决定任务放在哪个 CPU,schedutil 与 CPUFreq 决定 CPU 频率,GPU 与内存频率多由厂商实现,Power HAL、ADPF 与温控构成约束和提示层。

EAS 负责 CPU 放置

在 android17-6.18-2026-06_r6 中,kernel/sched/fair.c 的 find_energy_efficient_cpu() 会调用 compute_energy(),借助能耗模型(Energy Model)估算把刚唤醒的任务放到不同 CPU 之后的能量变化,回答的问题是“这个任务放在哪个可用 CPU 上更合适”。

注意 Energy Model 的用途:它服务于调度选择,量不了整机功耗。估算结果受任务利用率、CPU 容量、调度域、CPU 空闲状态、uclamp 和厂商扩展影响;它不知道一次 LLM 请求的 token deadline,也不直接控制 GPU 或内存频率。

schedutil 与 CPUFreq 负责 CPU 频率

同一内核版本里,kernel/sched/cpufreq_schedutil.c 的 schedutil governor 根据调度器利用率计算下一个目标频率:get_next_freq() 经过 map_util_freq() 等映射,并考虑策略容量、I/O wait boost、更新速率等条件;厂商可以通过 android_vh_map_util_freq 这个 vendor hook 调整映射。

最终频率还要叠加 CPUFreq policy 的最低 / 最高值、freq_qos 请求、热限制和硬件可用频点约束。

选核、选频与最终硬件频点的关系如下:

任务唤醒
  ├─ EAS / fair scheduler:选择可运行的 CPU
  └─ scheduler utilization → schedutil / vendor policy
                         → CPUFreq target
                         → policy、QoS、thermal clamp
                         → 硬件频点

这段流程里选核与选频会互相影响:任务落到哪个 CPU 核簇(cluster)会改变可用容量和频点,频率又会影响执行时间。所以两条路径要分开观察:任务迁核后变快,未必是 governor 升频的功劳;看到 cpu_frequency 变化,也未必说明 EAS 改变了放置决策。

选核、选频与最终约束

图:EAS 的能耗模型用于比较放置方案,不是整机功耗计。CPU 目标频率还受 QoS、温控与硬件限制;GPU/内存另有设备策略。

GPU 与内存 DVFS 多由厂商实现

Linux 提供了 devfreq 框架和通用 governor,但 Android 设备的 GPU、片上互连和内存控制器多由 SoC 厂商的驱动与策略管理,带宽请求和相关遥测数据(telemetry)也是。

AOSP 没有给普通应用提供指定 GPU 或 DRAM 频点的统一 API。某台设备上存在的 sysfs(内核向用户空间暴露状态和控制项的虚拟文件系统) 节点、tracepoint 或频率表,换一台设备就未必还在。

GPU delegate、NPU delegate 或厂商运行时可以通过驱动提交工作,间接触发性能状态变化;是否升频、升到哪里、维持多久,取决于厂商实现、系统功耗模式和热预算。

Power HAL、ADPF 与温控约束

AIDL 接口 IPower 支持创建性能提示会话(hint session)。Framework 把应用线程组、目标工作时长和实际工作时长交给 Power HAL,系统和厂商可以据此调整调度、频率或其他策略,也可以在不支持相应能力时忽略提示。对应用来说,性能提示是一个影响系统决策的输入,换不来保证的 CPU / GPU 频点。

温度达到限制后,thermal cooling device、CPUFreq / Devfreq 限制、Power HAL 和厂商策略都可能压低性能上限。这时再增加负载、增加线程数或反复发送性能提示,可能只是增加排队与功耗,突破不了热限制。

decode 阶段的利用率盲区

利用率驱动的策略本身没有问题,它看到的只是局部信号;LLM decode 又恰好容易让局部信号与端到端目标脱节。我们把容易出错的情形列出来:

  1. 串行依赖:CPU 必须等待加速器结果后才能采样并发起下一轮,任一侧空闲都可能表示正在等待前一阶段;
  2. 短时突发(burst):短 kernel 的利用率会在采样窗口内被平均;频率尚未稳定升高,工作可能已经结束;
  3. 跨设备交接:CPU 提交慢会让 GPU 暂时没有工作,GPU 执行慢又会让 CPU 休眠,两边的平均利用率都可能不高;
  4. 状态切换成本:升降频、唤醒 cluster、加载计算图和恢复缓存都需要时间;
  5. 带宽瓶颈:计算单元频率提高后,如果权重或 KV cache 受带宽限制,延迟改善会很小;
  6. 热与功率上限:观测到的低频可能来自 thermal clamp 或平台功率预算,不一定是 governor 低估了利用率;
  7. 后台竞争:相机、显示、网络、系统服务和其他应用会共同消耗 CPU、内存带宽与热余量。

所以诊断要从 token 时间线开始,再关联 CPU 放置、CPU / GPU 频率、调度、内存、功耗和温度。单张利用率(utilization)截图说明不了升频策略的问题。

Decode 的交接与利用率盲区

图:示意一个依赖周期,长度不代表实测时间。CPU 或加速器空闲可能是在等待另一侧,带宽与热限制也会拉长周期;低利用率本身不证明 governor 失效。

FUSE 论文:Pixel 7 上的频点组合实验

论文 Dissecting the Impact of Mobile DVFS Governors on LLM Inference Performance and Energy Efficiency(arXiv:2507.02135)研究的问题很具体:各 governor 独立选出的频率组合,可能偏离某个 LLM 请求在给定能耗预算下的低延迟组合。FUSE 是论文提出并评估的联合频率选择原型。

我们先看实验条件,它们决定了结论能用到哪里:

  • 设备为已取得 root 权限的 Pixel 7 / Pixel 7 Pro,片上系统(SoC)是 Tensor G2,GPU 是 Mali-G710 MP7;
  • 系统为 Android 13,CPU 使用论文所述 sched-pixel / EAS 配置,GPU governor 为 quickstep,内存策略为 interactive;
  • 推理运行时为带 OpenCL / CLBlast 计算后端的 llama.cpp,论文原型基于 llama.cpp b2202;
  • 设备经过拆机并绕过电池供电,屏幕关闭,Monsoon 外置功耗仪以 0.2 ms 间隔采样;
  • 实验通过 root 接口将最低 / 最高频率设为同一值,从而固定频率;
  • FUSE 评估包含 4-bit 模型和 ShareGPT 请求子集,prompt 与输出长度受到论文设定限制。

在这些条件下,论文报告了以下代表性现象:

场景governor 观测值固定频率实验论文报告的 TPOT 变化
TinyLlama decode / GPU约 424.4 MHz848 MHz降低 41.0%
StableLM decode / GPU约 411.4 MHz762 MHz降低 34.6%
TinyLlama decode / CPU约 1130.8 MHz2252 MHz降低 13.2%
StableLM decode / CPU约 1038.8 MHz2401 MHz降低 13.4%

这些表格行来自隔离单个 governor 的实验:测 GPU 时固定 CPU/内存,测 CPU 时固定其他部件,并非每行都表示整机所有 governor 同时默认运行。以 TinyLlama 的 GPU 案例为例:论文给出默认 governor 下 TPOT 为 215.1 ms、单 token 能耗为 402.7 mJ(毫焦耳);固定 GPU 848 MHz 后,TPOT 为 126.9 ms、单 token 能耗为 396.5 mJ。在这台设备与这一请求上,提高 GPU 频率缩短了执行时间,单 token 能耗仍与默认 governor 相近。这条数据支持的是这一个组合上的观察,还撑不起“GPU 总应保持高频”这样的普遍结论。

论文的前期测量枚举 CPU、GPU、内存频率组合;FUSE 原型的高效搜索则先选 GPU 候选频点,再搜索 CPU 频点,内存保留默认 governor。运行时按 prefill/decode 阶段和输入长度范围选择已测配置。论文的 TTFT 按其 prefill 计时、不包含输入排队,与本文 API 进入到 token 交付的边界应区分。论文在其工作负载上报告:单 token 能耗相同时,TTFT 平均改善 7.0%~16.9%,TPOT 平均改善 25.4%~36.8%。

FUSE 的搜索流程与实验边界

图:FUSE 并未在其高效搜索中枚举内存频点。论文设备拆机、旁路电池、关屏,用外置功耗仪测量;结果不能直接外推到 Android 17 量产 App 或 NPU。

这项研究能支持的工程判断是:CPU、GPU、内存和请求阶段要联合测量,各自独立的利用率策略可能错过更好的组合。把结果外推之前,先对照这份清单:

  • Android 17 的调度器和厂商策略;
  • Snapdragon、MediaTek 或其他 Tensor 代际;
  • 当前 LiteRT-LM、其他 llama.cpp 版本或不同算子后端;
  • NPU delegate;
  • 未 root 的量产应用;
  • 屏幕点亮、联网、边充边测等不同整机条件。

复现这篇论文时,我们把它当作 root 实验机上的机制验证:固定频率脚本依赖 root 接口,普通量产应用没有对应入口,它会改变默认 governor 的选频行为,但只写最小 / 最大频率并不保证绕过热限制、QoS 或硬件约束;是否另改温控必须单独记录。

普通应用、系统组件与 root 实验各能做什么

能用的手段取决于代码运行的位置。下表按普通应用、系统 / 厂商组件与 root 实验机分列:

能力普通应用系统 / 厂商组件root 实验机
选择模型、量化、上下文长度、采样参数可以可以可以
选择运行时公开的 CPU/GPU/NPU 后端可以,受设备支持限制可以可以
调整线程数、批次与请求队列可以可以可以
创建 ADPF hint sessionAPI 与设备支持时可以可以可以
查询公开 thermal status / thermal headroomAPI 支持时可以可以访问更多内部信号可以
调用 AIDL Power HAL不可以直接调用可以按平台权限和架构调用可实验,不代表应用接口
固定 CPU/GPU/内存频率不可以厂商策略可管理常可通过调试接口(debug)/ sysfs 实验
修改 governor、调度器或 thermal 配置不可以平台集成阶段可以可实验
读取原始 PowerStats / 厂商能量通道通常受权限限制可以按权限获取常可获取

普通应用应通过模型、上下文长度、线程数和请求队列等参数改变工作负载,并向系统描述时间目标;指定硬件频率这件事不在应用侧。Power HAL、调度参数、GPU / 内存策略和 thermal 配置适合由系统团队评估。两类结论需要分开记录,避免把 root 实验结果写成 SDK 能力。

用 ADPF 描述周期工作

我们可以用 android.os.PerformanceHintManager 为一组相互关联、长期存在的线程创建性能提示 Session。下面示例只为应用自管的稳定 CPU 工作段建立会话;target 与 actual 都对应这个 CPU 工作段,并在场景内复用会话:

// API 31+;在负责该 CPU 工作段的长期线程中串行执行。
PerformanceHintManager manager =
        context.getSystemService(PerformanceHintManager.class);
long targetNanos = decodeCpuTargetNanos; // 产品分配给本 CPU 段的预算
PerformanceHintManager.Session session = manager == null ? null :
        manager.createHintSession(
                new int[] {android.os.Process.myTid()}, targetNanos);
try {
    while (decoder.hasNextStep()) { // 应用自己的场景循环
        long start = System.nanoTime();
        decoder.runDecodeCpuPhase(); // 不包含 UI 消费或设备侧完成等待
        long actual = System.nanoTime() - start;
        if (session != null && actual > 0) {
            session.reportActualWorkDuration(actual);
        }
    }
} finally {
    if (session != null) session.close();
}

这段代码表达“这些线程会周期性完成一轮工作,并希望在目标时间内结束”。createHintSession() 可能返回 null,所以应用必须保留没有 hint session 时仍可正确执行的路径。线程 ID 应来自被度量 CPU 工作的长期 worker;示例用 Process.myTid() 取得 Linux TID,不能用 Java Thread.getId()。同一 Session 的更新、上报和关闭要串行化;只有公共 API 明确支持且边界匹配时,才使用 WorkDuration 分别报告 CPU/GPU 工作,不能把整个服务请求等待直接当成本进程 CPU 工作。

这个 Session 的主要操作包括:

  • updateTargetWorkDuration(long):目标发生变化时更新期限;
  • reportActualWorkDuration(long):每个周期报告实际时长;
  • setThreads(int[]):长期 worker 集合变化时更新线程;
  • setPreferPowerEfficiency(boolean):在相应的 flagged API(功能开关(flag)控制可用性)可用时表达能效偏好;
  • reportActualWorkDuration(WorkDuration):在相应 flagged API(由平台功能开关控制是否可用的 API) 可用时报告总时长及 CPU / GPU 时长;
  • close():工作结束后释放 session。

LLM decode 具有周期性,可以把一个稳定的 token 生成过程或一组 token 作为工作周期,但要满足三个条件:

  1. session 覆盖长期 worker,避免每生成一个 token 就创建和销毁 session;
  2. target 来自产品可接受的 token 周期或吞吐目标,避免长期填一个达不到的极小值;
  3. actual duration 的起止点固定,避免这轮只量 GPU、下一轮又把采样与 UI 回调计入。

Prefill 往往是一段较长的批量工作。如果运行时没有稳定、可重复的 prefill 分块(chunk)周期,把整段 prefill 硬当作一个高频次重复的周期上报,信号会难以解释。我们可以为起止点明确的 prefill chunk 单独建 session,也可以只为 decode 建立 session,用 trace 另行评估 TTFT。

Decode CPU 工作段的 HintSession

图:target 与 actual 必须对应同一工作范围。ADPF 只提供提示;GPU 细分报告需遵循 WorkDuration 契约,不控制 AICore 服务或保证频率。

目标时长也不是越短越好。如果产品只要求稳定达到某个 token 速率,可以将 target 设在体验预算附近,并在温度升高、电量策略变化或应用进入后台时适当放宽。setPreferPowerEfficiency(true) 只表达偏好,最终选择仍由平台决定;调用前检查当前 SDK、flag 和设备能力。

直接调用 IPower.createHintSession() 是系统侧的用法:Framework API 负责身份、权限、生命周期与版本兼容处理,AIDL HAL 则是系统与厂商实现之间的接口,普通应用通过 Framework API 使用 hint session。

从模型和运行时减少无效工作

频率策略只能在既定工作量上做取舍。对普通应用而言,先把每个请求必须完成的计算和数据传输降下来,通常比寻找某个固定频点更稳定。

量化与模型尺寸

降低权重位宽,也就是用更少的二进制位表示权重,通常能减小模型体积和权重带宽需求,但收益取决于后端是否有相应的高效 kernel。某种量化格式的文件更小,设备上的 delegate 未必更快。选择时要同时检查:

  • 后端是否原生支持该位宽与分组方式;
  • 不支持的算子是否回退 CPU;
  • 反量化(将低位宽数值转换为计算所需格式)是否增加临时内存和计算;
  • 精度、输出稳定性和安全评测是否仍满足产品要求。

KV cache 与上下文长度

未压缩 KV cache 的主体容量可以粗略估算为:

KV bytes ≈ 2 × layers × tokens
           × num_kv_heads × head_dim × bytes_per_element

该式假定单条序列、各层 KV 形状一致且 key/value 维度相同,系数 2 对应 key 和 value。相同长度的 batch 还要乘 batch size;不同序列长度则逐条求和,并另计对齐、分页、预分配和元数据开销。分组查询注意力(GQA)、多查询注意力(MQA)、分页 cache、量化 cache、滑动窗口或特殊布局,都会让公式参数和额外元数据发生变化。

上下文越长,cache 容量与访问成本通常越高,也更容易触发内存压力。我们应记录运行时真实分配、RSS 和 PSS,只按模型配置推算是靠不住的。

产品可以控制历史消息长度、检索片段数量、system prompt 大小和最大输出 token 数。截断规则要与语义评测一起验证,避免以明显降低回答质量为代价换取更高的 tokens/s。

后端选择与图切分

选定 CPU、GPU 或 NPU 后端,整张计算图未必都在相应硬件上执行。我们通过运行时日志、delegate 诊断或厂商工具确认:

  • 哪些算子被加速后端接收;
  • 是否发生 CPU 回退,即不受加速器支持的部分转由 CPU 执行,以及是否存在频繁同步;
  • 权重格式是否匹配目标后端;
  • 编译缓存是否命中;
  • 首次请求和后续请求是否使用同一路径。

切换后端前,需要按设备型号、模型和版本建立对照基线。GPU 在某台设备上更快,在短 prompt、低电量或持续受温控限制时未必仍然更省电。

模型与 session 生命周期

如果内存预算允许,应复用模型映射、已编译计算图、tokenizer 和长期运行的 session,避免每次对话都重复加载与编译。复用时还需处理:

  • 前后台生命周期与进程回收;
  • 多会话 KV cache 的容量上限和淘汰策略;
  • 模型升级后的缓存失效;
  • 取消请求后 worker、缓冲区(buffer)和 hint session 的清理;
  • 低内存回调和设备热状态变化。

建立可复现的能效实验

这一节按五个环节搭建能效实验:固定负载、标记阶段、采集频率、区分功耗、积分与重复试验。我们从固定工作负载开始。

1. 固定工作负载组合

能效实验要能被别人复现,第一步是把工作负载钉死。至少记录以下维度:

  • 设备型号、SoC、系统构建版本(build)、内核 build;
  • 模型名称、精度、量化格式和文件哈希;
  • 运行时、delegate、驱动版本;
  • prompt token 数、目标输出 token 数和采样参数;
  • CPU 线程数、CPU 亲和性(affinity)或运行时调度选项;
  • 冷启动、热启动、首轮预热(warm-up)是否计入;
  • 屏幕亮度、网络、充电状态、后台进程;
  • 环境温度、测试开始时的电池温度和热状态(thermal status)。

结果要按这些维度分组。将 32-token prompt 和 2K-token prompt 的 TTFT 混合求平均,所得数值很难用于指导优化。

2. 用应用 trace 标记请求阶段

我们为请求、prefill、decode、采样、回调和模型加载添加 Perfetto slice,token 事件应包含请求 ID 与序号,避免并发请求互相覆盖。事件只打在阶段边界上;给每个很小的函数都生成大量 trace 事件,测量本身就会改变 token 周期。

3. 同时采集 CPU 频率变化与轮询值

采集频率时要留意两套数据来源:Perfetto 的 power/cpu_frequency tracepoint 只在频率变化时产生事件,trace 开始时不保证给出初始频率;linux.sys_stats 的 cpufreq_period_ms 可以补上周期性读取的频率值。这份精简配置同时采集频率变化、CPU 空闲、调度事件和 100 ms 频率轮询:

buffers {
  size_kb: 65536
  fill_policy: RING_BUFFER
}

data_sources {
  config {
    name: "linux.ftrace"
    ftrace_config {
      ftrace_events: "power/cpu_frequency"
      ftrace_events: "power/cpu_idle"
      ftrace_events: "sched/sched_switch"
      ftrace_events: "sched/sched_wakeup"
    }
  }
}

data_sources {
  config {
    name: "linux.sys_stats"
    sys_stats_config {
      cpufreq_period_ms: 100
    }
  }
}

这段配置使用 ring buffer,数据写满后会覆盖旧数据。100 ms 适合作为观察长期趋势的起点,解析单个很短的 kernel 就不够用了。分析短周期时,我们把频率变化事件、调度 slice 和应用 token 标记放在一起看,同时评估 trace 本身的开销。

CPU 频率变化事件与轮询互补

图:变化事件与轮询提供不同证据。CPUFreq 通常按 policy/簇共享,读到的频率及支持情况依设备实现解释;GPU/NPU/内存轨道另行确认。

GPU、NPU、互连和内存频率数据源依赖设备,采集前要确认 counter 的名称、单位和更新含义。

4. 区分整机功耗与部件能量

这段 Perfetto android.power 配置每 250 ms 采集电量百分比、电流、电压等电池计数器,并请求采集部件能量轨道(power rails):

data_sources {
  config {
    name: "android.power"
    android_power_config {
      battery_poll_ms: 250
      battery_counters: BATTERY_COUNTER_CHARGE
      battery_counters: BATTERY_COUNTER_CAPACITY_PERCENT
      battery_counters: BATTERY_COUNTER_CURRENT
      battery_counters: BATTERY_COUNTER_VOLTAGE
      collect_power_rails: true
    }
  }
}

这份配置里的电池电流和电压代表整机,屏幕、调制解调器与后台任务都计在结果里。USB 充电会改变电池计数器的含义,边充电边测到的数据要与放电测试分开比较。collect_power_rails 依赖设备的 IPowerStats 实现,很多量产设备不提供可用的 rail;拿不到 rail 数据时就如实报告“不可用”,别用估算值填补。

IPowerStats 把 energy consumer 与 energy meter 作为厂商定义的数据返回,累计能量以微瓦秒等明确单位计量;调用方还要处理结果暂不可用、缺少按 UID 归因,以及通道名称因设备而异等情况。原始 HAL 数据通常不在普通应用的权限范围内。

使用外置功耗仪时,要记录供电路径、采样频率、电压设置、电池是否被旁路、屏幕状态与 ADB 连接状态。外置仪器能提高采样一致性,但拆机并旁路电池后的结论仍只适用于相应实验条件,与正常电池供电的数据要分开存放、分开比较。

电池计数、PowerStats 与外置仪器

图:电池计数描述电池侧整机流量;power rail/consumer 是设备定义的子系统口径,可能缺失。外置测量需记录电池旁路、屏幕与 ADB 条件。

5. 积分、基线与重复试验

若测得随时间变化的电压 V(t) 和电流 I(t),请求窗口能量可以积分近似为:

E_request = ∫ V(t) × I(t) dt

也就是把每个时刻的功率 V(t) × I(t) 按时间累加。电流正负方向由数据源定义,积分前必须核对。如果要扣除 idle baseline,应使用相同的屏幕、网络、温度和采样配置,在相近时间测量,并同时保留未扣除的整机能量;基线扣除(baseline subtraction)会放大短请求的误差。

每组配置都需要多次重复,并随机安排测试顺序,免得所有“高性能配置”都赶在设备尚未升温时运行,而“节能配置”全在设备已经升温后运行。至少报告样本数、中心趋势、离散程度和失败请求;长时间测试还应按时间窗口分别展示结果。

把温控纳入性能结论

短时间 benchmark 往往让激进升频策略表现更好,连续对话则更容易暴露热约束。一个完整测试应包含:

  1. 一组从相同初始温度开始的短请求,用于分析 TTFT 和单轮 decode;
  2. 持续多轮或固定时长的稳态(steady-state)测试;
  3. thermal status、thermal headroom、CPU / GPU 频率上限和 token 延迟的同一时间轴记录;
  4. 进入限制前、限制发生时、限制后的分段统计;
  5. 冷却后复测,排除后台任务与偶发驱动错误。

“尽快完成后进入空闲”(race to idle)只有在更高瞬时功率被更短执行时间抵消,并且没有提前触发温控或占用其他部件预算时,才有能效优势。判断依据是请求总能量、持续 TPOT、热状态和恢复时间,别只看峰值频率。

应用可以按产品目标采取降载措施:

  • 缩短允许的上下文或输出上限;
  • 降低并发请求数;
  • 切换更小模型或更低成本后端;
  • 放宽 token 速率目标;
  • 在后台或高温时暂停非必要 prefill;
  • 提前向用户展示“设备较热,生成速度可能下降”,并支持取消。

热余量(thermal headroom)是预测信号,其数值方向和可用性要按 API 文档处理。它适合辅助选择负载档位,但保证不了未来某一时刻仍有固定的频率预算。相关 API 与阈值含义见 5.2 节。

常见现象的排查顺序

把前面各节的机制压缩成一张排查表:先按现象定位方向,再做后续验证。

现象先检查后续验证
冷启动 TTFT 高,热启动正常模型读取、权重映射、计算图编译、缓存(cache)命中分开测量输入输出(I/O)、编译(compile)和首轮 kernel slice
冷热 TTFT 都高prompt 长度、prefill 后端、CPU 回退固定输入,比较后端覆盖和频率 / 带宽
平均 TPOT 尚可,偶发停顿明显P95 inter-token latency、线程抢占、垃圾回收(GC)、回调阻塞对齐 sched_switch、GC 与 token 事件
CPU / GPU 利用率都不高但 TPOT 高跨设备等待、短 burst、单线程采样查看线程状态、提交间隙和频率变化
频率提高后速度不变内存带宽、计算图回退、串行阶段、thermal clamp比较算子时间与 CPU / GPU 上限
前几轮快,随后持续变慢温度、频率上限、系统 thermal status冷却复测并观察限制发生点
tokens/s 提高但能量明显增加请求总时长、瞬时功率与任务结束后的空闲尾部功耗(idle tail)比较焦耳 / 请求(joules/request)和焦耳 / token(joules/token)
更换 NPU 后端反而慢计算图分区、编译、数据转换、CPU 回退(fallback)查看后端日志和冷 / 热启动差异
并发吞吐提高但单请求体验变差队列等待、带宽竞争、热预算分开报告吞吐、TTFT、TPOT 和公平性

我们排查时每轮只改变一个主要变量。同时更换模型、delegate、线程数和系统版本,就算结果改善,也说不清是哪项变化起了作用。

发布前检查清单

  • Android 平台结论已按 android-17.0.0_r1 复核;
  • 内核调度与 CPUFreq 结论已按 android17-6.18-2026-06_r6 复核;
  • EAS 选核与 schedutil / CPUFreq 选频已分开解释;
  • GPU、NPU、内存频率数据标明设备和厂商差异;
  • TTFT 的冷 / 热启动统计范围明确;
  • TPOT 同时提供 P50 / P95 或时间曲线;
  • 能耗分母和采样窗口明确;
  • prompt、输出长度、模型、运行时与 delegate 固定;
  • root 固频实验没有写成普通应用能力;
  • ADPF session 使用长期 worker,target 与 actual 起止点一致;
  • Power rail 不可用时,没有用估算值冒充部件能量;
  • 持续测试包含 thermal status/headroom 与频率上限;
  • 性能收益通过回答质量、失败率和取消路径验证。

版本边界与源码索引

Android 17 平台

  • frameworks/base/core/java/android/os/PerformanceHintManager.java
    • createHintSession()
    • Session.updateTargetWorkDuration()
    • Session.reportActualWorkDuration()
    • Session.setThreads()
    • flagged Session.setPreferPowerEfficiency()
    • flagged Session.reportActualWorkDuration(WorkDuration)
  • hardware/interfaces/power/aidl/android/hardware/power/IPower.aidl
    • createHintSession()
    • createHintSessionWithConfig()
    • hint session 通道(channel)与支持信息
  • hardware/interfaces/power/stats/aidl/android/hardware/power/stats/IPowerStats.aidl
    • 能量消费者(energy consumer)、能量计量器(energy meter)与驻留时间(residency)接口

上述平台路径按 android-17.0.0_r1 复核。

Android 17 内核

  • kernel/sched/fair.c
    • find_energy_efficient_cpu()
    • compute_energy()
  • kernel/sched/cpufreq_schedutil.c
    • get_next_freq()
    • map_util_freq()
    • Android 厂商钩子(vendor hook)android_vh_map_util_freq
  • drivers/cpufreq/cpufreq.c
    • CPUFreq policy 与服务质量(Quality of Service,QoS)约束
  • drivers/devfreq/
    • 通用 devfreq 框架;具体 GPU/内存实现需继续查看设备内核

上述内核路径按 android17-6.18-2026-06_r6 复核。

LiteRT-LM 的版本边界

LiteRT-LM 是 Google AI Edge 在 AOSP 之外维护的端侧生成式 AI 运行时项目。其公开仓库提供 Kotlin / C++ 接口、.litertlm 模型格式,以及 CPU、GPU、NPU 后端支持说明;具体硬件覆盖受版本、模型和设备影响。应用应固定经过验证的 LiteRT-LM 版本和模型产物,并保存运行时日志。

android-17.0.0_r1 能确定的是 Android 平台 API、Framework 与 HAL 的版本;LiteRT-LM 仓库版本要单独查证,AOSP 源码和 LiteRT-LM 发布版本(release)必须分别记录。系统服务提供的模型能力也不等同于应用内嵌 LiteRT-LM,二者的模型、权限、更新和资源管理互不相同,详见 5.5 节。

外部运行时与研究

FUSE 论文同样使用独立版本,不会随 android-17.0.0_r1 标签固定在同一版本。