开源大模型部署与微调深度指南

1 阅读35分钟

基于 Datawhale self-llm 项目的系统性学习笔记 重点:技术原理 + 工程经验,不是教程链接索引 更新日期:2026-04-27


目录


第一章 大模型工程的全景认知

1.1 你在和什么打交道

一个开源大模型的本质,是一个 几 GB 到几百 GB 的权重文件 + 一份模型架构代码。当我们说"用 Qwen2.5-7B",实际是:

  • 权重(weights):约 14 GB(FP16)或 7 GB(INT8)的 .safetensors 文件,存储 70 亿个浮点参数
  • 架构代码:几百行 Python,定义 Transformer 的层结构(attention、FFN、layernorm)
  • tokenizer:把文本切成 token id 的词表与规则
  • 配置文件(config.json):告诉加载器层数、隐藏维度、词表大小等

这四样齐了,你就能"复现"这个模型。HuggingFace transformers 库做的就是把这套加载流程标准化。

1.2 为什么"开源大模型"这件事重要

闭源 API(GPT-4、Claude 等)和开源大模型的本质差异不在能力,而在 三件事:

  1. 数据私有性:你的 prompt 和数据不会上传到任何第三方
  2. 成本曲线:闭源按 token 计费,开源按 GPU 时计费。批量任务到一定规模后开源更便宜
  3. 可定制性:你可以微调,可以改架构,可以蒸馏。闭源给不了

代价是:你得自己搞定部署、显存优化、推理加速、监控运维这一整套。self-llm 项目主要就是把"自己搞定"这部分的门槛降到最低。

1.3 全链路工作流

一个完整的开源大模型项目,从零到上线一般经过这些阶段:

[1] 选模型      → 看任务类型 + 显存预算 + 中英文需求
[2] 下载权重    → ModelScope / HuggingFace
[3] 跑通推理    → transformers 直接 .generate(),验证模型没坏
[4] 部署服务    → FastAPI / vLLM,暴露成 HTTP API
[5] 准备数据集  → 收集/清洗/格式化为 instruction 格式
[6] 微调        → LoRA/QLoRA,产出 adapter
[7] 合并部署    → 微调后的模型再走一次部署
[8] 应用集成    → 接 LangChain、做 RAG、串 Agent
[9] 评估迭代    → 看效果、调数据、再训

绝大多数人卡在 [4] 和 [6] 之间——能跑但不会优化、能微调但效果差。本文重点就在这两段。


第二章 环境与硬件:你需要知道的底层事实

2.1 显存到底花在哪里

很多人对"7B 模型要 16GB 显存"这种说法只有模糊印象。实际拆解:

推理阶段的显存占用 = 权重 + KV Cache + 激活值

  • 权重:参数量 × 精度字节数。7B × 2(FP16) = 14 GB
  • KV Cache:每个 token 的 attention key/value 都要缓存供后续 token 使用。公式约为 2 × num_layers × hidden_dim × seq_len × batch × 2 字节。Qwen2.5-7B 在 4K 上下文、batch=1 时约 1-2 GB,8K 上下文翻倍
  • 激活值:中间层的临时张量,约 0.5-1 GB

所以 7B 推理需要 ~16-20 GB,不是 14 GB。长上下文会让 KV Cache 暴涨,这是后面 vLLM PagedAttention 要解决的核心问题。

训练阶段显存占用 = 权重 + 梯度 + 优化器状态 + 激活值

用 AdamW 全量微调 7B:

  • 权重(FP16):14 GB
  • 梯度(FP16):14 GB
  • 优化器状态(FP32 一阶+二阶动量):56 GB(AdamW 是显存大户)
  • 激活值(取决于 batch 和 seq):10-30 GB

合计 80-120 GB,所以全量微调 7B 模型需要 A100 80GB 或多卡。

LoRA 把可训练参数压到 1% 以下,梯度和优化器状态也跟着缩水到 1%,这就是为什么 LoRA 能在 24GB 显卡上训 7B。

2.2 精度的取舍

精度字节/参数7B 模型大小损失用途
FP32428 GB训练老式模型,现在很少用
FP16214 GB极小推理标准
BF16214 GB极小,数值范围更广训练标准(更稳定)
INT817 GB推理省显存
INT4(NF4)0.53.5 GB中等QLoRA、极限部署

关键认知:训练用 BF16 比 FP16 更稳(梯度不容易溢出),推理用 FP16 更通用。INT4 量化后模型本身效果会有可感知的下降,但配合 QLoRA 训练 adapter 可以补回来很多。

2.3 国内环境的隐性成本

国内做大模型,最大的隐性成本是网络。三个关键替换:

模型下载:HuggingFace → ModelScope。这不是镜像,是国内独立仓库,Qwen/InternLM/GLM/Baichuan 等国内模型的官方发布地。

from modelscope import snapshot_download
model_dir = snapshot_download('Qwen/Qwen2.5-7B-Instruct', cache_dir='./models')

pip 源:换清华源,大文件下载速度差 10-100 倍。

pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

conda 源:同理,换清华或中科大。改 ~/.condarc 即可。

HuggingFace 必须用的情况(比如 Llama 系):设环境变量走镜像。

export HF_ENDPOINT=https://hf-mirror.com

2.4 没有显卡怎么办

三条可行路线:

  1. AutoDL 按小时租(国内主流):3090(24GB)约 ¥1.5/小时,4090 约 ¥2-3/小时。self-llm 的教程几乎都在 AutoDL 上验证过,有现成镜像
  2. Colab Pro:T4(16GB)免费,A100 收费。适合学习和小实验
  3. CPU 推理:仅限 1.8B 以下小模型(Qwen-1.8B、MiniCPM-2B)。能跑但慢,适合演示而非生产

关键经验:学习阶段不要买显卡。先租,跑通流程后再决定。一张 4090 ¥15000 起,租上 100 小时才 ¥300。


第三章 模型部署的四种范式

部署的本质是把"加载模型 + 推理"包装成一个可被外部调用的服务。四种范式各有适用场景。

3.1 范式一:直接 transformers 调用

最朴素的方式,适合学习、调试、Notebook 实验

核心三步:

  1. AutoTokenizer.from_pretrained(path) 加载分词器
  2. AutoModelForCausalLM.from_pretrained(path, torch_dtype=torch.float16, device_map="auto") 加载模型
  3. model.generate(input_ids, max_new_tokens=512) 生成

优点:代码透明,容易调试,改 prompt 立竿见影。 缺点:每次请求都要走一次完整的 generate,吞吐低,无法批处理多个并发请求。 何时用:学新模型、复现论文、做 demo。不要用于生产

3.2 范式二:FastAPI 包装

把上面的 transformers 调用包成 HTTP 接口。本质是单进程 Python 服务

工程上要注意:

  • 模型只加载一次:在 FastAPI 的 startup 事件里加载,放进全局变量。每次请求都加载模型是新手最常见错误,会导致每次响应几十秒
  • 流式输出:用 StreamingResponse 配合 TextIteratorStreamer,前端能逐字看到回复,体验好得多
  • 并发处理能力差:Python GIL + 模型 forward 是同步的,FastAPI 看似支持 async,但 GPU 推理无法真并行。多用户同时访问会排队

何时用:小规模内部工具、单用户场景、原型验证。 何时弃用:并发超过 5 个请求/秒、要求低延迟、要做生产环境。

3.3 范式三:WebDemo(Streamlit / Gradio)

StreamlitGradio 是把模型变成可交互网页的两个 Python 框架。一个 Python 文件、几十行代码就能出一个 ChatGPT 风格的 Web 界面。

差异:

  • Gradio:HuggingFace 亲儿子,组件多、ML 专属、gr.ChatInterface 一行就出聊天框
  • Streamlit:更通用的数据可视化框架,做 dashboard 强,做对话界面要自己写 session_state 管理

何时用:给非技术同事做 demo、内部展示、PoC。它不是生产部署——单进程、单用户体验、刷新就丢历史。

3.4 范式四:vLLM(生产推荐)⭐

vLLM 是伯克利团队开源的高性能推理引擎,比 transformers 直接推理快 2-24 倍(取决于负载)。它解决的核心问题:

  1. 多请求并发:连续批处理(continuous batching)而非静态批处理
  2. 长上下文显存:PagedAttention 让 KV Cache 不再占大块连续显存
  3. OpenAI API 兼容:启动后直接当 OpenAI API 用,无缝接入 LangChain、OpenAI SDK

启动一行命令:

python -m vllm.entrypoints.openai.api_server \
  --model /path/to/Qwen2.5-7B-Instruct \
  --served-model-name qwen2.5 \
  --max-model-len 8192 \
  --gpu-memory-utilization 0.9 \
  --tensor-parallel-size 1

关键参数详解:

参数作用调参建议
--max-model-len上下文最大长度越长 KV Cache 越大,按需设。8K 够用就别开 32K
--gpu-memory-utilizationvLLM 占用显存比例默认 0.9,显卡跑别的东西时降到 0.85
--tensor-parallel-size张量并行的 GPU 数单卡填 1,4 卡填 4。要求 GPU 数能被注意力头数整除
--quantization量化方式awqgptqfp8 等。需要量化后的模型权重
--enforce-eager关闭 CUDA Graph调试用。生产关掉(速度更快)

何时用:生产环境、多用户并发、API 服务、对吞吐有要求的批处理任务。

3.5 部署方式选择决策

场景推荐
跑 Notebook 学习transformers 直接调用
给团队做内部小工具FastAPI
给老板/客户演示Gradio WebDemo
公司内部生产 APIvLLM
多卡服务大模型vLLM + tensor parallel
极低显存设备transformers + INT4 量化(甚至 llama.cpp)

第四章 vLLM 为什么快:PagedAttention 原理

理解 vLLM 的核心创新,能帮你判断什么时候它真的快、什么时候没用。

4.1 传统推理的显存浪费

传统 attention 实现里,每个请求的 KV Cache 是连续显存块。问题:

  • 你不知道这个请求会生成多长,只能按 max_seq_len(比如 4096)预分配
  • 实际生成 200 个 token 就停了,剩下 3896 个 token 的显存白占
  • 多请求时,就算总显存够用,也可能因为碎片化而塞不下新请求

实测下来,传统方式 KV Cache 的有效利用率只有 20-40%

4.2 PagedAttention 的解法

借鉴操作系统虚拟内存的"分页"思想:

  • 把 KV Cache 切成固定大小的 block(比如每 16 个 token 一块)
  • 每个请求维护一张"page table",指向它实际占用的 block
  • block 可以不连续地分散在显存里
  • 请求结束后 block 立即归还到池子,可被下一个请求复用

效果:KV Cache 利用率提升到 96%+,同等显存能装 2-4 倍的并发请求。

4.3 连续批处理(Continuous Batching)

传统批处理(static batching):凑够 N 个请求一起跑,跑完一起返回。慢请求拖慢快请求。

vLLM 的连续批处理:每生成一个 token 就检查一次 batch

  • 短请求生成完了立即返回,空出来的位置马上让新请求加入
  • batch size 是动态变化的
  • 整体 GPU 利用率拉满

这就是为什么 vLLM 在高并发场景下吞吐暴增。但单请求场景下,vLLM 不会比 transformers 快多少——核心收益来自并发。

4.4 何时 vLLM 不够快

  • 单用户、单请求:vLLM 优势小,transformers 已经够快
  • 超长上下文(128K+):KV Cache 仍然是显存瓶颈,需要进一步的分块/卸载技术
  • 低端显卡(T4、3060):CUDA Graph 等优化对老架构不太友好

第五章 微调的本质与方法论

5.1 微调到底改了什么

预训练模型 = "见过半个互联网的语言专家",但它不知道你要它做什么具体任务。微调 = 在预训练模型上继续训练,让它学会特定的任务格式或领域知识

技术上,微调就是用新数据继续做梯度下降,只是:

  • 学习率更小(预训练 1e-4 → 微调 1e-5 / 5e-5)
  • 数据量小得多(预训练 TB 级,微调 几 MB 到几 GB)
  • 训练步数少(几百到几万步,而非几亿步)

5.2 微调能做什么、不能做什么

能做的:

  • 调整输出格式(一定要 JSON、一定要带 emoji、一定要先思考再回答)
  • 学新风格(模仿某个作家、模仿客服话术)
  • 强化某个领域(法律、医疗、代码)的术语和回答模式
  • 减少不该有的拒答(over-refusal)
  • 让模型学会调用特定工具的格式

做不到的:

  • 教模型新知识:LoRA 改的参数太少,塞不进新事实。新知识应该用 RAG 解决,不是微调
  • 大幅提升通用能力:微调反而经常降低模型的通用能力(灾难性遗忘)
  • 替代提示工程:能用 prompt 解决的别用微调,微调成本高得多

核心判断:如果你的需求能通过"写更好的 prompt"或"加一段示例"解决,不要微调

5.3 指令微调的数据格式

self-llm 里所有微调教程用的格式都是 Alpaca 风格:

{
  "instruction": "把下面这句话翻译成英文",
  "input": "今天天气不错",
  "output": "The weather is nice today."
}

三个字段的语义:

  • instruction:任务描述,告诉模型该做什么
  • input:任务的输入数据(可为空)
  • output:期望的标准答案

训练时会被拼接成:

### Instruction:
把下面这句话翻译成英文

### Input:
今天天气不错

### Response:
The weather is nice today.

模型学习的是"看到 Instruction + Input 后,输出 Response 部分"。loss 只计算 Response 部分,这是关键——否则模型会把 instruction 部分也作为生成目标,学歪。

5.4 微调方法对比

方法可训参数比例7B 显存速度效果适用
全量微调100%80GB+最好数据多、显卡够
LoRA0.1-1%~20GB接近全量首选
QLoRA0.1-1%~6GB较快接近 LoRA个人电脑
P-tuning v2<0.1%~16GB最快一般任务特定 prompt
Adapter~1%类似 LoRA中等较好较老的方法,被 LoRA 取代

实际上,90% 的场景你都应该用 LoRA 或 QLoRA。其他方法要么太贵(全量),要么效果不够(P-tuning)。


第六章 LoRA 深度解析

LoRA(Low-Rank Adaptation)是 2021 年微软提出的方法,现在已经是开源大模型微调的事实标准。理解它的原理对调参至关重要。

6.1 核心思想:低秩分解

观察:模型在微调时,参数变化(ΔW)其实是低秩的——也就是说,虽然 W 是个 4096×4096 的大矩阵,但 ΔW 用两个小矩阵的乘积就能表达得差不多。

LoRA 的做法:

原始:    h = W·x                  (W 是 d×d,被冻结)
LoRA:    h = W·x + B·A·x          (A 是 r×d, B 是 d×r,只训练 A 和 B)

其中 r 远小于 d(比如 d=4096, r=8)。可训练参数从 d²=16M 降到 2·d·r=64K,减少 250 倍

推理时,可以把 B·A 加回 W(merge),没有额外开销;也可以保持分离,方便切换不同任务的 adapter。

6.2 关键超参

from peft import LoraConfig

config = LoraConfig(
    r=8,                          # 秩,核心超参
    lora_alpha=32,                # 缩放因子
    lora_dropout=0.05,            # dropout,防过拟合
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],  # 哪些层加 LoRA
    bias="none",                  # bias 是否训练
    task_type="CAUSAL_LM"         # 因果语言模型
)

r(秩)的选择:

  • r=4-8:简单任务(格式调整、风格模仿)。够用且省
  • r=16-32:中等任务(领域适配、多任务)
  • r=64+:复杂任务、大数据集。容易过拟合,需要更多正则化

经验:先用 r=8 跑一遍,效果不够再加。盲目加大 r 会过拟合还浪费显存。

lora_alpha(缩放):

  • 实际加到原权重上的缩放系数是 lora_alpha / r
  • 常见组合:r=8, alpha=16r=8, alpha=32(系数=2 或 4)
  • 系数越大 LoRA 影响越强,但太大会让模型偏离原始能力

target_modules(目标层):

  • 最常见:q_proj, k_proj, v_proj, o_proj(注意力的四个投影)
  • 加 FFN:再加 gate_proj, up_proj, down_proj(效果略好,显存翻倍)
  • 全部加:用 target_modules="all-linear"(peft 0.7+ 支持)

经验:注意力四个 + FFN 三个 = 效果和成本最佳平衡点。

6.3 LoRA 训练完整代码骨架

from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments, Trainer
from peft import LoraConfig, get_peft_model
from datasets import load_dataset

# 1. 加载模型和 tokenizer
model_path = "./models/Qwen2.5-7B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_path)
model = AutoModelForCausalLM.from_pretrained(
    model_path,
    torch_dtype=torch.bfloat16,   # 训练用 BF16
    device_map="auto"
)

# 2. 套上 LoRA
lora_config = LoraConfig(
    r=8, lora_alpha=32, lora_dropout=0.05,
    target_modules=["q_proj","k_proj","v_proj","o_proj","gate_proj","up_proj","down_proj"],
    task_type="CAUSAL_LM"
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# 输出类似:trainable params: 20M || all params: 7B || trainable%: 0.28%

# 3. 准备数据(格式化函数)
def format_example(example):
    prompt = f"### Instruction:\n{example['instruction']}\n\n### Input:\n{example['input']}\n\n### Response:\n"
    full = prompt + example['output'] + tokenizer.eos_token
    tokens = tokenizer(full, truncation=True, max_length=1024, padding="max_length")
    # 关键:只对 Response 部分计算 loss
    prompt_len = len(tokenizer(prompt)["input_ids"])
    labels = tokens["input_ids"].copy()
    labels[:prompt_len] = [-100] * prompt_len   # -100 表示忽略
    tokens["labels"] = labels
    return tokens

dataset = load_dataset("json", data_files="train.json")["train"]
dataset = dataset.map(format_example)

# 4. 训练参数
args = TrainingArguments(
    output_dir="./lora-output",
    num_train_epochs=3,
    per_device_train_batch_size=2,
    gradient_accumulation_steps=8,    # 等效 batch size = 16
    learning_rate=2e-4,                # LoRA 学习率比全量大
    warmup_steps=100,
    logging_steps=10,
    save_strategy="epoch",
    bf16=True
)

trainer = Trainer(model=model, args=args, train_dataset=dataset)
trainer.train()

# 5. 保存(只存 adapter,~20MB)
model.save_pretrained("./lora-output/final")

6.4 LoRA 推理:两种姿势

姿势 1:动态加载 adapter(推荐)

from peft import PeftModel
base = AutoModelForCausalLM.from_pretrained("./models/Qwen2.5-7B-Instruct")
model = PeftModel.from_pretrained(base, "./lora-output/final")
# model 已经是带 LoRA 的版本,可以直接 .generate()

优点:可以在多个 adapter 之间切换(model.set_adapter("name")),适合多任务场景。 缺点:推理时多一次矩阵乘,有少量延迟。

姿势 2:合并(merge)

merged = model.merge_and_unload()
merged.save_pretrained("./qwen-merged")
# 之后这个模型就和普通模型一样,可以直接给 vLLM 部署

优点:推理零开销,可以直接给 vLLM 用。 缺点:产出一份完整大小的模型(14GB),失去了 LoRA "小巧"的好处。

生产建议:训练阶段保留 adapter(便于迭代),上线阶段 merge 后给 vLLM 部署。

6.5 LoRA 调参经验

现象原因解决
Loss 不降学习率太小、target_modules 太少lr 调到 2e-4、target 加 FFN
Loss 降但效果差数据质量差、epoch 不够检查数据、训 3-5 epoch
训练中 loss 跳变学习率太大lr 减半,加 warmup
微调后通用能力下降灾难性遗忘数据混入通用对话样本、降低 epoch
过拟合(train↓ val↑)数据少 r 大r 调小、加 dropout、减 epoch

第七章 QLoRA:6GB 显存训练 7B 模型的秘密

QLoRA = 4bit 量化 + LoRA,2023 年华盛顿大学提出,让普通显卡也能微调大模型。

7.1 它怎么省下显存的

回忆全量微调 7B 的显存账(80-120 GB)。LoRA 已经把梯度+优化器状态压到 1%,剩下大头是冻结的权重本身(14 GB FP16)。

QLoRA 的招:把冻结的权重量化到 4bit

  • 14 GB FP16 → 3.5 GB INT4(NF4),省下 10 GB
  • LoRA adapter 仍然是 BF16 训练,精度不受影响
  • 反向传播时 4bit 权重会反量化(dequantize)成 BF16 参与计算

最终 7B 模型微调只需:

  • 量化权重:3.5 GB
  • LoRA 参数+梯度+优化器:0.2 GB
  • 激活值:1-2 GB
  • 总计 5-6 GB

7.2 NF4 是什么

普通 INT4 是均匀量化(把浮点范围切成 16 等份),但神经网络权重的分布是正态分布,均匀量化会浪费表达力。

NF4(NormalFloat 4-bit):专为正态分布设计的非均匀 4bit 编码,16 个量化点不是等距,而是按正态分布的分位数选取。同样 4bit 下,信息保留比 INT4 好得多。

QLoRA 用 NF4 量化权重,几乎不损失模型质量

7.3 双重量化(Double Quantization)

量化时除了存量化值,还要存"缩放因子"(scale)。每 64 个权重一个 FP32 scale,这本身也占空间。

QLoRA 把这些 scale 再量化一次(8bit),进一步省 0.4 GB/模型。

7.4 分页优化器(Paged Optimizer)

训练时偶尔会出现峰值显存(梯度累积、长序列),容易 OOM。QLoRA 用 NVIDIA 的统一内存,把优化器状态在 GPU↔CPU 之间自动调度,峰值时溢出到 CPU 内存,不让进程崩。

7.5 QLoRA 代码改动很小

相比 LoRA,只多两行:

from transformers import BitsAndBytesConfig

bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=True
)

model = AutoModelForCausalLM.from_pretrained(
    model_path,
    quantization_config=bnb_config,    # ← 新增
    device_map="auto"
)

from peft import prepare_model_for_kbit_training
model = prepare_model_for_kbit_training(model)    # ← 新增

# 之后 LoraConfig 和训练流程完全一样

7.6 QLoRA 的代价

不是免费午餐:

  • 训练速度比 LoRA 慢 30-40%(每次 forward 都要反量化)
  • 效果比纯 LoRA 略差(量化损失)
  • 必须装 bitsandbytes,Windows 上一直有兼容问题(WSL 或 Linux 优先)

判断:24GB+ 显卡用 LoRA;6-16GB 显卡用 QLoRA;再小就别想 7B 模型了,换 1.5B 起步。


第八章 P-tuning 与全量微调

8.1 P-tuning v2:别忽视的小众选手

P-tuning 不改模型权重,只在每一层 attention 前插入若干"软 prompt" token(可训练的 embedding 向量)。这些 token 不对应真实词汇,纯粹是给模型加一个学习到的"任务信号"。

优势:

  • 可训练参数比 LoRA 还少 10 倍(0.01% 量级)
  • 不动原模型,推理时模型完全可复用
  • 多任务时,每个任务一组 prompt token,切换零成本

劣势:

  • 表达能力比 LoRA 弱,复杂任务效果不如 LoRA
  • 对超参更敏感(prompt 长度、初始化方式)
  • 社区生态不如 LoRA 活跃

适用场景:任务很简单(分类、抽取)、数据少、想保留原模型不变。

8.2 全量微调:什么时候才值得

全量微调把所有参数都参与训练。它需要的资源:

  • 7B 模型 + AdamW:80GB+ 显存,通常需要 A100 或多卡分布式
  • 数据要求:至少几万条高质量样本,否则比不过 LoRA
  • 训练时间:LoRA 1 小时能跑完的 epoch,全量要 3-5 小时

只在以下情况用全量:

  1. 你有大量(10万+)高质量数据
  2. 你要让模型彻底学会一个新领域(医疗、法律等),不只是格式调整
  3. 你做的是继续预训练(continued pretraining),给模型补充新知识
  4. 你要做后续的指令微调对齐(SFT 阶段),作为 RLHF 的基础

普通业务场景 99% 不需要全量。如果你以为需要,先用 LoRA r=64 试一遍,大概率够。

8.3 分布式训练简介

数据集大、模型大时需要多卡。常见三种并行:

  • 数据并行(DP/DDP):每张卡有完整模型副本,数据切分。要求显存能装下完整模型
  • 张量并行(TP):把单层的矩阵乘切到多卡。降低单卡显存,通信成本高
  • 流水线并行(PP):不同层放不同卡,像流水线一样传递。适合超大模型

实操工具:

  • 简单场景:accelerate launch --multi-gpu 一键 DDP
  • 复杂场景:DeepSpeed ZeRO(微软),自动切分优化器状态/梯度/权重到多卡,显存极致优化
  • 极致场景:Megatron-LM(NVIDIA),3D 并行(DP+TP+PP)

self-llm 里全量微调和大模型微调用的多是 DeepSpeed ZeRO-2 或 ZeRO-3。


第九章 微调数据集构建方法论

数据决定上限,模型只决定下限。这句话在微调场景百分百适用。

9.1 数据量的经验值

任务类型推荐样本数
格式调整(JSON 输出、固定模板)50-200
风格模仿(客服、特定语气)500-2000
单一领域问答(法律、医疗)2000-10000
多任务通用助手10000+
继续预训练100M+ token

少而精远胜多而杂。500 条人工精挑的数据 > 50000 条爬虫拼凑的脏数据。

9.2 数据质量的几个维度

多样性:

  • 任务类型多样(不要全是"翻译","翻译+总结+改写+问答"分布更好)
  • 输入长度多样(短问题、长文档都要有)
  • 领域多样(避免只在一个领域内反复)

正确性:

  • output 必须正确(模型会忠实地学错例)
  • 格式必须一致(JSON 就不要混 YAML)

真实性:

  • 不要全用 ChatGPT 生成(同质化严重,模型只会模仿 GPT 风格)
  • 至少 20-30% 用真实人类数据

9.3 数据构建的常用路径

路径 1:已有标注数据改造 公司里已有的 FAQ、客服记录、文档库,稍加清洗就是好数据。把"问题→答案"对转成 instruction 格式即可。

路径 2:大模型蒸馏 用 GPT-4 / Claude 生成训练数据,小模型微调后逼近大模型效果。注意:

  • 商业用途要看大模型 ToS(OpenAI 禁止用其输出训练竞争模型)
  • 要做去重和质检
  • 要混入人工数据避免过度同质

路径 3:开源数据集

  • 中文通用:BelleGroup、Alpaca-Chinese、Firefly
  • 中文垂域:DISC-Law(法律)、Huatuo(医疗)、Chinese-medical-dialogue
  • 英文:Alpaca、Dolly、OpenOrca、ShareGPT

直接用开源数据微调出来的模型效果有限,当作基础混入自有数据最佳。

路径 4:Self-Instruct(自我指令) 让模型基于少量种子样本自己生成训练数据。Stanford Alpaca 项目就是这么做的。代码不复杂,但质控难。

9.4 数据清洗清单

跑训练前过一遍:

  • 去重(完全重复、近似重复都要去)
  • 长度过滤(过短的低信息量、过长的容易 OOM)
  • 语言过滤(中文任务里夹杂英文样本要么去掉要么单独处理)
  • 敏感内容过滤(政治、色情、暴力,合规风险)
  • 格式校验(JSON 合法性、字段完整性)
  • 标注一致性(同一类问题答案风格要统一)

9.5 训练/验证集切分

不要把所有数据丢进训练。留 5-10% 作为验证集,训练时观察 val loss:

  • val loss 持续下降 → 还可以多训
  • val loss 开始上升 → 过拟合了,该停
  • val loss 不动 → 数据或学习率有问题

没有验证集就是盲训,模型是好是坏全靠玄学。


第十章 LangChain 与 RAG 的工程实现

10.1 为什么需要 RAG 而不是微调新知识

第五章提过:微调几乎不能给模型添加新事实。这是因为:

  • LoRA 改的参数太少,装不下大量新事实
  • 即使全量微调,事实知识在权重里是分布式存储的,边训边遗忘
  • 知识更新一次就要重训一次,代价巨大

RAG(Retrieval-Augmented Generation) 是另一条路:

  • 把知识存在外部向量数据库
  • 用户提问时,先检索相关文档,再把文档作为 context 喂给模型
  • 模型基于检索到的内容回答

优势:

  • 知识更新只要更新数据库,不用动模型
  • 能溯源(知道答案来自哪个文档)
  • 大幅减少幻觉(hallucination)

10.2 RAG 标准流程

[索引阶段,只做一次]
原始文档 → 切块(chunking) → embedding → 存入向量库

[查询阶段,每次问答]
用户问题 → embedding → 向量检索 Top-K → 拼到 prompt → LLM 生成

10.3 关键组件选型

Embedding 模型(决定检索质量,比 LLM 更关键):

  • 中文首选:BGE-M3bge-large-zh-v1.5(智源开源)
  • 中英双语:m3e-base
  • 英文:text-embedding-3-small(OpenAI)、bge-large-en

向量数据库:

  • 入门/小规模:Chroma(本地、零依赖)、FAISS(Facebook 出品,纯本地)
  • 生产:Milvus(分布式)、Qdrant(Rust,性能强)、Pinecone(SaaS)、Weaviate

切块策略(chunking)是最容易被忽视的关键:

  • 大小:300-1000 字符,太大检索不精准,太小上下文不完整
  • 重叠:相邻 chunk 之间留 50-100 字符重叠,避免句子被切碎
  • 按结构切:优先按段落、标题切,而不是固定字符数(LangChain 的 RecursiveCharacterTextSplitter)

10.4 LangChain 包装本地模型

LangChain 提供大量预制组件(检索器、提示模板、链式调用、agent 框架),但它的 LLM 抽象默认接 OpenAI。要用本地开源模型,有三种姿势:

姿势 1:走 vLLM 的 OpenAI 兼容 API(推荐)

from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
    base_url="http://localhost:8000/v1",
    api_key="dummy",                # vLLM 不校验
    model="qwen2.5"                 # 与 --served-model-name 一致
)

最简洁,vLLM 跑起来后所有 LangChain 工具都能用。

姿势 2:继承 LangChain 的 LLM 基类自己实现

from langchain.llms.base import LLM
class QwenLLM(LLM):
    def _call(self, prompt, **kwargs):
        # 调你自己的 transformers 推理代码
        ...

灵活但重复造轮子,除非有特殊需求否则不推荐。

姿势 3:用 HuggingFacePipeline LangChain 自带,适合不部署服务直接在 Python 里用。性能差,只适合调试。

10.5 一个最小 RAG 实现

from langchain_community.document_loaders import TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_openai import ChatOpenAI
from langchain.chains import RetrievalQA

# 1. 加载并切块
docs = TextLoader("knowledge.txt", encoding="utf-8").load()
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_documents(docs)

# 2. embedding 入库
embed = HuggingFaceEmbeddings(model_name="BAAI/bge-large-zh-v1.5")
vectordb = Chroma.from_documents(chunks, embed, persist_directory="./chroma_db")

# 3. 检索 + 生成
llm = ChatOpenAI(base_url="http://localhost:8000/v1", api_key="x", model="qwen2.5")
qa = RetrievalQA.from_chain_type(
    llm=llm,
    retriever=vectordb.as_retriever(search_kwargs={"k": 4}),
    return_source_documents=True
)

result = qa.invoke({"query": "公司报销流程是什么?"})
print(result["result"])
print("依据:", result["source_documents"])

50 行代码,一个能跑的私有知识库。

10.6 RAG 调优经验

检索不准是 99% 问题的根源,不是 LLM 的问题。

现象排查
答案完全不对检索结果里有没有正确文档?没有就是 retriever 问题
答案部分正确chunk 切得太碎,关键信息被切散
答案太啰嗦检索的 chunk 太多,prompt 里塞了无关信息
答案是模型自己编的LLM 没看检索结果。强化 system prompt:"只能基于以下资料回答,资料里没有就说不知道"

进阶技巧:

  • 混合检索:向量检索 + 关键词检索(BM25)取并集,中文场景效果显著
  • 重排序(rerank):检索 Top-20 后用专门的 reranker 模型(bge-reranker)精排到 Top-4
  • 查询改写:用 LLM 先把用户问题改写成更适合检索的形式
  • 多轮检索:复杂问题先分解成子问题,分别检索

第十一章 推理模型与多模态

11.1 推理模型(Thinking Models)是什么

2024 年底 OpenAI o1、DeepSeek R1 引领的新范式:模型在输出最终答案前,先输出一段"思考过程"。这段思考通常被特殊标签包裹(<think>...</think>),最终答案在标签外。

技术上,推理模型用 RL(强化学习)训练出"长链思考"能力。表现:

  • 数学、代码、复杂推理任务大幅提升
  • 单次推理 token 消耗暴增 5-50 倍(成本/延迟代价)
  • 简单问答没必要用,杀鸡用牛刀

11.2 自部署 R1 系列的现实

DeepSeek-R1 原版 671B(MoE),实际激活 37B,单机部署需要 8×H100,普通用户搞不定。

蒸馏版才是普通人的选择:

  • DeepSeek-R1-Distill-Qwen-1.5B / 7B / 14B / 32B
  • DeepSeek-R1-Distill-Llama-8B / 70B

把 R1 的"思考能力"蒸馏到了 Qwen/Llama 基座上,7B 蒸馏版数学能力超过 GPT-4o

11.3 部署蒸馏版的注意

python -m vllm.entrypoints.openai.api_server \
  --model ./DeepSeek-R1-Distill-Qwen-7B \
  --max-model-len 32768 \              # 思考链很长,要开大上下文
  --served-model-name r1-distill

调用时:

  • temperature 设 0.6(官方推荐,默认 1 会让思考链发散)
  • 不要加额外的 system prompt,模型自己就会思考

11.4 Qwen3 的双模式

Qwen3 系列(2025 出)集成了"思考模式"和"普通模式"在一个模型里。默认 enable_thinking=True,会先输出 <think>...</think> 再回答。

省 token 时关闭思考:

text = tokenizer.apply_chat_template(
    messages, tokenize=False, add_generation_prompt=True,
    enable_thinking=False        # ← 关键
)

或者在用户消息末尾加 /no_think(Qwen3 支持的特殊指令)。

11.5 多模态模型部署

视觉语言模型(VLM)的核心差异:输入除了文本,还有图像/视频。

Qwen2-VL 是当前开源 VLM 的代表,2B / 7B / 72B 三档。部署时:

from transformers import Qwen2VLForConditionalGeneration, AutoProcessor
processor = AutoProcessor.from_pretrained(model_path)
model = Qwen2VLForConditionalGeneration.from_pretrained(model_path, torch_dtype=torch.bfloat16)

messages = [{
    "role": "user",
    "content": [
        {"type": "image", "image": "path/to/image.jpg"},
        {"type": "text", "text": "图里有什么?"}
    ]
}]
text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = processor(text=[text], images=[image], return_tensors="pt")
out = model.generate(**inputs, max_new_tokens=512)

vLLM 也支持多模态(0.6+),启动命令一样,API 调用时图片用 base64 或 URL 传入。

显存注意:VLM 比同尺寸 LLM 吃显存,因为视觉编码器额外占用。Qwen2-VL-7B 推理建议 24GB 起步。

11.6 多模态微调的特殊性

VLM 微调时,通常冻结视觉编码器,只微调 LLM 部分。视觉编码器一般已经在大量图文对上预训练得很充分,继续训反而容易破坏对齐。

LoRA target_modules 只挑 LLM 里的层(q_proj 等),不要包括 visual 开头的模块。peft 库会按名字匹配,容易踩坑。


第十二章 工程踩坑全集

12.1 部署相关

坑 1:transformers 版本和模型不匹配 症状:KeyErrorAttributeError: ... has no attribute ...、tokenizer 加载失败。 原因:模型代码有时直接写在权重里(trust_remote_code=True),它依赖特定版本的 transformers。 解法:每个模型一个 conda 环境,严格按教程版本号装。Qwen2.5 推荐 transformers>=4.45,GLM-4-9B-chat-hf 要 >=4.46

坑 2:加载模型时 OOM,但显存看起来够 原因:device_map="auto" 默认尽量用 GPU,但加载过程中 CPU 也会临时占双倍内存。 解法:low_cpu_mem_usage=True 参数;或者先 from_pretrained(..., device_map="cpu").to("cuda")

坑 3:vLLM 启动卡很久 原因:第一次启动会编译 CUDA Graph,正常需要 1-3 分钟。 判断:看日志有没有动,有动就等。如果完全卡死,可能是模型路径错或显存不够,加 --enforce-eager 跳过编译。

坑 4:vLLM 输出和 transformers 不一致 原因:vLLM 的采样实现略有差异(主要是 top_p、repetition_penalty 数值边界处理)。 解法:对照设置,vLLM 推荐用 SamplingParams(temperature=0.7, top_p=0.8),和 transformers 默认值不同。

坑 5:FastAPI 流式输出乱码 原因:中文字符跨 token 边界,边解码边输出会切碎 UTF-8。 解法:用 TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True),它内部处理了边界。

12.2 微调相关

坑 6:LoRA 训练完推理用了原模型 症状:微调好像没生效,输出和 base 模型一样。 原因:加载推理时只加载了 base,没加 PeftModel。 解法:

# ❌ 错的
model = AutoModelForCausalLM.from_pretrained("./lora-output/final")
# ✅ 对的
base = AutoModelForCausalLM.from_pretrained("./Qwen2.5-7B-Instruct")
model = PeftModel.from_pretrained(base, "./lora-output/final")

坑 7:训练 loss 是 0 原因:label 全 -100(全被 mask 了)。检查 format_example 里 prompt_len 是不是算错了。 另一种原因:数据格式没处理好,模型输入和输出对不上。

坑 8:训练到一半显存炸 原因:之前 batch 都正常,某一个 batch 里有超长样本。 解法:数据预处理时严格 truncate;group_by_length=True 让相似长度的样本在一个 batch。

坑 9:bitsandbytes 在 Windows 上装不上 原因:bnb 官方不支持 Windows 原生。 解法:用 WSL2,或者用社区编译的 Windows 版(bitsandbytes-windows,但常年落后官方版本)。严肃训练就用 Linux

坑 10:微调后模型变蠢 症状:格式对了,但简单问答都答错。 原因:灾难性遗忘——只在窄领域数据上训得太多,模型忘了通用能力。 解法:

  • 数据里混入 10-30% 的通用对话(开源数据集如 BelleGroup 抽样)
  • 减少 epoch(3 epoch 通常够)
  • 降低学习率(2e-4 → 1e-4)
  • 减小 LoRA 影响范围(降 r、降 alpha)

12.3 应用集成相关

坑 11:RAG 检索 Top-K 全是无关内容 原因 1:embedding 模型不行,中文场景必须用 BGE 系列,别用 OpenAI ada。 原因 2:chunk 切得不合理,关键信息被切碎。 原因 3:索引时和检索时 embedding 模型不一致(改过模型但没重建索引)。

坑 12:LangChain 升级后代码全报错 LangChain 0.1 → 0.2 → 0.3 的 API 改动很激进,模块路径换了好几次。 解法:pip freeze | grep langchain 锁版本,生产环境不要轻易升级。

坑 13:模型一直循环输出同一句话 原因:repetition_penalty 没设,或者 prompt 模板拼错了让模型不知道何时停。 解法:repetition_penalty=1.05(不要设太高,1.2+ 会让输出诡异);检查 chat_template 是否带正确的 eos_token

12.4 数据相关

坑 14:训练数据里有泄漏 症状:模型在 val 集上 loss 极低,实际推理效果一般。 原因:val 集数据混进了 train 集(去重不彻底)。 解法:严格按内容哈希去重,而非只看 ID。

坑 15:用 GPT-4 生成的数据训练后模型有 GPT-4 风格 症状:模型回答都带"我作为一个 AI 助手...""根据你提供的信息...",一股 ChatGPT 味。 原因:模型忠实地模仿了 GPT-4 的话术习惯。 解法:数据生成时让 GPT-4 学指定风格;后期数据清洗去掉这类陈词滥调;混入人工编写的样本。


第十三章 选型决策树

把前面的所有内容总结成几张实操决策表。

13.1 我该选哪个模型?

任务是中文为主?
├─ 是 → Qwen2.5 系列(综合最强)、InternLM2/3(长文本强)、GLM-4(工具调用强)
└─ 否 → Llama3.1、Gemma2

任务是代码?
└─ DeepSeek-Coder-V2、Qwen2.5-Coder

任务是数学/逻辑?
└─ DeepSeek-R1-Distill-Qwen 系列(蒸馏版)

任务是图文理解?
└─ Qwen2-VL / Qwen3-VL

显存 < 8GB?
└─ MiniCPM-2B、Qwen2.5-1.5B、phi-3-mini

要做端侧部署?
└─ MiniCPM 系列(2.4B 非 embedding)

13.2 我该用哪种部署?

是个人 Notebook 实验?
└─ transformers 直接调

是给 1-3 个同事用的小工具?
└─ FastAPI

是给老板/客户看的演示?
└─ Gradio

是公司生产 API,日 PV > 1000?
└─ vLLM

是日 PV > 10万 的 C 端服务?
└─ vLLM 多卡 + 负载均衡 + 监控告警(超出 self-llm 范围,看 Triton/TGI)

显存 < 8GB 又必须本地跑?
└─ llama.cpp(CPU+少量 GPU 混合,GGUF 格式),不在 self-llm 范围

13.3 我该用哪种微调?

显存 ≥ 80GB 且数据 > 10万条?
└─ 可以考虑全量,不行就 LoRA r=64

显存 24-80GB?
└─ LoRA(首选)

显存 6-24GB?
└─ QLoRA(NF4)

显存 < 6GB?
└─ 别微调 7B 了,换 1.5B 起步,或者用 P-tuning

任务很简单(分类、抽取)?
└─ P-tuning v2 或 LoRA r=4

任务复杂(领域助手、多任务)?
└─ LoRA r=16-32,target_modules 包括 FFN

13.4 我该微调还是 RAG?

需求是"让模型知道一些它不知道的事实"?
└─ RAG(微调几乎做不到)

需求是"让模型按特定格式输出"?
└─ 微调(prompt 不稳定时)或者更好的 prompt(便宜)

需求是"模仿某种语言风格"?
└─ 微调

需求是"基于一份大文档回答问题"?
└─ RAG

需求是"领域专业回答(法律、医疗)"?
└─ RAG + 微调结合(微调学领域语气,RAG 提供准确事实)

13.5 学习路径建议

如果你是从零开始,推荐顺序:

  1. 第一周:租 24GB 显卡,跑通 Qwen2.5-7B 的 transformers 推理 → FastAPI → vLLM 三种部署
  2. 第二周:用一个 1000 条的小数据集跑通 LoRA 微调,理解参数。再切 QLoRA 跑一次对比
  3. 第三周:学 RAG,搭一个 50 篇文档的本地知识库,接到微调后的模型上
  4. 第四周:玩多模态(Qwen2-VL)、推理模型(R1-Distill),拓展视野
  5. 后续:持续关注新模型(每个月有新东西),建立自己的 prompt 库和数据集

附:关键术语速查

术语含义
LLMLarge Language Model,大语言模型
MLLM / VLMMultimodal LLM / Vision LLM,多模态/视觉大模型
SFTSupervised Fine-Tuning,监督微调(就是指令微调)
RLHFReinforcement Learning from Human Feedback,基于人类反馈的强化学习
LoRALow-Rank Adaptation,低秩适配
QLoRAQuantized LoRA,量化版 LoRA
KV Cache推理时缓存的 attention key/value
Tensor Parallel张量并行,把单层切到多 GPU
RAGRetrieval-Augmented Generation,检索增强生成
Embedding把文本转成向量,用于相似度检索
Chunk文档切片,RAG 索引的基本单位
Reranker重排序模型,用于精排检索结果
Adapter微调产出的小型权重(几十 MB)
Merge把 adapter 合并回 base 模型,产出完整模型
Token模型处理的最小单位,中文约 1 字符 = 1-2 token
Context Window上下文窗口,模型一次能处理的 token 数
Hallucination幻觉,模型编造不存在的事实
Instruction Tuning指令微调,SFT 的常见形式
Continued Pretraining继续预训练,在新语料上做无监督训练补充知识

附:进一步阅读

需要深入某个方向时,以下是值得读的:

  • LoRA 原论文:LoRA: Low-Rank Adaptation of Large Language Models(arxiv 2106.09685)
  • QLoRA 原论文:QLoRA: Efficient Finetuning of Quantized LLMs(arxiv 2305.14314)
  • PagedAttention 原论文:Efficient Memory Management for Large Language Model Serving with PagedAttention(arxiv 2309.06180)
  • self-llm 项目:github.com/datawhalech…——遇到具体模型的工程细节回这里查

本文档围绕 self-llm 的工程实践展开,重点讲技术原理与踩坑经验,代码作辅助说明。所有数字(显存、参数量、速度比)截至 2026-04 仍准确,但开源生态变化快,具体模型版本和库 API 请对照最新文档使用。