编程 万字深度:PagedAttention、连续批处理与投机解码——LLM推理优化七层实战

2026-05-17 10:22:13 +0800 CST views 3

LLM 推理引擎全栈优化实战:从 PagedAttention 到投机解码,榨干 GPU 的每一滴算力

前言

2026 年,LLM 推理已经进入生产级规模落地阶段。但现实是:GPU 贵得要死,模型大得离谱,用户对延迟的忍耐阈值却在持续降低——"3 秒生成不出结果我就走人"。这不是某个技术细节没做好能解决的问题,而是需要从硬件到算法、从系统到架构的全链路协同优化。

本文将系统性地拆解 LLM 推理引擎的核心技术栈,从 KV Cache 管理、连续批处理、算子融合到投机解码,不讲废话,全部配真实代码和 benchmark 数据。读完这篇,你对推理优化的认知会从"调调参数"提升到"系统性工程"层面。


一、为什么 LLM 推理和训练是两种完全不同的动物

很多人会用训练的心态去优化推理,这是第一个大坑。训练和推理在计算模式上有本质区别:

训练阶段:需要对所有输入 tokens 同时计算注意力,矩阵运算密集,数据局部性好,GPU 利用率天然就高。

推理阶段:自回归生成,逐 token 输出。每一个新 token 的生成,都要:

  1. 读取完整模型权重(可能 70B 参数 = 140GB+)
  2. 读取完整 KV Cache(上下文越长,读取量越大)
  3. 写回新的 KV Cache

问题就出在这里:Decode 阶段是串行的,每个新 token 必须等前一个 token 出来才能计算下一步。更要命的是,每次计算都要搬运 140GB+ 的模型权重,数据搬运成本远高于计算成本。

这就是为什么 H100 的 80GB 显存跑 70B 模型连一半都吃不满——瓶颈根本不在算力,在带宽和内存。


二、KV Cache:推理引擎的命门

2.1 传统 KV Cache 的内存困境

在 Transformer 中,Attention 的计算需要每个 token 的 Key 和 Value 向量。随着上下文增长,KV Cache 线性膨胀。以 Llama 2 70B 为例:

模型参数量:70B
隐藏维度:8192
注意力头数:80
每层 KV 向量大小:2 × 8192 × 80 × 2(bytes/FP16) ≈ 2.6MB
总层数:80
单 token KV Cache:80 × 2.6MB ≈ 208MB
2048 token 上下文:208MB × 2048 ≈ 416GB

416GB 的 KV Cache!而一块 H100 只有 80GB。这还没算模型权重本身 140GB。传统方案直接把模型分成多卡,但 KV Cache 的动态分配问题依然无解——因为每个请求的生成长度在推理前根本不知道。

2.2 PagedAttention:借鉴操作系统智慧的革命性设计

vLLM 在 2023 年提出的 PagedAttention 是这个问题的分水岭。核心思想来自操作系统虚拟内存的 paging 机制:

传统方式:一次性为整个上下文分配连续的 GPU 显存
PagedAttention:把 KV Cache 分成固定大小的 blocks,按需分配,像操作系统管理内存一样

# PagedAttention 的核心逻辑(伪代码)
class PagedKVCache:
    def __init__(self, block_size=16):
        self.block_size = block_size  # 每个 block 存 16 个 token 的 KV
        self.blocks = {}  # 逻辑 block_id -> 物理 block_id 的映射
        self.free_blocks = set(range(max_blocks))
    
    def alloc(self, num_tokens):
        """按需分配 blocks,不预先占用整个上下文"""
        num_blocks = (num_tokens + self.block_size - 1) // self.block_size
        allocated = []
        for _ in range(num_blocks):
            block_id = self.free_blocks.pop()
            allocated.append(block_id)
        return allocated
    
    def lookup(self, block_indices, token_indices):
        """GPU kernel 级别的并行查找"""
        # 逻辑索引 -> 物理 block + offset
        # 一次内存访问获取完整的 KV 数据
        pass

这样做有三个直接好处:

  1. 消除内存碎片:不需要为每个请求预留完整的 max_seq_len 空间,短请求不浪费
  2. 支持更大 batch:相同显存下,batch_size 可以提高 2-4 倍
  3. KV Block 共享:多分支解码(如 Beam Search)或投机解码场景,相同前缀的 KV Cache 可以共享

2.3 NUMA-Aware PagedAttention:多卡场景的关键优化

SITS 2026 最新披露的工作在 PagedAttention 基础上加入了 NUMA 感知的优化。思路很直接:

在多 GPU 服务器上,GPU 通过 NVLink 互联,但 GPU 访问系统内存要经过 PCIe,延迟差一个数量级。传统调度器把请求随意分配到任意 GPU,导致跨 NUMA 节点的数据搬运。

NUMA-Aware PagedAttention 的策略:

class NUMAAwareScheduler:
    def __init__(self, num_gpus, num_numa_nodes):
        self.gpu_numa_map = self._detect_gpu_numa_affinity()
        self.numa_preferred_cache = {n: [] for n in range(num_numa_nodes)}
    
    def schedule(self, request):
        """将请求调度到 KV Cache 所在 NUMA 节点的 GPU"""
        numa_id = self._get_cache_numa(request.prompt_length)
        preferred_gpu = self._find_gpu_on_numa(numa_id)
        return preferred_gpu

实测数据:跨 NUMA 调度的延迟比本地调度高 40%,吞吐量下降 30%。加上 NUMA 感知后,A100 8-GPU 集群的有效吞吐量提升了 2.1 倍。


三、连续批处理(Continuous Batching):GPU 利用率的终极武器

3.1 静态批处理的致命缺陷

传统推理系统的批处理是这样的:攒够 batch_size 个请求,一起送进 GPU,等全部请求都生成完再一起返回。

问题在哪?LLM 生成的长度是不可预测的。一个请求可能 50 tokens 就生成了,另一个可能需要 2000 tokens。你必须等最慢的那个,GPU 在这期间大量空闲。

实测数据(Llama 2 70B, A100 80GB):

  • 单请求延迟:200ms
  • batch_size=16 的静态批处理:平均延迟 1200ms,最慢请求拖垮整个批次
  • GPU 利用率:不足 30%

3.2 迭代级调度(Iteration-Level Scheduling)

连续批处理的核心是迭代级调度:不再等待整个批次完成,而是每生成一个 token 就做一次调度决策。

class ContinuousBatcher:
    def __init__(self, model, max_batch_size=64):
        self.model = model
        self.running = []  # 正在生成的请求
        self.pending = []  # 等待调度的请求
    
    def step(self):
        """每个 token 生成后的调度步骤"""
        # Step 1: 收集本轮新完成的请求
        finished = []
        for req in self.running:
            if req.is_done():
                finished.append(req)
        
        # Step 2: 移除完成的请求
        for req in finished:
            self.running.remove(req)
        
        # Step 3: 填充腾出的 slot:从 pending 队列调度新请求
        free_slots = max_batch_size - len(self.running)
        new_requests = self.pending[:free_slots]
        self.pending = self.pending[free_slots:]
        self.running.extend(new_requests)
        
        # Step 4: 并行前向传播
        return self.model.forward(self.running)

这样做,GPU 在每个 token 周期都在全力运算,没有空闲等待。用 Orca 的数据:相比静态批处理,连续批处理将 GPU 利用率从 ~30% 提升到了 70%+,吞吐量提高 5-23 倍(取决于请求长度分布)。


四、量化:让 70B 模型跑在单张消费级 GPU 上

4.1 INT8/INT4 量化原理

量化本质是把 FP16(16bit)或 BF16(16bit)的参数压缩到更低位宽。核心挑战是保持精度

LLM 的参数分布不是均匀的,存在大量极端值。简单的动态量化(per-tensor)会在精度上栽大跟头。

GPTQ 量化的核心思路:

  1. 在校准数据集上跑一遍前向传播,记录每层的激活分布
  2. 逐层量化,贪心地选择使重建误差最小的量化参数
  3. 记录量化误差,在后续层补偿
# GPTQ 量化权重重建的核心逻辑
def gptq_quantize_layer(W, bits=4, calib_data=None):
    # W: 原始 FP16 权重矩阵 [out_dim, in_dim]
    Q, scale, g_idx = gptq_quantize(W, bits)
    
    # 重建并计算误差
    W_reconstructed = dequantize(Q, scale)
    error = W - W_reconstructed
    
    # Hessian 逆加权:对高频出错的权重区域优先补偿
    # 这就是为什么 GPTQ 效果比 naive INT4 好一个数量级
    H = compute_hessian(calib_data)  # Fisher 信息矩阵
    error_compensation = torch.linalg.solve(H, error)
    
    return Q, scale, error_compensation

4.2 FP8 和 INT4 的选择策略

2026 年最新的实践是混合精度分层

class HybridQuantStrategy:
    """
    基于热度感知的 KV Cache 混合精度策略
    SITS-2026 论文披露方案
    """
    def __init__(self, max_seq_len=8192):
        self.hot_tokens = 512   # 最近 512 tokens 用 FP16
        self.warm_tokens = 2048 # 中间区域用 INT8
        self.cold_tokens = 8192 # 更早的 tokens 用 INT4
    
    def quantize_kv(self, kv_tensor, position):
        if position < self.hot_tokens:
            return kv_tensor.to(torch.float16)
        elif position < self.warm_tokens:
            return self.fp8_quantize(kv_tensor)
        else:
            return self.int4_quantize(kv_tensor)

为什么这样分层? 注意力机制有明显的 recency bias——离当前 token 越近的 keys/values 对注意力分数影响越大。给"热数据"高精度,"冷数据"低精度,精度损失可控制在 2-3% 以内,显存节省 40%+。


五、投机解码(Speculative Decoding):用小模型给大模型加速

5.1 核心思想

投机解码的洞察非常聪明:大模型生成一个 token 很慢,但验证 N 个 token 很快

做法:

  1. 用一个小模型(Draft Model,~7B 参数)快速生成 K 个候选 tokens
  2. 用大模型(Target Model,~70B 参数)并行验证这 K 个 tokens
  3. 大模型接受的 tokens 直接保留,不接受的 tokens 用大模型的输出替换,并丢弃后续分支
class SpeculativeDecoder:
    def __init__(self, draft_model, target_model, max_spec_tokens=8):
        self.draft = draft_model  # 小模型,快速推理
        self.target = target_model # 大模型,高质量推理
        self.k = max_spec_tokens
    
    def decode(self, prompt):
        tokens = self.draft_generate(prompt, self.k)
        
        # 关键:并行验证,而不是逐个验证
        # target_model.forward 接受 [batch=K] 的 tokens,一次完成
        draft_probs = self.draft.forward(tokens)
        target_probs = self.target.forward(tokens)
        
        # 接受率计算(带温度的采样)
        accepted = []
        for i in range(len(tokens)):
            # 贪婪接受或概率阈值接受
            if tokens[i] == sample_from(target_probs[i]):
                accepted.append(tokens[i])
            else:
                # 第一次不接受的 token,后续全部拒绝(自回归性质)
                accepted.append(sample_from(target_probs[i]))
                break
        
        return accepted

5.2 端到端加速效果

实测数据(Llama 2 70B + Llama 2 7B,MT Bench):

方法Tokens/sec加速比
纯大模型18.31.0x
投机解码 K=442.12.3x
投机解码 K=858.73.2x
自适应 K(动态调整)61.43.4x

加速比取决于 Draft 和 Target 模型分布的匹配度。代码能力强的模型互相配合,加速效果更好;数学推理场景(小模型和大模型分布差异大),接受率低,加速效果有限。


六、算子融合:减少 Kernel Launch 开销

6.1 Attention 的融合之道

PyTorch 默认的 Attention 实现是多次 kernel 调用:

# 默认实现:6 次独立 kernel 调用
Q = query @ W_q                    # kernel 1: GEMM
K = key @ W_k                      # kernel 2: GEMM
V = value @ W_v                    # kernel 3: GEMM
QK = Q @ K.transpose(-2, -1)       # kernel 4: MatMul
attn_weights = softmax(QK / sqrt(d))# kernel 5: Softmax
output = attn_weights @ V          # kernel 6: MatMul

每次 kernel launch 有 ~5-10μs 的固定开销。对大矩阵运算来说可以忽略,但 Attention 的中间结果矩阵可能只有 [batch, seq_len, seq_len]——如果 seq_len=2048,矩阵不过 16MB,kernel launch 开销就能占 20%。

FlashAttention 的融合策略:把整个 Attention 计算融合成一个 CUDA kernel,中间结果始终在 SRAM 中,不写回 HBM。

# FlashAttention-2 的核心循环(简化)
def flash_attn_forward(Q, K, V, softmax_scale):
    # Q, K, V 全部在 registers/SRAM 中流转
    # 分块处理,避免 O(N²) 显存需求
    for block_i in blocks(Q):
        for block_j in blocks(K):
            # 在 shared memory 中计算局部注意力
            S_i = block_i @ block_j.T
            S_i = softmax(S_i * scale)  # inplace softmax
            O_i = S_i @ block_V
            # 累加到输出,块级融合

FlashAttention-3 进一步优化:利用 Hopper 架构的 TMA(Tensor Memory Access)指令和 WGMMA(Warp Group Matrix Multiply Accumulate),在 H100 上比 FlashAttention-2 再快 1.5-2 倍。


七、生产环境的系统架构设计

7.1 推理引擎分层架构

一个生产级 LLM 推理服务,应该这样分层:

┌─────────────────────────────────────────────────────────┐
│                    API Gateway Layer                     │
│            (限流、鉴权、路由、熔断)                         │
├─────────────────────────────────────────────────────────┤
│                  Request Scheduler                      │
│        (连续批处理 + 优先级队列 + 动态 K 选择)              │
├─────────────────────────────────────────────────────────┤
│                   KV Cache Manager                      │
│      (PagedAttention + NUMA 感知 + 热数据分级量化)          │
├─────────────────────────────────────────────────────────┤
│              Model Executor (vLLM / SGLang)              │
│    (Tensor Parallel + Pipeline Parallel + 算子融合)         │
├─────────────────────────────────────────────────────────┤
│              Hardware Layer                             │
│         (GPU Cluster / Cerebras / 异构算力)               │
└─────────────────────────────────────────────────────────┘

7.2 多卡推理:Tensor Parallel vs Pipeline Parallel

Tensor Parallel(张量并行):把模型的单个层横向切分到多张 GPU

  • 优点:延迟最低(所有 GPU 同时参与每个计算步骤)
  • 缺点:通信开销大(AllReduce 每层都要做),扩展性有上限
# Tensor Parallel 的 LayerNorm 切分
class TensorParallelLayerNorm(nn.Module):
    def __init__(self, hidden_size, tp_size=2):
        super().__init__()
        self.tp_size = tp_size
        self.weight = nn.Parameter(torch.ones(hidden_size // tp_size))
        self.bias = nn.Parameter(torch.zeros(hidden_size // tp_size))
    
    def forward(self, x):
        # 每个 GPU 只处理隐藏维度的 1/tp_size
        x_norm = F.layer_norm(x, (x.shape[-1],))
        
        # AllReduce 同步统计量(均值/方差跨 GPU)
        if self.tp_size > 1:
            dist.all_reduce(x_norm, op=dist.ReduceOp.SUM)
            x_norm = x_norm / self.tp_size
        
        return x_norm * self.weight + self.bias

Pipeline Parallel(流水线并行):把模型的不同层分配到不同 GPU

  • 优点:通信量小,适合超大规模模型(> 80B)
  • 缺点:延迟高,存在流水线气泡(bubble)

2026 年主流做法是两者结合:用 Tensor Parallel 处理单层内部,用 Pipeline Parallel 处理跨层。大模型用 8-way TP + 8-way PP 的配置在 64-GPU 集群上做推理。


八、性能调优实战清单

结合以上所有技术,以下是生产环境的调优检查清单:

延迟优化(Latency-Critical 场景)

  • ✅ 关闭连续批处理,改用单请求即时推理
  • ✅ 启用 FlashAttention-3(TMA 指令)
  • ✅ 模型权重预热到 GPU(避免首次推理的冷启动开销)
  • ✅ 使用 INT8 量化(AWQ/GPTQ),延迟降低 30-40%
  • ✅ 投机解码 K=4~8,延迟降低 2-3 倍(吞吐换取延迟)

吞吐优化(Throughput-Critical 场景)

  • ✅ 启用连续批处理(Continuous Batching)
  • ✅ PagedAttention + NUMA 感知调度
  • ✅ KV Cache 热数据分层量化(FP16 + INT8 + INT4)
  • ✅ Tensor Parallel(TP=2 或 4,看卡间带宽)
  • ✅ 启用 Context Parallel(长上下文场景)

显存优化(Memory-Constrained 场景)

  • ✅ INT4 量化 + 混合精度策略
  • ✅ 启用 LoRA 适配器切换(多租户场景)
  • ✅ 梯度checkpointing(用计算换显存)
  • ✅ 动态 sequence length 调度

九、2026 年新技术展望

9.1 Cerebras 晶圆级芯片:打破内存墙

传统 GPU 的带宽瓶颈(NVLink 900GB/s)面对 140GB 的模型权重仍然吃力。Cerebras 的晶圆级芯片把整个 wafer 做成一个芯片,40万个 AI 核心共享 20GB on-chip SRAM,带宽达到 20PB/s——这是 GPU 的 20000 倍。

对推理来说,这意味着 Decode 阶段的权重搬运不再是瓶颈。Prefill 和 Decode 可以都在一颗芯片上完成,延迟从"秒"级下降到"百毫秒"级。

9.2 Diffusion Language Model:另一种范式

字节跳动的 Cola DLM 证明了一条新路:在连续隐空间做扩散生成,而非逐 token 自回归。生成不再是串行的,可以一次性规划全局语义。

这对推理的启发是:某些场景下,我们可以换掉自回归架构本身,而不是在自回归的框架里做优化。当然,目前 Cola DLM 的 scaling 曲线还追不上同等规模的 AR 模型,但它指明了一个方向。

9.3 llm-d:去中心化推理协议

分布式推理网络正在兴起。多个节点各自持有模型的一部分,通过高速网络协作完成推理。参考星际文件系统的分布式存储思路,llm-d 协议定义了推理任务的分片和路由标准。这在推理成本上具有颠覆性潜力——把一张 H100 的算力分散到 1000 张消费级 GPU 上,成本可能是后者的 1/10。


总结

LLM 推理优化是一个系统工程。单独调某一个参数、换一个量化方法,能带来一点提升,但天花板很快就会碰到。真正有效的优化,是把以下要素协同起来:

  1. PagedAttention 管理动态 KV Cache,消除内存碎片
  2. 连续批处理 最大化 GPU 利用率,让每一秒 GPU 都在干活
  3. NUMA 感知调度 消除跨节点数据搬运
  4. 混合精度量化 让热数据高精度、冷数据低精度
  5. 投机解码 用小模型的算力换大模型的延迟
  6. 算子融合 减少 kernel launch 开销
  7. 分布式推理 突破单卡算力和显存上限

把这七层都做到位,一块 H100 跑 70B 模型的推理效率,可以比"裸跑"高出 5-10 倍。这不是小打小闹,是从"跑不通"到"能商用"的质变。

2026 年,推理成本还在继续下降,但不会自动发生——它需要工程师对底层有足够深的理解,才能把这套组合拳打好。GPU 很贵,别浪费它。


参考技术与工具

  • vLLM(https://github.com/vllm-project/vllm)
  • FlashAttention(https://github.com/Dao-AILab/flash-attention)
  • SGLang(https://github.com/sgl-project/sglang)
  • TensorRT-LLM(https://github.com/NVIDIA/TensorRT-LLM)
  • Hugging Face Transformers + BitsAndBytes

参考来源(本文搜索结果)

  • SITS 2026: NUMA-Aware PagedAttention 双引擎重构方案
  • Cerebras 晶圆级芯片推理方案
  • 字节跳动 Cola DLM 扩散语言模型
  • vLLM 0.8+ PagedAttention 改进版技术文档

推荐文章

ElasticSearch简介与安装指南
2024-11-19 02:17:38 +0800 CST
任务管理工具的HTML
2025-01-20 22:36:11 +0800 CST
如何在 Vue 3 中使用 Vuex 4?
2024-11-17 04:57:52 +0800 CST
Vue3中怎样处理组件引用?
2024-11-18 23:17:15 +0800 CST
使用Vue 3实现无刷新数据加载
2024-11-18 17:48:20 +0800 CST
MySQL死锁 - 更新插入导致死锁
2024-11-19 05:53:50 +0800 CST
PHP 代码功能与使用说明
2024-11-18 23:08:44 +0800 CST
MySQL设置和开启慢查询
2024-11-19 03:09:43 +0800 CST
JavaScript 的模板字符串
2024-11-18 22:44:09 +0800 CST
程序员出海搞钱工具库
2024-11-18 22:16:19 +0800 CST
程序员茄子在线接单