173MB 的 0.3B 模型怎么跑:MNN-Pocket 的三条部署路径
先给一个具体的数字对比。
一个 300M 参数的模型,BF16 权重约 600MB;同一个模型换成 INT4 量化,理论上压到 150MB 左右。而阿里 MNN 团队开源的 MNN-Pocket-0.3B,INT4(W4A16)QAT 构建的实际体积是 173 MB。
173MB 是什么概念:一个中型的手机 App 安装包、两张手机照片、一段 15 秒的 4K 视频。
这个体积意味着它可以被塞进 App 内置资源、烧进嵌入式设备的存储、或者在树莓派上常驻。代价当然有,但代价是可以用数字衡量的——后文会给出。
本文按"怎么跑起来"的顺序走:先看规格,再看它凭什么这么小,然后三条部署路径各自给出完整命令,最后说清它的能力边界。
一、规格:361.55M 参数,28 层
**【官方】**模型卡给出的架构参数:
| 项目 | 数值 |
|---|---|
| 参数量 | 361.55M(标注约 300M) |
| 层数 | 28 |
| 隐藏维度 | 896 |
| 注意力头 | 14 heads,GQA 2 KV heads,head_dim 64 |
| FFN 中间维度 | 2560(SiLU) |
| 上下文 | 40,960 positions(部署支持 32K) |
| 词表 | 65,536 |
| Embedding | 输入输出 tied |
| 原生精度 | bfloat16 |
| 端侧精度 | INT4 W4A16(MNN 构建) |
| 语言 | 中文、英文 |
| 许可证 | Apache-2.0 |
| 底座 | Qwen3 系 |
这组参数里有三个值得注意的点。
**第一,GQA 只留 2 个 KV heads。**14 个注意力头对 2 个 KV 头,压缩比 7:1。这是小模型标配——KV cache 直接关系到长上下文的显存占用,32K 上下文下这一项能省下大量内存。
**第二,28 层 896 隐藏维度,是个"窄而深"的配置。**同参数量下,宽而浅的模型推理更快(并行度高),窄而深的模型表达力更强。选 28 层说明目标不是极限速度,而是在 300M 这个尺度上尽量保住能力。
**第三,40,960 个位置但部署只支持 32K。**差出来的 8K 是留给训练和特殊场景的余量,实际部署按 32K 用。
**【推算】**做个粗略参数核对:每层注意力约 1.83M(q/o 各 896×896,k/v 各 896×128),FFN 约 6.88M(896×2560 三矩阵),合计每层约 8.7M;28 层约 244M;词表嵌入 65,536 × 896 ≈ 58.7M。总计约 303M,与"约 300M"的标注吻合。多出的部分大概是额外的 norm 层、bias 与位置编码。
二、173MB 是怎么来的:W4A16 + QAT + block 64
三个技术名词,拆开说。
W4A16:只压权重,不压激活
W4A16 的意思是权重 4-bit,激活 16-bit。
为什么不一起压到 4-bit?因为激活值的动态范围随输入变化,压到 4-bit 会显著掉点。权重是静态的,可以做精细的量化校准,压得更狠而损失更小。这是端侧量化的通行做法。
block 64:分组量化
4-bit 不是简单地把每个数截断成 16 个档位。实际做法是把权重按每 64 个一组,每组独立算一个缩放因子。
分组越小,精度越高,但存储缩放因子的开销越大。block 64 是个常用平衡点——比 per-tensor 量化准得多,比 block 32 省一些元数据。
QAT:把量化放进训练流程
这是最关键的一项。
传统的做法是 PTQ(训练后量化):模型训好,再压成 4-bit,精度掉了就掉了。QAT(量化感知训练)是在训练时就让模型"知道"自己最终会被压成 4-bit,让权重分布主动适应这个约束。
**【交叉】**据团队公开说明,他们的做法是通过 Self-OPD 与量化感知训练,让 W4A16 学生在自身生成的上下文上对齐冻结的 BF16 模型。
效果是可以量化的:
| 版本 | 11 项中英评测平均分 |
|---|---|
| BF16 | 50.00 |
| INT4(W4A16 QAT) | 49.59 |
**差 0.41 分。**这就是 173MB 的全部代价。

对比一下不用 QAT 的情况:常规 PTQ 到 4-bit,在这种 300M 小模型上掉 3 到 8 分是常见的。0.41 分说明 QAT 确实起了作用,而不是"反正是小模型无所谓"。
【官方】顺带一提,模型卡说这个平均分"在约 300M 模型中最好,超过几个 ~500M 基线"。这个说法需要注意口径:11 项中英混合评测、平均分口径,具体哪 11 项、权重如何分配,模型卡未逐项列出。【未披露】
三、六种权重,别下错
MNN-Pocket-0.3B 不是一个模型,是一个家族。仓库里放了六个版本,用途完全不同:
| 版本 | 用途 | 该不该下 |
|---|---|---|
-Base |
预训练底座,供自己微调 | 要做继续预训练才下 |
-SFT |
监督微调检查点,研究中间态 | 做消融实验才下 |
MNN-Pocket-0.3B |
通用指令微调版(原始权重) | 要用 Transformers 跑就下这个 |
-GGUF |
llama.cpp 格式:F16 / Q8_0 来自 BF16,Q4_1 来自 QAT | 要用 llama.cpp 就下这个 |
-MNN |
INT4 W4A16 QAT 的 MNN 构建 | 要在端侧跑就下这个 |
-Dec |
非自回归决策模型,一次前向答一题 | 做分类/候选打分才下 |
**最容易下错的是 GGUF 版本。**注意它内部的 F16 / Q8_0 来自 BF16 检查点,只有 Q4_1 来自 QAT 检查点。想让 llama.cpp 也吃到 QAT 的好处,必须选 Q4_1——选 Q8_0 拿到的是没经过量化感知训练的版本,跟 INT4 那条路的收益不是一回事。
第二个容易搞混的是"哪个能直接跑"。-MNN 是编译好的端侧格式,不能喂给 Transformers;原始 MNN-Pocket-0.3B 是 safetensors,不能直接喂给 MNN。两边要用不同文件。
四、路径 A:MNN 原生(端侧首选)
这是体积和速度最优的一条,也是唯一能跑到 173MB 的路。
第一步:拿权重
已发布的 INT4 版本叫 MNN-Pocket-0.3B-MNN,直接下载即可。想用其他精度(比如 8-bit 或 block 32),从原始 safetensors 自己导出:
# 拉 MNN 源码
git clone https://github.com/alibaba/MNN.git
# 导出为 MNN 格式(INT4, block 64)
cd MNN/transformers/llm/export
python llmexport.py \
--path /path/to/MNN-Pocket-0.3B \
--export mnn \
--quant_bit 4 \
--quant_block 64
--quant_bit 和 --quant_block 是两个关键旋钮。改成 --quant_bit 8 体积翻倍但更准;--quant_block 32 更精细,代价是每组多一个缩放因子。
第二步:编译运行时
cd MNN
mkdir build && cd build
cmake .. \
-DMNN_LOW_MEMORY=true \
-DMNN_BUILD_LLM=true \
-DMNN_SUPPORT_TRANSFORMER_FUSE=true
make -j
三个编译开关都要开:
| 开关 | 作用 | 不开的后果 |
|---|---|---|
MNN_LOW_MEMORY |
低内存模式,权重按需加载 | 峰值内存明显上升 |
MNN_BUILD_LLM |
编译 LLM 推理相关代码 | 没有 llm_demo 可执行文件 |
MNN_SUPPORT_TRANSFORMER_FUSE |
启用 Transformer 算子融合 | 推理慢一截,融合是把 RMSNorm、MatMul 等合并执行的前提 |
第三步:跑
./llm_demo /path/to/MNN-Pocket-0.3B-MNN/config.json prompt.txt
prompt.txt 是你自己准备的输入文件。注意第一个参数是 config.json 的路径,不是目录——传目录会报错。
这条路的实测数据
**【官方】**在 iPad Pro M5(Metal 后端)上,decode 达到 466 tokens/s,超过团队设定的 200 tok/s 实时目标。
**【交叉】**另有报道提到 iPhone 18 Pro 上离线约 350 tokens/s。
这两个数字要读懂两点:
- **只是 decode 速度,不是端到端首字延迟。**用户感知的"点了之后多久出字"取决于 prefill,长 prompt 下 prefill 才是瓶颈。
- **是旗舰设备。**M5 的 GPU 和神经引擎不是中端机能比的。170MB 的模型在中端安卓机上能跑,但别指望同样的吞吐。
五、路径 B:Transformers(调试验证用)
如果只是想确认模型输出对不对、或者准备微调,用 Transformers 最省事。
from modelscope import snapshot_download
from modelscope import AutoModelForCausalLM, AutoTokenizer
model_id = snapshot_download("MNN/MNN-Pocket-0.3B")
model = AutoModelForCausalLM.from_pretrained(
model_id, device_map="auto", dtype="bfloat16"
)
tokenizer = AutoTokenizer.from_pretrained(model_id)
messages = [{"role": "user", "content": "What is MNN?"}]
inputs = tokenizer.apply_chat_template(
messages,
add_generation_prompt=True,
return_tensors="pt",
tokenize=True,
return_dict=True,
).to(model.device)
outputs = model.generate(
**inputs, max_new_tokens=512,
do_sample=True, temperature=0.8, top_p=0.8, top_k=20,
)
print(tokenizer.decode(
outputs[0][inputs["input_ids"].shape:], skip_special_tokens=True
))
三个细节:
- **
apply_chat_template不能省。**这个模型用 ChatML 格式,带im_start/im_end控制符。自己拼字符串十有八九格式不对,表现是"模型能回答但答得很差"。仓库里带了chat_template.jinja,用apply_chat_template会自动套用。 dtype="bfloat16",不是 float16。这是模型的原生精度。- 采样参数用
temperature=0.8, top_p=0.8, top_k=20,与官方示例一致。做确定性任务(数据抽取)时改成do_sample=False。
这条路拿到的是 BF16 权重,约 600MB 量级,不适合最终部署,只适合验证和微调。
六、路径 C:llama.cpp(桌面与通用)
如果你的目标是桌面、笔记本、或者已经有 llama.cpp 的工具链,走 GGUF。
从 MNN-Pocket-0.3B-GGUF 下载后,用 llama-server 起一个 OpenAI 兼容服务:
./llama-server \
-m MNN-Pocket-0.3B-Q4_1.gguf \
-ngl 99 \
--port 8080 \
--ctx-size 8192
两个参数说明:
-ngl 99:把所有层都放 GPU。这个模型只有 0.3B,24GB 显卡上完全放得下,甚至核显也能全放。数值给 99 是"全部卸载"的惯用写法。--ctx-size:显存够就往上给,但注意它最大支持 32K,超过没有意义。
起来之后用标准 OpenAI 客户端接:
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8080/v1", api_key="not-needed")
api_key 填任意非空字符串即可。
**这条路的关键是选 Q4_1。**前文说过,GGUF 仓库里只有 Q4_1 来自 QAT 检查点,Q8_0 和 F16 来自 BF16。想要"小体积 + 不掉点"的组合,选 Q4_1。
七、三条路径怎么选
| MNN 原生 | Transformers | llama.cpp | |
|---|---|---|---|
| 体积 | 173 MB(INT4) | 约 600MB(BF16) | Q4_1 约 200MB 级 |
| 适合平台 | 手机、嵌入式、端侧 | 有 Python 的服务器 | 桌面、笔记本、通用 |
| 峰值内存 | 最低(低内存模式) | 最高 | 中 |
| 跑一次要几步 | 编译 + 运行 | 装依赖 + 跑脚本 | 下二进制 + 起服务 |
| 改模型方便吗 | 难(要重新导出) | 最容易 | 中 |
| 适合做什么 | 最终交付 | 验证、微调、对比 | 本地服务、已有工具链 |
一句话选择:
- 要交付到设备上 → MNN
- 要验证效果或微调 → Transformers
- 要在本地搭个服务试试 → llama.cpp

八、Dec 版本:一次前向回答一个问题
家族里有一个特殊成员:MNN-Pocket-0.3B-Dec。
【官方】它的结构是 LoRA adapter + pointer head,工作方式是非自回归——不是逐 token 生成,而是在一次前向计算中给所有候选打分,选出答案。
这个设计针对的是"有明确候选项"的任务:分类、多选、候选排序。典型的如电商评论观点挖掘——给定一句评论,输出属性词、观点词、类别、情感极性四个字段。
为什么值得单独提?因为逐 token 生成在端侧是昂贵操作。生成 10 个 token 就要 10 次前向;而决策类任务本质是"从候选里挑一个",用 pointer head 一次算完,延迟能压到接近单次前向的水平。
**【官方】**团队还提到一个 DFlash2 草稿模型:一次提 8 个候选 Token,由目标模型统一验证,减少逐 token 生成的串行开销。这属于投机解码的思路,用一个小模型起草、大模型验收。
**【未披露】**Dec 版本的具体评测数据、pointer head 的候选集如何构造、DFlash2 的接受率。这三项对判断实际收益很关键,模型卡没给。
九、能力边界:官方自己划的线
模型卡写得很直白:
“Like other sub-billion-parameter models, it is not recommended for reasoning-heavy workloads such as advanced mathematics or long-horizon code generation.”
翻译:跟其他十亿参数以下的模型一样,不建议用于推理密集型任务,比如高等数学和长程代码生成。
官方给的适用场景是三个:
| 场景 | 说明 |
|---|---|
| 端侧助手 | 设备上的轻量对话、指令响应 |
| 数据抽取 | 从文本里抽结构化字段 |
| 轻量 agentic pipeline | 作为流程中的一环做判断/分类 |
这个边界划得实在。300M 参数配上 32K 上下文,能力天花板就在那儿——它不可能替代云端大模型做复杂推理。但它能做的事有一个云端大模型做不到的优势:离线、零延迟、零调用成本、数据不出设备。
如果你的场景是"每台设备每天要处理几千次简单的结构化抽取",那这个模型的定位就非常清楚。
十、Chat Template 的坑
这一节单独拎出来,因为它是最容易"跑起来了但效果莫名其妙"的原因。
MNN-Pocket 用 ChatML 格式,控制符是 im_start 和 im_end。正确的输入长这样:
[IM_START]system You are a helpful assistant.[IM_END]
[IM_START]user Hello![IM_END]
[IM_START]assistant
三个常见错误:
| 错误 | 表现 |
|---|---|
自己拼字符串,漏了 im_start |
模型能答,但质量明显下降 |
用了别的模型的模板(如 Llama 的 [INST]) |
输出格式错乱或直接跑偏 |
忘了 add_generation_prompt=True |
模型不知道轮到自己说话,可能继续补全用户的话 |
**统一用 tokenizer.apply_chat_template()。**仓库里带了 chat_template.jinja,这个函数会自动读它。手拼模板在调试阶段省下的那点时间,会在排查"为什么效果不对"时全部还回去。
十一、排查清单
| 现象 | 最可能的原因 | 怎么查 |
|---|---|---|
llm_demo 找不到 |
没开 -DMNN_BUILD_LLM=true |
重新 cmake 并确认开关 |
| 传目录报错 | 第一个参数要 config.json 路径 |
补全文件名 |
| 峰值内存高 | 没开 -DMNN_LOW_MEMORY=true |
重新编译 |
| 推理慢 | 没开 -DMNN_SUPPORT_TRANSFORMER_FUSE=true |
重新编译,对比耗时 |
| Transformers 报模型类型不支持 | 用了 -MNN 版本 |
换成原始 safetensors |
| 回答质量差 | Chat template 没套对 | 改用 apply_chat_template |
| GGUF 效果不如预期 | 选了 Q8_0 / F16(BF16 来源) | 换 Q4_1(QAT 来源) |
| 长文本截断 | 上下文超过 32K | 部署上限是 32K,不是 40,960 |
| 中端机吞吐远低于 466 tok/s | 那是 iPad Pro M5 的数据 | 按自己设备实测 |
十二、这套东西真正的价值
300M 的模型,能力上限摆在那儿,不会有人拿它去写代码或做数学证明。
它真正改变的是一件工程事实:过去"端侧跑模型"意味着要对模型做大幅妥协——砍功能、降精度、接受明显掉点。W4A16 QAT 把这件事的代价压缩到 0.41 分。
这意味着选型逻辑变了。以前是先问"端侧能跑多大的模型",再决定砍哪些功能;现在是先问"这个任务需要多少能力",然后发现 173MB 就够。
对做端侧应用的团队,这个模型的参考价值不止于它本身,还在于它演示了一条完整路径:多教师蒸馏压规模 → QAT 压精度损失 → 专用推理栈压内存 → 按设备形态出多个格式。这条路子可以套到任何尺寸的模型上。
- 点赞
- 收藏
- 关注作者
评论(0)