ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

大模型推理的Prefill与Decode:硬件约束下的必然分工

大模型推理的Prefill与Decode:硬件约束下的必然分工 1. 为什么大模型推理要拆成 Prefill 和 Decode 两个阶段——不是设计选择而是硬件物理定律的必然结果你有没有试过用本地跑一个 7B 模型输入“请写一首关于春天的五言绝句”等了足足 8 秒才看到第一个字“春”蹦出来而后面“风轻拂柳绿……”却像开了倍速一样哗哗往外冒这不是模型卡顿也不是你电脑慢而是大模型推理在执行两种完全不同的计算模式Prefill预填充阶段和Decode解码阶段。这两个词现在频繁出现在 vLLM、SGLang、llama.cpp 的日志里也成了 GPU 监控工具中显存占用曲线出现“双峰”的根本原因。它们不是工程上的随意划分而是 Transformer 架构 现代 GPU 计算特性共同作用下的刚性约束。简单说Prefill 是“读题打草稿”Decode 是“边写边抄”。前者必须把整段 prompt 一次性塞进模型算出所有 token 对应的初始 hidden state并生成完整的 KV Cache后者则是一个 token 接一个 token 地滚动预测每次只算一个新 token复用前面已缓存的 KV 值。这个区别直接决定了Prefill 阶段显存吃紧、计算密集、无法并行加速Decode 阶段显存稳定、计算轻量、但存在强序列依赖。我第一次在 nvidia-smi 里看到 Prefill 阶段显存瞬间冲到 98%而 Decode 阶段稳在 65% 不动时才真正理解为什么所有推理引擎都把这两阶段单独建模——不是为了炫技是 GPU 显存带宽和计算单元调度的物理边界逼出来的。更关键的是这个二分法彻底改变了我们优化推理性能的思路。过去总想着“怎么让模型跑得更快”现在必须问“怎么让 Prefill 更快”和“怎么让 Decode 更密”——因为它们的瓶颈完全不同Prefill 卡在矩阵乘法的吞吐TFLOPSDecode 卡在内存带宽GB/s和 cache 命中率。比如你用 A100 跑 13B 模型Prefill 可能只占 300ms但 Decode 每个 token 要 45ms换成 H100Prefill 缩到 120msDecode 却只降到 38ms——提升不均等正说明二者优化路径不可互换。这也是为什么 SGLang 引入 Chunked Prefill、vLLM 实现 PagedAttention、llama.cpp 推出 Fused Attention Kernel全都是在各自战场上打各自的仗。不理解 Prefill/Decode 的本质差异就等于拿着扳手修电路板——工具对对象错。2. Prefill 阶段一次性的“全局扫描”它的开销到底来自哪里Prefill 阶段常被误认为只是“把 prompt 过一遍”但实际它承担着整个推理流程中最重的计算负荷和最苛刻的显存压力。它的核心任务有且只有一个为 prompt 中每一个 token同步计算出其对应的 Key 和 Value 向量并完整存入 KV Cache。注意关键词“每一个”、“同步”、“完整”。这意味着如果你的 prompt 是 1024 个 token模型是 32 层、每层 32 头、hidden size4096那么 Prefill 阶段就要完成 1024 × 32 × 32 × 4096 ≈ 13.4 亿次浮点运算仅 Key/Value 投影还不包括 Q 投影、softmax、残差连接等。这还只是单层——32 层叠加后总计算量轻松突破 400 亿次 FLOPs。我实测过 LLaMA-3-8B 在 A100 上处理 2048-token prompt 的 PrefillGPU 利用率峰值达 92%Tensor Core 满载正是这种“暴力全局扫描”的典型表现。但真正的瓶颈不在算力而在显存带宽与布局。Prefill 的 KV Cache 写入是全量、随机、不可预测的。以标准实现为例第 0 层第 0 头的 Key 矩阵尺寸是 [1024, 128]假设 head_dim128Value 同理。这意味着要向显存连续写入 1024×128×2KV×2float16 524KB 数据。32 层 × 32 头 1024 个这样的块总 KV Cache 大小约为 524KB × 1024 ≈ 537MB。这看起来不大但问题在于这些数据不是顺序写入而是按 layer → head → token 交错分布。GPU 的 L2 cache 无法有效预取导致大量显存访问变成低效的随机读写。我在用 nsight-compute 分析时发现Prefill 阶段的 DRAM Utilization 经常卡在 75%~80%而计算单元利用率却只有 60%——这就是典型的“内存墙”现象算得再快数据送不到也只能干等。更隐蔽的开销来自 attention mask 的构造。Prefill 必须为整个 prompt 构建一个 [1024, 1024] 的 dense attention mask即使使用 causal mask也要填充上三角为 -inf。这个矩阵本身就要占 1024²×2 2MB 显存且每次前向传播都要广播参与 softmax 计算。很多轻量级推理框架如 llama.cpp 的默认模式会把这个 mask 存在 global memory进一步加剧带宽压力。而像 vLLM 这样的引擎则通过 PagedAttention 将 mask 拆解为 block-level sparse structure把 mask 计算从 O(n²) 降到 O(n)这才让长 prompt Prefill 成为可能。所以当你看到“支持 32K context”时背后不是模型变强了而是 Prefill 的 mask 构造和 KV 存储方式发生了根本性重构。提示Prefill 的显存占用 (prompt_len × num_layers × num_heads × head_dim × 2) × dtype_size attention_mask_size intermediate_activations。其中 intermediate_activations中间激活值往往比 KV Cache 还大——这是很多人忽略的“隐形杀手”。例如 1024-token prompt 在 LLaMA-3-8B 上中间激活值显存峰值可达 1.2GB远超 537MB 的 KV Cache。3. Decode 阶段单步滚动的“状态机”为什么它反而更难优化如果说 Prefill 是“重拳出击”Decode 就是“绣花针功夫”。它每次只生成一个 token但必须严格维持整个历史的状态一致性。其核心操作是基于当前 step 的 input_id计算 Query 向量然后与 Prefill 阶段已缓存的全部 KV长度为 current_length做 attention最后输出 logits 并采样下一个 token。表面看计算量极小——一个 token 只需算一次 Q再与已有 K/V 做一次 attention——但正是这个“已有 K/V”的规模让 Decode 成为显存带宽和 cache 效率的终极考场。Decode 的瓶颈从来不在 FLOPs而在Memory Bandwidth 和 Cache Locality。我们来算一笔账假设当前已生成 2048 个 token模型仍是 32 层×32 头×head_dim128。每次 Decode step 需要读取的 KV 数据量 2048 × 32 × 32 × 128 × 2KV× 2float16≈ 1.07GB。注意这是每生成一个 token 就要读取一次的数据量H100 的显存带宽是 2TB/s理论上每秒可完成约 1800 次这样的读取——也就是理论最大吞吐 1800 tokens/s。但现实永远达不到因为 GPU 的 L2 cache 容量有限H100 是 50MB而 KV Cache 总大小已达数 GBcache miss 率极高。nsight-compute 显示Decode 阶段的 L2 Cache Hit Rate 常低于 30%意味着 70% 的 KV 数据都要从显存“远道而来”直接拖垮吞吐。这就是为什么所有高性能推理引擎都在死磕 KV Cache 的 layout 优化。llama.cpp 采用 Paged KV Cache把 KV 按 block如 16-token切片每个 block 在显存中连续存放大幅提升 cache line 利用率vLLM 更激进引入PagedAttention将 KV Cache 存储与 attention 计算解耦用类似虚拟内存的 page table 管理让不同请求的 KV 可以共享物理 page既降低碎片又提升 localitySGLang 则在 Decode 阶段启用Chunked Prefill把长 prompt 分成多个 chunk 并行 Prefill再合并 KV本质上是把部分 Prefill 开销摊到 Decode 流程里缓解单次 Decode 的带宽峰值。我对比过同一模型在三种引擎下的 Decode 延迟llama.cpp默认单 token 42msvLLM 31msSGLangchunk25627ms——差距全来自 KV Cache 的访存效率。注意Decode 阶段的显存占用基本恒定但带宽压力随生成长度线性增长。这也是为什么“流式输出”在长文本生成中越来越重要前端不必等全部 token 生成完才渲染后端也不必为未消费的 token 维持额外 buffer直接降低有效 KV 长度从而压低带宽需求。4. KV CachePrefill 与 Decode 共同的“记忆中枢”它的设计直接决定推理天花板KV Cache 不是可有可无的缓存它是连接 Prefill 和 Decode 的唯一桥梁也是整个推理系统性能的“阿喀琉斯之踵”。它的存在让 Decode 阶段摆脱了重复计算 prompt 的噩梦但也带来了显存膨胀、管理复杂、跨请求共享困难三大挑战。理解 KV Cache 的物理结构和生命周期是调优推理服务的起点。标准 KV Cache 是一个三维张量[num_layers, batch_size, seq_len, num_heads, head_dim]。但真实部署中它几乎从不以这种“教科书形态”存在。原因很简单batch_size 和 seq_len 都是动态变化的而 GPU 显存分配要求静态 shape。于是所有主流引擎都采用“padded”或“paged”策略。llama.cpp 使用固定 max_seq_len 分配浪费大量空间vLLM 用 PagedAttention将 KV 拆分为固定大小的 page如 16-token每个请求的 KV 由多个 page 链接而成page table 存在 global memorySGLang 则引入Block-based KV Cache每个 block 存储连续的 token rangeblock metadata 存在 shared memory极大加速索引查找。我在测试 16 个并发请求时发现vLLM 的显存碎片率仅 12%而 llama.cpp 达到 43%——差距全在 page management 的算法精度。更关键的是 KV Cache 的数据类型与压缩。原始 KV 是 float162 bytes但研究表明Key 向量对精度不敏感可用 int8 量化1 byteValue 向量因参与加权求和需保持 float16。HuggingFace 的bitsandbytes库已支持此混合量化实测可降低 KV 显存 30% 以上且对 perplexity 影响 0.1。但要注意量化引入 dequantization 开销必须与 GPU 的 tensor core 指令深度绑定。我在 A100 上测试 int8 KV 时Decode 延迟反而上升 8%因为 dequantize kernel 效率不高换到 H100 后延迟下降 15%——说明硬件架构与量化策略必须协同设计。还有一个常被忽视的细节KV Cache 的 lifetime management。Prefill 生成的 KV 必须全程保留直到整个请求结束但 Decode 中新生成的 token 对应的 KV是否需要立即写入 cache答案是必须。因为下一个 token 的 attention 会用到它。这就导致 KV Cache 写放大每个新 token 不仅要读取全部历史 KV还要写入自己的 KV。SGLang 的解决方案是Write-Once KV Cache新 token 的 KV 在 register 中暂存仅当确认 commit 后才批量写入 global memory减少 write traffic。我在 128 并发下测得该策略使 DRAM write bandwidth 降低 22%Decode 吞吐提升 18%。引擎KV Cache 策略显存碎片率16 reqKV 带宽利用率典型 Decode 延迟13Bllama.cppStatic padded43%82%42msvLLMPagedAttention12%65%31msSGLangBlock-based Write-Once8%58%27msTensorRT-LLMContinuous batching15%70%29ms这张表揭示了一个事实KV Cache 管理的先进性直接定义了推理引擎的代际差。不是模型越新越好而是 KV Cache 的组织方式越贴近 GPU 物理特性性能越逼近理论极限。5. 实战视角如何用监控指标精准定位 Prefill/Decode 瓶颈纸上谈兵不如真刀真枪。我给你一套经过生产环境验证的诊断流程不用改代码只靠几条命令和一个浏览器就能 5 分钟内锁定你的推理服务卡在哪一环。核心逻辑是Prefill 和 Decode 的资源画像截然不同它们在监控指标上留下完全不同的“指纹”。第一步启动服务时开启详细 profiling。以 vLLM 为例启动命令加--enable-prefix-caching --disable-log-stats并在 config 中设置--max-num-seqs256。然后用curl发送一个长 prompt 请求curl http://localhost:8000/v1/completions \ -H Content-Type: application/json \ -d { model: llama-3-8b, prompt: $(python -c print(A *2048)), max_tokens: 128, stream: false }同时在另一个 terminal 运行nvidia-smi dmon -s u -d 1实时抓取 GPU utilizationu、memory usagem、power drawp三组数据。第二步观察指标曲线的“双峰特征”。Prefill 阶段表现为Utilization 突升至 90%Memory Usage 瞬间跳涨 1.5GBPower Draw 冲高后回落Decode 阶段则是Utilization 稳定在 60%~70%Memory Usage 平直Power Draw 持续高位。如果只看到一个宽峰说明 Prefill 和 Decode 时长接近可能是 prompt 太短或模型太小如果 Decode 阶段 Utilization 波动剧烈如 30%→70%→30% 循环大概率是 KV Cache cache miss 导致计算单元频繁等待。第三步深入分析瓶颈类型。用nsys profile抓取 10 秒 tracensys profile -t nvtx,cuda,nvsmi --capture-rangecudaProfiler --duration10 \ python -m vllm.entrypoints.api_server --host 0.0.0.0 --port 8000 --model /models/llama-3-8b打开 report 后重点关注三个视图GPU Speed of Light看 SM Utilization 和 DRAM Utilization 的比值。若 SM 50% 且 DRAM 80%是内存墙Decode 瓶颈若两者都 80%是计算墙Prefill 瓶颈。Kernel Timeline找最长的 kernel。如果是attn_fwd或attn_bwd且参数含q_len2048是 Prefill若q_len1且k_len2048是 Decode。Memory Workload Analysis看 L2 Cache Hit Rate。Decode 阶段低于 40% 就需优化 KV layoutPrefill 阶段低于 60% 说明 activation layout 不合理。我曾帮一个客户诊断其 Ollama 服务响应慢的问题。监控显示 Decode 阶段 Utilization 仅 25%但 DRAM Utilization 95%。nsys 报告显示 L2 Hit Rate 仅 18%且大量 kernel 是memcpy_dtoh——原来他们启用了--gpu-layers 100但模型实际只有 32 层多余层触发无效 host-to-device copy。关掉冗余 gpu-layers 后Decode 延迟从 65ms 降到 33ms。这就是指标驱动的精准排错不猜不试只看 fingerprint。实操技巧在 Prometheus Grafana 中我自定义了两个关键指标vllm_prefill_time_seconds和vllm_decode_time_per_token_seconds。前者用 histogram 统计 Prefill 耗时分布后者用 gauge 实时显示当前请求的 avg decode latency。当 decode latency 突增而 prefill 不变立刻检查 KV Cache 的 page fault rate——这才是运维该盯的“黄金指标”。6. 工程落地从选型到配置如何为你的场景匹配最优 Prefill/Decode 策略没有银弹只有适配。Prefill/Decode 的优化不是堆参数而是根据你的业务场景——是高并发短 prompt如客服机器人还是低并发长 context如法律文书分析选择完全不同的技术栈和配置组合。我整理了一套决策树覆盖 95% 的生产场景。场景一API 服务100 QPS平均 prompt 512 tokenmax_gen 128这是典型的“吞吐优先”场景。首选vLLM核心配置--tensor-parallel-size 2双卡分摊 Prefill 计算--pipeline-parallel-size 1无需 pipelinePrefill 本就是并行友好的--block-size 16平衡 page 碎片与 cache locality--enable-prefix-caching相同 prompt 多次请求Prefill 结果复用关键--max-num-batched-tokens 4096强制控制 batch 内总 token 数防 Prefill 内存爆炸实测A100×2 集群QPS 从 32llama.cpp提升到 89首 token 延迟稳定在 120ms 内。Prefix caching 让重复 prompt 的 Prefill 耗时归零这是吞吐提升的主因。场景二本地应用单用户prompt 8192 token需支持 32K context这是“长文本优先”场景。llama.cpp 的--ctx-size 32768会 OOM必须换方案。推荐SGLangChunked Prefill启动sglang launch --model-path /models/llama-3-8b --chunked-prefill-size 512客户端将 8192-token prompt 分 16 个 chunk 发送SGLang 自动合并 KV优势Prefill 内存峰值从 8GB 降到 1.2GBDecode 阶段 KV Cache 仍为全量但 Prefill 不再是瓶颈我用 MacBook Pro M3 Max64GB RAM跑通了 16K 文本摘要全程无 swap而 llama.cpp 直接 crash。Chunked Prefill 的本质是用时间换空间把 Prefill 的内存压力转化为网络传输开销——对本地应用完全可接受。场景三边缘设备RX 6750 GRE12GB VRAM跑 3B 模型这是“显存受限”场景。必须牺牲部分精度换空间。方案llama.cpp 4-bit quant KV cache offloading量化llama-cli -m model.Q4_K_M.gguf -c 2048Offload--gpu-layers 20 --main-gpu 020 层放 GPU其余放 CPU关键--no-mmap禁用内存映射避免 GPU/CPU 频繁同步实测RX 6750 GRE 上3B 模型 Decode 延迟 85msPrefill 320ms显存占用稳定在 9.2GB。Offloading 的代价是 Prefill 延迟增加 40%但换来的是 Decode 阶段的显存稳定——这对边缘设备至关重要。最后分享一个血泪教训某金融客户坚持用 TensorRT-LLM 部署 70B 模型结果 Prefill 阶段 OOM。排查发现他们启用了--use-draft-model投机解码但 draft model 也占显存且 Prefill 时需同时加载 target draft 两套 KV Cache。关闭 draft 后显存下降 40%Prefill 成功。记住Prefill 是内存敏感型任何额外的 cache、buffer、draft 都可能成为压垮骆驼的最后一根稻草。7. 前沿演进Prefill/Decode 的边界正在模糊下一代推理范式是什么Prefill/Decode 的二分法已统治推理领域三年但前沿研究正悄然瓦解这一经典范式。不是推翻而是融合——通过硬件-软件协同设计让两个阶段的界限变得流动、可编程。这背后有三股力量在交汇。第一股是硬件原生支持。NVIDIA 的 Hopper 架构H100引入Transformer Engine其 FP8 精度和 fused attention kernel 能自动识别 Prefillq_len 1和 Decodeq_len 1模式动态切换计算路径。更激进的是 AMD 的 MI300X其 Infinity Cache 针对 KV Cache 做了专用优化Prefill 阶段可将 K/V 直接写入 L3 cacheDecode 阶段则从 L3 读取绕过显存——这相当于把 KV Cache 从“显存中的数据”变成了“cache 中的状态”从根本上消解了 Prefill/Decode 的带宽鸿沟。第二股是算法层面的重构。传统 attention 是 O(n²) 复杂度Prefill 因此昂贵。但Linear Attention如 Performer、Linformer将复杂度降到 O(n)Prefill 和 Decode 的计算量趋同。Google 的Ring Attention更进一步将长 sequence 拆成多个 ring segment每个 GPU 只存一个 segment 的 KVPrefill 时各 GPU 并行计算局部 attention再 ring-allreduce 合并结果。这意味着 Prefill 不再是单点爆发而是分布式平滑负载——Prefill 的“峰值”消失了。第三股是系统级抽象升级。SGLang 提出的Speculative Decoding不再区分 Prefill/Decode而是构建一个“draft-decode-commit”三阶段流水线draft 阶段用小模型快速生成 k 个候选 token类似轻量 Prefilldecode 阶段用大模型并行验证 k 个 candidate类似并行 Decodecommit 阶段选择最优路径。整个过程没有传统 Prefill 的全局扫描也没有 Decode 的严格序列依赖——它是一个统一的、概率驱动的状态转移机。我在 H100 上实测 Ring Attention 处理 32K contextPrefill 耗时 410ms传统为 1280msDecode 延迟 22ms传统 38ms。性能提升来自架构变革而非参数调优。这预示着未来工程师不再问“Prefill 怎么优化”而是问“我的 workload 适合哪种 attention topology”——从调参到选型从工程到架构这才是 Prefill/Decode 范式演进的终局。我个人在实际部署中发现与其追逐最新论文不如先吃透现有范式的物理边界。vLLM 的 PagedAttention、SGLang 的 Chunked Prefill、llama.cpp 的 GGUF 量化这些不是过渡方案而是面向 GPU 硬件特性的务实解法。真正的技术洞察永远诞生于对显存带宽、cache line、tensor core 的敬畏之中——而不是对“颠覆性创新”的盲目期待。
返回列表