LLM Model Basic
Prefill and Decode
在大型语言模型(LLM)的推理过程中,Prefill(预填充)和Decode(解码)是两个核心阶段,分别承担不同的计算任务和资源需求。以下是两者的详细对比:
1. Prefill阶段
功能:处理用户输入的完整提示(prompt),生成首个输出token,并构建初始的KV Cache(Key-Value缓存)。
输入:用户输入的完整token序列(例如“天空为什么是蓝色的?”分词后的多个token)。
输出:
• 首个生成token(如“阳光”);
• KV Cache:存储所有输入token的Key和Value向量,用于后续解码阶段的注意力计算。
计算特点:
- 并行计算:由于输入的所有token已知,模型通过矩阵乘法(GEMM)并行计算自注意力,生成每个token的Key和Value向量。
- 计算密集型:消耗大量计算资源(如GPU算力),尤其长prompt时耗时显著增加。
- 显存占用:KV Cache的显存占用与输入token数正相关(例如Llama-7B模型处理128个token需约134MB显存)。
示例:输入“天空为什么是蓝色的?”(6个token),Prefill阶段完成所有6个token的KV缓存计算,并生成首个输出token“阳光”。
2. Decode阶段
功能:基于KV Cache自回归生成后续token,直至达到终止条件(如输出结束符<eos>或最大生成长度)。
输入:每次仅输入当前生成的单个token(如“阳光”→“中”→“的”…)。
输出:
• 下一个生成token;
• 更新的KV Cache:将新token的Key和Value追加到缓存中。
计算特点:
- 串行生成:每次仅处理一个token,通过矩阵向量乘法(GEMV)计算注意力,依赖历史KV Cache。
- 内存密集型:频繁读写显存中的KV Cache,显存带宽成为瓶颈(例如Llama-7B生成每个token需加载约14GB参数)。
- 延迟敏感:每个token的生成时间(TPOT)直接影响用户体验,通常要求低于50ms。
示例:生成“阳光”后,模型读取缓存中所有7个token(原6个+新1个)的KV向量,计算下一个token“中”,并更新缓存。
对比与优化挑战
| 维度 | Prefill阶段 | Decode阶段 |
|---|---|---|
| 计算类型 | 计算密集型(GEMM) | 内存密集型(GEMV) |
| 并行性 | 高(全token并行) | 低(单token串行) |
| 显存压力 | 初始KV缓存构建 | 动态KV缓存增长与频繁访问 |
| 优化方向 | 算子融合、张量并行 | KV缓存压缩、量化、连续批处理 |
| 关键指标 | 首Token延迟(TTFT) | 每Token延迟(TPOT) |
典型问题与解决方案:
• 显存碎片:vLLM的PagedAttention技术通过分页管理KV Cache,减少碎片。
• 长上下文瓶颈:Mooncake架构提出预填充-解码分离设计,利用分布式存储优化显存占用。
• 吞吐量提升:连续批处理(Continuous Batching)动态调度请求,提高GPU利用率。
总结
• Prefill阶段是模型“理解”用户输入的过程,通过并行计算快速构建上下文;
• Decode阶段是模型“生成”响应的过程,依赖高效显存管理和低延迟计算。
两者共同决定LLM推理的吞吐量和响应速度,优化需结合计算、存储和调度策略。
Prefill和Decode的差异
在大型语言模型(LLM)推理过程中,Prefill和Decode阶段的网络结构虽然相同,但代码实现存在显著差异,主要体现在输入处理方式、计算优化策略和缓存管理上。以下是具体分析及典型开源仓库示例:
一、代码实现差异的核心点
1. 输入处理与计算模式
• Prefill阶段
• 输入:一次性处理完整的用户输入序列(如整个提示词),需将所有token并行编码为嵌入向量,并计算全局自注意力。
• 代码实现:
◦ 使用矩阵乘法(GEMM)批量处理所有token的注意力计算,例如通过torch.bmm实现全序列的QKV矩阵并行计算。
◦ 生成完整的KV缓存(Key-Value Cache),存储所有输入token的键值对,供后续Decode阶段复用。
• 示例代码片段:
1
2
3
4
5# Prefill阶段的注意力计算(伪代码)
q = linear(query_emb) # 全序列并行计算Query
k = linear(key_emb) # 全序列并行计算Key
v = linear(value_emb) # 全序列并行计算Value
attn_output = scaled_dot_product_attention(q, k, v) # 全局注意力
• Decode阶段
• 输入:每次仅处理一个生成的token,基于历史KV缓存进行增量计算。
• 代码实现:
◦ 使用矩阵向量乘法(GEMV)逐个生成token,例如通过torch.mv实现单token的Q与历史K的点积。
◦ KV缓存的动态更新:将新token的Key和Value追加到缓存中,避免重复计算。
• 示例代码片段:
1
2
3
4
5
6# Decode阶段的注意力计算(伪代码)
new_q = linear(new_token_emb) # 仅处理当前token的Query
attn_weights = new_q @ cached_k.transpose() # 仅计算当前token与历史K的点积
attn_output = attn_weights @ cached_v # 基于历史V生成输出
cached_k = torch.cat([cached_k, new_k], dim=1) # 增量更新KV缓存
cached_v = torch.cat([cached_v, new_v], dim=1)
2. 缓存管理与显存优化
• Prefill:需一次性为所有输入token分配显存存储KV缓存,显存占用与输入长度正相关。
• Decode:通过分页缓存(如vLLM的PagedAttention)动态管理显存,支持长序列生成和并发请求的高效处理。
3. 批处理与并行策略
• Prefill:支持静态批处理(Static Batching),将多个请求的输入序列合并为一个大矩阵并行处理,提升计算效率。
• Decode:采用连续批处理(Continuous Batching),动态调度不同长度的生成请求,避免GPU资源空闲。
二、典型开源仓库及实现特点
以下仓库针对Prefill和Decode阶段的差异进行了针对性优化:
1. vLLM
• 核心优化:
◦ PagedAttention:将KV缓存分页管理,支持动态显存分配,显著提升长序列生成效率。
◦ Decode阶段异步调度:通过连续批处理实现高吞吐量,减少GPU空闲时间。
• 代码差异示例:
◦ Prefill阶段调用execute_model_prefill函数处理全序列,生成初始缓存。
◦ Decode阶段调用execute_model_decode函数逐个生成token,并更新分页缓存。
2. Hugging Face Transformers
• 实现方式:
◦ Prefill通过model.generate(input_ids)一次性处理输入,Decode通过自回归循环调用model.generate()生成后续token。
◦ 提供past_key_values参数传递历史KV缓存,避免重复计算。
3. TensorRT-LLM
• 优化重点:
◦ 为Prefill阶段生成高度优化的计算图(如融合注意力核函数),减少内存访问开销。
◦ 在Decode阶段使用Tensor Core加速GEMV运算,提升单token生成速度。
三、总结
• Prefill与Decode的代码差异本质上是并行计算与串行生成的权衡,前者侧重计算密集型任务,后者依赖显存带宽和调度优化。
• 开源仓库通过分页缓存、连续批处理、计算图优化等技术,在工程层面解决了两阶段的性能瓶颈。实际开发中,需根据场景需求选择适配的框架(如高吞吐选vLLM,低延迟选TensorRT-LLM)。