Chapter 10
案例:4090 上训一个 Llama-3-8B 中文对话模型
把前面 9 章的能力串起来,跑一个完整实战:在一张 4090(24G)上,用 Unsloth 把 unsloth/llama-3-8b-bnb-4bit
训成一个对中文友好的 chat 模型。本章给完整命令、训练曲线、和部署到 Ollama 的步骤。
10.1目标
| 指标 | 目标 |
|---|---|
| 资源 | 1 × 4090 24G + 32 GB RAM |
| 训练时间 | ~ 8-16 小时 |
| 模型规模 | Llama-3-8B QLoRA |
| 训练数据 | 5-10 万条中文指令 |
| 最终产物 | GGUF 量化模型,可用 Ollama 跑 |
| 预期效果 | 中文对话流畅、不串英文、不犯低级错误 |
10.2总流程
flowchart LR
A([Llama-3-8B base]) --> B[阶段 1
准备中文 SFT 数据
~ 1 小时] B --> C[阶段 2
SFT 训练
~ 8-16 小时] C --> D[阶段 3
测试 + 合并 LoRA
~ 30 分钟] D --> E[阶段 4
导出 GGUF + Ollama
~ 30 分钟] E --> F([部署完成])
准备中文 SFT 数据
~ 1 小时] B --> C[阶段 2
SFT 训练
~ 8-16 小时] C --> D[阶段 3
测试 + 合并 LoRA
~ 30 分钟] D --> E[阶段 4
导出 GGUF + Ollama
~ 30 分钟] E --> F([部署完成])
10.3阶段 1:准备中文数据
有几个公开中文 SFT 数据集可选:
| 数据集 | 规模 | 风格 |
|---|---|---|
silk-road/alpaca-data-gpt4-chinese | 52K | alpaca 格式 + GPT-4 增强 |
BelleGroup/train_2M_CN | 200 万 | 多任务中文指令 |
YeungNLP/firefly-train-1.1M | 110 万 | 23 类任务 |
shareAI/CodeChat-Chinese | 50K | 代码 + 中文对话 |
Skywork/Skywork-Reward-Preference-80K-v0.2 | 80K 偏好 | 给 DPO 用 |
本案例用 silk-road/alpaca-data-gpt4-chinese(5 万条质量好的)+ 自己写的 1000 条"身份认同"数据。
数据预处理脚本
from datasets import load_dataset, Dataset
import json
# 加载公开数据
zh_alpaca = load_dataset("silk-road/alpaca-data-gpt4-chinese", split="train")
# 加载自己的身份数据(让模型知道自己叫"小助手")
with open("identity.json") as f:
identity_data = json.load(f)
identity_ds = Dataset.from_list(identity_data)
# 合并
from datasets import concatenate_datasets
combined = concatenate_datasets([zh_alpaca, identity_ds])
combined = combined.shuffle(seed=3407)
# 转 chat 格式
def to_chat_messages(example):
return {
"messages": [
{"role": "user", "content": example["instruction"] + ("\n" + example["input"] if example.get("input") else "")},
{"role": "assistant", "content": example["output"]},
]
}
combined = combined.map(to_chat_messages, remove_columns=combined.column_names)
combined.save_to_disk("./zh_sft_data")
10.4阶段 2:SFT 训练
完整训练脚本 train.py
from unsloth import FastLanguageModel, is_bfloat16_supported
from unsloth.chat_templates import get_chat_template, train_on_responses_only
import torch
from datasets import load_from_disk
from trl import SFTTrainer
from transformers import TrainingArguments
# === 1. 模型 + LoRA ===
max_seq_length = 2048
model, tokenizer = FastLanguageModel.from_pretrained(
"unsloth/llama-3-8b-bnb-4bit",
max_seq_length = max_seq_length,
dtype = None,
load_in_4bit = True,
)
model = FastLanguageModel.get_peft_model(
model,
r = 32, # 中文这种"大调整"用大点的 r
target_modules = ["q_proj","k_proj","v_proj","o_proj",
"gate_proj","up_proj","down_proj"],
lora_alpha = 32,
lora_dropout = 0,
bias = "none",
use_gradient_checkpointing = "unsloth",
random_state = 3407,
)
# === 2. tokenizer + chat template ===
tokenizer = get_chat_template(tokenizer, chat_template="llama-3")
def format_messages(examples):
texts = [
tokenizer.apply_chat_template(m, tokenize=False, add_generation_prompt=False)
for m in examples["messages"]
]
return {"text": texts}
# === 3. 数据 ===
dataset = load_from_disk("./zh_sft_data")
dataset = dataset.map(format_messages, batched=True, num_proc=4)
print(f"训练样本数: {len(dataset)}")
print(f"样例: {dataset[0]['text'][:500]}")
# === 4. trainer ===
trainer = SFTTrainer(
model = model,
tokenizer = tokenizer,
train_dataset = dataset,
dataset_text_field = "text",
max_seq_length = max_seq_length,
dataset_num_proc = 4,
packing = True, # ★ 短数据训长上下文必开
args = TrainingArguments(
per_device_train_batch_size = 2,
gradient_accumulation_steps = 4, # 等效 batch=8
warmup_steps = 100,
num_train_epochs = 3, # 3 轮够了
learning_rate = 2e-4,
bf16 = is_bfloat16_supported(),
fp16 = not is_bfloat16_supported(),
logging_steps = 10,
optim = "adamw_8bit",
weight_decay = 0.01,
lr_scheduler_type = "cosine",
seed = 3407,
output_dir = "outputs",
save_strategy = "steps",
save_steps = 500,
save_total_limit = 3,
report_to = "wandb",
),
)
# === 5. 只对 assistant 部分计 loss ===
trainer = train_on_responses_only(
trainer,
instruction_part = "<|start_header_id|>user<|end_header_id|>\n\n",
response_part = "<|start_header_id|>assistant<|end_header_id|>\n\n",
)
# === 6. 开训 ===
trainer.train()
# === 7. 保存 LoRA adapter ===
model.save_pretrained("lora_model")
tokenizer.save_pretrained("lora_model")
启动 + 监控
python train.py 2>&1 | tee train.log
训练过程中你应该看到(4090 / Llama-3-8B / r=32 / packing):
- peak GPU memory:~ 18 GB;
- 单 step 时间:~ 1.5 秒;
- 3 epochs × 5 万样本 / packing → 总 step 约 8000-12000;
- 总训练时间:~ 4-6 小时;
- loss 从 ~1.8 → ~0.5(中文数据上 loss 范围)。
10.5阶段 3:测试 + 合并 LoRA
跑几个测试 prompt
from unsloth import FastLanguageModel
from unsloth.chat_templates import get_chat_template
model, tokenizer = FastLanguageModel.from_pretrained(
"unsloth/llama-3-8b-bnb-4bit",
max_seq_length=2048, load_in_4bit=True,
)
# 加载训好的 LoRA
model.load_adapter("lora_model")
tokenizer = get_chat_template(tokenizer, chat_template="llama-3")
# 推理模式
FastLanguageModel.for_inference(model)
prompts = [
"你是谁?你叫什么名字?",
"用三句话解释什么是相对论",
"写一首关于秋天的五言绝句",
"我家猫不爱吃饭,怎么办?",
]
for q in prompts:
messages = [{"role": "user", "content": q}]
inputs = tokenizer.apply_chat_template(messages, tokenize=True, add_generation_prompt=True, return_tensors="pt").to("cuda")
outputs = model.generate(input_ids=inputs, max_new_tokens=512, temperature=0.8, top_p=0.9, do_sample=True)
print("Q:", q)
print("A:", tokenizer.decode(outputs[0][inputs.shape[-1]:], skip_special_tokens=True))
print("-" * 60)
预期:
- "你叫什么名字?" → 回答自己叫"小助手"(如果有 identity 数据);
- 中文回答流畅,不夹英文;
- 古诗题能写出押韵的回答;
- 生活类问题答得有同理心。
合并 LoRA 到完整模型
# 在脚本里
model.save_pretrained_merged(
"merged_16bit",
tokenizer,
save_method="merged_16bit", # 完整 fp16;也可 "merged_4bit" 保持 4bit
)
得到 ./merged_16bit/ 是标准 HF 模型目录(约 16 GB),可以:
transformers.AutoModelForCausalLM.from_pretrained()加载;- vLLM 起 OpenAI 兼容 API;
- llama.cpp 转 GGUF 部署到 Ollama。
10.6阶段 4:导出 GGUF 给 Ollama 部署
Unsloth 自带 GGUF 导出,一行命令完成:
model.save_pretrained_gguf(
"llama3_zh_chat_q4",
tokenizer,
quantization_method = "q4_k_m", # 最常用的 4bit 量化
)
会生成 ./llama3_zh_chat_q4/unsloth.Q4_K_M.gguf(约 5 GB),是 llama.cpp / Ollama 直接能跑的格式。
Ollama 部署
写 Modelfile:
# Modelfile
FROM ./llama3_zh_chat_q4/unsloth.Q4_K_M.gguf
TEMPLATE """{{ if .System }}<|start_header_id|>system<|end_header_id|>
{{ .System }}<|eot_id|>{{ end }}<|start_header_id|>user<|end_header_id|>
{{ .Prompt }}<|eot_id|><|start_header_id|>assistant<|end_header_id|>
"""
SYSTEM """你是小助手,一个友好的中文 AI 助手。"""
PARAMETER stop "<|eot_id|>"
PARAMETER stop "<|end_of_text|>"
PARAMETER temperature 0.8
PARAMETER top_p 0.9
注册 + 跑:
ollama create llama3-zh -f Modelfile
ollama run llama3-zh
>>> 你好,介绍一下你自己
我是小助手,一个友好的中文 AI 助手……
整个模型在本地跑,不联网、不发数据,是 Ollama + GGUF 流的精髓。
10.7验证 / 评测
用一份小型评测集(自己准备 100 道题,每道题对照 base 模型和训完模型的回答)做 side-by-side:
| 维度 | base Llama-3-8B | 训完 8B |
|---|---|---|
| 中文流畅度 | 有时夹英文 | 纯中文 |
| 身份认知 | 说自己是 Meta 的 Llama | 说自己是小助手 |
| 古诗写作 | 能写但不押韵 | 押韵 / 平仄基本对 |
| 生活闲聊 | 偏冷淡 | 有温度 |
| 代码 / 数学 | 没退化 | 没退化 |
常见量化评测:用 opencompass / lm-eval-harness 跑 CMMLU / C-Eval / MMLU。
10.8这个 case 的成本
| 项 | 金额 |
|---|---|
| 电费(4090 跑 12 小时 × 0.45 kWh × 0.7 元/度) | ~ 4 元 |
| OpenAI API(如果用 GPT-4 蒸馏数据) | ~ 100 元 |
| 本地 SSD 存储 | ~ 30 GB |
| 人力 | 1 天 |
| 总计 | ~ 100 元 + 1 天 |
对比:"用 LLaMA-Factory + DeepSpeed 在 8 卡 A100 上训"的云成本 = ~ $200/小时 × 6 小时 = $1200,差两个数量级。 这就是 Unsloth 对个人 / 小团队的革命性意义。
10.9把这个 case 发散
| 诉求 | 改造 |
|---|---|
| 训行业模型(法律 / 医疗) | 换数据集;data 准备时加领域语料 |
| 训代码模型 | 用 codealpaca 数据;max_seq_length 升 4-8K |
| 训长上下文 | 第 7 章配置;用长文档数据 |
| 训完做 DPO 偏好 | 第 8.3 章 + 偏好数据集 |
| 训 GRPO 推理模型 | 第 8.4 章 + 数学 / 代码可验证 reward |
| 多模态 VLM | 用 FastVisionModel;数据集要带 image |
10.10这章带走的
- 4090 单卡 + 1 天能完成 Llama-3-8B 中文化全流程;
- 关键三步:FastLanguageModel + chat_template + train_on_responses_only;
- 训完用
save_pretrained_gguf一行导出,Ollama 直接装; - 同样的 case 成本约 100 元 + 1 天,比云端方案省两个数量级;
- "喂少量 identity 数据 + 大量公开 SFT" 是最简单的领域适配模板。