写在前面:如果想了解更多关于长视频理解和视频智能体新工作,可以关注笔者的Github仓库:Awesome-Video-Agent

论文简介 🍀

  • 📖 题目:VideoITG:Multimodal Video Understanding with Instructed Temporal Grounding
  • 📅 来源:CVPR 2026
  • 🏫 单位:1、The Hong Kong Polytechnic University;2、Nanjing University;3、NVIDIA;4、Harvard University
  • 🌍 主页:https://nvlabs.github.io/VideoITG
  • 💻 代码:已开源
  • ✒️ 摘要:尽管视频大语言模型(Video-LLMs)在多模态理解与推理任务中展现出了显著潜力,但如何从视频中高效选择最具信息量的帧仍然是一个关键挑战。现有方法试图通过减少帧间冗余或采用无监督事件定位来优化帧采样。然而,这些方法在处理复杂的指令跟随任务以及需要精确时间建模的场景时往往表现不足,导致其在语义对齐和时间推理两方面的性能都受到限制。为了解决上述挑战,论文提出了面向视频的指令式时间定位(VideoITG),这是一个旨在根据用户指令自适应定制帧采样策略的框架。具体而言,论文设计了VidThinker流程,该流程通过生成指令条件化描述、检索相关视频片段以及选择关键帧来实现自动标注,从而提供高效监督。利用VidThinker,论文构建了VideoITG-40K数据集,其中包含4万段视频和50万个时间定位标注。论文的即插即用VideoITG模型利用视频大语言模型的视觉-语言对齐能力与推理能力进行判别式帧选择。VideoITG在多个多模态视频理解基准上持续提升性能,展示了其有效性与潜力。

在这里插入图片描述

  

一、论文阅读

1.1 引言(Introduction)

  • 现有方法的不足:
    • 均匀采样简单但次优,容易遗漏对语义推理和时间推理关键的帧。
    • 仅做冗余压缩的方法虽然能减少帧数,但对复杂指令跟随和精确时间建模支持不足。
    • 扩展序列长度的方法计算开销高,而且存在信息被稀释的风险。
    • 基于问题选帧的方法虽优于均匀采样,但缺少跨帧时间建模,难以处理多事件或时间敏感问题。
    • 现有时间定位方法多依赖单一描述性查询或单时间点检索,泛化性和真实场景鲁棒性有限。
  • 论文的核心思想(贡献):
    • 提出VideoITG,把用户指令直接纳入帧选择过程,实现面向任务的指令驱动时间定位。
    • 提出VidThinker自动标注流程,包含指令式片段描述、片段检索和帧定位三个阶段。
    • 构建VideoITG-40K数据集,包含4万段视频和50万个细粒度、指令对齐标注,规模和指令质量均显著超过已有数据集。

  视频大语言模型(Video-LLMs)的快速发展为视频理解开辟了新的前沿,使其能够处理诸如视频描述、视觉问答,甚至具身智能体交互等复杂任务。然而,这类模型在处理长视频时仍然面临困难,因为高内存成本和计算开销限制了它们对扩展时间上下文的处理能力。一种常见的变通方法是均匀帧采样——这种方法虽然简单,但并不理想——它常常会遗漏对语义推理和时间推理至关重要的关键帧,从而限制整体性能。

  为缓解这一挑战,已有研究从多个方向进行了探索。其中一类方法聚焦于通过池化、相似性剪枝或基于聚类的压缩来减少时空冗余,只保留关键帧。另一类方法通过扩展模型序列长度来捕获长期依赖,但这类策略会带来较高的计算开销,并存在信息被稀释的风险。还有一些方法将以问题为中心的线索纳入帧选择过程,并表明其优于均匀采样。例如,SeViLA将BLIP-2应用于对每一帧进行独立处理,然后选择关键帧并输入视频推理流程。然而,由于缺乏跨帧的时间建模,这类方法难以对多事件或时间敏感型查询进行有效推理。

  尽管在压缩或扩展时间上下文方面已经取得进展,短视频与长视频之间仍然存在显著的性能差距,其主要原因在于缺乏大规模、由指令引导的时间定位数据。当人类分析长视频时,通常不会一次处理所有帧;相反,人们会先快速浏览以获得全局上下文,识别与问题相关的线索,再聚焦到具有判别性的时刻。受这种人类策略启发,论文提出了面向视频的指令式时间定位(VideoITG),将用户指令直接整合到帧选择过程中。与传统时间定位任务主要利用单一描述性查询来定位事件不同,VideoITG引入了由指令驱动的时间推理,能够针对每项任务自适应地定制采样策略。不同于以往的帧选择框架,论文的方法能够处理多时间点和多线索场景,具体体现在:

  • (1)跨视频片段定位时间线索以进行关系推理;
  • (2)采用混合采样来应对动态事件变化;
  • (3)保持整体覆盖,以支持内容验证和描述生成。

  为了支撑VideoITG,论文通过一个名为VidThinker的自动化标注流程构建了一个大规模数据集。如图2所示,VidThinker通过指令条件化片段描述生成、指令引导的相关片段检索以及细粒度帧级定位来实现自动化数据生成。在GPT-4o推理的驱动下,VidThinker模拟了一种“大海捞针”式的过程来检索相关时刻,并针对四种指令类型提供均衡监督:语义型,仅关注外观;运动型,强调动态线索;语义与运动联合型,用于联合推理;以及无线索型,即开放式的视频级提示,需要在整段视频中最大化视觉多样性。由此得到的VideoITG-40K数据集包含4万段视频(时长为30秒至3分钟)和50万个由指令引导的时间定位标注,在规模和指令质量上都比现有时间定位数据集高出4倍以上。在此基础上,论文设计了一系列VideoITG模型,包括文本生成式、基于锚点的因果注意力式以及全注意力池化式,以有效地将时间线索与用户指令对齐。

在这里插入图片描述

  总而言之,论文的主要贡献如下:

  • VideoITG-40K dataset. 论文通过自动化的VidThinker流程构建了VideoITG-40K数据集,其中包含4万段视频和50万个细粒度、与指令对齐的标注,显著扩展了现有时间定位资源的规模与多样性。
  • VideoITG models. 论文提出了三种互补的模型变体,探索了不同的注意力机制与解码机制,提供了一个统一的、即插即用的框架,可适配不同的视频大语言模型。
  • Consistent improvement. 论文还表明,在不同基准和不同模型上,VideoITG能够以更少的帧数提升准确率:在VideoMME上,采用VideoITG选择的16帧可以匹配64帧均匀采样的效果,并且与使用32帧的当前最优方法相当。

  

1.2 VideoITG-40K数据集构建(VideoITG-40K: dataset construction)

1.2.1 VidThinker:自动化标注流程(VidThinker: automated annotation pipeline)

  当人类在长视频中搜索信息时,通常会分三步进行:(i)从指令中提取关键信号;(ii)检索一个粗粒度的时间窗口;(iii)对目标事件进行细粒度定位。因此,论文提出了VidThinker,这是一个完全自动化且具有可解释性的流程,用于模拟这种面向指令引导时间定位的三步推理过程。它由三个相互依赖的阶段组成——指令式片段描述、指令式片段检索和指令式帧定位——在不断缩小搜索空间的同时,加强与指令的对齐。

  i)指令式片段描述(Instructed Clip Captioning):视频 v v v被均匀划分为短片段(每段5秒),记为 { v i } i = 0 n \{v_i\}_{i=0}^{n} {vi}i=0n。对于每个片段,论文使用大语言模型提取能够捕捉完成该指令所需核心信息的显著短语。举例来说,给定问题 q = q= q=“敲鼓的男人在敲鼓时脚在做什么?”以及答案 a = a= a=“移动他的脚”,系统会提炼出关键动作短语: k = k= k=“敲鼓的男人一边移动双脚,一边用手击鼓。”随后,论文将提取出的短语与原始视频片段一起输入视觉语言模型,以循环方式生成片段级描述 { c i } i = 0 n \{c_i\}_{i=0}^{n} {ci}i=0n。提取出的短语作为参考线索,引导模型将注意力集中在每个片段中的显著元素上。然而,视觉语言模型严格遵循视觉证据,只有当提取短语中的信息在当前片段中能够被明确观察到时,才会利用这些信息。这确保了系统不会仅仅基于提取短语产生幻觉或推断内容,从而使描述始终扎根于视觉内容。该过程可表述为:

k = L L M ( q , a ) ,   c i = V L M ( k , v i ) . ( 1 ) k = LLM(q, a),\ c_i = VLM(k, v_i).\quad(1) k=LLM(q,a), ci=VLM(k,vi).(1)

  以这些由指令和答案导出的线索为条件,论文确保每个片段的标注都具有相关性和信息量,从而促进精确的指令式时间定位。

  ii)指令式片段检索(Instructed Clip Retrieval):生成的片段描述 { c i } i = 0 n \{c_i\}_{i=0}^{n} {ci}i=0n按顺序组织,并由大语言模型根据问答对来评估其相关性。论文并非简单地分配二元相关性分数,而是让大语言模型执行思维链推理,显式同时考虑关键词匹配与时间关系,并直接输出相关片段的索引:

I r e l − c l i p = L L M ( { c i } i = 0 n , q , a ) . ( 2 ) \mathcal{I}_{rel-clip} = LLM(\{c_i\}_{i=0}^{n}, q, a). \quad(2) Irelclip=LLM({ci}i=0n,q,a).(2)

  这种思维链提示要求模型基于语义线索和时间线索来为其选择提供理由,而不是仅依赖浅层关键词匹配。这种自动化方式显著提升了相关片段选择的效率与可解释性。

  iii)指令式帧定位(Instructed Frame Localization):在对视频片段进行粗粒度定位之后,VidThinker 会根据指令类型进一步通过选择关键帧来细化标注。对于候选片段中的每一帧,论文提示大语言模型执行一个二分类任务:给定问答对和单帧图像,大语言模型判断该帧与指令是否相关,输出“是”或“否”。形式化地说,对于候选片段中的每一帧 f i f_i fi,大语言模型被提示如下:

y i = L L M ( f i , q , a ) ,  where  y i ∈ { y e s ,   n o } , ( 3 ) y_i = LLM(f_i, q, a),\ \text{where}\ y_i \in \{yes,\ no\}, \quad(3) yi=LLM(fi,q,a), where yi{yes, no},(3)

  其中, y i y_i yi表示帧 f i f_i fi是否与该问答对相关。只有得到肯定响应的帧,即 y i = y e s y_i = yes yi=yes,才会被保留为最终的时间定位结果。这种由指令引导的过滤机制使VidThinker能够以较高精度识别出对指令最具信息量的帧。

  

1.2.2 细粒度定位指令(Fine-grained grounding instruction)

  论文针对每种指令类型采用了细粒度帧选择策略,以确保视觉证据与各个问答任务的推理需求相匹配。由于不同指令对视觉理解的要求不同,论文根据指令是否需要静态语义、动态运动、两者兼具,或完全不需要显式线索(视频级),对指令进行分类。针对每一类,论文采用匹配的帧选择策略,使视觉证据与问答推理需求对齐。

  仅语义(Semantic only):这类指令查询静态外观线索,例如人物、物体、场景。例如:“那个男人在上车之前做了什么?”VidThinker会选择能够显示该男子穿着以及吉他的帧。在相关片段定位之后,论文选择能够捕捉代表性语义线索的多样化帧,以确保覆盖全面。具体而言,论文为每一帧提取CLIP特征,并计算相邻帧之间的余弦相似度;当某一帧与上一个被选中关键帧的相似度低于场景变化阈值时,该帧将被保留。更进一步的算法细节在附录中给出。

  仅运动(Motion only):这类指令聚焦于动态模式,例如类型、速度、方向。论文在定位出的片段内部采用固定速率采样,以捕捉运动演化过程。例如:“这个人是如何从跳板上跳下去的?”VidThinker会选择涵盖起跳、腾空和入水过程的帧。

  语义与运动(Semantic & Motion):这类指令同时需要静态语义和动态变化。论文在与运动相关的区域采用固定速率采样,同时保留语义信息丰富的帧,在两种需求之间取得平衡。例如:“你能描述一下视频中的镜头运动吗?”VidThinker会选择同时呈现手部击鼓和脚部动作的帧。

  无线索(Non Clues):这类指令属于没有明确语义或运动锚点的整段视频级指令。论文在整段视频中采样一组紧凑但多样化的帧,例如开头、中间和结尾,以在尽量减少冗余的同时保证整体覆盖。例如:“请详细描述这个视频。”

在这里插入图片描述

  

1.2.3 数据集统计(Dataset statistics)

  基于所提出的VidThinker流程,论文从LLaVA-Video构建了VideoITG-40K,用于时间定位,共包含4万段视频和50万个由指令驱动的标注。全部标注均由VidThinker自动完成,从而保证了效率、一致性以及与多种指令类型的对齐。视频平均时长为120秒,覆盖三个时长区间:30到60秒、1到2分钟、2到3分钟。每段视频含有10到15组问答对,包括多项选择题和开放式问答。根据表1的总结,VideoITG-40K的规模几乎是DiDeMo(10.6K)和QVHighlights(10.2K)的4倍,并且远超QuerYD(2.6K)和HiREST(3.4K)。与先前基于描述性查询的数据集不同,VideoITG-40K是显式由指令引导的,因此能够实现精确的、由查询条件驱动的时间定位。

在这里插入图片描述

  

1.3 VideoITG:模型设计(VideoITG: model design)

在这里插入图片描述

  在本节中,论文探讨了如何利用VideoITG-40K数据集为指令式时间定位任务训练模型,目标是优化视频帧选择并提升视频大语言模型的性能。如图4所示,该框架由三个模块组成:(1)视觉编码器(例如ViT),将视频帧映射为与文本对齐的视觉特征 F F F;(2)执行指令引导帧选择的VideoITG模块 I r e l \mathcal{I}_{rel} Irel ;以及(3)以所选帧 F I r e l F_{\mathcal{I}_{rel}} FIrel 和问题 q q q 为条件生成答案 a a a 的VideoLLM。该过程可描述如下:

F = V I T ( v ) ( 4 ) I r e l = V i d e o I T G ( F , q ) ( 5 ) a = V i d e o L L M ( F I r e l , q ) ( 6 ) F = VIT(v) \quad(4) \\ \mathcal{I}_{rel} = VideoITG(F, q) \quad(5) \\ a = VideoLLM(F_{\mathcal{I}_{rel}}, q) \quad(6) F=VIT(v)(4)Irel=VideoITG(F,q)(5)a=VideoLLM(FIrel,q)(6)

  VideoITG模块遵循即插即用的设计理念,由两个核心目标驱动:(1)增强视觉token与语言token之间的对齐,以提升指令跟随能力;(2)强化上下文编码,以捕获多粒度的时间线索。基于上述考虑,论文开发了三种模型变体:基于文本生成的分类、基于锚点的分类,以及基于池化的分类,如图4(b)所示。

  变体A:基于文本生成的分类(Variant A: Text-generation-based classification)。如图4(b)左图所示,该变体将指令式时间定位任务重构为一个下一token预测问题,其中模型在视频特征和指令特征的条件下按序输出文本token。这种形式自然与现有视频大语言模型的核心训练范式保持一致,从而保留了其强大的视觉-语言对齐能力和指令跟随能力。类似的生成式框架也已被先前的时间敏感模型采用,例如TimeChat和Grounded-VideoLLM。

  变体B:基于锚点的分类(Variant B: Anchor-based classification)。为了突破逐token生成的方式,该变体采用一种判别式范式,直接在帧级别对视觉token进行分类,如图4(b)中图所示。论文从一个预训练的视频大语言模型初始化该模型,同时保留其因果注意力掩码,以维持时间一致性。然而,因果掩码会阻止视觉token预先访问指令,并限制前面帧利用后续时间线索。为缓解这一限制,论文在指令之后插入一个锚点token,作为每一帧的时间中介。形式上,对于时间戳 t t t处的视频帧,锚点token A t A^t At通过对所有空间位置做全局平均得到:

A t = 1 M ∑ i , j F i j t ,   t ∈ [ 1 , T ] , ( 7 ) A^t = \frac{1}{M}\sum_{i,j} F_{ij}^{t},\ t \in [1, T], \quad(7) At=M1i,jFijt, t[1,T],(7)

  其中, F i j t F_{ij}^{t} Fijt表示第 t t t帧在网格 ( i , j ) (i, j) (i,j)处的视觉特征, M M M是每帧的patch总数。集合 { A t } t = 1 T \{A^t\}_{t=1}^{T} {At}t=1T在因果注意力下建立了跨帧的时间依赖关系。

  变体C:基于池化的分类(Variant C: Pooling-based classification)。由于因果注意力掩码限制了帧间通信,论文进一步移除了这一约束,以实现视觉token与文本token之间的双向全注意力,如图4(b)右图所示。对于每一帧,论文通过平均池化聚合其视觉token,随后接入一个分类头来判断其与指令的相关性,而不再引入显式的锚点token。这种全注意力设计丰富了跨帧的时间上下文建模,并促进了指令与视觉证据之间更强的交互。

  

1.4 实验(Experiments)

1.4.1 实施细节(Implementation Details)

  Implementation Details. 论文遵循LLaVA-Video的训练方法,使用预训练模型作为VideoITG模型预训练阶段的初始化。论文采用SigLIP作为视觉编码器,采用Qwen2作为语言模型。首先,论文在图像描述数据集上训练MLP投影器,批大小为256,学习率为 1 × 10 − 3 1 \times 10^{-3} 1×103。随后,论文在LLaVA-OV-SI和LLaVA-Video数据集上对全部模型参数进行微调。在这一阶段,视频帧采样率设为64,LLM的最大序列长度设为16K。之后,论文在所提出的VideoITG-40K数据集上训练VideoITG模型,并将视频采样率调整为1 fps。

  在训练和推理过程中,论文采用动态token空间尺寸策略。在所有阶段,LLM的学习率均为 2 × 10 − 5 2 \times 10^{-5} 2×105,而在最后阶段,分类头的学习率为 2 × 10 − 4 2 \times 10^{-4} 2×104。为了与其他领先的视频LMM进行公平比较,论文主要使用它们原始论文中的结果;当结果不可用时,论文将这些模型集成到LMMs-Eval[67]中,并在一致设置下进行评估。由于上下文长度限制,VideoITG模型最多支持512帧视频输入(每帧16个视觉token),并默认根据得分选择前32帧。

  

1.4.2 对比实验(Comparison with State-of-the-arts)

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

  

1.4.3 消融实验(Ablation Study)

在这里插入图片描述

  

1.5 结论(Conclusion)

  论文提出了VideoITG,这是一种用于视频大语言模型中与指令对齐的帧选择新框架。该方法的关键在于VidThinker流程,它通过生成细致的、由指令引导的片段描述、检索相关片段,并执行细粒度帧选择,来模拟人工标注。利用这一流程,论文构建了VideoITG-40K数据集,其中包含4万段视频和50万个时间定位标注。基于这一资源,论文开发了即插即用的VideoITG模型,利用视觉-语言对齐与推理能力来处理多样化的时间定位任务。实验表明,VideoITG在多个视频理解基准上持续提升了视频大语言模型的性能,凸显了其在推动指令驱动视频理解方面的有效性与潜力。

  

二、结合代码理解论文方法

2.1 时间定位部分代码解读

2.1.1 整体调度代码(lmms_eval/__main__.py/def cli_evaluate

  cli_evaluate() 的逻辑可以概括为:它是 lmms_eval 的总调度入口,先解析命令行或配置文件中的实验参数,初始化日志系统和 accelerate 运行环境,然后把待执行的一组评测任务逐个交给 cli_evaluate_single() 去真正完成;在每次子评测结束后,它负责做多进程同步、可选的 WandB 日志记录、异常兜底处理,以及最终在主进程上汇总打印结果表格。因此它本身不直接做任务下载、模型推理或指标计算,而是承担“组织参数、分发执行、收集结果、统一收尾”的控制层角色。

  运行命令:

bash scripts/eval_lmms_eval/videomme_grounding.sh

  videomme_grounding.sh脚本内容:

# 指定 HuggingFace 缓存目录,避免权重/数据集重复下载。
export HF_HOME=$(realpath ~/.cache/huggingface)

# 1. 通过 accelerate 启动 8 个进程;
# 2. 调用 lmms_eval 的 CLI 入口;
# 3. 在 VideoMME 任务上,使用 videoitg 模型对视频进行“指令条件下的时间定位/选帧打分”;
# 4. 输出每个样本的帧排序结果到 output_dir/results.jsonl。
python3 -m accelerate.commands.launch \
        --num_processes=8 \
        -m lmms_eval \ # 进入 lmms_eval/__main__.py,对应整个评测框架的统一入口
        --tasks videomme \ # 指定评测任务。videomme 会负责提供视频路径、问题文本和 doc_id
        --model videoitg \ # 指定模型后端为 videoitg,最终会注册到 lmms_eval/models/videoitg.py 里的 VideoITG 类。
        # model_args 会作为字符串传给 VideoITG.__init__:
        # - target_fps=1: 按 1 FPS 的时间密度做初始抽帧;
        # - output_dir=./videomme_result_512: 保存 grounding 结果的目录;
        # - pretrained=nvidia/VideoITG-8B: 使用官方发布的 VideoITG 权重;
        # - num_frames=512: 每个视频最多均匀抽取 512 帧参与打分。
        --model_args target_fps=1,output_dir=./videomme_result_512,pretrained=nvidia/VideoITG-8B,num_frames=512 \
        --batch_size 1 \
        --log_samples \
        --log_samples_suffix videomme \
        --output_path ./logs/

  相关核心代码部分:

# TaskManager 会扫描 lmms_eval/tasks/ 下的 yaml,把 `videomme` 这样的任务名映射到对应的配置文件和处理函数
task_manager = TaskManager(args.verbosity, include_path=args.include_path, model_name=args.model)
......

task_list = args.tasks.split(",")
    #对 grounding 脚本来说,这里会把字符串 `videomme`
    # 匹配成 lmms_eval/tasks/videomme/videomme.yaml
    task_names = task_manager.match_tasks(task_list)
    for task in [task for task in task_list if task not in task_names]:
        if os.path.isfile(task):
            config = utils.load_yaml_config(task)
            task_names.append(config)
    task_missing = [task for task in task_list if task not in task_names and "*" not in task]  # we don't want errors if a wildcard ("*") task name was used

......

# 进入统一评测调度器。
 # 这一步会做三件关键事:
 # 1. 加载任务对象(此时会触发 videomme 数据集下载/解压/读取)
 # 2. 加载模型对象(此时会实例化 videoitg,并加载 pretrained)
 # 3. 调 evaluate() 逐样本运行
 results = evaluator.simple_evaluate(
     model=args.model,
     model_args=args.model_args,
     tasks=task_names,
     num_fewshot=args.num_fewshot,
     batch_size=args.batch_size,
     max_batch_size=args.max_batch_size,
     device=args.device,
     use_cache=args.use_cache,
     limit=args.limit,
     check_integrity=args.check_integrity,
     write_out=args.write_out,
     log_samples=args.log_samples,
     evaluation_tracker=evaluation_tracker,
     system_instruction=args.system_instruction,
     apply_chat_template=args.apply_chat_template,
     fewshot_as_multiturn=args.fewshot_as_multiturn,
     gen_kwargs=args.gen_kwargs,
     task_manager=task_manager,
     verbosity=args.verbosity,
     predict_only=args.predict_only,
     random_seed=args.seed[0],
     numpy_random_seed=args.seed[1],
     torch_random_seed=args.seed[2],
     fewshot_random_seed=args.seed[3],
     cli_args=args,
     datetime_str=datetime_str,
     **request_caching_args,
 )

  

2.1.2 单次评估代码(lmms_eval/evaluator.py/def simple_evaluate

  1️⃣ 初始化评测上下文

  做运行前准备,包括设置 Python、NumPy、Torch 的随机种子,保证评测过程尽量可复现;同时检查任务列表是否为空,并解析命令行传入的 gen_kwargs。如果 model_args 为空,会补成空字符串;如果外部没有传 task_manager,这里也会新建一个。这个阶段的作用是把后续“加载任务、加载模型、执行评测”所需的公共上下文先准备好。

	# 先统一设置随机种子,保证评测过程中的采样、few-shot 构造等尽可能可复现。
    # 对 videomme_grounding.sh 而言,虽然主要是推理排序任务,但框架层仍然会统一做这一步。
    seed_message = []
    if random_seed is not None:
        # See https://github.com/EleutherAI/lm-evaluation-harness/pull/1412
        seed_message.append(f"Setting random seed to {random_seed}")
        random.seed(random_seed)

    if numpy_random_seed is not None:
        seed_message.append(f"Setting numpy seed to {numpy_random_seed}")
        np.random.seed(numpy_random_seed)

    if torch_random_seed is not None:
        seed_message.append(f"Setting torch manual seed to {torch_random_seed}")
        torch.manual_seed(torch_random_seed)

    if seed_message:
        eval_logger.info(" | ".join(seed_message))

    assert tasks != [], "No tasks specified, or no tasks found. Please verify the task names."

    if gen_kwargs:
        # 如果命令行显式传了生成参数,会覆盖 yaml 任务配置中的 generation_kwargs。
        # 不过对 VideoITG grounding 来说,核心输出不是文本生成,而是模型内部写出的帧排序结果。
        gen_kwargs = simple_parse_args_string(gen_kwargs)
        eval_logger.warning(f"generation_kwargs specified through cli, these settings will be used over set parameters in yaml tasks.")
        if gen_kwargs == "":
            gen_kwargs = None

    if model_args is None:
        model_args = ""

    if task_manager is None:
        task_manager = TaskManager(verbosity, model_name=model)

  2️⃣ 实例化任务和模型

  先根据任务名加载任务对象,再根据模型名加载模型类并实例化模型。对你的时间定位脚本来说,这里会把 videomme 解析成任务对象,把 videoitg 解析成 VideoITG 模型对象,并把 pretrained、num_frames、target_fps、output_dir 这些参数注入模型。这个阶段完成后,评测框架里已经拥有“数据任务”和“推理模型”两端实体。

	# 根据任务名加载任务对象。
    # 例如 `tasks=["videomme"]` 时,这里会去解析 videomme.yaml,
    # 然后在任务初始化过程中继续触发数据集下载/解压/读取。
    task_dict = get_task_dict(tasks, task_manager)

    # 根据 `--model videoitg` 找到注册好的模型类,
    # 并用 `--model_args` 实例化模型。
    # VideoITG 的 pretrained、num_frames、target_fps、output_dir 都是在这里注入进去的。
    ModelClass = get_model(model)
    lm = ModelClass.create_from_arg_string(
        model_args,
        {
            "batch_size": batch_size,
            "device": device,
        },
    )

  3️⃣ 对任务配置做统一覆盖和整理

  通过内部函数 _adjust_config() 遍历每个任务,把命令行层面的配置覆盖到具体任务上,比如覆盖 num_fewshot、设置 fewshot 随机种子、在 predict_only 时关闭指标;同时把每个任务对应的数据集对象挂到 lm.task_dict 上。对于 VideoITG 很关键的一点是,这样后续模型在 generate_until() 里就能通过 task/split/doc_id 回查原始样本,拿到视频路径和问题文本。

	# 递归地把命令行层面的覆盖项同步到每个叶子任务:
    # - 覆盖 num_fewshot
    # - predict_only 时关闭指标
    # - 设置 fewshot 随机种子
    # 同时还会把 task.dataset 挂到模型对象的 task_dict 上,
    # 这样 VideoITG 在 generate_until 里就能通过 task/split/doc_id 反查原始样本。
    def _adjust_config(task_dict):
        adjusted_task_dict = {}
        for task_name, task_obj in task_dict.items():
            if isinstance(task_obj, dict):
                adjusted_task_dict = {
                    **adjusted_task_dict,
                    **{task_name: _adjust_config(task_obj)},
                }

            else:
                task_obj = task_dict[task_name]
                if type(task_obj) == tuple:
                    group, task_obj = task_obj
                    if task_obj is None:
                        continue
                # 把每个任务对应的数据集对象缓存到模型实例里,
                # 后续 generate_until 会依赖这个映射拿到 doc_to_visual 所需的原始样本。
                lm.task_dict[task_name] = task_obj.dataset
                if "generate_until" in task_obj.get_config("output_type"):
                    if gen_kwargs is not None:
                        task_obj.set_config(key="generation_kwargs", value=gen_kwargs, update=True)

                if predict_only:
                    eval_logger.info(f"Processing {task_name} in output-only mode. Metrics will not be calculated!")
                    # we have to change the class properties post-hoc. This is pretty hacky.
                    task_obj.override_metric(metric_name="bypass")

                # override tasks' fewshot values to the provided num_fewshot arg value
                # except if tasks have it set to 0 manually in their configs--then we should never overwrite that
                if num_fewshot is not None:
                    if (default_num_fewshot := task_obj.get_config("num_fewshot")) == 0:
                        eval_logger.info(f"num_fewshot has been set to 0 for {task_name} in its config. Manual configuration will be ignored.")
                    else:
                        eval_logger.warning(f"Overwriting default num_fewshot of {task_name} from {default_num_fewshot} to {num_fewshot}")
                        task_obj.set_config(key="num_fewshot", value=num_fewshot)
                else:
                    # if num_fewshot not provided, and the task does not define a default one, default to 0
                    if (default_num_fewshot := task_obj.get_config("num_fewshot")) is None:
                        task_obj.set_config(key="num_fewshot", value=0)
                # fewshot_random_seed set for tasks, even with a default num_fewshot (e.g. in the YAML file)
                task_obj.set_fewshot_seed(seed=fewshot_random_seed)
                # eval_logger.info(f"Setting fewshot random generator seed to {fewshot_random_seed}")

                adjusted_task_dict[task_name] = task_obj

        return adjusted_task_dict

    task_dict = _adjust_config(task_dict)

  4️⃣ 记录实验元信息并进入正式评测

  把本次实验的配置写入 evaluation_tracker,例如模型来源、模型参数、system instruction、chat template 等;然后正式调用 evaluate(…)。也就是说,simple_evaluate() 前三阶段在做“装配”,从这里开始才真正进入“构造 request -> 调模型 -> 聚合结果”的主流程。

	# 进入真正的评测执行阶段:
    # - 构建每个样本的 request
    # - 调用模型的对应接口(如 generate_until)
    # - 回收并聚合结果
    results = evaluate(
        lm=lm,
        task_dict=task_dict,
        limit=limit,
        cache_requests=cache_requests,
        rewrite_requests_cache=rewrite_requests_cache,
        bootstrap_iters=bootstrap_iters,
        write_out=write_out,
        log_samples=True if predict_only else log_samples,
        system_instruction=system_instruction,
        apply_chat_template=apply_chat_template,
        fewshot_as_multiturn=fewshot_as_multiturn,
        verbosity=verbosity,
        cli_args=cli_args,
    )

  5️⃣ 主进程补充评测结果元信息

  evaluate(...) 返回后,如果当前是主进程,就会给结果补充配置和运行环境信息,比如模型名、model_args、batch_size、设备、缓存设置、随机种子、git hash 和日期等。这一阶段不再做推理,而是把结果整理成方便打印、保存和复现实验的结构化数据。

	if lm.rank == 0:
        # 只有主进程负责给最终结果补充配置、设备、随机种子、git hash 等元信息,
        # 然后返回给上层 __main__.py 去打印或落盘。
        if isinstance(model, str):
            model_name = model
        elif hasattr(model, "config") and hasattr(model.config, "_name_or_path"):
            model_name = model.config._name_or_path
        else:
            model_name = type(model).__name__

        # add info about the model and few shot config
        results["config"] = {
            "model": model_name,
            "model_args": model_args,
        }
        # add more detailed model info if available TODO: add model info
        # if isinstance(lm, lm_eval.models.huggingface.HFLM):
        #     results["config"].update(lm.get_model_info())
        # add info about execution
        results["config"].update(
            {
                "batch_size": batch_size,
                "batch_sizes": (list(lm.batch_sizes.values()) if hasattr(lm, "batch_sizes") else []),
                "device": device,
                "use_cache": use_cache,
                "limit": limit,
                "bootstrap_iters": bootstrap_iters,
                "gen_kwargs": gen_kwargs,
                "random_seed": random_seed,
                "numpy_seed": numpy_random_seed,
                "torch_seed": torch_random_seed,
                "fewshot_seed": fewshot_random_seed,
            }
        )
        results["git_hash"] = get_git_commit_hash()
        results["date"] = datetime_str
        # add_env_info(results)  # additional environment info to results
        # add_tokenizer_info(results, lm)  # additional info about tokenizer
        return results
    else:
        return None

  

2.1.3 核心评估代码(lmms_eval/evaluator.py/def evaluate

  1️⃣ 初始化评测过程中的各种容器

  创建一批字典和列表,用来存放整个评测过程中产生的中间结果和最终结果,包括任务级指标、任务配置、样本日志、模型请求、分布式补齐信息、任务层级关系等。它本质上是在为后续的“构造请求、执行推理、回收结果、聚合指标”预留存储空间。对时间定位脚本来说,这里最重要的两个容器是 requests 和 samples:前者保存发给 VideoITG 的请求,后者保存逐样本日志。

results = collections.defaultdict(dict)
versions = collections.defaultdict(dict)
configs = collections.defaultdict(dict)
samples = collections.defaultdict(list)
requests = collections.defaultdict(list)
results_agg = collections.defaultdict(dict)
groups_agg = collections.defaultdict(dict)
padding_requests = collections.defaultdict(int)
task_hierarchy = collections.defaultdict(list)
task_order = collections.defaultdict(int)
task_group_alias = collections.defaultdict(dict)
num_fewshot = collections.defaultdict(int)

eval_tasks = get_task_list(task_dict)
name_to_task = {}

  2️⃣ 遍历任务,构建任务级运行上下文

  遍历所有叶子任务,把每个任务的版本、配置、few-shot 数量、别名等信息整理出来,同时把命令行参数挂到任务对象上。更重要的是,它会调用 task.build_all_requests(...),把数据集样本转换成模型请求对象。对 videomme 来说,这一步会把每个样本变成包含 contexts、doc_to_visual、doc_id、task、split 等信息的 request,为后续 VideoITG.generate_until() 做准备。

for task_output in eval_tasks:
    task: Task = task_output.task
    task_name = task_output.task_name
    task.args = cli_args

    name_to_task[task_name] = task
    ...
    versions[task_name] = task.VERSION
    configs[task_name] = dict(task.dump_config())
    ...
    limit = get_sample_size(task, limit)
    task.build_all_requests(
        limit=limit,
        rank=lm.rank,
        world_size=lm.world_size,
        cache_requests=cache_requests,
        rewrite_requests_cache=rewrite_requests_cache,
        system_instruction=system_instruction,
        apply_chat_template=apply_chat_template,
        fewshot_as_multiturn=fewshot_as_multiturn,
        chat_template=getattr(lm, "apply_chat_template") if apply_chat_template else None,
        tokenizer_name=getattr(lm, "tokenizer_name", "") if apply_chat_template else "",
    )

  3️⃣ 按请求类型整理 request,并准备分布式补齐

  任务构造完 request 之后,这一阶段会把所有 request 按模型接口类型进行分组,比如 generate_until、loglikelihood 等。videomme 在 videomme.yaml 里配置的是 output_type: generate_until,所以它最终会全部归到 requests[“generate_until”]。如果是多卡场景,这一阶段还会计算各个 rank 的 request 数量差异,并算出需要补齐的 padding 数量,确保各卡前向次数一致。

for instance in task.instances:
    reqtype = instance.request_type
    requests[reqtype].append(instance)

if lm.world_size > 1:
    instances_rnk = torch.tensor(len(task._instances), device=lm.device)
    gathered_item = lm.accelerator.gather(instances_rnk).cpu().detach().numpy().tolist()
    reqtype = "loglikelihood" if task.OUTPUT_TYPE == "multiple_choice" else task.OUTPUT_TYPE
    numpad = max(gathered_item) - gathered_item[lm.rank]
    padding_requests[reqtype] += numpad

  4️⃣ 真正调用模型执行推理

  这是 evaluate() 最核心的执行阶段。它会遍历每种 reqtype,把 request 根据重复次数复制、根据多卡补齐需要再扩展,然后直接调用模型上对应的方法,比如 lm.generate_until(cloned_reqs)。对于时间定位脚本,这里实际进入的是 VideoITG.generate_until(),也就是开始读取视频、均匀采样 512 帧、拼接问题文本、计算每帧分数、排序并写出 results.jsonl 的地方。换句话说,真正的帧定位推理是从这里被触发的。

for reqtype, reqs in requests.items():
    eval_logger.info("Running {} requests".format(reqtype))
    cloned_reqs = []
    for req in reqs:
        cloned_reqs.extend([req] * req.repeats)

    if (lm.world_size > 1) and (padding_requests[reqtype] > 0):
        for _ in range(padding_requests[reqtype]):
            cloned_reqs.extend([req] * req.repeats)

    resps = getattr(lm, reqtype)(cloned_reqs)

    for x, req in zip(resps, cloned_reqs):
        req.resps.append(x)

    if lm.world_size > 1:
        lm.accelerator.wait_for_everyone()

  5️⃣ 按样本回收模型输出并做后处理

  模型推理完成后,这一阶段把 request 从“按接口类型组织”重新还原成“按样本组织”。它先按 doc_id 把多个 request 聚合回同一个样本,再调用 task.process_results(...) 把模型输出转成任务指标。同时,如果开启了 log_samples,这里会把样本内容、目标答案、模型响应、过滤后的响应、哈希值等都保存到日志结构中。对 VideoITG grounding 来说,框架层这里也会做任务指标处理,逐样本帧排序结果其实已经在上一阶段由模型内部写入 output_dir/results.jsonl 了。

for task_output in eval_tasks:
    task = task_output.task
    task.apply_filters()

    instances_by_doc_id = collections.defaultdict(list)
    for instance in task.instances:
        instances_by_doc_id[instance.doc_id].append(instance)

    for instances in instances_by_doc_id.values():
        instances.sort(key=lambda x: x.idx)

    for filter_key in task.instances[0].filtered_resps.keys():
        if not cli_args.process_with_media:
            doc_iterator = create_iterator(enumerate(task.eval_docs_no_media), rank=RANK, limit=int(limit) if limit else None, world_size=WORLD_SIZE)
        else:
            doc_iterator = task.doc_iterator(rank=RANK, limit=limit, world_size=WORLD_SIZE)

        for doc_id, doc in doc_iterator:
            requests = instances_by_doc_id[doc_id]
            metrics = task.process_results(doc, [req.filtered_resps[filter_key] for req in requests])

            if log_samples:
                example = {
                    "doc_id": doc_id,
                    "doc": saved_doc,
                    "target": target,
                    "arguments": filtered_arguments,
                    "resps": [req.resps for req in requests],
                    "filtered_resps": [req.filtered_resps[filter_key] for req in requests],
                    ...
                }
                example.update(metrics)
                task_output.logged_samples.append(example)

            for metric, value in metrics.items():
                task_output.sample_metrics[(metric, filter_key)].append(value)

  6️⃣ 释放模型并在多卡场景下汇总各 rank 结果

  后处理完成后,这一阶段先删除模型对象、清理 GPU 显存;如果是多卡环境,再把各 rank 上的样本日志和指标统一 gather 到主进程。它的作用是让后续的最终聚合只在 rank 0 上做,并且使用的是“全量样本结果”,而不是单卡局部结果。

if hasattr(lm, "_model"):
    del lm._model
    torch.cuda.empty_cache()

if WORLD_SIZE > 1:
    for task_output in eval_tasks:
        if log_samples:
            full_samples = [None] * WORLD_SIZE if RANK == 0 else None
            per_rank_samples = []
            for sample in task_output.logged_samples:
                per_rank_samples.append(sample)

            torch.distributed.gather_object(
                obj=per_rank_samples,
                object_gather_list=full_samples,
                dst=0,
            )

            if RANK == 0:
                task_output.logged_samples = list(itertools.chain.from_iterable(full_samples))

        for metrics in task_output.sample_metrics:
            metric_list = [None] * WORLD_SIZE if RANK == 0 else None
            torch.distributed.gather_object(
                obj=task_output.sample_metrics[metrics],
                object_gather_list=metric_list,
                dst=0,
            )
            if RANK == 0:
                task_output.sample_metrics[metrics] = list(itertools.chain.from_iterable(metric_list))

    dist.barrier()

  7️⃣ 主进程聚合任务级指标并构造最终返回结果

  最后只有 rank 0 会进入这一阶段。它会把样本级指标汇总成任务级结果,必要时再继续汇总 group 级结果,并整理出 results_dict,其中包括任务指标、group 指标、任务配置、版本、few-shot 设置、样本数量以及可选的 samples。这个 results_dict 会返回给 simple_evaluate(),再由上层 main.py 打印和保存。

if RANK == 0:
    for task_output in eval_tasks:
        task_output.calculate_aggregate_metric(bootstrap_iters=bootstrap_iters)

    (
        results,
        samples,
        configs,
        versions,
        num_fewshot,
        higher_is_better,
    ) = consolidate_results(eval_tasks)

    if bool(results):
        results, versions, show_group_table, *_ = consolidate_group_results(results, versions, task_dict)

    results_agg, group_agg = prepare_print_tasks(task_dict, results)
    subtask_list = get_subtask_list(task_dict)

    results_dict = {
        "results": dict(results_agg.items()),
        **({"groups": dict(group_agg.items())} if (bool(group_agg) & show_group_table) else {}),
        "group_subtasks": dict(reversed(subtask_list.items())),
        "configs": dict(sorted(configs.items())),
        "versions": dict(sorted(versions.items())),
        "n-shot": dict(sorted(num_fewshot.items())),
        "higher_is_better": dict(sorted(higher_is_better.items())),
        "n-samples": {
            task_output.task_name: {
                "original": len(task_output.task.eval_docs),
                "effective": min(limit if limit else len(task_output.task.eval_docs), len(task_output.task.eval_docs)),
            }
            for task_output in eval_tasks
        },
    }
    if log_samples:
        results_dict["samples"] = dict(samples)
else:
    results_dict = None

if hasattr(lm, "accelerator"):
    lm.accelerator.wait_for_everyone()

return results_dict

  

  

  

  

  

  

  

  

  

写在最后

  由于笔者🖊️精力有限且本文更多的目的是通过📒博客记录学习过程并分享更多知识,因此文中部分描述不太具体,如有不太理解💫的地方可在评论区👀留言。非特殊赶deadline⏰或假期⛱️期间,笔者会经常上线回复💬。如有不便之处,请海涵~

  如果想了解更多关于长视频理解和视频智能体新工作,可以关注笔者的Github仓库:Awesome-Video-Agent

  另外,创造不易,转载请注明出处💗💗💗~

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐