Chapter 09

模型合并、量化与推理加速

微调完成只是第一步。将 LoRA 权重合并,量化为高效格式,部署为高吞吐服务。

LoRA 权重合并

from peft import AutoPeftModelForCausalLM
from transformers import AutoTokenizer

# 加载带 LoRA 的模型
model = AutoPeftModelForCausalLM.from_pretrained(
    "./lora-output",
    torch_dtype="bfloat16"
)
tokenizer = AutoTokenizer.from_pretrained("./lora-output")

# 将 LoRA 权重合并进基座模型
merged_model = model.merge_and_unload()

# 保存合并后的完整模型
merged_model.save_pretrained("./merged-model", safe_serialization=True)
tokenizer.save_pretrained("./merged-model")

LoRA 权重合并

from peft import AutoPeftModelForCausalLM
from transformers import AutoTokenizer

# 加载带 LoRA 的模型
model = AutoPeftModelForCausalLM.from_pretrained(
    "./lora-output",
    torch_dtype="bfloat16"
)
tokenizer = AutoTokenizer.from_pretrained("./lora-output")

# 将 LoRA 权重合并进基座模型
merged_model = model.merge_and_unload()

# 保存合并后的完整模型
merged_model.save_pretrained("./merged-model", safe_serialization=True)
tokenizer.save_pretrained("./merged-model")

模型合并的数学原理

模型合并(Model Merging)是将多个微调后的模型权重组合成一个新模型的技术。这不是简单的平均,而是基于线性代数的权重空间操作。

线性插值合并(Linear Interpolation)

最简单的合并方法,适用于同一基座模型的不同微调版本:

W_merged = α × W_A + (1 - α) × W_B

其中:
- W_A, W_B 是两个微调模型的权重
- α ∈ [0, 1] 是插值系数
- α = 0.5 表示等权重平均
SLERP(球面线性插值)
Spherical Linear Interpolation,在权重向量的球面上进行插值,而非直线插值。公式:W_merged = (sin((1-α)θ)/sin(θ)) × W_A + (sin(αθ)/sin(θ)) × W_B,其中 θ = arccos(W_A · W_B / (||W_A|| ||W_B||))。SLERP 保持权重向量的范数,避免线性插值可能导致的"塌陷"问题。适合合并风格差异较大的模型。
Task Arithmetic(任务算术)
将微调视为在基座模型上的"向量加法"。公式:τ_A = W_A - W_base(任务向量),W_merged = W_base + λ_A × τ_A + λ_B × τ_B。可以用负系数"减去"不想要的能力(如减去有害内容生成能力)。这是 DARE、TIES 等高级合并方法的基础。
DARE(Drop And REscale)
随机丢弃一部分任务向量的参数(dropout),然后重新缩放剩余参数以保持期望值不变。公式:τ_pruned = mask ⊙ τ / (1-p),其中 mask 是伯努利分布采样(保留概率 1-p)。DARE 能减少合并后的"干扰",提高多任务模型的性能。论文显示 p=0.9(丢弃 90%)时效果最好。
TIES(TrIm, Elect, and Merge)
三步合并策略:① Trim:移除任务向量中绝对值小于阈值的参数(去噪);② Elect:对于冲突的参数(不同模型符号相反),选择绝对值之和最大的符号;③ Merge:对同符号参数取平均。TIES 解决了多模型合并时的"符号冲突"问题,是目前最先进的合并方法之一。

使用 mergekit 进行模型合并

# 安装 mergekit
pip install mergekit

# 创建合并配置文件 merge_config.yaml
cat > merge_config.yaml << 'EOF'
merge_method: slerp
slices:
  - sources:
      - model: meta-llama/Llama-3.1-8B-Instruct
        layer_range: [0, 32]
      - model: ./my-finetuned-model
        layer_range: [0, 32]
parameters:
  t: 0.5  # SLERP 插值系数
dtype: float16
EOF

# 执行合并
mergekit-yaml merge_config.yaml ./merged-model --cuda

量化技术深度解析

量化的数学基础

量化是将高精度浮点数(FP16/FP32)映射到低精度整数(INT8/INT4)的过程:

对称量化(Symmetric Quantization):
Q(x) = round(x / scale) × scale
scale = max(|x|) / (2^(bits-1) - 1)

非对称量化(Asymmetric Quantization):
Q(x) = round((x - zero_point) / scale)
scale = (max(x) - min(x)) / (2^bits - 1)
zero_point = round(-min(x) / scale)
Per-Tensor vs Per-Channel 量化
Per-Tensor:整个权重矩阵共享一个 scale 和 zero_point,简单但精度损失大。Per-Channel:每个输出通道(卷积)或每行(全连接)有独立的 scale,精度更高但计算开销稍大。现代量化(如 GPTQ)都用 Per-Channel。
校准数据集(Calibration Dataset)
量化需要少量代表性数据来计算 scale 和 zero_point。通常用 128-512 条训练集样本。校准数据的分布应与实际推理数据接近,否则量化误差会增大。GPTQ 用 Hessian 矩阵加权校准,比简单的 min/max 更精确。
NF4(4-bit NormalFloat)
QLoRA 使用的特殊 4-bit 格式,专为神经网络权重的正态分布设计。将 [-1, 1] 区间分为 16 个非均匀的量化点,在 0 附近密集(因为权重多集中在此),在边缘稀疏。公式:量化点 = Φ^(-1)((i+0.5)/16),其中 Φ^(-1) 是标准正态分布的逆 CDF。NF4 比均匀 INT4 量化误差低约 30%。
Double Quantization(双重量化)
QLoRA 的关键技术:不仅量化权重,还量化 scale 参数本身。第一层:权重 W → 4-bit + FP32 scale;第二层:FP32 scale → 8-bit + FP32 global_scale。这额外节省约 0.4 GB 显存(7B 模型)。公式:W_reconstructed = (W_4bit × scale_8bit) × global_scale_fp32。

GPTQ 量化实战

from transformers import AutoModelForCausalLM, AutoTokenizer
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig

# 1. 加载原始模型
model_id = "meta-llama/Llama-3.1-8B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_id)

# 2. 准备校准数据(128 条样本)
from datasets import load_dataset
calibration_data = load_dataset("wikitext", "wikitext-2-raw-v1", split="train")
calibration_samples = [
    tokenizer(text, return_tensors="pt", truncation=True, max_length=2048)
    for text in calibration_data["text"][:128]
]

# 3. 配置量化参数
quantize_config = BaseQuantizeConfig(
    bits=4,                    # 4-bit 量化
    group_size=128,           # 每 128 个权重共享一个 scale
    damp_percent=0.01,        # Hessian 阻尼系数
    desc_act=False,            # 是否量化激活值(通常 False)
    sym=True,                  # 对称量化
    true_sequential=True      # 逐层量化(更慢但更准确)
)

# 4. 执行量化(需要 GPU,约 10-30 分钟)
model = AutoGPTQForCausalLM.from_pretrained(
    model_id,
    quantize_config=quantize_config
)
model.quantize(calibration_samples)

# 5. 保存量化模型
model.save_quantized("./llama-3.1-8b-gptq")
tokenizer.save_pretrained("./llama-3.1-8b-gptq")

量化格式对比

格式来源精度损失推理速度适用场景
GGUF (Q4_K_M)llama.cppCPU/GPU 均可本地部署(Ollama)
GGUF (Q8_0)llama.cpp极低中等高精度本地推理
AWQ (4-bit)MIT Han Lab极低很快(GPU)GPU 服务器部署
GPTQ (4-bit)IST Austria快(GPU)GPU 服务器部署
BF16 原始最快(大 GPU)高精度 API 服务

转换为 GGUF(Ollama 本地部署)

# 安装 llama.cpp
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp && pip install -r requirements.txt

# 转换为 GGUF 格式
python convert_hf_to_gguf.py \
  --outfile my-model-q4.gguf \
  --outtype q4_k_m \
  ./merged-model

# 用 Ollama 运行
cat > Modelfile <<EOF
FROM ./my-model-q4.gguf
SYSTEM "你是专业的代码助手..."
EOF
ollama create my-finetune -f Modelfile
ollama run my-finetune

AWQ 量化(GPU 高性能部署)

from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer

model = AutoAWQForCausalLM.from_pretrained("./merged-model")
tokenizer = AutoTokenizer.from_pretrained("./merged-model")

# 量化(需要 128 条校准数据)
quant_config = {"zero_point": True, "q_group_size": 128, "w_bit": 4, "version": "GEMM"}
model.quantize(tokenizer, quant_config=quant_config)
model.save_quantized("./my-model-awq")

# 用 vLLM 部署 AWQ 模型
# vllm serve ./my-model-awq --quantization awq --max-model-len 4096

vLLM 高性能服务部署

# 安装 vLLM
pip install vllm

# 启动 OpenAI 兼容服务
vllm serve ./merged-model \
  --served-model-name my-finetune \
  --max-model-len 4096 \
  --gpu-memory-utilization 0.9 \
  --tensor-parallel-size 1 \
  --port 8000

# 调用(兼容 OpenAI 格式)
curl http://localhost:8000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model": "my-finetune", "messages": [{"role": "user", "content": "你好"}]}'
部署选型建议

个人或团队内部使用 → GGUF + Ollama(零运维成本)。生产 API 服务 → AWQ/GPTQ + vLLM(高吞吐)。研究/高精度场景 → BF16 原始权重 + vLLM。

量化的工作原理

权重量化的核心思想
LLM 的权重矩阵本质上是浮点数(BF16 = 16位),量化是将其压缩为低精度整数(INT4/INT8)存储。以 4-bit 量化为例:每16个连续权重值为一组(group size=16),记录组内最大/最小值,将原始浮点值映射到 0-15 的整数。推理时反量化回浮点做矩阵乘法。显存减少 4× (16bit→4bit),但存在一定精度损失。
GGUF vs AWQ vs GPTQ 的区别
GGUF(llama.cpp 格式):支持 CPU 推理,量化精度等级丰富(Q2 到 Q8),可以在没有 GPU 的机器上运行,但速度较慢。AWQ(Activation-aware Weight Quantization):基于激活值的异常值感知量化,精度损失比 GPTQ 更小,是 GPU 部署的首选 4-bit 量化方案。GPTQ:基于二阶近似(Hessian)的量化,量化速度更慢但精度好,兼容性广泛(更多框架支持)。
merge_and_unload 的原理
LoRA 的核心是 W = W₀ + αBA(其中 B、A 是低秩矩阵)。在推理时,每次前向传播都要计算 W₀x + αBAx,增加了额外的矩阵乘法。merge_and_unload 将 αBA 直接加到 W₀ 上,得到新的权重矩阵 W_merged = W₀ + αBA,推理时只需一次普通矩阵乘法,速度与原始模型完全相同。代价是不能再"卸载" LoRA 适配器,模型文件变大为完整权重。
vLLM 的 PagedAttention
标准 LLM 推理将每个请求的 KV Cache 分配为连续内存块,导致显存碎片化和浪费。vLLM 的 PagedAttention 借鉴操作系统的虚拟内存分页机制:KV Cache 被分为固定大小的 block(类似内存页),按需分配,允许不同请求共享相同前缀的 KV Cache(Prefix Caching)。这使 vLLM 的吞吐量可以比 HuggingFace 原生推理高 24× 以上。

推理性能基准测试

import time
import requests

def benchmark_vllm(n_requests: int = 100, prompt: str = "介绍一下机器学习:"):
    """测量 vLLM 服务的吞吐量(tokens/second)"""
    url = "http://localhost:8000/v1/chat/completions"

    start = time.time()
    total_tokens = 0

    for _ in range(n_requests):
        resp = requests.post(url, json={
            "model": "my-finetune",
            "messages": [{"role": "user", "content": prompt}],
            "max_tokens": 200
        })
        result = resp.json()
        total_tokens += result["usage"]["completion_tokens"]

    elapsed = time.time() - start
    tps = total_tokens / elapsed

    print(f"总耗时: {elapsed:.1f}s")
    print(f"总生成 tokens: {total_tokens}")
    print(f"吞吐量: {tps:.1f} tokens/s")
    print(f"平均延迟: {elapsed/n_requests*1000:.0f}ms/request")

本章小结

本章核心要点