大模型推理基础:Transformer架构、自回归生成、KV Cache、预填充与解码阶段
要搭建企业级vLLM推理服务,首先必须深入理解大语言模型(LLM)在推理时的底层运行机制。vLLM的诸多核心优化——如PagedAttention、连续批处理(Continuous Batching)、张量并行等——都是针对Transformer解码器的计算特征与显存瓶颈设计的。如果对模型推理的基本原理缺乏清晰认知,后续的部署调优、性能诊断和容量规划都将无从下手。本章将系统拆解Transformer架构的推理视角、自回归生成机制、KV Cache的作用原理,以及预填充(Prefill)与解码(Decode)两个关键阶段的计算特征。
Transformer架构的推理视角
Transformer架构自2017年提出以来,已成为大语言模型的事实标准。从推理服务的角度看,我们不需要关注训练阶段的细节,而应聚焦于其推理时的数据流与计算模式。一个标准的Decoder-only Transformer(如GPT系列、LLaMA系列)由多个相同的解码器层堆叠而成,每一层包含两个核心子模块:自注意力机制(Self-Attention)和前馈神经网络(FFN)。
在推理过程中,输入Token序列首先经过Embedding层映射为向量,然后逐层通过解码器。每一层的计算可以概括为:
- 自注意力计算:对输入向量分别乘以三个权重矩阵(WQ、WK、WV),得到Query、Key、Value向量。然后计算Query与所有Key的点积,经过Softmax归一化后加权求和Value,得到注意力输出。
- 残差连接与层归一化:注意力输出与输入相加(残差连接),再经过LayerNorm。
- 前馈网络:通常包含两个线性变换和一个激活函数(如GELU或SwiGLU),对每个Token位置独立计算。
- 再次残差连接与层归一化:FFN输出与输入相加,再经过LayerNorm。
从计算量的角度看,自注意力层的计算复杂度为O(n²·d),其中n是序列长度,d是隐藏维度;而FFN层的计算复杂度为O(n·d²)。当序列较短时(如几十个Token),FFN占主导;当序列较长时(如数千Token),自注意力的平方复杂度成为瓶颈。这直接影响我们在部署vLLM时对最大序列长度的设置——过长的序列不仅消耗大量显存,还会显著增加单次推理延迟。
自回归生成机制
大语言模型的文本生成本质上是自回归(Autoregressive)过程:模型在每一步只预测下一个Token的概率分布,然后从分布中采样(或取最大概率)得到新Token,将其拼接到输入序列末尾,再继续预测下一个Token,直到生成结束符(EOS)或达到最大长度限制。
以输入"请介绍一下vLLM"为例,完整的生成过程如下:
| 步骤 | 输入序列 | 模型输出(概率分布) | 采样结果 |
|---|---|---|---|
| 1 | 请介绍一下vLLM | P(的)=0.35, P(是)=0.20, P(中)=0.15... | 的 |
| 2 | 请介绍一下vLLM的 | P(核心)=0.28, P(架构)=0.22, P(原理)=0.18... | 核心 |
| 3 | 请介绍一下vLLM的核心 | P(特性)=0.30, P(概念)=0.25, P(技术)=0.20... | 特性 |
| ... | ... | ... | ... |
| N | 请介绍一下vLLM的核心特性... | P(<EOS>)=0.85, P(。)=0.10... | <EOS>(停止) |
这种逐Token生成的方式带来了两个重要推论:
- 推理延迟与生成长度成正比:生成100个Token所需的时间大约是生成50个Token的两倍(假设每步耗时相同)。这要求我们在SLA(服务等级协议)设计时,必须明确最大生成长度。
- 无法并行生成:第N个Token的生成必须等待前N-1个Token全部生成完毕。这限制了单请求的吞吐量上限,也是vLLM引入连续批处理来提升整体吞吐的根本原因。
max_tokens参数来限制最大生成长度。过大的max_tokens会占用更多显存(因为需要预留KV Cache空间),过小则可能导致输出被截断。合理的做法是根据业务场景统计历史请求的Token长度分布,设置P95或P99分位值。
KV Cache:原理与显存占用
KV Cache是Transformer推理中最关键的优化技术。其核心思想是:在自回归生成过程中,对于已经计算过的Token,将其Key和Value向量缓存下来,避免重复计算。具体来说,在第t步生成时,我们只需要计算新Token的Query、Key、Value,然后用新Query与所有历史Key(包括缓存的)做注意力计算,再与所有Value加权求和。
KV Cache的显存占用可以用以下公式估算:
显存占用(字节) = 2(K和V) × 层数 × 隐藏维度 × 序列长度 × 精度字节数
以LLaMA-7B模型为例(32层,隐藏维度4096,FP16精度),每Token的KV Cache占用为:
2 × 32 × 4096 × 2字节 = 524,288字节 ≈ 0.5MB/Token
这意味着,如果并发处理100个请求,每个请求生成1024个Token,仅KV Cache就需要约50GB显存——这已经超过了单张A100(80GB)的一半。更严重的是,KV Cache的大小是动态变化的(随生成进度增长),传统深度学习框架会预先分配最大可能长度的连续显存块,导致大量碎片和浪费。
预填充与解码阶段
一次完整的推理请求被vLLM明确划分为两个阶段:预填充(Prefill)和解码(Decode)。这两个阶段的计算特征截然不同,对GPU资源的利用方式也完全不同。
预填充阶段(Prefill)
预填充阶段处理用户输入的Prompt部分。此时,模型需要并行计算所有输入Token的Key和Value,并生成第一个输出Token。由于输入Token数量通常较多(几十到几千),这一阶段的计算具有高并行度的特点,GPU的计算单元(CUDA Cores/Tensor Cores)能够被充分利用。从计算性质上看,预填充是计算密集(Compute-Bound)的。
预填充的耗时主要取决于输入Token数量和模型大小。例如,一个2048 Token的Prompt在A100上可能需要数百毫秒。对于交互式应用(如聊天机器人),用户感知的"首Token延迟"(TTFT, Time To First Token)主要由预填充阶段决定。
解码阶段(Decode)
解码阶段逐Token生成输出。每一步只处理一个Token(或一个批次中的多个Token),并行度极低。此时,GPU的计算单元大量空闲,而主要瓶颈在于从显存中读取KV Cache。每一步需要读取所有历史Token的K和V,计算注意力分数。因此,解码阶段是访存密集(Memory-Bound)的。
解码阶段的性能指标通常用吞吐量(每秒生成的Token数)来衡量。在vLLM中,通过连续批处理(Continuous Batching),将多个请求的解码步骤合并为一个批次执行,显著提升了GPU利用率。
| 对比维度 | 预填充(Prefill) | 解码(Decode) |
|---|---|---|
| 处理对象 | 整个Prompt序列 | 单个新Token |
| 并行度 | 高(序列内并行) | 低(仅批次内并行) |
| 计算特征 | 计算密集(Compute-Bound) | 访存密集(Memory-Bound) |
| 关键指标 | TTFT(首Token延迟) | TPOT(每Token输出时间)/吞吐量 |
| GPU利用率 | 高 | 低(无批处理时) |
| vLLM优化 | Chunked Prefill(分块预填充) | Continuous Batching(连续批处理) |
实战场景:理解阶段特征对性能调优的指导意义
假设我们在一台配备4张A100(80GB)的GPU服务器上部署vLLM,服务一个企业级知识库问答应用。业务特征如下:
- 平均Prompt长度:800 Token(用户问题 + 检索到的上下文文档)
- 平均输出长度:300 Token
- 并发请求数:峰值约50个
- SLA要求:TTFT < 1.5秒,TPOT < 50ms(即每秒至少生成20个Token)
初始现象与问题:使用vLLM默认配置部署后,TTFT在低并发时表现良好(约300ms),但当并发达到30以上时,TTFT急剧恶化至3秒以上,且TPOT不稳定,部分请求出现明显卡顿。
分析判断过程:
- 首先检查GPU利用率:使用
nvidia-smi观察发现,GPU利用率在50%左右波动,但显存占用接近90%。这说明瓶颈不在计算能力,而在显存容量和KV Cache管理。 - 计算KV Cache需求:50并发 × 1100 Token(800+300)× 0.5MB/Token ≈ 27.5GB。加上模型权重(约14GB)和激活值,单张A100的80GB显存理论上足够,但vLLM默认的KV Cache预留比例(
gpu_memory_utilization=0.9)可能导致预留空间不足。 - 进一步分析:使用vLLM的
--verbose日志发现,存在大量KV Cache的"驱逐"(Eviction)和重新计算(Recompute)操作。这是因为默认的调度策略在显存压力大时,会抢占某些请求的KV Cache空间,导致这些请求需要重新执行预填充。
实际操作步骤:
# 调整vLLM启动参数
python -m vllm.entrypoints.openai.api_server \
--model /data/models/llama-7b \
--tensor-parallel-size 4 \
--gpu-memory-utilization 0.92 \
--max-num-seqs 64 \
--max-model-len 2048 \
--enable-chunked-prefill \
--chunked-prefill-size 512
关键调整说明:
--enable-chunked-prefill:将长Prompt拆分为512 Token的块,避免单个预填充请求独占GPU计算资源,阻塞其他请求的解码。--max-num-seqs 64:限制最大并发序列数,防止过度订阅导致显存溢出。--gpu-memory-utilization 0.92:略微提高显存利用率,为KV Cache预留更多空间。
结果对比:调整后,TTFT在50并发下稳定在800ms-1.2秒,TPOT稳定在35-45ms,满足SLA要求。GPU利用率提升至75%左右,吞吐量从原来的约800 Token/s提升至1500 Token/s。
风险与代价:Chunked Prefill会增加预填充的总计算时间(因为分块后注意力计算有重叠),但换取了更平滑的调度。对于TTFT极端敏感的场景(如实时语音交互),可能需要关闭Chunked Prefill并增加GPU数量。
复盘经验:理解预填充与解码的计算特征差异,是进行vLLM参数调优的前提。当出现TTFT恶化时,优先检查KV Cache是否充足;当TPOT不稳定时,优先检查是否存在请求抢占和重新计算。这些诊断思路都源于对本章基础概念的深刻理解。