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

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐