#!/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"?think>", 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*