#!/usr/bin/env python3 """Single-file vLLM evaluator for VideoMME, VideoMME-V2, VideoMMMU, MVBench, VSI, MMSI, MindCube, grounding, Tracking, and STVG. This is the only evaluation entry point. It owns: - task defaults - output directory layout - GPU sharding - worker execution - result merging Usage: python eval/task/eval_vllm.py --tasks videomme,vsi --model_path /path/to/model TASKS=mmsi bash eval/task/eval.sh """ from __future__ import annotations import argparse import ast import base64 import csv import io import json import os import re import shlex import subprocess import sys import time from collections import OrderedDict, defaultdict from concurrent.futures import Future, ThreadPoolExecutor from pathlib import Path from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Tuple import numpy as np from PIL import Image PROJECT_DIR = Path(__file__).resolve().parents[2] THIS_FILE = Path(__file__).resolve() THIS_DIR = Path(__file__).resolve().parent PYTHON = sys.executable if str(PROJECT_DIR) not in sys.path: sys.path.insert(0, str(PROJECT_DIR)) if str(THIS_DIR) not in sys.path: sys.path.insert(0, str(THIS_DIR)) from canonical_data import load_json_records # noqa: E402 # Centralized prompts (single source of truth) — see eval/task/eval_prompt.py. from eval_prompt import ( # noqa: E402 PROMPT_TAIL, VIDEO_QA_MC_TAIL, VSI_CENTIMETERS_TAIL, VSI_INTEGER_TAIL, VSI_MC_TAIL, VSI_METERS_TAIL, VSI_SQUARE_METERS_TAIL, ) from mindcube.data_utils import ( # noqa: E402 decode_mindcube_images, load_mindcube_records, mindcube_answer_value, mindcube_group, mindcube_prompt, ) TASKS = ("videomme", "videommev2", "videommmu", "mmvu", "mvbench", "videoholmes", "longvideobench", "lvbench", "mlvu", "vsi", "mmsi", "mindcube", "spatial_grounding", "tracking", "stvg") OUTPUT_TASK_GROUPS = { "videomme": "video_qa", "videommev2": "video_qa", "videommmu": "video_qa", "mmvu": "video_qa", "mvbench": "video_qa", "videoholmes": "video_qa", "longvideobench": "video_qa", "lvbench": "video_qa", "mlvu": "video_qa", "vsi": "spatial_intelligence", "mmsi": "spatial_intelligence", "mindcube": "spatial_intelligence", "stvg": "spatial_temporal_grounding", } CHOICES = list("ABCDEFGH") VIDEOMMMU_CHOICES = list("ABCDEFGHIJ") ANSWER_RE = re.compile(r"\s*(.*?)\s*", re.DOTALL | re.IGNORECASE) TAG_RE = re.compile(r"", re.IGNORECASE) LEGACY_MC_TAIL_RE = re.compile( r"\s*(?:" r"Answer with the option'?s letter from the given choices directly\.?|" r"Answer with the option letter within \.\.\. tags\.?\s*(?:e\.g\.|Example:)?\s*A|" r"Output only the option letter inside \.\.\.\.?\s*Do not explain\.?|" r"Choose the best answer from the options\. Put exactly one uppercase option letter inside \.\.\.\s*Do not explain\. Example: A" r")\s*$", re.IGNORECASE, ) ANSWER_PHRASES = [ "the answer is", "answer is", "the correct answer is", "correct answer is", "the best answer is", "best answer is", "the correct option is", "correct option is", "i choose", "i select", "my answer is", "答案是", "答案为", ] FORMAT_PRIORITY = { "start": 10, "end": 9, "phrase": 7, "parentheses": 6, "period": 5, "colon": 4, "right_paren": 3, "space": 2, "fallback": 0, } # --------------------------------------------------------------------------- # Common parsing / scoring helpers # --------------------------------------------------------------------------- def read_json_or_jsonl(path: str) -> List[Dict[str, Any]]: return load_json_records(path) def strip_think_block(text: str) -> str: text = (text or "").strip() if not text: return "" parts = re.split(r"", text, flags=re.IGNORECASE) if len(parts) > 1: text = parts[-1] else: text = re.sub(r".*?", "", text, flags=re.DOTALL | re.IGNORECASE) return TAG_RE.sub("", text).strip() def strip_answer_tags(text: str) -> str: matches = ANSWER_RE.findall(text or "") return matches[-1].strip() if matches else (text or "").strip() def extract_mcq_answer(response: str, choices: Optional[List[str]] = None) -> str: if not response or not response.strip(): return "" all_choices = choices or CHOICES text = strip_answer_tags(strip_think_block(response)).strip() if not text: return "" for char in [",", ".", "!", "?", ";", ":", "'", '"', "。", ":"]: text = text.strip(char) padded = " " + text + " " candidates = [] for ch in all_choices: for token, fmt in ( (f"({ch})", "parentheses"), (f"{ch}.", "period"), (f"{ch}:", "colon"), (f"{ch})", "right_paren"), (f"{ch} ", "space"), ): pos = padded.rfind(token) if pos != -1: candidates.append((ch, pos, fmt)) lower = padded.lower() for phrase in ANSWER_PHRASES: idx = lower.rfind(phrase.lower()) if idx != -1: after = idx + len(phrase) for ch in all_choices: m = re.search(rf"\b{re.escape(ch)}\b", padded[after:], flags=re.IGNORECASE) if m: candidates.append((ch, after + m.start(), "phrase")) stripped = padded.strip() for ch in all_choices: if stripped.upper() == ch: candidates.append((ch, 0, "start")) elif stripped.startswith(ch) and (len(stripped) == 1 or not stripped[1].isalpha()): candidates.append((ch, 0, "start")) elif stripped.endswith(ch) and (len(stripped) == 1 or not stripped[-2].isalpha()): candidates.append((ch, len(padded) - 1, "end")) if not candidates: for ch in all_choices: m = re.search(rf"\b{re.escape(ch)}\b", padded) if m: candidates.append((ch, m.start(), "fallback")) if not candidates: return "" candidates.sort(key=lambda x: (FORMAT_PRIORITY.get(x[2], 0), x[1]), reverse=True) return candidates[0][0] def _normalized_option_text(value: Any, label: str = "") -> str: text = strip_answer_tags(strip_think_block(str(value or ""))).strip() if label: text = re.sub( rf"^\s*{re.escape(label)}\s*[\.\)::]\s*", "", text, count=1, flags=re.IGNORECASE, ) return " ".join(text.split()).casefold() def mcq_ground_truth(record: Dict[str, Any], labels: List[str]) -> str: raw = next( ( record[key] for key in ("solution", "answer", "ground_truth") if record.get(key) not in (None, "") ), "", ) normalized_raw = _normalized_option_text(raw) options = record.get("options") or record.get("choices") or [] if isinstance(options, list): for index, option in enumerate(options): if index >= len(labels): break label = str(labels[index]) if normalized_raw in { _normalized_option_text(option), _normalized_option_text(option, label), }: return label return extract_mcq_answer(str(raw), labels) def require_mcq_ground_truth( record: Dict[str, Any], labels: List[str], *, task: str, ) -> str: answer = mcq_ground_truth(record, labels) if answer: return answer identifier = record.get("problem_id") or record.get("question_id") or record.get("id") raise ValueError(f"{task} sample {identifier!r} has no mappable ground-truth option") def pct(correct: int, total: int) -> float: return round(100.0 * correct / total, 2) if total else 0.0 def render_chat_prompt(messages: List[Dict[str, Any]], processor, enable_thinking: bool = False) -> str: return processor.apply_chat_template( messages, tokenize=False, add_generation_prompt=True, enable_thinking=enable_thinking, ) def normalize_video_metadata(metadata: Any, frames: Any) -> Dict[str, Any]: if isinstance(metadata, dict): raw = dict(metadata) elif hasattr(metadata, "keys") and hasattr(metadata, "__getitem__"): raw = {k: metadata[k] for k in metadata.keys()} else: total = frames.shape[0] if hasattr(frames, "shape") else len(frames) raw = {"fps": 2.0, "frames_indices": list(range(total)), "total_num_frames": total} return { k: raw[k] for k in ("fps", "frames_indices", "total_num_frames", "video_backend") if k in raw } _VIDEO_CACHE: "OrderedDict[str, Tuple[Any, Dict[str, Any]]]" = OrderedDict() def load_preprocessed_video(path: str): import torch cache_size = int(os.environ.get("VIDEO_CACHE_SIZE", "1")) if cache_size > 0 and path in _VIDEO_CACHE: value = _VIDEO_CACHE.pop(path) _VIDEO_CACHE[path] = value return value data = torch.load(path, map_location="cpu", weights_only=False) frames = data["frames"] value = (frames, normalize_video_metadata(data.get("metadata"), frames)) if cache_size > 0: _VIDEO_CACHE[path] = value while len(_VIDEO_CACHE) > cache_size: _VIDEO_CACHE.popitem(last=False) return value def prepare_preprocessed_video(messages: List[Dict[str, Any]], processor, path: str) -> Dict[str, Any]: frames, metadata = load_preprocessed_video(path) return { "prompt": render_chat_prompt(messages, processor, False), "multi_modal_data": {"video": [(frames, metadata)]}, "mm_processor_kwargs": {"do_sample_frames": False, "do_resize": False}, } def prepare_raw_video(messages: List[Dict[str, Any]], processor, patch_size: int) -> Dict[str, Any]: from qwen_vl_utils import process_vision_info text = render_chat_prompt(messages, processor, False) _images, video_inputs, video_kwargs = process_vision_info( messages, image_patch_size=patch_size, return_video_kwargs=True, return_video_metadata=True, ) video_kwargs = video_kwargs or {} video_kwargs.setdefault("do_resize", False) return { "prompt": text, "multi_modal_data": {"video": video_inputs}, "mm_processor_kwargs": video_kwargs, } # --------------------------------------------------------------------------- # Task defaults and output layout # --------------------------------------------------------------------------- def model_tags(model_path: str) -> Tuple[str, str]: path = Path(model_path.rstrip("/")) if path.name == "huggingface" and path.parent.name == "actor" and path.parent.parent.name.startswith("global_step_"): return path.parent.parent.parent.name, path.parent.parent.name if path.name.startswith("checkpoint-"): return path.parent.parent.name, path.name return path.name, "base" def make_output_dir(task: str, model_path: str, setting: str) -> Path: family, ckpt = model_tags(model_path) group = OUTPUT_TASK_GROUPS.get(task) task_directory = Path(group) / task if group is not None else Path(task) out = ( PROJECT_DIR / "outputs" / family / task_directory / ckpt / setting / time.strftime("%Y%m%d_%H%M%S") ) out.mkdir(parents=True, exist_ok=True) return out def json_safe(value: Any) -> Any: if isinstance(value, Path): return str(value) if isinstance(value, dict): return {str(k): json_safe(v) for k, v in value.items()} if isinstance(value, (list, tuple)): return [json_safe(v) for v in value] return value def split_gpus(gpus: str, tp_size: int) -> List[str]: values = [x.strip() for x in gpus.split(",") if x.strip()] if not values: raise ValueError("empty GPU list") duplicates = sorted({x for x in values if values.count(x) > 1}) if duplicates: raise ValueError(f"Duplicate GPU ids in GPUS={gpus}: {duplicates}") if len(values) % tp_size != 0: raise ValueError(f"NUM_GPUS={len(values)} must be divisible by TP_SIZE={tp_size}") return [",".join(values[i:i + tp_size]) for i in range(0, len(values), tp_size)] def shard_records(records: List[Dict[str, Any]], chunk: int, index: int) -> List[Dict[str, Any]]: if chunk <= 1: return records mode = os.environ.get("SHARD_MODE", "contiguous").strip().lower() if mode in {"round_robin", "stride", "strided"}: return records[index::chunk] n = len(records) start = n * index // chunk end = n * (index + 1) // chunk return records[start:end] def iter_prefetched_batches( records: List[Dict[str, Any]], batch_size: int, prepare_batch: Callable[[int, List[Dict[str, Any]]], Any], prefetch_batches: int = 1, ) -> Iterator[Any]: """Prepare upcoming eval batches in a background thread while vLLM runs.""" starts = list(range(0, len(records), batch_size)) if not starts: return prefetch_batches = max(0, int(prefetch_batches or 0)) if prefetch_batches <= 0: for start in starts: yield prepare_batch(start, records[start:start + batch_size]) return with ThreadPoolExecutor(max_workers=1) as ex: pending: Dict[int, Future] = {} submit_pos = 0 def submit_until_full() -> None: nonlocal submit_pos while submit_pos < len(starts) and len(pending) < prefetch_batches + 1: start = starts[submit_pos] pending[start] = ex.submit(prepare_batch, start, records[start:start + batch_size]) submit_pos += 1 submit_until_full() for start in starts: fut = pending.pop(start) yield fut.result() submit_until_full() def env_required(name: str) -> str: value = os.environ.get(name, "").strip() if not value: raise ValueError(f"Missing required env var {name}; set it in eval/task/eval.sh or the shell.") return value def env_value(name: str, default: Any) -> Any: value = os.environ.get(name) return default if value is None or value == "" else value def env_int(name: str, default: int) -> int: return int(env_value(name, default)) def env_float(name: str, default: float) -> float: return float(env_value(name, default)) def task_defaults(task: str) -> Dict[str, Any]: if task == "videomme": return { "setting": env_required("VIDEOMME_SETTING"), "data_file": env_required("VIDEOMME_DATA_FILE"), "video_base": env_required("VIDEOMME_VIDEO_BASE"), "video_dir": env_required("VIDEOMME_VIDEO_DIR"), "preprocessed_video_dir": env_required("VIDEOMME_PREPROCESSED_VIDEO_DIR"), "batch_size": env_int("VIDEOMME_BATCH_SIZE", 4), "max_model_len": env_int("VIDEOMME_MAX_MODEL_LEN", 32768), "max_new_tokens": env_int("VIDEOMME_MAX_NEW_TOKENS", 16), "max_num_batched_tokens": env_int("VIDEOMME_MAX_NUM_BATCHED_TOKENS", 32768), "gpu_memory_utilization": env_float("VIDEOMME_GPU_MEMORY_UTILIZATION", 0.90), "video_min_pixels": env_int("VIDEOMME_VIDEO_MIN_PIXELS", 4096), "video_max_pixels": env_int("VIDEOMME_VIDEO_MAX_PIXELS", 262144), "video_total_pixels": env_int("VIDEOMME_VIDEO_TOTAL_PIXELS", 0), "max_frames": env_int("VIDEOMME_MAX_FRAMES", 384), "fps": env_int("VIDEOMME_FPS", 2), "max_samples": env_int("VIDEOMME_MAX_SAMPLES", 0), } if task == "videommev2": return { "setting": env_required("VIDEOMMEV2_SETTING"), "data_file": env_required("VIDEOMMEV2_DATA_FILE"), "video_base": env_required("VIDEOMMEV2_VIDEO_BASE"), "video_dir": env_required("VIDEOMMEV2_VIDEO_DIR"), "preprocessed_video_dir": env_required("VIDEOMMEV2_PREPROCESSED_VIDEO_DIR"), "batch_size": env_int("VIDEOMMEV2_BATCH_SIZE", 1), "max_model_len": env_int("VIDEOMMEV2_MAX_MODEL_LEN", 65536), "max_new_tokens": env_int("VIDEOMMEV2_MAX_NEW_TOKENS", 128), "max_num_batched_tokens": env_int("VIDEOMMEV2_MAX_NUM_BATCHED_TOKENS", 65536), "gpu_memory_utilization": env_float("VIDEOMMEV2_GPU_MEMORY_UTILIZATION", 0.90), "video_min_pixels": env_int("VIDEOMMEV2_VIDEO_MIN_PIXELS", 4096), "video_max_pixels": env_int("VIDEOMMEV2_VIDEO_MAX_PIXELS", 262144), "video_total_pixels": env_int("VIDEOMMEV2_VIDEO_TOTAL_PIXELS", 0), "max_frames": env_int("VIDEOMMEV2_MAX_FRAMES", 384), "fps": env_int("VIDEOMMEV2_FPS", 2), "prompt_mode": str(env_value("VIDEOMMEV2_PROMPT_MODE", "default")), "answer_filter": str(env_value("VIDEOMMEV2_ANSWER_FILTER", "")), "max_samples": env_int("VIDEOMMEV2_MAX_SAMPLES", 0), } if task == "videommmu": return { "setting": env_required("VIDEOMMMU_SETTING"), "data_file": env_required("VIDEOMMMU_DATA_FILE"), "video_root": env_required("VIDEOMMMU_VIDEO_ROOT"), "batch_size": env_int("VIDEOMMMU_BATCH_SIZE", 4), "max_model_len": env_int("VIDEOMMMU_MAX_MODEL_LEN", 65536), "max_new_tokens": env_int("VIDEOMMMU_MAX_NEW_TOKENS", 64), "max_num_batched_tokens": env_int("VIDEOMMMU_MAX_NUM_BATCHED_TOKENS", 65536), "gpu_memory_utilization": env_float("VIDEOMMMU_GPU_MEMORY_UTILIZATION", 0.90), "video_min_pixels": env_int("VIDEOMMMU_VIDEO_MIN_PIXELS", 4096), "video_max_pixels": env_int("VIDEOMMMU_VIDEO_MAX_PIXELS", 262144), "video_total_pixels": env_int("VIDEOMMMU_VIDEO_TOTAL_PIXELS", 0), "max_frames": env_int("VIDEOMMMU_MAX_FRAMES", 128), "fps": env_int("VIDEOMMMU_FPS", 2), "prompt_mode": str(env_value("VIDEOMMMU_PROMPT_MODE", "default")), "image_max_pixels": env_int("VIDEOMMMU_IMAGE_MAX_PIXELS", 1048576), "enable_thinking": env_value("VIDEOMMMU_ENABLE_THINKING", "false").lower() == "true", "temperature": env_float("VIDEOMMMU_TEMPERATURE", 0.0), "top_p": env_float("VIDEOMMMU_TOP_P", 1.0), "top_k": env_int("VIDEOMMMU_TOP_K", -1), "presence_penalty": env_float("VIDEOMMMU_PRESENCE_PENALTY", 0.0), "min_p": env_float("VIDEOMMMU_MIN_P", 0.0), } if task == "mmvu": return { "setting": env_required("MMVU_SETTING"), "data_file": env_required("MMVU_DATA_FILE"), "video_root": env_value("MMVU_VIDEO_ROOT", ""), "batch_size": env_int("MMVU_BATCH_SIZE", 4), "max_model_len": env_int("MMVU_MAX_MODEL_LEN", 65536), "max_new_tokens": env_int("MMVU_MAX_NEW_TOKENS", 64), "max_num_batched_tokens": env_int("MMVU_MAX_NUM_BATCHED_TOKENS", 65536), "gpu_memory_utilization": env_float("MMVU_GPU_MEMORY_UTILIZATION", 0.90), "video_min_pixels": env_int("MMVU_VIDEO_MIN_PIXELS", 4096), "video_max_pixels": env_int("MMVU_VIDEO_MAX_PIXELS", 262144), "video_total_pixels": env_int("MMVU_VIDEO_TOTAL_PIXELS", 0), "max_frames": env_int("MMVU_MAX_FRAMES", 128), "fps": env_int("MMVU_FPS", 2), "prompt_mode": str(env_value("MMVU_PROMPT_MODE", "default")), "enable_thinking": env_value("MMVU_ENABLE_THINKING", "false").lower() == "true", "temperature": env_float("MMVU_TEMPERATURE", 0.0), "top_p": env_float("MMVU_TOP_P", 1.0), "top_k": env_int("MMVU_TOP_K", -1), "presence_penalty": env_float("MMVU_PRESENCE_PENALTY", 0.0), "min_p": env_float("MMVU_MIN_P", 0.0), "max_samples": env_int("MMVU_MAX_SAMPLES", 0), } if task == "mvbench": return { "setting": env_required("MVBENCH_SETTING"), "data_file": env_required("MVBENCH_DATA_FILE"), "video_root": env_value("MVBENCH_VIDEO_ROOT", ""), "batch_size": env_int("MVBENCH_BATCH_SIZE", 4), "max_model_len": env_int("MVBENCH_MAX_MODEL_LEN", 65536), "max_new_tokens": env_int("MVBENCH_MAX_NEW_TOKENS", 64), "max_num_batched_tokens": env_int("MVBENCH_MAX_NUM_BATCHED_TOKENS", 65536), "gpu_memory_utilization": env_float("MVBENCH_GPU_MEMORY_UTILIZATION", 0.90), "video_min_pixels": env_int("MVBENCH_VIDEO_MIN_PIXELS", 4096), "video_max_pixels": env_int("MVBENCH_VIDEO_MAX_PIXELS", 262144), "video_total_pixels": env_int("MVBENCH_VIDEO_TOTAL_PIXELS", 0), "max_frames": env_int("MVBENCH_MAX_FRAMES", 128), "fps": env_int("MVBENCH_FPS", 2), "prompt_mode": str(env_value("MVBENCH_PROMPT_MODE", "default")), "enable_thinking": env_value("MVBENCH_ENABLE_THINKING", "false").lower() == "true", "temperature": env_float("MVBENCH_TEMPERATURE", 0.0), "top_p": env_float("MVBENCH_TOP_P", 1.0), "top_k": env_int("MVBENCH_TOP_K", -1), "presence_penalty": env_float("MVBENCH_PRESENCE_PENALTY", 0.0), "min_p": env_float("MVBENCH_MIN_P", 0.0), "max_samples": env_int("MVBENCH_MAX_SAMPLES", 0), } if task == "videoholmes": return { "setting": env_required("VIDEOHOLMES_SETTING"), "data_file": env_required("VIDEOHOLMES_DATA_FILE"), "video_root": env_value("VIDEOHOLMES_VIDEO_ROOT", ""), "batch_size": env_int("VIDEOHOLMES_BATCH_SIZE", 4), "max_model_len": env_int("VIDEOHOLMES_MAX_MODEL_LEN", 65536), "max_new_tokens": env_int("VIDEOHOLMES_MAX_NEW_TOKENS", 64), "max_num_batched_tokens": env_int("VIDEOHOLMES_MAX_NUM_BATCHED_TOKENS", 65536), "gpu_memory_utilization": env_float("VIDEOHOLMES_GPU_MEMORY_UTILIZATION", 0.90), "video_min_pixels": env_int("VIDEOHOLMES_VIDEO_MIN_PIXELS", 4096), "video_max_pixels": env_int("VIDEOHOLMES_VIDEO_MAX_PIXELS", 262144), "video_total_pixels": env_int("VIDEOHOLMES_VIDEO_TOTAL_PIXELS", 0), "max_frames": env_int("VIDEOHOLMES_MAX_FRAMES", 128), "fps": env_int("VIDEOHOLMES_FPS", 2), "prompt_mode": str(env_value("VIDEOHOLMES_PROMPT_MODE", "default")), "enable_thinking": env_value("VIDEOHOLMES_ENABLE_THINKING", "false").lower() == "true", "temperature": env_float("VIDEOHOLMES_TEMPERATURE", 0.0), "top_p": env_float("VIDEOHOLMES_TOP_P", 1.0), "top_k": env_int("VIDEOHOLMES_TOP_K", -1), "presence_penalty": env_float("VIDEOHOLMES_PRESENCE_PENALTY", 0.0), "min_p": env_float("VIDEOHOLMES_MIN_P", 0.0), "max_samples": env_int("VIDEOHOLMES_MAX_SAMPLES", 0), } if task == "longvideobench": return { "setting": env_required("LONGVIDEOBENCH_SETTING"), "data_file": env_required("LONGVIDEOBENCH_DATA_FILE"), "video_root": env_value("LONGVIDEOBENCH_VIDEO_ROOT", ""), "subtitle_root": env_value("LONGVIDEOBENCH_SUBTITLE_ROOT", ""), "use_subtitles": env_value("LONGVIDEOBENCH_USE_SUBTITLES", "true").lower() == "true", "batch_size": env_int("LONGVIDEOBENCH_BATCH_SIZE", 4), "max_model_len": env_int("LONGVIDEOBENCH_MAX_MODEL_LEN", 65536), "max_new_tokens": env_int("LONGVIDEOBENCH_MAX_NEW_TOKENS", 64), "max_num_batched_tokens": env_int("LONGVIDEOBENCH_MAX_NUM_BATCHED_TOKENS", 65536), "gpu_memory_utilization": env_float("LONGVIDEOBENCH_GPU_MEMORY_UTILIZATION", 0.90), "video_min_pixels": env_int("LONGVIDEOBENCH_VIDEO_MIN_PIXELS", 4096), "video_max_pixels": env_int("LONGVIDEOBENCH_VIDEO_MAX_PIXELS", 262144), "video_total_pixels": env_int("LONGVIDEOBENCH_VIDEO_TOTAL_PIXELS", 0), "max_frames": env_int("LONGVIDEOBENCH_MAX_FRAMES", 128), "fps": env_int("LONGVIDEOBENCH_FPS", 2), "prompt_mode": str(env_value("LONGVIDEOBENCH_PROMPT_MODE", "default")), "enable_thinking": env_value("LONGVIDEOBENCH_ENABLE_THINKING", "false").lower() == "true", "temperature": env_float("LONGVIDEOBENCH_TEMPERATURE", 0.0), "top_p": env_float("LONGVIDEOBENCH_TOP_P", 1.0), "top_k": env_int("LONGVIDEOBENCH_TOP_K", -1), "presence_penalty": env_float("LONGVIDEOBENCH_PRESENCE_PENALTY", 0.0), "min_p": env_float("LONGVIDEOBENCH_MIN_P", 0.0), "max_samples": env_int("LONGVIDEOBENCH_MAX_SAMPLES", 0), } if task == "lvbench": return { "setting": env_required("LVBENCH_SETTING"), "data_file": env_required("LVBENCH_DATA_FILE"), "video_root": env_value("LVBENCH_VIDEO_ROOT", ""), "batch_size": env_int("LVBENCH_BATCH_SIZE", 4), "max_model_len": env_int("LVBENCH_MAX_MODEL_LEN", 65536), "max_new_tokens": env_int("LVBENCH_MAX_NEW_TOKENS", 64), "max_num_batched_tokens": env_int("LVBENCH_MAX_NUM_BATCHED_TOKENS", 65536), "gpu_memory_utilization": env_float("LVBENCH_GPU_MEMORY_UTILIZATION", 0.90), "video_min_pixels": env_int("LVBENCH_VIDEO_MIN_PIXELS", 4096), "video_max_pixels": env_int("LVBENCH_VIDEO_MAX_PIXELS", 262144), "video_total_pixels": env_int("LVBENCH_VIDEO_TOTAL_PIXELS", 0), "max_frames": env_int("LVBENCH_MAX_FRAMES", 128), "fps": env_int("LVBENCH_FPS", 2), "prompt_mode": str(env_value("LVBENCH_PROMPT_MODE", "default")), "enable_thinking": env_value("LVBENCH_ENABLE_THINKING", "false").lower() == "true", "temperature": env_float("LVBENCH_TEMPERATURE", 0.0), "top_p": env_float("LVBENCH_TOP_P", 1.0), "top_k": env_int("LVBENCH_TOP_K", -1), "presence_penalty": env_float("LVBENCH_PRESENCE_PENALTY", 0.0), "min_p": env_float("LVBENCH_MIN_P", 0.0), } if task == "mlvu": return { "setting": env_required("MLVU_SETTING"), "data_file": env_required("MLVU_DATA_FILE"), "video_root": env_value("MLVU_VIDEO_ROOT", ""), "batch_size": env_int("MLVU_BATCH_SIZE", 4), "max_model_len": env_int("MLVU_MAX_MODEL_LEN", 65536), "max_new_tokens": env_int("MLVU_MAX_NEW_TOKENS", 64), "max_num_batched_tokens": env_int("MLVU_MAX_NUM_BATCHED_TOKENS", 65536), "gpu_memory_utilization": env_float("MLVU_GPU_MEMORY_UTILIZATION", 0.90), "video_min_pixels": env_int("MLVU_VIDEO_MIN_PIXELS", 4096), "video_max_pixels": env_int("MLVU_VIDEO_MAX_PIXELS", 262144), "video_total_pixels": env_int("MLVU_VIDEO_TOTAL_PIXELS", 0), "max_frames": env_int("MLVU_MAX_FRAMES", 128), "fps": env_int("MLVU_FPS", 2), "prompt_mode": str(env_value("MLVU_PROMPT_MODE", "default")), "enable_thinking": env_value("MLVU_ENABLE_THINKING", "false").lower() == "true", "temperature": env_float("MLVU_TEMPERATURE", 0.0), "top_p": env_float("MLVU_TOP_P", 1.0), "top_k": env_int("MLVU_TOP_K", -1), "presence_penalty": env_float("MLVU_PRESENCE_PENALTY", 0.0), "min_p": env_float("MLVU_MIN_P", 0.0), "max_samples": env_int("MLVU_MAX_SAMPLES", 0), } if task == "vsi": return { "setting": env_required("VSI_SETTING"), "data_file": env_required("VSI_DATA_FILE"), "preprocessed_video_dir": env_required("VSI_PREPROCESSED_VIDEO_DIR"), "batch_size": env_int("VSI_BATCH_SIZE", 16), "max_model_len": env_int("VSI_MAX_MODEL_LEN", 32768), "max_new_tokens": env_int("VSI_MAX_NEW_TOKENS", 1024), "gpu_memory_utilization": env_float("VSI_GPU_MEMORY_UTILIZATION", 0.90), "video_min_pixels": env_int("VSI_VIDEO_MIN_PIXELS", 65536), "video_max_pixels": env_int("VSI_VIDEO_MAX_PIXELS", 262144), "video_total_pixels": env_int("VSI_VIDEO_TOTAL_PIXELS", 16777216), "max_frames": env_int("VSI_MAX_FRAMES", 128), "fps": env_int("VSI_FPS", 2), } if task == "mmsi": return { "setting": env_required("MMSI_SETTING"), "data_file": env_required("MMSI_DATA_FILE"), "max_images": env_int("MMSI_MAX_IMAGES", 8), "batch_size": env_int("MMSI_BATCH_SIZE", 16), "max_model_len": env_int("MMSI_MAX_MODEL_LEN", 32768), "max_new_tokens": env_int("MMSI_MAX_NEW_TOKENS", 1024), "max_num_batched_tokens": env_int("MMSI_MAX_NUM_BATCHED_TOKENS", 32768), "gpu_memory_utilization": env_float("MMSI_GPU_MEMORY_UTILIZATION", 0.90), } if task == "mindcube": return { "setting": env_required("MINDCUBE_SETTING"), "data_file": env_required("MINDCUBE_DATA_FILE"), "max_images": env_int("MINDCUBE_MAX_IMAGES", 3), "expected_samples": env_int( "MINDCUBE_EXPECTED_SAMPLES", 1050 ), "batch_size": env_int("MINDCUBE_BATCH_SIZE", 16), "max_model_len": env_int("MINDCUBE_MAX_MODEL_LEN", 32768), "max_new_tokens": env_int("MINDCUBE_MAX_NEW_TOKENS", 1024), "max_num_batched_tokens": env_int("MINDCUBE_MAX_NUM_BATCHED_TOKENS", 32768), "gpu_memory_utilization": env_float("MINDCUBE_GPU_MEMORY_UTILIZATION", 0.90), } if task == "spatial_grounding": return { "setting": env_required("SPATIAL_GROUNDING_SETTING"), "data_file": "", "processor_path": env_value("SPATIAL_GROUNDING_PROCESSOR_PATH", os.environ.get("MODEL_PATH", "")), "bench_dir": env_required("SPATIAL_GROUNDING_BENCH_DIR"), "datasets": env_value( "SPATIAL_GROUNDING_DATASETS", "refcoco-val,refcoco-testA,refcoco-testB,refcoco+-val,refcoco+-testA,refcoco+-testB,refcocog-val,refcocog-test", ), "prompt_style": env_value("SPATIAL_GROUNDING_PROMPT_STYLE", "qwen_native"), "coord_system": env_value("SPATIAL_GROUNDING_COORD_SYSTEM", "norm1000"), "bbox_select": env_value("SPATIAL_GROUNDING_BBOX_SELECT", "first"), "enable_thinking": env_value("SPATIAL_GROUNDING_ENABLE_THINKING", "false").lower() == "true", "min_tokens": env_int("SPATIAL_GROUNDING_MIN_TOKENS", 64), "total_tokens": env_int("SPATIAL_GROUNDING_TOTAL_TOKENS", 1024), "batch_size": env_int("SPATIAL_GROUNDING_BATCH_SIZE", 64), "max_model_len": env_int("SPATIAL_GROUNDING_MAX_MODEL_LEN", 32768), "max_new_tokens": env_int("SPATIAL_GROUNDING_MAX_NEW_TOKENS", 1024), "max_num_batched_tokens": env_int("SPATIAL_GROUNDING_MAX_NUM_BATCHED_TOKENS", 32768), "gpu_memory_utilization": env_float("SPATIAL_GROUNDING_GPU_MEMORY_UTILIZATION", 0.85), "max_samples": env_int("SPATIAL_GROUNDING_MAX_SAMPLES", 0), } if task == "tracking": return { "setting": env_required("TRACKING_SETTING"), "data_file": "", "processor_path": env_value("TRACKING_PROCESSOR_PATH", os.environ.get("MODEL_PATH", "")), "bench_dir": env_required("TRACKING_BENCH_DIR"), "base_prefix": env_value("TRACKING_BASE_PREFIX", env_required("TRACKING_BENCH_DIR")), "datasets": env_value("TRACKING_DATASETS", "eval_got10k"), "video_min_pixels": env_int("TRACKING_VIDEO_MIN_PIXELS", 4096), "video_max_pixels": env_int("TRACKING_VIDEO_MAX_PIXELS", 786432), "video_total_pixels": env_int("TRACKING_VIDEO_TOTAL_PIXELS", 8388608), "max_frames": env_int("TRACKING_MAX_FRAMES", 32), "fps": env_int("TRACKING_FPS", 1), "batch_size": env_int("TRACKING_BATCH_SIZE", 64), "max_model_len": env_int("TRACKING_MAX_MODEL_LEN", 32768), "max_new_tokens": env_int("TRACKING_MAX_NEW_TOKENS", 8192), "max_num_batched_tokens": env_int("TRACKING_MAX_NUM_BATCHED_TOKENS", 32768), "gpu_memory_utilization": env_float("TRACKING_GPU_MEMORY_UTILIZATION", 0.95), "enable_thinking": env_value("TRACKING_ENABLE_THINKING", "false").lower() == "true", "prompt_mode": env_value("TRACKING_PROMPT_MODE", "default"), "chunked_reprompt": env_int("TRACKING_CHUNKED_REPROMPT", 0), "reprompt_use_gt_first_box": env_value("TRACKING_REPROMPT_USE_GT_FIRST_BOX", "0") in {"1", "true", "True"}, "max_samples": env_int("TRACKING_MAX_SAMPLES", 0), } if task == "stvg": return { "setting": env_required("STVG_SETTING"), "data_file": "", "processor_path": env_value("STVG_PROCESSOR_PATH", os.environ.get("MODEL_PATH", "")), "bench_dir": env_required("STVG_BENCH_DIR"), "base_prefix": env_value("STVG_BASE_PREFIX", env_required("STVG_BENCH_DIR")), "datasets": env_value("STVG_DATASETS", "eval_stvg"), "video_min_pixels": env_int("STVG_VIDEO_MIN_PIXELS", 65536), "video_max_pixels": env_int("STVG_VIDEO_MAX_PIXELS", 393216), "video_total_pixels": env_int("STVG_VIDEO_TOTAL_PIXELS", 10485760), "max_frames": env_int("STVG_MAX_FRAMES", 128), "fps": env_int("STVG_FPS", 2), "batch_size": env_int("STVG_BATCH_SIZE", 64), "max_model_len": env_int("STVG_MAX_MODEL_LEN", 65536), "max_new_tokens": env_int("STVG_MAX_NEW_TOKENS", 2048), "max_num_batched_tokens": env_int("STVG_MAX_NUM_BATCHED_TOKENS", 65536), "gpu_memory_utilization": env_float("STVG_GPU_MEMORY_UTILIZATION", 0.85), "enable_thinking": env_value("STVG_ENABLE_THINKING", "false").lower() == "true", "prompt_mode": env_value("STVG_PROMPT_MODE", "train_stvg"), "max_samples": env_int("STVG_MAX_SAMPLES", 0), } raise ValueError(task) # --------------------------------------------------------------------------- # VideoMME # --------------------------------------------------------------------------- def videomme_prompt(rec: Dict[str, Any], prompt_mode: str = "default") -> str: question = str(rec.get("question") or rec.get("problem") or "").strip() options = rec.get("options") or rec.get("choices") or [] if isinstance(options, dict): options = [str(options[k]) for k in sorted(options)] elif isinstance(options, str): options = [x.strip() for x in options.split("\n") if x.strip()] else: options = [str(x) for x in options] tail = VIDEO_QA_MC_TAIL if prompt_mode == "explicit_ah": tail = ( "There are 8 options, A through H. Answer with exactly one option " "letter from A, B, C, D, E, F, G, H within ... tags. " "Example: H" ) elif prompt_mode == "careful_ah": tail = ( "Carefully watch the video and compare all options. The correct answer " "may be any option from A, B, C, D, E, F, G, or H. Do not favor earlier " "options. Answer with exactly one option letter within ... " "tags. Example: G" ) return ( f"{question}\n" f"Options:\n" + "\n".join(options) + "\n" f"{tail}" ) def videomme_preproc_path(rec: Dict[str, Any], root: str) -> str: value = rec.get("preprocessed_video") or rec.get("preprocessed_video_path") if not value: return "" value = str(value) if os.path.isabs(value): return value return os.path.join(root, value) def videomme_video_path(rec: Dict[str, Any], args) -> str: raw = rec.get("path") or rec.get("video_path") or rec.get("file_name") videos = rec.get("videos") if not raw and isinstance(videos, list) and videos: raw = videos[0] video_obj = rec.get("video") if not raw and isinstance(video_obj, dict): raw = video_obj.get("path") or video_obj.get("filename") elif not raw and isinstance(video_obj, str): raw = video_obj candidates: List[str] = [] for base in [getattr(args, "video_dir", ""), getattr(args, "video_base", "")]: if not base: continue if raw: rel = str(raw).lstrip("./").lstrip("/") candidates.extend([os.path.join(base, rel), os.path.join(base, os.path.basename(rel))]) video_id = str(rec.get("videoID") or rec.get("video_id") or "").strip() if video_id: stem, ext = os.path.splitext(video_id) names = [video_id] if ext else [video_id + e for e in (".mp4", ".MP4", ".mkv", ".avi", ".mov", ".webm")] for name in names: candidates.extend([os.path.join(base, name), os.path.join(base, "videos", name), os.path.join(base, "data", name)]) if raw and os.path.isabs(str(raw)): candidates.insert(0, str(raw)) seen = set() for path in candidates: if path and path not in seen: seen.add(path) if os.path.isfile(path): return path return "" def videomme_video_content(path: str, args) -> Dict[str, Any]: item: Dict[str, Any] = { "type": "video", "video": path, "max_pixels": args.video_max_pixels, "max_frames": args.max_frames, "fps": args.fps, } if args.video_min_pixels > 0: item["min_pixels"] = args.video_min_pixels if args.video_total_pixels > 0: item["total_pixels"] = args.video_total_pixels return item def worker_videomme(args) -> None: from transformers import AutoProcessor, AutoTokenizer from vllm import LLM, SamplingParams records = read_json_or_jsonl(args.data_file) records.sort(key=lambda r: (str(r.get("videoID") or r.get("video_id") or ""), str(r.get("question_id") or r.get("id") or ""))) answer_filter = {c for c in str(getattr(args, "answer_filter", "") or "").upper() if c in CHOICES} if answer_filter: records = [ r for r in records if extract_mcq_answer(str(r.get("answer") or r.get("ground_truth") or ""), CHOICES) in answer_filter ] if args.max_samples > 0: records = records[: args.max_samples] records = shard_records(records, args.chunk, args.index) processor = AutoProcessor.from_pretrained(args.model_path, padding_side="left", do_resize=False, trust_remote_code=True) tokenizer = AutoTokenizer.from_pretrained(args.model_path, trust_remote_code=True) tokenizer.padding_side = "left" processor.tokenizer = tokenizer patch_size = getattr(processor.image_processor, "patch_size", 16) llm = LLM( model=args.model_path, tensor_parallel_size=args.tp_size, gpu_memory_utilization=args.gpu_memory_utilization, max_model_len=args.max_model_len, max_num_batched_tokens=args.max_num_batched_tokens, trust_remote_code=True, dtype="bfloat16", limit_mm_per_prompt={"video": 1, "image": 1}, ) sampling = SamplingParams(max_tokens=args.max_new_tokens, temperature=0.0, top_p=1.0, top_k=-1) results = [] def prepare_batch(start: int, batch: List[Dict[str, Any]]): inputs = [] keep = [] for rec in batch: preprocessed_path = videomme_preproc_path( rec, args.preprocessed_video_dir, ) use_preprocessed = bool( args.preprocessed_video_dir and args.preprocessed_video_dir != "__none__" and preprocessed_path and os.path.isfile(preprocessed_path) ) path = ( preprocessed_path if use_preprocessed else videomme_video_path(rec, args) ) if not path: print(f"[warn] VideoMME video not found: video_id={rec.get('videoID') or rec.get('video_id')}", flush=True) continue messages = [{"role": "user", "content": [ {"type": "video", "video": path} if use_preprocessed else videomme_video_content(path, args), {"type": "text", "text": videomme_prompt(rec, getattr(args, "prompt_mode", "default"))}, ]}] if use_preprocessed: inputs.append(prepare_preprocessed_video(messages, processor, path)) else: inputs.append(prepare_raw_video(messages, processor, patch_size)) keep.append(rec) return keep, inputs for keep, inputs in iter_prefetched_batches(records, args.batch_size, prepare_batch, args.prefetch_batches): outs = llm.generate(inputs, sampling_params=sampling) if inputs else [] for rec, out in zip(keep, outs): raw = out.outputs[0].text options = rec.get("options") or rec.get("choices") or [] labels = CHOICES[:len(options)] if isinstance(options, list) and options else list("ABCD") pred = extract_mcq_answer(raw, labels) gt = require_mcq_ground_truth(rec, labels, task=args.task) results.append({ "question_id": rec.get("question_id") or rec.get("id"), "videoID": rec.get("videoID") or rec.get("video_id"), "duration": rec.get("duration", "unknown"), "category": rec.get("domain") or rec.get("category") or "unknown", "sub_category": rec.get("sub_category", "unknown"), "task_category": rec.get("task_type") or rec.get("task_category") or "unknown", "question": rec.get("question") or rec.get("problem"), "answer": gt, "pred_answer": pred, "score": 1.0 if pred and gt and pred == gt else 0.0, "raw_prediction": raw, }) write_json(args.output_dir, f"results_{args.task}_shard{args.index}.json", results) write_json(args.output_dir, f"summary_shard{args.index}.json", aggregate_videomme(results)) def aggregate_videomme(samples: List[Dict[str, Any]]) -> Dict[str, Any]: def group(key: str, order: Optional[List[str]] = None): d = defaultdict(lambda: {"correct": 0, "total": 0}) for s in samples: name = str(s.get(key) or "unknown") d[name]["total"] += 1 d[name]["correct"] += int(s.get("score", 0)) names = order or sorted(d) return {k: {"accuracy": pct(v["correct"], v["total"]), "correct": v["correct"], "total": v["total"]} for k, v in ((k, d[k]) for k in names if k in d)} correct = sum(int(s.get("score", 0)) for s in samples) parsed = sum(1 for s in samples if s.get("pred_answer")) labeled = sum(1 for s in samples if s.get("answer")) return { "num_samples": len(samples), "correct": correct, "accuracy": pct(correct, len(samples)), "parse_rate": pct(parsed, len(samples)), "ground_truth_rate": pct(labeled, len(samples)), "by_duration": group("duration", ["short", "medium", "long"]), "by_category": group("category"), "by_sub_category": group("sub_category"), "by_task_category": group("task_category"), } # --------------------------------------------------------------------------- # VideoMMMU # --------------------------------------------------------------------------- def videommmu_use_separate_images(root: str) -> bool: base = Path(root) images_dir = base / "images" if images_dir.is_dir() and any(images_dir.glob("*.png")): return True ot_dir = base / "Adaptation" / "test-00000-of-00001" return ot_dir.is_dir() and any(ot_dir.glob("*.png")) def videommmu_index(root: str, use_separate_images: bool = False) -> Dict[str, str]: base = Path(root) index: Dict[str, str] = {} suffixes = {".mp4", ".avi", ".mov", ".mkv", ".webm", ".png", ".jpg", ".jpeg", ".webp"} for path in base.glob("*/*"): if not path.is_file() or path.suffix.lower() not in suffixes: continue if use_separate_images and path.parent.name == "question_only": continue if use_separate_images: if path.name not in index: index[path.name] = str(path) continue # Legacy fallback: prefer question_only/*_image.mp4 when separate PNGs are unavailable. if path.parent.name == "question_only" or path.name not in index: index[path.name] = str(path) if path.parent.name == "question_only" and path.stem.endswith("_image"): original_name = path.stem[: -len("_image")] + path.suffix index[original_name] = str(path) for path in base.glob("images/*"): if path.is_file() and path.suffix.lower() in suffixes: index[path.name] = str(path) for path in (base / "Adaptation" / "test-00000-of-00001").glob("*"): if path.is_file() and path.suffix.lower() in suffixes: index[path.name] = str(path) for path in base.glob("*/images/*"): if path.is_file() and path.suffix.lower() in suffixes: index.setdefault(path.name, str(path)) return index def videommmu_resolve(path_value: Any, root: str, index: Dict[str, str]) -> str: raw = str(path_value or "").strip() raw = raw[2:] if raw.startswith("./") else raw if not raw: return "" path = Path(raw) if path.is_absolute() and path.is_file(): return str(path) direct = Path(root) / raw if direct.is_file(): return str(direct) return index.get(path.name, str(direct)) def videommmu_resolve_image(rec: Dict[str, Any], root: str, index: Dict[str, str]) -> str: # Only Adaptation questions carry a separate image. Comprehension/Perception # records have an empty image_path and must stay pure-video; do not fall back # to same-video figures, or we would staple the Adaptation image onto them. declared = str(rec.get("image_path") or rec.get("additional_path") or "").strip() if not declared: return "" for key in ("image_path", "additional_path"): resolved = videommmu_resolve(rec.get(key), root, index) if resolved and os.path.isfile(resolved): return resolved video_stem = Path(str(rec.get("path") or "")).stem if video_stem: by_stem = Path(root) / "images" / f"{video_stem}.png" if by_stem.is_file(): return str(by_stem) problem_id = rec.get("problem_id") if problem_id is not None and str(problem_id).strip(): by_id = Path(root) / "Adaptation" / "test-00000-of-00001" / f"{int(problem_id)}.png" if by_id.is_file(): return str(by_id) return "" def videommmu_safe_total_frames(video_path: str, cache: Dict[str, int]) -> int: if video_path in cache: return cache[video_path] counts: List[int] = [] try: from decord import VideoReader, cpu counts.append(len(VideoReader(video_path, ctx=cpu(0)))) except Exception: pass try: import torchvision.io as io video, _, _ = io.read_video(video_path, output_format="TCHW") counts.append(int(video.size(0))) except Exception: pass cache[video_path] = min(counts) if counts else 0 return cache[video_path] def videommmu_uses_appended_image( rec: Dict[str, Any], video_path: str, has_separate_image: bool = False, ) -> bool: if has_separate_image: return False if rec.get("image_path") or rec.get("additional_path"): return True return "/question_only/" in video_path.replace("\\", "/") def videommmu_video_item( rec: Dict[str, Any], video_path: str, args, frame_count_cache: Dict[str, int], ) -> Dict[str, Any]: item: Dict[str, Any] = { "type": "video", "video": video_path, "min_pixels": args.video_min_pixels, "max_pixels": args.video_max_pixels, } if args.video_total_pixels > 0: item["total_pixels"] = args.video_total_pixels # Adaptation questions either use a separate PNG image or, as a legacy # fallback, question_only/*_image.mp4 with the figure appended. Use # fps+max_frames for both so we do not scan full source videos just to cap # nframes. if ( rec.get("_has_separate_image") or videommmu_uses_appended_image(rec, video_path, bool(rec.get("_has_separate_image"))) ): item["fps"] = args.fps item["max_frames"] = args.max_frames return item if args.max_frames > 0: total = videommmu_safe_total_frames(video_path, frame_count_cache) if total > 0: nframes = min(args.max_frames, total) nframes = max(2, nframes - (nframes % 2)) item["nframes"] = nframes else: item["nframes"] = args.max_frames else: item["fps"] = args.fps return item # OneThinker (Evaluation/Eval/eval_bench.py) answer-format tails, ported but # WITHOUT the reasoning instruction (no-think / direct-answer mode). ONETHINK_MC_TAIL = ( "Please answer this question based on the visual content.\n" "Answer directly without any explanation or reasoning. " "Output only the single option letter (e.g., A, B, C, D, etc.) " "within the ... tags.\n" "Example:\nA" ) ONETHINK_NUM_TAIL = ( "Please answer this question based on the visual content.\n" "Answer directly without any explanation or reasoning. " "Output only the numerical value within the ... tags.\n" "Example:\n3.14" ) # OneThinker's verbatim CoT wrapper (Evaluation/Eval/eval_bench.py QUESTION_TEMPLATE # + TYPE_TEMPLATE). Use prompt_mode=onethink_cot with a large max_new_tokens (8192). ONETHINK_COT_TEMPLATE = ( "{Question}\n" "Please answer this question based on the visual content." "Provide your thinking process between the and tags, and then give your final answer between the and tags." "At the end, you must output the final answer in the format:\n" "\n" ) ONETHINK_COT_MC_TAIL = ( "Please provide only the single option letter (e.g., A, B, C, D, etc.) " "within the ... tags.\n" "Example:\nA" ) ONETHINK_COT_NUM_TAIL = ( "Please provide only the numerical value within the ... tags.\n" "Example:\n3.14" ) def videommmu_prompt_onethink(rec: Dict[str, Any], cot: bool = False) -> Tuple[str, List[str]]: question = str(rec.get("problem") or rec.get("question") or "").strip() options = normalize_choices(rec.get("options")) labels = VIDEOMMMU_CHOICES[:len(options)] if options else list("ABCD") is_numerical = str(rec.get("problem_type") or "").lower() == "numerical" if options and not is_numerical and "Options:" not in question: question += "\nOptions:\n" + "\n".join(str(x) for x in options) if cot: tail = ONETHINK_COT_NUM_TAIL if is_numerical else ONETHINK_COT_MC_TAIL prompt = ONETHINK_COT_TEMPLATE.format(Question=question) + tail else: tail = ONETHINK_NUM_TAIL if is_numerical else ONETHINK_MC_TAIL prompt = f"{question}\n{tail}" return prompt, ([] if is_numerical else labels) # Official Qwen3.5 benchmarking format (model card "Best Practices"): the model # reasons in its native thinking mode (enable_thinking=True) and we only fix the # final-answer format. Do NOT forbid reasoning here. def videommmu_prompt_qwen(rec: Dict[str, Any]) -> Tuple[str, List[str]]: question = str(rec.get("problem") or rec.get("question") or "").strip() options = normalize_choices(rec.get("options")) labels = VIDEOMMMU_CHOICES[:len(options)] if options else list("ABCD") is_numerical = str(rec.get("problem_type") or "").lower() == "numerical" if options and not is_numerical and "Options:" not in question: question += "\nOptions:\n" + "\n".join(str(x) for x in options) if is_numerical: tail = ( "Please answer the question based on the visual content. " "Put your final numerical answer inside tags, " "e.g., 3.14." ) return f"{question}\n{tail}", [] tail = ( "Please answer the question based on the visual content. " "Put only the choice letter inside tags, " "e.g., C." ) return f"{question}\n{tail}", labels def videommmu_prompt( rec: Dict[str, Any], has_separate_image: bool = False, prompt_mode: str = "default", ) -> Tuple[str, List[str]]: if prompt_mode == "onethink": return videommmu_prompt_onethink(rec, cot=False) if prompt_mode == "onethink_cot": return videommmu_prompt_onethink(rec, cot=True) if prompt_mode == "qwen": return videommmu_prompt_qwen(rec) question = str(rec.get("problem") or rec.get("question") or "").strip() options = normalize_choices(rec.get("options")) labels = VIDEOMMMU_CHOICES[:len(options)] if options else list("ABCD") if options and "Options:" not in question: question += "\nOptions:\n" + "\n".join(str(x) for x in options) has_appended_image = bool(rec.get("image_path") or rec.get("additional_path")) and not has_separate_image if has_appended_image: question = re.sub(r"", "the image at the end of the video", question, flags=re.IGNORECASE) question = "The image for this question is at the end of the video.\n" + question else: question = re.sub(r"", "the image", question, flags=re.IGNORECASE) if str(rec.get("problem_type") or "").lower() == "numerical": return ( f"{question}\n" "Answer with the final number in the format NUMBER. Do not explain.", [], ) return ( f"{question}\n{VIDEO_QA_MC_TAIL}", labels, ) def videommmu_extract_number(text: str) -> str: text = strip_answer_tags(strip_think_block(text)) match = re.search(r"[-+]?\d+(?:,\d{3})*(?:\.\d+)?(?:[eE][-+]?\d+)?", text) return match.group(0).replace(",", "") if match else "" def videommmu_score(rec: Dict[str, Any], raw: str, labels: List[str]) -> Tuple[str, str, float]: gt_text = str(rec.get("solution") or rec.get("answer") or rec.get("ground_truth") or "") if str(rec.get("problem_type") or "").lower() == "numerical": pred = videommmu_extract_number(raw) gt = videommmu_extract_number(gt_text) if not pred or not gt: return pred, gt, 0.0 try: pv, gv = float(pred), float(gt) except Exception: return pred, gt, 0.0 return pred, gt, 1.0 if round(pv, 2) == round(gv, 2) else 0.0 pred = extract_mcq_answer(raw, labels) gt = require_mcq_ground_truth(rec, labels, task="videommmu") return pred, gt, 1.0 if pred and gt and pred == gt else 0.0 def prepare_video_image( messages: List[Dict[str, Any]], processor, media_cache_key: Optional[Tuple[Any, ...]] = None, media_cache: Optional[Dict[Tuple[Any, ...], Tuple[Dict[str, Any], Dict[str, Any]]]] = None, enable_thinking: bool = False, ) -> Dict[str, Any]: from qwen_vl_utils import process_vision_info text = render_chat_prompt(messages, processor, enable_thinking) cached = media_cache.get(media_cache_key) if media_cache is not None and media_cache_key is not None else None if cached is None: image_inputs, video_inputs, video_kwargs = process_vision_info( messages, return_video_kwargs=True, return_video_metadata=True, ) multi_modal_data: Dict[str, Any] = {} if image_inputs: multi_modal_data["image"] = image_inputs if video_inputs: multi_modal_data["video"] = video_inputs if media_cache is not None and media_cache_key is not None: # Keep only the most recent media item. Records are path-sorted, so # this avoids repeated decoding for multiple questions on one video # without growing CPU memory across the whole shard. media_cache.clear() media_cache[media_cache_key] = (multi_modal_data, video_kwargs) else: multi_modal_data, video_kwargs = cached return { "prompt": text, "multi_modal_data": multi_modal_data, "mm_processor_kwargs": video_kwargs, } def worker_videommmu(args) -> None: from transformers import AutoProcessor, AutoTokenizer from vllm import LLM, SamplingParams records = read_json_or_jsonl(args.data_file) records.sort(key=lambda r: (str(r.get("path") or ""), str(r.get("problem_id") or ""))) records = shard_records(records, args.chunk, args.index) use_separate_images = videommmu_use_separate_images(args.preprocessed_video_dir) media_index = videommmu_index(args.preprocessed_video_dir, use_separate_images=use_separate_images) if use_separate_images: print("[videommmu] using subject videos + separate PNG images", flush=True) processor = AutoProcessor.from_pretrained(args.model_path, padding_side="left", trust_remote_code=True) tokenizer = AutoTokenizer.from_pretrained(args.model_path, trust_remote_code=True) tokenizer.padding_side = "left" processor.tokenizer = tokenizer llm = LLM( model=args.model_path, tensor_parallel_size=args.tp_size, gpu_memory_utilization=args.gpu_memory_utilization, max_model_len=args.max_model_len, max_num_batched_tokens=args.max_num_batched_tokens, trust_remote_code=True, dtype="bfloat16", limit_mm_per_prompt={"video": 1, "image": 1}, ) sampling_kwargs: Dict[str, Any] = { "max_tokens": args.max_new_tokens, "temperature": args.temperature, "top_p": args.top_p, "top_k": args.top_k, } if args.presence_penalty: sampling_kwargs["presence_penalty"] = args.presence_penalty if args.min_p: sampling_kwargs["min_p"] = args.min_p sampling = SamplingParams(**sampling_kwargs) print(f"[videommmu] sampling={sampling_kwargs} enable_thinking={args.enable_thinking} prompt_mode={args.prompt_mode}", flush=True) results = [] media_cache: Dict[Tuple[Any, ...], Tuple[Dict[str, Any], Dict[str, Any]]] = {} frame_count_cache: Dict[str, int] = {} def prepare_batch(start: int, batch: List[Dict[str, Any]]): inputs, kept, prompts, labels = [], [], {}, {} for j, rec in enumerate(batch): try: video_path = videommmu_resolve(rec.get("path"), args.preprocessed_video_dir, media_index) if not os.path.isfile(video_path): print(f"[warn] skip {rec.get('problem_id')}: video not found: {video_path}", flush=True) continue image_path = videommmu_resolve_image(rec, args.preprocessed_video_dir, media_index) has_separate_image = bool(image_path) rec = {**rec, "_has_separate_image": has_separate_image} prompt, labs = videommmu_prompt(rec, has_separate_image=has_separate_image, prompt_mode=args.prompt_mode) video_item = videommmu_video_item(rec, video_path, args, frame_count_cache) content: List[Dict[str, Any]] = [video_item] if has_separate_image: image_item: Dict[str, Any] = {"type": "image", "image": image_path} if args.image_max_pixels > 0: image_item["max_pixels"] = args.image_max_pixels content.append(image_item) content.append({"type": "text", "text": prompt}) messages = [{"role": "user", "content": content}] media_key = ( video_path, image_path if image_path and os.path.isfile(image_path) else "", args.video_min_pixels, args.video_max_pixels, args.video_total_pixels, args.image_max_pixels, video_item.get("nframes", 0), video_item.get("fps", 0), video_item.get("max_frames", 0), ) inputs.append(prepare_video_image(messages, processor, media_key, media_cache, enable_thinking=args.enable_thinking)) kept.append(j) prompts[j] = prompt labels[j] = labs except Exception as e: print(f"[warn] skip {start + j}: {type(e).__name__}: {e}", flush=True) return start, batch, inputs, kept, prompts, labels for start, batch, inputs, kept, prompts, labels in iter_prefetched_batches(records, args.batch_size, prepare_batch, args.prefetch_batches): outs = llm.generate(inputs, sampling_params=sampling) if inputs else [] for j, out in zip(kept, outs): rec = batch[j] raw = out.outputs[0].text pred, gt, score = videommmu_score(rec, raw, labels[j]) video_name = Path(str(rec.get("path") or "")).name group = video_name.split("_")[1] if "_" in video_name else "unknown" results.append({ "id": str(rec.get("problem_id") or start + j), "bench": "videommmu", "group": group, "question": rec.get("problem") or rec.get("question"), "prompt": prompts[j], "answer": gt, "pred_answer": pred, "raw_prediction": raw, "score": score, }) write_json(args.output_dir, f"results_videommmu_shard{args.index}.json", results) write_json(args.output_dir, f"summary_shard{args.index}.json", aggregate_image_mc("videommmu", results)) # --------------------------------------------------------------------------- # MMVU (multiple-choice, expert-level multi-discipline video understanding) # --------------------------------------------------------------------------- def mmvu_labels(rec: Dict[str, Any]) -> List[str]: labs = rec.get("labels") if isinstance(labs, list) and labs: return [str(x) for x in labs] options = rec.get("options") or [] return CHOICES[: len(options)] if options else list("ABCDE") def mmvu_prompt(rec: Dict[str, Any], prompt_mode: str = "default") -> Tuple[str, List[str]]: question = str(rec.get("problem") or rec.get("question") or "").strip() options = rec.get("options") or [] labels = mmvu_labels(rec) if options and "Options:" not in question: question += "\nOptions:\n" + "\n".join(str(x) for x in options) if prompt_mode == "qwen": tail = ( "Please answer the question based on the visual content. " "Put only the choice letter inside tags, e.g., C." ) else: tail = VIDEO_QA_MC_TAIL return f"{question}\n{tail}", labels def mmvu_video_item(rec: Dict[str, Any], video_path: str, args) -> Dict[str, Any]: item: Dict[str, Any] = { "type": "video", "video": video_path, "min_pixels": args.video_min_pixels, "max_pixels": args.video_max_pixels, "fps": args.fps, "max_frames": args.max_frames, } if args.video_total_pixels > 0: item["total_pixels"] = args.video_total_pixels return item def worker_mmvu(args) -> None: from transformers import AutoProcessor, AutoTokenizer from vllm import LLM, SamplingParams records = read_json_or_jsonl(args.data_file) records.sort(key=lambda r: (str(r.get("path") or ""), str(r.get("id") or ""))) if args.max_samples > 0: records = records[: args.max_samples] records = shard_records(records, args.chunk, args.index) processor = AutoProcessor.from_pretrained(args.model_path, padding_side="left", trust_remote_code=True) tokenizer = AutoTokenizer.from_pretrained(args.model_path, trust_remote_code=True) tokenizer.padding_side = "left" processor.tokenizer = tokenizer llm = LLM( model=args.model_path, tensor_parallel_size=args.tp_size, gpu_memory_utilization=args.gpu_memory_utilization, max_model_len=args.max_model_len, max_num_batched_tokens=args.max_num_batched_tokens, trust_remote_code=True, dtype="bfloat16", limit_mm_per_prompt={"video": 1, "image": 1}, ) sampling_kwargs: Dict[str, Any] = { "max_tokens": args.max_new_tokens, "temperature": args.temperature, "top_p": args.top_p, "top_k": args.top_k, } if args.presence_penalty: sampling_kwargs["presence_penalty"] = args.presence_penalty if args.min_p: sampling_kwargs["min_p"] = args.min_p sampling = SamplingParams(**sampling_kwargs) print(f"[mmvu] sampling={sampling_kwargs} enable_thinking={args.enable_thinking} prompt_mode={args.prompt_mode}", flush=True) results = [] media_cache: Dict[Tuple[Any, ...], Tuple[Dict[str, Any], Dict[str, Any]]] = {} def prepare_batch(start: int, batch: List[Dict[str, Any]]): inputs, kept, prompts, labels = [], [], {}, {} for j, rec in enumerate(batch): try: video_path = str(rec.get("path") or "") if not os.path.isfile(video_path): print(f"[warn] skip {rec.get('id')}: video not found: {video_path}", flush=True) continue prompt, labs = mmvu_prompt(rec, prompt_mode=args.prompt_mode) video_item = mmvu_video_item(rec, video_path, args) messages = [{"role": "user", "content": [video_item, {"type": "text", "text": prompt}]}] media_key = (video_path, args.video_min_pixels, args.video_max_pixels, args.video_total_pixels, args.fps, args.max_frames) inputs.append(prepare_video_image(messages, processor, media_key, media_cache, enable_thinking=args.enable_thinking)) kept.append(j) prompts[j] = prompt labels[j] = labs except Exception as e: print(f"[warn] skip {start + j}: {type(e).__name__}: {e}", flush=True) return start, batch, inputs, kept, prompts, labels for start, batch, inputs, kept, prompts, labels in iter_prefetched_batches(records, args.batch_size, prepare_batch, args.prefetch_batches): outs = llm.generate(inputs, sampling_params=sampling) if inputs else [] for j, out in zip(kept, outs): rec = batch[j] raw = out.outputs[0].text pred = extract_mcq_answer(raw, labels[j]) gt = require_mcq_ground_truth(rec, labels[j], task="mmvu") results.append({ "id": str(rec.get("id") or start + j), "bench": "mmvu", "group": rec.get("subject") or "unknown", "question": rec.get("problem") or rec.get("question"), "prompt": prompts[j], "answer": gt, "pred_answer": pred, "raw_prediction": raw, "score": 1.0 if pred and gt and pred == gt else 0.0, }) write_json(args.output_dir, f"results_mmvu_shard{args.index}.json", results) write_json(args.output_dir, f"summary_shard{args.index}.json", aggregate_image_mc("mmvu", results)) # --------------------------------------------------------------------------- # MVBench (multi-task short-video multiple-choice) # --------------------------------------------------------------------------- def resolve_relative_video(path_value: Any, data_file: str, video_root: str) -> str: raw = str(path_value or "").strip() if not raw: return "" if os.path.isabs(raw): return raw rel = raw[2:] if raw.startswith("./") else raw candidates = [] if video_root: candidates.append(os.path.join(video_root, rel)) candidates.append(os.path.join(os.path.dirname(data_file), rel)) for cand in candidates: if os.path.isfile(cand): return cand return candidates[0] if candidates else rel def mvbench_prompt(rec: Dict[str, Any], prompt_mode: str = "default") -> Tuple[str, List[str]]: question = str(rec.get("problem") or rec.get("question") or "").strip() options = rec.get("options") or [] labels = CHOICES[: len(options)] if options else list("ABCD") if options and "Options:" not in question: question += "\nOptions:\n" + "\n".join(str(x) for x in options) if prompt_mode == "qwen": tail = ( "Please answer the question based on the video. " "Put only the choice letter inside tags, e.g., C." ) else: tail = VIDEO_QA_MC_TAIL return f"{question}\n{tail}", labels def worker_mvbench(args) -> None: from transformers import AutoProcessor, AutoTokenizer from vllm import LLM, SamplingParams records = read_json_or_jsonl(args.data_file) records.sort(key=lambda r: (str(r.get("path") or ""), int(r.get("problem_id") or r.get("id") or 0))) if args.max_samples > 0: records = records[: args.max_samples] records = shard_records(records, args.chunk, args.index) processor = AutoProcessor.from_pretrained(args.model_path, padding_side="left", trust_remote_code=True) tokenizer = AutoTokenizer.from_pretrained(args.model_path, trust_remote_code=True) tokenizer.padding_side = "left" processor.tokenizer = tokenizer llm = LLM( model=args.model_path, tensor_parallel_size=args.tp_size, gpu_memory_utilization=args.gpu_memory_utilization, max_model_len=args.max_model_len, max_num_batched_tokens=args.max_num_batched_tokens, trust_remote_code=True, dtype="bfloat16", limit_mm_per_prompt={"video": 1, "image": 1}, ) sampling_kwargs: Dict[str, Any] = { "max_tokens": args.max_new_tokens, "temperature": args.temperature, "top_p": args.top_p, "top_k": args.top_k, } if args.presence_penalty: sampling_kwargs["presence_penalty"] = args.presence_penalty if args.min_p: sampling_kwargs["min_p"] = args.min_p sampling = SamplingParams(**sampling_kwargs) print(f"[mvbench] sampling={sampling_kwargs} enable_thinking={args.enable_thinking} prompt_mode={args.prompt_mode}", flush=True) results = [] media_cache: Dict[Tuple[Any, ...], Tuple[Dict[str, Any], Dict[str, Any]]] = {} def prepare_batch(start: int, batch: List[Dict[str, Any]]): inputs, kept, prompts, labels = [], [], {}, {} for j, rec in enumerate(batch): try: video_path = resolve_relative_video(rec.get("path") or rec.get("video"), args.data_file, args.preprocessed_video_dir) if not os.path.isfile(video_path): print(f"[warn] skip {rec.get('problem_id') or rec.get('id')}: video not found: {video_path}", flush=True) continue prompt, labs = mvbench_prompt(rec, prompt_mode=args.prompt_mode) video_item = mmvu_video_item(rec, video_path, args) messages = [{"role": "user", "content": [video_item, {"type": "text", "text": prompt}]}] media_key = (video_path, args.video_min_pixels, args.video_max_pixels, args.video_total_pixels, args.fps, args.max_frames) inputs.append(prepare_video_image(messages, processor, media_key, media_cache, enable_thinking=args.enable_thinking)) kept.append(j) prompts[j] = prompt labels[j] = labs except Exception as e: print(f"[warn] skip {start + j}: {type(e).__name__}: {e}", flush=True) return start, batch, inputs, kept, prompts, labels for start, batch, inputs, kept, prompts, labels in iter_prefetched_batches(records, args.batch_size, prepare_batch, args.prefetch_batches): outs = llm.generate(inputs, sampling_params=sampling) if inputs else [] for j, out in zip(kept, outs): rec = batch[j] raw = out.outputs[0].text pred = extract_mcq_answer(raw, labels[j]) gt = require_mcq_ground_truth(rec, labels[j], task="mvbench") results.append({ "id": str(rec.get("problem_id") or rec.get("id") or start + j), "bench": "mvbench", "group": rec.get("original_question_type") or rec.get("problem_type") or "multiple choice", "question": rec.get("problem") or rec.get("question"), "prompt": prompts[j], "answer": gt, "pred_answer": pred, "raw_prediction": raw, "score": 1.0 if pred and gt and pred == gt else 0.0, }) write_json(args.output_dir, f"results_mvbench_shard{args.index}.json", results) write_json(args.output_dir, f"summary_shard{args.index}.json", aggregate_image_mc("mvbench", results)) # --------------------------------------------------------------------------- # Video-Holmes (complex multi-clue video reasoning, all multiple-choice A-F) # --------------------------------------------------------------------------- # Official Video-Holmes prompt (evaluate.py) is a CoT reasoning prompt. VIDEOHOLMES_TASK_TYPES = ["SR", "IMC", "TCI", "TA", "MHR", "PAR", "CTI"] def videoholmes_labels(rec: Dict[str, Any]) -> List[str]: labs = rec.get("labels") if isinstance(labs, list) and labs: return [str(x) for x in labs] options = rec.get("options") or [] return CHOICES[: len(options)] if options else list("ABCDEF") def videoholmes_prompt(rec: Dict[str, Any], prompt_mode: str = "default") -> Tuple[str, List[str]]: question = str(rec.get("problem") or rec.get("question") or "").strip() options = rec.get("options") or [] labels = videoholmes_labels(rec) opts_inline = ", ".join(str(x).replace(". ", ": ", 1) for x in options) if prompt_mode == "holmes": # Verbatim official CoT prompt (TencentARC/Video-Holmes evaluate.py). prompt = ( "Based on the given video, reason and answer the single-choice question. " "Provide your reasoning between the and tags, and then give " "your final answer between the and tags. " f"The question is: {question}. The options are: {opts_inline}. Your answer:" ) return prompt, labels # Direct-answer (no-think) default, consistent with our other MC evals. if options and "Options:" not in question: question += "\nOptions:\n" + "\n".join(str(x) for x in options) tail = VIDEO_QA_MC_TAIL return f"{question}\n{tail}", labels def worker_videoholmes(args) -> None: from transformers import AutoProcessor, AutoTokenizer from vllm import LLM, SamplingParams records = read_json_or_jsonl(args.data_file) records.sort(key=lambda r: (str(r.get("path") or ""), str(r.get("id") or ""))) if args.max_samples > 0: records = records[: args.max_samples] records = shard_records(records, args.chunk, args.index) processor = AutoProcessor.from_pretrained(args.model_path, padding_side="left", trust_remote_code=True) tokenizer = AutoTokenizer.from_pretrained(args.model_path, trust_remote_code=True) tokenizer.padding_side = "left" processor.tokenizer = tokenizer llm = LLM( model=args.model_path, tensor_parallel_size=args.tp_size, gpu_memory_utilization=args.gpu_memory_utilization, max_model_len=args.max_model_len, max_num_batched_tokens=args.max_num_batched_tokens, trust_remote_code=True, dtype="bfloat16", limit_mm_per_prompt={"video": 1, "image": 1}, ) sampling_kwargs: Dict[str, Any] = { "max_tokens": args.max_new_tokens, "temperature": args.temperature, "top_p": args.top_p, "top_k": args.top_k, } if args.presence_penalty: sampling_kwargs["presence_penalty"] = args.presence_penalty if args.min_p: sampling_kwargs["min_p"] = args.min_p sampling = SamplingParams(**sampling_kwargs) print(f"[videoholmes] sampling={sampling_kwargs} enable_thinking={args.enable_thinking} prompt_mode={args.prompt_mode}", flush=True) results = [] media_cache: Dict[Tuple[Any, ...], Tuple[Dict[str, Any], Dict[str, Any]]] = {} def prepare_batch(start: int, batch: List[Dict[str, Any]]): inputs, kept, prompts, labels = [], [], {}, {} for j, rec in enumerate(batch): try: video_path = str(rec.get("path") or "") if not os.path.isfile(video_path): print(f"[warn] skip {rec.get('id')}: video not found: {video_path}", flush=True) continue prompt, labs = videoholmes_prompt(rec, prompt_mode=args.prompt_mode) video_item = mmvu_video_item(rec, video_path, args) messages = [{"role": "user", "content": [video_item, {"type": "text", "text": prompt}]}] media_key = (video_path, args.video_min_pixels, args.video_max_pixels, args.video_total_pixels, args.fps, args.max_frames) inputs.append(prepare_video_image(messages, processor, media_key, media_cache, enable_thinking=args.enable_thinking)) kept.append(j) prompts[j] = prompt labels[j] = labs except Exception as e: print(f"[warn] skip {start + j}: {type(e).__name__}: {e}", flush=True) return start, batch, inputs, kept, prompts, labels for start, batch, inputs, kept, prompts, labels in iter_prefetched_batches(records, args.batch_size, prepare_batch, args.prefetch_batches): outs = llm.generate(inputs, sampling_params=sampling) if inputs else [] for j, out in zip(kept, outs): rec = batch[j] raw = out.outputs[0].text pred = extract_mcq_answer(raw, labels[j]) gt = require_mcq_ground_truth(rec, labels[j], task="videoholmes") results.append({ "id": str(rec.get("id") or start + j), "bench": "videoholmes", "group": rec.get("q_type") or "unknown", "question": rec.get("problem") or rec.get("question"), "prompt": prompts[j], "answer": gt, "pred_answer": pred, "raw_prediction": raw, "score": 1.0 if pred and gt and pred == gt else 0.0, }) write_json(args.output_dir, f"results_videoholmes_shard{args.index}.json", results) write_json(args.output_dir, f"summary_shard{args.index}.json", aggregate_image_mc("videoholmes", results)) # --------------------------------------------------------------------------- # LongVideoBench (long-form video MC, optional subtitles) # --------------------------------------------------------------------------- def longvideobench_prompt( rec: Dict[str, Any], prompt_mode: str = "default", use_subtitles: bool = True, ) -> Tuple[str, List[str]]: question = str(rec.get("problem") or rec.get("question") or "").strip() options = rec.get("options") or [] labels = rec.get("labels") if isinstance(rec.get("labels"), list) else CHOICES[: len(options)] subtitle = str(rec.get("subtitle_text") or "").strip() if use_subtitles else "" if use_subtitles and not subtitle: subtitle_path = str(rec.get("subtitle_path") or "").strip() if subtitle_path and os.path.isfile(subtitle_path): subtitle = Path(subtitle_path).read_text( encoding="utf-8", errors="replace", ).strip() if subtitle: question = f"Subtitles/transcript snippets:\n{subtitle}\n\nQuestion: {question}" if options and "Options:" not in question: question += "\nOptions:\n" + "\n".join(str(x) for x in options) if prompt_mode == "qwen": tail = ( "Please answer the question based on the video and any provided subtitles. " "Put only the choice letter inside tags, e.g., C." ) else: tail = VIDEO_QA_MC_TAIL return f"{question}\n{tail}", [str(x) for x in labels] if labels else list("ABCDE") def worker_longvideobench(args) -> None: from transformers import AutoProcessor, AutoTokenizer from vllm import LLM, SamplingParams records = read_json_or_jsonl(args.data_file) records.sort(key=lambda r: (str(r.get("path") or ""), str(r.get("id") or ""))) if args.max_samples > 0: records = records[: args.max_samples] records = shard_records(records, args.chunk, args.index) processor = AutoProcessor.from_pretrained(args.model_path, padding_side="left", trust_remote_code=True) tokenizer = AutoTokenizer.from_pretrained(args.model_path, trust_remote_code=True) tokenizer.padding_side = "left" processor.tokenizer = tokenizer llm = LLM( model=args.model_path, tensor_parallel_size=args.tp_size, gpu_memory_utilization=args.gpu_memory_utilization, max_model_len=args.max_model_len, max_num_batched_tokens=args.max_num_batched_tokens, trust_remote_code=True, dtype="bfloat16", limit_mm_per_prompt={"video": 1, "image": 1}, ) sampling_kwargs: Dict[str, Any] = { "max_tokens": args.max_new_tokens, "temperature": args.temperature, "top_p": args.top_p, "top_k": args.top_k, } if args.presence_penalty: sampling_kwargs["presence_penalty"] = args.presence_penalty if args.min_p: sampling_kwargs["min_p"] = args.min_p sampling = SamplingParams(**sampling_kwargs) print(f"[longvideobench] sampling={sampling_kwargs} enable_thinking={args.enable_thinking} prompt_mode={args.prompt_mode}", flush=True) results = [] media_cache: Dict[Tuple[Any, ...], Tuple[Dict[str, Any], Dict[str, Any]]] = {} def prepare_batch(start: int, batch: List[Dict[str, Any]]): inputs, kept, prompts, labels = [], [], {}, {} for j, rec in enumerate(batch): try: video_path = str(rec.get("path") or "") if not os.path.isfile(video_path): print(f"[warn] skip {rec.get('id')}: video not found: {video_path}", flush=True) continue prompt, labs = longvideobench_prompt( rec, prompt_mode=args.prompt_mode, use_subtitles=args.use_subtitles, ) video_item = mmvu_video_item(rec, video_path, args) messages = [{"role": "user", "content": [video_item, {"type": "text", "text": prompt}]}] media_key = (video_path, args.video_min_pixels, args.video_max_pixels, args.video_total_pixels, args.fps, args.max_frames) inputs.append(prepare_video_image(messages, processor, media_key, media_cache, enable_thinking=args.enable_thinking)) kept.append(j) prompts[j] = prompt labels[j] = labs except Exception as e: print(f"[warn] skip {start + j}: {type(e).__name__}: {e}", flush=True) return start, batch, inputs, kept, prompts, labels for start, batch, inputs, kept, prompts, labels in iter_prefetched_batches(records, args.batch_size, prepare_batch, args.prefetch_batches): outs = llm.generate(inputs, sampling_params=sampling) if inputs else [] for j, out in zip(kept, outs): rec = batch[j] raw = out.outputs[0].text pred = extract_mcq_answer(raw, labels[j]) gt = require_mcq_ground_truth(rec, labels[j], task="longvideobench") results.append({ "id": str(rec.get("id") or start + j), "bench": "longvideobench", "group": rec.get("question_category") or "unknown", "topic_category": rec.get("topic_category") or "unknown", "duration_group": rec.get("duration_group"), "question": rec.get("problem") or rec.get("question"), "prompt": prompts[j], "answer": gt, "pred_answer": pred, "raw_prediction": raw, "score": 1.0 if pred and gt and pred == gt else 0.0, }) write_json(args.output_dir, f"results_longvideobench_shard{args.index}.json", results) write_json(args.output_dir, f"summary_shard{args.index}.json", aggregate_image_mc("longvideobench", results)) # --------------------------------------------------------------------------- # LVBench (long-video multiple-choice) # --------------------------------------------------------------------------- def lvbench_prompt(rec: Dict[str, Any], prompt_mode: str = "default") -> Tuple[str, List[str]]: question = str(rec.get("problem") or rec.get("question") or "").strip() options = rec.get("options") or [] labels = CHOICES[: len(options)] if options else list("ABCD") if options and "Options:" not in question: question += "\nOptions:\n" + "\n".join(str(x) for x in options) if prompt_mode == "qwen": tail = ( "Please answer the question based on the video. " "Put only the choice letter inside tags, e.g., C." ) else: tail = VIDEO_QA_MC_TAIL return f"{question}\n{tail}", labels def resolve_lvbench_video(path_value: Any, data_file: str, video_root: str) -> str: raw = str(path_value or "").strip() if not raw: return "" if os.path.isabs(raw): return raw rel = raw[2:] if raw.startswith("./") else raw candidates = [] if video_root: candidates.append(os.path.join(video_root, rel)) candidates.append(os.path.join(os.path.dirname(data_file), rel)) for cand in candidates: if os.path.isfile(cand): return cand return candidates[0] if candidates else rel def worker_lvbench(args) -> None: from transformers import AutoProcessor, AutoTokenizer from vllm import LLM, SamplingParams records = read_json_or_jsonl(args.data_file) records.sort(key=lambda r: (str(r.get("path") or ""), int(r.get("problem_id") or 0))) records = shard_records(records, args.chunk, args.index) processor = AutoProcessor.from_pretrained(args.model_path, padding_side="left", trust_remote_code=True) tokenizer = AutoTokenizer.from_pretrained(args.model_path, trust_remote_code=True) tokenizer.padding_side = "left" processor.tokenizer = tokenizer llm = LLM( model=args.model_path, tensor_parallel_size=args.tp_size, gpu_memory_utilization=args.gpu_memory_utilization, max_model_len=args.max_model_len, max_num_batched_tokens=args.max_num_batched_tokens, trust_remote_code=True, dtype="bfloat16", limit_mm_per_prompt={"video": 1, "image": 1}, ) sampling_kwargs: Dict[str, Any] = { "max_tokens": args.max_new_tokens, "temperature": args.temperature, "top_p": args.top_p, "top_k": args.top_k, } if args.presence_penalty: sampling_kwargs["presence_penalty"] = args.presence_penalty if args.min_p: sampling_kwargs["min_p"] = args.min_p sampling = SamplingParams(**sampling_kwargs) print(f"[lvbench] sampling={sampling_kwargs} enable_thinking={args.enable_thinking} prompt_mode={args.prompt_mode}", flush=True) results = [] media_cache: Dict[Tuple[Any, ...], Tuple[Dict[str, Any], Dict[str, Any]]] = {} def prepare_batch(start: int, batch: List[Dict[str, Any]]): inputs, kept, prompts, labels = [], [], {}, {} for j, rec in enumerate(batch): try: video_path = resolve_lvbench_video(rec.get("path"), args.data_file, args.preprocessed_video_dir) if not os.path.isfile(video_path): print(f"[warn] skip {rec.get('problem_id')}: video not found: {video_path}", flush=True) continue prompt, labs = lvbench_prompt(rec, prompt_mode=args.prompt_mode) video_item = mmvu_video_item(rec, video_path, args) messages = [{"role": "user", "content": [video_item, {"type": "text", "text": prompt}]}] media_key = (video_path, args.video_min_pixels, args.video_max_pixels, args.video_total_pixels, args.fps, args.max_frames) inputs.append(prepare_video_image(messages, processor, media_key, media_cache, enable_thinking=args.enable_thinking)) kept.append(j) prompts[j] = prompt labels[j] = labs except Exception as e: print(f"[warn] skip {start + j}: {type(e).__name__}: {e}", flush=True) return start, batch, inputs, kept, prompts, labels for start, batch, inputs, kept, prompts, labels in iter_prefetched_batches(records, args.batch_size, prepare_batch, args.prefetch_batches): outs = llm.generate(inputs, sampling_params=sampling) if inputs else [] for j, out in zip(kept, outs): rec = batch[j] raw = out.outputs[0].text pred = extract_mcq_answer(raw, labels[j]) gt = require_mcq_ground_truth(rec, labels[j], task="lvbench") results.append({ "id": str(rec.get("problem_id") or rec.get("id") or start + j), "bench": "lvbench", "group": rec.get("problem_type") or "multiple choice", "data_source": rec.get("data_source") or "LVBench", "question": rec.get("problem") or rec.get("question"), "prompt": prompts[j], "answer": gt, "pred_answer": pred, "raw_prediction": raw, "score": 1.0 if pred and gt and pred == gt else 0.0, }) write_json(args.output_dir, f"results_lvbench_shard{args.index}.json", results) write_json(args.output_dir, f"summary_shard{args.index}.json", aggregate_image_mc("lvbench", results)) # --------------------------------------------------------------------------- # MLVU (dev, multi-task long video understanding, multiple-choice M-Avg) # --------------------------------------------------------------------------- def mlvu_prompt(rec: Dict[str, Any], prompt_mode: str = "default") -> Tuple[str, List[str]]: question = str(rec.get("problem") or rec.get("question") or "").strip() options = rec.get("options") or [] labels = rec.get("labels") if isinstance(rec.get("labels"), list) else CHOICES[: len(options)] if options and "Options:" not in question: question += "\nOptions:\n" + "\n".join(str(x) for x in options) if prompt_mode == "qwen": tail = ( "Please answer the question based on the visual content. " "Put only the choice letter inside tags, e.g., C." ) else: tail = VIDEO_QA_MC_TAIL return f"{question}\n{tail}", [str(x) for x in labels] if labels else list("ABCD") def worker_mlvu(args) -> None: from transformers import AutoProcessor, AutoTokenizer from vllm import LLM, SamplingParams records = read_json_or_jsonl(args.data_file) records.sort(key=lambda r: (str(r.get("path") or ""), str(r.get("id") or ""))) if args.max_samples > 0: records = records[: args.max_samples] records = shard_records(records, args.chunk, args.index) processor = AutoProcessor.from_pretrained(args.model_path, padding_side="left", trust_remote_code=True) tokenizer = AutoTokenizer.from_pretrained(args.model_path, trust_remote_code=True) tokenizer.padding_side = "left" processor.tokenizer = tokenizer llm = LLM( model=args.model_path, tensor_parallel_size=args.tp_size, gpu_memory_utilization=args.gpu_memory_utilization, max_model_len=args.max_model_len, max_num_batched_tokens=args.max_num_batched_tokens, trust_remote_code=True, dtype="bfloat16", limit_mm_per_prompt={"video": 1, "image": 1}, ) sampling_kwargs: Dict[str, Any] = { "max_tokens": args.max_new_tokens, "temperature": args.temperature, "top_p": args.top_p, "top_k": args.top_k, } if args.presence_penalty: sampling_kwargs["presence_penalty"] = args.presence_penalty if args.min_p: sampling_kwargs["min_p"] = args.min_p sampling = SamplingParams(**sampling_kwargs) print(f"[mlvu] sampling={sampling_kwargs} enable_thinking={args.enable_thinking} prompt_mode={args.prompt_mode}", flush=True) results = [] media_cache: Dict[Tuple[Any, ...], Tuple[Dict[str, Any], Dict[str, Any]]] = {} def prepare_batch(start: int, batch: List[Dict[str, Any]]): inputs, kept, prompts, labels = [], [], {}, {} for j, rec in enumerate(batch): try: video_path = str(rec.get("path") or "") if not os.path.isfile(video_path): print(f"[warn] skip {rec.get('id')}: video not found: {video_path}", flush=True) continue prompt, labs = mlvu_prompt(rec, prompt_mode=args.prompt_mode) video_item = mmvu_video_item(rec, video_path, args) messages = [{"role": "user", "content": [video_item, {"type": "text", "text": prompt}]}] media_key = (video_path, args.video_min_pixels, args.video_max_pixels, args.video_total_pixels, args.fps, args.max_frames) inputs.append(prepare_video_image(messages, processor, media_key, media_cache, enable_thinking=args.enable_thinking)) kept.append(j) prompts[j] = prompt labels[j] = labs except Exception as e: print(f"[warn] skip {start + j}: {type(e).__name__}: {e}", flush=True) return start, batch, inputs, kept, prompts, labels for start, batch, inputs, kept, prompts, labels in iter_prefetched_batches(records, args.batch_size, prepare_batch, args.prefetch_batches): outs = llm.generate(inputs, sampling_params=sampling) if inputs else [] for j, out in zip(kept, outs): rec = batch[j] raw = out.outputs[0].text pred = extract_mcq_answer(raw, labels[j]) gt = require_mcq_ground_truth(rec, labels[j], task="mlvu") results.append({ "id": str(rec.get("id") or start + j), "bench": "mlvu", "group": rec.get("task_type") or "unknown", "question": rec.get("problem") or rec.get("question"), "prompt": prompts[j], "answer": gt, "pred_answer": pred, "raw_prediction": raw, "score": 1.0 if pred and gt and pred == gt else 0.0, }) write_json(args.output_dir, f"results_mlvu_shard{args.index}.json", results) write_json(args.output_dir, f"summary_shard{args.index}.json", aggregate_image_mc("mlvu", results)) # --------------------------------------------------------------------------- # VSI # --------------------------------------------------------------------------- NUMERICAL_TASKS = {"object_abs_distance", "object_counting", "object_size_estimation", "room_size_estimation"} VSI_REPORT = [ ("Obj. Count", "object_counting", "MRA"), ("Abs. Dist", "object_abs_distance", "MRA"), ("Obj. Size", "object_size_estimation", "MRA"), ("Room Size", "room_size_estimation", "MRA"), ("Rel. Dis", "object_rel_distance", "ACC"), ("Rel. Dir", "object_rel_direction", "ACC"), ("Route Plan", "route_planning", "ACC"), ("Appr. Order", "obj_appearance_order", "ACC"), ] def vsi_qtype(rec: Dict[str, Any]) -> str: return str( rec.get("original_question_type") or rec.get("question_type") or rec.get("problem_type") or "unknown" ) def vsi_options(options: Any) -> str: if not options: return "" if isinstance(options, dict): values = [str(options[k]) for k in sorted(options)] elif isinstance(options, (list, tuple)): values = [str(x) for x in options] else: values = [str(options)] return "\n".join(v.strip() for v in values if v.strip()) def vsi_prompt(rec: Dict[str, Any]) -> str: qtype = vsi_qtype(rec) prompt = str(rec.get("prompt") or rec.get("question") or rec.get("problem") or "").strip() prompt = re.sub(r"^(?:\s*