Skip to content

Scaling and Deployment

将大型模型 serving 给数百万用户,需要将 inference 分布到多个 GPU 上,在 token 需要之前预测它们,缓存共享上下文,并选择正确的框架。本文涵盖 inference 并行性、speculative decoding、前缀缓存、inference 框架、成本优化和监控。

  • 单张 H100 GPU serving 700 亿参数模型,可以处理约 100 名并发用户并保持交互式 latency。Serving 1000 万用户需要 100,000 张 GPU——每年 cloud 计算成本约 30 亿美元。每提高一个百分点的效率就能节省数千万美元。这就是为什么 inference 优化不是学术问题:它直接决定了 AI 产品的经济可行性。

Inference 的模型并行

  • 当模型对于单张 GPU 来说太大时,必须将其分布到多个 GPU 上。训练中的并行策略(第 6 章)在 inference 时应用时有不同的权衡。

Tensor 并行

  • Tensor 并行(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)\]
  • 在 inference 时,tensor 并行是无法放入单张 GPU 的模型的默认选择。FP16 下 700 亿参数模型需要 140 GB——通过 tensor 并行分布到 2 张 80 GB GPU 上。

  • Latency 影响:tensor 并行每层增加一个 all-reduce 通信步骤。在 NVLink(900 GB/s)上,每层增加约 0.1 毫秒。在 PCIe(32 GB/s)上,增加约 3 毫秒。对于 2 张 GPU 上有 80 层的 700 亿参数模型:NVLink 总共增加约 8 毫秒,PCIe 增加约 240 毫秒。这就是为什么 NVLink 对于多 GPU inference 至关重要。

Pipeline 并行

  • Pipeline 并行将不同层分配给不同 GPU。GPU 1 处理层 0-39,GPU 2 处理层 40-79。Token 顺序流经 pipeline。

  • 在 inference 时,pipeline 并行的 latency 比 tensor 并行高(整个 pipeline 必须为每个 token 遍历),但通信开销更低(只有激活在 GPU 之间传递,无需 all-reduce)。当 GPU 通过慢速互连连接(不同节点,无 NVLink)时,它更受青睐。

Sequence 并行

  • 对于非常长的序列,即使模型适合单张 GPU,KV-cache 本身也可能不适合。Sequence 并行将 KV-cache 分片到 GPU 上:每个 GPU 存储序列缓存的 key 和 value 的一部分。

  • 在 attention 期间,每个 GPU 计算其缓存段的部分 attention 分数,然后通过规约合并结果。这用于长上下文 inference(128K+ token),其中 KV-cache 超过单 GPU 内存。

Speculative Decoding

  • Speculative decoding 是最具影响力的 LLM inference 优化之一。核心思想:解码速度慢是因为它每次生成一个 token,每个 token 需要大型模型的完整前向传播。但小模型可以更快地生成候选 token,而大型模型可以并行 验证 多个候选。

Speculative decoding:快速草稿模型生成 5 个候选 token,目标模型在一次传播中验证所有 token,接受的 token 保留,拒绝的重新采样

  • 算法
    1. 草稿模型(小型、快速——例如 10 亿参数)自回归地生成 \(k\) 个候选 token。
    2. 目标模型(大型、准确——例如 700 亿参数)对整个草稿序列运行单次前向传播,计算每个候选 token 的概率。
    3. 如果目标模型同意(其对该 token 的概率足够高),则每个候选被 接受。被拒绝的候选从目标模型的分布中重新采样。
    4. 平均每次验证步骤接受多个 token,速度提升与接受率成比例。
\[\text{加速比} \approx \frac{k \times \text{接受率}}{\text{成本比}} \approx 2\text{-}3\times\]
  • 为什么在不损失质量的情况下有效:拒绝采样方案保证输出分布与目标模型完全匹配。Speculative decoding 是无损的——输出在统计上与单独运行目标模型完全相同,只是更快。

  • 变体

    • Medusa(Cai 等,2024):不使用单独的草稿模型,而是为目标模型添加多个轻量级"头",同时预测多个未来 token。不需要单独的模型。
    • EAGLE(Li 等,2024):训练一个轻量级草稿头,利用目标模型的隐状态预测未来 token。比独立草稿模型接受率更高。
    • 自 speculative decoding:目标模型本身使用早退生成草稿(只运行前几层进行草稿,然后用完整模型验证)。
    • 并行解码:并行生成多个延续(候选树),一次验证整个树。Throughput 更高,但为分叉的 KV-cache 使用更多内存。

前缀缓存

  • 许多请求共享公共前缀:系统提示、少样本示例或常见查询模式。前缀缓存存储这些前缀的 KV-cache 并在请求间复用。

  • 系统提示缓存:如果每个请求都以相同的 2000 token 系统提示开始,这 2000 个 token 的 KV-cache 只需计算一次,并在所有请求间共享。对于 80 层 700 亿参数模型,每次请求节省约 200 MB。

  • 基数树缓存(SGLang):在基数树(trie)中组织缓存的前缀。当新请求到来时,找到最长的缓存前缀匹配,并从那里开始生成,跳过匹配前缀的计算。

  • 影响:对于具有长共享前缀的应用(带系统提示的聊天机器人、带公共检索段落的 RAG),前缀缓存将 TTFT 减少 50-90%,并节省相应的 GPU 计算。

KV-Cache 驱逐

  • 除了量化 KV-cache(第 1 文件)和使用 GQA/MLA 减少其大小(第 2 文件),KV-cache 驱逐策略还可以选择性地移除未来不可能被关注的缓存 token。

  • H2O(Heavy-Hitter Oracle,Zhang 等,2023)观察到 attention 分数遵循幂律:小部分 token("重要击中者")获得大部分 attention,而大多数获得可忽略的 attention。H2O 保留:

    1. 最近 token(像 StreamingLLM 一样的最后 \(w\) 个 token 的滑动窗口)。
    2. 重要击中 token(在所有过去解码步骤中按累计 attention 分数排名的前 \(k\) 个 token)。
  • 既不是最近也不是重要击中者的 token 被驱逐。这在保留实际影响生成的 token 的同时维护固定大小的 KV-cache。H2O 仅用 20% 的内存就能达到接近完整 KV-cache 的质量。

  • Scissorhands(Liu 等,2023)采用类似方法,但使用更复杂的重要性度量:在 当前步骤获得高 attention 的 token 被保留,而超过 \(T\) 步未被关注的 token 被驱逐。这能适应生成过程中变化的 attention 模式。

  • 动态驱逐 + StreamingLLM:结合 attention sinks(永久保留前几个 token)和动态驱逐(保留最近 + 重要击中 token)。这是非常长的生成中内存效率最高的方法,能以有界的质量降级实现无限长度生成。

  • 所有驱逐方法的关键洞察:LLM attention 在实践中是 稀疏的——即使架构计算所有缓存 token 的 attention,实际 attention 权重也集中在一小部分上。驱逐其余的对输出质量影响很小。

Inference 框架

  • LLM serving 生态系统已经收敛到几个主流框架:
框架 优势 最适合
vLLM PagedAttention、continuous batching、高 throughput 通用 LLM serving,最高 throughput
TensorRT-LLM NVIDIA 优化 kernel,FP8,in-flight batching NVIDIA GPU 上的最大性能
SGLang 前缀缓存(RadixAttention),快速结构化生成 共享前缀应用,约束输出
llama.cpp CPU/Metal/CUDA/Vulkan,GGUF 量化,便携 消费者硬件,设备端 inference
TGI(HuggingFace) 简单 API,易于 deployment,模型中心集成 快速 deployment,HuggingFace 生态
Ollama 一键模型下载和 serving 个人使用,本地开发
ExLlamaV2 极限量化优化(EXL2 格式) 内存受限 GPU inference
  • vLLM 是生产 LLM serving 的默认选择。它支持 continuous batching、PagedAttention、tensor 并行、speculative decoding、LoRA serving 以及大多数开源模型。

  • TensorRT-LLM 在 NVIDIA 硬件上实现最高原始性能(比同一 GPU 上的 vLLM 快 10-30%),但灵活性较差,定制更难。

  • SGLang 在应用有结构化输出(JSON、特定格式代码)或共享前缀时表现出色,得益于其 radix attention 缓存和约束解码引擎。

成本优化

  • 规模化时,inference 成本主导 ML 预算。降低成本的策略:

  • 适当选择 GPU:并非每个模型都需要 H100。量化后的 70 亿参数模型在 A10G(约 1 美元/小时)上运行良好,而非 H100(约 8 美元/小时)。将 GPU 与工作负载匹配。

  • Spot 实例:cloud 提供商以 60-90% 折扣提供未使用的 GPU 容量(AWS Spot,GCP Preemptible)。Spot 实例可能被中断,所以它们适合批处理 inference,但不适合 latency 敏感的 serving。结合抢占处理(保存状态,在新实例上恢复),spot 实例也可以 serving 交互流量。

  • 自动缩放:根据流量缩放 GPU 数量。高峰时段扩容,夜间缩容。Kubernetes HPA(Horizontal Pod Autoscaler)或 cloud 原生自动缩放(AWS SageMaker,GCP Vertex AI)处理这一问题。

  • Batching + 利用率:30% 和 90% GPU 利用率之间的差异是每 token 成本 3 倍。Continuous batching、智能调度和 PagedAttention 都能提高利用率。

  • Quantisation:INT4 vs FP16 内存少 4 倍 → 可以在更小的 GPU 上运行 → 成本降低 2-4 倍。此外,更多请求可以放入同一 batch → throughput 更高 → 每 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(硬件摊销)

监控

  • 生产 inference 需要持续监控,以在影响用户之前发现降级:

  • Latency 监控:跟踪 p50、p95 和 p99 处的 TTFT 和 TPOT。设置 p99 超过 SLO 的警报。p99 的峰值通常表明:KV-cache 内存压力(抖动)、长时间运行的请求垄断 batch,或 GPU 热降频。

  • Throughput 监控:跟踪每 GPU 每秒 token 数。下降表明:批处理效率降低(许多短请求 → 低 batch 利用率)、序列长度增加(每个请求更多 KV-cache 内存)或硬件问题(GPU 处于 ECC 错误校正模式,运行速度较慢)。

  • GPU 利用率:跟踪 SM 占用率、内存利用率和内存带宽。低 SM 占用率 + 高内存利用率 = 内存受限(需要更多带宽或量化)。高 SM 占用率 + 低内存利用率 = 计算受限(需要更多 FLOPS 或更小模型)。

  • 模型质量监控:跟踪每次请求的指标(响应长度、保留集上的困惑度、用户反馈信号)。模型质量可能因以下原因降级:数据漂移(传入请求的分布变化)、长对话中 KV-cache 量化误差累积,或 serving pipeline 中的 bug。

  • 成本监控:跟踪每模型每 GPU 类型的每 token 成本。如果成本增加而 throughput 没有增加,调查效率回归(内存使用更高的新模型版本、次优 batch 配置或利用率不足的 GPU)。

  • 工具:用于基础设施指标的 Prometheus + Grafana(第 15 章),vLLM/TRT-LLM 的内置指标端点,以及用于模型级指标的自定义日志记录。

编程任务(使用 CoLab 或 notebook)

  1. 模拟 speculative decoding。使用快速"草稿"函数和慢速"目标"函数,测量一次生成和验证多个 token 带来的加速。

    import random
    import time
    
    def target_model(tokens):
        """慢但准确的模型。返回每个候选 token 的概率。"""
        time.sleep(0.01)  # 模拟每次前向传播 10 毫秒
        # 模拟:接受偶数 token
        return [0.9 if t % 2 == 0 else 0.1 for t in tokens]
    
    def draft_model():
        """快但近似的模型。生成一个候选 token。"""
        time.sleep(0.001)  # 模拟每 token 1 毫秒
        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"Speculative: {spec_time:.2f}s({target_calls} 次目标调用)")
    print(f"加速比:     {standard_time / spec_time:.1f} 倍")
    

  2. 估计不同优化策略应用于 LLM serving deployment 的成本节省。

    def serving_cost_analysis(
        model_name, params_B, precision_bits,
        gpu_name, gpu_mem_gb, gpu_cost_per_hr,
        target_throughput_tps,
    ):
        """估计 LLM deployment 的 serving 成本。"""
        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.2 倍用于 KV-cache
    
        # 粗略 throughput 估计(内存带宽受限)
        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} 位 on {gpu_name}:")
        print(f"  模型大小: {model_size_gb:.0f} GB → {gpus_for_model} GPU/副本")
        print(f"  Throughput: {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)