论文 _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。你需要:
- 定位视觉Token的位置:Qwen3-VL在输入中用特殊标记
<|vision_start|>和<|vision_end|>包裹视觉Token - 在第K层forward后提取注意力,对视觉Token排序并删除底部R%
- 后续层用缩短的序列继续计算
最小可行实现
# 伪代码 — 修改 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及其变体。几个可直接用的项目:
- SparseVLMs(ICML 2025):官方代码已适配Qwen系列,
github.com/Gumpest/SparseVLMs - VisionZip(CVPR 2025):文本无关方法,
github.com/dvlab-research/VisionZip,已有Qwen2-VL/2.5-VL的实验 - 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剪枝的加速收益相对更小,但在高分辨率/视频场景仍有显著价值。