# 扩缩与部署

*向数百万用户提供大模型服务需要跨多个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。

![推测性解码：快速草稿模型生成5个候选token，目标模型一次验证所有，接受的token保留，拒绝的重新采样](../images/speculative_decoding.svg)

- **算法**：
    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)
```
