ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

小米大模型端侧部署落地探索:LLM量化、内存调度与算子适配实战

小米大模型端侧部署落地探索:LLM量化、内存调度与算子适配实战 简介这份PDF整理自小米大模型算法工程师黄武伟的端侧部署落地探索分享面向关注端侧AI、大模型轻量化与移动端推理的算法工程师、移动端开发者及技术决策者帮助理解大模型在手机等终端设备上落地的可行路径与工程取舍。内容围绕端侧AI的重要性、LLM端侧部署挑战、相关技术探索与总结展望四大模块展开具体涵盖可靠性、隐私安全、个性化服务与成本效益四类端侧优势云端与端侧在算力、内存、功耗、带宽上的差异对比以及剪枝、量化、Sheared LLaMA、TransAct、结构搜索、模型分片等优化手段并给出端侧推理速度与内存瓶颈的量化分析。资源包为1个PDF文件大小约4.23MB单文件结构便于通读与检索。目前已有144人学习适合希望系统了解端侧大模型部署技术路线与优化思路的读者参考。1. 端侧跑大模型为什么小米的落地路径值得单独拆一遍去年底拿到一份名为「小米大模型端侧部署落地探索」的 PDF翻完之后我最大的感受是端侧部署这件事真正难的不是把模型跑起来而是让它在手机这种散热受限、内存吃紧、功耗敏感的硬件上稳定地跑出可用的推理速度。小米在这份材料里给出的思路核心围绕 LLM 量化、内存调度和算子适配三条线展开恰好也是当前端侧 AI 硬件部署最值得复现的路径。如果你手上有 Qwen 系列或类似量级的开源模型想把它塞进一台 8GB 内存的安卓设备里并且希望首 token 延迟控制在可接受范围内那这套方案里的选型逻辑和参数取舍基本可以直接搬。它适合两类人一是做端侧推理框架的工程师二是想在自己的 App 里集成离线大模型能力的开发者。下面我按「先立住理论、再动手复现、最后排坑」的顺序把这份材料里最值得抄的部分拆开讲。2. 端侧 LLM 部署的选型逻辑量化精度、内存占用与推理框架怎么三角平衡2.1 为什么端侧部署绕不开量化从 FP16 到 INT4 的收益账在服务器上跑 LLM大家默认 FP16 甚至 BF16显存管够。但到了端侧一台典型的小米旗舰手机可用给模型推理的内存往往只有 24GB还要和系统、相机、后台应用抢。一个 7B 参数的模型FP16 权重就要占 14GB 左右根本放不下。所以端侧部署的第一道门槛就是量化。量化的本质是用更低的位宽表示权重和激活值。常见档位有 INT8、INT4以及更激进的混合量化。以 7B 模型为例INT8 大约占 7GBINT4 能压到 3.5GB 左右再配合分组量化group-wise quantization实际占用还能再降。小米这份材料里重点讨论的是 INT4 分组量化在端侧 NPU 上的适配因为 INT4 是当前精度和体积平衡得比较好的档位。但量化不是免费的。位宽越低精度损失越明显尤其是对注意力层的 Key/Value 缓存做量化时长文本生成容易出现重复、跑题。我的经验是权重可以大胆上 INT4但 KV Cache 至少保留 INT8否则多轮对话到第三轮就开始胡言乱语。这个取舍在材料里也有体现它建议对 FFN 层做更激进的量化对 Attention 层保留更高精度。提示量化档位不是越低越好先确认你的推理框架支持哪些量化格式再决定压缩目标。2.2 推理框架怎么选NCNN、MNN、TFLite 与厂商 NPU SDK 的适配差异端侧推理框架的选择直接决定了你能不能用上手机 NPU 的算力。目前主流的有几条路线NCNN 和 MNN 是腾讯阿里系的开源方案社区活跃CPU 推理优化好TFLite 背靠 GoogleGPU Delegate 成熟而小米这类厂商通常会推自己的 NPU SDK比如通过高通 QNN 或联发科 NeuroPilot 做硬件加速。材料里给出的结论很务实如果追求快速验证先用 MNN 或 NCNN 在 CPU 上跑通量化模型确认精度可接受如果要上生产必须走厂商 NPU SDK因为 CPU 推理的功耗和延迟在端侧不可接受。以高通平台为例QNN SDK 支持 INT8 和部分 INT4 算子但需要把模型转成 DLC 格式转换过程中算子不支持是最大的坑。我一般会建议团队先做一次算子兼容性扫描把模型里所有算子列出来对照目标 NPU SDK 的支持列表不支持的算子要么替换要么回退到 CPU。这个步骤不做后面转换失败会浪费大量时间。2.3 内存与功耗的硬约束端侧部署必须算清的三笔账端侧部署和服务器最大的区别是内存、功耗、散热三者互相制约。材料里提到一个很具体的数字在小米某款旗舰机上7B INT4 模型推理时峰值内存占用约 4.2GB持续推理 10 分钟后机身温度上升约 8℃系统开始降频。这意味着你不能只关注模型本身的大小还要算三笔账。第一笔是权重内存这是固定的第二笔是 KV Cache它随序列长度线性增长长对话场景下可能超过权重本身第三笔是中间激活值虽然可以复用内存池但峰值仍然可观。我的做法是在模型加载前就预分配好内存池把权重、KV Cache、激活值分区域管理避免运行时碎片化。同时设置一个序列长度上限比如 2048 token超过就截断或滑动窗口。功耗方面如果 NPU 支持动态频率调节尽量让推理跑在中低频档位虽然单次延迟略高但持续推理更稳定。3. 从 PDF 到可运行 Demo量化、转换、部署的三步复现路径3.1 用 GPTQ 或 AWQ 做 INT4 量化脚本参数与精度校验假设你手上有一个 Hugging Face 格式的模型比如 Qwen2.5-7B第一步是量化。目前端侧最常用的是 GPTQ 和 AWQ 两种后训练量化方法。GPTQ 对硬件友好AWQ 在精度保持上略好。下面是一个用 AutoGPTQ 做 INT4 量化的最小脚本from transformers import AutoModelForCausalLM, AutoTokenizer from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig model_id Qwen/Qwen2.5-7B tokenizer AutoTokenizer.from_pretrained(model_id) # 量化配置4bit分组大小128对称量化 quantize_config BaseQuantizeConfig( bits4, group_size128, desc_actFalse, # 端侧建议关闭减少推理开销 ) # 加载原始模型 model AutoModelForCausalLM.from_pretrained(model_id, torch_dtypeauto) # 准备校准数据这里用少量样本即可 calib_texts [端侧部署需要平衡精度和速度。] * 128 # 执行量化 quant_model AutoGPTQForCausalLM.from_pretrained( model_id, quantize_configquantize_config, modelmodel, ) quant_model.quantize(calib_texts) quant_model.save_quantized(./qwen2.5-7b-int4-gptq) tokenizer.save_pretrained(./qwen2.5-7b-int4-gptq)这段脚本的关键参数有三个。bits4指定量化位宽group_size128是分组大小越小精度越高但元数据越多端侧一般 128 是平衡点desc_actFalse关闭激活值重排序能减少推理时的额外计算代价是精度略降。校准数据不需要多128 条左右就能让量化误差收敛。量化完成后必须做精度校验。我的做法是拿一组固定 prompt对比原始模型和量化模型的输出看困惑度PPL上升是否在可接受范围。一般 INT4 量化后 PPL 上升 0.20.5 算正常超过 1.0 就要检查校准数据或调整 group_size。3.2 模型格式转换从 Hugging Face 到端侧推理引擎的算子对齐量化后的模型还是 PyTorch 格式端侧推理引擎不认。你需要把它转成目标框架的格式。以 MNN 为例转换流程如下# 安装 MNN 转换工具 pip install MNN # 将 Hugging Face 模型导出为 ONNX python -m transformers.onnx --model./qwen2.5-7b-int4-gptq --featurecausal-lm onnx_model/ # 用 MNN 转换器把 ONNX 转成 MNN 格式 mnnconvert -f ONNX --modelFile onnx_model/model.onnx \ --MNNModel qwen2.5-7b-int4.mnn \ --bizCode MNN \ --weightQuantBits 4这里最容易翻车的地方是算子对齐。Hugging Face 模型里的一些算子比如 Rotary Embedding 的自定义实现ONNX 导出时可能变成一堆基础算子组合MNN 转换时又可能不支持某些组合。我的血泪经验是导出 ONNX 后先用 onnxruntime 跑一遍确认输出和原始模型一致再转 MNN。如果 MNN 转换报错看日志里哪个算子不支持要么在导出时替换成等价实现要么在 MNN 里注册自定义算子。另外--weightQuantBits 4是在转换时再做一次权重量化如果你已经在 GPTQ 阶段量化过这里可以跳过避免二次量化导致精度崩掉。3.3 在安卓端加载与推理JNI 接口、线程数与 KV Cache 配置模型转好后最后一步是在安卓 App 里加载推理。MNN 提供了 Java API但为了性能通常走 JNI 调用 C 接口。下面是一个简化的 JNI 加载示例// native-lib.cpp #include MNN/Interpreter.hpp #include MNN/Tensor.hpp extern C JNIEXPORT jlong JNICALL Java_com_example_llm_LlmEngine_loadModel(JNIEnv *env, jobject, jstring modelPath) { const char *path env-GetStringUTFChars(modelPath, nullptr); // 配置推理后端CPU 4线程开启 FP16 计算 MNN::ScheduleConfig config; config.numThread 4; config.type MNN_FORWARD_CPU; MNN::BackendConfig backendConfig; backendConfig.precision MNN::BackendConfig::Precision_High; config.backendConfig backendConfig; auto interpreter MNN::Interpreter::createFromFile(path); auto session interpreter-createSession(config); env-ReleaseStringUTFChars(modelPath, path); return reinterpret_castjlong(session); }线程数设置很关键。端侧 CPU 通常有 4 个大核和 4 个小核numThread4让推理跑在大核上避免小核拖后腿。但线程数不是越多越好超过物理大核数会导致调度开销。KV Cache 的配置在 MNN 里通过setCache接口管理建议预分配固定大小的 Cache比如 2048 token避免动态扩容带来的内存抖动。推理时还有一个隐藏坑输入 token 的 embedding 查表。如果词表很大比如 15 万每次查表都是一次随机内存访问端侧缓存命中率低。我的优化做法是把词表按频率排序高频词放前面或者直接用量化后的 embedding 表减少内存带宽压力。4. 端侧部署避坑实录量化掉点、算子不支持、内存泄漏与发热降频4.1 量化后模型胡言乱语校准集与 group_size 的排查顺序现象INT4 量化后模型在简单问答上表现正常但一到长文本生成就开始重复、跑题甚至输出乱码。原因最常见的是校准集分布和实际使用场景不匹配。如果你用通用语料做校准但实际场景是代码生成量化误差就会在代码 token 上放大。其次是 group_size 设得太大比如 256导致每组内权重差异大量化精度下降。解决先换校准集用 128 条实际场景的样本重新量化如果还不行把 group_size 降到 64 或 32代价是模型体积增加约 10%15%。另外检查desc_act是否误开端侧建议关闭。4.2 转换成功但推理崩溃算子回退与内存对齐的检查清单现象模型转换没报错但在安卓端加载后推理直接崩溃日志显示segmentation fault或out of memory。原因通常是算子回退导致的。某些算子 NPU 不支持框架自动回退到 CPU但回退路径的内存对齐没处理好或者回退后的算子实现有 bug。另一个常见原因是内存池大小没设对KV Cache 动态扩容时越界。解决先在 PC 上用 MNN 的 CPU 后端跑一遍确认模型本身没问题然后在安卓端打开 MNN 的详细日志看哪些算子走了回退路径。如果是内存对齐问题检查模型输入输出的 tensor 是否要求 16 字节对齐不对齐就手动 padding。4.3 推理十分钟后变卡发热降频与线程调度的联动调整现象刚启动时推理速度正常连续跑 10 分钟后明显变慢首 token 延迟从 200ms 涨到 800ms。原因手机散热能力有限持续推理导致 SoC 温度升高系统触发降频保护。如果推理线程一直跑在大核最高频温度上升更快。解决把推理线程绑定到中核或者动态调整线程数。我的做法是监听系统温度超过阈值就把numThread从 4 降到 2同时降低 NPU 频率档位。虽然单次延迟增加但持续推理更稳定。另外推理间隙让线程休眠 1020ms给散热留出缓冲。4.4 内存泄漏的隐蔽来源KV Cache 复用与 Tensor 释放时机现象App 运行一段时间后内存持续上涨最终被系统杀掉。原因KV Cache 没有复用每次推理都新分配一块内存或者 Tensor 在 JNI 层创建后没有及时释放Java GC 管不到 native 内存。解决在 JNI 层维护一个全局的 KV Cache 池每次推理前 reset 而不是重新分配。Tensor 的释放要配对createTensor和destroyTensor必须成对出现。建议用 RAII 封装避免手动管理遗漏。5. 进阶技巧用混合精度与动态序列长度把端侧推理压榨到极限走到这一步模型已经能跑了但离「好用」还有距离。我一般会再压榨两轮。第一轮是混合精度不是所有层都适合 INT4。Attention 的 QKV 投影对精度敏感保留 INT8FFN 层参数量大但对精度容忍度高压到 INT4。这样整体体积比全 INT4 只大 15%但生成质量明显提升。实现方式是在量化配置里按层指定 bitsGPTQ 支持layer_config参数把q_proj、k_proj、v_proj设为 8其余设为 4。第二轮是动态序列长度。端侧场景下大部分请求的输入很短但偶尔有长文本。如果固定分配 2048 token 的 KV Cache短请求就浪费了内存。我的做法是分档短请求用 512 token 的 Cache长请求切到 2048 档位。切换时把已有 KV 数据拷贝过去虽然有一次拷贝开销但整体内存利用率提升明显。还有一个容易被忽略的技巧是预热。首次推理时NPU 需要加载权重、初始化算子延迟通常是稳定后的 35 倍。在 App 启动后、用户发起请求前先跑一次空推理把 NPU 预热用户感知的首 token 延迟会好很多。验证方法上我习惯用三个指标首 token 延迟、解码速度token/s、连续推理 10 分钟后的速度衰减率。前两个看体验第三个看稳定性。如果衰减率超过 30%说明散热或调度有问题需要回头调线程和频率。最后说个我自己的教训端侧部署不要追求一次到位。我最早想把所有优化都堆上去结果量化、转换、NPU 加速一起上出了问题根本不知道是哪一步的锅。后来改成每步单独验证量化后先在 PC 上跑通转换后先在 CPU 后端跑通最后才上 NPU。这样虽然慢一点但每一步都有后悔药。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进