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.cpp:0.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.safetensors 与 adapter_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-mergedtest -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.gzmv llama.cpp-master llama.cppcd 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-envsource /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 等软件与旧版 protobuf、numpy 的依赖冲突提示。由于转换环境是独立 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-mergedtest -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-quantize、build/bin/llama-cli 和 build/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 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:关闭思考内容展示;
/exit 或 Ctrl+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
浏览器访问:
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. 后续建议
保留最终的 Qwen3-8B-ai-style-Q4_K_M.gguf,并将其 SHA-256 记录在独立文本中。
在确认本地文件哈希一致且长期运行正常后,可删除云端约 16 GB 的 F16 中间文件。
云端 Q4_K_M 文件建议短期保留作为备份。
使用固定的评测问题,对比原始 Qwen3-8B、合并模型和 Q4_K_M 模型,量化微调效果与量化损失。
如需集成本地应用,优先使用 llama-server 的 OpenAI 兼容 API,不必在应用中直接管理模型加载。
如需局域网或公网访问,必须增加 API key、反向代理、TLS、防火墙和访问控制,不应直接暴露无鉴权服务。
以后升级 llama.cpp 时,应重新进行一次命令参数和模型兼容性验证,因为当前项目命令行接口仍在快速演进。
报告日期:2026-09-08 部署状态:已完成并通过本地验证