06: 工程实践
核心主题:量化、推理加速、部署、Tokenizer、数据工程、评测
来源:Base-LLM 第四部分 + Happy-LLM 第五/七章
1. 模型量化
1.1 量化基础
目标:用低精度(INT8/INT4)表示原始 FP16/BF16 权重,减少显存占用和推理延迟。
| 精度 | 每参数字节 | 7B 模型显存 | 精度损失 |
|---|---|---|---|
| FP32 | 4 | 28GB | 无 |
| FP16/BF16 | 2 | 14GB | 极小 |
| INT8 | 1 | 7GB | 小 |
| INT4 | 0.5 | 3.5GB | 中等 |
1.2 常见量化方法
| 方法 | 类型 | 核心思想 | 适用场景 |
|---|---|---|---|
| GPTQ | PTQ (训后量化) | 逐层最优量化 + Hessian 重建 | 部署推理 |
| AWQ | PTQ | 保护 salient weights(重要通道精度更高) | 部署推理 |
| GGUF | PTQ | CPU 友好格式,多种量化粒度 | 边缘/CPU推理 |
| NF4 (QLoRA) | QAT-like | Normal Float 4-bit(信息论最优) | 训练+推理 |
| SmoothQuant | PTQ | 将激活的量化难度迁移到权重 | W8A8 推理 |
量化核心公式
线性量化(对称):
$$q = \text{round}\left(\frac{x}{s}\right), \quad s = \frac{\max(|x|)}{2^{b-1} - 1}$$反量化:$\hat{x} = q \times s$
2. 推理加速
2.1 Flash Attention
核心思想:IO-Aware 分块计算
标准 Attention 需将完整 $N \times N$ 矩阵写入 HBM(显存带宽瓶颈)。
Flash Attention 将 Q/K/V 分块加载到 SRAM(片上缓存),计算完直接输出,避免写回完整注意力矩阵。
| 版本 | 优化 | 加速 |
|---|---|---|
| Flash Attention v1 | 分块 + 在线 softmax | 2-4x |
| Flash Attention v2 | + 减少非矩阵乘 FLOPs + 更好并行 | 5-9x (vs PyTorch) |
| Flash Attention v3 | + 利用 H100 TMA + FP8 | 进一步 1.5-2x |
2.2 vLLM & PagedAttention
问题:KV Cache 显存管理碎片化严重(预分配 max_seq_len 但实际使用不满)。
PagedAttention
借鉴操作系统虚拟内存分页思想:
- 将 KV Cache 分成固定大小的"页"(block)
- 按需分配,不需要预留 max_seq_len 的连续空间
- 显存利用率接近 100%(vs 传统方法 ~50-70%)
效果:吞吐量提升 2-4x(同等显存下可服务更多并发请求)。
2.3 投机解码 (Speculative Decoding)
思想:用小模型快速生成候选 tokens,大模型并行验证。
- Draft model(小/快):自回归生成 $k$ 个候选 token
- Target model(大/慢):一次前向验证所有 $k$ 个 token
- 接受正确的前缀,拒绝后的重新采样
加速比:取决于 draft model 的 acceptance rate,典型 2-3x。
3. 模型部署
3.1 推理框架选型
| 框架 | 特点 | 适用场景 |
|---|---|---|
| vLLM | PagedAttention, 高吞吐 | 在线服务、高并发 |
| TGI (HuggingFace) | 易用, Tensor Parallelism | HuggingFace 生态 |
| llama.cpp | CPU 优化, GGUF 格式 | 边缘部署、本地运行 |
| TensorRT-LLM | NVIDIA 优化, In-flight batching | NVIDIA GPU 最高性能 |
| SGLang | RadixAttention, 结构化生成 | 复杂 prompt 复用场景 |
3.2 API 服务部署
# FastAPI + vLLM 基本模式
from fastapi import FastAPI
from vllm import LLM, SamplingParams
app = FastAPI()
llm = LLM(model="Qwen/Qwen2.5-7B-Instruct", tensor_parallel_size=2)
@app.post("/generate")
async def generate(prompt: str, max_tokens: int = 512):
params = SamplingParams(temperature=0.7, top_p=0.9, max_tokens=max_tokens)
outputs = llm.generate([prompt], params)
return {"text": outputs[0].outputs[0].text}
生产部署要点:
- Continuous Batching:动态合并请求,提升 GPU 利用率
- Streaming:SSE 流式输出,降低首 token 延迟
- Docker 容器化:可重复部署,版本管理
- 健康检查 + 自动扩缩容
4. Tokenizer 实践
| 算法 | 代表模型 | 核心思想 |
|---|---|---|
| BPE | GPT, LLaMA | 迭代合并最频繁的 byte pair |
| WordPiece | BERT | 类似 BPE,但按最大化似然选择合并 |
| Unigram | T5, XLNet | 从大词表开始剪枝,保留使似然最大的子集 |
| SentencePiece | LLaMA, Qwen | 语言无关的 BPE/Unigram 实现 |
# 训练自定义 BPE Tokenizer
from tokenizers import Tokenizer, models, trainers, pre_tokenizers
tokenizer = Tokenizer(models.BPE())
tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)
trainer = trainers.BpeTrainer(
vocab_size=32000,
special_tokens=["<unk>", "<s>", "</s>", "<|im_start|>", "<|im_end|>"]
)
tokenizer.train(files=["corpus.txt"], trainer=trainer)
5. 数据工程
数据质量 > 数据数量
数据质量对模型性能的影响远大于数据规模。高质量数据的关键维度:
| 维度 | 方法 |
|---|---|
| 去重 | MinHash + LSH 近似去重;exact dedup via suffix array |
| 质量过滤 | perplexity 过滤(KenLM)、规则过滤、分类器过滤 |
| 有害内容 | 毒性分类器、PII 检测与移除 |
| 领域配比 | 多轮实验确定最优比例(代码/数学/通用/多语言) |
| 数据混合 | Temperature sampling: $p_i \propto n_i^{1/T}$ |
6. 评测体系
| 评测集 | 评估能力 | 指标 |
|---|---|---|
| MMLU | 多学科知识(57科) | Accuracy |
| HumanEval / MBPP | 代码生成 | pass@k |
| GSM8K / MATH | 数学推理 | Accuracy |
| HellaSwag | 常识推理 | Accuracy |
| MT-Bench / Arena | 多轮对话质量 | Elo Rating / Win Rate |
| TruthfulQA | 真实性 | %truthful + %informative |
| MMBench / MME | 多模态理解 | Accuracy / Score |
评测注意事项
- 数据污染(data contamination):测试数据可能泄露到训练集
- 提示敏感性:不同 prompt template 可能导致性能差异 10%+
- 评测≠真实能力:Benchmark saturation 后区分度下降
面试要点总结
高频面试题
- INT8 量化的两种方式? Symmetric(对称,scale only)vs Asymmetric(非对称,scale + zero_point)
- Flash Attention 为什么快? 减少 HBM 读写(IO-bound → compute-bound),在 SRAM 分块计算
- vLLM PagedAttention 原理? 借鉴 OS 分页,KV Cache 按需分配固定大小 block,消除碎片
- 投机解码不影响输出质量? 是的,使用 rejection sampling 保证与直接大模型生成分布一致
- BPE vs WordPiece? BPE按频率合并,WordPiece按似然合并;BPE更常用于现代LLM
- 数据去重为什么重要? 重复数据导致模型过拟合特定模式,且浪费计算资源
- Continuous Batching? 请求完成后立即替换新请求(vs static batching 等所有完成),提升 GPU 利用率
- 模型推理延迟的两个阶段? Prefill(处理整个 prompt,计算密集)+ Decode(逐token生成,带宽密集)