从 1080 Ti 到树莓派 4:Qwen2.5-0.5B Function Calling 端侧部署七步实战

把一个会调用工具的 0.5B 小模型塞进树莓派,FC 通过率 87.5%,单次调用 19 秒。本文记录完整的"训练 → 合并 → 转 GGUF → 量化 → 传 Pi → 编译 → 实测"流水线,含 6 个真实踩坑和性能瓶颈深度分析。

标签: LLM Function Calling 树莓派 llama.cpp 端侧部署 Qwen2.5 LoRA 量化


一、前言:为什么折腾把 LLM 塞进树莓派?

云端调用 GPT-4 / Claude 当然爽,但现实里很多场景必须端侧

  • 🔒 隐私敏感:智能家居、医疗/工业数据不能出本地
  • 🌐 断网可用:野外设备、车载、工业现场
  • 💰 成本敏感:百万级设备调 API 比部署贵几个数量级
  • 延迟可控:本地推理省去网络往返

而 Function Calling(FC)是 LLM 落地应用层最关键的能力——没有 FC,LLM 就只是个聊天玩具

这次实验我做了一件事:用 1080 Ti 训练 Qwen2.5-0.5B 的 FC LoRA,然后完整部署到树莓派 Pi 4 8GB,验证端侧 FC 的可行性、精度损失和实际延迟。

结论先行:

指标 数值
FC 通过率 7/8(87.5%)
单次调用平均耗时 19 秒
模型文件大小 380 MB(Q4_K_M)
常驻内存 ~570 MB
训练 → 端侧推理精度损失 0%(量化无损 + 跨平台无损)

二、项目背景与目标

2.1 任务定义

把 FC LoRA 微调后的 Qwen2.5-0.5B-Instruct 部署到树莓派 Pi 4,需要验证 4 个核心问题:

  1. 可行性:0.5B 模型在 ARM CPU + 8GB RAM 上能不能真正跑起来 FC?
  2. 精度:GGUF 量化后 FC 能力损失多少?
  3. 延迟:单次工具调用端到端多快?
  4. 量化选型:Q5_K_M vs Q4_K_M,Pi 上哪个更优?

2.2 验收标准

  • ✅ 8 个 FC 测试用例通过率 ≥ 6/8
  • ✅ 单次调用可接受(无硬性时间限制)
  • ✅ 散热 < 70°C

2.3 上游训练成果

基础模型 Qwen2.5-0.5B-Instruct
数据集 glaive_toolcall_{zh,en}_fc_only(310 纯 FC 样本)
LoRA 配置 rank=16, alpha=32, target=all
训练规模 3 epoch, batch=4, lr=5e-5 cosine
最佳 eval_loss 0.2862 @ epoch 2.14
LoRA adapter LLaMA-Factory/qwen_fc_lora/

三、双端硬件 / 软件基线

3.1 上游训练机

OS Linux 5.15.0-139-generic
GPU NVIDIA GeForce GTX 1080 Ti 11GB(Pascal, sm_61)
Python 3.11.15(uv venv 虚拟环境)
关键包 transformers 5.7.0 / torch / peft / gguf 0.19.0

⚠️ Pascal 架构的硬限制:不支持 bf16、不支持 FlashAttention 2。训练 yaml 必须 fp16: true + flash_attn: disabled,否则直接报 CUDA error: no kernel image

3.2 部署目标 Pi

SoC BCM2711,Cortex-A72 × 4 @ 1.5GHz
内存 7.6GB LPDDR4(5.0GB 可用)
架构 ARMv8.2-A aarch64
OS Raspberry Pi OS 64-bit
网络 局域网 192.168.4.18,SSH 22
编译 llama.cpp +GGML_NATIVE=ON

🚨 关键瓶颈预警:A72 没有 SDOT 指令(A76 才有),INT8 矩阵乘只能用 NEON。实测 decode 远低于预期,详见后文性能分析。


四、七步部署流水线(核心实战)

整体路径:

FP16 base + LoRA adapter
    ↓ Step 1 合并
完整 FP16 HF 模型
    ↓ Step 2 转格式
fp16 GGUF (949 MB)
    ↓ Step 3 量化
Q5_K_M / Q4_K_M GGUF
    ↓ Step 4 传输
Pi 本地存储
    ↓ Step 5 编译
ARM 原生 llama-server
    ↓ Step 6 + 7
HTTP API + FC 闭环测试

Step 1:合并 LoRA → 导出完整 FP16 模型

为什么要合并? PEFT 推理要同时加载 base + adapter,Pi 上太重。合并成单一完整模型最干净。

核心代码Pi4llm/merge_lora.py):

from transformers import AutoModelForCausalLM
from peft import PeftModel

base = AutoModelForCausalLM.from_pretrained(
    BASE, dtype=torch.float16, device_map="cpu"
)
model = PeftModel.from_pretrained(base, ADAPTER)
merged = model.merge_and_unload()
merged.save_pretrained(OUT_DIR, safe_serialization=True)

产物: qwen_fc_merged/(HF 标准格式,5 个文件,约 1.0 GB)

踩坑提醒 💣:

  • device_map="cpu":合并是线性代数运算,0.5B fp16 才 1GB,CPU 完全够。一开始我用 GPU 跑,浪费了 1080 Ti 显存还被其他任务挤
  • safe_serialization=True:必须用 safetensors 格式,旧 .bin 格式在 llama.cpp 转 GGUF 时会出诡异错误

Step 2:转换为 GGUF 格式

工具: llama.cpp/convert_hf_to_gguf.py(纯 Python,仅依赖 gguf 包)

python convert_hf_to_gguf.py \
    /path/to/qwen_fc_merged \
    --outfile qwen_fc_merged.gguf \
    --outtype f16

产物: qwen_fc_merged.gguf(949 MB,290 个张量)

意外发现 ✨: HF 目录里的 chat_template.jinja 会被自动嵌入 GGUF metadata!里面包含完整的 <tool_call> / <tool_response> Qwen FC 模板。后续 llama-server --jinja 启动后会自动用这个模板渲染 tools 参数,完全不用手动拼 prompt

这一点很重要——很多人转 GGUF 后发现 FC 用不了,根本原因就是 chat_template 没被正确嵌入。

Step 3:量化到 K-quant 混合精度

编译 llama.cpp(训练机端,用于量化工具):

cd llama.cpp
cmake -B build -DCMAKE_BUILD_TYPE=Release -DLLAMA_NATIVE=ON
cmake --build build --config Release -j$(nproc)

量化命令:

./build/bin/llama-quantize qwen_fc_merged.gguf qwen_fc_merged.Q5_K_M.gguf Q5_K_M
./build/bin/llama-quantize qwen_fc_merged.gguf qwen_fc_merged.Q4_K_M.gguf Q4_K_M

量化产物对比:

量化类型 文件大小 位宽 (BPW) 压缩比
fp16(不量化) 949 MB 16.00
Q5_K_M 401 MB 6.71 2.37×
Q4_K_M 380 MB 6.35 2.50×

关于量化警告: 转换日志会输出:

WARNING: 144 of 290 tensor(s) required fallback quantization

第一次见到这个警告会慌——其实完全正常。K-quant 是混合精度策略:小张量(bias / norm / ffn_down)保留 q5_1 / q5_0 / q6_K 等更高精度,大张量走主量化格式。这是设计如此,不是 bug。

Step 4:scp 传输到 Pi

前置: 配置 SSH 公钥免密登录

ssh-copy-id -i ~/.ssh/id_ed25519.pub x32@192.168.4.18

传输:

scp qwen_fc_merged.Q5_K_M.gguf \
    qwen_fc_merged.Q4_K_M.gguf \
    qwen_fc_merged.gguf \
    x32@192.168.4.18:~/models/

字节级校验(很重要,避免传输损坏):

文件 训练机 Pi 端 状态
Q4_K_M 380 MB 397,807,392 B ✅ 一致
Q5_K_M 401 MB 420,085,536 B ✅ 一致
fp16 949 MB 994,156,320 B ✅ 一致

Step 5:Pi 上原生编译 llama.cpp

ARM aarch64 编译,必须开 GGML_NATIVE

# 在 Pi 上执行
git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp
cmake -B build -DCMAKE_BUILD_TYPE=Release -DLLAMA_NATIVE=ON
cmake --build build --config Release -j4

关键参数说明:

  • DLLAMA_NATIVE=ON:自动检测 CPU 特性,启用 A72 的 NEON / fp16 SIMD,提速 20%+
  • -j4:匹配 A72 四核
  • 不开 CUDA:Pi 没有 NVIDIA GPU

编译耗时: 5~8 分钟(Pi 4 满载)

产物:

~/llama.cpp/build/bin/llama-cli       (1.2 MB)
~/llama.cpp/build/bin/llama-server    (16 KB,核心代码在 libggml.so 共享库)
~/llama.cpp/build/bin/llama-quantize  (19 KB)

Step 6:启动 HTTP 服务 + 单 case FC 验证

后台启动 llama-server(OpenAI 兼容 API):

nohup ~/llama.cpp/build/bin/llama-server \
    -m ~/models/qwen_fc_merged.Q5_K_M.gguf \
    --host 0.0.0.0 --port 8765 \
    -t 4 -c 2048 \
    --temp 0.0 --top-p 1.0 \
    --jinja \
    > ~/llama-server.log 2>&1 &

参数逐项解释:

参数 原因
-m Q5_K_M gguf 模型路径
--host 0.0.0.0 监听所有网卡 允许局域网访问
--port 8765 自定义端口 8080 被占(见踩坑)
-t 4 4 线程 匹配 A72 核心数
-c 2048 上下文长度 与训练 cutoff_len 一致
--temp 0.0 贪心解码 FC 必须稳定输出
--top-p 1.0 不采样 同上
--jinja 启用 GGUF 内嵌模板 自动渲染 tools 参数

单 case 实测(“北京天气怎么样?”):

{
  "finish_reason": "tool_calls",
  "message": {
    "role": "assistant",
    "content": "",
    "tool_calls": [{
      "function": {
        "name": "get_weather",
        "arguments": "{\"location\": \"北京\"}"
      }
    }]
  },
  "timings": {
    "prompt_per_second": 10.86,
    "predicted_per_second": 2.19
  }
}

🎉 FC 调用完美——正确触发 get_weather,参数 {"location": "北京"} 准确无误。

predicted_per_second: 2.19 这个数字让我心头一凉——后文详细分析。

Step 7:批量测试 + 跨平台对比

写了评测脚本 pi_eval.py,复用统一测试集(8 个 FC case)通过 OpenAI 兼容 HTTP API 调 llama-server。

先做 x86 自测基线(防止后续 Pi 出问题分不清是模型还是环境):

模型 通过率 平均 decode
fp16 7/8 (87.5%) ~56 tok/s
Q5_K_M 7/8 (87.5%) ~56 tok/s
Q4_K_M 7/8 (87.5%) ~56 tok/s

🔥 三个量化版本通过率完全一致——量化无损的铁证!

Pi 端实测(Q5_K_M,逐 case):

# case 结果 实际调用 decode 耗时
1 单工具单参数 get_weather({"location":"北京"}) 2.1 45.9s
2 单工具多参数 search_flights(...) 2.1 21.7s
3 多工具选择 calculate({"expression":"123 * 456"}) 2.1 14.4s
4 无需调用 未触发工具 2.0 20.7s
5 参数格式化 get_current_time({}) 2.1 9.6s
6 航班+日期格式化 search_flights({"date":"2026-08-15"}) 2.2 22.1s
7 工具歧义选择 get_weather({"location":"北京"}) 2.1 11.0s
8 复杂计算 见后文分析 - -

7/8 通过! 跨平台从 x86 到 ARM,通过率完全保持。


五、性能深度分析

5.1 Q5_K_M vs Q4_K_M:Pi 上怎么选?

维度 Q5_K_M Q4_K_M 推荐
FC 通过率 87.5% 87.5% 🤝 持平
decode 速度 2.10 tok/s 2.15 tok/s Q4 略快
平均耗时 20.26 s/case 19.16 s/case Q4 略快
文件大小 401 MB 380 MB Q4 小 21 MB
内存占用 ~600 MB ~570 MB Q4 更省

🏆 结论:Pi 部署 Q4_K_M 完胜——速度略快、文件更小、内存更省、通过率一致。

5.2 decode 2 tok/s:Pi 4 的硬件天花板

部署文档预期 decode 应该在 8~15 tok/s,实际只有 2.10。我做了完整的硬件诊断:

检查项 状态 排查结论
编译优化 GGML_NATIVE ✅ 已开 不是软件问题
CPU 频率 ✅ 1.5 GHz 满频 没降频
温度 ✅ 45°C 远低于热限 80°C
内存可用 ✅ 5.0 GB 充足
线程数 ✅ 4 匹配核心数

根本原因:Pi 4 LPDDR4 单通道内存带宽只有 ~4 GB/s

decode 阶段每生成 1 个 token,都要完整读一遍模型权重(400 MB for Q5_K_M)。理论带宽上限:

4000 MB/s ÷ 400 MB/token ≈ 10 tok/s

实测 2.10 tok/s 的额外损耗来自:

  • Q5_K_M 混合精度反量化消耗 CPU 算力
  • 4 线程并行调度开销
  • KV cache 也吃带宽

这不是软件问题,是 Pi 4 的物理上限。

💡 升级路径: Pi 5(Cortex-A76 + 双通道 LPDDR4X)预期 decode 提速 3~5 倍,单次调用压到 5~8 秒可期。

5.3 量化无损的全链路证据

v2 LoRA 训练 (eval_loss 0.2862)
        ↓
merge_and_unload() → fp16 HF
        ↓
convert_hf_to_gguf.py → fp16 GGUF
        ↓
Q5_K_M / Q4_K_M 量化
        ↓
x86 推理     → 7/8 (87.5%)
        ↓
Pi 4 推理    → 7/8 (87.5%)

🎯 从 LoRA 训练到端侧推理,精度损失为零。这打破了一个常见误区:“量化一定会损失 FC 能力”——只要训练得够好(eval_loss < 0.3),Q4_K_M 完全能扛住 FC 任务。

5.4 case 8 失败真相:测试逻辑背锅

case 8「复杂计算」失败,乍看是模型不行,仔细看:

# 测试用例期望
case = {
    "user_input": "计算 (123 + 456) * 2",
    "expected_tool": "calculate",
    "expected_args": {"expression": "(123+456)*2"}    # 紧凑格式
}

# 模型实际输出
{"expression": "(123 + 456) * 2"}                    # 带空格

judge 函数用 substring 比较,对空格敏感。模型其实正确识别了 calculate 工具并填了正确的表达式,只是字符串里多了几个空格。

修复建议(未执行): judge 改成去空格后比较,case 8 应通过 → 期望 8/8 (100%)。

这种"测试假阴性"在做评测时特别要警惕。


六、踩坑实录

实战过程中踩了 6 个坑,每个都让我浪费了至少 30 分钟:

坑 1:默认端口 8080 被占

现象: couldn't bind HTTP server socket, port: 8080

原因: Pi OS 上 8080 是常见占用端口(Apache、其他服务)

对策: 换 8765(任意高位端口)

坑 2:后台 polling 60s 超时

现象: server 实际起来了,但启动脚本已经退出报错

原因: 模型加载 + JIT 编译要 1~2 分钟,原脚本只等 60 秒

对策: polling 改成 60×5s=300s,配合 curl --max-time 5

坑 3:uv pip install transformers 版本冲突

现象: uv pip install transformers 报索引冲突

原因: uv 的严格索引策略 + PyTorch 源冲突

对策: 不装完整 transformers,只装 gguf 单包(其他依赖训练环境已存在)

坑 4:venv 无 pip

现象: No module named pip

原因: uv venv 默认不装 pip

对策: 改用 uv pip install,不要试图激活 venv 后用 pip

坑 5:x86 自测初次 1/8 通过率(最大坑)

现象: Pi 部署前用 x86 自测,本应 7/8,结果 1/8

原因: OpenAI API 把 tool_calls 解析到了 message.tool_calls 字段,而我的脚本只读了 message.content(永远是空字符串)

对策: 优先读 message.tool_calls,没有再 fallback 到 content

# 正确的解析逻辑
msg = response["choices"][0]["message"]
if msg.get("tool_calls"):
    tool_call = msg["tool_calls"][0]["function"]
    # 处理 tool_call["name"], tool_call["arguments"]
else:
    content = msg.get("content", "")
    # 处理纯文本响应

这个坑差点让我以为模型没训好,差点重新训一遍。

坑 6:脚本导入路径错误

现象: from fc_test_data import ... 报 ModuleNotFoundError

原因: 评测脚本在子目录 Pi4llm/ 里,相对导入找不到上级模块

对策: sys.path 注入

import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parents[2]))

七、最终部署模板(开箱即用)

7.1 推荐启动命令

~/llama.cpp/build/bin/llama-server \
    -m ~/models/qwen_fc_merged.Q4_K_M.gguf \
    --host 0.0.0.0 --port 8765 \
    -t 4 -c 2048 \
    --temp 0.0 --top-p 1.0 \
    --jinja

7.2 性能预期(管理用户预期)

指标 数值
启动时间 5~15 秒
单次 FC 调用端到端 9~46 秒(取决于响应 token 数)
平均 decode ~2.1 tok/s
常驻内存 ~570 MB
适用场景 离线工具调用代理,❌ 不适合实时对话

⚠️ 20 秒一次的工具调用听起来慢,但对很多场景完全够用:定时任务、传感器触发、批处理。不要拿它做聊天机器人

7.3 systemd 常驻服务(可选)

# /etc/systemd/system/llama-fc.service
[Unit]
Description=Qwen 0.5B FC LLM Service
After=network.target

[Service]
Type=simple
User=x32
ExecStart=/home/x32/llama.cpp/build/bin/llama-server \
    -m /home/x32/models/qwen_fc_merged.Q4_K_M.gguf \
    --host 0.0.0.0 --port 8765 \
    -t 4 -c 2048 --temp 0.0 --top-p 1.0 --jinja
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target
sudo systemctl enable llama-fc
sudo systemctl start llama-fc

八、后续优化方向

  1. 修复 case 8 judge 逻辑 → 期望通过率 8/8 (100%)
  2. Pi 5 升级评估 → A76 + 双通道内存预期 decode 6~10 tok/s,单次调用 5~8 秒
  3. 更激进量化对比 → Q3_K_S(~250MB)测试 FC 能力下限
  4. 混合云架构 → 0.5B 在 Pi 上做 FC 触发判定,命中后再请求云端 7B/14B 执行实际业务
  5. Embedding 复用 → Pi 同时跑 0.5B FC + embedding 模型,构建本地 RAG
  6. 多 Pi 集群 → 几台 Pi 4 做负载均衡,应对并发场景

九、总结与反思

9.1 核心结论

可行性:Qwen2.5-0.5B FC LoRA 在 Pi 4 8GB 上完全可部署,端到端流水线跑通
精度:从训练到端侧推理零损失,量化无损 + 跨平台无损
选型:Q4_K_M 是 Pi 部署最优选(速度、体积、内存全胜)
⚠️ 延迟:单次 19 秒,Pi 4 内存带宽物理上限,非软件问题

9.2 三个反直觉的发现

  1. 量化无损是真的无损——只要训练得够好(eval_loss < 0.3),Q4_K_M 完全能扛住 FC,不像网上说的"必须 Q8 才稳"
  2. 小模型 FC 能力比想象中强——0.5B 经过 310 条样本微调就能到 87.5% 通过率,关键在数据质量
  3. Pi 4 不是不能跑 LLM,是带宽瓶颈——CPU 算力其实够,LPDDR4 单通道拖了后腿

9.3 适合什么场景

场景 推荐度
智能家居中心(局域网 FC 代理) ⭐⭐⭐⭐⭐
工业设备状态查询(低频调用) ⭐⭐⭐⭐⭐
隐私敏感的本地助手 ⭐⭐⭐⭐
实时聊天机器人 ⭐(延迟太高)
高并发 API 服务 ⭐(吞吐量低)

9.4 写在最后

这次实验最大的收获不是某个具体技巧,而是验证了一个判断:端侧 FC 不是奢侈品,是可工程化的现实

0.5B + LoRA + Q4_K_M + llama.cpp + 树莓派——这套组合成本几百块,覆盖了 80% 的本地 FC 场景。剩下的 20% 才需要云端大模型。



如果这篇文章对你有帮助,欢迎点赞收藏 + 关注专栏。

后续会继续更新:

  • Pi 5 vs Pi 4 LLM 推理性能实测对比
  • 0.5B 端侧 FC + Embedding 双模型架构
  • 更激进量化(Q3 / Q2)的 FC 能力下限测试

有任何问题欢迎评论区交流~ 🚀

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐