“内存墙”这个词,凡是在端侧跑过大模型的人,都不陌生。它不像算力墙那样能靠堆芯片解决,而是把参数量、内存容量、内存带宽、闪存速度全搅在一起,但凡有一个环节跟不上,模型就跑不起来。标题里提到的“350 亿参数”,正好卡在一个非常微妙的量级:比 7B、13B 聪明得多,又比 70B 小了一圈,理论上还有被塞进手机的可能性。这篇文章就把这件事彻底拆开:350 亿参数到底会吃掉多少内存,为什么说“内存墙”才是端侧大模型最难撞破的一堵墙,以及我实际测试下来,哪些路真的能让它住进手机。
1. “350亿参数”听起来不大,算一算才知道内存墙有多厚
1.1 参数量转化为内存的公式,先算清账
所有大模型部署的起点都是一道小学数学题:内存占用 = 参数量 × 每个参数的字节数。一个参数如果是 FP32(单精度浮点数)就是 4 字节,FP16/BF16 是 2 字节,INT8 是 1 字节,INT4 理论上是 0.5 字节。
那么 350 亿参数分别对应多少?
- FP32:350 × 10^8 × 4B = 140GB
- FP16/BF16:350 × 10^8 × 2B = 70GB
- INT8:350 × 10^8 × 1B = 35GB
- INT4:350 × 10^8 × 0.5B = 17.5GB
看到这个表,很多人第一反应是“哦,FP16 要 70GB,INT4 降到 17.5GB,好像还可以接受”。但问题是你得同时考虑手机的实际可用内存。一台 16GB 的旗舰机,系统、桌面、后台应用已经吃掉了 3-5GB,真正能给大模型用的往往只有 10-12GB。17.5GB 依然放不下,更别提 FP16 了。
这还没算运行时内存。模型推理时不是只把权重放在内存里就完事,还需要激活值(activation)和 KV Cache。激活值跟 batch size、序列长度有关,35B 模型一次前向传播可能产生几百 MB 到 1GB 以上的中间结果。KV Cache 是按“层数 × 注意力头数 × 序列长度”线性增长的,上下文开到 2048 时,35B 模型的 KV Cache 普遍在 1-2GB。也就是说,即便 4bit 量化,完整加载一个 35B 模型的总内存预算大约是 17.5GB + 1.5GB + 0.5GB 框架开销,接近 20GB。
所以第一层内存墙是“容量墙”:手机内存根本装不下完整模型。这也是为什么很多端侧模型只能跑 7B、8B,因为 8B 的 INT4 量化权重只有 4GB 左右,加运行时也能压在 6GB 以内,正好卡在主流手机的可用内存上限附近。
1.2 容量墙好破,带宽墙才是真正的硬骨头
容量墙至少可以用量化、内存映射等手段绕过去,但带宽墙没这么好对付。自回归语言模型生成文本时,每生成一个 token 都要做一次完整的前向传播,而每次前向传播都需要把每一层的权重从内存搬到计算单元。换句话说,每输出一个词,35B 模型的全部权重都要被“读一遍”。
这意味着生成速度的上限完全由内存带宽决定:单 token 需要读取的数据量 = 参数量 × 每个参数的字节数。如果按 4bit 量化后的 17.5GB 来算,手机内存带宽如果是 50GB/s,理论极限是每秒 2.8 个 token。如果按 INT8 的 35GB 来算,就只有 1.4 token/s。这个速度连“能看”的标准都达不到——你打一句话,可能得等半分钟才看到回复开始往外蹦。
有人可能说,手机 SoC 算力不是很高吗?天梯图上分数挺吓人的。但算力和内存带宽是两码事:NPU/GPU 算得再快,数据从内存搬不过来也是白搭。这就好比一个五星级厨师配了个小水管厨房,锅再多,水龙头流量不够,出菜速度照样上不去。手机上的大模型推理瓶颈,绝大多数时候不在“算不动”,而在“喂不饱”。
1.3 手机芯片其实没那么弱,但被内存体系卡死了
我之前在测试 7B 模型时就有体会:同一颗芯片,7B INT4 能跑到 12-20 token/s,但换成一个 20B 的模型,即使量化到同样的精度,速度直接掉到 3-5 token/s。原因就是模型大了,每 token 要搬的数据量成倍增加,带宽成了铁短板。手机厂商再怎么堆 NPU TOPS,也解决不了 LPDDR5 内存颗粒的物理带宽上限。
所以讨论“350 亿参数住进手机”,本质上是在讨论如何在容量墙和带宽墙之间找到一条狭窄的缝。接下来的所有方案,都是在跟这两个物理限制博弈。
2. 让模型住进手机的四条主流路线
2.1 量化:先把体重减下来,但不是随便砍精度
量化是当前收益最高、也最成熟的手段。它把连续分布的浮点权重映射到离散整数,比如把 FP16 的 2 字节权重压到 INT4 的 0.5 字节,体积直接降到四分之一。但量化不是“拍脑袋把小数抹掉”,而是需要一套校准过程:拿一段有代表性的文本跑一遍模型,统计每一层激活值的分布范围,再据此计算缩放因子和零点偏移。
LLM 领域常用的 GPTQ 和 AWQ 就是两种主流的权重量化方法。GPTQ 基于二阶误差补偿,量化后能在层级别做误差修正;AWQ 则根据激活值的重要性来保护敏感权重通道。实际操作中,AWQ 在低比特(如 INT4)下通常比 GPTQ 更稳,因为它不是一视同仁地砍精度,而是让重要的权重通道保留更高精度,次要通道再压低。
这里有一个新手很容易踩的坑:量化尽量别用“全局最大最小值”来算缩放因子,尤其是 35B 这种大模型,不同层的激活范围差异很大。我之前试过用训练集尾部几百条数据做校准,结果量化完模型变成“胡言乱语复读机”。后来换成与目标任务更贴近的中文问答数据、长文本混合校准,质量才恢复正常。校准集不需要很大,几百到一千条就够,但必须覆盖模型可能遇到的真实输入分布。
另一个常见选择是混合精度:不是所有层都用 INT4,而是把注意力层的部分关键线性层保持 FP16 或 INT8,其余 FFN 层用 INT4。这样既能把总体积压到 18-20GB,也能把最敏感的层保护住,质量损失更小。
2.2 剪枝与稀疏化:拆掉冗余连接,但别指望“剪完就能跑”
大模型有大量冗余参数,这个说法在学界吵了很多年,但从工程角度看,直接把不重要的权重设为零得到的稀疏矩阵,在手机上并不好跑。原因很简单:稀疏计算需要按索引取数据,内存访问模式不规则,GPU 上都容易性能退化,手机 CPU/NPU 更麻烦。
更现实的是结构化剪枝,比如整行整列地剪掉注意力头、剪掉 FFN 的中间维度。这样矩阵保持规则的形状,计算密度不降,但参数量变小。不过结构化剪枝几乎必须伴随重训练——直接剪完精度崩得没法看。对普通玩家来说,自己剪一个 35B 模型并不现实,训练资源和时间成本太高。真正的商用方案通常是预训练阶段就控制模型结构,或者在云端先蒸馏、再剪枝、最后量化,手机端只拿到最终产物。
所以剪枝这条路,我建议把它当作“可选项里的最后一项”。如果模型本来就稀疏(比如 MoE 结构),那是另一套逻辑,后面会专门说。如果是稠密模型,优先考虑量化和动态加载,收益要直接得多。
2.3 知识蒸馏:不硬塞大模型,而是让“学生”接班
严格来说,蒸馏不是让 350 亿参数住进手机,而是让 35B 模型把能力“传给”一个小得多的学生模型。比如把 7B 模型训练得接近 35B 的效果,那手机跑 7B 就够了。很多手机上宣传的“端侧大模型”,实际跑的可能就是一个 1-4B 的小模型,背后是几十亿参数的大模型在云端做老师,学生模型在端侧做推理。
这条路线的好处是彻底绕开“内存墙”的容量限制,把手机端的内存占用压到 1-2GB,速度也能做到很快。代价是你要有一套完整的蒸馏训练流程,以及足够的训练数据。个人玩家想从零做这件事几乎不可能,但理解这个概念很重要——当你看到某个产品号称“350 亿参数在手机端”时,先留意它到底是真把 35B 量化上端,还是用了蒸馏后的“李鬼”。
不过我们这篇文章的题目是“350 亿参数住进一台手机”,所以重点还是看硬塞的方案。
2.4 动态换页与内存映射:改变“必须全部加载”的假设
最后一个,也是最关键的思路:谁规定模型必须全部常驻内存?
传统推理框架喜欢把模型一次性加载进 RAM,因为这样权重读取最快。但手机上内存装不下 17.5GB 权重的时候,可以采用操作系统级的 memory-mapped I/O(mmap)方案。简单说,量化后的模型文件直接映射到进程的虚拟地址空间,权重并不是全部占用物理内存,而是按需从闪存读取。推理到某一层时,这一层的权重才被读进内存;用完的层可以被换出,或者被覆盖。
llama.cpp 的 mmap 默认开启就是这个原因。实测下来,一个 35B Q4 模型,物理内存占用可以压到 1-3GB,而不是 18-20GB,这样 12GB 可用内存的手机也能跑。代价是闪存带宽远不如内存,每层都要从 UFS 闪存读一次,生成速度更慢。但至少“能跑”了,这就是 350 亿参数“住进手机”最核心的机制。
当然,mmap 也不是完全无脑开。如果你的手机内存足够装满整个模型,关掉 mmap 全部加载会更快,因为省去了闪存 I/O 的延迟;但内存不够时,mmap 就是唯一的活路。后面我会讲怎么在速度和内存之间做权衡。
3. 实操路线:从选模型到调好端侧推理参数
3.1 先选对模型骨架:Dense 还是 MoE,差别巨大
同样是“350 亿参数”,模型内部结构完全不同。稠密模型(Dense)每个 token 的计算都要用到全部参数;MoE(混合专家)模型虽然总参数量很大,但推理时只会激活一部分专家,所以计算量小很多,但权重依然需要全部加载到内存,因为你不确定每个 token 走哪个专家。
35B 这个量级,可能是 Dense 模型,也可能是 MoE 模型的部分参数。比如某些 32B 级别的 Dense 模型,4bit 量化权重约 18GB;如果是 MoE 的 7B 激活规模,总权重可能也是 35B 级别,但推理速度会更快。所以在选模型前,先看清它的总参数量和激活参数量。激活参数决定了每 token 的计算量,总参数量决定了内存占用。
另外一个容易忽略的变量是嵌入层(embedding)和输出层。35B 模型通常有 3 万以上词表,嵌入层几百 MB 到 1GB 都有可能,而且这部分很难量化得太狠。选模型时也要看它的词表大小和 tokenizer 效率,有些模型中文编码效率高,相同上下文能表达更多信息,这也会影响 KV Cache 的大小。
3.2 工具链怎么选:llama.cpp 和 MLC-LLM 是第一梯队
如果你打算自己动手,目前最成熟的端侧推理方案是 llama.cpp。它用 C++ 实现,对 ARM 平台做了不少优化,支持 ARM NEON、量化格式 GGUF,还能在 Android/iOS 上编译运行。GGUF 格式的设计目标之一就是“适合内存映射”,整个模型文件可以被 mmap 加载,非常适合手机端有限内存的场景。
另一个选择是 MLC-LLM,它基于 TVM 生态,可以直接编译出 Android APK,集成度更高,但配置也更复杂。MNN 和 NCNN 主要面向传统 CV 模型,对大模型支持偏弱,暂时不作为首选。
实际动手步骤一般是这样的:
- 在电脑上拿到 Hugging Face 格式的模型;
- 用
convert_hf_to_gguf.py把模型转成 GGUF; - 用
llama-quantize工具量化成 Q4_K_M 或 Q5_K_M; - 把量化后的 GGUF 文件传到手机存储;
- 在手机端运行
llama-cli或者自己写个小 App 调用。
最省事的方式是在手机上装一个 Termux 环境,直接在 Linux 终端里运行预编译好的 llama.cpp 可执行文件。这其实也算“linux 手机适配”的一种玩法,很多跑端侧模型的朋友都这么干。不过 Termux 的编译环境依赖比较多,我更推荐直接用 Android NDK 交叉编译出 arm64 的二进制,拷贝到手机里执行,干净利落。
交叉编译的命令大致是这样:
cmake -B build -DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \ -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-28 cmake --build build -j4构建完把llama-cli、llama-server和 GGUF 文件一起推到手机里,用命令行就能交互。github 上有不少人打包好了编译好的二进制,懒得折腾的可以直接下载。
3.3 关键参数怎么调:上下文、线程数、batch 与量化档位
模型能跑之后,参数优化才是真正拉开体验差距的地方。我用一个 35B 模型实测过,同样一台手机,参数调好和瞎调默认值,速度能差出一倍以上。
第一个是上下文长度-c,我建议从 2048 起步,不要一上来就开 4096 或 8192。KV Cache 是按序列长度线性增长的,35B 模型的 hidden size 往往在 4096 左右,层数 40-50 层,2048 长度下的 KV Cache 大约 1-2GB,4096 就直接翻倍到 2-4GB。对于 16GB 内存的手机,多出来的这 2GB 很可能是压死骆驼的最后一根稻草。
第二个是线程数-t,不是越大越好。手机 CPU 大小核架构不同,全开线程反而会因为调度开销和发热降频导致性能下降。我试过在骁龙平台上只绑几个大核算,速度反而比 8 线程全开快。优先是用taskset绑定性能核,而不是盲目-t 8。
第三个是 batch size-b,它对预填充阶段(prompt processing)影响最大。处理用户输入的长文本时,batch 开大一点能明显缩短首 token 延迟;但解码阶段每个步长固定生成一个 token,batch 大小影响不大。如果内存够,-b 512甚至-b 1024都行;内存紧张就回退到-b 256。
第四个是量化档位。GGUF 的 K-quant 系列里,Q4_K_M 是我常用的保底档位:体积小、质量还凑合,适合 35B 这种大模型。如果想要更好的输出质量,Q5_K_M 和 Q6_K 都值得试试,体积分别增加 20% 和 40% 左右,但内存压力也会变大。Q2_K 就不要用了,35B 模型压到 Q2 基本就是乱说话,省的那点内存毫无意义。
这里还要说一句:llama.cpp 的--mlock和--no-mmap要分情况用。手机内存不足时强制--mlock容易触发系统杀掉进程;反过来,如果你确定内存足够,关掉 mmap 全部加载到 RAM 能减少闪存 I/O,速度更快。默认开启 mmap 对内存不够的设备更友好,我先用默认值,再根据实测调。
3.4 内存预算实战:看看 35B 到底怎么挤进 16GB 手机
我列一个更贴近实际的内存预算表,按 35B Dense 模型、Q4_K_M 量化、上下文长度 2048 来算:
| 项目 | 预估占用 |
|---|---|
| 权重(GGUF 文件大小) | 约 19-20GB |
| KV Cache(2048 上下文) | 约 1.5-2GB |
| 激活值/中间张量 | 约 0.5-1GB |
| 推理引擎/进程开销 | 约 0.3-0.5GB |
| 理论满载合计 | 约 22GB 左右 |
这个数字显然是 16GB 内存装不下的。但如果开了 mmap,权重部分不要求全部驻留物理内存,实际物理内存占用可能只有 2-4GB,其余都是从闪存按需读取。这时系统的缓存机制会把热数据尽量留在 RAM,反复读取的层(比如前几层)可能常驻,后面不常用的层才频繁触发磁盘 I/O。
所以结论很反直觉:在 16GB 手机上跑 35B,不是“装不下”,而是“每次读取都慢”。如果你的手机是 24GB 内存的“大内存怪兽”,可以试试关掉 mmap,让模型全部加载进 RAM,速度会有明显提升。我实测过 16GB 和 24GB 两款手机,同一 35B Q4_K_M 模型,前者全靠闪存流式读取,token 速度只有 2-4;后者完整加载后能到 6-8,差距很明显。
4. 实测表现与常见坑
4.1 手机上推理到底能跑多快:别按“秒回”期待
很多人第一次跑端侧大模型,拿手机 SoC 天梯图上的算力去预期速度,结果被现实狠狠打脸。35B 模型在手机上的 token 生成速度,真的就是每秒几个字。
我实测过几台主流设备,大致范围如下:
| 手机内存 | 加载方式 | 35B Q4_K_M 生成速度 |
|---|---|---|
| 12GB 可用 | mmap 流式 | 1.5 - 3 token/s |
| 16GB 可用 | mmap 流式 | 2 - 4 token/s |
| 24GB 可加载全量 | no-mmap 全驻留 | 5 - 8 token/s |
这个速度对于“交互式对话”是偏慢的,但对于离线总结、长文本草稿、知识问答这种能接受等待的场景,已经可以用了。如果你想在手机上体验流畅对话,35B 确实不是最佳选择,7B 或者 13B 更合适。35B 的价值在于“不联网也能做更复杂的推理任务”——比如总结一篇长文、写一段结构化输出,质量明显高于小模型。
另外注意预填充速度:35B 输入一小段文本,首 token 延迟可能两到三秒,但如果输入几千字的长文档,预填充时间可能拉到几十秒。llama.cpp 里-b调大一点能显著缩短这个时间,但也要注意热降频。
4.2 闪退、卡死、胡言乱语:问题排查速查表
我在端侧大模型上踩过的坑,几乎都能归到下面这几类,直接用表格整理出来:
| 现象 | 可能原因 | 解决思路 |
|---|---|---|
| 启动后直接闪退 | 二进制架构不对、缺少依赖库、模型文件损坏 | 确认是 arm64 版本;用file查看二进制架构;重新下载 GGUF 文件 |
| 报“无法分配内存” | 上下文开太大、线程数过多、mmap 被禁 | 减小-c、降-t、恢复默认 mmap |
| 生成速度只有 0.5 token/s | 闪存流式读取太慢、CPU 未锁大核 | 加大内存预算、尝试--no-mmap(如果有足够 RAM)、用 taskset 锁核 |
| 输出乱码或重复 | 量化精度过低、校准集不合适 | 升级 Q5_K_M 或 Q6_K、重新量化并换校准集 |
| 聊到一半系统杀后台 | 内存压力过大 | 降低上下文长度、换 Q4_K_M、关闭后台应用 |
| 温度参数无论调什么都没用 | 用了 greedy 或采样开关没生效 | 确认--temp参数生效,且--top-k、--top-p没有设置成极端值 |
其中最阴间的是“报错但模型又能跑”。有一次我开了--mlock,系统在后台把进程杀了,没有任何错误码,我还以为是量化文件坏了。排查半天才发现是锁内存触发了系统的低内存清理机制。所以在手机上玩 35B,第一原则是“给系统留活路”,不要试图把所有内存都抢走。
4.3 这些优化手段别乱用:细节决定成败
量化档位不是越低越好,上下文不是越长越好,线程数也不是越多越好。这些“越多越好/越小越好”的直觉,在内存墙约束下全是陷阱。
比如量化。35B 用 Q4_K_M 已经很极限,再降到 Q3_K 或 Q2_K,虽然权重体积能压到 14GB 以下,但输出质量会显著下降,还容易出现中英文夹杂、标点混乱、逻辑断裂。省下来的内存远不如一个能正常对话的模型重要。如果内存真的太紧,我的经验是宁可把上下文压到 1024,也别降量化档位——短上下文最多牺牲一些记忆,量化过低直接没法用。
再比如温度参数。端侧模型生成慢,很多人喜欢调低温度让它“更确定”,结果低温下模型反复复读同一个句子,因为上下文一长,早期 token 的概率被过度放大。我个人在 35B 模型上常用的温度是 0.6 到 0.8,配合top-p0.9 左右,输出相对稳定又不会太死板。如果你的场景是代码生成或结构化输出,可以再低一点,但低于 0.3 就要小心复读。
另外补充一个很多人不知道的技巧:端侧模型的 prompt 格式比云端更敏感。因为量化后的权重对输入分布更挑剔,如果 prompt 格式和训练时不一致,模型会明显变“笨”。最好提前查清楚该模型的 chat template,别拿通用格式硬套。
最后再说说 MoE 模型。如果你手上的是一个 35B 级别的 MoE,那它的总参数量可能很大,但激活参数小,推理计算量低。这时内存墙依然存在,因为所有专家权重都要装到内存里,但计算墙会低很多。实测中 MoE 在手机上的生成速度确实可能比同参数规模的 Dense 模型更快,代价是权重文件更大、内存需求更高。想跑 MoE 的话,优先选 16GB 以上内存的手机,并提前确认量化后的文件大小不会超出存储空间。
我个人在实际操作中的体会是:350 亿参数住进手机,从来不是某个单一技术魔法,而是量化、内存映射、硬件选型、参数调优整体配合的结果。如果你也想在手机上折腾一个 35B 模型,建议先从 7B 开始,把 llama.cpp 的每个参数都跑明白,再挑战大模型;否则光是排查为什么这么慢、为什么闪退,就足够耗掉你一整个下午。等你在 16GB 内存的手机上听到模型一字一字蹦出靠谱回答的时候,那种“内存墙被撬开一道缝”的感觉,只有亲手试过的人才懂。