# 扩缩与部署 *向数百万用户提供大模型服务需要跨多个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) ```