Skip to content

扩展与部署

向数百万用户提供大模型服务需要跨多个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装不下的模型的默认选择。一个70B模型在FP16下需要140 GB——使用张量并行拆分到2×80 GB GPU上。

  • 延迟影响:张量并行在每层添加一个all-reduce通信步骤。在NVLink(900 GB/s)上,每层增加约0.1 ms。在PCIe(32 GB/s)上,每层增加约3 ms。对于2张GPU上80层的70B模型: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+ tokens),其中KV缓存超过单GPU内存。

推测解码

  • 推测解码是影响最大的LLM推理优化之一。其洞察:解码慢是因为每次只生成一个token,每个token需要大模型的完整前向传播。但小模型可以快得多地生成候选token,大模型可以并行验证多个候选。

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

  • 算法
    1. 草稿模型(小、快——例如1B参数)自回归生成\(k\)个候选token。
    2. 目标模型(大、准确——例如70B)对整段草稿序列运行一次前向传播,计算每个候选token的概率。
    3. 如果目标模型同意(其对该token的概率足够高),每个候选被接受。被拒绝的候选从目标模型的分布中重新采样。
    4. 平均而言,每次验证步骤接受多个token,加速与接受率成正比。
\[\text{Speedup} \approx \frac{k \times \text{acceptance\_rate}}{\text{cost\_ratio}} \approx 2\text{-}3\times\]
  • 为什么无质量损失:拒绝采样方案保证输出分布与目标模型完全一致。推测解码是无损的——输出在统计上与单独运行目标模型相同,只是更快。

  • 变体

    • Medusa(Cai et al., 2024):不使用单独的草稿模型,而是在目标模型上添加多个轻量"头",同时预测多个未来token。不需要单独的模型。
    • EAGLE(Li et al., 2024):训练一个轻量草稿头,使用目标模型的隐藏状态预测未来token。比独立草稿模型接受率更高。
    • 自推测解码:目标模型自身生成草稿,使用提前退出(仅运行为草稿的前几层,然后用完整模型验证)。
    • 并行解码:并行生成多个延续(候选树),并一次验证整棵树。更高吞吐量但为分支KV缓存使用更多内存。

前缀缓存

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

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

  • Radix树缓存(SGLang):将缓存前缀组织为radix树(trie)。当新请求到达时,找到最长缓存前缀匹配并从那里开始生成,跳过匹配前缀的计算。

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

KV缓存淘汰

  • 除了量化KV缓存(文件01)和使用GQA/MLA减小其大小(文件02),KV缓存淘汰策略选择性地移除不太可能在未来被关注的缓存token。

  • H2O(Heavy-Hitter Oracle,Zhang et al., 2023)观察到注意力分数遵循幂律:一小部分token("heavy hitters")接收大部分注意力,而大多数接收微不足道的注意力。H2O保留:

    1. 近期token(最近\(w\)个token的滑动窗口,类似StreamingLLM)。
    2. Heavy-hitter token(跨所有过去解码步骤累计注意力分数最高的\(k\)个token)。
  • 既非近期也非heavy-hitter的token被淘汰。这维持固定大小的KV缓存,同时保留真正影响生成的token。H2O仅用20%的内存就实现接近完整KV缓存的质量。

  • Scissorhands(Liu et al., 2023)采用类似方法,但使用更复杂的指标:在当前步骤接收高注意力的token被保留,而\(T\)步未被关注的token被淘汰。这适应生成过程中变化的注意力模式。

  • 动态淘汰 + StreamingLLM:将注意力sink(永久保留前几个token)与动态淘汰(保留近期 + heavy-hitter token)结合。这是非常长生成的内存最优方法,在有限质量下降下实现无限长度生成。

  • 所有淘汰方法的关键洞察:LLM注意力在实践中是稀疏的——即使架构对所有缓存token计算注意力,实际注意力权重集中在小子集上。淘汰其余部分对输出质量影响极小。

推理框架

  • LLM服务生态系统已收敛到几个主导框架:
框架 优势 最适合
vLLM PagedAttention、continuous batching、高吞吐量 通用LLM服务,最高吞吐量
TensorRT-LLM NVIDIA优化内核、FP8、in-flight batching 在NVIDIA GPU上的最高性能
SGLang 前缀缓存(RadixAttention)、快速结构化生成 有共享前缀或约束输出的应用
llama.cpp CPU/Metal/CUDA/Vulkan、GGUF量化、可移植 消费级硬件、设备端推理
TGI(HuggingFace) 简单API、易于部署、模型库集成 快速部署、HuggingFace生态
Ollama 一键下载和启动模型 个人使用、本地开发
ExLlamaV2 极致量化优化(EXL2格式) 内存受限的GPU推理
  • vLLM是生产级LLM服务的默认选择。它支持continuous batching、PagedAttention、张量并行、推测解码、LoRA服务和大多数开源模型。

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

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

成本优化

  • 在大规模下,推理成本主导ML预算。降低成本策略:

  • 合理的GPU选择:并非每个模型都需要H100。量化后的7B模型在A10G(约\(1/小时)上运行良好,而非H100(约\)8/小时)。将GPU与工作负载匹配。

  • 竞价实例:云服务商以60-90%折扣提供未使用的GPU容量(AWS Spot、GCP Preemptible)。竞价实例可能被中断,因此适合批量推理,但不适合对延迟敏感的服务。结合抢占处理(保存状态,在新实例上恢复),竞价实例也可以服务交互式流量。

  • 自动扩缩容:基于流量扩缩GPU数量。高峰扩容,夜间缩容。Kubernetes HPA(水平Pod自动扩缩器)或云原生自动扩缩(AWS SageMaker、GCP Vertex AI)处理此问题。

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

  • 量化:INT4与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 on-device(llama.cpp) $0(硬件摊销)

监控

  • 生产级推理需要持续监控以在用户受影响前捕捉退化:

  • 延迟监控:跟踪TTFT和TPOT的p50、p95和p99。为p99超过SLO设置告警。p99的尖峰通常表明:KV缓存内存压力(抖动)、一个长时间运行的请求独占批次,或GPU热节流。

  • 吞吐量监控:跟踪每张GPU的每秒token数。下降表明:批处理效率降低(许多短请求 → 低批次利用率)、序列长度增加(每请求更多KV缓存内存)或硬件问题(GPU处于ECC错误纠正模式,运行变慢)。

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

  • 模型质量监控:跟踪每请求指标(响应长度、留出集上的困惑度、用户反馈信号)。模型质量可能由于数据漂移(新请求的分布变化)、长对话中KV缓存量化误差累积或推理流水线中的bug而退化。

  • 成本监控:跟踪每token每模型每GPU类型的成本。如果成本增加而吞吐量未增加,调查效率退化(内存使用更高的新模型版本、次优批次配置或利用不足的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")
    

  2. 估算不同优化策略应用于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}-位 on {gpu_name}:")
        print(f"  模型大小: {model_size_gb:.0f} GB → {gpus_for_model} GPU/副本")
        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")
    
    # 基线:FP16 on H100
    serving_cost_analysis("Llama-70B", 70, 16, "H100", 80, 8.0, 1000)
    
    # 量化:INT8 on H100
    serving_cost_analysis("Llama-70B", 70, 8, "H100", 80, 8.0, 1000)
    
    # 量化:INT4 on A100
    serving_cost_analysis("Llama-70B", 70, 4, "A100", 80, 4.0, 1000)
    
    # 小模型:8B on A10G
    serving_cost_analysis("Llama-8B", 8, 4, "A10G", 24, 1.0, 1000)