2536c937e3
翻译自英文原版 maths-cs-ai-compendium,共 20 章全部完成。 第01章 向量 | 第02章 矩阵 | 第03章 微积分 第04章 统计学 | 第05章 概率论 | 第06章 机器学习 第07章 计算语言学 | 第08章 计算机视觉 | 第09章 音频与语音 第10章 多模态学习 | 第11章 自主系统 | 第12章 图神经网络 第13章 计算与操作系统 | 第14章 数据结构与算法 第15章 生产级软件工程 | 第16章 SIMD与GPU编程 第17章 AI推理 | 第18章 ML系统设计 第19章 应用人工智能 | 第20章 前沿人工智能 翻译说明: - 所有数学公式 $...$ / $$...$$、代码块、图片引用完整保留 - mkdocs.yml 配置中文导航 + language: zh - README.md 已翻译为中文(兼 docs/index.md) - docs/ 目录包含指向各章文件的 symlink - 约 29,000 行中文内容,排除 .cache/ 构建缓存
252 lines
15 KiB
Markdown
252 lines
15 KiB
Markdown
# 扩缩与部署
|
||
|
||
*向数百万用户提供大模型服务需要跨多个GPU分布推理、在需要之前预测token、缓存共享上下文以及选择合适的框架。本文涵盖推理时的并行性、推测性解码、前缀缓存、推理框架、成本优化和监控*
|
||
|
||
- 单个H100 GPU服务一个70B模型可以处理约100个并发用户,交互延迟可接受。服务1000万用户需要100,000个GPU——云计算每年花费约30亿美元。每一个百分点的效率提升就能节省数千万美元。这就是推理优化不是学术问题的原因:它直接决定AI产品的经济性。
|
||
|
||
## 推理时的模型并行
|
||
|
||
- 当模型太大无法装入单张GPU时,必须跨多个GPU拆分。训练时的并行策略(第6章)在推理时适用,但权衡不同。
|
||
|
||
### 张量并行
|
||
|
||
- **张量并行**(Megatron风格,第6章)跨GPU拆分单个权重矩阵。对于线性层$Y = XW$,权重矩阵$W$跨$N$个GPU按列拆分。每个GPU计算部分结果,然后all-reduce聚合:
|
||
|
||
$$W = [W_1 | W_2 | \cdots | W_N], \quad Y_i = X W_i, \quad Y = \text{concat}(Y_1, \ldots, Y_N)$$
|
||
|
||
- 在推理时,张量并行是模型无法装入单张GPU时的默认选择。FP16的70B模型需要140 GB——跨2张80 GB GPU使用张量并行拆分。
|
||
|
||
- **延迟影响**:张量并行每层增加一个all-reduce通信步骤。在NVLink(900 GB/s)上,每层增加约0.1 ms。在PCIe(32 GB/s)上,每层增加约3 ms。对于80层的70B模型在2张GPU上:NVLink总增加约8 ms,PCIe总增加约240 ms。这就是NVLink对多GPU推理至关重要的原因。
|
||
|
||
### 流水线并行
|
||
|
||
- **流水线并行**将不同的层分配给不同的GPU。GPU 1处理第0-39层,GPU 2处理第40-79层。token顺序流过流水线。
|
||
|
||
- 在推理时,流水线并行的延迟高于张量并行(每个token必须遍历整个流水线),但通信开销更低(只有激活值在GPU之间传递,无需all-reduce)。当GPU通过慢速互连(不同节点,无NVLink)连接时,更倾向于使用流水线并行。
|
||
|
||
### 序列并行
|
||
|
||
- 对于非常长的序列,即使模型本身适合,KV缓存本身可能无法装入单张GPU。**序列并行**将KV缓存分片到多个GPU上:每个GPU存储序列缓存键和值的一部分。
|
||
|
||
- 在注意力期间,每个GPU在其缓存的段上计算部分注意力分数,然后通过规约合并结果。这用于长上下文推理(128K+ token),其中KV缓存超过单GPU内存。
|
||
|
||
## 推测性解码
|
||
|
||
- **推测性解码**是影响最大的LLM推理优化之一。其洞察:解码速度慢是因为一次只生成一个token,每个token需要大模型的完整前向传播。但小模型可以更快地生成候选token,而大模型可以**验证**多个候选token。
|
||
|
||

|
||
|
||
- **算法**:
|
||
1. **草稿模型**(小型、快速——例如1B参数)自回归生成$k$个候选token。
|
||
2. **目标模型**(大型、准确——例如70B)对整个草稿序列运行一次前向传播,计算每个候选token的概率。
|
||
3. 如果目标模型同意(该token的概率足够高),每个候选被**接受**。被拒绝的候选从目标模型的分布中重新采样。
|
||
4. 平均每个验证步骤接受多个token,加速比与接受率成正比。
|
||
|
||
$$\text{加速比} \approx \frac{k \times \text{acceptance\_rate}}{\text{cost\_ratio}} \approx 2\text{-}3\times$$
|
||
|
||
- **为什么无质量损失**:拒绝采样方案保证输出分布与目标模型完全匹配。推测性解码是无损的——输出在统计上与单独运行目标模型相同,只是更快。
|
||
|
||
- **变体**:
|
||
- **Medusa**(Cai等人,2024):不是独立的草稿模型,而是向目标模型添加多个轻量级"头",同时预测多个未来token。无需独立模型。
|
||
- **EAGLE**(Li等人,2024):训练一个使用目标模型隐藏状态预测未来token的轻量级草稿头。接受率高于独立的草稿模型。
|
||
- **自推测性解码**:目标模型本身使用提前退出生成草稿(仅运行前几层作为草稿,然后用完整模型验证)。
|
||
- **并行解码**:并行生成多个延续(候选树)并一次性验证整棵树。吞吐量更高,但分支KV缓存使用更多内存。
|
||
|
||
## 前缀缓存
|
||
|
||
- 许多请求共享共同的前缀:系统提示、few-shot示例或常见查询模式。**前缀缓存**存储这些前缀的KV缓存并在请求之间重用。
|
||
|
||
- **系统提示缓存**:如果每个请求都以相同的2000-token系统提示开始,这2000个token的KV缓存被计算一次,并在所有请求之间共享。对于80层的70B模型,每次请求节省约200 MB。
|
||
|
||
- **基数树缓存**(SGLang):将缓存的前缀组织在基数树(trie)中。当新请求到达时,找到最长的缓存前缀匹配,并从那里开始生成,跳过匹配前缀的计算。
|
||
|
||
- **影响**:对于具有长共享前缀的应用(带系统提示的聊天机器人、具有常见检索段落的RAG),前缀缓存将TTFT降低50-90%,并节省相应的GPU计算。
|
||
|
||
## KV缓存驱逐
|
||
|
||
- 除了量化KV缓存(文件01)和使用GQA/MLA减小其大小(文件02)之外,**KV缓存驱逐**策略选择性地移除不太可能在未来被关注的缓存token。
|
||
|
||
- **H2O**(重要token识别器,Zhang等人,2023)观察到注意力分数遵循幂律:一小部分token("重要token")获得大部分注意力,而大多数token获得的注意力可以忽略不计。H2O保留:
|
||
|
||
1. **最近token**(最后$w$个token的滑动窗口,类似StreamingLLM)。
|
||
2. **重要token**(在所有过去解码步骤中累积注意力分数最高的前$k$个token)。
|
||
|
||
- 既不是最近也不是重要token的token被驱逐。这保持固定大小的KV缓存,同时保留实际影响生成的token。H2O仅使用20%的内存就实现了接近完整KV缓存的质量。
|
||
|
||
- **Scissorhands**(Liu等人,2023)采用类似方法,但使用更复杂的度量:在当前**步骤**中获得高注意力的token被保留,而已经$T$步没有被关注的token被驱逐。这适应了生成过程中注意力模式的变化。
|
||
|
||
- **动态驱逐+StreamingLLM**:结合注意力汇聚点(永久保留前几个token)和动态驱逐(保留最近+重要token)。这是最内存高效的方法,适用于非常长的生成,实现了无限长度生成,质量下降有限。
|
||
|
||
- 所有驱逐方法的核心洞察:LLM注意力在实践中是**稀疏的**——尽管架构会对所有缓存的token计算注意力,但实际注意力权重集中在小子集上。驱逐其余部分对输出质量影响极小。
|
||
|
||
## 推理框架
|
||
|
||
- LLM服务生态已收敛到几个主要框架:
|
||
|
||
| 框架 | 优势 | 最适合 |
|
||
|-----------|-----------|----------|
|
||
| **vLLM** | PagedAttention、连续批处理、高吞吐量 | 通用LLM服务,最高吞吐量 |
|
||
| **TensorRT-LLM** | NVIDIA优化内核、FP8、飞行中批处理 | NVIDIA GPU上的最大性能 |
|
||
| **SGLang** | 前缀缓存(RadixAttention)、快速结构化生成 | 具有共享前缀的应用,受限输出 |
|
||
| **llama.cpp** | CPU/Metal/CUDA/Vulkan、GGUF量化、可移植 | 消费级硬件,设备端推理 |
|
||
| **TGI**(HuggingFace) | 简单API,易于部署,模型中心集成 | 快速部署,HuggingFace生态 |
|
||
| **Ollama** | 一键下载和提供服务 | 个人使用,本地开发 |
|
||
| **ExLlamaV2** | 极致量化优化(EXL2格式) | 内存受限的GPU推理 |
|
||
|
||
- **vLLM**是生产级LLM服务的默认选择。它支持连续批处理、PagedAttention、张量并行、推测性解码、LoRA服务和大多数开源模型。
|
||
|
||
- **TensorRT-LLM**在NVIDIA硬件上实现最高的原始性能(在相同GPU上比vLLM快10-30%),但灵活性较低且更难以定制。
|
||
|
||
- **SGLang**在应用具有结构化输出(JSON、特定格式的代码)或共享前缀时表现出色,这得益于其基数注意力缓存和受限解码引擎。
|
||
|
||
## 成本优化
|
||
|
||
- 在规模上,推理成本主导ML预算。降低成本的策略:
|
||
|
||
- **合理选择GPU**:并非每个模型都需要H100。量化的7B模型在A10G(约$1/小时)上运行良好,而不是H100(约$8/小时)。匹配GPU到工作负载。
|
||
|
||
- **竞价实例**:云提供商提供未使用的GPU容量,折扣60-90%(AWS Spot、GCP Preemptible)。竞价实例可能被中断,因此适用于批处理推理而不是延迟关键型服务。结合抢占处理(保存状态,在新实例上恢复),竞价实例也可以服务交互式流量。
|
||
|
||
- **自动扩缩**:根据流量扩展GPU数量。高峰期扩展,夜间缩减。Kubernetes HPA(水平Pod自动扩缩器)或云原生自动扩缩(AWS SageMaker、GCP Vertex AI)处理此功能。
|
||
|
||
- **批处理+利用率**:30%和90% GPU利用率之间的差异是每token成本3倍。连续批处理、智能调度和PagedAttention都提高了利用率。
|
||
|
||
- **量化**:INT4 vs FP16是4倍更少内存 → 适合更小的GPU → 成本降低2-4倍。此外,更多请求适合同一批次 → 更高吞吐量 → 更低每token成本。
|
||
|
||
- **每token成本基准**(近似值,2026年):
|
||
|
||
| 配置 | 每100万token成本 |
|
||
|-------|-------------------|
|
||
| GPT-4o API | $2.50 |
|
||
| Claude 3.5 Sonnet API | $3.00 |
|
||
| Llama-70B on H100(vLLM,FP16) | $0.50 |
|
||
| Llama-70B on H100(TRT-LLM,INT8) | $0.25 |
|
||
| Llama-8B on A10G(vLLM,INT4) | $0.05 |
|
||
| Llama-3B 设备端(llama.cpp) | $0(硬件成本摊销) |
|
||
|
||
## 监控
|
||
|
||
- 生产推理需要持续监控,以便在用户受到影响之前发现降级:
|
||
|
||
- **延迟监控**:跟踪TTFT和TPOT的p50、p95和p99。设置告警,当p99超过SLO时触发。p99的尖峰通常指示:KV缓存内存压力(抖动)、长时间运行的请求垄断批次、或GPU热节流。
|
||
|
||
- **吞吐量监控**:跟踪每GPU每秒token数。下降指示:批次效率降低(许多短请求→批次利用率低)、序列长度增加(每个请求更多KV缓存内存)、或硬件问题(GPU处于ECC纠错模式,运行更慢)。
|
||
|
||
- **GPU利用率**:跟踪SM占用率、内存利用率和内存带宽。低SM占用率+高内存利用率=内存受限(需要更多带宽或量化)。高SM占用率+低内存利用率=计算受限(需要更多FLOPS或更小模型)。
|
||
|
||
- **模型质量监控**:跟踪每请求指标(响应长度、保留集上的困惑度、用户反馈信号)。模型质量可能因以下原因降级:数据漂移(传入请求的分布变化)、KV缓存量化误差在长对话中累积、或服务流水线中的错误。
|
||
|
||
- **成本监控**:跟踪每模型每GPU类型每token成本。如果成本增加而吞吐量没有增加,调查效率回归(新模型版本内存使用更高、批次配置次优、或GPU利用不足)。
|
||
|
||
- **工具**:Prometheus + Grafana(第15章)用于基础设施指标,vLLM/TRT-LLM的内置指标端点,以及用于模型级指标的自定义日志记录。
|
||
|
||
## 编程任务(使用CoLab或notebook)
|
||
|
||
1. 模拟推测性解码。使用快速的"草稿"函数和慢速的"目标"函数,测量一次生成和验证多个token的加速比。
|
||
```python
|
||
import random
|
||
import time
|
||
|
||
def target_model(tokens):
|
||
"""慢但准确的模型。返回每个候选token的概率。"""
|
||
time.sleep(0.01) # 模拟每次前向传播10ms
|
||
# 用于模拟:接受偶数token
|
||
return [0.9 if t % 2 == 0 else 0.1 for t in tokens]
|
||
|
||
def draft_model():
|
||
"""快但近似的模型。生成一个候选token。"""
|
||
time.sleep(0.001) # 模拟每token 1ms
|
||
return random.randint(0, 9)
|
||
|
||
def standard_decoding(n_tokens):
|
||
"""一次生成一个token,使用目标模型。"""
|
||
tokens = []
|
||
for _ in range(n_tokens):
|
||
time.sleep(0.01) # 目标模型生成1个token
|
||
tokens.append(random.randint(0, 9))
|
||
return tokens
|
||
|
||
def speculative_decoding(n_tokens, k=4):
|
||
"""生成k个草稿token,用目标模型验证,接受/拒绝。"""
|
||
tokens = []
|
||
total_target_calls = 0
|
||
|
||
while len(tokens) < n_tokens:
|
||
# 草稿:快速生成k个候选
|
||
candidates = [draft_model() for _ in range(k)]
|
||
|
||
# 验证:一次目标模型调用验证所有k个候选
|
||
probs = target_model(candidates)
|
||
total_target_calls += 1
|
||
|
||
# 接受token,直到一个被拒绝
|
||
for i, (tok, prob) in enumerate(zip(candidates, probs)):
|
||
if random.random() < prob:
|
||
tokens.append(tok)
|
||
if len(tokens) >= n_tokens:
|
||
break
|
||
else:
|
||
# 从目标分布重新采样
|
||
tokens.append(tok + 1) # 简化重新采样
|
||
break
|
||
|
||
return tokens, total_target_calls
|
||
|
||
n = 50
|
||
|
||
start = time.time()
|
||
_ = standard_decoding(n)
|
||
standard_time = time.time() - start
|
||
|
||
start = time.time()
|
||
_, target_calls = speculative_decoding(n, k=5)
|
||
spec_time = time.time() - start
|
||
|
||
print(f"标准: {standard_time:.2f}s ({n} 次目标调用)")
|
||
print(f"推测性: {spec_time:.2f}s ({target_calls} 次目标调用)")
|
||
print(f"加速比: {standard_time / spec_time:.1f}x")
|
||
```
|
||
|
||
2. 估计应用于LLM服务部署的不同优化策略的成本节省。
|
||
```python
|
||
def serving_cost_analysis(
|
||
model_name, params_B, precision_bits,
|
||
gpu_name, gpu_mem_gb, gpu_cost_per_hr,
|
||
target_throughput_tps,
|
||
):
|
||
"""估计LLM部署的服务成本。"""
|
||
model_size_gb = params_B * 1e9 * precision_bits / 8 / 1e9
|
||
gpus_for_model = max(1, int((model_size_gb * 1.2) / gpu_mem_gb + 0.99)) # 1.2x用于KV缓存
|
||
|
||
# 粗略吞吐量估计(内存带宽受限)
|
||
tokens_per_gpu = 500 / (params_B * precision_bits / 16) # 归一化到7B FP16的500 tok/s
|
||
total_throughput = tokens_per_gpu * gpus_for_model
|
||
|
||
replicas = max(1, int(target_throughput_tps / total_throughput + 0.99))
|
||
total_gpus = gpus_for_model * replicas
|
||
cost_per_hr = total_gpus * gpu_cost_per_hr
|
||
cost_per_1M_tokens = cost_per_hr / (total_throughput * replicas * 3600 / 1e6)
|
||
|
||
print(f"{model_name} @ {precision_bits}-位 在 {gpu_name} 上:")
|
||
print(f" 模型大小: {model_size_gb:.0f} GB → {gpus_for_model} GPU(s)/副本")
|
||
print(f" 吞吐量: {total_throughput:.0f} tok/s/副本")
|
||
print(f" 需达到{target_throughput_tps} tok/s的副本数: {replicas}")
|
||
print(f" 总GPU数: {total_gpus}")
|
||
print(f" 成本: ${cost_per_hr:.0f}/小时, ${cost_per_1M_tokens:.2f}/100万token")
|
||
print()
|
||
|
||
print("=== 成本比较 ===\n")
|
||
|
||
# 基线:H100上的FP16
|
||
serving_cost_analysis("Llama-70B", 70, 16, "H100", 80, 8.0, 1000)
|
||
|
||
# 量化后:H100上的INT8
|
||
serving_cost_analysis("Llama-70B", 70, 8, "H100", 80, 8.0, 1000)
|
||
|
||
# 量化后:A100上的INT4
|
||
serving_cost_analysis("Llama-70B", 70, 4, "A100", 80, 4.0, 1000)
|
||
|
||
# 小模型:A10G上的8B
|
||
serving_cost_analysis("Llama-8B", 8, 4, "A10G", 24, 1.0, 1000)
|
||
```
|