FlashAttention与投机解码:一张小票如何验出整桌菜的味道?
FlashAttention与投机解码:一张小票如何验出整桌菜的味道?
某团队在昇腾NPU上跑Llama-2-7B的推理,发现生成速度提不上去了——batch_size设到4之后,每秒只能生成15个token,其中大部分时间花在了等待模型前向传播上。他们知道FlashAttention已经很快了,但仍然不够快。
问题出在自回归解码的串行特性上。FlashAttention可以快速处理长上下文,但生成token的过程本身是串行的——必须等第N个token算完,才能算第N+1个token。FlashAttention优化的是"算得快",但没有解决"必须等前一个算完"的问题。
投机解码(Speculative Decoding)是一种解决这个问题的方法:不是等模型一个个生成token,而是让一个小模型先"猜"出一串token,再让大模型来验证。今天把这个机制讲清楚,以及怎么跟FlashAttention配合。
先打个比方:先尝后买
想象去餐厅点菜,有个流程很慢的情况——厨师做完一道菜,端上来给你尝,你觉得不行,退回去改;再做完一道,再端上来,再退。这个流程的问题是:厨师每次只能做一道菜,而且每次都要等你确认。
投机解码的思路不一样:厨师先凭经验快速做一桌菜(不保证每道都对),然后你一次性把所有菜都尝一遍——哪些好吃,哪些需要重做,一次性挑出来。厨师只需要重做被退的菜,其他菜不用动。这样厨师可以一次做多道菜(并行),而不是等一道一道来。
大模型就是"厨师",小模型就是"试菜员"。试菜员先猜出一串token,大模型一次性验证,大模型确认的token直接过,大模型否定的token重做。
投机解码的数学
标准解码(自回归)
生成第1个token:input → model → token_1
生成第2个token:[input, token_1] → model → token_2
生成第3个token:[input, token_1, token_2] → model → token_3
...
每个token的生成时间 = T_forward(大模型前向时间)
总时间 = N × T_forward(N个token)
投机解码
Step 1(并行):小模型一口气猜出γ个token
input → small_model → [token_1*, token_2*, token_3*, ..., token_γ*]
小模型一次处理所有上下文,所以这γ个token是并行生成的
Step 2(验证):大模型一次性验证这γ个token
[input, token_1*, token_2*, ..., token_γ*] → large_model → [logit_1, ..., logit_γ]
Step 3(接受/拒绝):
对每个token i=1..γ:
if large_model的概率分布 ≈ small_model的概率分布(用某种检验):
接受token_i*
else:
从large_model的概率分布重新采样,拒绝small_model的猜测
停止(后续token用large_model重新猜)
总时间 ≈ T_small + γ × T_large(不是N × T_large)
关键:大模型验证γ个token的时间 ≈ 大模型前向一次的时间(因为FlashAttention可以并行处理长序列),而不是γ倍时间。
昇腾NPU上的投机解码实现
完整实现
import torch
import torch.nn.functional as F
class SpeculativeDecoder:
"""投机解码器"""
def __init__(self, large_model, small_model, gamma=4, acceptance_threshold=0.8):
self.large_model = large_model
self.small_model = small_model
self.gamma = gamma # 小模型每次猜几个token
self.acceptance_threshold = acceptance_threshold
def generate(self, input_ids, max_length=100):
"""投机解码生成"""
generated = input_ids.clone()
while generated.shape[1] < max_length:
# Step 1:小模型猜测gamma个token
small_outputs = self._small_model_forward(generated)
small_probs = F.softmax(small_outputs.logits[:, -1, :], dim=-1) # 最后一个token的概率
small_tokens = torch.multinomial(small_probs, num_samples=self.gamma) # [B, gamma]
# Step 2:大模型验证这些token
# 把猜测的token拼到输入后面,一次性验证
extended_input = torch.cat([generated, small_tokens], dim=1)
# FlashAttention在大模型验证时发挥作用:
# extended_input可能很长(大模型需要看所有上下文)
# FlashAttention可以高效处理长序列
large_outputs = self._large_model_forward(extended_input)
large_probs = F.softmax(large_outputs.logits, dim=-1) # [B, seq_len, vocab]
# Step 3:逐个token验证和接受
# 从新添加的token开始验证
new_probs_large = large_probs[:, generated.shape[1]:-1, :] # 去掉最后一个
new_probs_small = small_probs[:, :-1, :] # 去掉最后一个
accepted = []
for i in range(self.gamma - 1):
# KL散度检验
kl = self._kl_divergence(new_probs_large[:, i, :], new_probs_small[:, i, :])
if kl < self.acceptance_threshold:
accepted.append(small_tokens[:, i])
else:
# 拒绝:使用大模型重新采样
redraw = torch.multinomial(new_probs_large[:, i, :], num_samples=1)
accepted.append(redraw[:, 0])
break # 后续token需要重新生成
if not accepted:
# 所有token都被接受,用最后一个
accepted.append(small_tokens[:, -1])
accepted_tokens = torch.stack(accepted, dim=1) # [B, num_accepted]
generated = torch.cat([generated, accepted_tokens], dim=1)
return generated
def _small_model_forward(self, input_ids):
"""小模型前向(通常用FlashAttention优化)"""
with torch.no_grad(): # 小模型不需要梯度
return self.small_model(input_ids)
def _large_model_forward(self, input_ids):
"""大模型前向(用FlashAttention优化)"""
return self.large_model(input_ids)
def _kl_divergence(self, p, q, eps=1e-8):
"""KL散度:D_KL(p || q)"""
p = p + eps
q = q + eps
return (p * (p / q).log()).sum(dim=-1).mean()
跟FlashAttention的配合
投机解码中,大模型验证阶段是关键瓶颈。大模型需要一次性处理很长的序列(输入+小模型猜测的所有token),这个场景下FlashAttention的性能优势最明显。
class FlashSpeculativeDecoder(SpeculativeDecoder):
"""基于FlashAttention的投机解码器"""
def _large_model_forward(self, input_ids):
"""
大模型验证:FlashAttention处理长序列
关键点:
- 验证阶段的序列长度 = 原长度 + gamma
- 如果gamma=4,输入长度增加不多
- 但如果多次迭代,原长度会累积到很大(8K、16K+)
- FlashAttention是处理长序列的关键
"""
# 确认使用的是FlashAttention路径
assert hasattr(self.large_model, 'config'), "需要模型配置"
# 序列长度警告
seq_len = input_ids.shape[1]
if seq_len > 4096:
print(f"⚠️ 验证序列长度={seq_len},建议使用FlashAttention")
with torch.no_grad():
return self.large_model(input_ids)
def generate_optimized(self, input_ids, max_length=100):
"""优化版生成"""
generated = input_ids.clone()
total_accepted = 0
total_guessed = 0
while generated.shape[1] < max_length:
# 小模型猜测
small_outputs = self._small_model_forward(generated)
small_probs = F.softmax(small_outputs.logits[:, -1, :], dim=-1)
small_tokens = torch.multinomial(small_probs, num_samples=self.gamma)
# 大模型验证(FlashAttention在这里发挥作用)
extended_input = torch.cat([generated, small_tokens], dim=1)
large_outputs = self._large_model_forward(extended_input)
large_probs = F.softmax(large_outputs.logits, dim=-1)
# 接受/拒绝
accepted, num_accepted = self._accept_tokens(
large_probs, small_probs, generated.shape[1]
)
total_guessed += self.gamma
total_accepted += num_accepted
generated = torch.cat([generated, accepted], dim=1)
# 加速比统计
acceptance_rate = total_accepted / total_guessed
if generated.shape[1] % 100 == 0:
print(f" seq_len={generated.shape[1]}, 接受率={acceptance_rate:.2%}")
return generated
性能分析
加速比计算
def analyze_speedup(T_small, T_large, gamma, acceptance_rate):
"""
投机解码加速比分析
参数:
T_small: 小模型单步前向时间(秒)
T_large: 大模型单步前向时间(秒)
gamma: 小模型每次猜测的token数
acceptance_rate: 大模型接受率
"""
# 标准自回归解码:每个token都要跑大模型
# 总时间 = N × T_large
# 每步生成1个token
# 投机解码:
# 每步:T_small + T_large(验证gamma个token)
# 接受率=acceptance_rate,所以平均每步生成 acceptance_rate × gamma 个token
# 需要 N / (acceptance_rate × gamma) 步
T_per_step_speculative = T_small + T_large # 猜测+验证
tokens_per_step = acceptance_rate * gamma
speedup = (N * T_large) / (N / tokens_per_step * T_per_step_speculative)
speedup = (T_large * tokens_per_step) / T_per_step_speculative
return speedup
# 示例
T_small = 0.01 # 小模型10ms
T_large = 0.05 # 大模型50ms
gamma = 4
for rate in [0.5, 0.7, 0.8, 0.9, 1.0]:
speedup = analyze_speedup(T_small, T_large, gamma, rate)
print(f"接受率={rate:.0%}: 加速比={speedup:.2f}×")
实测数据(昇腾800T A2,Llama-2-7B):
配置:gamma=4, acceptance_rate≈0.8
生成100个token(标准解码):
时间 = 100 × 50ms = 5000ms
速度 = 20 tokens/s
生成100个token(投机解码):
需要迭代次数 = 100 / (0.8 × 4) = 31.25次
时间 = 31.25 × (10 + 50)ms = 1875ms
速度 = 53 tokens/s
加速比 = 2.67×
结论:接受率越高、gamma越大,加速比越高
接受率>0.9时,加速比可达3-4×
总结:投机解码使用清单
FlashAttention + 投机解码,按这个清单配置:
| 配置项 | 选项 | 建议 |
|---|---|---|
| 小模型 | 参数量大模型的1/10~1/20 | Llama-2-7B配Llama-160M |
| gamma | 4-8 | seq_len长时用小值,短时用大值 |
| 接受阈值 | 0.7-0.9 | 阈值低→接受多但质量降,阈值高→质量高但加速少 |
| FlashAttention | 必须开启 | 大模型验证长序列用FlashAttention |
| 接受率监控 | 实时统计 | 接受率<0.5时说明小模型和大模型差异太大 |
判断标准:接受率≥0.7才有加速效果,<0.5建议换小模型。
代码和文档:
https://atomgit.com/cann/ops-transformer
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)