“350亿参数,住进一台手机”——这话第一次听像发布会PPT,直到我自己真把模型跑起来才发现,让35B的Transformer住进手机内存,比让它住进机房难了足有一个量级。这里的核心瓶颈不是算力,是“内存墙”:参数要占地方,推理要喂带宽,手机RAM就那么点,闪存还慢。这篇文章我会从参数账目算起,拆解量化、mmap、统一内存、进程保活这些端侧推理的关键技术,再附上我实测两台手机跑35B的真实数据和建议。不管你是在玩llama.cpp还是在研究端侧AI部署,这篇都值得看完。
1. 先算账:350亿参数到底在手机里占多少地方
很多人低估了“35B”这三个数字的分量。在做任何端侧部署之前,得先把内存账单算清楚,不然后面每一步都是瞎调参。
1.1 一张内存账单:权重、KV Cache和运行时
大模型推理时的内存占用分三块:权重、KV Cache、运行时激活值和临时缓冲。权重是大头,KV Cache随着上下文长度线性增长,运行时在batch=1的逐token生成场景占用相对小,但也不能忽略。
以35B参数模型为例,不同精度的权重文件大小如下:
| 精度 | 每参数字节数 | 权重总大小 | 8GB手机 | 16GB手机 | 24GB手机 |
|---|---|---|---|---|---|
| FP16 | 2 | 约70GB | 完全没戏 | 完全没戏 | 完全没戏 |
| INT8 | 1 | 约35GB | 没戏 | 没戏 | 没戏 |
| INT6 | 0.75 | 约26GB | 没戏 | 勉强搭配swap | 没戏 |
| INT4 | 0.5 | 约17.5GB | 没戏 | 勉强搭配swap | 能完整塞下 |
| INT3 | 0.375 | 约13.1GB | 搭配swap | 能完整塞下 | 舒服 |
| INT2 | 0.25 | 约8.75GB | 搭配swap | 舒服 | 舒服 |
再算KV Cache。假设一个35B模型有大约60层,KV heads走GQA后为8个,head_dim为128,上下文长度设为4096,FP16存储下KV Cache大约是:
2(K和V) × 4096(序列长度) × 60(层数) × 8(KV头数) × 128(维度) × 2(字节) ≈ 536MB
如果上下文拉到8K,这个数字直接翻倍到1GB以上。再叠加运行时激活值、临时buffer、App本身开销,实际上你想跑4096上下文,至少要在“权重+1GB”的额度上再留出500MB甚至更多。
所以最现实的方案是:INT4量化后的模型约17.5GB,加上KV Cache和运行时,总量逼近19GB。这意味着:
- 24GB内存机型:可以完整装下,运行得比较从容。
- 16GB内存机型:勉强,必须用更小的量化档、更短的上下文,并依赖系统内存压缩。
- 8GB内存机型:单靠RAM装不下Q4,必须走闪存流式加载,速度会非常感人,这个后面讲到。
我见过很多人拿了Q4的35B模型就往16GB手机上塞,结果进程直接被系统杀掉。这不是模型问题,是预算问题——你从一开始就该把“权重大小 + KV Cache + 运行时余量”三部分都算进去。
1.2 内存墙其实是两堵墙:容量墙与带宽墙
“内存墙”这个词被滥用了,但真正做端侧部署的人都清楚,它其实是两堵墙。
第一堵是容量墙:模型权重和KV Cache超过了可用内存容量,放不下。前面算的账就是在讲这堵墙。容量墙容易理解,量化、剪枝、蒸馏都是拆这堵墙的手段。
第二堵是带宽墙:即使模型全部驻留在内存里,每个token生成时,处理器都必须把全部权重从内存读取一遍。这是因为Transformer是自回归解码——每生成一个新token,所有层都要重新计算一遍,每一层的权重都要重新被读一次。这个过程不是按需只读一小部分,而是全量扫描。
带宽墙有多狠,算一下就知道。iPhone 15 Pro上的A17 Pro配的是LPDDR5内存,理论峰值带宽大约51.2GB/s。如果35B模型用INT4量化,权重17.5GB,每生成一个token都要把这17.5GB全部读一遍,理想情况下:
17.5GB ÷ 51.2GB/s ≈ 0.34秒/token
这已经是理论极限了,折算下来大约3 tokens/s。但这是纯读取权重的时间,还没算算子、KV Cache读写、缓存未命中、系统其他进程抢带宽。所以实际能跑到2 tokens/s已经非常不错。
高通骁龙8 Gen 3用LPDDR5X,峰值带宽约68GB/s,理论极限能到4 tokens/s;天玑9300用LPDDR5X-9600,带宽约77GB/s,理论能到4.5 tokens/s。这些数字就是35B模型在手机上的天花板,跟算力关系不大,纯粹是内存带宽锁死的。
把带宽墙和生活类比一下:你从仓库(内存)往工位(计算单元)搬零件,工位处理每个零件需要1秒,但仓库每秒只能递出10个零件。就算工位再快,产出也被仓库递件速度卡住。模型的“计算单元”早就不是瓶颈,瓶颈在内存这个“递件窗口”。
这一点决定了策略:想要提高手机上的生成速度,要么减小权重体积(量化、蒸馏、MoE),要么提高有效带宽(更好的内存标准、更高效的缓存利用),光堆算力毫无意义。
1.3 为什么手机不能像电脑那样随意换页
有人会问:电脑内存不够还可以靠虚拟内存swap到硬盘,手机为什么不行?
技术上手机也有类似机制。Linux内核支持mmap缺页换入,iOS在内存压力下也会把页面丢弃并重新从闪存读回。但问题在于手机闪存的随机读取性能远不如PC的NVMe SSD,而且手机上App的存储配额、文件系统权限、进程冻结机制都让“换页”变得极不可控。
PC上你swap到NVMe SSD,顺序读取能到2-3GB/s,随机读取的IOPS也有几十万。手机闪存顺序读取确实不差,但随机读取性能差很多。LLM推理时的权重访问是高度迭代的——每一层按顺序扫过去,每次解码都扫一遍,这意味着如果你的模型文件大于可用RAM,系统会反复换页。闪存的写放大、磨损、功耗和发热都会成为问题。
更麻烦的是手机的内存回收策略。Android的Low Memory Killer(LMK)会根据进程优先级杀进程,你的大内存推理App往往就是第一个被杀的对象。iOS的Jetsam机制更严,后台App的页面会被优先清空。
所以结论很直接:如果模型权重超过手机物理内存的60%,这趟“住进手机”的路就会很艰难。要么压缩权重,要么接受慢速,要么干脆换一台更大内存的设备。
2. 量化是入场券:真4位和假4位
既然FP16的35B模型有70GB,那一切的前提就是把权重大幅度压缩。量化是目前唯一在手机端“真能跑”的方案,但它不是无代价的。
2.1 怎么把70GB压到17.5GB:分组量化与混合精度
量化的核心思路,是把每个浮点权重从FP16的16位,压到INT4的4位,用更少的比特存储同样的信息。但直接每个权重单独映射到4位会带来严重的精度损失,所以现代量化都采用“分组缩放”策略。
以llama.cpp里常用的Q4_K_M为例:权重矩阵会被切成若干group,每个group通常是128个权重值。group内部共享一个缩放因子(scale)和零点偏移(zero point)。量化时,把这128个浮点值先归一化到某个范围,再映射到16个离散整数档位(4位能表示0-15)。解码时,根据scale把整数还原成接近原始浮点的值。
这样做的意义在于:相邻权重往往数值范围接近,shared scale能保留group内的相对精度,而不像全局scale那样让小数部分失真。实验结果也表明,group size越小,精度越高,但存储scale的开销也越大。group=128是一个内存开销和精度的平衡点。
Q4_K_M里的“_M”指的是混合精度。具体来说,模型的某些敏感层(比如attention的key、value投影层)用更高的精度(如Q6),而FFN部分用Q4。这是因为attention对数值误差更敏感,如果KV投影层的量化误差过大,模型的上下文理解和长文本表现会明显退化。K_M这种混合策略,用大约多1-2%的存储开销,换回了接近Q5甚至Q6的生成质量,性价比很高。
还有一个常见概念是“激活值量化”。权重可以离线量化,但激活值只能在运行时算,所以端侧推理框架通常保留FP16的激活值,这被称为“W4A16”(权重4位、激活16位)。跟训练时的“INT8全量化”不同,推理侧W4A16是主流,因为端侧不需要反向传播,只关心激活值精度对生成的影响。
2.2 量化档位怎么选:Q8到Q2的实测取舍
不同量化档位的取舍,本质是质量与体积的交换。以35B模型为例,我在测试里常用这些档位:
| 量化档位 | 近似体积 | 能跑的内存 | 质量表现 |
|---|---|---|---|
| Q8_0 | 约35GB | 40GB以上 | 接近原版,几乎无损 |
| Q6_K | 约26GB | 32GB以上 | 质量很好,日常够用 |
| Q5_K_M | 约23GB | 24GB以上 | 质量良好 |
| Q4_K_M | 约18GB | 24GB勉强/16GB需要swap | 质量可接受,端侧主力档位 |
| Q3_K_M | 约14GB | 16GB能跑 | 质量明显下降,复杂任务拉胯 |
| Q2_K | 约9.5GB | 8GB+swap能跑 | 质量大幅下降,算术和逻辑易出错 |
我个人的实践建议是:
- 24GB内存机型,优先Q4_K_M,这是速度、质量、内存三方权衡下的甜点档。
- 16GB内存机型,Q4_K_M大概率在长上下文时被杀,建议Q3_K_M,同时把上下文压到2048。
- 8GB内存机型,别贪,Q2_K配swap,或者干脆换小模型。
量化的代价在具体任务上体现得很明显。我拿同样35B模型分别做Q4_K_M和Q2_K实测,中文常识问答时Q4的表现接近未量化版本,而Q2_K经常在“9.11和9.8谁大”这种问题上翻车,代码生成的错误率也明显更高。你要是有中文长文本或数学推理需求,务必留足内存上Q4,别为了塞进手机强行Q2。
2.3 量化的代价:为什么手机上35B可能干不过蒸馏小模型
这是很多人的认知盲区。手机上的“35B Q2_K”和云端的“35B FP16”完全不是同一个东西。Q2_K只有约9.5GB,质量损失非常大,在很多实际任务上可能还不如一个7B的Q4。
我做一个直观对比:Qwen2.5-7B-Instruct用Q4_K_M,文件约4.4GB,在手机上能跑12-15 tokens/s。Llama-3.1-35B用Q2_K,在8GB手机上只能跑1-2 tokens/s,质量还时好时坏。如果你是做客服问答、资料摘要这种实际业务,我会毫不犹豫选7B Q4,而不是35B Q2。
所以“参数越大越聪明”是有前提的——量化损失可以抹平几个B的参数优势。这也是为什么端侧AI最终还是要走“大模型蒸馏成小模型”的路,而不是把大模型硬塞进手机。后面第5节还会细说。
3. 住进手机以后:mmap、统一内存和进程保命
权重量化完,只是拿到了“入场券”。真正住进手机,还要面对操作系统这一层。这部分的经验,是我跑了无数次“进程被杀”以后总结出来的。
3.1 mmap:让闪存排队假装内存
llama.cpp加载GGUF模型时默认使用mmap,而不是一次性把整个文件读进内存。mmap的作用是把磁盘上的模型文件映射到进程的虚拟地址空间,实际数据按需加载——用到了哪个页面,内核才从磁盘读哪个页面。
这个机制在PC上很成熟,但手机上完全不一样。PC的NVMe SSD快,内存不够时换页也能支撑。手机的闪存虽然顺序读取速度不差,但端侧LLM推理的权重访问是反复顺序扫描,每一层权重都要完整过一遍。如果模型文件大于物理内存,mmap的页面会在每次扫描时反复缺页、换入、丢弃、再缺页,性能可能跌到0.5 tokens/s以下。
所以mmap是一把双刃剑:
- 好处:不用一次性把所有权重加载进内存,进程启动快,内存占用低。
- 坏处:内存不够时,每次解码都会触发大量磁盘换页,速度急剧下降。
我在8GB的iPhone上跑35B Q2_K时,明显感受到“首token要等半天,后续token等得更久”的换页地狱。一个非常实用的排查方法是:如果生成的每token延迟呈现锯齿状波动,时快时慢,那就是页面换入的典型症状。
相应地,llama.cpp有个--mlock参数,作用是把权重页面锁进物理内存,防止被换出。PC上这招很好用,但手机上内存本来就紧,mlock只会让你更快触发OOM后被系统杀掉。我在手机端从来没有成功用过mlock跑35B级别模型,不建议尝试。
3.2 统一内存和GPU分片:为什么Apple Silicon占便宜
手机SoC和PC的一个大区别在于:GPU和CPU共享同一块物理内存,这在硬件上天然是“统一内存”。但不同平台的软件栈差距很大。
Apple的Metal后端在llama.cpp里非常成熟,GPU可以直接访问模型权重和KV Cache,不需要CPU中转拷贝。这也是为什么同样跑7B模型,A17 Pro的Metal后端往往能跑出比一些Android旗舰更稳的速度。
Android这边虽然GPU也共享RAM,但Vulkan驱动的成熟度参差不齐。Adreno的驱动在高端机上表现不错,Mali的驱动就看机型和驱动版本了。我实测过同一台手机用CPU跑和Vulkan跑,结果经常是CPU路径更稳定——Vulkan编译着色器以后有加速,但不同opition下反而更慢,这是很常见的坑。
llama.cpp的-ngl参数控制多少层offload到GPU。在手机上,这个参数不是越大越好,因为GPU和CPU共享内存,你offload的层数越多,系统内存占用越大。通常我建议先试-ngl 20到40,观察内存压力和一个token生成速度,再逐步增加。
还有一个容易踩的坑是:别在手机上用--tensor-split这类参数硬拆。手机没有多GPU,拆了反而会让每个设备之间的同步开销吃掉所有收益。
3.3 后台进程与LMK:安卓的生死局
这部分的知识是我用“被杀了无数次”换来的。
Android的Low Memory Killer会根据oom_score_adj给每个进程打分,内存不足时优先杀掉分数高的进程。一个占用大量内存的本地LLM推理进程,在系统看来就是一个完美的“牺牲品”。你切到别的App发个消息,回来发现模型已经没了,需要重新加载——这种事我踩了太多次。
实用的应对策略:
- 在系统设置里为推理App关闭电池优化,并手动锁定后台任务。
- 如果用的是Termux跑llama.cpp,在系统应用管理里把Termux设为“不受限制”。
- 尽量把模型文件放在App自己的沙盒目录,而不是公共存储目录,避免文件系统权限问题导致mmap失败。
- 不要追求过长的上下文。4096和8192之间,KV Cache的差距可能直接决定你是否触发LMK。
iOS这边虽然Jetsam机制更严格,但用LLMFarm这类原生App时,模型加载后只要不切到重度App,通常能稳定跑完一次对话。iOS不支持像Android那样手动调LMK白名单,所以唯一的办法就是控制模型体积、控制上下文长度,让进程的内存画像保持在安全线以下。
4. 实测日志:两种“跑起来”的姿势
理论讲再多,不如看真实跑起来的样子。我分别用两台设备做了35B模型的端侧推理测试,涵盖了“内存不够硬撑”和“内存够用舒服跑”两种典型场景。
4.1 姿势一:8GB iPhone强行streaming 35B Q2
测试设备是一台8GB内存的iPhone 15 Pro,模型是Llama-3.1-35B-Instruct的Q2_K GGUF,文件大小约9.8GB,用的是LLMFarm加载,Metal后端。
启动加载过程花了大概40秒,因为9.8GB文件超过物理内存,首token延迟长达20多秒。生成阶段稳定下来后,速度在0.8-1.3 tokens/s之间波动。内存压力大时,系统会让App的页面频繁换出,速度会掉到0.5 tokens/s以下。
体验上,这基本属于“能跑但没法用”的范畴:问一个简单问题,等半分钟才看到第一个字开始蹦,然后每个字间隔一秒多。我把上下文从4096降到2048,速度略有提升,但依然达不到2 tokens/s。A17 Pro的LPDDR5带宽51.2GB/s在这个场景下完全被换页拖垮。
4.2 姿势二:24GB安卓把Q4整个塞进内存
第二台测试设备是24GB内存的骁龙8 Gen 3机型,模型是同样的Llama-3.1-35B-Instruct,但换成了Q4_K_M,GGUF约18.5GB。用llama.cpp的Android构建跑,开启Vulkan后端,-ngl设为30,上下文4096。
模型加载大约12秒(18.5GB全部进入内存),首token延迟约3秒,之后稳定在2.8-3.4 tokens/s之间。这个速度虽然比不上云端,但已经属于“能忍受”的范畴:等一小会儿,能看到完整生成的句子,用来写邮件草稿、做简单代码补全都行。
发热和降频是另一个影响因素。连续生成2分钟后机身明显发烫,A17 Pro和骁龙8 Gen 3都会降频,导致tokens/s下降10-20%。我后来加了一个散热背夹,速度波动明显改善。
4.3 性能数据汇总与选择建议
| 设备 | 模型量化档 | 权重大小 | 首token延迟 | 稳定生成速度 | 体验评价 |
|---|---|---|---|---|---|
| iPhone 15 Pro 8GB | Q2_K | 约9.8GB | 20秒以上 | 0.8-1.3 tokens/s | 能跑但基本没法用 |
| 骁龙8 Gen 3 24GB | Q4_K_M | 约18.5GB | 约3秒 | 2.8-3.4 tokens/s | 勉强可用 |
我个人的结论是:35B模型在手机上“能跑”早就不是问题,问题是“跑到什么速度才算能用”。如果你只有8GB内存的设备,老老实实跑7B-14B级别模型,体验会好很多。如果非要体验35B,请选24GB以上内存的旗舰机,并接受每秒2-3个token的现实。
5. 还能更快吗:投机解码、KV Cache与模型精简
跑通只是第一步,能不能让体验从“痛苦”变成“凑合”,才是真正决定端侧大模型能否落地的关键。
5.1 投机解码:小模型打草稿,大模型批改
投机解码的思路是:先用一个很小的模型(比如3B)快速生成4-8个候选token,然后把候选token序列交给35B大模型一次性验证。大模型确认哪些token是对的,对的直接采纳,错的就从第一个错的位置重新生成。
为什么这个策略适合手机?因为大模型的decode瓶颈是内存带宽,逐token生成要一次次扫描全部权重。投机解码通过batch验证,让一次权重扫描能“批发”出多个token,相当于把内存带宽的利用率拉高了。实测中,如果小模型和大模型能力差距不大,投机解码能把端侧生成速度提升1.5到2倍。
llama.cpp已经内置了投机解码支持,在服务器参数里加上--speculative模型路径和草稿模型参数即可。手机上内存更紧张,但这个思路依然是当前效能提升空间最明显的方案。需要注意:小模型不能太“笨”,否则它生成的候选token大部分被大模型否决,反而增加无效开销。
5.2 KV Cache也能压缩
KV Cache在长上下文下会占几百MB甚至1GB以上。对手机这种寸土寸金的环境,KV Cache量化是性价比很高的一步。
llama.cpp提供--cache-type-k和--cache-type-v参数,把KV Cache从FP16压到Q8_0甚至Q4_0。实测在35B模型、4096上下文场景下,KV Cache量化能省下200-300MB内存,质量损失在大多数任务中几乎不可感知。MLC-LLM等框架也支持FP8的KV Cache。
另外,对话轮次多的场景建议定期清空KV Cache或做context compaction。自己写客户端时,多轮对话的上下文会越积越长,KV Cache会持续增长直到把内存吃光。这算是我在实践中踩过最深的坑之一——明明权重没动,内存却随着对话轮次一路涨到被系统杀掉。
5.3 真正的终局:MoE与蒸馏
就算量化再激进、投机解码再高效,把35B Dense模型全量塞进手机,依然是一件“性价比很低”的事。端侧AI真正的大方向是两个:MoE架构和蒸馏小模型。
MoE(混合专家)模型的总参数量可能很大,但每次推理只激活其中一小部分专家。比如Mixtral 8x7B总参数约47B,但每个token只激活约12B参数;Qwen3-30B-A3B总参数30B,激活参数只有3B。这类模型特别适合端侧:总容量决定了知识量,激活参数决定了推理时的实际内存带宽开销。做端侧部署选型时,优先看激活参数量而不是总参数量。
蒸馏则更直接:把35B模型的知识“压缩”到3B-7B的小模型里。Llama-3.2-3B、Qwen2.5-3B这类模型在手机上能跑到15-20 tokens/s,很多NLP任务的效果已经够用。对绝大多数实际场景来说,一个流畅运行的3B蒸馏模型,比一个卡成PPT的35B硬塞模型更有价值。
5.4 实用建议:手机跑35B之前先想清楚场景
最后给准备自己动手折腾的朋友几条实操建议:
- 先量化再跑:直接用FP16的35B模型在手机上跑,等于自杀。至少转成GGUF并量化到Q4_K_M再谈其他。
- 控制上下文长度:手机端别贪4096以上,KV Cache和内存是线性关系。
- 先跑通再优化:首token延迟高,先看是加载问题还是PreFill问题;稳定速度差,先看是不是触发了换页。
- 用框架现成方案:iOS直接用LLMFarm,Android直接用llama.cpp或Ollama,不要从零造轮子。MLC-LLM也可以,但配置门槛高一些。
- 散热别忽视:持续推理发热降频会让tokens/s再掉20%,风扇或散热背夹不是智商税。
我在实际折腾中最大的体会是:参数数量的执念并没有意义,“模型多大能在手机上跑多快”才是真正要回答的问题。350亿参数住进手机这件事,技术上确实做到了,但目前它是一件“为了证明能跑而跑”的事。真要天天用,我反而建议你把目光从小参数版本开始,把速度和稳定性做扎实,再考虑怎么往上够更大的模型。端侧推理的乐趣本来就该在“折腾”这个过程里,而不在最终那个炫酷的数字上。