扩展与部署


文档摘要

扩展与部署 把大模型服务给上百万用户,需要把推理分布到多张 GPU、提前预测所需 token、缓存共享上下文,并选对框架。本文件涵盖推理并行、投机解码(speculative decoding)、前缀缓存、KV-cache 驱逐、推理框架、成本优化和监控 单张 H100 GPU 服务 70B 模型,在交互级延迟下大约能扛 100 个并发用户。要服务 1000 万用户,需要 10 万张 GPU——云算力成本一年约 30 亿美元。每提升一个百分点的效率,就能省下上千万美元。这就是为什么推理优化不是纸上谈兵:它直接决定了 AI 产品的经济账。 推理时的模型并行 当一个模型大到单张 GPU 装不下时,就必须切到多张 GPU 上。训练时的并行策略(第 6 章)在推理时同样适用,只是权衡不同。

扩展与部署

把大模型服务给上百万用户,需要把推理分布到多张 GPU、提前预测所需 token、缓存共享上下文,并选对框架。本文件涵盖推理并行、投机解码(speculative decoding)、前缀缓存、KV-cache 驱逐、推理框架、成本优化和监控

  • 单张 H100 GPU 服务 70B 模型,在交互级延迟下大约能扛 100 个并发用户。要服务 1000 万用户,需要 10 万张 GPU——云算力成本一年约 30 亿美元。每提升一个百分点的效率,就能省下上千万美元。这就是为什么推理优化不是纸上谈兵:它直接决定了 AI 产品的经济账。

推理时的模型并行

  • 当一个模型大到单张 GPU 装不下时,就必须切到多张 GPU 上。训练时的并行策略(第 6 章)在推理时同样适用,只是权衡不同。

张量并行

  • 张量并行(tensor parallelism)(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)
  • 推理时,对于单卡装不下的模型,张量并行是默认选择。一个 FP16 的 70B 模型需要 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 推理至关重要。

流水线并行

  • **流水线并行(pipeline parallelism)**把不同层分给不同 GPU。GPU 1 跑 0-39 层,GPU 2 跑 40-79 层。token 串行地流过流水线。

  • 推理时,流水线并行的延迟比张量并行更高(每个 token 都要穿过整个流水线),但通信开销更低(只在 GPU 之间传激活,没有 all-reduce)。当 GPU 之间用慢速互联(跨节点、没有 NVLink)时,更倾向用流水线并行。

序列并行

  • 对于超长序列,即便模型装得下单卡,KV-cache 本身也可能装不下。**序列并行(sequence parallelism)**把 KV-cache 切到多张 GPU 上:每张 GPU 存这个序列缓存 key/value 的一部分。

  • 注意力计算时,每张 GPU 对自己缓存的那段算部分注意力分数,再用一次规约合并结果。这用于长上下文推理(128K+ token),此时 KV-cache 超过单卡显存。

投机解码

  • 投机解码(speculative decoding)是影响最大的 LLM 推理优化之一。洞察是:解码之所以慢,是因为它一次只生成一个 token,而每个 token 都要让大模型跑一次完整前向。但一个小模型可以快得多地生成候选 token,大模型则可以一次验证多个候选。

投机解码:快速的草稿模型生成 5 个候选 token,目标模型一次前向全部验证,接受的保留、拒绝的重采样

  • 算法
    1. 草稿模型(draft model)(小而快,例如 1B 参数)自回归地生成 k 个候选 token。
    2. 目标模型(target model)(大而准,例如 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。接受率比独立草稿模型更高。
    • 自投机解码(self-speculative decoding):目标模型自己用提前退出(draft 只跑前几层,再用完整模型验证)生成草稿。
    • 并行解码(parallel decoding):并行生成多个续写(候选树),一次验证整棵树。吞吐更高,但分支的 KV-cache 占用更多内存。

前缀缓存

  • 很多请求共享公共前缀:系统 prompt、few-shot 示例、常见查询模式。**前缀缓存(prefix caching)**为这些前缀存下 KV-cache,跨请求复用。

  • 系统 prompt 缓存:如果每个请求都以同样的 2000 token 系统 prompt 开头,那 2000 token 的 KV-cache 只算一次,在所有请求间共享。对一个 80 层的 70B 模型,这每请求省下约 200 MB。

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

  • 效果:对有长公共前缀的应用(带系统 prompt 的聊天机器人、有公共检索段落的 RAG),前缀缓存能把 TTFT 降 50-90%,并按比例省 GPU 算力。

KV-cache 驱逐

  • 除了量化 KV-cache(文件 01)和用 GQA/MLA 减小其体积(文件 02),**KV-cache 驱逐(eviction)**策略会有选择地移除未来不太可能被 attend 到的缓存 token。

  • H2O(Heavy-Hitter Oracle,张等,2023)观察到注意力分数服从幂律:少数 token("重磅选手")拿到大部分注意力,而大多数 token 的注意力可忽略。H2O 保留:

    1. 最近的 token(最近 w 个 token 的滑动窗口,类似 StreamingLLM)。
    2. 重磅 token(按所有过去解码步累计注意力分数取 top-k)。
  • 既不是最近、也不是重磅的 token 就被驱逐。这维持了固定大小的 KV-cache,同时保留真正影响生成的 token。H2O 用 20% 的内存就能达到接近完整 KV-cache 的质量。

  • Scissorhands(Liu 等,2023)思路类似但用更精细的重要性度量:当前步收到高注意力的 token 保留,已 T 步没被 attend 到的 token 驱逐。这能适应生成过程中注意力模式的变化。

  • 动态驱逐 + StreamingLLM:把注意力沉淀(永久保留最前面几个 token)和动态驱逐(保留最近 + 重磅 token)结合。这是超长生成中最省内存的方案,能在有界质量退化下实现无限长度生成。

  • 所有驱逐方法的共同关键洞察:LLM 注意力实际上是稀疏的——尽管架构对所有缓存 token 算注意力,真实的注意力权重集中在一小撮子集上。驱逐其余的对输出质量影响微乎其微。

推理框架

  • LLM 服务生态已经收敛到几个主流框架:
框架 强项 最适合
vLLM PagedAttention、连续批处理、高吞吐 通用 LLM 服务、最高吞吐
TensorRT-LLM NVIDIA 优化 kernel、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 与工作负载匹配。

  • Spot 实例:云厂商把闲置 GPU 容量以 60-90% 折扣出售(AWS Spot、GCP Preemptible)。Spot 实例可能被中断,所以适合批处理推理,不适合延迟敏感的服务。配合抢占处理(保存状态、在新实例上恢复),Spot 也能服务交互流量。

  • 自动扩缩容(autoscaling):按流量扩缩 GPU 数量。高峰期扩容、夜间缩容。Kubernetes HPA(Horizontal Pod Autoscaler)或云原生自动扩缩容(AWS SageMaker、GCP Vertex AI)都能搞定。

  • 批处理 + 利用率:30% 和 90% 的 GPU 利用率,每 token 成本差 3 倍。连续批处理、智能调度和 PagedAttention 都能提升利用率。

  • 量化:INT4 相比 FP16 少 4 倍内存 → 装进更小的 GPU → 成本降 2-4 倍。再加上同样 batch 能塞更多请求 → 吞吐更高 → 每 token 成本更低。

  • 每 token 成本基准(大致,2026 年):

配置 每百万 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(硬件摊销)

监控

  • 生产推理需要持续监控,以便在用户受影响前发现退化:

  • 延迟监控:跟踪 p50、p95、p99 的 TTFT 和 TPOT。为 p99 超出 SLO 设告警。p99 飙升往往意味着:KV-cache 显存压力(抖动)、某个长请求独占 batch,或 GPU 热降频。

  • 吞吐监控:跟踪每 GPU 每秒 token 数。下降意味着:批处理效率降低(很多短请求 → batch 利用率低)、序列变长(每请求 KV-cache 显存增加),或硬件问题(GPU 进入 ECC 纠错模式、跑得变慢)。

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

  • 模型质量监控:跟踪每请求指标(响应长度、留出集困惑度、用户反馈信号)。模型质量可能因这些而退化:数据漂移(进来的请求分布变了)、KV-cache 量化误差在长对话中累积、服务流水线里的 bug。

  • 成本监控:按模型、按 GPU 类型跟踪每 token 成本。如果成本上升但吞吐没升,就要查效率退化(新模型版本内存占用更高、批处理配置欠优、GPU 利用不足)。

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

编程练习(使用 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: {standard_time:.2f}s ({n} target calls)") print(f"Speculative: {spec_time:.2f}s ({target_calls} target calls)") print(f"Speedup: {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-cache # 粗略吞吐估算(访存带宽受限) 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}-bit on {gpu_name}:") print(f" Model size: {model_size_gb:.0f} GB → {gpus_for_model} GPU(s)/replica") print(f" Throughput: {total_throughput:.0f} tok/s/replica") print(f" Replicas for {target_throughput_tps} tok/s: {replicas}") print(f" Total GPUs: {total_gpus}") print(f" Cost: ${cost_per_hr:.0f}/hr, ${cost_per_1M_tokens:.2f}/1M tokens") print() print("=== Cost Comparison ===\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)

发布者: 作者: HenryNdubuaku 转发
评论区 (0)
U