Official codebase for VSeek-R1 — a tool-using video agent that learns to seek answer-critical evidence in long videos via reinforcement learning (GRPO), rather than ingesting dense frame stacks end-to-end.
Slurm / HPC (TACC Vista): see
README_SLURM.md.
Path note (local vs Slurm): defaults on this branch (
v0.6.1) point at the local lab layout under/nas/mars/...and/home/.../vseek/dataset. Theharsh_tacc/ Slurm configs use TACC$SCRATCH/$WORKpaths instead. Always rewrite dataset, index, ViCLIP, and parquet paths for your machine before launching the retriever or training — see Path conventions.
- Overview
- Path conventions (local vs TACC)
- Requirements
- Installation
- Download Datasets
- Process Datasets
- Run the Retriever Server
- Inference
- Training (GRPO)
- Evaluation
- Repository Layout
- Citation
Configs and launch scripts hard-code filesystem roots. v0.6.1 assumes a local shared NAS; harsh_tacc retargets only some of those files to Vista. Mixing branches or launching training scripts without checking paths will break the retrieval server and tool calls.
Audited with git grep over both trees (excluding vendor/). These are the files whose dataset / index / weight roots change nas/home ↔ scratch/work:
| File | Local (v0.6.1) |
TACC (harsh_tacc) |
|---|---|---|
src/vseek/config/retriever/config.yaml |
/nas/mars/dataset/..., /home/.../vseek/dataset, ViCLIP under /nas/mars/model_weights/, port 9005 |
$SCRATCH/.../datasets/..., $SCRATCH/.../vseek, ViCLIP under $SCRATCH/.../model_weights/, port 9000 |
src/vseek/config/retriever/toolconfig.yaml |
same local roots (note: MLVU is /nas/mars/dataset/MLVU here vs .../MLVU/MLVU in config.yaml) |
$SCRATCH roots; MLVU uses .../MLVU/MLVU |
scripts/tacc/* (harsh only) |
n/a | parquet / checkpoints under $WORK/.../vista/...; PROJECT_ROOT fallback /home1/.../VSeek-R1; HF_HOME → $WORK or $SCRATCH |
| Resource | Local (v0.6.1) |
TACC (harsh_tacc / Slurm) |
|---|---|---|
| Raw / burned videos | /nas/mars/dataset/... |
$SCRATCH/.../datasets/... |
| FAISS / frame indexes | /home/<user>/vseek/dataset |
$SCRATCH/.../vseek |
| ViCLIP weights | /nas/mars/model_weights/viclip/ |
$SCRATCH/.../model_weights/viclip/ |
| RL parquet (Slurm jobs) | /nas/mars/vseek/... or per-dataset window_8/ |
$WORK/.../vista/vseek/tagsummary/{train,test}.parquet |
| Checkpoints (Slurm jobs) | local checkpoints/ |
$WORK/.../vista/checkpoints or $SCRATCH/checkpoints |
| Retriever port | 9005 |
9000 |
These were not rewritten on harsh_tacc. On slurm you must override them (CLI / env / edit) or avoid them in favor of scripts/tacc/:
scripts/train/*.sh—data.train_files/data.val_filesstill under/nas/mars/...scripts/data_ops/preprocess_*.sh,scripts/utils/unzip_videos.sh— defaultDATASET_PATH/INDEX_PATH/CHUNKS_DIRunder/nas/mars(and CGBench preprocess defaultsINDEX_PATHto/home/hg22723/vseek/dataset)scripts/evals/vllm_vseek_tests_ckpt.sh— per-dataset parquet roots under/nas/mars/dataset/.../window_8/- Scratch / demo scripts with hardcoded
PROJECT_ROOT=/home/hg22723/projects/VSeek-R1(e.g.scripts/run_agent_data_vllmrollout.py,scripts/run_api_eval.py,scripts/run_video_tree.py,scripts/sftdata/run_gpt_agent_data.py) scripts/puls/*.sh, VideoTree baseline wrappers, and some notebooks — local-only paths (many exist only onv0.6.1)
additional_readme.mdmentions indexes/data under$WORK/.../vista/{vseek,datasets}, whileconfig.yaml/toolconfig.yamlon that branch point at$SCRATCH/.../{vseek,datasets}. Confirm which tree you actually populated.run_vseek_job*.slurmhard-codesCHECKPOINT_DIR/ parquet under/work/11123/harshgoel99/...;run_vseek_all_jobs*.slurmoften uses${SCRATCH}/checkpointsand env-providedTRAIN_DATA_PATH.HF_HOMEis$WORK/huggingfacein some jobs and$SCRATCH/huggingfacein others /ray_wrapper.sh.- Slurm
PROJECT_ROOTfallback is/home1/11123/harshgoel99/VSeek-R1(TACC home), not the lab/home/hg22723/projects/....
Files to touch (or override via Hydra) when moving between local and slurm:
src/vseek/config/retriever/config.yamlsrc/vseek/config/retriever/toolconfig.yamlscripts/tacc/run_vseek_job.slurm/vseek_grpo_tag_qvl34Bt_vllm_tagsummary.sh- Any
scripts/train/*.shor preprocess/eval script you still invoke on that machine - Optionally
src/vseek/setting.py
On TACC, prefer Hydra / CLI overrides over committing personal $SCRATCH / $WORK paths. Full Slurm examples: README_SLURM.md.
VSeek-R1 couples a vision–language model with a video retrieval toolkit (ViCLIP + FAISS indexes over temporal windows). The agent interacts in multi-turn think → tool call → observe loops, and is trained with Group Relative Policy Optimization (GRPO) on top of verl.
Supported benchmarks (download + preprocess scripts included):
| Dataset | Hugging Face repo | Config name |
|---|---|---|
| LongVideoBench | lmms-lab/LongVideoBench |
lvb |
| LVBench | lmms-lab/LVBench |
lvbench |
| Video-MME | lmms-lab/Video-MME |
videomme |
| MLVU | MLVU/MVLU |
mlvu |
| CGBench | (local / custom) | cgbench |
- Linux + NVIDIA GPU (CUDA 12.4+ recommended)
- Python ≥ 3.11
- Conda or uv
- Sufficient disk for video datasets (hundreds of GB)
git clone https://github.com/UTAustin-SwarmLab/VSeek-R1.git
cd VSeek-R1
git submodule update --init --recursive # pulls vendor/verlOption A — uv (recommended on a workstation):
pip install uv
uv venv .venv
source .venv/bin/activate
uv syncOption B — conda:
conda create -n vseek python=3.11 -y
conda activate vseek
conda install pytorch torchvision torchaudio pytorch-cuda=12.4 -c pytorch -c nvidiaexport CUDA_HOME="${CUDA_HOME:-$CONDA_PREFIX}"
# Choose one inference stack
pip install -r requirements_vllm.txt # vLLM (default for training / eval)
# or
pip install -r requirements_sglang.txt # SGLang
pip install flash-attn==2.8.3 --no-build-isolationpip install -e .
pip install -e vendor/verlpython -c "import torch; import vllm; print(torch.__version__, torch.cuda.is_available())"HPC / Slurm users: do not follow this path blindly on TACC Vista (Grace Hopper / ARM). Use
README_SLURM.mdinstead — vLLM and FlashAttention must be built forsm_90.
Set a root directory and download with huggingface-cli (faster than the Python helper):
export DATASET_ROOT=/path/to/datasets # e.g. /nas/mars/dataset
mkdir -p "$DATASET_ROOT"
huggingface-cli download MLVU/MVLU --repo-type dataset \
--local-dir "$DATASET_ROOT/MLVU"
huggingface-cli download lmms-lab/Video-MME --repo-type dataset \
--local-dir "$DATASET_ROOT/Video-MME"
huggingface-cli download lmms-lab/LVBench --repo-type dataset \
--local-dir "$DATASET_ROOT/LVBench"
huggingface-cli download lmms-lab/LongVideoBench --repo-type dataset \
--local-dir "$DATASET_ROOT/longvideobench"Or download everything in one loop:
for dataset in \
"MLVU/MVLU:MLVU" \
"lmms-lab/Video-MME:Video-MME" \
"lmms-lab/LVBench:LVBench" \
"lmms-lab/LongVideoBench:longvideobench"
do
IFS=':' read -r repo_id local_dir <<< "$dataset"
echo "→ $repo_id → $DATASET_ROOT/$local_dir"
huggingface-cli download "$repo_id" --repo-type dataset \
--local-dir "$DATASET_ROOT/$local_dir"
doneAlso place ViCLIP weights somewhere accessible and point configs at them (defaults live under src/vseek/setting.py):
ViClip-InternVid-10M-FLT.pthbpe_simple_vocab_16e6.txt.gz
Each dataset follows the same pattern:
- Unzip videos / burn subtitles (when needed)
- Convert parquet → JSON (Video-MME, LVBench)
- Build a retrieval index (
run_data_pipeline.py) - Optionally extract PULS primitives
- Create train/test parquet for RL (
preprocess_*.sh)
Set shared paths once:
export DATASET_ROOT=/path/to/datasets
export INDEX_PATH=/path/to/vseek/indexes # FAISS / frame indexes
export WINDOW_SIZE=8
export PROMPT_TYPE=tagsummary # or: tag# Burn subtitles into frames
python3 scripts/utils/burn_subtitles.py \
--json-file lvb_val.json \
--output-dir "$DATASET_ROOT/longvideobench/burn-subtitles" \
--data-folder "$DATASET_ROOT/longvideobench/LongVideoBench/"
# Index windows
python3 scripts/data_ops/run_data_pipeline.py \
retriever.window_size=$WINDOW_SIZE \
dataset.name=lvb \
dataset.lvb.dataset_path="$DATASET_ROOT/longvideobench/LongVideoBench/" \
dataset.lvb.burned_path="$DATASET_ROOT/longvideobench/" \
retriever.index_path="$INDEX_PATH"
# Optional PULS extraction
python3 src/vseek/puls/primitives.py dataset.name=lvb
# RL parquet splits
bash scripts/data_ops/preprocess_lvb.sh \
--local_dataset_path "$DATASET_ROOT/longvideobench/LongVideoBench" \
--burned_path "$DATASET_ROOT/longvideobench/" \
--train_ratio 0.8 \
--local_save_dir "$DATASET_ROOT/longvideobench" \
--index_path "$INDEX_PATH" \
--window_size $WINDOW_SIZE \
--prompt_type $PROMPT_TYPECHUNKS_DIR="$DATASET_ROOT/LVBench/video_chunks" \
OUTPUT_DIR="$DATASET_ROOT/LVBench/videos" \
bash scripts/utils/unzip_videos.sh
python3 scripts/utils/convert_parquet.py \
--parquet_path "$DATASET_ROOT/LVBench/data/train-00000-of-00001.parquet" \
--json_path "$DATASET_ROOT/LVBench/data/lvbench_val.json"
python3 scripts/data_ops/run_data_pipeline.py \
retriever.window_size=$WINDOW_SIZE \
dataset.name=lvbench \
dataset.lvbench.dataset_path="$DATASET_ROOT/LVBench" \
dataset.lvbench.burned_path="$DATASET_ROOT/LVBench/videos" \
retriever.index_path="$INDEX_PATH"
python3 src/vseek/puls/primitives.py dataset.name=lvbench
bash scripts/data_ops/preprocess_lvbench.sh \
--local_dataset_path "$DATASET_ROOT/LVBench" \
--burned_path "$DATASET_ROOT/LVBench/videos" \
--train_ratio 0.8 \
--local_save_dir "$DATASET_ROOT/LVBench" \
--index_path "$INDEX_PATH" \
--window_size $WINDOW_SIZE \
--prompt_type $PROMPT_TYPESUBTITLE_DIR="$DATASET_ROOT/Video-MME" \
CHUNKS_DIR="$DATASET_ROOT/Video-MME" \
OUTPUT_DIR="$DATASET_ROOT/Video-MME/videos" \
bash scripts/utils/unzip_videos.sh
python3 scripts/utils/convert_parquet.py \
--parquet_path "$DATASET_ROOT/Video-MME/videomme/test-00000-of-00001.parquet" \
--json_path "$DATASET_ROOT/Video-MME/videomme/videomme_val.json"
python3 scripts/utils/burn_subtitles.py \
--json-file videomme/videomme_val.json \
--output-dir "$DATASET_ROOT/Video-MME/burn-subtitles" \
--data-folder "$DATASET_ROOT/Video-MME"
python3 scripts/data_ops/run_data_pipeline.py \
dataset.name=videomme \
retriever.window_size=$WINDOW_SIZE \
dataset.videomme.dataset_path="$DATASET_ROOT/Video-MME/" \
dataset.videomme.burned_path="$DATASET_ROOT/Video-MME/burn-subtitles/" \
retriever.index_path="$INDEX_PATH" \
retriever.gpu_number=0
python3 src/vseek/puls/primitives.py dataset.name=videomme
bash scripts/data_ops/preprocess_videomme.sh \
--local_dataset_path "$DATASET_ROOT/Video-MME" \
--burned_path "$DATASET_ROOT/Video-MME/burn-subtitles" \
--train_ratio 0.8 \
--local_save_dir "$DATASET_ROOT/Video-MME" \
--index_path "$INDEX_PATH" \
--window_size $WINDOW_SIZE \
--prompt_type $PROMPT_TYPETasks: plotQA, needle, ego, count, order, anomaly_reco, topic_reasoning.
python3 scripts/data_ops/run_data_pipeline.py \
retriever.window_size=$WINDOW_SIZE \
dataset.name=mlvu \
dataset.mlvu.dataset_path="$DATASET_ROOT/MLVU/MLVU" \
dataset.mlvu.burned_path="$DATASET_ROOT/MLVU/MLVU" \
retriever.index_path="$INDEX_PATH"
python3 src/vseek/puls/primitives.py dataset.name=mlvu
bash scripts/data_ops/preprocess_mlvu.sh \
--local_dataset_path "$DATASET_ROOT/MLVU/MLVU" \
--burned_path "$DATASET_ROOT/MLVU/MLVU" \
--train_ratio 0.8 \
--local_save_dir "$DATASET_ROOT/MLVU" \
--index_path "$INDEX_PATH" \
--window_size $WINDOW_SIZE \
--prompt_type $PROMPT_TYPESUBTITLE_DIR="$DATASET_ROOT/CGBench" \
CHUNKS_DIR="$DATASET_ROOT/CGBench" \
OUTPUT_DIR="$DATASET_ROOT/CGBench/videos" \
bash scripts/utils/unzip_videos.sh
python3 scripts/utils/burn_subtitles.py \
--json-file cgbench_mini.json \
--output-dir "$DATASET_ROOT/CGBench/burn-subtitles" \
--data-folder "$DATASET_ROOT/CGBench"
python3 scripts/data_ops/run_data_pipeline.py \
dataset.name=cgbench \
retriever.window_size=$WINDOW_SIZE \
dataset.cgbench.dataset_path="$DATASET_ROOT/CGBench/" \
dataset.cgbench.burned_path="$DATASET_ROOT/CGBench/burn-subtitles/" \
retriever.index_path="$INDEX_PATH"
bash scripts/data_ops/preprocess_cgbench.sh \
--local_dataset_path "$DATASET_ROOT/CGBench" \
--burned_path "$DATASET_ROOT/CGBench/burn-subtitles" \
--train_ratio 0.8 \
--local_save_dir "$DATASET_ROOT/CGBench" \
--index_path "$INDEX_PATH" \
--window_size $WINDOW_SIZE \
--prompt_type $PROMPT_TYPEThe agent calls a Flask retrieval server during training and inference.
On local setups (v0.6.1), defaults in config.yaml / toolconfig.yaml use /nas/mars/... datasets and /home/.../vseek/dataset indexes (port 9005). On TACC, those must be rewritten to $SCRATCH / $WORK paths (port 9000) — see Path conventions and README_SLURM.md.
CUDA_VISIBLE_DEVICES=0 python3 src/vseek/tools/server.py \
retriever.window_size=$WINDOW_SIZE \
dataset.name=['lvb','lvbench','videomme','mlvu'] \
retriever.index_path="$INDEX_PATH" \
retriever.retrieval_model_path=/path/to/ViClip-InternVid-10M-FLT.pth \
retriever.text_encoder_model_path=/path/to/bpe_simple_vocab_16e6.txt.gzUpdate src/vseek/config/retriever/toolconfig.yaml so the agent points at this server URL and the same dataset/index roots the server was started with.
CUDA_VISIBLE_DEVICES=0 python3 scripts/run_uniform_agent_data.py \
inference.max_images_per_turn=64 \
+inference.agent_prompt_type=cot \
llm.model=Qwen/Qwen3-VL-4B-Thinking \
inference.output_dir=results/uniform_q3_4bt_cot \
inference.max_output_tokens=2048 \
inference.gpu_number=0bash scripts/evals/run_vseek.sh
# or see scripts/run_agent_data_vllm.py / scripts/evals/*python3 -m verl.model_merger merge \
--backend fsdp \
--local_dir checkpoints/vseek/<exp>/global_step_XXX/actor \
--target_dir checkpoints/vseek/<exp>/global_step_XXX/actor/huggingfaceExample single-node GRPO run (edit paths / GPU count to match your machine):
bash scripts/train/vseek_grpo_tag_qvl34Bt_vllm_tagsummary.shKey knobs live in:
src/vseek/config/lvb_grpo.yaml— trainer / rollout configsrc/vseek/config/retriever/toolconfig.yaml— tool serversrc/vseek/config/retriever/vllm_agent.yaml— agent loop
Multi-node Slurm jobs: README_SLURM.md.
Convenience wrappers under scripts/evals/:
bash scripts/evals/run_vseek.sh
bash scripts/evals/run_uniform_vllm.sh
bash scripts/evals/run_gpt_eval.sh
bash scripts/evals/vllm_vseek_tests_ckpt.sh \
--devices 0 \
--batch-size 16 \
--output-base results/vseek/my-run \
--model-path /path/to/hf/checkpoint \
--datasets lvb,lvbench,videomme,mlvuVSeek-R1/
├── src/vseek/ # Package: agent, tools, trainer, data, PULS
├── scripts/
│ ├── data_ops/ # Indexing + parquet preprocessing
│ ├── train/ # GRPO launch scripts
│ ├── evals/ # Evaluation wrappers
│ ├── utils/ # Download, unzip, burn subtitles, …
│ └── tacc/ # Slurm / Ray helpers (see README_SLURM.md)
├── vendor/verl/ # Vendored verl (RL training)
├── requirements_vllm.txt
├── requirements_sglang.txt
├── requirements_vllm_slurm2.txt
├── README.md # This file (workstation setup)
└── README_SLURM.md # TACC / Slurm installation
If you use this code, please cite:
@inproceedings{hg_2026_ECCV,
author={Goel, Harsh and Sharan, SP and Shah, Sahil and Choi, Minkyu and An, Joungbin, and Grauman, Kristen, and Chinchali, Sandeep},
title={Incentivizing Vision Language Models to Search for Long Video Question Answering},
booktitle={Proceedings of the European Conference on Computer Vision (ECCV)},
year = {2026},
address = {Malmo, Sweden},
month = {9},
publisher = {Springer},
keywords={2026},
}Built on verl, vLLM, and the public video understanding benchmarks listed above.