22. 端侧推理:在手机和笔记本上运行模型
前面二十一篇都在数据中心里:80 GB 显存、每秒几 TB 的带宽、几十个请求一起批处理。这一篇把模型搬到用户自己的设备上:笔记本、手机、嵌入式设备。约束完全变了:内存只有几 GB 到几十 GB,而且和系统、其他应用共用;带宽低一个数量级;有电池和散热的限制;只服务一个用户,没有批可以凑。读完你应该能估算一个模型在某台设备上能跑多快,了解主流的端侧运行时,并知道端侧测试要额外关注什么。
为什么要在端侧运行
资料助手想做一个离线版本,让出差的同事在没有网络时也能查制度。把模型放在设备上运行,有四个常见的理由:
- 延迟:不经过网络,没有排队,首字可以很快。
- 离线:没有网络也能用。
- 隐私:数据不离开设备,适合敏感内容。
- 成本:计算由用户的设备承担,不占用服务器 GPU。
代价是只能运行小得多的模型,回答质量通常不如云端的大模型。很多产品采用端云结合:简单、隐私敏感的请求在端侧处理,复杂的请求发往云端。
同一套原理,不同的数字
第 6 篇的 Roofline 在端侧同样成立,而且更加明显:端侧只有一个用户,没有批处理可以摊薄权重搬运,Decode 几乎完全受内存带宽限制。
单用户 Decode 速度的上限 ≈ 内存带宽 ÷ 每步要读的字节数(主要是权重)。以 Llama-3.1-8B 的 4 位量化版本为例,llama.cpp 常用的 Q4_K_M 格式约 4.58 GiB(约 4.9 GB):
| 设备 | 内存带宽 | Decode 速度上限 |
|---|---|---|
| 一张 H100 | 3.35 TB/s | 约 680 token/s |
| Apple M4 Max(最高配置) | 546 GB/s | 约 110 token/s |
| Apple M4 Pro | 273 GB/s | 约 55 token/s |
| Apple M5 | 153 GB/s | 约 31 token/s |
这些都是理论上限,实际速度会低一些,但比例关系基本准确。手机的内存带宽通常还要再低,所以手机上常用的是 1B 到 4B 的更小模型。
Prefill 则是计算受限的。端侧的计算能力远低于数据中心 GPU,输入很长时,Prefill 会非常慢:在手机上读几千 token 的文档可能要好几秒甚至更久。这就是为什么端侧应用尤其要控制输入长度,并且很重视用 NPU 来加速 Prefill。
内存:能用的比看到的少
端侧的内存账和第 4 篇一样,权重、KV、工作区三笔,只是数字小得多:
text
权重 ≈ 参数个数 × 每个参数的位数 ÷ 8
3B 模型:FP16 约 6 GB,INT8 约 3 GB,INT4 约 1.5 GB但设备标称 8 GB 内存,不代表模型能用 8 GB。操作系统、其他应用都要占用;手机系统对单个应用的内存还有上限,超过就会被系统直接终止。KV 也不能忽视:小模型的上下文一长,KV 可能占到几百 MB 甚至上 GB。
Apple 芯片的统一内存是一个特点:CPU 和 GPU 共用同一块内存,不需要把权重从内存拷贝到显存。一台 128 GB 统一内存的 Mac 能放下的模型,比很多数据中心单卡还大,只是带宽低一些。
一个模型怎样到达设备
端侧部署通常经过这样一条链路:
理解这条链路,关键是分清三个层次:
- 模型格式:模型以什么文件保存,比如 llama.cpp 用的 GGUF、ExecuTorch 的
.pte、ONNX 的.onnx、Core ML 的.mlpackage。 - 运行时:在设备上加载模型、管理内存、执行计算的程序。
- 后端:运行时实际调用哪个硬件执行,比如 CPU、GPU(Metal、Vulkan)、NPU。
同一个运行时可能支持多个后端,同一个后端也可能被多个运行时使用。“支持 Android”“支持 NPU”“支持这个模型”是三个独立的条件,要确认的是模型 × 运行时版本 × 后端 × 设备这个组合确实可用。
主流的端侧运行时
| 运行时 | 特点 | 适合的场景 |
|---|---|---|
| llama.cpp | C/C++ 实现,GGUF 格式,量化种类丰富,支持 CPU、Metal、CUDA、Vulkan 等多种后端 | 桌面和服务器本地运行,Ollama 等工具的常见底层 |
| MLX / mlx-lm | Apple 为自家芯片设计的数组框架,利用统一内存 | Mac 上的本地推理与实验 |
| MLC-LLM | 基于 TVM 的机器学习编译,一份模型编译到多种平台,包括浏览器(WebGPU) | 需要跨平台、浏览器内运行 |
| ExecuTorch | PyTorch 官方的端侧运行时,模型导出成 .pte,后端包括 XNNPACK(CPU)、Core ML、Qualcomm、Vulkan 等 | 移动应用,PyTorch 生态的模型 |
| ONNX Runtime、LiteRT、Core ML | 通用的端侧推理运行时 | 通用模型,尤其是非生成式模型 |
| sherpa-onnx | 语音识别、合成等语音任务(第 19 篇) | 端侧语音 |
llama.cpp 的 GGUF 量化类型名字很多,比如 Q4_K_M、Q8_0。Q4_K_M 是最常用的平衡选择:主体用分组的 4 位整数量化,部分对质量敏感的张量用更高位数,平均每个参数约 4.9 位,所以 8B 模型约 4.6 GiB。Q8_0 约 8.5 位,质量更接近原模型,体积约为 Q4_K_M 的 1.7 倍。
端侧的硬件:CPU、GPU 和 NPU
- CPU:兼容性最好,任何模型都能跑。依赖向量指令(ARM 上的 NEON 等)加速,大小核的调度会影响速度。
- GPU:通过 Metal(Apple)、Vulkan 或 OpenCL 调用,适合矩阵运算,但在部分平台上和 CPU 之间搬运数据有成本。
- NPU:专门为神经网络设计,能效最高,尤其适合计算受限的 Prefill。但它支持的算子和数据类型有严格限制,遇到不支持的算子,那部分计算会回退到 CPU 执行,来回切换可能让整体比纯 CPU 还慢。高通平台的 NPU 通过 Qualcomm AI Engine Direct SDK(QNN)调用,ExecuTorch 等运行时提供了对应的后端。
端侧的优化手段
数据中心里的很多技术在端侧仍然适用,但侧重点不同:
- 量化是第一选择。 内存和带宽都紧张,4 位权重量化几乎是端侧大模型的标配,直接决定能否放下和 Decode 速度。
- 选更小的模型。 端侧常用 1B 到 4B 的模型,或者从大模型蒸馏出的小模型。在目标任务上评估,小模型往往比想象中够用。
- 投机解码很合适。 端侧只有一个用户、Decode 严重访存受限,算力有富余,正好是第 15 篇说的投机解码最有效的场景。
- 前缀缓存。 系统提示是固定的,可以把它的 KV 预先算好保存,每次对话直接加载,减少端侧最慢的 Prefill。
- 用内存映射加载权重。 文件按需读入内存,启动更快,系统内存紧张时也可以把未使用的部分换出。
- 控制上下文长度。 端侧 Prefill 慢、KV 占内存,输入能短则短。
端侧要测什么
端侧测试除了第 20 篇的 TTFT 和 Decode 速度,还要关注:
- 冷启动时间:应用打开后,模型加载到可以回答要多久。
- 峰值内存:是否会触发系统的内存限制。
- 安装包体积:模型是随应用分发还是首次使用时下载。
- 功耗与温度:持续生成几分钟后,设备会发热降频,速度可能明显下降。要测持续运行后的稳定速度,而不只是第一次的峰值。
- 设备差异:不同型号、不同芯片的表现差异很大,要在目标用户的主流设备上分别测。
测试时同样要固定模型文件、量化格式、运行时版本、线程数和后端,预热后多次测量,记录分位数。
端侧推理遵循同样的原理,只是约束更紧:单用户、无批处理,Decode 速度上限由内存带宽除以模型大小决定;内存要和系统共享,Prefill 受限于有限的算力。部署时分清模型格式、运行时和后端三个层次,确认整个组合可用。4 位量化、小模型、投机解码和预先计算的系统提示 KV 是端侧最有效的手段,测试时要额外关注冷启动、内存、功耗和持续运行后的速度。
回到起点
这个系列从小林问“年假怎么申请?”开始。一句话变成 token,经过 Prefill 读题、Decode 答题,在显存里留下 KV Cache;Decode 受带宽限制,所以要批处理、要分页管理 KV、要共享前缀、要把长输入切块;再往下,量化让每一步搬更少的字节,投机解码让一步推进更多 token;规模变大以后,模型被拆到多张卡上,Prefill 和 Decode 分到不同的机器;最后,测试证明优化有效,运维让服务长期稳定,端侧把模型带到用户手里。
遇到一项新的推理技术时,可以继续用这套坐标系问:它在优化 TTFT 还是 TPOT?在省显存、省带宽,还是省计算?它付出了什么代价?可以回到系列总览按问题重读,也可以去模型数值精度专题深入了解量化背后的数值格式。
