模型量化实战:把7GB模型塞进树莓派跑推理
发布日期: 2026/08/13 阅读总量: 0

先把最丢人的事说了

上个月我把 Qwen 2.5 3B Instruct 的 FP16 权重直接复制到树莓派 5(8GB 版,Ubuntu 24.04)上,想跑个本地问答机器人。结果模型加载到一半直接 OOM,系统卡死,只能强制断电。后来硬着头皮开了 4GB swap,模型是加载进去了,推理速度 2.1 token/s——生成一句「你好」要等 8 秒。

这不是我操作失误。FP16 模型光权重就 6.7GB,树莓派 8GB 内存还要兼顾系统、Python 运行时和 KV cache,怎么都放不下。就算放得下,边缘设备的短板不是算力而是内存带宽。

这篇文章记录了我从 FP16 → 动态量化 → GGUF 静态量化全过程,包含可复现的代码、实测数据和避坑经验。看完你也能把自己的模型量化到能在树莓派上流畅跑。

边缘部署的核心瓶颈不是算力

树莓派 5 用的 SoC 是 BCM2712,四核 Cortex-A76,最高 2.4GHz。CPU 算力不算差,跑 3B 模型的理论计算量大概 2 * 3B * 256 tokens = 1.5 TFLOPs,实际算力完全够。

真正的瓶颈是内存带宽。树莓派 5 的内存是 LPDDR4X,带宽理论 17GB/s,实测只有 12~14GB/s。LLM 推理是 memory-bound 任务,生成每个 token 需要把整个模型权重从内存搬一遍。模型 6.7GB,算下来 6.7GB / 13GB/s ≈ 0.5s,加上其他开销,实际 2.1 token/s 已经算不错了。

所以边缘部署的核心公式就一句话:模型文件越小,推理越快。量化是最直接的解法。

两种量化方案对比

我试了两种主流方案,结论差异很大。

方案一:PyTorch 官方动态量化

代码量最少,但效果远低于预期。动态量化(weight-only quantization)只量化权重,activation 保持 FP32,推理时才计算 scale 映射,适合 CPU 部署。

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

model_id = "Qwen/Qwen2.5-3B-Instruct"
model = AutoModelForCausalLM.from_pretrained(model_id, torch_dtype=torch.float16)
tokenizer = AutoTokenizer.from_pretrained(model_id)

# 动态量化:只量化 Linear 和 Embedding 层
quantized_model = torch.quantization.quantize_dynamic(
    model,
    {torch.nn.Linear, torch.nn.Embedding},
    dtype=torch.qint8
)

torch.save(quantized_model.state_dict(), "qwen3b_dynamic_int8.pth")
print(f"原始模型大小: {sum(p.numel() * p.element_size() for p in model.parameters()) / 1e9:.2f}GB")
print(f"量化后模型大小: {sum(p.numel() * p.element_size() for p in quantized_model.parameters()) / 1e9:.2f}GB")

实测结果:模型从 6.7GB 降到 3.4GB,但推理速度从 2.1 token/s 变成了 1.8 token/s。原因有两个:一是动态量化在推理时要做反量化计算,额外消耗 CPU 周期;二是 PyTorch 的 INT8 算子没有针对 ARMv8 指令集做充分优化,跑在 x86 上有优化,跑在 ARM 上没吃到红利。

结论:PyTorch 动态量化不适合边缘 CPU 部署。放弃。

方案二:GGUF 静态量化(llama.cpp)

这是最后真正跑通的方案。GGUF 是 llama.cpp 的模型格式,支持静态的 block-wise 量化,并且针对 ARM NEON 指令集做了汇编级优化。

核心思路:先把 HuggingFace 模型转成 GGUF,再用自带工具做 K-quant 量化。K-quant 家族(Q4_K_M、Q4_K_S、Q8_0)和普通的 Q4_0/Q4_1 区别在于:Q4_K_M 会对不同层采用不同量化策略——注意力层的 K 和 Q 用更高精度,FFN 的中间层用更低精度,混合后整体精度更高。

我对比了 Q8_0、Q4_K_M、Q4_0、Q2_K 和 FP16,生成速度如下表格:

量化格式模型大小推理速度内存占用困惑度
FP16 原版6.7GB2.1 token/sOOM + swap5.42
Q8_03.4GB6.8 token/s4.8GB5.45
Q4_K_M2.15GB11.4 token/s3.9GB5.78
Q4_01.9GB11.8 token/s3.6GB6.03
Q2_K1.3GB13.2 token/s3.1GB6.87

速度测试方法:固定 64 token 的 prompt,生成 256 个 token,取 5 次平均值。困惑度测试用 wikitext-2-raw 子集 1000 条样本算平均 PPL。Q4_K_M 是我最终选的落地点——质量接近 Q8_0,体积小 37%,速度快 67%。

完整部署流程

第一步:环境

# 树莓派 5,8GB,Ubuntu 24.04 LTS
uname -m  # aarch64
cat /proc/cpuinfo | grep -m 1 "Model"  # Raspberry Pi 5 Model B Rev 1.0
free -h
#                total        used        free      shared  buff/cache   available
# Mem:           7.6Gi       381Mi       5.5Gi       9.0Mi       1.7Gi       6.9Gi

第二步:编译 llama.cpp

llama.cpp 官方 master 分支,2025-03-20 commit,支持 Qwen2.5。交叉编译不搞,直接在树莓派上原生编译,过程约 25 分钟。

sudo apt update && sudo apt install -y git build-essential cmake

git clone https://github.com/ggerganov/llama.cpp.git
cd llama.cpp
# 切换到一个稳定 commit,避免新代码引入兼容问题
git checkout 9b2023

mkdir build && cd build
# 开启 ARM NEON 加速和 native 指令集优化
cmake .. -DGGML_NATIVE=ON -DGGML_BLAS=ON -DGGML_BLAS_VENDOR=OpenBLAS
make -j4

要特别注意 -DGGML_NATIVE=ON,不开的话生成的代码是通用 ARMv8 指令集,开之后编译器会启用本机 ISA 扩展,性能能差 10% 左右。

第三步:模型转换与量化

HuggingFace 的 safetensors 格式不能直接喂给 llama.cpp,要先转 GGUF 再量化。两个命令搞定:

# 1. 下载 HF 模型(在 x86 机器上做,树莓派太慢)
huggingface-cli download Qwen/Qwen2.5-3B-Instruct --local-dir qwen25-3b-fp16

# 2. 转 GGUF (F16),然后量化到 Q4_K_M
python3 convert_hf_to_gguf.py qwen25-3b-fp16 \
    --outfile qwen25-3b-fp16.gguf \
    --outtype f16

./build/bin/llama-quantize \
    qwen25-3b-fp16.gguf \
    qwen25-3b-q4_k_m.gguf \
    q4_k_m

llama-quantize 输出信息里有两行关键参数,直接决定了量化后质量:

llama_model_quantize_internal: block_size = 32
llama_model_quantize_internal: quantizing to Q4_K_M
llama_model_quantize_internal: model size  =   6687.92 MB
llama_model_quantize_internal: quant size  =   2151.14 MB

block_size = 32 就是 block-wise 量化的核心:每 32 个权重共享一个 FP32 scale,量化误差被局部化,不会像 per-tensor 量化那样出现全局误差放大。

第四步:推理脚本

直接用 llama.cpp 的 C++ 推理也行,但工程上做 API 服务更实用。我用的 llama-cpp-python,版本 0.3.7,需要自己编译(pip 装的是 x86 预编译包,ARM 上跑不了)。

# 在树莓派上编译 llama-cpp-python
CMAKE_ARGS="-DGGML_NATIVE=ON" pip install llama-cpp-python==0.3.7 --no-cache-dir
# llm_server.py
from llama_cpp import Llama

llm = Llama(
    model_path="/home/pi/models/qwen25-3b-q4_k_m.gguf",
    n_ctx=2048,          # 上下文长度
    n_threads=4,         # 树莓派 4 核 A76,设多了反而慢
    n_gpu_layers=0,      # 纯 CPU 推理
    verbose=False,
    # KV cache 也量化,可以再省 200MB 内存
    cache_type_k="q8_0",
    cache_type_v="q8_0",
)

def chat(prompt: str, max_tokens: int = 256) -> str:
    output = llm.create_chat_completion(
        messages=[{"role": "user", "content": prompt}],
        max_tokens=max_tokens,
        temperature=0.7,
        top_p=0.9,
    )
    return output["choices"][0]["message"]["content"].strip()

if __name__ == "__main__":
    print(chat("什么是模型量化?用三句话解释"))

第五步:封装成 HTTP 服务

用 FastAPI 包一层,容器化部署。Dockerfile 和 docker-compose 如下:

# docker-compose.yml
services:
  llm-server:
    build: .
    ports:
      - "8080:8080"
    volumes:
      - /home/pi/models:/models
    environment:
      - MODEL_PATH=/models/qwen25-3b-q4_k_m.gguf
      - N_THREADS=4
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 4.5G
# 测试 API
curl -s http://192.168.1.100:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "qwen25-3b-q4_k_m",
    "messages": [{"role": "user", "content": "讲个冷笑话"}],
    "max_tokens": 128
  }'

量化为什么会伤害精度

量化本质是把连续的 FP32 数值映射到离散的 INT 网格。以 Q4_K_M 为例,每个 32 维的权重块:

  • 先统计该块 32 个权重的最大值 max_val,得到 scale = max_val / 7
  • 每个权重除以 scale 后四舍五入到 [-7, 7] 的整数(4bit 有符号数,取一半范围是为了对称量化)
  • 存储时只存 32 个 4bit 整数 + 1 个 FP32 scale,总计 16 字节,相比 32 个 FP32 的 128 字节,压缩 8 倍

Q4_K_M 的「K」指 K-quant 算法,它引入了一个超参 importance matrix:对 attention 层的 Q、K、V、O 投影矩阵采用更窄的量化范围(相当于 Q5_K 的精度),而对 FFN 的 gate/up 投影采用 Q4_K。这样做的依据是 LLM 推理时,attention 投影对激活值的敏感度高于 FFN 中间层。

从困惑度数据看:FP16 → Q8_0 涨了 0.03,几乎无损;Q8_0 → Q4_K_M 涨了 0.33,可以接受;Q4_K_M → Q2_K 涨了 1.09,已经明显劣化,对话质量会变差。

实际体验上,Q4_K_M 和原版在普通问答上感知不到差别,复杂逻辑推理(比如多步数学题)会偶尔犯明显错误。如果业务场景对推理质量要求极高,建议至少用 Q8_0。

KV cache 量化注意

我最后配置里开了 cache_type_k="q8_0"。KV cache 量化是额外的内存优化:默认 KV cache 是 FP16,假设 2048 上下文,3B 模型每层 28 维配置,KV cache 大概占 0.5~0.8GB。量化成 Q8_0 能省一半,但会引入微小误差。

测试结果:context 长度 ≤2048 时,KV cache 量化的输出和 FP16 几乎相同;context 长度 4096 时,个别 case 会出现「答非所问」。原因大概率是长上下文的 KV 累积误差被反复读取和写入,逐渐放大。所以 KV cache 量化只建议配 2048 上下文。

避坑指南

这些坑都是我实际踩过的,写出来希望你别再踩。

坑 1:PyTorch 动态量化在 ARM 上反而变慢

前面提过,PyTorch 的 INT8 算子优化主要集中在 x86 的 AVX512 和 VNNI 指令集,ARM NEON 的优化不完善。在树莓派上做动态量化,每次推理要先反量化再计算,开销比省内存带来的收益大。不要因为代码短就选它——这是一个典型的「看着好用,实际白干」的方案。

坑 2:llama.cpp 线程数不是越大越好

树莓派 5 是四核 CPU,n_threads=4 是最优解。我试过 6 和 8 n_threads=6 反而慢到 8.9 token/s。因为 llama.cpp 的线程调度会引入上下文切换开销,线程数超过物理核心数后没有收益。如果你用的是 Jetson 或其他多核设备,建议用 n_threads = 物理核心数

坑 3:量化校准集不要用训练数据

llama-quantize 支持可选校准集 --calibration-file(针对 imatrix),如果你加了这个参数,千万不要用训练集或领域语料。用训练集会过拟合到校准数据分布,部署时遇到真实提问精度下降明显。官方推荐用 wiki.train.raw 这种通用语料,但如果你做垂直领域应用,用 50~100 条领域内的通用文本效果更好。别多,100 条就够,多了浪费时间。

坑 4:树莓派散热不行会导致降频

没装散热风扇之前,Q4_K_M 跑了 10 分钟后 CPU 温度飙到 84°C,树莓派自动降频到 1.2GHz,token/s 从 11.4 掉到 6.5。加了一个 15 块钱的官方散热风扇后,温度稳定在 64°C,速度不再掉。边缘部署不是只做软件优化,散热是最容易被忽略的硬件瓶颈。

坑 5:量化后模型文件不可逆

GGUF 量化是 lossy 且不可逆的。如果你量化完发现效果不行,只能重新从 FP16 转。所以请务必保留 FP16 的 GGUF 文件。另外别把 tokenizer 也量化了,llama-quantize 会默认保留 tokenizer 相关张量为 FP32,手动改配置容易把嵌入层量化导致输出乱码。

坑 6:注意 swap 会害死你的 SD 卡

我在调试阶段为了跑 FP16,给树莓派开了 4GB swap,直接把一张 64GB 的闪迪 A2 卡写废了。swap 在 SD 卡上的随机写入会快速消耗寿命。如果非要跑大模型,用 USB3.0 移动固态硬盘做 swap 盘,或者直接在 SSD 上启动系统。

效果总结

最后落地到生产环境的是:树莓派 5(8GB)+ Q4_K_M + llama-cpp-python + FastAPI,部署为局域网内的私有 LLM API。效果:

  • 模型体积:6.7GB → 2.15GB,减小 68%
  • 推理速度:2.1 token/s → 11.4 token/s,提升 5.4 倍
  • 首 token 延迟:约 1.7s(64 token prompt)
  • 服务内存占用:3.9GB(含上下文和 KV cache)
  • 同一张 16GB SD 卡上可以同时跑程序和数据

如果你也要做边缘部署,直接抄作业:模型大于 10GB 先用 AWQ 或 GPTQ 配合 GPU 量化;模型小于 10GB 直接用 GGUF Q4_K_M;模型小于 2GB 才考虑 Q8_0 保质量。数据比经验值钱,量化参数一定自己跑一遍 PPL 和速度测试再决定,别直接照搬。