Files
maths-cs-ai-compendium-zh/chapter 17: AI inference/05. scaling and deployment.md
T
flykhan 2536c937e3 feat: 完整中文翻译 maths-cs-ai-compendium(数学·计算机科学·AI 知识大全)
翻译自英文原版 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/ 构建缓存
2026-05-03 10:23:20 +08:00

15 KiB
Raw Blame History

扩缩与部署

向数百万用户提供大模型服务需要跨多个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通信步骤。在NVLink900 GB/s)上,每层增加约0.1 ms。在PCIe(32 GB/s)上,每层增加约3 ms。对于80层的70B模型在2张GPU上:NVLink总增加约8 msPCIe总增加约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保留,拒绝的重新采样

  • 算法
    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量化、可移植 消费级硬件,设备端推理
TGIHuggingFace 简单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 H100vLLMFP16 $0.50
Llama-70B on H100TRT-LLMINT8 $0.25
Llama-8B on A10GvLLMINT4 $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的加速比。
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")
  1. 估计应用于LLM服务部署的不同优化策略的成本节省。
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)