论文 _An Image is Worth 1/2 Tokens After Layer 2_|ECCV 2024 Oral|北大 & 阿里 代码: github.com/pkunlp-icler/FastV

核心发现

在LLaVA-1.5中,视觉Token占输入的64%,但在第2层之后,每个视觉Token获得的平均注意力仅为系统提示Token的1/472。信息已在浅层被聚合到少量”锚定Token”上,深层的视觉Token几乎是冗余的。

方法(3个参数定义全部)

  • K:在第K层执行剪枝(默认K=2)
  • R%:剪除R%的视觉Token(默认R=50%)
  • 排序准则:第K层中每个视觉Token收到的平均注意力分数,分数最低的R%被物理删除

前K层正常计算 → 第K层排序+删除 → 后续所有层序列变短,自注意力+FFN全部节省。

结果

K=2, R=50%时,LLaVA-1.5-13B FLOPs降低45%,性能无损(均值73.6→73.6)。13B+FastV延迟(0.341s)低于原始7B(0.344s),精度更高。视频任务FLOPs降低48%且性能略有提升。

已知局限

后续研究(FEATHER, ICCV 2025)发现RoPE导致注意力偏向图像底部Token,FastV的注意力排序有时不如随机剪枝。属于文本相关型方法(注意力含文本信号),不支持预缓存。


在 Qwen3-VL-2B 上复现 FastV

核心难点: FastV官方代码只适配了LLaVA系列。Qwen3-VL架构不同(动态分辨率、mRoPE、视觉Token数量可变),需要改模型forward。但原理很简单,只需改十几行代码。

思路

Qwen3-VL-2B的LLM部分是Qwen3(约28层decoder),视觉Token通过ViT编码后经压缩(32×空间压缩)送入decoder。你需要:

  1. 定位视觉Token的位置:Qwen3-VL在输入中用特殊标记 <|vision_start|><|vision_end|> 包裹视觉Token
  2. 在第K层forward后提取注意力,对视觉Token排序并删除底部R%
  3. 后续层用缩短的序列继续计算

最小可行实现

# 伪代码 — 修改 Qwen3VLForConditionalGeneration 的 forward
 
import torch
from transformers import Qwen2_5_VLForConditionalGeneration, AutoProcessor
# Qwen3-VL transformers>=4.57.0 用 Qwen3VLForConditionalGeneration
# 如果 transformers 版本不够新,Qwen3-VL-2B 结构与 Qwen2.5-VL 类似
 
FASTV_K = 2    # 在第几层剪枝
FASTV_R = 0.5  # 剪除比例
 
def fastv_forward_hook(module, input, output, layer_idx, 
                       image_token_mask, pruned_indices_container):
    """在第K层的attention输出后执行剪枝"""
    if layer_idx != FASTV_K:
        return output
    
    # output: (batch, seq_len, hidden_dim)
    # 需要从该层的attention weights中获取分数
    # 方法:对注意力矩阵沿query维度求平均,得到每个token被关注的程度
    # 注意:需要 attn_implementation="eager" 才能拿到注意力权重
    
    # 简化版:用hidden states的L2范数作为代理指标(避免改attention代码)
    hidden = output[0] if isinstance(output, tuple) else output
    
    # 只对视觉Token计算重要性
    vis_mask = image_token_mask  # (seq_len,) bool
    vis_indices = vis_mask.nonzero(as_tuple=True)[0]
    
    if len(vis_indices) == 0:
        return output
    
    # 计算每个视觉token的重要性(用范数近似注意力)
    vis_hidden = hidden[0, vis_indices]  # (n_vis, hidden_dim)
    scores = vis_hidden.norm(dim=-1)     # (n_vis,)
    
    # 保留 top (1-R) 的token
    n_keep = max(1, int(len(vis_indices) * (1 - FASTV_R)))
    _, topk_idx = scores.topk(n_keep)
    keep_vis = vis_indices[topk_idx.sort().values]
    
    # 存储要保留的全部token索引
    text_indices = (~vis_mask).nonzero(as_tuple=True)[0]
    all_keep = torch.cat([text_indices, keep_vis]).sort().values
    pruned_indices_container['keep'] = all_keep
    
    return output

实际操作步骤

方案A:最简单——在prefill后直接裁剪KV Cache

from transformers import AutoProcessor, Qwen2_5_VLForConditionalGeneration
# 或 Qwen3VLForConditionalGeneration(需 transformers>=4.57)
import torch
 
model_name = "Qwen/Qwen3-VL-2B-Instruct"
model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
    model_name, torch_dtype=torch.bfloat16, 
    attn_implementation="eager",  # 必须用eager才能拿注意力
    device_map="auto"
)
processor = AutoProcessor.from_pretrained(model_name)
 
# 1. 正常编码输入
messages = [{"role": "user", "content": [
    {"type": "image", "image": "your_image.jpg"},
    {"type": "text", "text": "Describe this image."}
]}]
text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = processor(text=[text], images=["your_image.jpg"], 
                   return_tensors="pt").to(model.device)
 
# 2. 手动跑prefill的前K层,提取注意力
# 然后裁掉低注意力的视觉Token对应的KV Cache条目
# 后续层用裁剪后的KV Cache继续decode
 
# 具体实现需要 monkey-patch model.model.layers 的 forward

方案B:更推荐——直接用已适配Qwen系列的开源项目

近期研究已在Qwen2.5-VL-7B(28层decoder)上成功运行FastV及其变体。几个可直接用的项目:

  1. SparseVLMs(ICML 2025):官方代码已适配Qwen系列,github.com/Gumpest/SparseVLMs
  2. VisionZip(CVPR 2025):文本无关方法,github.com/dvlab-research/VisionZip,已有Qwen2-VL/2.5-VL的实验
  3. DART(EMNLP 2025):最简洁的文本无关方法(余弦相似度去重),github.com/ZichenWen1/DART

注意事项

Qwen2.5-VL比LLaVA-1.5有更深的”信息地平线”——视觉Token在更深的层才变得冗余。因此直接用LLaVA上的K=2可能太激进。建议Qwen3-VL-2B上从K=3~5、R=50%开始调参,逐步加大R观察性能变化。

Qwen3-VL的视觉压缩比为32×,本身已比LLaVA的576 Token少很多(同分辨率下),所以Token剪枝的加速收益相对更小,但在高分辨率/视频场景仍有显著价值。