上周有个朋友问我:边缘设备能不能跑 30B? 我说:能,但你先接受一个事实——它比你的内存大。 然后我把 Qwen3-30B-A3B(权重文件 17.66 GB)跑在了一块 16 GB 内存的 RK3588 开发板上,并且让它的服务进程峰值内存停在了 1.11 GB。 这不是“理论上可以”,是板端实测,日志和数字我全放在下面,你可以照着复现。
一、故事的起点:一块塞不进去的模型
先说清楚矛盾有多大:
- 模型:Qwen3-30B-A3B,MoE 结构,q4 量化后权重文件 17.66 GB;
- 板子:Orange Pi 5 Plus(RK3588,8 核,eMMC),MemTotal 15,647,860 kB ≈ 15.6 GB。
17.66 > 15.6,这是小学算术题。全层常驻的做法下,它连“把权重放进去”这一步都很勉强——实测服务峰值 14.23 GB,几乎吃掉整块内存,KV 缓存和系统进程只能捡剩下的。所以问题不是“怎么优化”,而是:能不能不把权重全放进去?
二、三个数字,讲完整个故事
我不想堆术语,先把三个数字放这儿:
数字一:1.11 GB。开启逐层推理(VLLM_VQF_STREAM=1)后,同一个 30B 模型的服务峰值内存是 1.11 GB,对比全层档的 14.23 GB——省 12.8 倍。权重常驻部分从 11.73 GB 降到 0.51 GB。
数字二:620 秒 → 23 秒。省钱不等于好用。基线条件下,30B 处理一轮 300 token 的上下文要 620 秒(对,十分钟)。在组合①(L3 驱逐 × 前缀复用)之上再叠 MoE 组合⑤(VLLM_ACTQ=1 VLLM_MOE_BATCH=1)之后,第一轮降到 23 秒,tpot 从 2,240 ms 降到 504 ms。
数字三:4.7 秒 / 3.2 秒。这是第二轮、第三轮追问的 prefill 时间——对比基线同样是 722 秒 / 822 秒。也就是说,多轮对话里的追问,从“泡杯咖啡”变成了“眨个眼”。
三、怎么做到的(人话版)
1)权重不搬进内存,而是“挂在文件上”
我们的权重格式(自研 VQF v2)在转换阶段就把量化、布局、对齐全干完了。加载时只做一件事:mmap。进程刚起来时它占多少内存?988 kB。不到 1 MB。因为那会儿它只映射了地址,一页物理内存都还没读。
2)用完一层,就把那层的内存还给系统
逐层推理模式下的每一层:
第 N 层要算了 → 触碰它的文件页(内核从存储读进内存)→ 算完
→ MADV_DONTNEED 立刻丢弃这些页 → 下一层
只保留 keep=1 层常驻。板端启动日志就是这么写的:
[VQF-STREAM] enabled keep=1 nl=48 segs=11 per-layer=334.1MB data=16847.2MB resident~808.4MB rss=988kB
data=16847.2MB(模型总权重)与 resident~808.4MB(实际常驻)摆在一起,就是“解耦”两个字。
为什么丢弃是免费的? 因为这些页是“干净”的——只读映射,没被改写过,内核不需要回写磁盘,直接扔掉就行,下次从文件重新读。
3)KV 缓存也得会过日子
权重的问题解决了,KV 缓存还在涨。v1.0 的 KV v2 改成惰性分配:底座随用随长,而不是一上来按 max_seq 预分配一大块 F32。
4)MoE 模型还有额外红利
30B-A3B 是 MoE(激活约 3B)。组合⑤ 打开专家激活量化 + 专家批量后,同样的 prefill 计算量下速度直接跳一个档:第一轮 620 s → 23 s,追问轮 822 s → 3.2 s。
省内存和跑得快,这两件事没有互相排斥。
四、完整数据(可以直接拿去对线)
多轮追问实测(300 token 上文 + 32 token 生成,组合⑧为基线,组合①是 L3 驱逐 × 前缀复用):
| 2B | 逐层 | ⑧ 基线 | 6,057 ms | 6,706 ms | — |
| 2B | 逐层 | ① P3 | 1,189 ms | 1,130 ms | −80.4% / −83.1% |
| 8B | 逐层 | ⑧ 基线 | 19,579 ms | 21,545 ms | — |
| 8B | 逐层 | ① P3 | 2,048 ms | 2,203 ms | −89.5% / −89.8% |
| 30B-A3B | 全层 | ⑧ 基线 | 722,394 ms | 822,409 ms | — |
| 30B-A3B | 全层 | ① P3 | 28,633 ms | 32,140 ms | −96.0% / −96.1% |
| 30B-A3B | 全层 | ①+⑤ P3+MoE | 4,727 ms | 3,157 ms | −99.3% / −99.6% |
| 30B-A3B | 逐层 | ⑧ 基线 | 724,367 ms | 824,535 ms | — |
| 30B-A3B | 逐层 | ① P3 | 29,368 ms | 32,820 ms | −95.9% / −96.0% |
| 30B-A3B | 逐层 | ①+⑤ P3+MoE | 5,269 ms | 3,695 ms | −99.3% / −99.6% |
注意最后两行:逐层档 + 组合① + 组合⑤ 同时开,省 12.8 倍内存的同时,追问轮 prefill 只要 3.7 ~ 5.3 秒。这就是我说“省内存和快不矛盾”的依据。
内存对照(服务口径,含 KV):
| 2B(4.16 GB) | 2,968,920 kB | 854,476 kB | 3.5× |
| 8B(6.60 GB) | 4,523,240 kB | 914,448 kB | 4.9× |
| 30B-A3B(17.66 GB) | 14,228,868 kB | 1,112,500 kB | 12.8× |
五、自己跑一遍
git clone https://gitee.com/pei-xiaoguang/kestrel-llm.git && cd kestrel-llm
./build_rk3588.sh
转换一次(v1 只认自研 VQF v2 格式)
cd vqf_convert && ./build.sh
./vqf_conv –model <safetensors-dir> –convert-vqf <out.vqf> –wmode q4 && cd ..
省内存 + 多轮加速 一起上
VLLM_VQF_STREAM=1 VLLM_L3_PREFIX_REUSE=1 VLLM_ACTQ=1 VLLM_MOE_BATCH=1
./vllm_kestrel –serve –port 8080 –model <模型目录> –auto-load –wmode q4
–sparse-attn –sparse-k 32 –l3-evict –l3-ratio 0.75 –l3-min-seq 128
日志里你应该能看到逐层生效的那一行,以及多轮对话时 [KV-PREFIX] reuse … 的前缀复用记录。
六、丑话说在前面
- 30B 在这块板上的绝对速度依然很低。基线 0.45 tok/s,优化后好很多,但别指望它做实时对话机器人——它的正确姿势是“归档式推理”“批量摘要”“离线问答”这类能容忍秒级延迟的活。
- 逐层只支持明文权重。加密或签名的 VQF 会被显式拒绝(必须全层驻留)——安全和省内存目前二选一。
- 只适配 Qwen3 系(Qwen3-VL 2B/8B、Qwen3-MoE)。其他架构没验证过,别拿 Llama 来试然后骂人。
- 代价是真实存在的:逐层档热态 tpot 会涨 18% ~ 39%,模型越大、存储越慢,这笔“利息”越高。
七、最后一句
把 17.66 GB 塞进 15.6 GB,靠的不是压缩算法,是换了个前提:“权重必须同时待在内存里”——这个前提,在 mmap + 按层建页 + 用完就还的模型下,本来就不成立。
Kestrel(vllm_kestrel)已完全开放:https://gitee.com/pei-xiaoguang/kestrel-llm
双许可协议:学习与学术研究完全免费,商业与产品化部署请先取得商业授权。跑出不一样的数据,欢迎来 Issues 砸场子。
附:配图清单与提示词
| 封面 | 大模型方块 vs 小开发板的视觉冲突,“17.66 GB / 15.6 GB” 字样 | split composition: left a huge dense blue data cube labeled 17.66 GB, right a small green ARM board labeled 15.6 GB, dark studio background, cinematic, photorealistic, 16:9 |
| §二 数字一 | 14.23 GB → 1.11 GB 的横条对比 | 建议脚本绘制:两条横条 + 12.8× 标注 |
| §三 第 2 点 | 逐层“建页 → 算完 → DONTNEED”循环图 | circular technical diagram: map page, compute layer, release page with MADV_DONTNEED, isometric style, blue/orange, white background, 16:9 |
| §四 表 | 三模型 × 全层/逐层 峰值柱状图 | 建议脚本绘制:6 根柱,逐层用高亮色 |




