
从“内存墙”被反复提起到“350亿参数住进一台手机”出现在各种新闻标题里这个方向的热度已经不需要我再渲染。真正值得聊的是这事儿到底怎么做到的代价是什么以及你自己能不能在手机上复现一遍。这篇东西就是我基于实际部署经验把存储体积、运行内存、内存带宽这三个绕不开的关卡逐层拆开给想折腾端侧大模型的工程师和玩家一条能直接照着走的路。先说结论350亿参数的模型通过4bit量化体积能压到17.5GB左右配合16GB以上内存的旗舰机加上量化和内核优化确实能跑但体验和云端满血版是两回事。内存墙的本质从来不只是“装得下装不下”更扎心的是“就算装下了读出来也要时间”。1. 内存墙到底是一堵什么样的墙1.1 先算一笔账350亿参数的“行李”有多重参数体量的第一道坎就是存储。一个参数在模型文件里占多少字节取决于精度。FP32是4字节FP16是2字节INT8是1字节INT4是0.5字节也就是4个bit。咱们按350亿参数也就是35B来算精度350亿参数的理论体积12GB内存手机16GB内存手机24GB内存手机FP16约70GB完全装不下完全装不下完全装不下INT8约35GB完全装不下完全装不下完全装不下INT4约17.5GB需要极限优化能跑但紧张比较从容3bit约13.1GB能装质量存疑能装能装2bit约8.75GB能装质量损失大能装能装注意这个表只是“模型权重”的体积。推理的时候手机还要额外给KV Cache、中间激活、临时缓冲区留内存。也就是说一台12GB内存的手机光是把17.5GB的INT4权重塞进去就已经撑满了ROM不是RAM物理内存不够就是不够。这也是为什么真正跑大模型的手机起步就得16GB24GB才是舒服区。我自己的习惯是拿到一个模型先别急着量化先用脚本看一眼它的config.json里num_hidden_layers、hidden_size、intermediate_size这些参数心里估算一下原生体积。有了这个底再决定用多少bit量化而不是看到“350亿”就直接上4bit最后发现内存爆了。1.2 算得快不如读得快被忽略的带宽瓶颈装进内存只是第一步。Transformer做自回归生成每生成一个token都要把整个模型的权重从头到尾读一遍。这意味着模型推理的耗时大头不是“计算”而是“把数据从内存搬到计算单元”。这就是内存带宽的瓶颈。举个具象的例子假设手机内存带宽是70GB/s左右旗舰机LPDDR5X常见水平跑一个17.5GB的INT4模型光是把权重读一遍就需要0.25秒所以理论上限大约只有4个token/s。而你往内存里看一眼实际还有反量化、算子调度、缓存未命中这些开销最终体验普遍要打个对折。这就是内存墙的第二层含义模型大不只是占地方更是拖速度。就算你把350亿参数硬塞进手机带宽不足跑起来依旧像老牛拉车。要缓解带宽压力常用三板斧一是降低权重的bit数保证体积尽量小二是用KV Cache量化、Flash Attention这类优化减少推理过程中的中间数据搬运三是用NPU或GPU做算子下沉让一部分计算在更靠近存储单元的硬件上完成变相降低DRAM的带宽压力。2. 把350亿参数“瘦身”塞进手机的三条主线2.1 量化压缩把每个参数从16bit降到4bit量化是端侧部署的基石。原理说起来不复杂FP16能表达很大范围的数值但神经网络权重经训练后大部分值集中在一个相对小的区间里于是我们可以用更少的bit去近似它。具体做法有两种路线。一种是训练后量化模型训练好之后拿一批校准数据统计每个张量的数值分布然后决定scale和zero point。端侧生态里最常见的GPTQ、AWQ都属于这一类它们会额外考虑某些“异常值”通道对量化误差的影响尽量保住敏感权重。另一种是直接训练时就考虑低比特比如QLoRA里的NF4这类量化感知训练效果好但门槛高手机端用得少。实操上最容易上手的方式是把模型转成GGUF格式然后用llama.cpp的量化脚本处理。比如python convert_hf_to_gguf.py ./Qwen2.5-32B-Instruct --outfile qwen2.5-32b-f16.gguf ./llama-quantize qwen2.5-32b-f16.gguf qwen2.5-32b-Q4_K_M.gguf Q4_K_MQ4_K_M这种量化类型不是把所有层一刀切到4bit。K_M后缀代表它是一种混合策略注意力机制的权重可能保留更高精度比如Q6而FFN层用4bit某些小的张量甚至用Q5、Q8。这是经验值好处是同样体积下质量损失更小。我踩过的一个坑是量化时group size选太大会导致损失明显选太小又会让文件体积变大。拿350亿这种体量的模型来说group size选128通常是平衡点选32虽然质量更好但模型体积肉眼可见地涨手机端未必划算。2.2 架构层面的减法MoE、GQA与KV Cache优化如果仅靠量化仍然压不进内存就得从模型结构上想办法。这里最大的突破口是稀疏激活也就是MoE混合专家架构。MoE模型的总参数量很大但每次推理只激活其中一部分专家。比如总参数47B的Mixtral 8x7B每次实际用到的参数只有约13B。换句话说它把“知识容量”和“单次计算量”解耦了。手机端跑这类模型权重文件还是要全部加载因为不知道下次会命中哪个专家但推理时的计算量和带宽消耗明显低于同参数的稠密模型。除了MoEKV Cache优化也是端侧部署的重点。KV Cache是Transformer推理时保存历史Key和Value的地方它的体积和上下文长度成正比而且随着上下文变长会吃掉越来越多的内存。这里有个非常直观的算术题。一个经典的7B模型结构假设32层、32个KV头、每个头128维、FP16存储那么每个token的KV Cache是2K和V乘以32层乘以32头乘以128维乘以2字节约512KB。跑2048个token的上下文光KV Cache就要吃掉1GB内存。如果模型换成GQAGrouped Query Attention把KV头从32个降到8个同样的体积直接缩小到原来的四分之一也就是128KB/token。这个优化对长上下文场景的意义比量化还大。如果你的模型本身没有GQA结构在端侧部署时也可以通过KV Cache量化的方式补救比如把KV Cache从FP16压到INT8同样能省一半。代价是极端长上下文下模型对细节的记忆会略微模糊但大多数对话场景感受不明显。2.3 异构计算与内存复用把每一分资源都用起来量化和架构优化都是“物理上的压缩”剩下要解决的是工程调度问题。手机不是一台只有CPU的设备。旗舰SoC上通常有CPU、GPU、NPU或者叫APU/DSP它们各有各的存储层级和带宽特性。真正靠谱的端侧引擎不会把整个模型都丢给CPU去跑而是把矩阵乘法这类计算密集的算子分给GPU或NPUCPU只负责调度和部分前处理。MLC-LLM之所以在部分手机上性能比llama.cpp好就是因为它对GPU算子做了更激进的编译优化。另一个招是mmap加载。GGUF格式天然支持内存映射意思是模型文件不用一次性全部读入物理内存而是让操作系统按页按需加载。这样你可以在16GB内存手机上运行一个17.5GB的模型因为实际只有被访问到的页才会占用物理内存。代价是频繁的page fault会拖慢速度所以运行前最好用mlock把关键层锁在内存里避免被系统回收。最后还得说一个残酷的事实手机一旦发热SoC就会降频。降频不只是算力下降内存带宽同样会缩水。我实测过一台手机跑大模型连续十分钟后因为机身温度上来生成速度能掉到初始状态的六成左右。所以端侧推理不是“能跑就行”散热设计和功耗控制直接影响实际体验。3. 端侧推理引擎选型与全链路部署3.1 四款主流端侧推理引擎怎么选现阶段端侧跑大模型绕不开这几个引擎我直接说过一遍引擎模型格式核心卖点适合谁llama.cppGGUF生态最全CPU优化好工具链完整想在安卓上快速验证、折腾量化的玩家MLC-LLMTVM编译产物针对移动端GPU/Metal/Vulkan深度优化追求GPU加速能接受编译复杂度的开发者MNN-LLMMNN格式国内团队维护算子覆盖好上手相对平滑工程化落地想集成进自己App的团队ExecuTorchTorchScript/定制Meta官方支持配合PyTorch生态已经在用PyTorch想跟进官方方案的团队坦白说如果你只是想“在手机上跑一个本地模型”首选永远是llama.cpp。原因很简单模型文件好找量化命令现成Termux里装一个就能跑踩坑的人多网上资料也多。MLC-LLM的优势在于GPU路径如果你用的是iPhoneMetal或者对性能有执念它更合适。MNN则更适合你要把模型嵌进自己App的场景它的Android适配和权限管理做得更工程化。3.2 llama.cpp路线从模型文件到手机可跑的完整流程我用llama.cpp跑通过一个32B模型4bit整个过程大概是这么几步。第一步拿到原生模型。去Hugging Face找一个你信得过的开源模型比如Qwen、Llama、Mistral系。注意看许可协议商用和纯自用差别很大。第二步转格式加量化。在电脑上执行前文那段命令把Hugging Face格式转成GGUF再量化成Q4_K_M。如果电脑配置一般直接去Hugging Face搜别人转好的GGUF文件也行省时省力。第三步传到手机。用USB或者局域网上传到手机的存储目录。第四步在手机Termux里编译llama.cpppkg update pkg install git cmake build-essential git clone https://github.com/ggerganov/llama.cpp cd llama.cpp mkdir build cd build cmake -DCMAKE_BUILD_TYPERelease .. make -j4编译完成后用llama-cli跑起来./llama-cli -m /storage/emulated/0/qwen2.5-32b-Q4_K_M.gguf -c 2048 -t 4 --mlock这里的参数我解释一下-c控制上下文长度2048是比较稳的起步值-t是线程数手机一般用4或6开满反而会因为调度开销变慢--mlock把模型锁进物理内存防止被系统回收导致卡顿。实测下来7B模型在近两年的旗舰机上能有7到15 token/s14B大概4到8 token/s32B基本就是2到4 token/s了。做文字聊天勉强能等做实时对话就会觉得拖沓。3.3 MLC-LLM路线面向移动端GPU的编译式部署MLC-LLM的思路不同它把模型编译成TVM的IR再针对具体硬件平台生成算子。好处是GPU利用率通常更高特别在iOS的Metal路径上性能表现明显优于纯CPU方案。流程上先用Python端编译python -m mlc_llm.build --model Qwen2.5-32B-Instruct --quantization q4f16_1 --target android --output-dir dist/Qwen2.5-32B-Instruct-q4f16_1编译产物是一个包含模型参数和推理库的Android库之后你用Android Studio打包成APK或者直接跑官方提供的Android demo工程把编译产物替换进去。MLC这套路线的坑在于它的编译时间很长而且不同SoC的GPU驱动差异会导致同样的模型在不同手机上速度差很多。同一个模型在骁龙和天玑上可能一个能用GPU加速另一个GPU算子报错只能回退CPU。所以我通常建议先拿llama.cpp验证模型可用性再上MLC优化性能不要一上来就走编译路线。4. 跑起来之后性能观测与常见问题排查4.1 为什么量化后反而更慢了这是我最常被问到的问题之一。量化按道理说数据量变小加载变快应该更快才对。但实际跑起来量化后反而更慢的情况很常见。原因不在量化本身而在于反量化开销。如果你用的是纯CPU推理4bit权重读取之后要先反量化成浮点数再送进矩阵乘法。这个转化过程如果算子实现不够好会制造大量额外的计算指令。尤其在一些不支持neon/AVX指令集的旧手机上反量化开销可能比省下的加载时间还多。另一个原因是内存对齐。GGUF的量化张量不是简单地按字节对齐存储不同量化类型的布局策略不同。有些手机内存访问对齐要求严格跨页访问会触发性能惩罚。遇到这种情况可以换成Q5_K_M或者Q6_K试试有时候高bit版本反而比低bit版本更快。最后检查一下线程数。线程开太多CPU核心之间争夺内存带宽性能不升反降。端侧推理不是线程越多越好我通常从4线程开始逐个往上试哪个速度最快就用哪个。4.2 内存占用怎么比预期高这么多模型权重只是内存占用的一部分。很多人盯着17.5GB的权重却忘了KV Cache、激活值、临时缓冲区加起来可能超过2GB。排查顺序我一般是这样先用free -h看整体内存状态。跑模型之前记下空闲内存跑一个固定长度的对话后再记一次差值就是推理进程的实际占用。如果KV Cache占大头把上下文长度从4096降到2048占用立刻减半。如果激活值占大头检查是不是开了过大的batch。端侧对话模式batch通常为1不需要贪大。还有个常见的隐性占用是内存碎片。手机长时间运行内存碎片化严重即使总量够也可能分配不出连续的大块内存。遇到这种情况重启一下手机或者杀掉后台进程再跑往往就好了。4.3 输出质量变差的排查顺序量化后模型“变笨”这种感觉很主观但也确实存在客观规律。排查顺序应该是第一确认量化损失是否正常。你可以拿一段固定测试文本让原模型和量化模型各生成一遍对比perplexity困惑度差异。如果差异在10%以内通常可以接受如果明显恶化说明模型对量化特别敏感这种时候考虑用AWQ替代普通4bit量化或者把关键层提升到6bit。第二检查采样参数。温度、top-p这些参数对输出质量的影响比量化更大。温度太高会让回答变得发散温度太低会让回答变得机械。别一上来就怪模型先调回一套保守参数再对比。第三确认上下文是否被截断。手机端为了省内存很多人会把上下文长度设得很短。一旦对话超过这个长度早期内容会被丢弃模型就会“失忆”答非所问看起来就像变蠢了。“内存墙之下”这个标题其实还有一层潜台词就算用尽所有技术把350亿参数塞进手机也还有散热、续航、带宽这么多现实约束在等着你。我个人的体会是端侧推理真正的价值不是替代云端而是覆盖那些没网、隐私敏感、低延迟的场景。所以别执着于一定要跑最大的模型先让“恰好够用”的模型在手机上稳定运行再一步步往上加才是更务实的路径。