大模型推理加速:从延迟瓶颈到吞吐量突破

cover

一、延迟与吞吐:大模型落地的拦路虎

大模型一旦离开实验室,推理性能就是最直接的拦路虎。一个 70B 参数的模型,单次推理耗时可能达到数秒,但线上服务通常要求首 Token 延迟(TTFT)控制在 200ms 以内,吞吐量还得达到每秒数百请求。这不是简单堆显卡就能解决的——GPU 利用率低、KV Cache 内存碎片、调度策略粗糙,这些都会让算力资源在 30% 以下空转。

推理和训练的优化方向完全不同。训练追求大规模并行和梯度收敛,推理则盯着单请求延迟(TTFT)、Token 间延迟(TPOT)和系统整体吞吐。两者在内存访问模式、计算密集度和批处理策略上存在根本差异,直接套用训练阶段的优化思路往往南辕北辙。

二、推理流水线:从 Token 到 Tensor

大模型推理的执行过程可以抽象为一条从输入到输出的流水线,每个阶段都有独特的性能特征与瓶颈点。

flowchart TD
    A[Tokenization 输入编码] --> B[Embedding 查表与位置编码]
    B --> C[Prefill 阶段: 并行计算所有输入 Token 的 KV Cache]
    C --> D[Decode 阶段: 逐 Token 自回归生成]
    D --> E[Sampling 采样与 Top-P/Top-K 过滤]
    E --> F[Detokenization 输出解码]

    C -->|内存密集: KV Cache 分配| G[(GPU HBM)]
    D -->|计算密集: 单步矩阵乘| G
    E -->|内存带宽密集: Logits 读取| G

    style C fill:#ff6b6b,color:#fff
    style D fill:#4ecdc4,color:#fff
    style E fill:#ffe66d,color:#333

Prefill 阶段是计算密集型(Compute-bound),大量输入 Token 能充分利用 GPU 的并行计算能力;而 Decode 阶段是内存带宽密集型(Memory-bound),每次只处理一个 Token,矩阵乘法的实际计算量极小,但需要从 HBM 读取完整的权重矩阵,导致算力利用率通常不足 5%。

这一根本性差异决定了优化策略必须分阶段设计。Prefill 阶段的核心优化目标是减少冗余计算,而 Decode 阶段的核心优化目标是减少内存访问次数。混淆这两个阶段的优化方向,是许多工程实践中最常见的误区。

三、生产级推理加速:从调度到内核的全栈优化

3.1 Continuous Batching:打破静态批处理的吞吐天花板

传统的 Static Batching 要求同一批次的所有请求必须等最长的请求完成后才能释放资源,导致短请求被长请求拖拽,GPU 大量时间在等待中浪费。Continuous Batching(又称 In-flight Batching)在请求级别实现细粒度调度:当一个请求生成完毕,立即将其占用的 KV Cache 槽位释放给新请求,无需等待同批次其他请求。

# Continuous Batching 调度核心逻辑(伪代码)
class ContinuousBatchScheduler:
    def __init__(self, max_batch_size: int, max_kv_cache_tokens: int):
        self.max_batch_size = max_batch_size
        self.kv_cache_pool = KVCachePool(max_kv_cache_tokens)
        self.running_requests: list[Request] = []

    def schedule(self, waiting_queue: deque[Request]) -> list[Request]:
        """每步调度:优先回收已完成请求,再填充新请求"""
        # 回收已完成或被中止的请求,释放 KV Cache
        completed = [r for r in self.running_requests if r.is_finished()]
        for req in completed:
            self.kv_cache_pool.free(req.kv_cache_handle)
            self.running_requests.remove(req)

        # 在剩余配额内接纳新请求
        available_slots = self.max_batch_size - len(self.running_requests)
        for _ in range(available_slots):
            if not waiting_queue:
                break
            req = waiting_queue[0]
            # 检查 KV Cache 剩余容量是否足以容纳新请求的 Prefill
            needed_tokens = req.input_length + req.max_output_length
            if self.kv_cache_pool.available() >= needed_tokens:
                req.kv_cache_handle = self.kv_cache_pool.allocate(needed_tokens)
                self.running_requests.append(req)
                waiting_queue.popleft()
            else:
                # KV Cache 不足,停止接纳,避免 OOM
                break

        return self.running_requests

实测数据:在 A100 上部署 Llama-2-70B,Static Batching 的吞吐量约为 420 tokens/s,切换到 Continuous Batching 后提升至 1350 tokens/s,提升幅度超过 3 倍。但代价是调度逻辑的复杂度显著上升,P99 延迟可能因批内竞争而劣化 15%-20%。

3.2 PagedAttention:消灭 KV Cache 内存碎片

KV Cache 是推理过程中最昂贵的内存开销。以 Llama-2-70B 为例,单个请求的 KV Cache 在 FP16 下约占 1.2MB/Token,2048 Token 上下文需要约 2.4GB。传统方案为每个请求预分配一块连续的物理内存,但请求长度差异导致严重的内存碎片——预留不足则 OOM,预留过多则浪费。

PagedAttention 借鉴了操作系统的虚拟内存分页机制:将 KV Cache 划分为固定大小的 Block(通常 16 个 Token 为一个 Block),按需分配,无需物理连续。这使 KV Cache 的内存利用率从 60%-70% 提升到 95% 以上。

# PagedAttention Block 管理核心逻辑
class PagedKVCacheManager:
    BLOCK_SIZE = 16  # 每个 Block 容纳 16 个 Token 的 KV Cache

    def __init__(self, total_blocks: int):
        self.free_blocks: deque[int] = deque(range(total_blocks))
        self.request_blocks: dict[str, list[int]] = {}  # req_id -> block_id 列表

    def allocate_block(self, req_id: str) -> int:
        """为请求分配一个新的 KV Cache Block"""
        if not self.free_blocks:
            raise OOMError("KV Cache Block 耗尽,需触发驱逐或拒绝请求")
        block_id = self.free_blocks.popleft()
        self.request_blocks.setdefault(req_id, []).append(block_id)
        return block_id

    def free_request(self, req_id: str):
        """请求完成后释放其所有 Block"""
        for block_id in self.request_blocks.pop(req_id, []):
            self.free_blocks.append(block_id)

3.3 Kernel 融合与算子优化:减少 GPU Kernel Launch 开销

Decode 阶段每次只计算一个 Token,但一个 Transformer 层涉及数十个 GPU Kernel(RMSNorm、QKV 投影、Attention、FFN 等),每个 Kernel Launch 都有约 5-10us 的固定开销。在短序列场景下,Kernel Launch 开销可能占总耗时的 30% 以上。

解决方案是 Kernel 融合(Fuse Kernels):将多个连续算子合并为一个 CUDA Kernel,减少全局内存的读写次数和 Kernel Launch 次数。例如将 RMSNorm + Residual + QKV 投影融合为单个 Kernel,可减少 2 次 HBM 读写和 2 次 Kernel Launch。

四、优化不是免费的午餐:每项加速的隐性代价

优化手段 收益 代价与风险
Continuous Batching 吞吐提升 2-3x P99 延迟劣化 15%-20%,调度逻辑复杂度 O(N)
PagedAttention 内存利用率 95%+ Block 边界引入非连续访存,单 Token 延迟增加 3%-5%
Kernel 融合 延迟降低 20%-30% 通用性差,需针对每款 GPU 架构单独调优
Speculative Decoding 延迟降低 40%-60% 需要额外的 Draft Model,内存开销增加 10%-15%
KV Cache 量化(FP8) 内存节省 50% 长上下文场景下精度损失可能导致输出质量下降

适用边界:上述优化方案主要面向 Decode 阶段占主导的在线推理场景。如果是离线批量推理(Batch Inference),Prefill 阶段占比更高,优化重心应转向 FlashAttention、Tensor Parallelism 等计算密集型优化。此外,Speculative Decoding 对输出确定性要求高的场景(如代码生成)效果有限,因为 Draft Model 的接受率会显著下降。

禁用场景:当 GPU 显存不足以容纳模型权重本身时,所有推理加速手段都无济于事——必须先通过量化或分布式推理解决显存容量问题。在显存极度紧张的条件下强行开启 Continuous Batching,反而会因频繁的 KV Cache 换入换出导致吞吐量断崖式下跌。

五、总结

大模型推理加速的本质是在延迟、吞吐和资源利用率三者之间寻找最优解。Continuous Batching 通过细粒度调度打破吞吐天花板,PagedAttention 通过分页管理消灭内存碎片,Kernel 融合通过减少访存与调度开销压榨单步延迟。每项优化都有其适用场景与隐性代价,不存在银弹。

落地路线建议:优先实施 Continuous Batching + PagedAttention,这是投入产出比最高的组合,可将吞吐提升 2-3 倍且内存利用率接近理论极限;其次针对 Decode 阶段做 Kernel 融合优化,压榨单步延迟;最后在精度允许的前提下尝试 KV Cache 量化或 Speculative Decoding。所有优化必须以基准测试数据为依据,避免凭直觉调参——性能优化的第一步永远是量化现状。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐