自我蒸馏(新)

上一篇的续作。

上一篇写的用模型量化自己的教程一塌糊涂,而且量化出来的也不太能够使用,会有严重的胡说八道的现象。
大概是处于一个既不是我,又不是Qwen的状态。——反正就是离人很远

Q: 为什么你喜欢我
A: 因为我们有共同的语言,能分享生活中的点点滴滴,还有你的支持与陪伴让我感到很温暖。

Q: 你在做什么
A: 我在聊天呢
Q: 聊什么
A: 没什么,就是随意聊聊
Q: 和谁聊
A: 你呀

所以又开了一个新的GPU实例,配置了48G显存的RTX4090,这次可以量化的模型参数量可以配置的高一些了。
希望能量化出可以使用的模型。

项目 规格详情
GPU 48G RTX4090 *1
可用区 华北二A
镜像 Ubuntu-nvidia 24.04
CPU 16核
内存 96 GB
系统盘 200 GB

拿到新系统的第一刻,还是无卡启动进行配置更新,安装conda,git,fnm,安装pytorch,node,cmake……
让这个系统到一个可以被使用的地步。

环境配置

更新系统软件源

1
2
sudo apt update
sudo apt upgrade -y

安装Git

1
2
sudo apt install git -y
git --version

使用Miniforge配置Python 3.11环境

1
2
3
4
5
6
7
wget https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-x86_64.sh
bash Miniforge3-Linux-x86_64.sh
source ~/.bashrc
conda config --set auto_activate_base false
conda create -n llama python=3.11 -y
conda activate llama
python --version

执行最后一项验证后,终端输出将为Python 3.11.x。Miniforge默认配置conda-forge源并集成mamba,后续进入该环境执行conda activate llama即可。

安装Node.js

使用fnm安装并配置Node.js 24

1
2
3
4
5
6
7
curl -fsSL https://fnm.vercel.app/install | bash
source ~/.bashrc
fnm install 24
fnm use 24
fnm default 24
node --version
npm --version

安装训练框架LLaMA-Factory

LLaMA-Factory是一站式微调平台,封装了数据处理、LoRA训练、权重合并与导出全流程。

1
2
3
4
5
6
7
8
9
10
# 激活训练环境
conda activate llama

# 克隆仓库
git clone --depth 1 https://github.com/hiyouga/LLaMA-Factory.git
cd LLaMA-Factory

# 安装依赖
pip install -e ".[torch,metrics]"
pip install flash-attn --no-build-isolation

锁定与框架兼容的依赖版本:

1
2
pip install "huggingface_hub<2.0,>=1.5.0"
pip check

验证:pip check无冲突输出;执行llamafactory-cli --help能正常显示帮助信息。

说明:huggingface-hub 2.x版本与transformers、gradio、tokenizers存在依赖冲突,锁定在1.x可保证WebUI与训练流程正常。

下载基座模型

维度 7B 14B(推荐) 32B
语言风格复刻 良好 极佳 极致
接梗与潜台词理解 一般 敏锐 极高
抗「AI腔」漂移 中等 强 极强
48G训练开销 约15GB 约28GB 需QLoRA
推理速度 80+ tok/s 50~60 tok/s 20~30 tok/s

推荐基座:OpenPipe/Qwen3-14B-Instruct

modelshow

该版本已移除Qwen3默认的<think>思维链输出,回归直接问答模式,对微调友好,契合微信聊天中碎片化、短句、快速接梗的表达特征。

版本选择三条准则

  • 选择带-Instruct后缀的指令对齐版,不使用-Base预训练底模(Base未做对话对齐,只会续写不会问答)。
  • 不使用带-Thinking的版本(会先输出大段思维链,破坏随性对话节奏)。
  • 不使用带-AWQ/-GPTQ的版本(这类是量化推理专用包,不用于训练)。

模型完整权重约29GB,下载到本地目录,训练时直接指定本地路径。

1
2
3
4
5
# 国内网络建议启用镜像加速
export HF_ENDPOINT=https://hf-mirror.com

hf download OpenPipe/Qwen3-14B-Instruct \
--local-dir ./models/Qwen3-14B-Instruct

验证:进入./models/Qwen3-14B-Instruct目录,确认存在以下文件:

  • config.json
  • tokenizer.json、tokenizer_config.json、vocab.json、merges.txt
  • model-00001-of-00006.safetensors等多个权重分卷
  • model.safetensors.index.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(base) ubuntu@10-60-22-134:~/models$ ls -alh
total 25G
drwxrwxr-x 2 ubuntu ubuntu 4.0K Oct 17 20:03 .
drwxr-x--- 17 ubuntu ubuntu 4.0K Oct 17 20:06 ..
-rw-rw-r-- 1 ubuntu ubuntu 381 Oct 17 20:03 Modelfile
-rw-rw-r-- 1 ubuntu ubuntu 2.7K Oct 17 20:03 chat_template.jinja
-rw-rw-r-- 1 ubuntu ubuntu 1.7K Oct 17 20:01 config.json
-rw-rw-r-- 1 ubuntu ubuntu 213 Oct 17 20:01 generation_config.json
-rw------- 1 ubuntu ubuntu 4.7G Oct 17 20:02 model-00001-of-00006.safetensors
-rw------- 1 ubuntu ubuntu 4.6G Oct 17 20:02 model-00002-of-00006.safetensors
-rw------- 1 ubuntu ubuntu 4.7G Oct 17 20:02 model-00003-of-00006.safetensors
-rw------- 1 ubuntu ubuntu 4.6G Oct 17 20:02 model-00004-of-00006.safetensors
-rw------- 1 ubuntu ubuntu 4.7G Oct 17 20:03 model-00005-of-00006.safetensors
-rw------- 1 ubuntu ubuntu 4.5G Oct 17 20:03 model-00006-of-00006.safetensors
-rw-rw-r-- 1 ubuntu ubuntu 36K Oct 17 20:03 model.safetensors.index.json
-rw-rw-r-- 1 ubuntu ubuntu 11M Oct 17 20:03 tokenizer.json
-rw-rw-r-- 1 ubuntu ubuntu 719 Oct 17 20:03 tokenizer_config.json

说明:上文4个分词相关文件在后续转换GGUF时必需,请确认目录完整。

微信聊天记录处理

导出聊天记录

使用WeFlow导出微信私聊记录,得到JSON文件。导出格式说明:

字段 含义
isSend: 1 你本人发送的消息
isSend: 0 对方发送的消息
type: "文本消息" 纯文本内容
type: "引用消息" 引用他人消息后的回复
createTime 消息时间戳

说明:私聊场景中isSend: 1恒为你本人,与聊天对象无关,因此批量处理多个联系人的逻辑完全通用。

建议导出多个联系人的私聊记录,合并为统一训练集,样本越丰富风格覆盖越完整。

语料规模建议

1,000 ~ 5,000条高质量多轮 / 单轮样本,足以让LoRA捕捉稳定的语言偏置。

转换脚本

目的:把原始导出文件转换为ShareGPT格式的多轮训练数据。核心处理是时间窗口合并——把你在连续几分钟内发出的多条短句用换行符拼接成一条完整回复,保留真实的断句节奏。

创建process_weflow.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import json
import re
from pathlib import Path

# 配置待处理的文件列表(WeFlow 导出的各联系人 JSON)
INPUT_FILES = [
"私聊_***-7a31c*****.json", # 示范文件
]
OUTPUT_FILE = "persona_sft_dataset_all.json"

TIME_THRESHOLD = 180 # 3 分钟(180 秒)内的连续发言合并为一轮
MAX_TURNS = 6 # 每个多轮对话样本包含 6 轮问答(12 条消息)


def clean_content(msg: dict) -> str | None:
"""清洗单条消息,返回纯文本;无效消息返回 None"""
msg_type = msg.get("type", "")
content = msg.get("content", "")

# 只保留文本消息与引用消息
if msg_type not in ["文本消息", "引用消息"]:
return None
if not content:
return None

# 过滤纯占位符
ignore_list = ["[图片]", "[表情包]", "[视频]", "[语音通话]", "[视频通话]",
"[位置]", "[名片]", "[炸弹]", "[OK]"]
if content.strip() in ignore_list:
return None

# 剥离引用消息自动附加的后缀标签
content = re.sub(r"\[\s*引用[^\]]*\]", "", content)

# 清理未配对的右中括号
content = re.sub(r"(?<!\[)\]", "", content)

# 手机号脱敏
content = re.sub(r"1\d{10}", "13800000000", content)

content = content.strip()
return content if content else None


def process_single_file(file_path: str) -> list[dict]:
"""处理单个聊天文件,返回样本列表"""
if not Path(file_path).exists():
print(f"未找到文件: {file_path},跳过")
return []

with open(file_path, "r", encoding="utf-8") as f:
data = json.load(f)

# 步骤 1:筛选清洗出有效文本序列
clean_msgs = []
for m in data.get("messages", []):
text = clean_content(m)
if text:
clean_msgs.append({
"isSend": m.get("isSend"),
"time": m.get("createTime", 0),
"text": text,
})

if not clean_msgs:
return []

# 步骤 2:滑动时间窗口,合并同一角色的连续发言
merged_blocks = []
current_block = {
"role": "gpt" if clean_msgs[0]["isSend"] == 1 else "human",
"texts": [clean_msgs[0]["text"]],
"last_time": clean_msgs[0]["time"],
}
for m in clean_msgs[1:]:
role = "gpt" if m["isSend"] == 1 else "human"
if role == current_block["role"] and (m["time"] - current_block["last_time"] <= TIME_THRESHOLD):
current_block["texts"].append(m["text"])
current_block["last_time"] = m["time"]
else:
merged_blocks.append(current_block)
current_block = {
"role": role,
"texts": [m["text"]],
"last_time": m["time"],
}
merged_blocks.append(current_block)

# 步骤 3:规范为 human -> gpt 严格交替
dialogue = []
for block in merged_blocks:
content = "\n".join(block["texts"])
if block["role"] == "human":
dialogue.append({"from": "human", "value": content})
elif block["role"] == "gpt":
if dialogue and dialogue[-1]["from"] == "human":
dialogue.append({"from": "gpt", "value": content})

# 步骤 4:按固定轮次切分为多个样本
samples = []
temp = []
for i in range(0, len(dialogue) - 1, 2):
if dialogue[i]["from"] == "human" and dialogue[i + 1]["from"] == "gpt":
temp.extend([dialogue[i], dialogue[i + 1]])
if len(temp) >= MAX_TURNS * 2:
samples.append({"conversations": temp})
temp = []
if temp:
samples.append({"conversations": temp})

return samples


def main():
total_dataset = []
for f in INPUT_FILES:
samples = process_single_file(f)
print(f"文件 {f} 生成有效样本: {len(samples)} 组")
total_dataset.extend(samples)

with open(OUTPUT_FILE, "w", encoding="utf-8") as out:
json.dump(total_dataset, out, ensure_ascii=False, indent=2)

print(f"\n全部合并完成,总计生成 {len(total_dataset)} 组微调样本,已保存至 {OUTPUT_FILE}")


if __name__ == "__main__":
main()

运行转换

1
python process_weflow.py

验证:生成persona_sft_dataset_all.json,结构如下。注意value中的\n完整保留了连发消息的节奏感:

1
2
3
4
5
6
7
8
9
10
[
{
"conversations": [
{"from": "human", "value": "我早饭是不吃的 午饭是没有的 晚饭是现做的\n能熬到天亮"},
{"from": "gpt", "value": "我还好,只有早饭不吃\n但我要89点就起来"},
{"from": "human", "value": "毕竟人还没睡醒 就来房间通知我她要出去打麻将了\n午饭是根本不用烧的"},
{"from": "gpt", "value": "她这何尝不是一种自律呢?\n那你吃什么?"}
]
}
]

说明:原始导出文件通常有几十MB,转换后仅几百KB到几MB,属于正常现象。原始文件包含大量渲染用元数据(内部ID、XML签名、头像CDN链接、表情下载URL等),且大量表情包与图片消息已被过滤,连续短句也被合并压缩。

清洗与隐私脱敏

目的:剔除噪音与敏感信息。训练数据中的隐私会被模型记忆并在外部对话中复述,脱敏是必经步骤。

需处理的内容:

  • 表情标签未闭合(如[捂脸缺少右括号)
  • 引用消息残留的孤立右中括号
  • 真实姓名、私密信息、手机号、登录密码

创建clean_dataset_all.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import json
import re

INPUT_FILE = "persona_sft_dataset_all.json"
OUTPUT_FILE = "persona_sft_dataset_all_clean.json"


def sanitize_text(text: str) -> str:
# 1. 敏感信息脱敏(按你自己的实际情况调整替换规则)
text = re.sub(re.escape("1112***"), "11125000", text) # 学号
text = re.sub(re.escape("你的真实姓名"), "***", text) # 真实姓名
text = re.sub(re.escape("南苑******"), "某宿舍楼101", text) # 宿舍地址
text = re.sub(re.escape("密码abc123456"), "密码******", text) # 明文密码

# 2. 手机号统一脱敏
text = re.sub(r"1\d{10}", "13800000000", text)

# 3. 补全未闭合的微信表情标签
emoji_names = [
"坏笑", "捂脸", "流汗", "抱拳", "恐惧", "OK", "握手", "色", "大哭", "玫瑰",
"害羞", "裂开", "发抖", "发呆", "得意", "调皮", "让我看看", "胜利", "骷髅",
"转圈", "奸笑", "惊讶", "强", "打脸", "破涕为笑", "憨笑", "合十", "傲慢",
"凋谢", "生病", "白眼", "快哭了", "微笑", "机智", "偷笑", "困", "衰",
]
for name in emoji_names:
text = re.sub(rf"\[{name}(?!\])", f"[{name}]", text)

# 4. 清理多余空行
text = re.sub(r"\n{3,}", "\n\n", text)
return text.strip()


with open(INPUT_FILE, "r", encoding="utf-8") as f:
data = json.load(f)

cleaned_data = []
for item in data:
new_conversations = []
skip_item = False
for conv in item.get("conversations", []):
val = conv.get("value", "")

cleaned_val = sanitize_text(val)
if not cleaned_val:
skip_item = True
break
new_conversations.append({"from": conv["from"], "value": cleaned_val})

if not skip_item and len(new_conversations) >= 2:
cleaned_data.append({"conversations": new_conversations})

with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
json.dump(cleaned_data, f, ensure_ascii=False, indent=2)

print(f"清洗完成!共处理样本 {len(cleaned_data)} 组,已保存至 {OUTPUT_FILE}")

运行清洗:

1
python clean_dataset_all.py

验证:统计有效回复轮数。

1
grep -c '"from": "gpt"' persona_sft_dataset_all_clean.json

输出 ≥ 1000即数据量充裕,可直接进入训练。

注册数据集

放置文件

1
cp persona_sft_dataset_all_clean.json LLaMA-Factory/data/

注册配置

编辑LLaMA-Factory/data/dataset_info.json,在最外层{内添加:

1
2
3
4
5
6
7
8
9
10
11
"my_persona": {
"file_name": "persona_sft_dataset_all_clean.json",
"formatting": "sharegpt",
"columns": { "messages": "conversations" },
"tags": {
"role_tag": "from",
"content_tag": "value",
"user_tag": "human",
"assistant_tag": "gpt"
}
},

校验配置

1
2
cd LLaMA-Factory
python -c "import json; json.load(open('data/dataset_info.json'))"

无输出即JSON语法正确。

在界面中确认(使用WebUI时)

在LLaMA-Factory仓库根目录启动界面:

1
2
cd LLaMA-Factory
nohup llamafactory-cli webui > webui.log 2>&1 &

启动后:

  1. 在「数据集」下拉框勾选my_persona。
  2. 点击「预览数据集」,确认能看到human与gpt交替的多轮对话。

说明:务必在LLaMA-Factory仓库目录内启动,程序会在当前目录的./data下查找配置。若在其它目录启动,界面将读不到任何数据集。修改配置后需重启界面进程并强制刷新浏览器(Mac:Cmd + Shift + R)。

启动LoRA微调

参数配置表(48GB显存优化值)

配置项 推荐值 说明
模型路径 /你的绝对路径/models/Qwen3-14B-Instruct 使用本地绝对路径
对话模板 qwen3_nothink 非思考模式,直接问答
微调方法 lora —
量化等级 none 48G显存直接跑BF16全精度
数据集 my_persona 前面注册的数据集
截断长度 2048 保证多轮上下文不被截断
批处理大小 2 —
梯度累积 8 等效总batch = 2 × 8 = 16
学习率 1.5e-4 LoRA标准区间1e-4 ~ 2e-4
训练轮数 4.0 3~4轮可拿捏口癖
计算精度 bf16 4090原生支持
LoRA作用模块 all 覆盖全部线性层,风格拟合最彻底
LoRA秩 32 —
LoRA缩放系数 64 取秩的2倍

三条参数纪律

  • 对话模板必须选qwen3_nothink(或qwen),不能用default。default不带千问专属角色标识符,模型无法区分对话双方。
  • 学习率必须设在1e-4 ~ 2e-4。1e-5是全参数微调的量级,对LoRA过小,训练后仍会保留基座的通用AI腔。
  • LoRA作用模块填all。留空时框架默认只训练注意力层的q/v投影,风格还原不彻底。

启动训练

Parameter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
cd LLaMA-Factory
conda activate llama

CUDA_VISIBLE_DEVICES=0 llamafactory-cli train \
--stage sft \
--do_train \
--model_name_or_path /你的绝对路径/models/Qwen3-14B-Instruct \
--dataset my_persona \
--dataset_dir ./data \
--template qwen3_nothink \
--finetuning_type lora \
--lora_target all \
--lora_rank 32 \
--lora_alpha 64 \
--lora_dropout 0.05 \
--output_dir ./saves/persona_14b_lora \
--overwrite_output_dir \
--cutoff_len 2048 \
--preprocessing_num_workers 4 \
--per_device_train_batch_size 2 \
--gradient_accumulation_steps 8 \
--lr_scheduler_type cosine \
--logging_steps 5 \
--save_steps 50 \
--learning_rate 1.5e-4 \
--num_train_epochs 4.0 \
--plot_loss \
--bf16

训练过程观察

process

观察项 正常表现
显存占用 约42GB(48G显卡余量充足)
训练耗时 几百条样本约8~15分钟
Loss曲线 从5.x平稳下降至2.x
完成标志 终端输出Training completed,或界面出现绿色完成提示

说明:训练启动阶段显存被占满而GPU利用率接近0% 属正常现象,此时正在加载权重与分词。正式训练开始后功耗升至400W、利用率达100%。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Every 1.0s: nvidia-smi                                                          10-60-22-134: Sat Oct 17 19:43:50 2026

Sat Oct 17 19:43:50 2026
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 615.71.09 KMD Version: 615.71.09 CUDA UMD Version: 13.4 |
+-----------------------------------------+------------------------+----------------------+
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|=========================================+========================+======================|
| 0 NVIDIA GeForce RTX 4090 On | 00000000:00:06.0 Off | Off |
|100% 67C P2 432W / 450W | 48507MiB / 49140MiB | 100% Default |
| | | N/A |
+-----------------------------------------+------------------------+----------------------+

+-----------------------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=========================================================================================|
| 0 N/A N/A 1427 C ...ge3/envs/llama/bin/python3.11 386MiB |
| 0 N/A N/A 1513 C ...ge3/envs/llama/bin/python3.11 48106MiB |
+-----------------------------------------------------------------------------------------+

result

合并导出完整模型

目的:把LoRA增量权重合并进基座,得到独立可用的完整模型,后续部署不再依赖适配器。

1
2
3
4
5
6
7
8
9
10
11
12
cd LLaMA-Factory
conda activate llama

llamafactory-cli export \
--model_name_or_path /你的绝对路径/models/Qwen3-14B-Instruct \
--adapter_name_or_path ./saves/persona_14b_lora \
--template qwen3_nothink \
--finetuning_type lora \
--export_dir /你的绝对路径/models/my_persona_14b_merged \
--export_size 4 \
--export_device cpu \
--export_legacy_format False

验证:导出目录包含多个.safetensors分卷(14B约25~29GB)、config.json、tokenizer.json、tokenizer_config.json。


转换为GGUF并量化

为什么选择GGUF

量化格式按部署平台选择:

格式 量化是否需要GPU 适用生态 适用平台
GGUF(Q4_K_M / Q6_K_M) 否,纯CPU与内存即可 Ollama、LM Studio、llama.cpp Mac、跨平台
AWQ(4-bit GEMM) 是,需跑前向激活 vLLM、TGI、SGLang NVIDIA GPU高并发服务

Mac部署使用GGUF。AWQ与GPTQ面向CUDA架构设计,在Apple Silicon的Metal统一内存上无法发挥硬件加速;GGUF深度整合Metal,省内存、发热低、速度快。

编译llama.cpp

1
2
3
4
5
6
7
8
9
# 安装编译工具
sudo apt install cmake -y

# 拉取并编译
git clone https://github.com/ggerganov/llama.cpp.git
cd llama.cpp
cmake -B build
cmake --build build --config Release -j
pip install -r requirements.txt

说明:cmake必须在llama.cpp目录内执行,该目录才包含CMakeLists.txt。

确认分词器配置

目的:确保合并模型的分词配置符合llama.cpp转换脚本的读取要求。

检查合并模型目录(如my_persona_14b_merged)中的tokenizer_config.json,确认extra_special_tokens字段为字典(键值对)格式:

1
2
3
4
"extra_special_tokens": {
"<|im_start|>": "<|im_start|>",
"<|im_end|>": "<|im_end|>"
}

若为列表格式(如下),请改为上面的字典格式:

1
"extra_special_tokens": ["<|im_start|>", "<|im_end|>"]

同时确认目录内分词文件齐全:tokenizer.json、tokenizer_config.json、vocab.json、merges.txt。缺失时从基座模型目录复制补齐:

1
2
3
4
cp /你的绝对路径/models/Qwen3-14B-Instruct/tokenizer.json       /你的绝对路径/models/my_persona_14b_merged/
cp /你的绝对路径/models/Qwen3-14B-Instruct/tokenizer_config.json /你的绝对路径/models/my_persona_14b_merged/
cp /你的绝对路径/models/Qwen3-14B-Instruct/vocab.json /你的绝对路径/models/my_persona_14b_merged/
cp /你的绝对路径/models/Qwen3-14B-Instruct/merges.txt /你的绝对路径/models/my_persona_14b_merged/

说明:Qwen使用BPE词表,本身不含SentencePiece的tokenizer.model文件。转换脚本会先尝试SentencePiece再回退到GPT2词表路径,属正常流程,无需额外补充tokenizer.model。

转换为GGUF

1
2
3
python /你的绝对路径/llama.cpp/convert_hf_to_gguf.py /你的绝对路径/models/my_persona_14b_merged \
--outfile /你的绝对路径/models/my_persona_14b_f16.gguf \
--outtype f16

量化

推荐Q6_K_M:精度接近无损,体积比浮点小一半以上,适合内存充裕的Mac。若追求更小体积选Q4_K_M(约8.5GB)。

1
2
3
4
/你的绝对路径/llama.cpp/build/bin/llama-quantize \
/你的绝对路径/models/my_persona_14b_f16.gguf \
/你的绝对路径/models/my_persona_14b_Q6_K_M.gguf \
Q6_K_M

验证:生成my_persona_14b_Q6_K_M.gguf,Q6_K_M约11~12GB,Q4_K_M约8.5GB。

可在服务器上量化完成后再传回Mac:原始浮点模型近30GB,量化后体积缩减约70%,传输大幅加快。中间产物my_persona_14b_f16.gguf与导出目录的分卷权重可在确认成品无误后清理。

1
2
3
(llama) ubuntu@10-60-22-134:~$ ls -lh /home/ubuntu/models/
-rw-rw-r-- 1 ubuntu ubuntu 28G Oct 17 20:32 my_persona_14b_f16.gguf
-rw-rw-r-- 1 ubuntu ubuntu 12G Oct 17 20:35 my_persona_14b_Q6_K_M.gguf

服务器端加载测试

目的:在传回Mac前确认「转换 → 量化」整条链路成功。

单次推理测试

1
2
3
4
/你的绝对路径/llama.cpp/build/bin/llama-cli \
-m /你的绝对路径/models/my_persona_14b_Q6_K_M.gguf \
-p "你好,请介绍一下你自己。" \
-n 128 -ngl 99 -c 4096

参数说明:

  • -ngl 99:全部网络层卸载到GPU。显存不足时会自动回退部分层到CPU,日志显示实际卸载层数。
  • -c:上下文长度。Q6_K模型本体约11.3GB,更长上下文需额外数GB显存放KV Cache;若显卡已被其它进程占用较多,先用-c 4096测试。

验证:输出正常、无乱码、无重复,即链路完全成功。

附录 · 关键参数速查

环节 关键项 正确值
基座模型 版本 OpenPipe/Qwen3-14B-Instruct
对话模板 template qwen3_nothink
微调方法 finetuning_type lora
量化等级 quantization_bit none(48G直接用BF16)
学习率 learning_rate 1.5e-4
训练轮数 num_train_epochs 4.0
截断长度 cutoff_len 2048
批处理 × 累积 batch × accumulation 2 × 8(等效16)
LoRA作用模块 lora_target all
LoRA秩 / 缩放 rank / alpha 32 / 64
计算精度 — bf16
部署格式 Mac GGUF(Q6_K_M或Q4_K_M)
分词配置 extra_special_tokens 字典格式

附录 · 全流程命令速查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
llamafactory-cli train \
--stage sft \
--do_train \
--model_name_or_path /home/ubuntu/models/Qwen3-14B-Instruct \
--preprocessing_num_workers 16 \
--finetuning_type lora \
--template qwen3_nothink \
--flash_attn fa2 \
--dataset_dir data \
--dataset my_persona \
--cutoff_len 2048 \
--learning_rate 1.5e-4 \
--num_train_epochs 4.0 \
--max_samples 100000 \
--seed 42 \
--per_device_train_batch_size 2 \
--gradient_accumulation_steps 8 \
--lr_scheduler_type cosine \
--max_grad_norm 1.0 \
--logging_steps 5 \
--save_steps 100 \
--warmup_steps 0 \
--packing False \
--enable_thinking False \
--report_to none \
--output_dir saves/persona_14b_lora \
--bf16 \
--plot_loss \
--trust_remote_code True \
--ddp_timeout 180000000 \
--include_num_input_tokens_seen True \
--optim adamw_torch \
--lora_rank 32 \
--lora_alpha 64 \
--lora_dropout 0.05 \
--lora_target all