Qwen3-8B 微调模型合并、量化与本地部署总结报告

Qwen3-8B 微调模型合并、量化与本地部署总结报告

1. 项目概述

本次部署的目标,是将在魔搭(ModelScope)Notebook 环境中使用 LoRA 微调得到的 Qwen3-8B 模型,合并为完整 Hugging Face 模型,转换并量化为适合个人电脑运行的 GGUF 文件,最后迁移到 Apple M1、16 GB 内存的 MacBook Pro,通过 llama.cpp 实现命令行聊天、本地网页以及 OpenAI 兼容 API 服务。

最终结果:部署成功。模型已能在本地通过 Apple Metal 加速运行,命令行聊天和 llama-server 均验证通过。关闭思考输出时,本地实测生成速度约为 10~12 tokens/s。

本次流程如下:

1
2
3
4
5
6
7
LoRA 微调产物
→ 与 Qwen3-8B 底座模型合并
→ Hugging Face 完整模型(约 16 GB)
→ 转换为 F16 GGUF(约 16 GB)
→ Q4_K_M 量化(约 4.7 GB)
→ 从魔搭 Notebook 下载到 Mac
→ llama.cpp + Metal 本地推理

2. 环境与最终产物

2.1 云端环境

  • 平台:魔搭 ModelScope Notebook / DSW
  • 系统架构:Linux x86_64
  • 工作目录:/mnt/workspace
  • 已有 Python:Python 3.12
  • 已有 PyTorch:2.10.0+cu128
  • 转换工具:llama.cpp
  • 合并模型目录:/mnt/workspace/models/Qwen3-8B-ai-style-merged
  • GGUF 转换目录:/mnt/workspace/llama.cpp

2.2 本地环境

  • 设备:MacBook Pro
  • 芯片:Apple M1,8 核 CPU
  • 内存:16 GB 统一内存
  • 系统架构:Darwin arm64
  • 本地 llama.cpp0.4.0,build 10809,commit 5266f24da
  • 本地模型路径:/Users/bilibili/Desktop/llm/Qwen3-8B-ai-style-Q4_K_M.gguf

2.3 最终产物

产物 云端路径 大小 用途
合并后的 Hugging Face 模型 /mnt/workspace/models/Qwen3-8B-ai-style-merged 约 16 GB 完整模型源文件
F16 GGUF /mnt/workspace/Qwen3-8B-ai-style-F16.gguf 约 16 GB 量化中间文件
Q4_K_M GGUF /mnt/workspace/Qwen3-8B-ai-style-Q4_K_M.gguf 约 4.7 GB 最终本地部署文件
本地 Q4_K_M GGUF /Users/bilibili/Desktop/llm/Qwen3-8B-ai-style-Q4_K_M.gguf 约 4.7 GB Mac 实际运行文件

Q4_K_M 在模型体积、输出质量、内存占用和运行速度之间较均衡,适合 M1 16 GB 设备。

3. LoRA 与底座模型合并

本次进入 GGUF 转换阶段前,LoRA 已经与 Qwen3-8B 底座模型合并完成。合并后的目录结构包括:

1
2
3
4
5
6
7
8
9
10
11
12
Qwen3-8B-ai-style-merged/
├── chat_template.jinja
├── config.json
├── generation_config.json
├── model-00001-of-00005.safetensors
├── model-00002-of-00005.safetensors
├── model-00003-of-00005.safetensors
├── model-00004-of-00005.safetensors
├── model-00005-of-00005.safetensors
├── model.safetensors.index.json
├── tokenizer_config.json
└── tokenizer.json

合并模型必须同时包含模型权重、权重索引、模型配置、Tokenizer 和聊天模板。仅有 adapter_model.safetensorsadapter_config.json 的 LoRA 目录不能脱离底座模型独立运行。

如果需要复现合并步骤,可在 LLaMA-Factory 中使用如下结构的导出配置。以下路径和模板名称是示例,实际复现时必须使用训练阶段的底座模型路径、LoRA 输出路径和相同聊天模板:

1
2
3
4
5
6
7
8
9
model_name_or_path: /mnt/workspace/models/Qwen3-8B
adapter_name_or_path: /mnt/workspace/saves/qwen3-8b/lora/train_xxx
template: qwen3
finetuning_type: lora

export_dir: /mnt/workspace/models/Qwen3-8B-ai-style-merged
export_size: 5
export_device: cpu
export_legacy_format: false

执行:

1
llamafactory-cli export merge_lora.yaml

合并完成后进行基础检查:

1
2
3
4
5
MODEL_DIR=/mnt/workspace/models/Qwen3-8B-ai-style-merged

test -f "$MODEL_DIR/config.json" && echo "模型目录正常"
test -f "$MODEL_DIR/model.safetensors.index.json" && echo "模型分片正常"
du -sh "$MODEL_DIR"

本次检查结果为模型目录正常、5 个权重分片完整,总体积约 16 GB。

4. 获取和准备 llama.cpp

Notebook 直接访问 github.com:443 时出现连接超时或传输中断,因此没有继续使用常规 git clone。改用 GitHub 的源码下载域名 codeload.github.com 后,约 35.6 MB 的源码包成功下载。

1
2
3
4
5
6
7
8
9
10
11
12
13
cd /mnt/workspace

curl -L \
--connect-timeout 20 \
--retry 5 \
--retry-delay 3 \
--retry-all-errors \
-o llama.cpp.tar.gz \
https://codeload.github.com/ggml-org/llama.cpp/tar.gz/refs/heads/master

tar -xzf llama.cpp.tar.gz
mv llama.cpp-master llama.cpp
cd llama.cpp

源码压缩包不包含 .git 历史,但不影响编译、模型转换和量化。更新版本时需要重新下载源码包。

5. 创建隔离的模型转换环境

最初直接安装转换依赖时,requirements 指定下载约 190 MB 的 CPU 版 torch==2.11.0,下载源 download-r2.pytorch.org 速度极慢并最终超时。Notebook 系统环境已经安装 torch 2.10.0+cu128,因此采用“虚拟环境复用系统 PyTorch”的方案,避免重复下载并避免直接修改训练环境。

创建虚拟环境:

1
2
3
4
5
6
7
cd /mnt/workspace/llama.cpp

python -m venv \
--system-site-packages \
/mnt/workspace/llama-convert-env

source /mnt/workspace/llama-convert-env/bin/activate

从依赖文件中移除固定的 PyTorch 版本,并将相对 requirements 路径改为绝对路径:

1
2
3
4
5
sed \
-e '/^[[:space:]]*torch==/d' \
-e 's|^-r \./|-r /mnt/workspace/llama.cpp/requirements/|' \
requirements/requirements-convert_hf_to_gguf.txt \
> /tmp/requirements-convert-no-torch.txt

使用阿里云 PyPI 镜像安装其余依赖:

1
2
3
4
5
python -m pip install \
--index-url https://mirrors.cloud.aliyuncs.com/pypi/simple \
--timeout 300 \
--retries 10 \
-r /tmp/requirements-convert-no-torch.txt

实际安装的关键版本:

1
2
3
4
torch        2.10.0+cu128
numpy 1.26.4
protobuf 4.25.9
transformers 4.57.6

安装时出现了系统环境中 vllm、TensorBoard、OpenCV 等软件与旧版 protobufnumpy 的依赖冲突提示。由于转换环境是独立 venv,且仅用于 GGUF 转换,这些提示没有影响转换。训练、vLLM 推理等任务不应在此转换环境中运行。

验证环境:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python -c "
import torch
import numpy
import transformers
import gguf
import safetensors
import google.protobuf
print('torch:', torch.__version__)
print('numpy:', numpy.__version__)
print('protobuf:', google.protobuf.__version__)
print('transformers:', transformers.__version__)
print('依赖导入正常')
"

python convert_hf_to_gguf.py --help

注意:PyPI 包名是 protobuf,Python 中正确的导入名是 google.protobuf,不是 import protobuf

6. 处理 Qwen3 Tokenizer 兼容问题

第一次转换权重时,权重张量已经开始处理,但在设置 Tokenizer 阶段失败。主要异常为:

1
2
FileNotFoundError: tokenizer.model
AttributeError: 'list' object has no attribute 'keys'

Qwen3 使用 tokenizer.json,没有 tokenizer.model 本身并不是问题;转换器会先尝试 SentencePiece,再回退到 GPT-2/BPE Tokenizer。真正的失败原因是合并模型的 tokenizer_config.json 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"extra_special_tokens": [
"<|im_start|>",
"<|im_end|>",
"<|object_ref_start|>",
"<|object_ref_end|>",
"<|box_start|>",
"<|box_end|>",
"<|quad_start|>",
"<|quad_end|>",
"<|vision_start|>",
"<|vision_end|>",
"<|vision_pad|>",
"<|image_pad|>",
"<|video_pad|>"
]

当前 transformers 4.57.6 的相关代码把 extra_special_tokens 视为字典并调用 .keys(),而当前文件保存的是列表。解决方案是先备份配置,再将列表迁移到支持列表格式的 additional_special_tokens,保留全部特殊 token。

1
2
3
4
5
MODEL_DIR=/mnt/workspace/models/Qwen3-8B-ai-style-merged

test -e "$MODEL_DIR/tokenizer_config.json.before-gguf" || \
cp "$MODEL_DIR/tokenizer_config.json" \
"$MODEL_DIR/tokenizer_config.json.before-gguf"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
python -c "
import json
from pathlib import Path

path = Path('$MODEL_DIR/tokenizer_config.json')
data = json.loads(path.read_text(encoding='utf-8'))
extra = data.get('extra_special_tokens')
additional = data.get('additional_special_tokens')

if not isinstance(extra, list):
raise SystemExit('extra_special_tokens 不是列表')
if additional is None:
additional = []
if not isinstance(additional, list):
raise SystemExit('additional_special_tokens 不是列表')

data['additional_special_tokens'] = list(dict.fromkeys(additional + extra))
del data['extra_special_tokens']
path.write_text(
json.dumps(data, ensure_ascii=False, indent=2) + '\\n',
encoding='utf-8'
)
print('迁移完成,共保留特殊 token:', len(data['additional_special_tokens']))
"

修复后验证 Tokenizer 可以加载,并确认对话标记仍有有效 token ID:

1
2
3
4
5
6
7
8
9
python -c "
from transformers import AutoTokenizer
t = AutoTokenizer.from_pretrained('$MODEL_DIR', local_files_only=True)
print('Tokenizer 类型:', type(t).__name__)
print('Tokenizer 长度:', len(t))
print('<|im_start|> ID:', t.convert_tokens_to_ids('<|im_start|>'))
print('<|im_end|> ID:', t.convert_tokens_to_ids('<|im_end|>'))
print('Tokenizer 加载成功')
"

如需回滚配置:

1
2
3
cp \
"$MODEL_DIR/tokenizer_config.json.before-gguf" \
"$MODEL_DIR/tokenizer_config.json"

7. 将 Hugging Face 模型转换为 F16 GGUF

Tokenizer 修复并验证通过后,重新执行转换:

1
2
3
4
5
6
cd /mnt/workspace/llama.cpp

python convert_hf_to_gguf.py \
/mnt/workspace/models/Qwen3-8B-ai-style-merged \
--outfile /mnt/workspace/Qwen3-8B-ai-style-F16.gguf \
--outtype f16

日志中的转换形式符合预期:

1
2
大型权重矩阵:torch.bfloat16 → F16
RMSNorm 等敏感参数:torch.bfloat16 → F32

完成后检查:

1
ls -lh /mnt/workspace/Qwen3-8B-ai-style-F16.gguf

本次生成的 F16 GGUF 约 16 GB。F16 文件主要作为量化输入,不适合直接在 16 GB 内存的 M1 Mac 上运行。

8. 编译 llama.cpp 并执行 Q4_K_M 量化

如果 build/bin/llama-quantizebuild/bin/llama-clibuild/bin/llama-server 不存在,可执行:

1
2
3
4
5
6
7
cd /mnt/workspace/llama.cpp

cmake -B build -DCMAKE_BUILD_TYPE=Release

cmake --build build \
--target llama-quantize llama-cli llama-server \
-j "$(nproc)"

执行 Q4_K_M 量化:

1
2
3
4
./build/bin/llama-quantize \
/mnt/workspace/Qwen3-8B-ai-style-F16.gguf \
/mnt/workspace/Qwen3-8B-ai-style-Q4_K_M.gguf \
Q4_K_M

本次量化统计:

1
2
3
4
张量进度:399 / 399
原始模型大小:15623.18 MiB,16.00 BPW
量化模型大小:4789.19 MiB,4.90 BPW
量化耗时:963056.87 ms,约 16 分钟

最终文件:

1
2
/mnt/workspace/Qwen3-8B-ai-style-F16.gguf       约 16 GB
/mnt/workspace/Qwen3-8B-ai-style-Q4_K_M.gguf 约 4.7 GB

9. 在云端验证 GGUF 模型

当前使用的 llama-cli 0.4.0-dev 属于新版命令界面,默认会进入对话模式,不支持旧参数 -cnv-i--interactive--conversation。因此直接运行即可:

1
2
3
4
5
cd /mnt/workspace/llama.cpp

./build/bin/llama-cli \
-m /mnt/workspace/Qwen3-8B-ai-style-Q4_K_M.gguf \
-c 4096

也可以进行一次性测试:

1
2
3
4
5
6
./build/bin/llama-cli \
-m /mnt/workspace/Qwen3-8B-ai-style-Q4_K_M.gguf \
-p "你好,请介绍一下你自己。" \
-n 256 \
-st \
-c 4096

云端实测结果:

  • 模型成功加载;
  • 中文生成正常;
  • Prompt 处理速度约 15.6 tokens/s;
  • 生成速度约 7.2 tokens/s;
  • GGUF 转换和 Q4_K_M 量化均可正常推理。

模型自我介绍仍可能使用“通义千问”等底座身份。这不能单独证明 LoRA 未生效。验证微调效果时,应使用与训练任务同类型、但不与训练集原文完全相同的提示词,并对比底座模型、合并模型和量化模型的输出。

10. 从 Notebook 直接下载模型

最终只需迁移 Q4_K_M 文件:

1
/mnt/workspace/Qwen3-8B-ai-style-Q4_K_M.gguf

下载前在 Notebook 生成 SHA-256:

1
sha256sum /mnt/workspace/Qwen3-8B-ai-style-Q4_K_M.gguf

随后在魔搭 Notebook 左侧文件管理器中进入 /mnt/workspace,找到该 GGUF 文件并直接下载到 Mac。下载完成后在 Mac 校验:

1
2
shasum -a 256 \
/Users/bilibili/Desktop/llm/Qwen3-8B-ai-style-Q4_K_M.gguf

本地哈希应与 Notebook 中的哈希完全一致。直接下载成功后不需要再搬运 F16 GGUF、原始 safetensors 分片或云端 llama.cpp 源码。

11. 在 Apple M1 Mac 上部署 llama.cpp

通过 Homebrew 安装:

1
brew install llama.cpp

验证版本:

1
llama-cli --version

本次实际版本:

1
2
version: 0.4.0 (build 10809, commit 5266f24da)
built with AppleClang 16.0.0.16000026 for Darwin arm64

该构建是 Apple Silicon 原生 arm64 版本,可通过 Metal 使用 M1 的统一内存和 GPU。

12. 本地命令行运行

显示思考过程:

1
2
3
4
llama-cli \
-m "/Users/bilibili/Desktop/llm/Qwen3-8B-ai-style-Q4_K_M.gguf" \
-ngl 99 \
-c 4096

隐藏 Qwen3 思考过程:

1
2
3
4
5
llama-cli \
-m "/Users/bilibili/Desktop/llm/Qwen3-8B-ai-style-Q4_K_M.gguf" \
-ngl 99 \
-c 4096 \
--reasoning off

参数说明:

  • -m:GGUF 模型路径;
  • -ngl 99:尽可能将模型层卸载到 Metal GPU;
  • -c 4096:上下文窗口为 4096 tokens;
  • --reasoning off:关闭思考内容展示;
  • /exitCtrl+C:退出聊天。

本地实际验证结果:

1
2
3
4
5
6
模型格式:Q4_K - Medium
模型模态:text
Prompt 处理速度:最高约 32.6 tokens/s
生成速度:约 11.4~12.3 tokens/s
中文对话:正常
多轮交互:正常

如果出现明显内存压力,可将上下文降低到 2048:

1
2
3
4
5
llama-cli \
-m "/Users/bilibili/Desktop/llm/Qwen3-8B-ai-style-Q4_K_M.gguf" \
-ngl 99 \
-c 2048 \
--reasoning off

13. 启动本地网页与 API 服务

启动 llama-server

1
2
3
4
5
6
llama-server \
-m "/Users/bilibili/Desktop/llm/Qwen3-8B-ai-style-Q4_K_M.gguf" \
-ngl 99 \
-c 4096 \
--host 127.0.0.1 \
--port 8080

成功标志:

1
2
model loaded
listening on http://127.0.0.1:8080

浏览器访问:

1
http://127.0.0.1:8080

OpenAI 兼容接口:

1
http://127.0.0.1:8080/v1/chat/completions

API 测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
curl http://127.0.0.1:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "local-model",
"messages": [
{
"role": "user",
"content": "你好,请介绍一下自己"
}
],
"temperature": 0.7,
"max_tokens": 256
}'

本地服务实测:

1
2
3
Prompt 评估速度:约 87.03 tokens/s
生成速度:约 10.13 tokens/s
300 tokens 总耗时:约 11.2 秒

服务日志会提示 CORS 允许所有来源且没有 API key。当前服务只绑定 127.0.0.1,仅本机可访问,个人本地使用风险较低。除非已经设置 API key、防火墙和访问控制,否则不要把监听地址改为 0.0.0.0

14. 故障及解决方案汇总

问题 原因 解决方案
git clone 连接 GitHub 超时 Notebook 到 github.com:443 链路不稳定 使用 codeload.github.com 下载源码压缩包
安装转换依赖时 PyTorch 下载超时 download-r2.pytorch.org 速度极慢 创建 venv,复用系统 PyTorch,并从 requirements 中排除固定 torch 版本
import protobuf 失败 Python 模块名不是包名 使用 import google.protobuf
转换时找不到 tokenizer.model Qwen3 使用 tokenizer.json 而非 SentencePiece 文件 允许转换器回退到 GPT-2/BPE Tokenizer;该提示本身不是最终故障
AttributeError: 'list' object has no attribute 'keys' extra_special_tokens 是列表,但当前 Transformers 按字典处理 备份配置,将列表迁移到 additional_special_tokens
-cnv-i--conversation 无效 新版 llama-cli 已调整命令接口并默认聊天 不传这些旧参数,直接运行 llama-cli -m ...
</s> 被识别为非 control token Tokenizer 元数据标记与 llama.cpp 预期不同 llama.cpp 运行时自动覆盖;实际推理正常,可暂时忽略
Server 报 CORS 与无 API key 警告 默认允许跨域且没有鉴权 仅绑定 127.0.0.1;对外服务时再配置 API key 和网络访问控制

15. 验收结论

本次部署已经通过以下验收项:

  • LoRA 与 Qwen3-8B 底座模型已经合并为完整 Hugging Face 模型;
  • 5 个 safetensors 权重分片、索引、配置和 Tokenizer 文件完整;
  • Tokenizer 兼容问题已经修复,特殊 token 得到保留;
  • 完整模型已成功转换为 F16 GGUF;
  • F16 GGUF 已成功量化为约 4.7 GB 的 Q4_K_M GGUF;
  • Q4_K_M 模型在云端 llama-cli 中成功生成中文;
  • 模型已直接下载到 Apple M1 Mac;
  • 本地 arm64 版 llama.cpp 已安装;
  • Metal GPU 加速运行正常;
  • 命令行多轮聊天正常;
  • --reasoning off 生效;
  • llama-server 网页和 OpenAI 兼容接口均正常;
  • 本地生成速度约 10~12 tokens/s,满足个人交互使用。

综合判断:本次 Qwen3-8B LoRA 微调模型已经完成从云端训练产物到本地可用模型的完整部署,可进入日常使用、效果评估或本地应用集成阶段。

16. 后续建议

  1. 保留最终的 Qwen3-8B-ai-style-Q4_K_M.gguf,并将其 SHA-256 记录在独立文本中。
  2. 在确认本地文件哈希一致且长期运行正常后,可删除云端约 16 GB 的 F16 中间文件。
  3. 云端 Q4_K_M 文件建议短期保留作为备份。
  4. 使用固定的评测问题,对比原始 Qwen3-8B、合并模型和 Q4_K_M 模型,量化微调效果与量化损失。
  5. 如需集成本地应用,优先使用 llama-server 的 OpenAI 兼容 API,不必在应用中直接管理模型加载。
  6. 如需局域网或公网访问,必须增加 API key、反向代理、TLS、防火墙和访问控制,不应直接暴露无鉴权服务。
  7. 以后升级 llama.cpp 时,应重新进行一次命令参数和模型兼容性验证,因为当前项目命令行接口仍在快速演进。

报告日期:2026-09-08
部署状态:已完成并通过本地验证