Image-Text-to-Text
Transformers
Safetensors
qwen3_5
vllm
video
multimodal
reinforcement-learning
temporal-grounding
object-tracking
video-segmentation
visual-question-answering
spatial-reasoning
qwen3.5
conversational
Instructions to use OraRL/Video-ORA-9B with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use OraRL/Video-ORA-9B with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-text-to-text", model="OraRL/Video-ORA-9B") messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] pipe(text=messages)# pip install -U transformers accelerate # Load model directly from transformers import AutoProcessor, AutoModelForMultimodalLM processor = AutoProcessor.from_pretrained("OraRL/Video-ORA-9B") model = AutoModelForMultimodalLM.from_pretrained("OraRL/Video-ORA-9B", device_map="auto") messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] inputs = processor.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=256) print(processor.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use OraRL/Video-ORA-9B with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "OraRL/Video-ORA-9B" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "OraRL/Video-ORA-9B", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker
docker model run hf.co/OraRL/Video-ORA-9B
- SGLang
How to use OraRL/Video-ORA-9B with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "OraRL/Video-ORA-9B" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "OraRL/Video-ORA-9B", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "OraRL/Video-ORA-9B" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "OraRL/Video-ORA-9B", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }' - Docker Model Runner
How to use OraRL/Video-ORA-9B with Docker Model Runner:
docker model run hf.co/OraRL/Video-ORA-9B
Add OraRL training and evaluation code
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +41 -0
- code/.gitignore +80 -0
- code/LICENSE +201 -0
- code/MANIFEST.in +20 -0
- code/NOTICE +7 -0
- code/README.md +201 -0
- code/README_zh.md +193 -0
- code/assets/orarl-data-scaling.gif +3 -0
- code/assets/orarl-hero.gif +3 -0
- code/assets/orarl-method.gif +3 -0
- code/assets/orarl-model-scaling.gif +3 -0
- code/assets/orarl-teaser.mp4 +3 -0
- code/assets/paper-data-scaling.png +3 -0
- code/assets/paper-framework.png +3 -0
- code/assets/paper-model-scaling.png +3 -0
- code/assets/paper-results.png +3 -0
- code/assets/video_ora_benchmark_matrix_dark.svg +362 -0
- code/assets/video_ora_benchmark_matrix_light.svg +362 -0
- code/configs/data_sources.example.yaml +90 -0
- code/configs/grpo_4b.yaml +139 -0
- code/configs/grpo_9b.yaml +139 -0
- code/configs/orarl_4b.yaml +162 -0
- code/configs/orarl_9b.yaml +162 -0
- code/data/eval/README.md +25 -0
- code/data/eval/datasets.jsonl +29 -0
- code/docs/environment.md +60 -0
- code/docs/environment_zh.md +57 -0
- code/docs/evaluation.md +121 -0
- code/docs/evaluation_zh.md +116 -0
- code/docs/training.md +162 -0
- code/docs/training_zh.md +151 -0
- code/environment.yml +18 -0
- code/eval/README.md +72 -0
- code/eval/task/canonical_data.py +450 -0
- code/eval/task/eval.sh +1018 -0
- code/eval/task/eval_image_mc_vllm.py +511 -0
- code/eval/task/eval_prompt.py +247 -0
- code/eval/task/eval_revsi_multinode.sh +229 -0
- code/eval/task/eval_vllm.py +0 -0
- code/eval/task/mindcube/data_utils.py +254 -0
- code/eval/task/mmsi/eval_mmsi_transformers.py +323 -0
- code/eval/task/mmsi/run_eval_transformers.sh +214 -0
- code/eval/task/qwenvl_decord_patch.py +271 -0
- code/eval/task/revsi/eval_revsi_vllm.py +1025 -0
- code/eval/task/revsi/merge_multinode_shards.py +81 -0
- code/eval/task/revsi/run_eval_vllm.sh +321 -0
- code/eval/task/segmentation/eval_seg_vllm.py +446 -0
- code/eval/task/segmentation/post_sam2.py +491 -0
- code/eval/task/segmentation/run_eval_vllm.sh +500 -0
- code/eval/task/spatial_grounding/_grounding_utils.py +386 -0
.gitattributes
CHANGED
|
@@ -1,2 +1,43 @@
|
|
| 1 |
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 2 |
tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 2 |
tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
code/assets/orarl-data-scaling.gif filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
code/assets/orarl-hero.gif filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
code/assets/orarl-method.gif filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
code/assets/orarl-model-scaling.gif filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
code/assets/orarl-teaser.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
code/assets/paper-data-scaling.png filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
code/assets/paper-framework.png filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
code/assets/paper-model-scaling.png filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
code/assets/paper-results.png filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
code/runs/smoke-training-9b-20260825_175949/grpo/global_step_1/actor/model_world_size_8_rank_0.pt filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
code/runs/smoke-training-9b-20260825_175949/grpo/global_step_1/actor/model_world_size_8_rank_1.pt filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
code/runs/smoke-training-9b-20260825_175949/grpo/global_step_1/actor/model_world_size_8_rank_2.pt filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
code/runs/smoke-training-9b-20260825_175949/grpo/global_step_1/actor/model_world_size_8_rank_3.pt filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
code/runs/smoke-training-9b-20260825_175949/grpo/global_step_1/actor/model_world_size_8_rank_4.pt filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
code/runs/smoke-training-9b-20260825_175949/grpo/global_step_1/actor/model_world_size_8_rank_5.pt filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
code/runs/smoke-training-9b-20260825_175949/grpo/global_step_1/actor/model_world_size_8_rank_6.pt filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
code/runs/smoke-training-9b-20260825_175949/grpo/global_step_1/actor/model_world_size_8_rank_7.pt filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
code/runs/smoke-training-9b-20260825_175949/grpo/global_step_1/actor/optim_world_size_8_rank_0.pt filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
code/runs/smoke-training-9b-20260825_175949/grpo/global_step_1/actor/optim_world_size_8_rank_1.pt filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
code/runs/smoke-training-9b-20260825_175949/grpo/global_step_1/actor/optim_world_size_8_rank_2.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
code/runs/smoke-training-9b-20260825_175949/grpo/global_step_1/actor/optim_world_size_8_rank_3.pt filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
code/runs/smoke-training-9b-20260825_175949/grpo/global_step_1/actor/optim_world_size_8_rank_4.pt filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
code/runs/smoke-training-9b-20260825_175949/grpo/global_step_1/actor/optim_world_size_8_rank_5.pt filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
code/runs/smoke-training-9b-20260825_175949/grpo/global_step_1/actor/optim_world_size_8_rank_6.pt filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
code/runs/smoke-training-9b-20260825_175949/grpo/global_step_1/actor/optim_world_size_8_rank_7.pt filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
code/runs/smoke-training-9b-20260825_175949/orarl/global_step_1/actor/model_world_size_8_rank_0.pt filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
code/runs/smoke-training-9b-20260825_175949/orarl/global_step_1/actor/model_world_size_8_rank_1.pt filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
code/runs/smoke-training-9b-20260825_175949/orarl/global_step_1/actor/model_world_size_8_rank_2.pt filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
code/runs/smoke-training-9b-20260825_175949/orarl/global_step_1/actor/model_world_size_8_rank_3.pt filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
code/runs/smoke-training-9b-20260825_175949/orarl/global_step_1/actor/model_world_size_8_rank_4.pt filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
code/runs/smoke-training-9b-20260825_175949/orarl/global_step_1/actor/model_world_size_8_rank_5.pt filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
code/runs/smoke-training-9b-20260825_175949/orarl/global_step_1/actor/model_world_size_8_rank_6.pt filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
code/runs/smoke-training-9b-20260825_175949/orarl/global_step_1/actor/model_world_size_8_rank_7.pt filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
code/runs/smoke-training-9b-20260825_175949/orarl/global_step_1/actor/optim_world_size_8_rank_0.pt filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
code/runs/smoke-training-9b-20260825_175949/orarl/global_step_1/actor/optim_world_size_8_rank_1.pt filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
code/runs/smoke-training-9b-20260825_175949/orarl/global_step_1/actor/optim_world_size_8_rank_2.pt filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
code/runs/smoke-training-9b-20260825_175949/orarl/global_step_1/actor/optim_world_size_8_rank_3.pt filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
code/runs/smoke-training-9b-20260825_175949/orarl/global_step_1/actor/optim_world_size_8_rank_4.pt filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
code/runs/smoke-training-9b-20260825_175949/orarl/global_step_1/actor/optim_world_size_8_rank_5.pt filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
code/runs/smoke-training-9b-20260825_175949/orarl/global_step_1/actor/optim_world_size_8_rank_6.pt filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
code/runs/smoke-training-9b-20260825_175949/orarl/global_step_1/actor/optim_world_size_8_rank_7.pt filter=lfs diff=lfs merge=lfs -text
|
code/.gitignore
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python and test state
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*.egg-info/
|
| 5 |
+
.pytest_cache/
|
| 6 |
+
.ruff_cache/
|
| 7 |
+
.mypy_cache/
|
| 8 |
+
.nox/
|
| 9 |
+
.tox/
|
| 10 |
+
.coverage
|
| 11 |
+
htmlcov/
|
| 12 |
+
build/
|
| 13 |
+
dist/
|
| 14 |
+
.venv/
|
| 15 |
+
venv/
|
| 16 |
+
|
| 17 |
+
# Local inputs and generated runs
|
| 18 |
+
/artifacts/
|
| 19 |
+
/cache/
|
| 20 |
+
/checkpoint/
|
| 21 |
+
/checkpoints/
|
| 22 |
+
/data/
|
| 23 |
+
/local_data/
|
| 24 |
+
/logs/
|
| 25 |
+
/models/
|
| 26 |
+
/outputs/
|
| 27 |
+
/prepared/
|
| 28 |
+
/ray_results/
|
| 29 |
+
/runs/
|
| 30 |
+
/wandb/
|
| 31 |
+
/orarl-eval-summary.json
|
| 32 |
+
nohup.out
|
| 33 |
+
core.*
|
| 34 |
+
|
| 35 |
+
# Model, media, and generated dataset payloads
|
| 36 |
+
*.arrow
|
| 37 |
+
*.avi
|
| 38 |
+
*.bin
|
| 39 |
+
*.ckpt
|
| 40 |
+
*.jsonl
|
| 41 |
+
*.mkv
|
| 42 |
+
*.mov
|
| 43 |
+
*.mp4
|
| 44 |
+
*.npy
|
| 45 |
+
*.npz
|
| 46 |
+
*.parquet
|
| 47 |
+
*.pt
|
| 48 |
+
*.pth
|
| 49 |
+
*.safetensors
|
| 50 |
+
*.webm
|
| 51 |
+
*.log
|
| 52 |
+
|
| 53 |
+
# Only the small evaluation profile manifest is release source. Canonical
|
| 54 |
+
# annotations, media, subtitles, and processed artifacts live on Hugging Face.
|
| 55 |
+
!/data/
|
| 56 |
+
/data/*
|
| 57 |
+
!/data/eval/
|
| 58 |
+
/data/eval/*
|
| 59 |
+
!/data/eval/datasets.jsonl
|
| 60 |
+
!/data/eval/README.md
|
| 61 |
+
|
| 62 |
+
# README media shipped with the repository
|
| 63 |
+
!/assets/orarl-teaser.mp4
|
| 64 |
+
|
| 65 |
+
# Editor and operating-system state
|
| 66 |
+
.DS_Store
|
| 67 |
+
.idea/
|
| 68 |
+
.ipynb_checkpoints/
|
| 69 |
+
.vscode/
|
| 70 |
+
*~
|
| 71 |
+
|
| 72 |
+
# Local configuration
|
| 73 |
+
.env
|
| 74 |
+
.env.*
|
| 75 |
+
*.local.yaml
|
| 76 |
+
*.local.yml
|
| 77 |
+
*.key
|
| 78 |
+
*.pem
|
| 79 |
+
credentials.json
|
| 80 |
+
token
|
code/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Apache License
|
| 2 |
+
Version 2.0, January 2004
|
| 3 |
+
http://www.apache.org/licenses/
|
| 4 |
+
|
| 5 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 6 |
+
|
| 7 |
+
1. Definitions.
|
| 8 |
+
|
| 9 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 10 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 11 |
+
|
| 12 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 13 |
+
the copyright owner that is granting the License.
|
| 14 |
+
|
| 15 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 16 |
+
other entities that control, are controlled by, or are under common
|
| 17 |
+
control with that entity. For the purposes of this definition,
|
| 18 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 19 |
+
direction or management of such entity, whether by contract or
|
| 20 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 21 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 22 |
+
|
| 23 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 24 |
+
exercising permissions granted by this License.
|
| 25 |
+
|
| 26 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 27 |
+
including but not limited to software source code, documentation
|
| 28 |
+
source, and configuration files.
|
| 29 |
+
|
| 30 |
+
"Object" form shall mean any form resulting from mechanical
|
| 31 |
+
transformation or translation of a Source form, including but
|
| 32 |
+
not limited to compiled object code, generated documentation,
|
| 33 |
+
and conversions to other media types.
|
| 34 |
+
|
| 35 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 36 |
+
Object form, made available under the License, as indicated by a
|
| 37 |
+
copyright notice that is included in or attached to the work
|
| 38 |
+
(an example is provided in the Appendix below).
|
| 39 |
+
|
| 40 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 41 |
+
form, that is based on (or derived from) the Work and for which the
|
| 42 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 43 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 44 |
+
of this License, Derivative Works shall not include works that remain
|
| 45 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 46 |
+
the Work and Derivative Works thereof.
|
| 47 |
+
|
| 48 |
+
"Contribution" shall mean any work of authorship, including
|
| 49 |
+
the original version of the Work and any modifications or additions
|
| 50 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 51 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 52 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 53 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 54 |
+
means any form of electronic, verbal, or written communication sent
|
| 55 |
+
to the Licensor or its representatives, including but not limited to
|
| 56 |
+
communication on electronic mailing lists, source code control systems,
|
| 57 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 58 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 59 |
+
excluding communication that is conspicuously marked or otherwise
|
| 60 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 61 |
+
|
| 62 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 63 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 64 |
+
subsequently incorporated within the Work.
|
| 65 |
+
|
| 66 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 67 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 68 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 69 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 70 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 71 |
+
Work and such Derivative Works in Source or Object form.
|
| 72 |
+
|
| 73 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 74 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 75 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 76 |
+
(except as stated in this section) patent license to make, have made,
|
| 77 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 78 |
+
where such license applies only to those patent claims licensable
|
| 79 |
+
by such Contributor that are necessarily infringed by their
|
| 80 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 81 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 82 |
+
institute patent litigation against any entity (including a
|
| 83 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 84 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 85 |
+
or contributory patent infringement, then any patent licenses
|
| 86 |
+
granted to You under this License for that Work shall terminate
|
| 87 |
+
as of the date such litigation is filed.
|
| 88 |
+
|
| 89 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 90 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 91 |
+
modifications, and in Source or Object form, provided that You
|
| 92 |
+
meet the following conditions:
|
| 93 |
+
|
| 94 |
+
(a) You must give any other recipients of the Work or
|
| 95 |
+
Derivative Works a copy of this License; and
|
| 96 |
+
|
| 97 |
+
(b) You must cause any modified files to carry prominent notices
|
| 98 |
+
stating that You changed the files; and
|
| 99 |
+
|
| 100 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 101 |
+
that You distribute, all copyright, patent, trademark, and
|
| 102 |
+
attribution notices from the Source form of the Work,
|
| 103 |
+
excluding those notices that do not pertain to any part of
|
| 104 |
+
the Derivative Works; and
|
| 105 |
+
|
| 106 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 107 |
+
distribution, then any Derivative Works that You distribute must
|
| 108 |
+
include a readable copy of the attribution notices contained
|
| 109 |
+
within such NOTICE file, excluding those notices that do not
|
| 110 |
+
pertain to any part of the Derivative Works, in at least one
|
| 111 |
+
of the following places: within a NOTICE text file distributed
|
| 112 |
+
as part of the Derivative Works; within the Source form or
|
| 113 |
+
documentation, if provided along with the Derivative Works; or,
|
| 114 |
+
within a display generated by the Derivative Works, if and
|
| 115 |
+
wherever such third-party notices normally appear. The contents
|
| 116 |
+
of the NOTICE file are for informational purposes only and
|
| 117 |
+
do not modify the License. You may add Your own attribution
|
| 118 |
+
notices within Derivative Works that You distribute, alongside
|
| 119 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 120 |
+
that such additional attribution notices cannot be construed
|
| 121 |
+
as modifying the License.
|
| 122 |
+
|
| 123 |
+
You may add Your own copyright statement to Your modifications and
|
| 124 |
+
may provide additional or different license terms and conditions
|
| 125 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 126 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 127 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 128 |
+
the conditions stated in this License.
|
| 129 |
+
|
| 130 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 131 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 132 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 133 |
+
this License, without any additional terms or conditions.
|
| 134 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 135 |
+
the terms of any separate license agreement you may have executed
|
| 136 |
+
with Licensor regarding such Contributions.
|
| 137 |
+
|
| 138 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 139 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 140 |
+
except as required for reasonable and customary use in describing the
|
| 141 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 142 |
+
|
| 143 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 144 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 145 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 146 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 147 |
+
implied, including, without limitation, any warranties or conditions
|
| 148 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 149 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 150 |
+
appropriateness of using or redistributing the Work and assume any
|
| 151 |
+
risks associated with Your exercise of permissions under this License.
|
| 152 |
+
|
| 153 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 154 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 155 |
+
unless required by applicable law (such as deliberate and grossly
|
| 156 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 157 |
+
liable to You for damages, including any direct, indirect, special,
|
| 158 |
+
incidental, or consequential damages of any character arising as a
|
| 159 |
+
result of this License or out of the use or inability to use the
|
| 160 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 161 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 162 |
+
other commercial damages or losses), even if such Contributor
|
| 163 |
+
has been advised of the possibility of such damages.
|
| 164 |
+
|
| 165 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 166 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 167 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 168 |
+
or other liability obligations and/or rights consistent with this
|
| 169 |
+
License. However, in accepting such obligations, You may act only
|
| 170 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 171 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 172 |
+
defend, and hold each Contributor harmless for any liability
|
| 173 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 174 |
+
of your accepting any such warranty or additional liability.
|
| 175 |
+
|
| 176 |
+
END OF TERMS AND CONDITIONS
|
| 177 |
+
|
| 178 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 179 |
+
|
| 180 |
+
To apply the Apache License to your work, attach the following
|
| 181 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 182 |
+
replaced with your own identifying information. (Don't include
|
| 183 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 184 |
+
comment syntax for the file format. We also recommend that a
|
| 185 |
+
file or class name and description of purpose be included on the
|
| 186 |
+
same "printed page" as the copyright notice for easier
|
| 187 |
+
identification within third-party archives.
|
| 188 |
+
|
| 189 |
+
Copyright 2026 The OraRL Authors
|
| 190 |
+
|
| 191 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 192 |
+
you may not use this file except in compliance with the License.
|
| 193 |
+
You may obtain a copy of the License at
|
| 194 |
+
|
| 195 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 196 |
+
|
| 197 |
+
Unless required by applicable law or agreed to in writing, software
|
| 198 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 199 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 200 |
+
See the License for the specific language governing permissions and
|
| 201 |
+
limitations under the License.
|
code/MANIFEST.in
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
include LICENSE
|
| 2 |
+
include NOTICE
|
| 3 |
+
include README.md
|
| 4 |
+
include environment.yml
|
| 5 |
+
include pyproject.toml
|
| 6 |
+
include requirements-cu129.txt
|
| 7 |
+
recursive-include configs *.yaml
|
| 8 |
+
recursive-include data/eval *.jsonl *.md
|
| 9 |
+
recursive-include docs *.md
|
| 10 |
+
recursive-include eval *.md *.py *.sh
|
| 11 |
+
recursive-include scripts *.py *.sh
|
| 12 |
+
recursive-include tests *.py
|
| 13 |
+
recursive-include verl *.py
|
| 14 |
+
prune build
|
| 15 |
+
prune dist
|
| 16 |
+
prune .pytest_cache
|
| 17 |
+
prune .ruff_cache
|
| 18 |
+
prune checkpoint
|
| 19 |
+
prune checkpoints
|
| 20 |
+
global-exclude __pycache__ *.py[cod] .DS_Store
|
code/NOTICE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
OraRL
|
| 2 |
+
Copyright 2026 The OraRL Authors
|
| 3 |
+
|
| 4 |
+
Portions of this software are derived from veRL and other Apache-2.0 upstream
|
| 5 |
+
projects. The derived portions retain their original copyright notices,
|
| 6 |
+
including Copyright 2024 ByteDance Ltd. and/or its affiliates, and are provided
|
| 7 |
+
under the Apache License, Version 2.0.
|
code/README.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<p align="right"><a href="README_zh.md">简体中文</a></p>
|
| 2 |
+
|
| 3 |
+
<div align="center">
|
| 4 |
+
|
| 5 |
+
# OraRL
|
| 6 |
+
|
| 7 |
+
### Annotations as Rollouts
|
| 8 |
+
|
| 9 |
+
**Efficient and scalable reinforcement learning for unified video MLLMs**
|
| 10 |
+
|
| 11 |
+
Yunheng Li · Guohong Mu · Hao Li · Shengsheng Qian · Dingwen Zhang ·
|
| 12 |
+
Qibin Hou · Ming-Ming Cheng
|
| 13 |
+
|
| 14 |
+
<p>
|
| 15 |
+
<a href="https://arxiv.org/abs/2608.20492">📄 Paper</a>
|
| 16 |
+
·
|
| 17 |
+
<a href="https://orarl.github.io/">🌐 Project Page</a>
|
| 18 |
+
·
|
| 19 |
+
<a href="#models">🤗 Models (4B / 9B)</a>
|
| 20 |
+
</p>
|
| 21 |
+
<p>
|
| 22 |
+
<a href="docs/environment.md">⚙️ Environment</a>
|
| 23 |
+
·
|
| 24 |
+
<a href="docs/training.md">🚀 Training</a>
|
| 25 |
+
·
|
| 26 |
+
<a href="docs/evaluation.md">📊 Evaluation</a>
|
| 27 |
+
·
|
| 28 |
+
<a href="LICENSE">⚖️ License</a>
|
| 29 |
+
</p>
|
| 30 |
+
|
| 31 |
+
<a href="https://orarl.github.io/assets/orarl-teaser.mp4">
|
| 32 |
+
<img src="assets/orarl-hero.gif"
|
| 33 |
+
alt="Animated OraRL method preview" width="92%">
|
| 34 |
+
</a>
|
| 35 |
+
|
| 36 |
+
**▶ Click the image to watch the 1:38 project overview.**
|
| 37 |
+
|
| 38 |
+
</div>
|
| 39 |
+
|
| 40 |
+
## Why OraRL
|
| 41 |
+
|
| 42 |
+
- **Annotation-as-rollout:** annotations become reliable positive rollouts while
|
| 43 |
+
policy samples retain an on-policy baseline.
|
| 44 |
+
- **Seven task families:** one update rule covers temporal and spatial grounding,
|
| 45 |
+
segmentation, tracking, spatial-temporal grounding, video QA, and spatial
|
| 46 |
+
intelligence.
|
| 47 |
+
- **Efficient training (4B):** sign-balanced pruning delivers **1.48× faster
|
| 48 |
+
updates** (**92.5 → 62.4 s/step**) while reducing peak per-GPU memory from
|
| 49 |
+
**62.4 to 50.9 GB**.
|
| 50 |
+
- **Efficient inference:** on one H20 with vLLM in BF16, weight loading occupies
|
| 51 |
+
**8.6 GiB (4B)** and **17.6 GiB (9B)**. On ten-minute, 2-fps videos,
|
| 52 |
+
answer-only decoding cuts median post-TTFT latency from **4.78 s to 130 ms**
|
| 53 |
+
and total latency from **29.03 to 24.30 s**.
|
| 54 |
+
- **Multimodal veRL infrastructure:** a unified video contract carries cached
|
| 55 |
+
artifacts, raw paths, or inline frame tensors through vLLM rollouts and FSDP
|
| 56 |
+
updates, with decode-once frame reuse, temporal metadata, task-grouped
|
| 57 |
+
batching, asynchronous Ray rewards, and safe hybrid-engine cache handling.
|
| 58 |
+
|
| 59 |
+
## OraRL in One Update
|
| 60 |
+
|
| 61 |
+
<p align="center">
|
| 62 |
+
<img src="assets/orarl-method.gif"
|
| 63 |
+
alt="Animated OraRL framework" width="96%">
|
| 64 |
+
</p>
|
| 65 |
+
|
| 66 |
+
An OraRL update separates reliable annotation guidance from on-policy
|
| 67 |
+
normalization:
|
| 68 |
+
|
| 69 |
+
1. **Build the group:** append one serialized annotation rollout to the policy
|
| 70 |
+
samples generated for the same prompt.
|
| 71 |
+
2. **Keep the baseline on-policy:** estimate the group baseline from policy
|
| 72 |
+
rewards only.
|
| 73 |
+
3. **Guide and select:** convert the annotation-policy reward gap into a
|
| 74 |
+
correction, then retain a sign-balanced subset for the update.
|
| 75 |
+
|
| 76 |
+
This design uses task-native annotations directly and requires no
|
| 77 |
+
chain-of-thought supervision or decoding.
|
| 78 |
+
|
| 79 |
+
## Video-ORA Results
|
| 80 |
+
|
| 81 |
+
<p align="center">
|
| 82 |
+
<img src="assets/paper-results.png"
|
| 83 |
+
alt="Video-ORA-9B results across seven task families" width="100%">
|
| 84 |
+
</p>
|
| 85 |
+
|
| 86 |
+
### Dataset-Level Results
|
| 87 |
+
|
| 88 |
+
<picture>
|
| 89 |
+
<source media="(prefers-color-scheme: dark)"
|
| 90 |
+
srcset="assets/video_ora_benchmark_matrix_dark.svg">
|
| 91 |
+
<source media="(prefers-color-scheme: light)"
|
| 92 |
+
srcset="assets/video_ora_benchmark_matrix_light.svg">
|
| 93 |
+
<img src="assets/video_ora_benchmark_matrix_light.svg"
|
| 94 |
+
alt="Dataset-level benchmark matrix comparing Video-ORA with multimodal baselines"
|
| 95 |
+
width="100%">
|
| 96 |
+
</picture>
|
| 97 |
+
|
| 98 |
+
Video-ORA-9B leads the matched seven-family comparison without CoT decoding.
|
| 99 |
+
Best and second-best values are highlighted per row; `†` denotes an
|
| 100 |
+
original-report value whose frame, prompt, split, or decoding settings may
|
| 101 |
+
differ. Averages require complete family coverage.
|
| 102 |
+
|
| 103 |
+
<!-- <details>
|
| 104 |
+
<summary>Benchmark sources</summary>
|
| 105 |
+
|
| 106 |
+
Unmarked values come from Tables 1–8 and Appendix Table 20 of the latest
|
| 107 |
+
[OraRL paper](https://arxiv.org/abs/2608.20492). External entries follow the original
|
| 108 |
+
[LLaVA-OneVision-2](https://arxiv.org/abs/2605.25979),
|
| 109 |
+
[VideoChat3](https://github.com/MCG-NJU/VideoChat3), and
|
| 110 |
+
[OneThinker](https://arxiv.org/abs/2512.03043) reports. OneThinker is cited only
|
| 111 |
+
as the source of public Qwen3-VL scores. ReVSI uses each model's reported frame
|
| 112 |
+
setting; the paper's three-benchmark spatial-intelligence average excludes it.
|
| 113 |
+
|
| 114 |
+
</details> -->
|
| 115 |
+
|
| 116 |
+
### Model Scaling
|
| 117 |
+
|
| 118 |
+
<p align="center">
|
| 119 |
+
<img src="assets/orarl-model-scaling.gif"
|
| 120 |
+
alt="Animated Video-ORA model scaling from 0.8B to 9B" width="100%">
|
| 121 |
+
</p>
|
| 122 |
+
|
| 123 |
+
### Data Scaling
|
| 124 |
+
|
| 125 |
+
<p align="center">
|
| 126 |
+
<img src="assets/orarl-data-scaling.gif"
|
| 127 |
+
alt="Animated OraRL data scaling and reward dynamics" width="100%">
|
| 128 |
+
</p>
|
| 129 |
+
|
| 130 |
+
## Models
|
| 131 |
+
|
| 132 |
+
| Model | Backbone | Released recipe | Weights |
|
| 133 |
+
| --- | --- | --- | --- |
|
| 134 |
+
| **Video-ORA-9B** | Qwen3.5-9B | `orarl_9b.yaml` | [Hugging Face](https://huggingface.co/OraRL/Video-ORA-9B) |
|
| 135 |
+
| **Video-ORA-4B** | Qwen3.5-4B | `orarl_4b.yaml` | Hugging Face (coming soon) |
|
| 136 |
+
|
| 137 |
+
### vLLM Serving
|
| 138 |
+
|
| 139 |
+
Both Video-ORA checkpoints load directly with **vLLM 0.19.1** for
|
| 140 |
+
OpenAI-compatible serving:
|
| 141 |
+
|
| 142 |
+
```bash
|
| 143 |
+
MODEL=OraRL/Video-ORA-9B
|
| 144 |
+
|
| 145 |
+
vllm serve "$MODEL" \
|
| 146 |
+
--served-model-name Video-ORA-9B \
|
| 147 |
+
--trust-remote-code \
|
| 148 |
+
--dtype bfloat16 \
|
| 149 |
+
--tensor-parallel-size 1 \
|
| 150 |
+
--max-model-len 131072 \
|
| 151 |
+
--limit-mm-per-prompt '{"image": 1, "video": 1}'
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
Set `--tensor-parallel-size` to the GPU count for multi-GPU deployment and
|
| 155 |
+
lower `--max-model-len` on smaller-memory devices. Use
|
| 156 |
+
`enable_thinking=false` in the chat template for answer-only inference.
|
| 157 |
+
|
| 158 |
+
## Use OraRL
|
| 159 |
+
|
| 160 |
+
The release is organized around three user-facing workflows:
|
| 161 |
+
|
| 162 |
+
1. **[Environment](docs/environment.md):** install the pinned CUDA stack that
|
| 163 |
+
covers both the bundled trainer and the evaluators.
|
| 164 |
+
2. **[Training](docs/training.md):** prepare licensed local training data and
|
| 165 |
+
launch GRPO or OraRL on one or multiple nodes.
|
| 166 |
+
3. **[Evaluation](docs/evaluation.md):** download Video-ORA and OraRL-Data,
|
| 167 |
+
then run a smoke test or the complete paper suite.
|
| 168 |
+
|
| 169 |
+
Training and evaluation are dry runs by default; inspect the resolved command
|
| 170 |
+
before adding `--run`. Checkpoints and evaluation media are hosted under the
|
| 171 |
+
[OraRL Hugging Face organization](https://huggingface.co/OraRL).
|
| 172 |
+
|
| 173 |
+
## Acknowledgements
|
| 174 |
+
|
| 175 |
+
OraRL is built on [veRL](https://github.com/volcengine/verl) — a
|
| 176 |
+
high-performance RL framework with HybridEngine. We thank its authors and
|
| 177 |
+
contributors for open-sourcing the training infrastructure.
|
| 178 |
+
|
| 179 |
+
## License
|
| 180 |
+
|
| 181 |
+
OraRL source is released under [Apache-2.0](LICENSE). Datasets, models,
|
| 182 |
+
benchmarks, and optional dependencies retain their original licenses; see
|
| 183 |
+
[NOTICE](NOTICE).
|
| 184 |
+
|
| 185 |
+
## Citation
|
| 186 |
+
|
| 187 |
+
If you find OraRL useful, please consider giving this repository a ⭐ and
|
| 188 |
+
citing our [paper](https://arxiv.org/abs/2608.20492).
|
| 189 |
+
|
| 190 |
+
```bibtex
|
| 191 |
+
@article{li2026orarl,
|
| 192 |
+
title = {Annotations as Rollouts: Efficient and Scalable
|
| 193 |
+
Reinforcement Learning for Video MLLMs},
|
| 194 |
+
author = {Li, Yunheng and Mu, Guohong and Li, Hao and
|
| 195 |
+
Qian, Shengsheng and Zhang, Dingwen and Hou, Qibin
|
| 196 |
+
and Cheng, Ming-Ming},
|
| 197 |
+
journal = {arXiv preprint arXiv:2608.20492},
|
| 198 |
+
year = {2026},
|
| 199 |
+
url = {https://arxiv.org/abs/2608.20492}
|
| 200 |
+
}
|
| 201 |
+
```
|
code/README_zh.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<p align="right"><a href="README.md">English</a></p>
|
| 2 |
+
|
| 3 |
+
<div align="center">
|
| 4 |
+
|
| 5 |
+
# OraRL
|
| 6 |
+
|
| 7 |
+
### 将标注作为 Rollout
|
| 8 |
+
|
| 9 |
+
**面向统一视频多模态大模型的高效、可扩展强化学习**
|
| 10 |
+
|
| 11 |
+
Yunheng Li · Guohong Mu · Hao Li · Shengsheng Qian · Dingwen Zhang ·
|
| 12 |
+
Qibin Hou · Ming-Ming Cheng
|
| 13 |
+
|
| 14 |
+
<p>
|
| 15 |
+
<a href="https://arxiv.org/abs/2608.20492">📄 论文</a>
|
| 16 |
+
·
|
| 17 |
+
<a href="https://orarl.github.io/">🌐 项目主页</a>
|
| 18 |
+
·
|
| 19 |
+
<a href="#models">🤗 模型(4B / 9B)</a>
|
| 20 |
+
</p>
|
| 21 |
+
<p>
|
| 22 |
+
<a href="docs/environment_zh.md">⚙️ 环境</a>
|
| 23 |
+
·
|
| 24 |
+
<a href="docs/training_zh.md">🚀 训练</a>
|
| 25 |
+
·
|
| 26 |
+
<a href="docs/evaluation_zh.md">📊 评测</a>
|
| 27 |
+
·
|
| 28 |
+
<a href="LICENSE">⚖️ 许可证</a>
|
| 29 |
+
</p>
|
| 30 |
+
|
| 31 |
+
<a href="https://orarl.github.io/assets/orarl-teaser.mp4">
|
| 32 |
+
<img src="assets/orarl-hero.gif"
|
| 33 |
+
alt="OraRL 方法动态预览" width="92%">
|
| 34 |
+
</a>
|
| 35 |
+
|
| 36 |
+
**▶ 点击图片观看 1 分 38 秒的项目介绍视频。**
|
| 37 |
+
|
| 38 |
+
</div>
|
| 39 |
+
|
| 40 |
+
## 为什么选择 OraRL
|
| 41 |
+
|
| 42 |
+
- **将标注作为 Rollout:** 将标注转化为可靠的正样本 rollout,同时策略样本
|
| 43 |
+
仍使用纯 on-policy 基线。
|
| 44 |
+
- **七类任务统一训练:** 同一套更新规则覆盖时序定位、空间定位、分割、跟踪、
|
| 45 |
+
时空定位、视频问答和空间智能。
|
| 46 |
+
- **高效训练(4B):** 符号均衡剪枝带来 **1.48× 的更新加速**
|
| 47 |
+
(**92.5 → 62.4 秒/步**),同时将单卡峰值显存从 **62.4 GB 降至
|
| 48 |
+
50.9 GB**。
|
| 49 |
+
- **高效推理:** 在单张 H20 上使用 vLLM 和 BF16 时,权重加载占用分别为
|
| 50 |
+
**8.6 GiB(4B)**和 **17.6 GiB(9B)**。对于 2 fps 采样的十分钟视频,
|
| 51 |
+
仅生成答案将首 token 后的中位延迟从 **4.78 秒降至 130 毫秒**,并将总
|
| 52 |
+
中位延迟从 **29.03 秒降至 24.30 秒**。
|
| 53 |
+
- **多模态 veRL 基础设施:** 统一的视频数据契约支持缓存产物、原始路径和内联
|
| 54 |
+
帧张量,并贯通 vLLM rollout 与 FSDP 更新;同时加入单次解码帧复用、时序
|
| 55 |
+
元数据、按任务分组的批处理、异步 Ray 奖励和安全的混合引擎缓存处理。
|
| 56 |
+
|
| 57 |
+
## 一次 OraRL 更新
|
| 58 |
+
|
| 59 |
+
<p align="center">
|
| 60 |
+
<img src="assets/orarl-method.gif"
|
| 61 |
+
alt="OraRL 方法框架动画" width="96%">
|
| 62 |
+
</p>
|
| 63 |
+
|
| 64 |
+
一次 OraRL 更新将可靠的标注引导与 on-policy 归一化明确分开:
|
| 65 |
+
|
| 66 |
+
1. **构造样本组:** 在同一 prompt 的策略样本后加入一个序列化标注 rollout。
|
| 67 |
+
2. **保持 on-policy 基线:** 仅使用策略样本的奖励估计组内基线。
|
| 68 |
+
3. **引导并筛选:** 将标注与策略样本的奖励差转化为修正量,再选择符号均衡的
|
| 69 |
+
子集执行更新。
|
| 70 |
+
|
| 71 |
+
该设计直接使用任务原生标注,不需要思维链监督或解码。
|
| 72 |
+
|
| 73 |
+
## Video-ORA 结果
|
| 74 |
+
|
| 75 |
+
<p align="center">
|
| 76 |
+
<img src="assets/paper-results.png"
|
| 77 |
+
alt="Video-ORA-9B 在七类任务上的结果" width="100%">
|
| 78 |
+
</p>
|
| 79 |
+
|
| 80 |
+
### 数据集级结果
|
| 81 |
+
|
| 82 |
+
<picture>
|
| 83 |
+
<source media="(prefers-color-scheme: dark)"
|
| 84 |
+
srcset="assets/video_ora_benchmark_matrix_dark.svg">
|
| 85 |
+
<source media="(prefers-color-scheme: light)"
|
| 86 |
+
srcset="assets/video_ora_benchmark_matrix_light.svg">
|
| 87 |
+
<img src="assets/video_ora_benchmark_matrix_light.svg"
|
| 88 |
+
alt="Video-ORA 与多模态基线的数据集级对比矩阵"
|
| 89 |
+
width="100%">
|
| 90 |
+
</picture>
|
| 91 |
+
|
| 92 |
+
Video-ORA-9B 在不使用思维链解码的情况下,在统一的七类任务对比中取得领先。
|
| 93 |
+
每行的最佳和次佳结果均已突出显示;`†` 表示该数值来自原始报告,其帧数、提示词、
|
| 94 |
+
数据划分或解码设置可能不同。仅在完整覆盖该任务类别时才计算类别平均值。
|
| 95 |
+
|
| 96 |
+
<!-- <details>
|
| 97 |
+
<summary>基准来源</summary>
|
| 98 |
+
|
| 99 |
+
未标注的数值来自最新版 [OraRL 论文](https://arxiv.org/abs/2608.20492)的表 1–8 和附录表 20。
|
| 100 |
+
外部数值来自
|
| 101 |
+
[LLaVA-OneVision-2](https://arxiv.org/abs/2605.25979)、
|
| 102 |
+
[VideoChat3](https://github.com/MCG-NJU/VideoChat3) 和
|
| 103 |
+
[OneThinker](https://arxiv.org/abs/2512.03043) 的原始报告。
|
| 104 |
+
此处仅将 OneThinker 作为公开 Qwen3-VL 分数的来源。ReVSI 使用各模型报告的
|
| 105 |
+
帧设置;论文中的三基准空间智能平均值不包含 ReVSI。
|
| 106 |
+
|
| 107 |
+
</details> -->
|
| 108 |
+
|
| 109 |
+
### 模型规模扩展
|
| 110 |
+
|
| 111 |
+
<p align="center">
|
| 112 |
+
<img src="assets/orarl-model-scaling.gif"
|
| 113 |
+
alt="Video-ORA 从 0.8B 到 9B 的模型规模扩展动画" width="100%">
|
| 114 |
+
</p>
|
| 115 |
+
|
| 116 |
+
### 数据规模扩展
|
| 117 |
+
|
| 118 |
+
<p align="center">
|
| 119 |
+
<img src="assets/orarl-data-scaling.gif"
|
| 120 |
+
alt="OraRL 数据规模扩展与奖励变化动画" width="100%">
|
| 121 |
+
</p>
|
| 122 |
+
|
| 123 |
+
<a id="models"></a>
|
| 124 |
+
|
| 125 |
+
## 模型
|
| 126 |
+
|
| 127 |
+
| 模型 | 基座模型 | 发布配置 | 权重 |
|
| 128 |
+
| --- | --- | --- | --- |
|
| 129 |
+
| **Video-ORA-9B** | Qwen3.5-9B | `orarl_9b.yaml` | [Hugging Face](https://huggingface.co/OraRL/Video-ORA-9B) |
|
| 130 |
+
| **Video-ORA-4B** | Qwen3.5-4B | `orarl_4b.yaml` | Hugging Face(即将发布) |
|
| 131 |
+
|
| 132 |
+
### vLLM 部署
|
| 133 |
+
|
| 134 |
+
两个 Video-ORA 检查点均可通过 **vLLM 0.19.1** 直接提供��容 OpenAI 的
|
| 135 |
+
推理服务:
|
| 136 |
+
|
| 137 |
+
```bash
|
| 138 |
+
MODEL=OraRL/Video-ORA-9B
|
| 139 |
+
|
| 140 |
+
vllm serve "$MODEL" \
|
| 141 |
+
--served-model-name Video-ORA-9B \
|
| 142 |
+
--trust-remote-code \
|
| 143 |
+
--dtype bfloat16 \
|
| 144 |
+
--tensor-parallel-size 1 \
|
| 145 |
+
--max-model-len 131072 \
|
| 146 |
+
--limit-mm-per-prompt '{"image": 1, "video": 1}'
|
| 147 |
+
```
|
| 148 |
+
|
| 149 |
+
多卡部署时,将 `--tensor-parallel-size` 设为 GPU 数量;显存较小时可降低
|
| 150 |
+
`--max-model-len`。如需仅生成答案,请在聊天模板中设置
|
| 151 |
+
`enable_thinking=false`。
|
| 152 |
+
|
| 153 |
+
## 使用 OraRL
|
| 154 |
+
|
| 155 |
+
本仓库围绕三个面向用户的工作流组织:
|
| 156 |
+
|
| 157 |
+
1. **[环境](docs/environment_zh.md):** 安装同时覆盖自带训练运行时与评测运行时的
|
| 158 |
+
固定 CUDA 软件栈。
|
| 159 |
+
2. **[训练](docs/training_zh.md):** 准备已获许可的本地训练数据,并启动单节点或
|
| 160 |
+
多节点 GRPO/OraRL。
|
| 161 |
+
3. **[评测](docs/evaluation_zh.md):** 下载 Video-ORA 和 OraRL-Data,运行
|
| 162 |
+
smoke test 或完整论文评测。
|
| 163 |
+
|
| 164 |
+
训练和评测默认仅执行 dry run;检查解析后的命令后再添加 `--run`。Checkpoint
|
| 165 |
+
和评测媒体托管在 [OraRL Hugging Face 组织](https://huggingface.co/OraRL)下。
|
| 166 |
+
|
| 167 |
+
## 致谢
|
| 168 |
+
|
| 169 |
+
OraRL 基于 [veRL](https://github.com/volcengine/verl) 构建——这是一个采用
|
| 170 |
+
HybridEngine 的高性能强化学习框架。感谢其作者和贡献者开源训练基础设施。
|
| 171 |
+
|
| 172 |
+
## 许可证
|
| 173 |
+
|
| 174 |
+
OraRL 源代码采用 [Apache-2.0](LICENSE) 许可证发布。数据集、模型、基准和可选
|
| 175 |
+
依赖仍遵循各自的原始许可证,详情请参阅 [NOTICE](NOTICE)。
|
| 176 |
+
|
| 177 |
+
## 引用
|
| 178 |
+
|
| 179 |
+
如果 OraRL 对您有所帮助,欢迎为本仓库点亮 ⭐,并引用我们的
|
| 180 |
+
[论文](https://arxiv.org/abs/2608.20492)。
|
| 181 |
+
|
| 182 |
+
```bibtex
|
| 183 |
+
@article{li2026orarl,
|
| 184 |
+
title = {Annotations as Rollouts: Efficient and Scalable
|
| 185 |
+
Reinforcement Learning for Video MLLMs},
|
| 186 |
+
author = {Li, Yunheng and Mu, Guohong and Li, Hao and
|
| 187 |
+
Qian, Shengsheng and Zhang, Dingwen and Hou, Qibin
|
| 188 |
+
and Cheng, Ming-Ming},
|
| 189 |
+
journal = {arXiv preprint arXiv:2608.20492},
|
| 190 |
+
year = {2026},
|
| 191 |
+
url = {https://arxiv.org/abs/2608.20492}
|
| 192 |
+
}
|
| 193 |
+
```
|
code/assets/orarl-data-scaling.gif
ADDED
|
Git LFS Details
|
code/assets/orarl-hero.gif
ADDED
|
Git LFS Details
|
code/assets/orarl-method.gif
ADDED
|
Git LFS Details
|
code/assets/orarl-model-scaling.gif
ADDED
|
Git LFS Details
|
code/assets/orarl-teaser.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7915acbb2207376ef20467792c3e5b2b86b7a7a292d7f10c1bf3f44817c28e0b
|
| 3 |
+
size 5901910
|
code/assets/paper-data-scaling.png
ADDED
|
Git LFS Details
|
code/assets/paper-framework.png
ADDED
|
Git LFS Details
|
code/assets/paper-model-scaling.png
ADDED
|
Git LFS Details
|
code/assets/paper-results.png
ADDED
|
Git LFS Details
|
code/assets/video_ora_benchmark_matrix_dark.svg
ADDED
|
|
code/assets/video_ora_benchmark_matrix_light.svg
ADDED
|
|
code/configs/data_sources.example.yaml
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copy this file, then replace every ../local_data placeholder.
|
| 2 |
+
#
|
| 3 |
+
# OraRL never downloads data or media. You must obtain each dataset under its
|
| 4 |
+
# license, keep the required attribution, and point input/media_root at your
|
| 5 |
+
# own local, licensed copy. The optional license and url values are audit
|
| 6 |
+
# metadata only and are never opened by the builder.
|
| 7 |
+
#
|
| 8 |
+
# The seven quotas reproduce the final paper mixture: 100,032 train prompts
|
| 9 |
+
# (1,563 batches of 64), with 62,656 structured prompts and 37,376 answer-only
|
| 10 |
+
# prompts. The canary is
|
| 11 |
+
# additional held-out data and is not subtracted from the train target.
|
| 12 |
+
version: 1
|
| 13 |
+
seed: 42
|
| 14 |
+
target: 100032
|
| 15 |
+
canary_size: 512
|
| 16 |
+
max_prompts_per_media: 2
|
| 17 |
+
require_media: true
|
| 18 |
+
allow_shortfall: false
|
| 19 |
+
|
| 20 |
+
# Supply JSON/JSONL benchmark records before a release build. Any candidate
|
| 21 |
+
# sharing a normalized prompt identity or media identity is removed.
|
| 22 |
+
benchmark_excludes:
|
| 23 |
+
- ../local_data/exclusions/public_benchmarks.jsonl
|
| 24 |
+
|
| 25 |
+
sources:
|
| 26 |
+
- name: temporal_grounding_train
|
| 27 |
+
input: ../local_data/annotations/temporal_grounding.jsonl
|
| 28 |
+
task: temporal grounding
|
| 29 |
+
family: temporal
|
| 30 |
+
quota: 20096
|
| 31 |
+
media_root: ../local_data/media/temporal
|
| 32 |
+
license: REPLACE_WITH_DATASET_LICENSE
|
| 33 |
+
url: REPLACE_WITH_DATASET_HOME_PAGE
|
| 34 |
+
|
| 35 |
+
- name: tracking_train
|
| 36 |
+
input: ../local_data/annotations/tracking.jsonl
|
| 37 |
+
task: tracking
|
| 38 |
+
family: tracking
|
| 39 |
+
quota: 13952
|
| 40 |
+
media_root: ../local_data/media/tracking
|
| 41 |
+
license: REPLACE_WITH_DATASET_LICENSE
|
| 42 |
+
url: REPLACE_WITH_DATASET_HOME_PAGE
|
| 43 |
+
|
| 44 |
+
- name: segmentation_train
|
| 45 |
+
input: ../local_data/annotations/segmentation.jsonl
|
| 46 |
+
task: segmentation
|
| 47 |
+
family: segmentation
|
| 48 |
+
quota: 12032
|
| 49 |
+
media_root: ../local_data/media/segmentation
|
| 50 |
+
license: REPLACE_WITH_DATASET_LICENSE
|
| 51 |
+
url: REPLACE_WITH_DATASET_HOME_PAGE
|
| 52 |
+
|
| 53 |
+
- name: spatial_grounding_train
|
| 54 |
+
input: ../local_data/annotations/spatial_grounding.jsonl
|
| 55 |
+
task: spatial grounding
|
| 56 |
+
family: spatial
|
| 57 |
+
quota: 7040
|
| 58 |
+
media_root: ../local_data/media/spatial
|
| 59 |
+
license: REPLACE_WITH_DATASET_LICENSE
|
| 60 |
+
url: REPLACE_WITH_DATASET_HOME_PAGE
|
| 61 |
+
|
| 62 |
+
- name: spatial_temporal_grounding_train
|
| 63 |
+
input: ../local_data/annotations/spatial_temporal_grounding.jsonl
|
| 64 |
+
task: spatial-temporal grounding
|
| 65 |
+
family: stvg
|
| 66 |
+
quota: 9536
|
| 67 |
+
media_root: ../local_data/media/spatial_temporal
|
| 68 |
+
license: REPLACE_WITH_DATASET_LICENSE
|
| 69 |
+
url: REPLACE_WITH_DATASET_HOME_PAGE
|
| 70 |
+
|
| 71 |
+
- name: video_qa_train
|
| 72 |
+
input: ../local_data/annotations/video_qa.jsonl
|
| 73 |
+
task: video_qa_mc
|
| 74 |
+
family: video_qa
|
| 75 |
+
quota: 20288
|
| 76 |
+
media_root: ../local_data/media/video_qa
|
| 77 |
+
license: REPLACE_WITH_DATASET_LICENSE
|
| 78 |
+
url: REPLACE_WITH_DATASET_HOME_PAGE
|
| 79 |
+
|
| 80 |
+
- name: spatial_intelligence_train
|
| 81 |
+
input: ../local_data/annotations/spatial_intelligence.jsonl
|
| 82 |
+
task: spatial intelligence
|
| 83 |
+
family: spatial_intelligence
|
| 84 |
+
# Keep subtype labels such as object_counting and object_rel_direction;
|
| 85 |
+
# the reward adapter uses them to select the official scoring rule.
|
| 86 |
+
preserve_problem_type: true
|
| 87 |
+
quota: 17088
|
| 88 |
+
media_root: ../local_data/media/spatial_intelligence
|
| 89 |
+
license: REPLACE_WITH_DATASET_LICENSE
|
| 90 |
+
url: REPLACE_WITH_DATASET_HOME_PAGE
|
code/configs/grpo_4b.yaml
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Complete 4B GRPO baseline. Required paths are supplied by the CLI or environment.
|
| 2 |
+
data:
|
| 3 |
+
train_files: ${oc.env:ORARL_TRAIN_DATA}
|
| 4 |
+
val_files: ${oc.env:ORARL_VAL_DATA}
|
| 5 |
+
prompt_key: problem
|
| 6 |
+
answer_key: answer
|
| 7 |
+
image_key: images
|
| 8 |
+
video_key: videos
|
| 9 |
+
image_dir: ${oc.env:ORARL_MEDIA_ROOT,null}
|
| 10 |
+
dataloader_num_workers: ${oc.env:ORARL_DATALOADER_WORKERS,4}
|
| 11 |
+
video_max_pixels: ${oc.env:ORARL_VIDEO_MAX_PIXELS,786432}
|
| 12 |
+
video_min_pixels: ${oc.env:ORARL_VIDEO_MIN_PIXELS,4096}
|
| 13 |
+
video_max_frames: ${oc.env:ORARL_VIDEO_MAX_FRAMES,128}
|
| 14 |
+
video_fps: ${oc.env:ORARL_VIDEO_FPS,2}
|
| 15 |
+
video_total_pixels: ${oc.env:ORARL_VIDEO_TOTAL_PIXELS,8388608}
|
| 16 |
+
min_pixels: ${oc.env:ORARL_IMAGE_MIN_PIXELS,4096}
|
| 17 |
+
image_max_pixels: ${oc.env:ORARL_IMAGE_MAX_PIXELS,1048576}
|
| 18 |
+
use_preprocessed_videos: false
|
| 19 |
+
video_source_mode: ${oc.env:ORARL_VIDEO_SOURCE_MODE,realtime_only}
|
| 20 |
+
preprocessed_video_dir: ${oc.env:ORARL_PREPROCESSED_DIR,null}
|
| 21 |
+
inline_video_tensors: ${oc.env:ORARL_INLINE_VIDEO_TENSORS,false}
|
| 22 |
+
max_prompt_length: ${oc.env:ORARL_MAX_PROMPT_LENGTH,16384}
|
| 23 |
+
max_response_length: ${oc.env:ORARL_MAX_RESPONSE_LENGTH,4096}
|
| 24 |
+
rollout_batch_size: ${oc.env:ORARL_ROLLOUT_BATCH_SIZE,64}
|
| 25 |
+
mini_rollout_batch_size: null
|
| 26 |
+
val_batch_size: ${oc.env:ORARL_VAL_BATCH_SIZE,16}
|
| 27 |
+
format_prompt: null
|
| 28 |
+
override_chat_template: null
|
| 29 |
+
enable_thinking: ${oc.env:ORARL_ENABLE_THINKING,false}
|
| 30 |
+
response_prefix: ""
|
| 31 |
+
shuffle: true
|
| 32 |
+
seed: ${oc.env:ORARL_SEED,42}
|
| 33 |
+
group_by_task: true
|
| 34 |
+
group_by_task_key: problem_type
|
| 35 |
+
filter_overlong_prompts: false
|
| 36 |
+
|
| 37 |
+
algorithm:
|
| 38 |
+
name: grpo
|
| 39 |
+
adv_estimator: grpo
|
| 40 |
+
scale_rewards: true
|
| 41 |
+
disable_kl: true
|
| 42 |
+
use_kl_loss: false
|
| 43 |
+
kl_penalty: low_var_kl
|
| 44 |
+
kl_coef: 0.0
|
| 45 |
+
|
| 46 |
+
worker:
|
| 47 |
+
actor:
|
| 48 |
+
global_batch_size: ${oc.env:ORARL_GLOBAL_BATCH_SIZE,64}
|
| 49 |
+
micro_batch_size_per_device_for_update: ${oc.env:ORARL_UPDATE_MICRO_BATCH,2}
|
| 50 |
+
micro_batch_size_per_device_for_experience: ${oc.env:ORARL_EXPERIENCE_MICRO_BATCH,1}
|
| 51 |
+
ppo_epochs: 1
|
| 52 |
+
max_grad_norm: 1.0
|
| 53 |
+
clip_ratio_low: 0.2
|
| 54 |
+
clip_ratio_high: 0.2
|
| 55 |
+
loss_avg_mode: token
|
| 56 |
+
padding_free: true
|
| 57 |
+
dynamic_batching: true
|
| 58 |
+
max_token_len_per_gpu: ${oc.env:ORARL_MAX_TOKEN_LEN_PER_GPU,24576}
|
| 59 |
+
ulysses_size: 1
|
| 60 |
+
use_torch_compile: false
|
| 61 |
+
model:
|
| 62 |
+
model_path: ${oc.env:ORARL_MODEL_PATH}
|
| 63 |
+
tokenizer_path: ${oc.env:ORARL_TOKENIZER_PATH,${oc.env:ORARL_MODEL_PATH}}
|
| 64 |
+
enable_gradient_checkpointing: true
|
| 65 |
+
trust_remote_code: false
|
| 66 |
+
freeze_vision_tower: true
|
| 67 |
+
train_vision_merger: false
|
| 68 |
+
optim:
|
| 69 |
+
lr: ${oc.env:ORARL_LEARNING_RATE,2.0e-6}
|
| 70 |
+
weight_decay: 0.0
|
| 71 |
+
strategy: adamw
|
| 72 |
+
lr_warmup_ratio: 0.0
|
| 73 |
+
lr_scheduler_type: constant
|
| 74 |
+
fsdp:
|
| 75 |
+
torch_dtype: bf16
|
| 76 |
+
enable_full_shard: true
|
| 77 |
+
enable_cpu_offload: false
|
| 78 |
+
enable_rank0_init: true
|
| 79 |
+
use_orig_params: false
|
| 80 |
+
offload:
|
| 81 |
+
offload_params: false
|
| 82 |
+
offload_optimizer: false
|
| 83 |
+
|
| 84 |
+
rollout:
|
| 85 |
+
name: ${oc.env:ORARL_ROLLOUT_BACKEND,vllm}
|
| 86 |
+
n: 8
|
| 87 |
+
seed: ${oc.env:ORARL_SEED,42}
|
| 88 |
+
temperature: 1.0
|
| 89 |
+
top_p: 0.85
|
| 90 |
+
limit_images: 10
|
| 91 |
+
gpu_memory_utilization: 0.5
|
| 92 |
+
enforce_eager: false
|
| 93 |
+
enable_chunked_prefill: true
|
| 94 |
+
tensor_parallel_size: 1
|
| 95 |
+
max_num_batched_tokens: 32768
|
| 96 |
+
disable_tqdm: true
|
| 97 |
+
calculate_log_probs: false
|
| 98 |
+
collect_seq_logprob_for_filter: false
|
| 99 |
+
val_override_config:
|
| 100 |
+
temperature: 0.7
|
| 101 |
+
top_p: 0.95
|
| 102 |
+
n: 1
|
| 103 |
+
|
| 104 |
+
ref:
|
| 105 |
+
fsdp:
|
| 106 |
+
torch_dtype: bf16
|
| 107 |
+
enable_full_shard: true
|
| 108 |
+
enable_cpu_offload: false
|
| 109 |
+
enable_rank0_init: true
|
| 110 |
+
offload:
|
| 111 |
+
offload_params: false
|
| 112 |
+
|
| 113 |
+
reward:
|
| 114 |
+
reward_function: orarl.rewards:compute_score
|
| 115 |
+
reward_function_kwargs: {}
|
| 116 |
+
|
| 117 |
+
trainer:
|
| 118 |
+
total_epochs: 1
|
| 119 |
+
max_steps: null
|
| 120 |
+
project_name: OraRL
|
| 121 |
+
experiment_name: grpo-4b
|
| 122 |
+
logger: ["console"]
|
| 123 |
+
nnodes: 1
|
| 124 |
+
n_gpus_per_node: 8
|
| 125 |
+
val_freq: -1
|
| 126 |
+
val_before_train: false
|
| 127 |
+
val_only: false
|
| 128 |
+
val_generations_to_log: 0
|
| 129 |
+
save_freq: 50
|
| 130 |
+
save_limit: 3
|
| 131 |
+
save_model_only: false
|
| 132 |
+
keep_optim_only_latest: true
|
| 133 |
+
save_checkpoint_path: ${oc.env:ORARL_OUTPUT_DIR}
|
| 134 |
+
load_checkpoint_path: null
|
| 135 |
+
find_last_checkpoint: false
|
| 136 |
+
keep_best_train_ckpt: false
|
| 137 |
+
best_train_metric_key: reward/overall
|
| 138 |
+
best_train_smooth_window: 5
|
| 139 |
+
best_train_min_step: 10
|
code/configs/grpo_9b.yaml
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Complete 9B GRPO baseline. Required paths are supplied by the CLI or environment.
|
| 2 |
+
data:
|
| 3 |
+
train_files: ${oc.env:ORARL_TRAIN_DATA}
|
| 4 |
+
val_files: ${oc.env:ORARL_VAL_DATA}
|
| 5 |
+
prompt_key: problem
|
| 6 |
+
answer_key: answer
|
| 7 |
+
image_key: images
|
| 8 |
+
video_key: videos
|
| 9 |
+
image_dir: ${oc.env:ORARL_MEDIA_ROOT,null}
|
| 10 |
+
dataloader_num_workers: ${oc.env:ORARL_DATALOADER_WORKERS,4}
|
| 11 |
+
video_max_pixels: ${oc.env:ORARL_VIDEO_MAX_PIXELS,786432}
|
| 12 |
+
video_min_pixels: ${oc.env:ORARL_VIDEO_MIN_PIXELS,4096}
|
| 13 |
+
video_max_frames: ${oc.env:ORARL_VIDEO_MAX_FRAMES,128}
|
| 14 |
+
video_fps: ${oc.env:ORARL_VIDEO_FPS,2}
|
| 15 |
+
video_total_pixels: ${oc.env:ORARL_VIDEO_TOTAL_PIXELS,8388608}
|
| 16 |
+
min_pixels: ${oc.env:ORARL_IMAGE_MIN_PIXELS,4096}
|
| 17 |
+
image_max_pixels: ${oc.env:ORARL_IMAGE_MAX_PIXELS,1048576}
|
| 18 |
+
use_preprocessed_videos: false
|
| 19 |
+
video_source_mode: ${oc.env:ORARL_VIDEO_SOURCE_MODE,realtime_only}
|
| 20 |
+
preprocessed_video_dir: ${oc.env:ORARL_PREPROCESSED_DIR,null}
|
| 21 |
+
inline_video_tensors: ${oc.env:ORARL_INLINE_VIDEO_TENSORS,false}
|
| 22 |
+
max_prompt_length: ${oc.env:ORARL_MAX_PROMPT_LENGTH,24576}
|
| 23 |
+
max_response_length: ${oc.env:ORARL_MAX_RESPONSE_LENGTH,4096}
|
| 24 |
+
rollout_batch_size: ${oc.env:ORARL_ROLLOUT_BATCH_SIZE,64}
|
| 25 |
+
mini_rollout_batch_size: null
|
| 26 |
+
val_batch_size: ${oc.env:ORARL_VAL_BATCH_SIZE,16}
|
| 27 |
+
format_prompt: null
|
| 28 |
+
override_chat_template: null
|
| 29 |
+
enable_thinking: ${oc.env:ORARL_ENABLE_THINKING,false}
|
| 30 |
+
response_prefix: ""
|
| 31 |
+
shuffle: true
|
| 32 |
+
seed: ${oc.env:ORARL_SEED,42}
|
| 33 |
+
group_by_task: true
|
| 34 |
+
group_by_task_key: problem_type
|
| 35 |
+
filter_overlong_prompts: false
|
| 36 |
+
|
| 37 |
+
algorithm:
|
| 38 |
+
name: grpo
|
| 39 |
+
adv_estimator: grpo
|
| 40 |
+
scale_rewards: true
|
| 41 |
+
disable_kl: true
|
| 42 |
+
use_kl_loss: false
|
| 43 |
+
kl_penalty: low_var_kl
|
| 44 |
+
kl_coef: 0.0
|
| 45 |
+
|
| 46 |
+
worker:
|
| 47 |
+
actor:
|
| 48 |
+
global_batch_size: ${oc.env:ORARL_GLOBAL_BATCH_SIZE,64}
|
| 49 |
+
micro_batch_size_per_device_for_update: ${oc.env:ORARL_UPDATE_MICRO_BATCH,1}
|
| 50 |
+
micro_batch_size_per_device_for_experience: ${oc.env:ORARL_EXPERIENCE_MICRO_BATCH,1}
|
| 51 |
+
ppo_epochs: 1
|
| 52 |
+
max_grad_norm: 1.0
|
| 53 |
+
clip_ratio_low: 0.2
|
| 54 |
+
clip_ratio_high: 0.2
|
| 55 |
+
loss_avg_mode: token
|
| 56 |
+
padding_free: true
|
| 57 |
+
dynamic_batching: true
|
| 58 |
+
max_token_len_per_gpu: ${oc.env:ORARL_MAX_TOKEN_LEN_PER_GPU,32768}
|
| 59 |
+
ulysses_size: 1
|
| 60 |
+
use_torch_compile: false
|
| 61 |
+
model:
|
| 62 |
+
model_path: ${oc.env:ORARL_MODEL_PATH}
|
| 63 |
+
tokenizer_path: ${oc.env:ORARL_TOKENIZER_PATH,${oc.env:ORARL_MODEL_PATH}}
|
| 64 |
+
enable_gradient_checkpointing: true
|
| 65 |
+
trust_remote_code: false
|
| 66 |
+
freeze_vision_tower: true
|
| 67 |
+
train_vision_merger: false
|
| 68 |
+
optim:
|
| 69 |
+
lr: ${oc.env:ORARL_LEARNING_RATE,1.0e-6}
|
| 70 |
+
weight_decay: 0.0
|
| 71 |
+
strategy: adamw
|
| 72 |
+
lr_warmup_ratio: 0.0
|
| 73 |
+
lr_scheduler_type: constant
|
| 74 |
+
fsdp:
|
| 75 |
+
torch_dtype: bf16
|
| 76 |
+
enable_full_shard: true
|
| 77 |
+
enable_cpu_offload: false
|
| 78 |
+
enable_rank0_init: true
|
| 79 |
+
use_orig_params: false
|
| 80 |
+
offload:
|
| 81 |
+
offload_params: false
|
| 82 |
+
offload_optimizer: false
|
| 83 |
+
|
| 84 |
+
rollout:
|
| 85 |
+
name: ${oc.env:ORARL_ROLLOUT_BACKEND,vllm}
|
| 86 |
+
n: 8
|
| 87 |
+
seed: ${oc.env:ORARL_SEED,42}
|
| 88 |
+
temperature: 1.0
|
| 89 |
+
top_p: 0.85
|
| 90 |
+
limit_images: 10
|
| 91 |
+
gpu_memory_utilization: 0.45
|
| 92 |
+
enforce_eager: false
|
| 93 |
+
enable_chunked_prefill: true
|
| 94 |
+
tensor_parallel_size: 1
|
| 95 |
+
max_num_batched_tokens: 32768
|
| 96 |
+
disable_tqdm: true
|
| 97 |
+
calculate_log_probs: false
|
| 98 |
+
collect_seq_logprob_for_filter: false
|
| 99 |
+
val_override_config:
|
| 100 |
+
temperature: 0.7
|
| 101 |
+
top_p: 0.95
|
| 102 |
+
n: 1
|
| 103 |
+
|
| 104 |
+
ref:
|
| 105 |
+
fsdp:
|
| 106 |
+
torch_dtype: bf16
|
| 107 |
+
enable_full_shard: true
|
| 108 |
+
enable_cpu_offload: false
|
| 109 |
+
enable_rank0_init: true
|
| 110 |
+
offload:
|
| 111 |
+
offload_params: false
|
| 112 |
+
|
| 113 |
+
reward:
|
| 114 |
+
reward_function: orarl.rewards:compute_score
|
| 115 |
+
reward_function_kwargs: {}
|
| 116 |
+
|
| 117 |
+
trainer:
|
| 118 |
+
total_epochs: 1
|
| 119 |
+
max_steps: null
|
| 120 |
+
project_name: OraRL
|
| 121 |
+
experiment_name: grpo-9b
|
| 122 |
+
logger: ["console"]
|
| 123 |
+
nnodes: 1
|
| 124 |
+
n_gpus_per_node: 8
|
| 125 |
+
val_freq: -1
|
| 126 |
+
val_before_train: false
|
| 127 |
+
val_only: false
|
| 128 |
+
val_generations_to_log: 0
|
| 129 |
+
save_freq: 50
|
| 130 |
+
save_limit: 3
|
| 131 |
+
save_model_only: false
|
| 132 |
+
keep_optim_only_latest: true
|
| 133 |
+
save_checkpoint_path: ${oc.env:ORARL_OUTPUT_DIR}
|
| 134 |
+
load_checkpoint_path: null
|
| 135 |
+
find_last_checkpoint: false
|
| 136 |
+
keep_best_train_ckpt: false
|
| 137 |
+
best_train_metric_key: reward/overall
|
| 138 |
+
best_train_smooth_window: 5
|
| 139 |
+
best_train_min_step: 10
|
code/configs/orarl_4b.yaml
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Complete 4B OraRL recipe. Required paths are supplied by the CLI or environment.
|
| 2 |
+
data:
|
| 3 |
+
train_files: ${oc.env:ORARL_TRAIN_DATA}
|
| 4 |
+
val_files: ${oc.env:ORARL_VAL_DATA}
|
| 5 |
+
prompt_key: problem
|
| 6 |
+
answer_key: answer
|
| 7 |
+
image_key: images
|
| 8 |
+
video_key: videos
|
| 9 |
+
image_dir: ${oc.env:ORARL_MEDIA_ROOT,null}
|
| 10 |
+
dataloader_num_workers: ${oc.env:ORARL_DATALOADER_WORKERS,4}
|
| 11 |
+
video_max_pixels: ${oc.env:ORARL_VIDEO_MAX_PIXELS,786432}
|
| 12 |
+
video_min_pixels: ${oc.env:ORARL_VIDEO_MIN_PIXELS,4096}
|
| 13 |
+
video_max_frames: ${oc.env:ORARL_VIDEO_MAX_FRAMES,128}
|
| 14 |
+
video_fps: ${oc.env:ORARL_VIDEO_FPS,2}
|
| 15 |
+
video_total_pixels: ${oc.env:ORARL_VIDEO_TOTAL_PIXELS,8388608}
|
| 16 |
+
min_pixels: ${oc.env:ORARL_IMAGE_MIN_PIXELS,4096}
|
| 17 |
+
image_max_pixels: ${oc.env:ORARL_IMAGE_MAX_PIXELS,1048576}
|
| 18 |
+
use_preprocessed_videos: false
|
| 19 |
+
video_source_mode: ${oc.env:ORARL_VIDEO_SOURCE_MODE,realtime_only}
|
| 20 |
+
preprocessed_video_dir: ${oc.env:ORARL_PREPROCESSED_DIR,null}
|
| 21 |
+
inline_video_tensors: ${oc.env:ORARL_INLINE_VIDEO_TENSORS,false}
|
| 22 |
+
max_prompt_length: ${oc.env:ORARL_MAX_PROMPT_LENGTH,16384}
|
| 23 |
+
max_response_length: ${oc.env:ORARL_MAX_RESPONSE_LENGTH,4096}
|
| 24 |
+
rollout_batch_size: ${oc.env:ORARL_ROLLOUT_BATCH_SIZE,64}
|
| 25 |
+
mini_rollout_batch_size: null
|
| 26 |
+
val_batch_size: ${oc.env:ORARL_VAL_BATCH_SIZE,16}
|
| 27 |
+
format_prompt: null
|
| 28 |
+
override_chat_template: null
|
| 29 |
+
enable_thinking: ${oc.env:ORARL_ENABLE_THINKING,false}
|
| 30 |
+
response_prefix: ""
|
| 31 |
+
shuffle: true
|
| 32 |
+
seed: ${oc.env:ORARL_SEED,42}
|
| 33 |
+
group_by_task: true
|
| 34 |
+
group_by_task_key: problem_type
|
| 35 |
+
filter_overlong_prompts: false
|
| 36 |
+
|
| 37 |
+
algorithm:
|
| 38 |
+
name: orarl
|
| 39 |
+
adv_estimator: grpo
|
| 40 |
+
scale_rewards: false
|
| 41 |
+
oracle_injection: true
|
| 42 |
+
oracle_injection_mode: append
|
| 43 |
+
oracle_builder: orarl.rewards:build_oracle_response_from_ground_truth
|
| 44 |
+
oracle_log_exclude: true
|
| 45 |
+
directional_gain: true
|
| 46 |
+
directional_gain_gamma: 0.25
|
| 47 |
+
directional_gain_positive_only: true
|
| 48 |
+
directional_gain_recenter: true
|
| 49 |
+
detached_oracle_advantage: true
|
| 50 |
+
detached_oracle_advantage_scale: 2.0
|
| 51 |
+
detached_oracle_use_directional_gain: false
|
| 52 |
+
detached_oracle_match_best_ratio: 1.2
|
| 53 |
+
detached_oracle_match_best_min: 0.05
|
| 54 |
+
detached_oracle_match_best_max: 1.0
|
| 55 |
+
oracle_reward_gate_beta: 2.0
|
| 56 |
+
selection_prune_ratio: 0.5
|
| 57 |
+
selection_keep_oracle: true
|
| 58 |
+
selection_positive_quota: 1
|
| 59 |
+
selection_negative_quota: 2
|
| 60 |
+
selection_strict_sign_balance: true
|
| 61 |
+
post_selection_recenter: true
|
| 62 |
+
post_selection_rms_match: true
|
| 63 |
+
post_selection_rms_min_scale: 0.25
|
| 64 |
+
disable_kl: true
|
| 65 |
+
use_kl_loss: false
|
| 66 |
+
kl_penalty: low_var_kl
|
| 67 |
+
kl_coef: 0.0
|
| 68 |
+
|
| 69 |
+
worker:
|
| 70 |
+
actor:
|
| 71 |
+
global_batch_size: ${oc.env:ORARL_GLOBAL_BATCH_SIZE,64}
|
| 72 |
+
micro_batch_size_per_device_for_update: ${oc.env:ORARL_UPDATE_MICRO_BATCH,2}
|
| 73 |
+
micro_batch_size_per_device_for_experience: ${oc.env:ORARL_EXPERIENCE_MICRO_BATCH,1}
|
| 74 |
+
ppo_epochs: 1
|
| 75 |
+
max_grad_norm: 1.0
|
| 76 |
+
clip_ratio_low: 0.2
|
| 77 |
+
clip_ratio_high: 0.2
|
| 78 |
+
loss_avg_mode: token
|
| 79 |
+
padding_free: true
|
| 80 |
+
dynamic_batching: true
|
| 81 |
+
max_token_len_per_gpu: ${oc.env:ORARL_MAX_TOKEN_LEN_PER_GPU,24576}
|
| 82 |
+
ulysses_size: 1
|
| 83 |
+
use_torch_compile: false
|
| 84 |
+
model:
|
| 85 |
+
model_path: ${oc.env:ORARL_MODEL_PATH}
|
| 86 |
+
tokenizer_path: ${oc.env:ORARL_TOKENIZER_PATH,${oc.env:ORARL_MODEL_PATH}}
|
| 87 |
+
enable_gradient_checkpointing: true
|
| 88 |
+
trust_remote_code: false
|
| 89 |
+
freeze_vision_tower: true
|
| 90 |
+
train_vision_merger: false
|
| 91 |
+
optim:
|
| 92 |
+
lr: ${oc.env:ORARL_LEARNING_RATE,2.0e-6}
|
| 93 |
+
weight_decay: 0.0
|
| 94 |
+
strategy: adamw
|
| 95 |
+
lr_warmup_ratio: 0.0
|
| 96 |
+
lr_scheduler_type: constant
|
| 97 |
+
fsdp:
|
| 98 |
+
torch_dtype: bf16
|
| 99 |
+
enable_full_shard: true
|
| 100 |
+
enable_cpu_offload: false
|
| 101 |
+
enable_rank0_init: true
|
| 102 |
+
use_orig_params: false
|
| 103 |
+
offload:
|
| 104 |
+
offload_params: false
|
| 105 |
+
offload_optimizer: false
|
| 106 |
+
|
| 107 |
+
rollout:
|
| 108 |
+
name: ${oc.env:ORARL_ROLLOUT_BACKEND,vllm}
|
| 109 |
+
n: 8
|
| 110 |
+
seed: ${oc.env:ORARL_SEED,42}
|
| 111 |
+
temperature: 1.0
|
| 112 |
+
top_p: 0.85
|
| 113 |
+
limit_images: 10
|
| 114 |
+
gpu_memory_utilization: 0.5
|
| 115 |
+
enforce_eager: false
|
| 116 |
+
enable_chunked_prefill: true
|
| 117 |
+
tensor_parallel_size: 1
|
| 118 |
+
max_num_batched_tokens: 32768
|
| 119 |
+
disable_tqdm: true
|
| 120 |
+
calculate_log_probs: false
|
| 121 |
+
collect_seq_logprob_for_filter: false
|
| 122 |
+
val_override_config:
|
| 123 |
+
temperature: 0.7
|
| 124 |
+
top_p: 0.95
|
| 125 |
+
n: 1
|
| 126 |
+
|
| 127 |
+
ref:
|
| 128 |
+
fsdp:
|
| 129 |
+
torch_dtype: bf16
|
| 130 |
+
enable_full_shard: true
|
| 131 |
+
enable_cpu_offload: false
|
| 132 |
+
enable_rank0_init: true
|
| 133 |
+
offload:
|
| 134 |
+
offload_params: false
|
| 135 |
+
|
| 136 |
+
reward:
|
| 137 |
+
reward_function: orarl.rewards:compute_score
|
| 138 |
+
reward_function_kwargs: {}
|
| 139 |
+
|
| 140 |
+
trainer:
|
| 141 |
+
total_epochs: 1
|
| 142 |
+
max_steps: null
|
| 143 |
+
project_name: OraRL
|
| 144 |
+
experiment_name: orarl-4b
|
| 145 |
+
logger: ["console"]
|
| 146 |
+
nnodes: 1
|
| 147 |
+
n_gpus_per_node: 8
|
| 148 |
+
val_freq: -1
|
| 149 |
+
val_before_train: false
|
| 150 |
+
val_only: false
|
| 151 |
+
val_generations_to_log: 0
|
| 152 |
+
save_freq: 50
|
| 153 |
+
save_limit: 3
|
| 154 |
+
save_model_only: false
|
| 155 |
+
keep_optim_only_latest: true
|
| 156 |
+
save_checkpoint_path: ${oc.env:ORARL_OUTPUT_DIR}
|
| 157 |
+
load_checkpoint_path: null
|
| 158 |
+
find_last_checkpoint: false
|
| 159 |
+
keep_best_train_ckpt: false
|
| 160 |
+
best_train_metric_key: reward/overall
|
| 161 |
+
best_train_smooth_window: 5
|
| 162 |
+
best_train_min_step: 10
|
code/configs/orarl_9b.yaml
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Complete 9B OraRL recipe. Required paths are supplied by the CLI or environment.
|
| 2 |
+
data:
|
| 3 |
+
train_files: ${oc.env:ORARL_TRAIN_DATA}
|
| 4 |
+
val_files: ${oc.env:ORARL_VAL_DATA}
|
| 5 |
+
prompt_key: problem
|
| 6 |
+
answer_key: answer
|
| 7 |
+
image_key: images
|
| 8 |
+
video_key: videos
|
| 9 |
+
image_dir: ${oc.env:ORARL_MEDIA_ROOT,null}
|
| 10 |
+
dataloader_num_workers: ${oc.env:ORARL_DATALOADER_WORKERS,4}
|
| 11 |
+
video_max_pixels: ${oc.env:ORARL_VIDEO_MAX_PIXELS,786432}
|
| 12 |
+
video_min_pixels: ${oc.env:ORARL_VIDEO_MIN_PIXELS,4096}
|
| 13 |
+
video_max_frames: ${oc.env:ORARL_VIDEO_MAX_FRAMES,128}
|
| 14 |
+
video_fps: ${oc.env:ORARL_VIDEO_FPS,2}
|
| 15 |
+
video_total_pixels: ${oc.env:ORARL_VIDEO_TOTAL_PIXELS,8388608}
|
| 16 |
+
min_pixels: ${oc.env:ORARL_IMAGE_MIN_PIXELS,4096}
|
| 17 |
+
image_max_pixels: ${oc.env:ORARL_IMAGE_MAX_PIXELS,1048576}
|
| 18 |
+
use_preprocessed_videos: false
|
| 19 |
+
video_source_mode: ${oc.env:ORARL_VIDEO_SOURCE_MODE,realtime_only}
|
| 20 |
+
preprocessed_video_dir: ${oc.env:ORARL_PREPROCESSED_DIR,null}
|
| 21 |
+
inline_video_tensors: ${oc.env:ORARL_INLINE_VIDEO_TENSORS,false}
|
| 22 |
+
max_prompt_length: ${oc.env:ORARL_MAX_PROMPT_LENGTH,24576}
|
| 23 |
+
max_response_length: ${oc.env:ORARL_MAX_RESPONSE_LENGTH,4096}
|
| 24 |
+
rollout_batch_size: ${oc.env:ORARL_ROLLOUT_BATCH_SIZE,64}
|
| 25 |
+
mini_rollout_batch_size: null
|
| 26 |
+
val_batch_size: ${oc.env:ORARL_VAL_BATCH_SIZE,16}
|
| 27 |
+
format_prompt: null
|
| 28 |
+
override_chat_template: null
|
| 29 |
+
enable_thinking: ${oc.env:ORARL_ENABLE_THINKING,false}
|
| 30 |
+
response_prefix: ""
|
| 31 |
+
shuffle: true
|
| 32 |
+
seed: ${oc.env:ORARL_SEED,42}
|
| 33 |
+
group_by_task: true
|
| 34 |
+
group_by_task_key: problem_type
|
| 35 |
+
filter_overlong_prompts: false
|
| 36 |
+
|
| 37 |
+
algorithm:
|
| 38 |
+
name: orarl
|
| 39 |
+
adv_estimator: grpo
|
| 40 |
+
scale_rewards: false
|
| 41 |
+
oracle_injection: true
|
| 42 |
+
oracle_injection_mode: append
|
| 43 |
+
oracle_builder: orarl.rewards:build_oracle_response_from_ground_truth
|
| 44 |
+
oracle_log_exclude: true
|
| 45 |
+
directional_gain: true
|
| 46 |
+
directional_gain_gamma: 0.25
|
| 47 |
+
directional_gain_positive_only: true
|
| 48 |
+
directional_gain_recenter: true
|
| 49 |
+
detached_oracle_advantage: true
|
| 50 |
+
detached_oracle_advantage_scale: 2.0
|
| 51 |
+
detached_oracle_use_directional_gain: false
|
| 52 |
+
detached_oracle_match_best_ratio: 1.2
|
| 53 |
+
detached_oracle_match_best_min: 0.05
|
| 54 |
+
detached_oracle_match_best_max: 1.0
|
| 55 |
+
oracle_reward_gate_beta: 2.0
|
| 56 |
+
selection_prune_ratio: 0.5
|
| 57 |
+
selection_keep_oracle: true
|
| 58 |
+
selection_positive_quota: 1
|
| 59 |
+
selection_negative_quota: 2
|
| 60 |
+
selection_strict_sign_balance: true
|
| 61 |
+
post_selection_recenter: true
|
| 62 |
+
post_selection_rms_match: true
|
| 63 |
+
post_selection_rms_min_scale: 0.25
|
| 64 |
+
disable_kl: true
|
| 65 |
+
use_kl_loss: false
|
| 66 |
+
kl_penalty: low_var_kl
|
| 67 |
+
kl_coef: 0.0
|
| 68 |
+
|
| 69 |
+
worker:
|
| 70 |
+
actor:
|
| 71 |
+
global_batch_size: ${oc.env:ORARL_GLOBAL_BATCH_SIZE,64}
|
| 72 |
+
micro_batch_size_per_device_for_update: ${oc.env:ORARL_UPDATE_MICRO_BATCH,1}
|
| 73 |
+
micro_batch_size_per_device_for_experience: ${oc.env:ORARL_EXPERIENCE_MICRO_BATCH,1}
|
| 74 |
+
ppo_epochs: 1
|
| 75 |
+
max_grad_norm: 1.0
|
| 76 |
+
clip_ratio_low: 0.2
|
| 77 |
+
clip_ratio_high: 0.2
|
| 78 |
+
loss_avg_mode: token
|
| 79 |
+
padding_free: true
|
| 80 |
+
dynamic_batching: true
|
| 81 |
+
max_token_len_per_gpu: ${oc.env:ORARL_MAX_TOKEN_LEN_PER_GPU,32768}
|
| 82 |
+
ulysses_size: 1
|
| 83 |
+
use_torch_compile: false
|
| 84 |
+
model:
|
| 85 |
+
model_path: ${oc.env:ORARL_MODEL_PATH}
|
| 86 |
+
tokenizer_path: ${oc.env:ORARL_TOKENIZER_PATH,${oc.env:ORARL_MODEL_PATH}}
|
| 87 |
+
enable_gradient_checkpointing: true
|
| 88 |
+
trust_remote_code: false
|
| 89 |
+
freeze_vision_tower: true
|
| 90 |
+
train_vision_merger: false
|
| 91 |
+
optim:
|
| 92 |
+
lr: ${oc.env:ORARL_LEARNING_RATE,1.0e-6}
|
| 93 |
+
weight_decay: 0.0
|
| 94 |
+
strategy: adamw
|
| 95 |
+
lr_warmup_ratio: 0.0
|
| 96 |
+
lr_scheduler_type: constant
|
| 97 |
+
fsdp:
|
| 98 |
+
torch_dtype: bf16
|
| 99 |
+
enable_full_shard: true
|
| 100 |
+
enable_cpu_offload: false
|
| 101 |
+
enable_rank0_init: true
|
| 102 |
+
use_orig_params: false
|
| 103 |
+
offload:
|
| 104 |
+
offload_params: false
|
| 105 |
+
offload_optimizer: false
|
| 106 |
+
|
| 107 |
+
rollout:
|
| 108 |
+
name: ${oc.env:ORARL_ROLLOUT_BACKEND,vllm}
|
| 109 |
+
n: 8
|
| 110 |
+
seed: ${oc.env:ORARL_SEED,42}
|
| 111 |
+
temperature: 1.0
|
| 112 |
+
top_p: 0.85
|
| 113 |
+
limit_images: 10
|
| 114 |
+
gpu_memory_utilization: 0.45
|
| 115 |
+
enforce_eager: false
|
| 116 |
+
enable_chunked_prefill: true
|
| 117 |
+
tensor_parallel_size: 1
|
| 118 |
+
max_num_batched_tokens: 32768
|
| 119 |
+
disable_tqdm: true
|
| 120 |
+
calculate_log_probs: false
|
| 121 |
+
collect_seq_logprob_for_filter: false
|
| 122 |
+
val_override_config:
|
| 123 |
+
temperature: 0.7
|
| 124 |
+
top_p: 0.95
|
| 125 |
+
n: 1
|
| 126 |
+
|
| 127 |
+
ref:
|
| 128 |
+
fsdp:
|
| 129 |
+
torch_dtype: bf16
|
| 130 |
+
enable_full_shard: true
|
| 131 |
+
enable_cpu_offload: false
|
| 132 |
+
enable_rank0_init: true
|
| 133 |
+
offload:
|
| 134 |
+
offload_params: false
|
| 135 |
+
|
| 136 |
+
reward:
|
| 137 |
+
reward_function: orarl.rewards:compute_score
|
| 138 |
+
reward_function_kwargs: {}
|
| 139 |
+
|
| 140 |
+
trainer:
|
| 141 |
+
total_epochs: 1
|
| 142 |
+
max_steps: null
|
| 143 |
+
project_name: OraRL
|
| 144 |
+
experiment_name: orarl-9b
|
| 145 |
+
logger: ["console"]
|
| 146 |
+
nnodes: 1
|
| 147 |
+
n_gpus_per_node: 8
|
| 148 |
+
val_freq: -1
|
| 149 |
+
val_before_train: false
|
| 150 |
+
val_only: false
|
| 151 |
+
val_generations_to_log: 0
|
| 152 |
+
save_freq: 50
|
| 153 |
+
save_limit: 3
|
| 154 |
+
save_model_only: false
|
| 155 |
+
keep_optim_only_latest: true
|
| 156 |
+
save_checkpoint_path: ${oc.env:ORARL_OUTPUT_DIR}
|
| 157 |
+
load_checkpoint_path: null
|
| 158 |
+
find_last_checkpoint: false
|
| 159 |
+
keep_best_train_ckpt: false
|
| 160 |
+
best_train_metric_key: reward/overall
|
| 161 |
+
best_train_smooth_window: 5
|
| 162 |
+
best_train_min_step: 10
|
code/data/eval/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Evaluation profiles
|
| 2 |
+
|
| 3 |
+
This directory contains only the small, reviewable `datasets.jsonl` manifest
|
| 4 |
+
that pins the paper-suite splits, prompts, preprocessing settings, parsers, and
|
| 5 |
+
metrics.
|
| 6 |
+
|
| 7 |
+
Evaluation annotations and raw media are hosted separately at
|
| 8 |
+
[`OraRL/OraRL-Data`](https://huggingface.co/datasets/OraRL/OraRL-Data).
|
| 9 |
+
Model checkpoints are hosted under the
|
| 10 |
+
[`OraRL`](https://huggingface.co/OraRL) organization. They are intentionally not
|
| 11 |
+
duplicated in this Git repository.
|
| 12 |
+
|
| 13 |
+
Use the Hugging Face dataset ID or a complete local snapshot when evaluating:
|
| 14 |
+
|
| 15 |
+
```bash
|
| 16 |
+
orarl-eval \
|
| 17 |
+
--model /path/to/Video-ORA-9B \
|
| 18 |
+
--tasks paper \
|
| 19 |
+
--dataset OraRL/OraRL-Data \
|
| 20 |
+
--summary ./evaluation.json \
|
| 21 |
+
--run
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
The manifest in this directory is release metadata, not a standalone dataset
|
| 25 |
+
root; it does not include `assets.jsonl` or media payloads.
|
code/data/eval/datasets.jsonl
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{"annotation_path":"annotations/video_qa/longvideobench/val.jsonl","artifact_paths":[],"benchmark":"longvideobench","evaluation":{"metric_profile":"accuracy","parser_profile":"multiple_choice","prompt_profile":"longvideobench_default"},"expected_count":1337,"family":"video_qa","legacy_environment":{"LONGVIDEOBENCH_BATCH_SIZE":1,"LONGVIDEOBENCH_ENABLE_THINKING":false,"LONGVIDEOBENCH_FPS":2,"LONGVIDEOBENCH_GPU_MEMORY_UTILIZATION":0.9,"LONGVIDEOBENCH_MAX_FRAMES":384,"LONGVIDEOBENCH_MAX_MODEL_LEN":65536,"LONGVIDEOBENCH_MAX_NEW_TOKENS":128,"LONGVIDEOBENCH_MAX_NUM_BATCHED_TOKENS":65536,"LONGVIDEOBENCH_MIN_P":0.0,"LONGVIDEOBENCH_PRESENCE_PENALTY":0.0,"LONGVIDEOBENCH_PROMPT_MODE":"default","LONGVIDEOBENCH_TEMPERATURE":0.0,"LONGVIDEOBENCH_TOP_K":-1,"LONGVIDEOBENCH_TOP_P":1.0,"LONGVIDEOBENCH_VIDEO_MAX_PIXELS":262144,"LONGVIDEOBENCH_VIDEO_MIN_PIXELS":4096,"LONGVIDEOBENCH_VIDEO_TOTAL_PIXELS":0},"license":"See the upstream benchmark terms","media_paths":["media/video_qa/longvideobench/subtitles","media/video_qa/longvideobench/subtitles/val","media/video_qa/longvideobench/videos","media/video_qa/longvideobench/videos/val"],"preprocessing":{"batch_size":1,"enable_thinking":false,"fps":2,"gpu_memory_utilization":0.9,"max_frames":384,"max_model_len":65536,"max_new_tokens":128,"max_num_batched_tokens":65536,"min_p":0.0,"presence_penalty":0.0,"prompt_mode":"default","temperature":0.0,"top_k":-1,"top_p":1.0,"video_max_pixels":262144,"video_min_pixels":4096,"video_total_pixels":0},"redistribution_authorized":true,"schema_version":1,"source_url":"https://github.com/longvideobench/LongVideoBench","split":"val","task":"longvideobench"}
|
| 2 |
+
{"annotation_path":"annotations/spatial_intelligence/mindcube/test.jsonl","artifact_paths":[],"benchmark":"mindcube","evaluation":{"aggregation":"micro","expected_group_counts":{"among":600,"around":250,"rotation":200},"metric_profile":"micro_accuracy","parser_profile":"multiple_choice","prompt_profile":"mindcube_official"},"expected_count":1050,"family":"spatial_intelligence","legacy_environment":{"MINDCUBE_BATCH_SIZE":16,"MINDCUBE_MAX_MODEL_LEN":32768,"MINDCUBE_MAX_NEW_TOKENS":1024},"license":"See the upstream benchmark terms","media_paths":["media/spatial_intelligence/mindcube/images","media/spatial_intelligence/mindcube/images/test"],"redistribution_authorized":true,"schema_version":1,"source_url":"https://huggingface.co/datasets/oscarqjh/MindCube_lmmseval/tree/7dd2725d9bd4149f2aad00a9843f72a3824da003","split":"test","task":"mindcube"}
|
| 3 |
+
{"annotation_path":"annotations/video_qa/mlvu/dev.jsonl","artifact_paths":[],"benchmark":"mlvu","evaluation":{"metric_profile":"macro_accuracy","parser_profile":"multiple_choice","prompt_profile":"mlvu_default"},"expected_count":2174,"family":"video_qa","legacy_environment":{"MLVU_BATCH_SIZE":1,"MLVU_ENABLE_THINKING":false,"MLVU_FPS":2,"MLVU_GPU_MEMORY_UTILIZATION":0.9,"MLVU_MAX_FRAMES":384,"MLVU_MAX_MODEL_LEN":65536,"MLVU_MAX_NEW_TOKENS":128,"MLVU_MAX_NUM_BATCHED_TOKENS":65536,"MLVU_MIN_P":0.0,"MLVU_PRESENCE_PENALTY":0.0,"MLVU_PROMPT_MODE":"default","MLVU_TEMPERATURE":0.0,"MLVU_TOP_K":-1,"MLVU_TOP_P":1.0,"MLVU_VIDEO_MAX_PIXELS":262144,"MLVU_VIDEO_MIN_PIXELS":4096,"MLVU_VIDEO_TOTAL_PIXELS":0},"license":"See the upstream benchmark terms","media_paths":["media/video_qa/mlvu/videos","media/video_qa/mlvu/videos/dev"],"preprocessing":{"batch_size":1,"enable_thinking":false,"fps":2,"gpu_memory_utilization":0.9,"max_frames":384,"max_model_len":65536,"max_new_tokens":128,"max_num_batched_tokens":65536,"min_p":0.0,"presence_penalty":0.0,"prompt_mode":"default","temperature":0.0,"top_k":-1,"top_p":1.0,"video_max_pixels":262144,"video_min_pixels":4096,"video_total_pixels":0},"redistribution_authorized":true,"schema_version":1,"source_url":"https://github.com/JUNJIE99/MLVU","split":"dev","task":"mlvu"}
|
| 4 |
+
{"annotation_path":"annotations/spatial_intelligence/mmsi/test.jsonl","artifact_paths":[],"benchmark":"mmsi","evaluation":{"metric_profile":"accuracy","parser_profile":"multiple_choice","prompt_profile":"mmsi_default"},"expected_count":1000,"family":"spatial_intelligence","legacy_environment":{"MMSI_BACKEND":"transformers","MMSI_ENABLE_THINKING":false,"MMSI_IMAGE_MAX_PIXELS":262144,"MMSI_IMAGE_MIN_PIXELS":4096},"license":"See the upstream benchmark terms","media_paths":["media/spatial_intelligence/mmsi/images/test"],"preprocessing":{"image_max_pixels":262144,"image_min_pixels":4096},"redistribution_authorized":true,"schema_version":1,"source_url":"https://huggingface.co/datasets/RunsenXu/MMSI-Bench","split":"test","task":"mmsi"}
|
| 5 |
+
{"annotation_path":"annotations/video_qa/mmvu/test.jsonl","artifact_paths":[],"benchmark":"mmvu","evaluation":{"metric_profile":"accuracy","parser_profile":"multiple_choice","prompt_profile":"mmvu_default"},"expected_count":625,"family":"video_qa","legacy_environment":{"MMVU_BATCH_SIZE":1,"MMVU_ENABLE_THINKING":false,"MMVU_FPS":2,"MMVU_GPU_MEMORY_UTILIZATION":0.9,"MMVU_MAX_FRAMES":384,"MMVU_MAX_MODEL_LEN":65536,"MMVU_MAX_NEW_TOKENS":128,"MMVU_MAX_NUM_BATCHED_TOKENS":65536,"MMVU_MIN_P":0.0,"MMVU_PRESENCE_PENALTY":0.0,"MMVU_PROMPT_MODE":"default","MMVU_TEMPERATURE":0.0,"MMVU_TOP_K":-1,"MMVU_TOP_P":1.0,"MMVU_VIDEO_MAX_PIXELS":262144,"MMVU_VIDEO_MIN_PIXELS":4096,"MMVU_VIDEO_TOTAL_PIXELS":0},"license":"See the upstream benchmark terms","media_paths":["media/video_qa/mmvu/videos","media/video_qa/mmvu/videos/test"],"preprocessing":{"batch_size":1,"enable_thinking":false,"fps":2,"gpu_memory_utilization":0.9,"max_frames":384,"max_model_len":65536,"max_new_tokens":128,"max_num_batched_tokens":65536,"min_p":0.0,"presence_penalty":0.0,"prompt_mode":"default","temperature":0.0,"top_k":-1,"top_p":1.0,"video_max_pixels":262144,"video_min_pixels":4096,"video_total_pixels":0},"redistribution_authorized":true,"schema_version":1,"source_url":"https://github.com/yale-nlp/MMVU","split":"test","task":"mmvu"}
|
| 6 |
+
{"annotation_path":"annotations/video_qa/mvbench/test.jsonl","artifact_paths":[],"benchmark":"mvbench","evaluation":{"metric_profile":"accuracy","parser_profile":"multiple_choice","prompt_profile":"mvbench_default"},"expected_count":3586,"family":"video_qa","legacy_environment":{"MVBENCH_BATCH_SIZE":1,"MVBENCH_ENABLE_THINKING":false,"MVBENCH_FPS":2,"MVBENCH_GPU_MEMORY_UTILIZATION":0.9,"MVBENCH_MAX_FRAMES":384,"MVBENCH_MAX_MODEL_LEN":65536,"MVBENCH_MAX_NEW_TOKENS":128,"MVBENCH_MAX_NUM_BATCHED_TOKENS":65536,"MVBENCH_MIN_P":0.0,"MVBENCH_PRESENCE_PENALTY":0.0,"MVBENCH_PROMPT_MODE":"default","MVBENCH_TEMPERATURE":0.0,"MVBENCH_TOP_K":-1,"MVBENCH_TOP_P":1.0,"MVBENCH_VIDEO_MAX_PIXELS":262144,"MVBENCH_VIDEO_MIN_PIXELS":4096,"MVBENCH_VIDEO_TOTAL_PIXELS":0},"license":"See the upstream benchmark terms","media_paths":["media/video_qa/mvbench/videos","media/video_qa/mvbench/videos/test"],"preprocessing":{"batch_size":1,"enable_thinking":false,"fps":2,"gpu_memory_utilization":0.9,"max_frames":384,"max_model_len":65536,"max_new_tokens":128,"max_num_batched_tokens":65536,"min_p":0.0,"presence_penalty":0.0,"prompt_mode":"default","temperature":0.0,"top_k":-1,"top_p":1.0,"video_max_pixels":262144,"video_min_pixels":4096,"video_total_pixels":0},"redistribution_authorized":true,"schema_version":1,"source_url":"https://github.com/OpenGVLab/Ask-Anything","split":"test","task":"mvbench"}
|
| 7 |
+
{"annotation_path":"annotations/spatial_intelligence/revsi/test.jsonl","artifact_paths":[],"benchmark":"revsi","evaluation":{"aggregation":"macro_by_question_type","frame_protocol":"native_all_frame","metric_profile":"revsi_macro_average","parser_profile":"revsi_answer","prompt_profile":"revsi_official"},"expected_count":6808,"family":"spatial_intelligence","legacy_environment":{"REVSI_BATCH_SIZE":16,"REVSI_ENABLE_THINKING":false,"REVSI_EXACT_NFRAMES":true,"REVSI_EXPECTED_SAMPLES":6808,"REVSI_FPS":2,"REVSI_FRAME_BUDGET":"all","REVSI_GPU_MEMORY_UTILIZATION":0.9,"REVSI_MAX_FRAMES":128,"REVSI_MAX_MODEL_LEN":32768,"REVSI_MAX_NEW_TOKENS":64,"REVSI_SETTING":"native-all-f128-exacttrue-fps2-min65536-maxnone-total16777216","REVSI_VIDEO_MIN_PIXELS":65536,"REVSI_VIDEO_TOTAL_PIXELS":16777216},"license":"See the upstream benchmark terms","media_paths":["media/spatial_intelligence/revsi/videos","media/spatial_intelligence/revsi/videos/test"],"preprocessing":{"exact_nframes":true,"fps":2,"max_frames":128,"video_min_pixels":65536,"video_total_pixels":16777216},"redistribution_authorized":true,"schema_version":1,"source_url":"https://arxiv.org/abs/2605.25979","split":"test","task":"revsi"}
|
| 8 |
+
{"annotation_path":"annotations/segmentation/mevis.jsonl","artifact_paths":[],"benchmark":"segmentation","evaluation":{"metric_profile":"segmentation_official","oracle_location":"task_payload.segmentation_output","oracle_profile":"segmentation_rle","parser_profile":"sam2_prompt","prompt_profile":"train_seg"},"expected_count":424,"family":"segmentation","legacy_environment":{"SEGMENTATION_BATCH_SIZE":16,"SEGMENTATION_DATASETS":"refcoco,refcocop,refcocog,mevis,reasonvos","SEGMENTATION_DATA_TYPE":"all","SEGMENTATION_ENABLE_THINKING":false,"SEGMENTATION_FPS":2,"SEGMENTATION_GPU_MEM_UTIL":0.85,"SEGMENTATION_MAX_FRAMES":128,"SEGMENTATION_MAX_MODEL_LEN":32768,"SEGMENTATION_MAX_NEW_TOKENS":1024,"SEGMENTATION_PROMPT_MODE":"train_seg","SEGMENTATION_RUN_SAM2":false,"SEGMENTATION_SEED":42,"SEGMENTATION_SETTING":"segmentation-refcoco_refcocop_refcocog_mevis_reasonvos-train_seg-f128-fps2-min4096-max262144-total16777216-readerdecord-new1024","SEGMENTATION_VIDEO_MAX_PIXELS":262144,"SEGMENTATION_VIDEO_MIN_PIXELS":4096,"SEGMENTATION_VIDEO_READER":"decord","SEGMENTATION_VIDEO_TOTAL_PIXELS":16777216},"license":"See the upstream benchmark terms","media_paths":["media/segmentation","media/segmentation/videos/mevis"],"preprocessing":{"fps":2,"max_frames":128,"video_max_pixels":262144,"video_min_pixels":4096,"video_reader":"decord","video_total_pixels":16777216},"redistribution_authorized":true,"schema_version":1,"source_url":"https://huggingface.co/datasets/OneThink/OneThinker-eval","split":"mevis","task":"segmentation"}
|
| 9 |
+
{"annotation_path":"annotations/segmentation/reasonvos.jsonl","artifact_paths":[],"benchmark":"segmentation","evaluation":{"metric_profile":"segmentation_official","oracle_location":"task_payload.segmentation_output","oracle_profile":"segmentation_rle","parser_profile":"sam2_prompt","prompt_profile":"train_seg"},"expected_count":458,"family":"segmentation","legacy_environment":{"SEGMENTATION_BATCH_SIZE":16,"SEGMENTATION_DATASETS":"refcoco,refcocop,refcocog,mevis,reasonvos","SEGMENTATION_DATA_TYPE":"all","SEGMENTATION_ENABLE_THINKING":false,"SEGMENTATION_FPS":2,"SEGMENTATION_GPU_MEM_UTIL":0.85,"SEGMENTATION_MAX_FRAMES":128,"SEGMENTATION_MAX_MODEL_LEN":32768,"SEGMENTATION_MAX_NEW_TOKENS":1024,"SEGMENTATION_PROMPT_MODE":"train_seg","SEGMENTATION_RUN_SAM2":false,"SEGMENTATION_SEED":42,"SEGMENTATION_SETTING":"segmentation-refcoco_refcocop_refcocog_mevis_reasonvos-train_seg-f128-fps2-min4096-max262144-total16777216-readerdecord-new1024","SEGMENTATION_VIDEO_MAX_PIXELS":262144,"SEGMENTATION_VIDEO_MIN_PIXELS":4096,"SEGMENTATION_VIDEO_READER":"decord","SEGMENTATION_VIDEO_TOTAL_PIXELS":16777216},"license":"See the upstream benchmark terms","media_paths":["media/segmentation","media/segmentation/videos/reasonvos"],"preprocessing":{"fps":2,"max_frames":128,"video_max_pixels":262144,"video_min_pixels":4096,"video_reader":"decord","video_total_pixels":16777216},"redistribution_authorized":true,"schema_version":1,"source_url":"https://huggingface.co/datasets/OneThink/OneThinker-eval","split":"reasonvos","task":"segmentation"}
|
| 10 |
+
{"annotation_path":"annotations/segmentation/refcoco.jsonl","artifact_paths":[],"benchmark":"segmentation","evaluation":{"metric_profile":"segmentation_official","oracle_location":"task_payload.segmentation_output","oracle_profile":"segmentation_rle","parser_profile":"sam2_prompt","prompt_profile":"train_seg"},"expected_count":3811,"family":"segmentation","legacy_environment":{"SEGMENTATION_BATCH_SIZE":16,"SEGMENTATION_DATASETS":"refcoco,refcocop,refcocog,mevis,reasonvos","SEGMENTATION_DATA_TYPE":"all","SEGMENTATION_ENABLE_THINKING":false,"SEGMENTATION_FPS":2,"SEGMENTATION_GPU_MEM_UTIL":0.85,"SEGMENTATION_MAX_FRAMES":128,"SEGMENTATION_MAX_MODEL_LEN":32768,"SEGMENTATION_MAX_NEW_TOKENS":1024,"SEGMENTATION_PROMPT_MODE":"train_seg","SEGMENTATION_RUN_SAM2":false,"SEGMENTATION_SEED":42,"SEGMENTATION_SETTING":"segmentation-refcoco_refcocop_refcocog_mevis_reasonvos-train_seg-f128-fps2-min4096-max262144-total16777216-readerdecord-new1024","SEGMENTATION_VIDEO_MAX_PIXELS":262144,"SEGMENTATION_VIDEO_MIN_PIXELS":4096,"SEGMENTATION_VIDEO_READER":"decord","SEGMENTATION_VIDEO_TOTAL_PIXELS":16777216},"license":"See the upstream benchmark terms","media_paths":["media/segmentation","media/segmentation/images/refcoco"],"preprocessing":{"fps":2,"max_frames":128,"video_max_pixels":262144,"video_min_pixels":4096,"video_reader":"decord","video_total_pixels":16777216},"redistribution_authorized":true,"schema_version":1,"source_url":"https://huggingface.co/datasets/OneThink/OneThinker-eval","split":"refcoco","task":"segmentation"}
|
| 11 |
+
{"annotation_path":"annotations/segmentation/refcocog.jsonl","artifact_paths":[],"benchmark":"segmentation","evaluation":{"metric_profile":"segmentation_official","oracle_location":"task_payload.segmentation_output","oracle_profile":"segmentation_rle","parser_profile":"sam2_prompt","prompt_profile":"train_seg"},"expected_count":2537,"family":"segmentation","legacy_environment":{"SEGMENTATION_BATCH_SIZE":16,"SEGMENTATION_DATASETS":"refcoco,refcocop,refcocog,mevis,reasonvos","SEGMENTATION_DATA_TYPE":"all","SEGMENTATION_ENABLE_THINKING":false,"SEGMENTATION_FPS":2,"SEGMENTATION_GPU_MEM_UTIL":0.85,"SEGMENTATION_MAX_FRAMES":128,"SEGMENTATION_MAX_MODEL_LEN":32768,"SEGMENTATION_MAX_NEW_TOKENS":1024,"SEGMENTATION_PROMPT_MODE":"train_seg","SEGMENTATION_RUN_SAM2":false,"SEGMENTATION_SEED":42,"SEGMENTATION_SETTING":"segmentation-refcoco_refcocop_refcocog_mevis_reasonvos-train_seg-f128-fps2-min4096-max262144-total16777216-readerdecord-new1024","SEGMENTATION_VIDEO_MAX_PIXELS":262144,"SEGMENTATION_VIDEO_MIN_PIXELS":4096,"SEGMENTATION_VIDEO_READER":"decord","SEGMENTATION_VIDEO_TOTAL_PIXELS":16777216},"license":"See the upstream benchmark terms","media_paths":["media/segmentation","media/segmentation/images/refcocog"],"preprocessing":{"fps":2,"max_frames":128,"video_max_pixels":262144,"video_min_pixels":4096,"video_reader":"decord","video_total_pixels":16777216},"redistribution_authorized":true,"schema_version":1,"source_url":"https://huggingface.co/datasets/OneThink/OneThinker-eval","split":"refcocog","task":"segmentation"}
|
| 12 |
+
{"annotation_path":"annotations/segmentation/refcocop.jsonl","artifact_paths":[],"benchmark":"segmentation","evaluation":{"metric_profile":"segmentation_official","oracle_location":"task_payload.segmentation_output","oracle_profile":"segmentation_rle","parser_profile":"sam2_prompt","prompt_profile":"train_seg"},"expected_count":3805,"family":"segmentation","legacy_environment":{"SEGMENTATION_BATCH_SIZE":16,"SEGMENTATION_DATASETS":"refcoco,refcocop,refcocog,mevis,reasonvos","SEGMENTATION_DATA_TYPE":"all","SEGMENTATION_ENABLE_THINKING":false,"SEGMENTATION_FPS":2,"SEGMENTATION_GPU_MEM_UTIL":0.85,"SEGMENTATION_MAX_FRAMES":128,"SEGMENTATION_MAX_MODEL_LEN":32768,"SEGMENTATION_MAX_NEW_TOKENS":1024,"SEGMENTATION_PROMPT_MODE":"train_seg","SEGMENTATION_RUN_SAM2":false,"SEGMENTATION_SEED":42,"SEGMENTATION_SETTING":"segmentation-refcoco_refcocop_refcocog_mevis_reasonvos-train_seg-f128-fps2-min4096-max262144-total16777216-readerdecord-new1024","SEGMENTATION_VIDEO_MAX_PIXELS":262144,"SEGMENTATION_VIDEO_MIN_PIXELS":4096,"SEGMENTATION_VIDEO_READER":"decord","SEGMENTATION_VIDEO_TOTAL_PIXELS":16777216},"license":"See the upstream benchmark terms","media_paths":["media/segmentation","media/segmentation/images/refcocop"],"preprocessing":{"fps":2,"max_frames":128,"video_max_pixels":262144,"video_min_pixels":4096,"video_reader":"decord","video_total_pixels":16777216},"redistribution_authorized":true,"schema_version":1,"source_url":"https://huggingface.co/datasets/OneThink/OneThinker-eval","split":"refcocop","task":"segmentation"}
|
| 13 |
+
{"annotation_path":"annotations/spatial_grounding/refcoco_test_a.jsonl","artifact_paths":[],"benchmark":"spatial_grounding","evaluation":{"metric_profile":"refcoco_iou","parser_profile":"norm1000_bbox","prompt_profile":"qwen_native"},"expected_count":5657,"family":"spatial_grounding","legacy_environment":{"SPATIAL_GROUNDING_BBOX_SELECT":"first","SPATIAL_GROUNDING_COORD_SYSTEM":"norm1000","SPATIAL_GROUNDING_DATASETS":"refcoco-val,refcoco-testA,refcoco-testB,refcoco+-val,refcoco+-testA,refcoco+-testB,refcocog-val,refcocog-test","SPATIAL_GROUNDING_MAX_NEW_TOKENS":1024,"SPATIAL_GROUNDING_MIN_TOKENS":64,"SPATIAL_GROUNDING_PROMPT_STYLE":"qwen_native","SPATIAL_GROUNDING_TOTAL_TOKENS":1024},"license":"See the upstream benchmark terms","media_paths":["media/spatial_grounding/images","media/spatial_grounding/images/refcoco_test_a"],"preprocessing":{"coordinate_system":"norm1000"},"redistribution_authorized":true,"schema_version":1,"source_url":"https://huggingface.co/datasets/OneThink/OneThinker-eval","split":"refcoco_test_a","task":"spatial_grounding"}
|
| 14 |
+
{"annotation_path":"annotations/spatial_grounding/refcoco_test_b.jsonl","artifact_paths":[],"benchmark":"spatial_grounding","evaluation":{"metric_profile":"refcoco_iou","parser_profile":"norm1000_bbox","prompt_profile":"qwen_native"},"expected_count":5095,"family":"spatial_grounding","legacy_environment":{"SPATIAL_GROUNDING_BBOX_SELECT":"first","SPATIAL_GROUNDING_COORD_SYSTEM":"norm1000","SPATIAL_GROUNDING_DATASETS":"refcoco-val,refcoco-testA,refcoco-testB,refcoco+-val,refcoco+-testA,refcoco+-testB,refcocog-val,refcocog-test","SPATIAL_GROUNDING_MAX_NEW_TOKENS":1024,"SPATIAL_GROUNDING_MIN_TOKENS":64,"SPATIAL_GROUNDING_PROMPT_STYLE":"qwen_native","SPATIAL_GROUNDING_TOTAL_TOKENS":1024},"license":"See the upstream benchmark terms","media_paths":["media/spatial_grounding/images","media/spatial_grounding/images/refcoco_test_b"],"preprocessing":{"coordinate_system":"norm1000"},"redistribution_authorized":true,"schema_version":1,"source_url":"https://huggingface.co/datasets/OneThink/OneThinker-eval","split":"refcoco_test_b","task":"spatial_grounding"}
|
| 15 |
+
{"annotation_path":"annotations/spatial_grounding/refcoco_val.jsonl","artifact_paths":[],"benchmark":"spatial_grounding","evaluation":{"metric_profile":"refcoco_iou","parser_profile":"norm1000_bbox","prompt_profile":"qwen_native"},"expected_count":10834,"family":"spatial_grounding","legacy_environment":{"SPATIAL_GROUNDING_BBOX_SELECT":"first","SPATIAL_GROUNDING_COORD_SYSTEM":"norm1000","SPATIAL_GROUNDING_DATASETS":"refcoco-val,refcoco-testA,refcoco-testB,refcoco+-val,refcoco+-testA,refcoco+-testB,refcocog-val,refcocog-test","SPATIAL_GROUNDING_MAX_NEW_TOKENS":1024,"SPATIAL_GROUNDING_MIN_TOKENS":64,"SPATIAL_GROUNDING_PROMPT_STYLE":"qwen_native","SPATIAL_GROUNDING_TOTAL_TOKENS":1024},"license":"See the upstream benchmark terms","media_paths":["media/spatial_grounding/images","media/spatial_grounding/images/refcoco_val"],"preprocessing":{"coordinate_system":"norm1000"},"redistribution_authorized":true,"schema_version":1,"source_url":"https://huggingface.co/datasets/OneThink/OneThinker-eval","split":"refcoco_val","task":"spatial_grounding"}
|
| 16 |
+
{"annotation_path":"annotations/spatial_grounding/refcocog_test.jsonl","artifact_paths":[],"benchmark":"spatial_grounding","evaluation":{"metric_profile":"refcoco_iou","parser_profile":"norm1000_bbox","prompt_profile":"qwen_native"},"expected_count":9602,"family":"spatial_grounding","legacy_environment":{"SPATIAL_GROUNDING_BBOX_SELECT":"first","SPATIAL_GROUNDING_COORD_SYSTEM":"norm1000","SPATIAL_GROUNDING_DATASETS":"refcoco-val,refcoco-testA,refcoco-testB,refcoco+-val,refcoco+-testA,refcoco+-testB,refcocog-val,refcocog-test","SPATIAL_GROUNDING_MAX_NEW_TOKENS":1024,"SPATIAL_GROUNDING_MIN_TOKENS":64,"SPATIAL_GROUNDING_PROMPT_STYLE":"qwen_native","SPATIAL_GROUNDING_TOTAL_TOKENS":1024},"license":"See the upstream benchmark terms","media_paths":["media/spatial_grounding/images","media/spatial_grounding/images/refcocog_test"],"preprocessing":{"coordinate_system":"norm1000"},"redistribution_authorized":true,"schema_version":1,"source_url":"https://huggingface.co/datasets/OneThink/OneThinker-eval","split":"refcocog_test","task":"spatial_grounding"}
|
| 17 |
+
{"annotation_path":"annotations/spatial_grounding/refcocog_val.jsonl","artifact_paths":[],"benchmark":"spatial_grounding","evaluation":{"metric_profile":"refcoco_iou","parser_profile":"norm1000_bbox","prompt_profile":"qwen_native"},"expected_count":4896,"family":"spatial_grounding","legacy_environment":{"SPATIAL_GROUNDING_BBOX_SELECT":"first","SPATIAL_GROUNDING_COORD_SYSTEM":"norm1000","SPATIAL_GROUNDING_DATASETS":"refcoco-val,refcoco-testA,refcoco-testB,refcoco+-val,refcoco+-testA,refcoco+-testB,refcocog-val,refcocog-test","SPATIAL_GROUNDING_MAX_NEW_TOKENS":1024,"SPATIAL_GROUNDING_MIN_TOKENS":64,"SPATIAL_GROUNDING_PROMPT_STYLE":"qwen_native","SPATIAL_GROUNDING_TOTAL_TOKENS":1024},"license":"See the upstream benchmark terms","media_paths":["media/spatial_grounding/images","media/spatial_grounding/images/refcocog_val"],"preprocessing":{"coordinate_system":"norm1000"},"redistribution_authorized":true,"schema_version":1,"source_url":"https://huggingface.co/datasets/OneThink/OneThinker-eval","split":"refcocog_val","task":"spatial_grounding"}
|
| 18 |
+
{"annotation_path":"annotations/spatial_grounding/refcocop_test_a.jsonl","artifact_paths":[],"benchmark":"spatial_grounding","evaluation":{"metric_profile":"refcoco_iou","parser_profile":"norm1000_bbox","prompt_profile":"qwen_native"},"expected_count":5726,"family":"spatial_grounding","legacy_environment":{"SPATIAL_GROUNDING_BBOX_SELECT":"first","SPATIAL_GROUNDING_COORD_SYSTEM":"norm1000","SPATIAL_GROUNDING_DATASETS":"refcoco-val,refcoco-testA,refcoco-testB,refcoco+-val,refcoco+-testA,refcoco+-testB,refcocog-val,refcocog-test","SPATIAL_GROUNDING_MAX_NEW_TOKENS":1024,"SPATIAL_GROUNDING_MIN_TOKENS":64,"SPATIAL_GROUNDING_PROMPT_STYLE":"qwen_native","SPATIAL_GROUNDING_TOTAL_TOKENS":1024},"license":"See the upstream benchmark terms","media_paths":["media/spatial_grounding/images","media/spatial_grounding/images/refcocop_test_a"],"preprocessing":{"coordinate_system":"norm1000"},"redistribution_authorized":true,"schema_version":1,"source_url":"https://huggingface.co/datasets/OneThink/OneThinker-eval","split":"refcocop_test_a","task":"spatial_grounding"}
|
| 19 |
+
{"annotation_path":"annotations/spatial_grounding/refcocop_test_b.jsonl","artifact_paths":[],"benchmark":"spatial_grounding","evaluation":{"metric_profile":"refcoco_iou","parser_profile":"norm1000_bbox","prompt_profile":"qwen_native"},"expected_count":4889,"family":"spatial_grounding","legacy_environment":{"SPATIAL_GROUNDING_BBOX_SELECT":"first","SPATIAL_GROUNDING_COORD_SYSTEM":"norm1000","SPATIAL_GROUNDING_DATASETS":"refcoco-val,refcoco-testA,refcoco-testB,refcoco+-val,refcoco+-testA,refcoco+-testB,refcocog-val,refcocog-test","SPATIAL_GROUNDING_MAX_NEW_TOKENS":1024,"SPATIAL_GROUNDING_MIN_TOKENS":64,"SPATIAL_GROUNDING_PROMPT_STYLE":"qwen_native","SPATIAL_GROUNDING_TOTAL_TOKENS":1024},"license":"See the upstream benchmark terms","media_paths":["media/spatial_grounding/images","media/spatial_grounding/images/refcocop_test_b"],"preprocessing":{"coordinate_system":"norm1000"},"redistribution_authorized":true,"schema_version":1,"source_url":"https://huggingface.co/datasets/OneThink/OneThinker-eval","split":"refcocop_test_b","task":"spatial_grounding"}
|
| 20 |
+
{"annotation_path":"annotations/spatial_grounding/refcocop_val.jsonl","artifact_paths":[],"benchmark":"spatial_grounding","evaluation":{"metric_profile":"refcoco_iou","parser_profile":"norm1000_bbox","prompt_profile":"qwen_native"},"expected_count":10758,"family":"spatial_grounding","legacy_environment":{"SPATIAL_GROUNDING_BBOX_SELECT":"first","SPATIAL_GROUNDING_COORD_SYSTEM":"norm1000","SPATIAL_GROUNDING_DATASETS":"refcoco-val,refcoco-testA,refcoco-testB,refcoco+-val,refcoco+-testA,refcoco+-testB,refcocog-val,refcocog-test","SPATIAL_GROUNDING_MAX_NEW_TOKENS":1024,"SPATIAL_GROUNDING_MIN_TOKENS":64,"SPATIAL_GROUNDING_PROMPT_STYLE":"qwen_native","SPATIAL_GROUNDING_TOTAL_TOKENS":1024},"license":"See the upstream benchmark terms","media_paths":["media/spatial_grounding/images","media/spatial_grounding/images/refcocop_val"],"preprocessing":{"coordinate_system":"norm1000"},"redistribution_authorized":true,"schema_version":1,"source_url":"https://huggingface.co/datasets/OneThink/OneThinker-eval","split":"refcocop_val","task":"spatial_grounding"}
|
| 21 |
+
{"annotation_path":"annotations/stvg/stvg.jsonl","artifact_paths":[],"benchmark":"stvg","evaluation":{"metric_profile":"stvg_official","parser_profile":"temporal_spatial_boxes","prompt_profile":"train_stvg"},"expected_count":1851,"family":"spatial_temporal_grounding","legacy_environment":{"STVG_DATASETS":"eval_stvg","STVG_FPS":2,"STVG_MAX_FRAMES":128,"STVG_MAX_NEW_TOKENS":2048,"STVG_PROMPT_MODE":"train_stvg","STVG_VIDEO_MAX_PIXELS":393216,"STVG_VIDEO_MIN_PIXELS":65536,"STVG_VIDEO_TOTAL_PIXELS":10485760},"license":"See the upstream benchmark terms","media_paths":["media/stvg","media/stvg/videos/stvg"],"preprocessing":{"stvg_fps":2,"stvg_max_frames":128,"stvg_video_max_pixels":393216,"stvg_video_min_pixels":65536,"stvg_video_total_pixels":10485760},"redistribution_authorized":true,"schema_version":1,"source_url":"https://huggingface.co/datasets/OneThink/OneThinker-eval","split":"stvg","task":"stvg"}
|
| 22 |
+
{"annotation_path":"annotations/temporal_grounding/activitynet_timelens.jsonl","artifact_paths":[],"benchmark":"temporal_grounding","checksums":{"annotations/temporal_grounding/activitynet_timelens.jsonl":"3091f03a20b2bf4548d4079d95bb8dc564da53d4e6b2c32470862b15622c7a77"},"evaluation":{"metric_profile":"temporal_iou","parser_profile":"temporal_spans","prompt_profile":"timelens_same"},"expected_count":4500,"family":"temporal_grounding","legacy_environment":{"TIMELENS_DATASETS":"charades_timelens,activitynet_timelens,qvhighlights_timelens","TIMELENS_ENABLE_THINKING":false,"TIMELENS_FPS":4,"TIMELENS_MAX_FRAMES":2048,"TIMELENS_MAX_NEW_TOKENS":128,"TIMELENS_MAX_PIXELS":409600,"TIMELENS_MIN_TOKENS":1,"TIMELENS_NUM_WORKERS":2,"TIMELENS_PROMPT_MODE":"same","TIMELENS_STOP_AFTER_ANSWER":true,"TIMELENS_TOTAL_TOKENS":128000},"license":"See the upstream benchmark terms","media_paths":["media/temporal_grounding/videos"],"preprocessing":{"fps":4,"max_frames":2048,"max_pixels":409600,"min_tokens":1,"total_tokens":128000},"redistribution_authorized":true,"schema_version":1,"source_url":"https://huggingface.co/datasets/TencentARC/TimeLens-Bench","split":"activitynet_timelens","task":"temporal_grounding"}
|
| 23 |
+
{"annotation_path":"annotations/temporal_grounding/charades_timelens.jsonl","artifact_paths":[],"benchmark":"temporal_grounding","checksums":{"annotations/temporal_grounding/charades_timelens.jsonl":"af289d9be3e8c56f98b12c39bda37758a34205c1aa75e2c03e3174dbbe6f2794"},"evaluation":{"metric_profile":"temporal_iou","parser_profile":"temporal_spans","prompt_profile":"timelens_same"},"expected_count":3363,"family":"temporal_grounding","legacy_environment":{"TIMELENS_DATASETS":"charades_timelens,activitynet_timelens,qvhighlights_timelens","TIMELENS_ENABLE_THINKING":false,"TIMELENS_FPS":4,"TIMELENS_MAX_FRAMES":2048,"TIMELENS_MAX_NEW_TOKENS":128,"TIMELENS_MAX_PIXELS":409600,"TIMELENS_MIN_TOKENS":1,"TIMELENS_NUM_WORKERS":2,"TIMELENS_PROMPT_MODE":"same","TIMELENS_STOP_AFTER_ANSWER":true,"TIMELENS_TOTAL_TOKENS":128000},"license":"See the upstream benchmark terms","media_paths":["media/temporal_grounding/videos"],"preprocessing":{"fps":4,"max_frames":2048,"max_pixels":409600,"min_tokens":1,"total_tokens":128000},"redistribution_authorized":true,"schema_version":1,"source_url":"https://huggingface.co/datasets/TencentARC/TimeLens-Bench","split":"charades_timelens","task":"temporal_grounding"}
|
| 24 |
+
{"annotation_path":"annotations/temporal_grounding/qvhighlights_timelens.jsonl","artifact_paths":[],"benchmark":"temporal_grounding","checksums":{"annotations/temporal_grounding/qvhighlights_timelens.jsonl":"922a736a73ff9fb3d3996e0b75ee1343283bee23d7e60b368d6840f8612e619b"},"evaluation":{"metric_profile":"temporal_iou","parser_profile":"temporal_spans","prompt_profile":"timelens_same"},"expected_count":1541,"family":"temporal_grounding","legacy_environment":{"TIMELENS_DATASETS":"charades_timelens,activitynet_timelens,qvhighlights_timelens","TIMELENS_ENABLE_THINKING":false,"TIMELENS_FPS":4,"TIMELENS_MAX_FRAMES":2048,"TIMELENS_MAX_NEW_TOKENS":128,"TIMELENS_MAX_PIXELS":409600,"TIMELENS_MIN_TOKENS":1,"TIMELENS_NUM_WORKERS":2,"TIMELENS_PROMPT_MODE":"same","TIMELENS_STOP_AFTER_ANSWER":true,"TIMELENS_TOTAL_TOKENS":128000},"license":"See the upstream benchmark terms","media_paths":["media/temporal_grounding/videos"],"preprocessing":{"fps":4,"max_frames":2048,"max_pixels":409600,"min_tokens":1,"total_tokens":128000},"redistribution_authorized":true,"schema_version":1,"source_url":"https://huggingface.co/datasets/TencentARC/TimeLens-Bench","split":"qvhighlights_timelens","task":"temporal_grounding"}
|
| 25 |
+
{"annotation_path":"annotations/tracking/got10k.jsonl","artifact_paths":[],"benchmark":"tracking","checksums":{"annotations/tracking/got10k.jsonl":"1206da3c1fd75360ccea074a9e211965fe2d8cfd514903564b037641a00fb12b"},"evaluation":{"metric_profile":"got10k_ao","parser_profile":"tracking_boxes","prompt_profile":"tracking_default"},"expected_count":180,"family":"tracking","legacy_environment":{"TRACKING_DATASETS":"eval_got10k","TRACKING_FPS":1,"TRACKING_MAX_FRAMES":32,"TRACKING_MAX_NEW_TOKENS":8192,"TRACKING_PROMPT_MODE":"default","TRACKING_VIDEO_MAX_PIXELS":786432,"TRACKING_VIDEO_MIN_PIXELS":4096,"TRACKING_VIDEO_TOTAL_PIXELS":8388608},"license":"See the upstream benchmark terms","media_paths":["media/tracking","media/tracking/videos"],"preprocessing":{"tracking_fps":1,"tracking_max_frames":32,"tracking_video_max_pixels":786432,"tracking_video_min_pixels":4096,"tracking_video_total_pixels":8388608},"redistribution_authorized":true,"schema_version":1,"source_url":"https://huggingface.co/datasets/OneThink/OneThinker-eval","split":"got10k","task":"tracking"}
|
| 26 |
+
{"annotation_path":"annotations/video_qa/videoholmes/test.jsonl","artifact_paths":[],"benchmark":"videoholmes","evaluation":{"metric_profile":"accuracy","parser_profile":"multiple_choice","prompt_profile":"videoholmes_default"},"expected_count":1837,"family":"video_qa","legacy_environment":{"VIDEOHOLMES_BATCH_SIZE":1,"VIDEOHOLMES_ENABLE_THINKING":false,"VIDEOHOLMES_FPS":2,"VIDEOHOLMES_GPU_MEMORY_UTILIZATION":0.9,"VIDEOHOLMES_MAX_FRAMES":384,"VIDEOHOLMES_MAX_MODEL_LEN":65536,"VIDEOHOLMES_MAX_NEW_TOKENS":128,"VIDEOHOLMES_MAX_NUM_BATCHED_TOKENS":65536,"VIDEOHOLMES_MIN_P":0.0,"VIDEOHOLMES_PRESENCE_PENALTY":0.0,"VIDEOHOLMES_PROMPT_MODE":"default","VIDEOHOLMES_TEMPERATURE":0.0,"VIDEOHOLMES_TOP_K":-1,"VIDEOHOLMES_TOP_P":1.0,"VIDEOHOLMES_VIDEO_MAX_PIXELS":262144,"VIDEOHOLMES_VIDEO_MIN_PIXELS":4096,"VIDEOHOLMES_VIDEO_TOTAL_PIXELS":0},"license":"See the upstream benchmark terms","media_paths":["media/video_qa/videoholmes/videos","media/video_qa/videoholmes/videos/test"],"preprocessing":{"batch_size":1,"enable_thinking":false,"fps":2,"gpu_memory_utilization":0.9,"max_frames":384,"max_model_len":65536,"max_new_tokens":128,"max_num_batched_tokens":65536,"min_p":0.0,"presence_penalty":0.0,"prompt_mode":"default","temperature":0.0,"top_k":-1,"top_p":1.0,"video_max_pixels":262144,"video_min_pixels":4096,"video_total_pixels":0},"redistribution_authorized":true,"schema_version":1,"source_url":"https://github.com/TencentARC/Video-Holmes","split":"test","task":"videoholmes"}
|
| 27 |
+
{"annotation_path":"annotations/video_qa/videomme/test.jsonl","artifact_paths":["artifacts/video_qa/videomme"],"benchmark":"videomme","evaluation":{"metric_profile":"accuracy","parser_profile":"multiple_choice","prompt_profile":"videomme_default"},"expected_count":2700,"family":"video_qa","legacy_environment":{"VIDEOMME_BATCH_SIZE":1,"VIDEOMME_ENABLE_THINKING":false,"VIDEOMME_FPS":2,"VIDEOMME_GPU_MEMORY_UTILIZATION":0.9,"VIDEOMME_MAX_FRAMES":384,"VIDEOMME_MAX_MODEL_LEN":65536,"VIDEOMME_MAX_NEW_TOKENS":128,"VIDEOMME_MAX_NUM_BATCHED_TOKENS":65536,"VIDEOMME_MIN_P":0.0,"VIDEOMME_PRESENCE_PENALTY":0.0,"VIDEOMME_PROMPT_MODE":"default","VIDEOMME_SETTING":"all-qwen3_vl-sub0-f384-fps2-min4096-max262144-total0-videomme_preprocessed_384f_262k_total0","VIDEOMME_TEMPERATURE":0.0,"VIDEOMME_TOP_K":-1,"VIDEOMME_TOP_P":1.0,"VIDEOMME_VIDEO_MAX_PIXELS":262144,"VIDEOMME_VIDEO_MIN_PIXELS":4096,"VIDEOMME_VIDEO_TOTAL_PIXELS":0},"license":"See the upstream benchmark terms","media_paths":["media/video_qa/videomme/videos","media/video_qa/videomme/videos/test"],"preprocessing":{"batch_size":1,"enable_thinking":false,"fps":2,"gpu_memory_utilization":0.9,"max_frames":384,"max_model_len":65536,"max_new_tokens":128,"max_num_batched_tokens":65536,"min_p":0.0,"presence_penalty":0.0,"prompt_mode":"default","temperature":0.0,"top_k":-1,"top_p":1.0,"video_max_pixels":262144,"video_min_pixels":4096,"video_total_pixels":0},"redistribution_authorized":true,"schema_version":1,"source_url":"https://github.com/BradyFU/Video-MME","split":"test","task":"videomme"}
|
| 28 |
+
{"annotation_path":"annotations/video_qa/videommev2/test.jsonl","artifact_paths":["artifacts/video_qa/videommev2"],"benchmark":"videommev2","evaluation":{"metric_profile":"accuracy","parser_profile":"multiple_choice","prompt_profile":"videommev2_default"},"expected_count":3200,"family":"video_qa","legacy_environment":{"VIDEOMMEV2_ANSWER_FILTER":"","VIDEOMMEV2_BATCH_SIZE":1,"VIDEOMMEV2_ENABLE_THINKING":false,"VIDEOMMEV2_FPS":2,"VIDEOMMEV2_GPU_MEMORY_UTILIZATION":0.9,"VIDEOMMEV2_MAX_FRAMES":384,"VIDEOMMEV2_MAX_MODEL_LEN":65536,"VIDEOMMEV2_MAX_NEW_TOKENS":128,"VIDEOMMEV2_MAX_NUM_BATCHED_TOKENS":65536,"VIDEOMMEV2_MIN_P":0.0,"VIDEOMMEV2_PRESENCE_PENALTY":0.0,"VIDEOMMEV2_PROMPT_MODE":"default","VIDEOMMEV2_TEMPERATURE":0.0,"VIDEOMMEV2_TOP_K":-1,"VIDEOMMEV2_TOP_P":1.0,"VIDEOMMEV2_VIDEO_MAX_PIXELS":262144,"VIDEOMMEV2_VIDEO_MIN_PIXELS":4096,"VIDEOMMEV2_VIDEO_TOTAL_PIXELS":0},"license":"See the upstream benchmark terms","media_paths":["media/video_qa/videommev2/videos","media/video_qa/videommev2/videos/test"],"preprocessing":{"batch_size":1,"enable_thinking":false,"fps":2,"gpu_memory_utilization":0.9,"max_frames":384,"max_model_len":65536,"max_new_tokens":128,"max_num_batched_tokens":65536,"min_p":0.0,"presence_penalty":0.0,"prompt_mode":"default","temperature":0.0,"top_k":-1,"top_p":1.0,"video_max_pixels":262144,"video_min_pixels":4096,"video_total_pixels":0},"redistribution_authorized":true,"schema_version":1,"source_url":"https://github.com/BradyFU/Video-MME","split":"test","task":"videommev2"}
|
| 29 |
+
{"annotation_path":"annotations/spatial_intelligence/vsi/test.jsonl","artifact_paths":["artifacts/spatial_intelligence/vsi"],"benchmark":"vsi","evaluation":{"metric_profile":"vsi_official","parser_profile":"vsi_answer","prompt_profile":"vsi_default"},"expected_count":5130,"family":"spatial_intelligence","legacy_environment":{"VSI_BATCH_SIZE":16,"VSI_MAX_MODEL_LEN":32768,"VSI_MAX_NEW_TOKENS":1024,"VSI_SETTING":"video128-16M-video-f128-fps2-min65536-maxnone-total16777216-vsibench_preprocessed_128f_16M"},"license":"See the upstream benchmark terms","media_paths":["media/spatial_intelligence/vsi/videos","media/spatial_intelligence/vsi/videos/test"],"preprocessing":{"fps":2,"max_frames":128,"video_min_pixels":65536,"video_total_pixels":16777216},"redistribution_authorized":true,"schema_version":1,"source_url":"https://huggingface.co/datasets/nyu-visionx/VSI-Bench","split":"test","task":"vsi"}
|
code/docs/environment.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<p align="right"><a href="environment_zh.md">简体中文</a></p>
|
| 2 |
+
|
| 3 |
+
# Reproduce the OraRL Environment
|
| 4 |
+
|
| 5 |
+
OraRL uses one reproducible software stack for policy optimization and
|
| 6 |
+
task-native evaluation. The released configuration was validated on NVIDIA
|
| 7 |
+
H20/Hopper GPUs with CUDA 12.9 and includes both runtimes in this repository.
|
| 8 |
+
|
| 9 |
+
## Requirements
|
| 10 |
+
|
| 11 |
+
- Linux with Conda available in `PATH`
|
| 12 |
+
- NVIDIA GPU with a recent driver
|
| 13 |
+
- CUDA 12.9-compatible hardware for the paper-matched environment
|
| 14 |
+
|
| 15 |
+
CUDA 12.9 GA officially requires NVIDIA Linux driver 575.51.03 or newer.
|
| 16 |
+
Other NVIDIA GPUs may work, but the published setup was validated on H20.
|
| 17 |
+
|
| 18 |
+
## Install the pinned stack
|
| 19 |
+
|
| 20 |
+
```bash
|
| 21 |
+
bash scripts/create_conda_env.sh
|
| 22 |
+
conda activate orarl
|
| 23 |
+
|
| 24 |
+
python scripts/check_environment.py \
|
| 25 |
+
--require-gpu \
|
| 26 |
+
--model /path/to/local/model
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
The installer pins Python 3.11, PyTorch 2.10.0+cu129, Transformers 5.5.4,
|
| 30 |
+
vLLM 0.19.1, FlashAttention 2.8.3, and the remaining packages in
|
| 31 |
+
`requirements-cu129.txt`. The final editable install exposes the `orarl-*`
|
| 32 |
+
commands and the bundled `verl` trainer package from this checkout; do not
|
| 33 |
+
install an unrelated `verl` release from PyPI over it.
|
| 34 |
+
|
| 35 |
+
## Validate an evaluation-only node
|
| 36 |
+
|
| 37 |
+
If a node will only run evaluation, skip the training-side checks:
|
| 38 |
+
|
| 39 |
+
```bash
|
| 40 |
+
bash scripts/create_conda_env.sh --evaluation-only
|
| 41 |
+
conda activate orarl
|
| 42 |
+
|
| 43 |
+
python scripts/check_environment.py --evaluation-only
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
## Verify the release checkout
|
| 47 |
+
|
| 48 |
+
Run the release checks before allocating a long job:
|
| 49 |
+
|
| 50 |
+
```bash
|
| 51 |
+
python scripts/check_release.py
|
| 52 |
+
python -m pytest -q
|
| 53 |
+
ruff check .
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
On a paper-matched H20 node, add `--require-gpu --require-h20` to
|
| 57 |
+
`scripts/check_environment.py`. Use the same source revision and environment on
|
| 58 |
+
every node of a distributed run.
|
| 59 |
+
|
| 60 |
+
Continue with [Training](training.md) or [Evaluation](evaluation.md).
|
code/docs/environment_zh.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<p align="right"><a href="environment.md">English</a></p>
|
| 2 |
+
|
| 3 |
+
# 复现 OraRL 环境
|
| 4 |
+
|
| 5 |
+
OraRL 使用同一套可复现软件栈完成策略优化与任务原生评测。发布配置在 NVIDIA
|
| 6 |
+
H20/Hopper 与 CUDA 12.9 上完成验证,本仓库同时包含两部分运行时。
|
| 7 |
+
|
| 8 |
+
## 环境要求
|
| 9 |
+
|
| 10 |
+
- `PATH` 中可用 Conda 的 Linux 系统
|
| 11 |
+
- 驱动版本较新的 NVIDIA GPU
|
| 12 |
+
- 复现论文环境时使用兼容 CUDA 12.9 的硬件
|
| 13 |
+
|
| 14 |
+
CUDA 12.9 GA 官方要求 NVIDIA Linux 驱动不低于 575.51.03。其他 NVIDIA GPU
|
| 15 |
+
也可能运行,但发布配置仅在 H20 上完成验证。
|
| 16 |
+
|
| 17 |
+
## 安装固定版本软件栈
|
| 18 |
+
|
| 19 |
+
```bash
|
| 20 |
+
bash scripts/create_conda_env.sh
|
| 21 |
+
conda activate orarl
|
| 22 |
+
|
| 23 |
+
python scripts/check_environment.py \
|
| 24 |
+
--require-gpu \
|
| 25 |
+
--model /path/to/local/model
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
脚本会固定 Python 3.11、PyTorch 2.10.0+cu129、Transformers 5.5.4、
|
| 29 |
+
vLLM 0.19.1、FlashAttention 2.8.3,以及 `requirements-cu129.txt` 中的其余依赖。
|
| 30 |
+
最后一步的可编辑安装会同时提供 `orarl-*` 命令和本仓库自带的 `verl` 训练包;
|
| 31 |
+
请勿再用 PyPI 上无关的 `verl` 版本覆盖它。
|
| 32 |
+
|
| 33 |
+
## 验证仅用于评测的节点
|
| 34 |
+
|
| 35 |
+
若某个节点只跑评测,可跳过训练侧检查:
|
| 36 |
+
|
| 37 |
+
```bash
|
| 38 |
+
bash scripts/create_conda_env.sh --evaluation-only
|
| 39 |
+
conda activate orarl
|
| 40 |
+
|
| 41 |
+
python scripts/check_environment.py --evaluation-only
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
## 验证发布源码
|
| 45 |
+
|
| 46 |
+
分配长任务前先执行:
|
| 47 |
+
|
| 48 |
+
```bash
|
| 49 |
+
python scripts/check_release.py
|
| 50 |
+
python -m pytest -q
|
| 51 |
+
ruff check .
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
在与论文一致的 H20 节点上,可为 `scripts/check_environment.py` 增加
|
| 55 |
+
`--require-gpu --require-h20`。多节点任务应在所有节点使用相同的源码版本和环境。
|
| 56 |
+
|
| 57 |
+
随后参阅[训练](training_zh.md)或[评测](evaluation_zh.md)。
|
code/docs/evaluation.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<p align="right"><a href="evaluation_zh.md">简体中文</a></p>
|
| 2 |
+
|
| 3 |
+
# Evaluate Video-ORA
|
| 4 |
+
|
| 5 |
+
OraRL evaluates direct, task-native answers with canonical profiles for the
|
| 6 |
+
seven released task families. The corresponding runtime lives under
|
| 7 |
+
`eval/task/`; first
|
| 8 |
+
[validate the evaluation node](environment.md#validate-an-evaluation-only-node).
|
| 9 |
+
|
| 10 |
+
## 1. Materialize model and data
|
| 11 |
+
|
| 12 |
+
Install the Hugging Face CLI and download the released assets:
|
| 13 |
+
|
| 14 |
+
```bash
|
| 15 |
+
python -m pip install -U huggingface_hub
|
| 16 |
+
|
| 17 |
+
hf download OraRL/Video-ORA-9B \
|
| 18 |
+
--local-dir "$PWD/models/Video-ORA-9B"
|
| 19 |
+
|
| 20 |
+
hf download OraRL/OraRL-Data \
|
| 21 |
+
--repo-type dataset \
|
| 22 |
+
--include "OraRL-eval-data/**" \
|
| 23 |
+
--local-dir "$PWD/OraRL-Data"
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
The complete evaluation release is large. Downloads are resumable, and
|
| 27 |
+
`OraRL-eval-data/assets.jsonl` is the authoritative file inventory.
|
| 28 |
+
|
| 29 |
+
The local dataset layout is:
|
| 30 |
+
|
| 31 |
+
```text
|
| 32 |
+
OraRL-Data/OraRL-eval-data/
|
| 33 |
+
├── datasets.jsonl # benchmark, prompt, parser, metric, and sampling profiles
|
| 34 |
+
├── assets.jsonl # released-file inventory
|
| 35 |
+
├── annotations/ # canonical JSONL rows
|
| 36 |
+
└── media/ # raw images, videos, and subtitles
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
Derived preprocessing caches are intentionally excluded. Evaluators decode the
|
| 40 |
+
declared raw media when no compatible cache is present.
|
| 41 |
+
|
| 42 |
+
## 2. Run the canonical paper profile
|
| 43 |
+
|
| 44 |
+
Preview the resolved command first:
|
| 45 |
+
|
| 46 |
+
```bash
|
| 47 |
+
orarl-eval \
|
| 48 |
+
--model "$PWD/models/Video-ORA-9B" \
|
| 49 |
+
--tasks paper \
|
| 50 |
+
--dataset "$PWD/OraRL-Data/OraRL-eval-data" \
|
| 51 |
+
--summary "$PWD/outputs/Video-ORA-9B/evaluation.json"
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
`orarl-eval` is a dry run by default. After checking the model, dataset,
|
| 55 |
+
evaluator, task profiles, and output paths, add `--run`.
|
| 56 |
+
|
| 57 |
+
To validate the pipeline with a bounded smoke test:
|
| 58 |
+
|
| 59 |
+
```bash
|
| 60 |
+
orarl-eval \
|
| 61 |
+
--model "$PWD/models/Video-ORA-9B" \
|
| 62 |
+
--tasks videomme \
|
| 63 |
+
--dataset "$PWD/OraRL-Data/OraRL-eval-data" \
|
| 64 |
+
--max-samples 8 \
|
| 65 |
+
--summary "$PWD/outputs/Video-ORA-9B/videomme-smoke.json" \
|
| 66 |
+
--run
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
Smoke scores only validate execution and must not be reported as benchmark
|
| 70 |
+
results.
|
| 71 |
+
|
| 72 |
+
## 3. Compose a task suite
|
| 73 |
+
|
| 74 |
+
`--tasks paper` selects all released tasks. `--tasks video_qa` selects the
|
| 75 |
+
seven Video QA benchmarks. Individual task names may be comma-separated.
|
| 76 |
+
|
| 77 |
+
| Family | Task names |
|
| 78 |
+
| --- | --- |
|
| 79 |
+
| Video QA | `videomme`, `videommev2`, `mvbench`, `mmvu`, `videoholmes`, `longvideobench`, `mlvu` |
|
| 80 |
+
| Spatial intelligence | `vsi`, `mmsi`, `mindcube`, `revsi` |
|
| 81 |
+
| Temporal grounding | `temporal_grounding` |
|
| 82 |
+
| Spatial grounding | `spatial_grounding` |
|
| 83 |
+
| Tracking | `tracking` |
|
| 84 |
+
| Spatial-temporal grounding | `stvg` |
|
| 85 |
+
| Segmentation | `segmentation` |
|
| 86 |
+
|
| 87 |
+
The canonical profiles in `datasets.jsonl` pin frame sampling, resolution,
|
| 88 |
+
prompts, parsers, and metrics. Changing them defines a different evaluation
|
| 89 |
+
setting. ReVSI is reported separately from the three-benchmark
|
| 90 |
+
spatial-intelligence average.
|
| 91 |
+
|
| 92 |
+
## 4. Add segmentation post-processing
|
| 93 |
+
|
| 94 |
+
Segmentation inference runs without SAM2 post-processing by default. Enabling
|
| 95 |
+
`--segmentation-run-sam2` additionally requires:
|
| 96 |
+
|
| 97 |
+
- SAM2 weights (`SEGMENTATION_SAM2_CKPT`)
|
| 98 |
+
- the matching Hydra config (`SEGMENTATION_SAM2_CFG`)
|
| 99 |
+
- the official OneThinker `seg_post_sam2.py`
|
| 100 |
+
(`SEGMENTATION_POSTPROCESSOR_PATH`)
|
| 101 |
+
- the `sam2` Python package
|
| 102 |
+
|
| 103 |
+
OraRL validates all three paths before launch.
|
| 104 |
+
|
| 105 |
+
## 5. Preserve reportable outputs
|
| 106 |
+
|
| 107 |
+
Each task evaluator writes its native summary, then `orarl-eval` creates the
|
| 108 |
+
requested aggregate JSON with requested, completed, and missing tasks, the
|
| 109 |
+
return code, and official metrics. Outputs are grouped by paper task family
|
| 110 |
+
under `outputs/<model>/`.
|
| 111 |
+
|
| 112 |
+
For a reportable run, retain:
|
| 113 |
+
|
| 114 |
+
- the OraRL source revision
|
| 115 |
+
- the model and data revisions
|
| 116 |
+
- the exact command and aggregate summary
|
| 117 |
+
- software versions and accelerator type/count
|
| 118 |
+
- every non-default profile or CLI override
|
| 119 |
+
|
| 120 |
+
See [`../eval/README.md`](../eval/README.md) for the evaluator map, video
|
| 121 |
+
decoding backend, and checkpoint-format notes.
|
code/docs/evaluation_zh.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<p align="right"><a href="evaluation.md">English</a></p>
|
| 2 |
+
|
| 3 |
+
# 评测 Video-ORA
|
| 4 |
+
|
| 5 |
+
OraRL 使用七个发布任务族的规范配置评测任务原生答案,对应运行时位于
|
| 6 |
+
`eval/task/`。运行前请先
|
| 7 |
+
[验证评测节点](environment_zh.md#验证仅用于评测的节点)。
|
| 8 |
+
|
| 9 |
+
## 1. 准备模型和数据
|
| 10 |
+
|
| 11 |
+
安装 Hugging Face CLI 并下载发布文件:
|
| 12 |
+
|
| 13 |
+
```bash
|
| 14 |
+
python -m pip install -U huggingface_hub
|
| 15 |
+
|
| 16 |
+
hf download OraRL/Video-ORA-9B \
|
| 17 |
+
--local-dir "$PWD/models/Video-ORA-9B"
|
| 18 |
+
|
| 19 |
+
hf download OraRL/OraRL-Data \
|
| 20 |
+
--repo-type dataset \
|
| 21 |
+
--include "OraRL-eval-data/**" \
|
| 22 |
+
--local-dir "$PWD/OraRL-Data"
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
完整评测数据体积较大,下载支持断点续传;
|
| 26 |
+
`OraRL-eval-data/assets.jsonl` 是权威文件清单。
|
| 27 |
+
|
| 28 |
+
本地数据结构为:
|
| 29 |
+
|
| 30 |
+
```text
|
| 31 |
+
OraRL-Data/OraRL-eval-data/
|
| 32 |
+
├── datasets.jsonl # benchmark、prompt、parser、metric 与采样配置
|
| 33 |
+
├── assets.jsonl # 发布文件清单
|
| 34 |
+
├── annotations/ # 规范化 JSONL 标注
|
| 35 |
+
└── media/ # 原始图像、视频和字幕
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
发布数据有意排除衍生预处理缓存;不存在兼容缓存时,评测器会直接解码清单中声明的
|
| 39 |
+
原始媒体。
|
| 40 |
+
|
| 41 |
+
## 2. 运行论文规范配置
|
| 42 |
+
|
| 43 |
+
先预览解析后的命令:
|
| 44 |
+
|
| 45 |
+
```bash
|
| 46 |
+
orarl-eval \
|
| 47 |
+
--model "$PWD/models/Video-ORA-9B" \
|
| 48 |
+
--tasks paper \
|
| 49 |
+
--dataset "$PWD/OraRL-Data/OraRL-eval-data" \
|
| 50 |
+
--summary "$PWD/outputs/Video-ORA-9B/evaluation.json"
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
`orarl-eval` 默认为 dry run。检查模型、数据、评测器、任务配置和输出路径后,
|
| 54 |
+
增加 `--run`。
|
| 55 |
+
|
| 56 |
+
可先运行限制样本数的 smoke test:
|
| 57 |
+
|
| 58 |
+
```bash
|
| 59 |
+
orarl-eval \
|
| 60 |
+
--model "$PWD/models/Video-ORA-9B" \
|
| 61 |
+
--tasks videomme \
|
| 62 |
+
--dataset "$PWD/OraRL-Data/OraRL-eval-data" \
|
| 63 |
+
--max-samples 8 \
|
| 64 |
+
--summary "$PWD/outputs/Video-ORA-9B/videomme-smoke.json" \
|
| 65 |
+
--run
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
Smoke test 分数仅用于验证流程,不应作为 benchmark 结果报告。
|
| 69 |
+
|
| 70 |
+
## 3. 组合任务集
|
| 71 |
+
|
| 72 |
+
`--tasks paper` 选择所有发布任务,`--tasks video_qa` 选择 7 个视频问答
|
| 73 |
+
benchmark;多个单项任务可用逗号分隔。
|
| 74 |
+
|
| 75 |
+
| 任务族 | 任务名 |
|
| 76 |
+
| --- | --- |
|
| 77 |
+
| 视频问答 | `videomme`, `videommev2`, `mvbench`, `mmvu`, `videoholmes`, `longvideobench`, `mlvu` |
|
| 78 |
+
| 空间智能 | `vsi`, `mmsi`, `mindcube`, `revsi` |
|
| 79 |
+
| 时序定位 | `temporal_grounding` |
|
| 80 |
+
| 空间定位 | `spatial_grounding` |
|
| 81 |
+
| 跟踪 | `tracking` |
|
| 82 |
+
| 时空定位 | `stvg` |
|
| 83 |
+
| 分割 | `segmentation` |
|
| 84 |
+
|
| 85 |
+
`datasets.jsonl` 中的规范配置固定了帧采样、分辨率、prompt、parser 和 metric;
|
| 86 |
+
修改这些值即代表不同的评测设置。ReVSI 单独报告,不计入三个 benchmark 的空间
|
| 87 |
+
智能平均分。
|
| 88 |
+
|
| 89 |
+
## 4. 添加分割后处理
|
| 90 |
+
|
| 91 |
+
分割推理默认不执行 SAM2 后处理。启用 `--segmentation-run-sam2` 时还需要:
|
| 92 |
+
|
| 93 |
+
- SAM2 权重(`SEGMENTATION_SAM2_CKPT`)
|
| 94 |
+
- 匹配的 Hydra 配置(`SEGMENTATION_SAM2_CFG`)
|
| 95 |
+
- OneThinker 官方 `seg_post_sam2.py`
|
| 96 |
+
(`SEGMENTATION_POSTPROCESSOR_PATH`)
|
| 97 |
+
- `sam2` Python 包
|
| 98 |
+
|
| 99 |
+
OraRL 会在启动前检查三个路径。
|
| 100 |
+
|
| 101 |
+
## 5. 保存可报告的输出
|
| 102 |
+
|
| 103 |
+
每个任务评测器先写入原生 summary,随后 `orarl-eval` 生成指定的聚合 JSON,
|
| 104 |
+
其中包含请求、完成和缺失的任务、返回码以及官方 metric。输出按论文任务族组织在
|
| 105 |
+
`outputs/<model>/` 下。
|
| 106 |
+
|
| 107 |
+
正式报告结果时请保留:
|
| 108 |
+
|
| 109 |
+
- OraRL 源码版本
|
| 110 |
+
- 模型和数据版本
|
| 111 |
+
- 完整命令与聚合 summary
|
| 112 |
+
- 软件版本和加速卡类型/数量
|
| 113 |
+
- 所有非默认配置或命令行覆盖项
|
| 114 |
+
|
| 115 |
+
评测器映射、视频解码后端和 checkpoint 格式说明见
|
| 116 |
+
[`../eval/README.md`](../eval/README.md)。
|
code/docs/training.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<p align="right"><a href="training_zh.md">简体中文</a></p>
|
| 2 |
+
|
| 3 |
+
# Train with OraRL
|
| 4 |
+
|
| 5 |
+
This workflow turns licensed source records into an auditable GRPO or OraRL
|
| 6 |
+
run. Complete the
|
| 7 |
+
[pinned environment installation](environment.md#install-the-pinned-stack)
|
| 8 |
+
first.
|
| 9 |
+
|
| 10 |
+
`orarl-train` is the public launch boundary: it validates paths and overrides,
|
| 11 |
+
resolves one of the released recipes, and then starts the trainer bundled with
|
| 12 |
+
this checkout. No second runtime repository is required.
|
| 13 |
+
|
| 14 |
+
## 1. Build the training manifest
|
| 15 |
+
|
| 16 |
+
The prepared training-data release will be uploaded separately and is not
|
| 17 |
+
included in this Git repository yet. Until it is available, obtain each
|
| 18 |
+
annotation and media source under its upstream license, then copy the example
|
| 19 |
+
manifest:
|
| 20 |
+
|
| 21 |
+
```bash
|
| 22 |
+
cp configs/data_sources.example.yaml ./data_sources.local.yaml
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
Replace every `../local_data` placeholder with a licensed local path. Relative
|
| 26 |
+
paths are resolved from the manifest location. Each source declares its input
|
| 27 |
+
annotations, task, family, quota, media root, and optional license/source-page
|
| 28 |
+
metadata.
|
| 29 |
+
|
| 30 |
+
The public example reproduces the 100,032-row paper mixture:
|
| 31 |
+
|
| 32 |
+
| Family | Rows |
|
| 33 |
+
| --- | ---: |
|
| 34 |
+
| Temporal grounding | 20,096 |
|
| 35 |
+
| Tracking | 13,952 |
|
| 36 |
+
| Segmentation | 12,032 |
|
| 37 |
+
| Spatial grounding | 7,040 |
|
| 38 |
+
| Spatial-temporal grounding | 9,536 |
|
| 39 |
+
| Video QA | 20,288 |
|
| 40 |
+
| Spatial intelligence | 17,088 |
|
| 41 |
+
|
| 42 |
+
Build the deterministic train/canary split:
|
| 43 |
+
|
| 44 |
+
```bash
|
| 45 |
+
orarl-prepare \
|
| 46 |
+
--config ./data_sources.local.yaml \
|
| 47 |
+
--output ./prepared/train.jsonl \
|
| 48 |
+
--require-media
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
This writes:
|
| 52 |
+
|
| 53 |
+
```text
|
| 54 |
+
prepared/
|
| 55 |
+
├── train.jsonl
|
| 56 |
+
├── train.canary.jsonl
|
| 57 |
+
└── train.manifest.json
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
The builder validates local media, normalizes task records, enforces source
|
| 61 |
+
quotas and per-media caps, removes duplicate prompt identities, excludes
|
| 62 |
+
supplied benchmark identities, and keeps train/canary media disjoint. The audit
|
| 63 |
+
manifest records counts, shortfalls, source metadata, and SHA-256 checksums.
|
| 64 |
+
|
| 65 |
+
## 2. Choose GRPO or OraRL
|
| 66 |
+
|
| 67 |
+
| Recipe | Model scale | Method |
|
| 68 |
+
| --- | --- | --- |
|
| 69 |
+
| `grpo_4b.yaml` | 4B | GRPO baseline |
|
| 70 |
+
| `grpo_9b.yaml` | 9B | GRPO baseline |
|
| 71 |
+
| `orarl_4b.yaml` | 4B | OraRL |
|
| 72 |
+
| `orarl_9b.yaml` | 9B | OraRL |
|
| 73 |
+
|
| 74 |
+
The paper defaults use 64 prompts per rollout/update batch and eight policy
|
| 75 |
+
samples per prompt. The 100,032-row mixture therefore runs for 1,563 steps in
|
| 76 |
+
one epoch.
|
| 77 |
+
|
| 78 |
+
## 3. Preview, then launch
|
| 79 |
+
|
| 80 |
+
Set paths to a compatible local base model and the prepared data:
|
| 81 |
+
|
| 82 |
+
```bash
|
| 83 |
+
MODEL_DIR=/path/to/local/base-model
|
| 84 |
+
OUTPUT_DIR="$PWD/runs/orarl-4b"
|
| 85 |
+
|
| 86 |
+
orarl-train \
|
| 87 |
+
--config orarl_4b.yaml \
|
| 88 |
+
--model "$MODEL_DIR" \
|
| 89 |
+
--train-data "$PWD/prepared/train.jsonl" \
|
| 90 |
+
--val-data "$PWD/prepared/train.canary.jsonl" \
|
| 91 |
+
--output "$OUTPUT_DIR" \
|
| 92 |
+
--nodes 1 \
|
| 93 |
+
--gpus-per-node 8
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
The command is a dry run by default. Inspect the resolved invocation, then add
|
| 97 |
+
`--run` to start training. Use `--set KEY=VALUE` for an explicit config
|
| 98 |
+
override; retain all overrides with the run artifacts.
|
| 99 |
+
|
| 100 |
+
For a one-update smoke test:
|
| 101 |
+
|
| 102 |
+
```bash
|
| 103 |
+
orarl-train \
|
| 104 |
+
--config orarl_4b.yaml \
|
| 105 |
+
--model "$MODEL_DIR" \
|
| 106 |
+
--train-data "$PWD/prepared/train.jsonl" \
|
| 107 |
+
--val-data "$PWD/prepared/train.canary.jsonl" \
|
| 108 |
+
--output "$OUTPUT_DIR" \
|
| 109 |
+
--nodes 1 \
|
| 110 |
+
--gpus-per-node 8 \
|
| 111 |
+
--set trainer.max_steps=1 \
|
| 112 |
+
--run
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
Repeat with `grpo_4b.yaml` and a different output directory to validate the
|
| 116 |
+
baseline. Use the matching model and recipe for 9B runs.
|
| 117 |
+
|
| 118 |
+
## 4. Scale across nodes
|
| 119 |
+
|
| 120 |
+
All nodes must see the same source, model, data, and output paths:
|
| 121 |
+
|
| 122 |
+
```bash
|
| 123 |
+
HOSTS=node-a,node-b \
|
| 124 |
+
bash scripts/launch_multinode.sh \
|
| 125 |
+
--gpus-per-node 8 \
|
| 126 |
+
-- \
|
| 127 |
+
--config "$PWD/configs/orarl_4b.yaml" \
|
| 128 |
+
--model "$MODEL_DIR" \
|
| 129 |
+
--train-data "$PWD/prepared/train.jsonl" \
|
| 130 |
+
--val-data "$PWD/prepared/train.canary.jsonl" \
|
| 131 |
+
--output "$OUTPUT_DIR"
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
The launcher is also a dry run unless its own `--run` is supplied before the
|
| 135 |
+
`--` separator. SSH host-key checking is strict by default.
|
| 136 |
+
|
| 137 |
+
## 5. Accept a run
|
| 138 |
+
|
| 139 |
+
`scripts/smoke_training.sh` runs one GRPO update and one OraRL update with small
|
| 140 |
+
batches and saves a checkpoint for each:
|
| 141 |
+
|
| 142 |
+
```bash
|
| 143 |
+
bash scripts/smoke_training.sh \
|
| 144 |
+
--model "$MODEL_DIR" \
|
| 145 |
+
--train-data "$PWD/prepared/train.jsonl" \
|
| 146 |
+
--val-data "$PWD/prepared/train.canary.jsonl" \
|
| 147 |
+
--size 4b \
|
| 148 |
+
--gpus-per-node 8
|
| 149 |
+
```
|
| 150 |
+
|
| 151 |
+
Add `--dry-run` to inspect the resolved commands without allocating GPUs.
|
| 152 |
+
|
| 153 |
+
Before a full experiment, verify that:
|
| 154 |
+
|
| 155 |
+
- GRPO and OraRL each complete one update with finite rewards, losses, gradient
|
| 156 |
+
norms, and selection metrics.
|
| 157 |
+
- A checkpoint can be saved, reloaded, and used for another update.
|
| 158 |
+
- Multi-node runs form the expected Ray cluster and complete one update.
|
| 159 |
+
- The source revision, config, command, data-manifest checksum, environment
|
| 160 |
+
versions, accelerator type, and all overrides are retained.
|
| 161 |
+
|
| 162 |
+
Use [Evaluation](evaluation.md) to evaluate an exported checkpoint.
|
code/docs/training_zh.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<p align="right"><a href="training.md">English</a></p>
|
| 2 |
+
|
| 3 |
+
# 使用 OraRL 训练
|
| 4 |
+
|
| 5 |
+
本流程将已获许可的源数据整理为可审计的 GRPO 或 OraRL 训练任务。请先
|
| 6 |
+
[安装固定版本软件栈](environment_zh.md#安装固定版本软件栈)。
|
| 7 |
+
|
| 8 |
+
`orarl-train` 是公开的训练入口:它会检查路径与覆盖项、解析发布配置,再启动
|
| 9 |
+
本仓库自带的训练器,无需准备第二份运行时源码。
|
| 10 |
+
|
| 11 |
+
## 1. 构建训练清单
|
| 12 |
+
|
| 13 |
+
整理后的训练数据将稍后单独上传,目前尚未包含在本 Git 仓库中。在发布完成前,
|
| 14 |
+
请按上游许可证自行获取标注和媒体文件,然后复制示例清单:
|
| 15 |
+
|
| 16 |
+
```bash
|
| 17 |
+
cp configs/data_sources.example.yaml ./data_sources.local.yaml
|
| 18 |
+
```
|
| 19 |
+
|
| 20 |
+
将所有 `../local_data` 占位符替换为已获许可的本地路径。相对路径以清单文件所在
|
| 21 |
+
目录为基准。每个数据源需要声明标注文件、任务、任务族、配额和媒体根目录,并可
|
| 22 |
+
记录许可证与来源页面。
|
| 23 |
+
|
| 24 |
+
公开示例对应论文使用的 100,032 条训练混合:
|
| 25 |
+
|
| 26 |
+
| 任务族 | 条数 |
|
| 27 |
+
| --- | ---: |
|
| 28 |
+
| 时序定位 | 20,096 |
|
| 29 |
+
| 跟踪 | 13,952 |
|
| 30 |
+
| 分割 | 12,032 |
|
| 31 |
+
| 空间定位 | 7,040 |
|
| 32 |
+
| 时空定位 | 9,536 |
|
| 33 |
+
| 视频问答 | 20,288 |
|
| 34 |
+
| 空间智能 | 17,088 |
|
| 35 |
+
|
| 36 |
+
构建确定性的训练集和 canary 集:
|
| 37 |
+
|
| 38 |
+
```bash
|
| 39 |
+
orarl-prepare \
|
| 40 |
+
--config ./data_sources.local.yaml \
|
| 41 |
+
--output ./prepared/train.jsonl \
|
| 42 |
+
--require-media
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
输出结构为:
|
| 46 |
+
|
| 47 |
+
```text
|
| 48 |
+
prepared/
|
| 49 |
+
├── train.jsonl
|
| 50 |
+
├── train.canary.jsonl
|
| 51 |
+
└── train.manifest.json
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
构建器会检查本地媒体、规范化任务记录、执行数据源配额和单媒体上限、去除重复
|
| 55 |
+
prompt、排除给定评测集中的样本,并保证训练集与 canary 集媒体互斥。审计清单会
|
| 56 |
+
记录计数、缺额、来源信息和 SHA-256 校验值。
|
| 57 |
+
|
| 58 |
+
## 2. 选择 GRPO 或 OraRL
|
| 59 |
+
|
| 60 |
+
| 配置 | 模型规模 | 方法 |
|
| 61 |
+
| --- | --- | --- |
|
| 62 |
+
| `grpo_4b.yaml` | 4B | GRPO baseline |
|
| 63 |
+
| `grpo_9b.yaml` | 9B | GRPO baseline |
|
| 64 |
+
| `orarl_4b.yaml` | 4B | OraRL |
|
| 65 |
+
| `orarl_9b.yaml` | 9B | OraRL |
|
| 66 |
+
|
| 67 |
+
论文默认每个 rollout/update batch 使用 64 个 prompt,每个 prompt 采样 8 个策略
|
| 68 |
+
回复,因此 100,032 条混合数据在一个 epoch 中对应 1,563 步。
|
| 69 |
+
|
| 70 |
+
## 3. 预览并启动
|
| 71 |
+
|
| 72 |
+
设置兼容的本地基础模型和已准备的数据路径:
|
| 73 |
+
|
| 74 |
+
```bash
|
| 75 |
+
MODEL_DIR=/path/to/local/base-model
|
| 76 |
+
OUTPUT_DIR="$PWD/runs/orarl-4b"
|
| 77 |
+
|
| 78 |
+
orarl-train \
|
| 79 |
+
--config orarl_4b.yaml \
|
| 80 |
+
--model "$MODEL_DIR" \
|
| 81 |
+
--train-data "$PWD/prepared/train.jsonl" \
|
| 82 |
+
--val-data "$PWD/prepared/train.canary.jsonl" \
|
| 83 |
+
--output "$OUTPUT_DIR" \
|
| 84 |
+
--nodes 1 \
|
| 85 |
+
--gpus-per-node 8
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
命令默认只执行 dry run。检查解析后的调用后,增加 `--run` 开始训练。可通过
|
| 89 |
+
`--set KEY=VALUE` 显式覆盖配置,并应随运行产物保留全部覆盖项。
|
| 90 |
+
|
| 91 |
+
单步 smoke test:
|
| 92 |
+
|
| 93 |
+
```bash
|
| 94 |
+
orarl-train \
|
| 95 |
+
--config orarl_4b.yaml \
|
| 96 |
+
--model "$MODEL_DIR" \
|
| 97 |
+
--train-data "$PWD/prepared/train.jsonl" \
|
| 98 |
+
--val-data "$PWD/prepared/train.canary.jsonl" \
|
| 99 |
+
--output "$OUTPUT_DIR" \
|
| 100 |
+
--nodes 1 \
|
| 101 |
+
--gpus-per-node 8 \
|
| 102 |
+
--set trainer.max_steps=1 \
|
| 103 |
+
--run
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
再使用 `grpo_4b.yaml` 和独立输出目录验证 baseline。9B 任务需使用匹配的模型与
|
| 107 |
+
配置。
|
| 108 |
+
|
| 109 |
+
## 4. 扩展到多节点
|
| 110 |
+
|
| 111 |
+
所有节点必须看到相同的源码、模型、数据和输出路径:
|
| 112 |
+
|
| 113 |
+
```bash
|
| 114 |
+
HOSTS=node-a,node-b \
|
| 115 |
+
bash scripts/launch_multinode.sh \
|
| 116 |
+
--gpus-per-node 8 \
|
| 117 |
+
-- \
|
| 118 |
+
--config "$PWD/configs/orarl_4b.yaml" \
|
| 119 |
+
--model "$MODEL_DIR" \
|
| 120 |
+
--train-data "$PWD/prepared/train.jsonl" \
|
| 121 |
+
--val-data "$PWD/prepared/train.canary.jsonl" \
|
| 122 |
+
--output "$OUTPUT_DIR"
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
启动器同样默认为 dry run;需要在 `--` 分隔符前加入其自身的 `--run`。SSH
|
| 126 |
+
主机密钥检查默认使用严格模式。
|
| 127 |
+
|
| 128 |
+
## 5. 验收一次运行
|
| 129 |
+
|
| 130 |
+
`scripts/smoke_training.sh` 会以较小的 batch 分别跑一步 GRPO 和一步 OraRL,
|
| 131 |
+
并各保存一个 checkpoint:
|
| 132 |
+
|
| 133 |
+
```bash
|
| 134 |
+
bash scripts/smoke_training.sh \
|
| 135 |
+
--model "$MODEL_DIR" \
|
| 136 |
+
--train-data "$PWD/prepared/train.jsonl" \
|
| 137 |
+
--val-data "$PWD/prepared/train.canary.jsonl" \
|
| 138 |
+
--size 4b \
|
| 139 |
+
--gpus-per-node 8
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
加上 `--dry-run` 可在不占用 GPU 的情况下检查解析后的命令。
|
| 143 |
+
|
| 144 |
+
正式实验前确认:
|
| 145 |
+
|
| 146 |
+
- GRPO 和 OraRL 均可完成一步更新,reward、loss、梯度范数和选择指标均为有限值。
|
| 147 |
+
- checkpoint 可保存、重新加载并继续完成一步更新。
|
| 148 |
+
- 多节点任务能组成预期的 Ray 集群并完成一步更新。
|
| 149 |
+
- 保存源码版本、配置、命令、数据清单校验值、环境版本、加速卡类型和所有覆盖项。
|
| 150 |
+
|
| 151 |
+
使用[评测文档](evaluation_zh.md)评估导出的 checkpoint。
|
code/environment.yml
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Base Conda layer. Use scripts/create_conda_env.sh so CUDA wheels and editable
|
| 2 |
+
# packages are installed in the required order.
|
| 3 |
+
name: orarl
|
| 4 |
+
|
| 5 |
+
channels:
|
| 6 |
+
- nvidia
|
| 7 |
+
- conda-forge
|
| 8 |
+
- defaults
|
| 9 |
+
|
| 10 |
+
dependencies:
|
| 11 |
+
- python=3.11
|
| 12 |
+
- pip
|
| 13 |
+
- setuptools>=77
|
| 14 |
+
- wheel
|
| 15 |
+
- packaging
|
| 16 |
+
- ninja
|
| 17 |
+
- ffmpeg=7
|
| 18 |
+
- cuda-toolkit=12.9
|
code/eval/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Evaluation runtime
|
| 2 |
+
|
| 3 |
+
`eval/task/` holds the evaluators that produce every number in the paper. They
|
| 4 |
+
ship with this repository, so a single clone can reproduce the full suite.
|
| 5 |
+
`orarl-eval` is the supported entry point: it resolves the canonical dataset
|
| 6 |
+
layout into per-task environment variables and then runs `eval/task/eval.sh`.
|
| 7 |
+
|
| 8 |
+
```bash
|
| 9 |
+
orarl-eval \
|
| 10 |
+
--model /path/to/exported-model \
|
| 11 |
+
--tasks paper \
|
| 12 |
+
--dataset OraRL/OraRL-Data \
|
| 13 |
+
--summary ./evaluation.json \
|
| 14 |
+
--run
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
Without `--run` the command prints the resolved plan and exits, which is the
|
| 18 |
+
fastest way to confirm data and model paths before allocating GPUs.
|
| 19 |
+
`eval/task/eval.sh` can also be called directly when you want to bypass the
|
| 20 |
+
canonical dataset resolution and set the per-task variables yourself.
|
| 21 |
+
|
| 22 |
+
## Task map
|
| 23 |
+
|
| 24 |
+
| Family | Benchmarks | Entry point |
|
| 25 |
+
| --- | --- | --- |
|
| 26 |
+
| Video QA | VideoMME, VideoMME-v2, MVBench, MMVU, Video-Holmes, LongVideoBench, MLVU | `eval_vllm.py` |
|
| 27 |
+
| Spatial intelligence | VSI-Bench, MindCube, MMSI-Bench, ReVSI | `eval_vllm.py`, `mmsi/eval_mmsi_transformers.py`, `revsi/eval_revsi_vllm.py` |
|
| 28 |
+
| Temporal grounding | TimeLens (Charades, ActivityNet, QVHighlights) | `temporal_grounding/eval_timelens_hf.py` |
|
| 29 |
+
| Spatial grounding | RefCOCO, RefCOCO+, RefCOCOg | `spatial_grounding/eval_refcoco_vllm.py` |
|
| 30 |
+
| Tracking | GOT-10k | `tracking/eval_tracking_vllm.py` |
|
| 31 |
+
| Spatio-temporal grounding | STVG | `spatial_temporal_grounding/eval_stvg_vllm.py` |
|
| 32 |
+
| Segmentation | RefCOCO series, MeViS, ReasonVOS | `segmentation/eval_seg_vllm.py` plus `segmentation/post_sam2.py` |
|
| 33 |
+
|
| 34 |
+
`eval_prompt.py` is the single source of truth for prompts, and
|
| 35 |
+
`canonical_data.py` adapts the canonical `ORARL_EVAL_*` layout for every
|
| 36 |
+
evaluator. MMSI-Bench runs through Transformers rather than vLLM; the other
|
| 37 |
+
families run through vLLM.
|
| 38 |
+
|
| 39 |
+
## Assets you must obtain separately
|
| 40 |
+
|
| 41 |
+
Annotations and media come from the `OraRL/OraRL-Data` dataset repository, and
|
| 42 |
+
model weights come from the released checkpoints. Neither is vendored here.
|
| 43 |
+
|
| 44 |
+
Segmentation additionally needs three inputs, all supplied by you under their
|
| 45 |
+
upstream licenses:
|
| 46 |
+
|
| 47 |
+
- SAM2 weights and the matching Hydra config, passed as
|
| 48 |
+
`SEGMENTATION_SAM2_CKPT` and `SEGMENTATION_SAM2_CFG`.
|
| 49 |
+
- The official OneThinker `seg_post_sam2.py`, passed as
|
| 50 |
+
`SEGMENTATION_POSTPROCESSOR_PATH`. `segmentation/post_sam2.py` is a thin
|
| 51 |
+
wrapper that injects paths into it so mask metrics stay identical to the
|
| 52 |
+
upstream implementation.
|
| 53 |
+
- The `sam2` Python package.
|
| 54 |
+
|
| 55 |
+
`orarl-eval` refuses to start segmentation with `--segmentation-run-sam2`
|
| 56 |
+
unless all three paths exist.
|
| 57 |
+
|
| 58 |
+
## Video decoding
|
| 59 |
+
|
| 60 |
+
Video evaluators pin the decord backend through `FORCE_QWENVL_VIDEO_READER`.
|
| 61 |
+
`eval/task/qwenvl_decord_patch.py` replaces `qwen_vl_utils.fetch_video` so the
|
| 62 |
+
backend is honoured across `qwen_vl_utils` releases that otherwise hard-code
|
| 63 |
+
torchvision. The backend is not cosmetic: switching MeViS and ReasonVOS from
|
| 64 |
+
torchcodec to decord moved MeViS J&F from 56.7 to 60.6. Set
|
| 65 |
+
`SEGMENTATION_VIDEO_READER` to `torchcodec` or `torchvision` only when
|
| 66 |
+
deliberately measuring that difference.
|
| 67 |
+
|
| 68 |
+
## Checkpoint format
|
| 69 |
+
|
| 70 |
+
Exported Hugging Face checkpoints run as-is. Pass `--force-merge` when the model
|
| 71 |
+
path is a sharded FSDP actor directory; `eval/task/eval.sh` then merges it with
|
| 72 |
+
`scripts/model_merger.py` before inference.
|
code/eval/task/canonical_data.py
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Dependency-light compatibility adapter for canonical OraRL evaluation rows."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import copy
|
| 6 |
+
import json
|
| 7 |
+
import os
|
| 8 |
+
import re
|
| 9 |
+
from collections.abc import Mapping, Sequence
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import Any
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
_CANONICAL_FIELDS = {
|
| 15 |
+
"schema_version",
|
| 16 |
+
"eval_task",
|
| 17 |
+
"sample_id",
|
| 18 |
+
"benchmark",
|
| 19 |
+
"split",
|
| 20 |
+
"problem",
|
| 21 |
+
"answer",
|
| 22 |
+
"images",
|
| 23 |
+
"videos",
|
| 24 |
+
"problem_type",
|
| 25 |
+
"source",
|
| 26 |
+
}
|
| 27 |
+
_MEDIA_PATH_KEYS = (
|
| 28 |
+
"path",
|
| 29 |
+
"video",
|
| 30 |
+
"image",
|
| 31 |
+
"video_path",
|
| 32 |
+
"image_path",
|
| 33 |
+
"file_name",
|
| 34 |
+
)
|
| 35 |
+
_PATH_KEY_RE = re.compile(
|
| 36 |
+
r"(?:^|_)(?:artifact|artifacts|cfg|checkpoint|ckpt|dir|directory|file|"
|
| 37 |
+
r"files|image|images|media|path|paths|prefix|root|subtitle|"
|
| 38 |
+
r"subtitles|tensor|tensors|video|videos)$"
|
| 39 |
+
)
|
| 40 |
+
_GROUND_TRUTH_KEYS = (
|
| 41 |
+
"bbox",
|
| 42 |
+
"bboxes",
|
| 43 |
+
"boxes",
|
| 44 |
+
"coordinates",
|
| 45 |
+
"duration",
|
| 46 |
+
"end",
|
| 47 |
+
"frame",
|
| 48 |
+
"frame_id",
|
| 49 |
+
"frame_ids",
|
| 50 |
+
"gt_bbox",
|
| 51 |
+
"mask",
|
| 52 |
+
"masks",
|
| 53 |
+
"negative_points",
|
| 54 |
+
"normalized_solution",
|
| 55 |
+
"object",
|
| 56 |
+
"positive_points",
|
| 57 |
+
"query",
|
| 58 |
+
"segment",
|
| 59 |
+
"segmentation",
|
| 60 |
+
"span",
|
| 61 |
+
"spans",
|
| 62 |
+
"start",
|
| 63 |
+
"target",
|
| 64 |
+
"time",
|
| 65 |
+
"timestamp",
|
| 66 |
+
"timestamps",
|
| 67 |
+
"trajectory",
|
| 68 |
+
)
|
| 69 |
+
_ASSET_DIRECTORIES = frozenset({"artifacts", "media"})
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class CanonicalDataError(ValueError):
|
| 73 |
+
"""Raised when a canonical row cannot be safely adapted."""
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def is_canonical_row(record: Any) -> bool:
|
| 77 |
+
"""Return whether a mapping uses the canonical evaluation-row contract."""
|
| 78 |
+
|
| 79 |
+
return (
|
| 80 |
+
isinstance(record, Mapping)
|
| 81 |
+
and record.get("schema_version") == 1
|
| 82 |
+
and _CANONICAL_FIELDS.issubset(record)
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def _configured_root() -> Path | None:
|
| 87 |
+
value = os.environ.get("ORARL_EVAL_DATA_ROOT", "").strip()
|
| 88 |
+
if value:
|
| 89 |
+
return Path(value).expanduser().resolve()
|
| 90 |
+
manifest = os.environ.get("ORARL_EVAL_DATASETS_JSONL", "").strip()
|
| 91 |
+
if manifest:
|
| 92 |
+
return Path(manifest).expanduser().resolve().parent
|
| 93 |
+
return None
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def _configured_asset_root() -> Path | None:
|
| 97 |
+
value = os.environ.get("ORARL_EVAL_ASSET_ROOT", "").strip()
|
| 98 |
+
return Path(value).expanduser().resolve() if value else None
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def _infer_root(input_path: Path) -> Path | None:
|
| 102 |
+
configured = _configured_root()
|
| 103 |
+
if configured is not None:
|
| 104 |
+
return configured
|
| 105 |
+
for candidate in (input_path.parent, *input_path.parents):
|
| 106 |
+
if (candidate / "datasets.jsonl").is_file():
|
| 107 |
+
return candidate.resolve()
|
| 108 |
+
return None
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def canonical_dataset_record(
|
| 112 |
+
eval_task: str,
|
| 113 |
+
split: str,
|
| 114 |
+
root: str | os.PathLike[str] | None = None,
|
| 115 |
+
) -> dict[str, Any] | None:
|
| 116 |
+
"""Return one canonical ``datasets.jsonl`` record for a task split."""
|
| 117 |
+
|
| 118 |
+
repository_root = (
|
| 119 |
+
Path(root).expanduser().resolve() if root is not None else _configured_root()
|
| 120 |
+
)
|
| 121 |
+
if repository_root is None:
|
| 122 |
+
return None
|
| 123 |
+
manifest = repository_root / "datasets.jsonl"
|
| 124 |
+
if not manifest.is_file():
|
| 125 |
+
return None
|
| 126 |
+
|
| 127 |
+
normalized_split = str(split).strip().casefold().replace("-", "_")
|
| 128 |
+
matches: list[dict[str, Any]] = []
|
| 129 |
+
try:
|
| 130 |
+
with manifest.open("r", encoding="utf-8") as handle:
|
| 131 |
+
for line_number, line in enumerate(handle, start=1):
|
| 132 |
+
if not line.strip():
|
| 133 |
+
continue
|
| 134 |
+
try:
|
| 135 |
+
record = json.loads(line)
|
| 136 |
+
except json.JSONDecodeError as error:
|
| 137 |
+
raise CanonicalDataError(
|
| 138 |
+
f"{manifest}:{line_number}: invalid JSON: {error}"
|
| 139 |
+
) from error
|
| 140 |
+
if not isinstance(record, Mapping):
|
| 141 |
+
raise CanonicalDataError(
|
| 142 |
+
f"{manifest}:{line_number}: dataset record must be an object"
|
| 143 |
+
)
|
| 144 |
+
record_task = str(record.get("task") or "").casefold()
|
| 145 |
+
record_split = str(record.get("split") or "").casefold()
|
| 146 |
+
if record_task == str(eval_task).casefold() and record_split == normalized_split:
|
| 147 |
+
matches.append(copy.deepcopy(dict(record)))
|
| 148 |
+
except OSError as error:
|
| 149 |
+
raise CanonicalDataError(f"cannot read canonical dataset manifest: {error}") from error
|
| 150 |
+
if len(matches) > 1:
|
| 151 |
+
raise CanonicalDataError(
|
| 152 |
+
f"duplicate canonical dataset profile for {eval_task}/{normalized_split}"
|
| 153 |
+
)
|
| 154 |
+
return matches[0] if matches else None
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def resolve_repository_path(
|
| 158 |
+
value: str,
|
| 159 |
+
root: str | os.PathLike[str],
|
| 160 |
+
asset_root: str | os.PathLike[str] | None = None,
|
| 161 |
+
) -> str:
|
| 162 |
+
"""Resolve one repository-relative canonical path without allowing escape."""
|
| 163 |
+
|
| 164 |
+
raw = str(value).strip()
|
| 165 |
+
if not raw:
|
| 166 |
+
raise CanonicalDataError("canonical repository path must be nonempty")
|
| 167 |
+
repository_root = Path(root).expanduser().resolve()
|
| 168 |
+
configured_asset_root = (
|
| 169 |
+
Path(asset_root).expanduser().resolve()
|
| 170 |
+
if asset_root is not None
|
| 171 |
+
else _configured_asset_root()
|
| 172 |
+
)
|
| 173 |
+
candidate = Path(raw).expanduser()
|
| 174 |
+
selected_root = repository_root
|
| 175 |
+
if (
|
| 176 |
+
not candidate.is_absolute()
|
| 177 |
+
and candidate.parts
|
| 178 |
+
and candidate.parts[0].casefold() in _ASSET_DIRECTORIES
|
| 179 |
+
and configured_asset_root is not None
|
| 180 |
+
):
|
| 181 |
+
selected_root = configured_asset_root
|
| 182 |
+
resolved = (
|
| 183 |
+
candidate.resolve()
|
| 184 |
+
if candidate.is_absolute()
|
| 185 |
+
else (selected_root / candidate).resolve()
|
| 186 |
+
)
|
| 187 |
+
allowed_roots = [repository_root]
|
| 188 |
+
if configured_asset_root is not None:
|
| 189 |
+
allowed_roots.append(configured_asset_root)
|
| 190 |
+
try:
|
| 191 |
+
next(root for root in allowed_roots if resolved.is_relative_to(root))
|
| 192 |
+
except StopIteration as error:
|
| 193 |
+
raise CanonicalDataError(
|
| 194 |
+
f"canonical path resolves outside configured evaluation roots: {raw}"
|
| 195 |
+
) from error
|
| 196 |
+
return os.path.normpath(str(resolved))
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def repository_relative_output_path(
|
| 200 |
+
value: str,
|
| 201 |
+
root: str | os.PathLike[str],
|
| 202 |
+
) -> str:
|
| 203 |
+
"""Return a ``./``-prefixed path when an asset is contained by ``root``."""
|
| 204 |
+
|
| 205 |
+
raw = str(value).strip()
|
| 206 |
+
if not raw or not str(root).strip():
|
| 207 |
+
return raw
|
| 208 |
+
candidate = Path(raw).expanduser()
|
| 209 |
+
if not candidate.is_absolute():
|
| 210 |
+
return raw
|
| 211 |
+
repository_root = Path(root).expanduser().resolve()
|
| 212 |
+
resolved = candidate.resolve()
|
| 213 |
+
try:
|
| 214 |
+
relative = resolved.relative_to(repository_root)
|
| 215 |
+
except ValueError:
|
| 216 |
+
return raw
|
| 217 |
+
return f"./{relative.as_posix()}"
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
def _is_path_key(value: object) -> bool:
|
| 221 |
+
return isinstance(value, str) and bool(_PATH_KEY_RE.search(value.casefold()))
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def _resolve_media_entry(value: Any, root: Path, asset_root: Path | None) -> Any:
|
| 225 |
+
if isinstance(value, str):
|
| 226 |
+
return resolve_repository_path(value, root, asset_root)
|
| 227 |
+
if not isinstance(value, Mapping):
|
| 228 |
+
return copy.deepcopy(value)
|
| 229 |
+
resolved = copy.deepcopy(dict(value))
|
| 230 |
+
for key in _MEDIA_PATH_KEYS:
|
| 231 |
+
path = resolved.get(key)
|
| 232 |
+
if isinstance(path, str) and path.strip():
|
| 233 |
+
resolved[key] = resolve_repository_path(path, root, asset_root)
|
| 234 |
+
return resolved
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def _resolve_declared_paths(
|
| 238 |
+
value: Any,
|
| 239 |
+
root: Path,
|
| 240 |
+
asset_root: Path | None,
|
| 241 |
+
path_context: bool = False,
|
| 242 |
+
) -> Any:
|
| 243 |
+
if isinstance(value, str):
|
| 244 |
+
return (
|
| 245 |
+
resolve_repository_path(value, root, asset_root)
|
| 246 |
+
if path_context
|
| 247 |
+
else value
|
| 248 |
+
)
|
| 249 |
+
if isinstance(value, Mapping):
|
| 250 |
+
return {
|
| 251 |
+
key: _resolve_declared_paths(
|
| 252 |
+
item,
|
| 253 |
+
root,
|
| 254 |
+
asset_root,
|
| 255 |
+
path_context=path_context or _is_path_key(key),
|
| 256 |
+
)
|
| 257 |
+
for key, item in value.items()
|
| 258 |
+
}
|
| 259 |
+
if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
|
| 260 |
+
return [
|
| 261 |
+
_resolve_declared_paths(
|
| 262 |
+
item,
|
| 263 |
+
root,
|
| 264 |
+
asset_root,
|
| 265 |
+
path_context=path_context,
|
| 266 |
+
)
|
| 267 |
+
for item in value
|
| 268 |
+
]
|
| 269 |
+
return copy.deepcopy(value)
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
def _media_path(value: Any) -> str:
|
| 273 |
+
if isinstance(value, str):
|
| 274 |
+
return value
|
| 275 |
+
if isinstance(value, Mapping):
|
| 276 |
+
for key in _MEDIA_PATH_KEYS:
|
| 277 |
+
candidate = value.get(key)
|
| 278 |
+
if isinstance(candidate, str) and candidate:
|
| 279 |
+
return candidate
|
| 280 |
+
return ""
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
def _declared_path_values(value: Any, path_context: bool = False) -> list[str]:
|
| 284 |
+
if isinstance(value, str):
|
| 285 |
+
return [value] if path_context else []
|
| 286 |
+
if isinstance(value, Mapping):
|
| 287 |
+
paths: list[str] = []
|
| 288 |
+
for key, item in value.items():
|
| 289 |
+
paths.extend(
|
| 290 |
+
_declared_path_values(
|
| 291 |
+
item,
|
| 292 |
+
path_context=path_context or _is_path_key(key),
|
| 293 |
+
)
|
| 294 |
+
)
|
| 295 |
+
return paths
|
| 296 |
+
if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
|
| 297 |
+
paths = []
|
| 298 |
+
for item in value:
|
| 299 |
+
paths.extend(_declared_path_values(item, path_context=path_context))
|
| 300 |
+
return paths
|
| 301 |
+
return []
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
def adapt_canonical_row(
|
| 305 |
+
record: Mapping[str, Any],
|
| 306 |
+
root: str | os.PathLike[str] | None = None,
|
| 307 |
+
asset_root: str | os.PathLike[str] | None = None,
|
| 308 |
+
) -> dict[str, Any]:
|
| 309 |
+
"""Resolve canonical paths and add non-destructive legacy aliases."""
|
| 310 |
+
|
| 311 |
+
if not is_canonical_row(record):
|
| 312 |
+
return copy.deepcopy(dict(record))
|
| 313 |
+
repository_root = (
|
| 314 |
+
Path(root).expanduser().resolve() if root is not None else _configured_root()
|
| 315 |
+
)
|
| 316 |
+
if repository_root is None:
|
| 317 |
+
raise CanonicalDataError(
|
| 318 |
+
"canonical rows require ORARL_EVAL_DATA_ROOT or an explicit root"
|
| 319 |
+
)
|
| 320 |
+
resolved_asset_root = (
|
| 321 |
+
Path(asset_root).expanduser().resolve()
|
| 322 |
+
if asset_root is not None
|
| 323 |
+
else _configured_asset_root()
|
| 324 |
+
)
|
| 325 |
+
|
| 326 |
+
adapted = copy.deepcopy(dict(record))
|
| 327 |
+
for field in ("images", "videos", "subtitles"):
|
| 328 |
+
values = adapted.get(field)
|
| 329 |
+
if isinstance(values, list):
|
| 330 |
+
adapted[field] = [
|
| 331 |
+
_resolve_media_entry(value, repository_root, resolved_asset_root)
|
| 332 |
+
for value in values
|
| 333 |
+
]
|
| 334 |
+
for field in ("preprocessed", "task_payload", "metadata", "evaluation"):
|
| 335 |
+
value = adapted.get(field)
|
| 336 |
+
if isinstance(value, Mapping):
|
| 337 |
+
adapted[field] = _resolve_declared_paths(
|
| 338 |
+
value,
|
| 339 |
+
repository_root,
|
| 340 |
+
resolved_asset_root,
|
| 341 |
+
)
|
| 342 |
+
|
| 343 |
+
image_paths = [
|
| 344 |
+
path for value in adapted.get("images", []) if (path := _media_path(value))
|
| 345 |
+
]
|
| 346 |
+
video_paths = [
|
| 347 |
+
path for value in adapted.get("videos", []) if (path := _media_path(value))
|
| 348 |
+
]
|
| 349 |
+
subtitle_paths = [
|
| 350 |
+
path
|
| 351 |
+
for value in adapted.get("subtitles", [])
|
| 352 |
+
if (path := _media_path(value))
|
| 353 |
+
]
|
| 354 |
+
|
| 355 |
+
adapted.setdefault("id", adapted["sample_id"])
|
| 356 |
+
adapted.setdefault("problem_id", adapted["sample_id"])
|
| 357 |
+
adapted.setdefault("question_id", adapted["sample_id"])
|
| 358 |
+
adapted.setdefault("question", adapted["problem"])
|
| 359 |
+
adapted.setdefault("prompt", adapted["problem"])
|
| 360 |
+
adapted.setdefault("ground_truth", adapted["answer"])
|
| 361 |
+
adapted.setdefault("solution", adapted["answer"])
|
| 362 |
+
adapted.setdefault("dataset", adapted["benchmark"])
|
| 363 |
+
adapted.setdefault("options", copy.deepcopy(adapted.get("choices", [])))
|
| 364 |
+
adapted.setdefault("image_list", image_paths)
|
| 365 |
+
if image_paths:
|
| 366 |
+
adapted.setdefault("image", image_paths[0])
|
| 367 |
+
adapted.setdefault("image_path", image_paths[0])
|
| 368 |
+
if video_paths:
|
| 369 |
+
adapted.setdefault("video", video_paths[0])
|
| 370 |
+
adapted.setdefault("video_path", video_paths[0])
|
| 371 |
+
if video_paths or image_paths:
|
| 372 |
+
adapted.setdefault("path", (video_paths or image_paths)[0])
|
| 373 |
+
if subtitle_paths:
|
| 374 |
+
adapted.setdefault("subtitle", subtitle_paths[0])
|
| 375 |
+
adapted.setdefault("subtitle_path", subtitle_paths[0])
|
| 376 |
+
|
| 377 |
+
metadata = adapted.get("metadata")
|
| 378 |
+
if isinstance(metadata, Mapping):
|
| 379 |
+
for key, value in metadata.items():
|
| 380 |
+
adapted.setdefault(str(key), copy.deepcopy(value))
|
| 381 |
+
payload = adapted.get("task_payload")
|
| 382 |
+
if isinstance(payload, Mapping):
|
| 383 |
+
for key, value in payload.items():
|
| 384 |
+
adapted.setdefault(str(key), copy.deepcopy(value))
|
| 385 |
+
if isinstance(adapted.get("answer"), Mapping):
|
| 386 |
+
for key in _GROUND_TRUTH_KEYS:
|
| 387 |
+
if key in adapted["answer"]:
|
| 388 |
+
adapted.setdefault(key, copy.deepcopy(adapted["answer"][key]))
|
| 389 |
+
|
| 390 |
+
preprocessed = adapted.get("preprocessed")
|
| 391 |
+
if isinstance(preprocessed, Mapping):
|
| 392 |
+
for key, value in preprocessed.items():
|
| 393 |
+
adapted.setdefault(str(key), copy.deepcopy(value))
|
| 394 |
+
paths = _declared_path_values(preprocessed)
|
| 395 |
+
if paths:
|
| 396 |
+
adapted.setdefault("preprocessed_video", paths[0])
|
| 397 |
+
adapted.setdefault("preprocessed_video_path", paths[0])
|
| 398 |
+
|
| 399 |
+
if video_paths:
|
| 400 |
+
adapted.setdefault("data_type", "video")
|
| 401 |
+
elif image_paths:
|
| 402 |
+
adapted.setdefault("data_type", "image")
|
| 403 |
+
adapted.setdefault("question_type", adapted["problem_type"])
|
| 404 |
+
adapted.setdefault("task_type", adapted["problem_type"])
|
| 405 |
+
return adapted
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
def load_json_records(
|
| 409 |
+
path: str | os.PathLike[str],
|
| 410 |
+
*,
|
| 411 |
+
root: str | os.PathLike[str] | None = None,
|
| 412 |
+
asset_root: str | os.PathLike[str] | None = None,
|
| 413 |
+
) -> list[dict[str, Any]]:
|
| 414 |
+
"""Read JSON/JSONL and adapt only rows that use the canonical contract."""
|
| 415 |
+
|
| 416 |
+
input_path = Path(path)
|
| 417 |
+
text = input_path.read_text(encoding="utf-8")
|
| 418 |
+
if not text.strip():
|
| 419 |
+
records: list[Any] = []
|
| 420 |
+
else:
|
| 421 |
+
try:
|
| 422 |
+
payload = json.loads(text)
|
| 423 |
+
except json.JSONDecodeError:
|
| 424 |
+
records = [json.loads(line) for line in text.splitlines() if line.strip()]
|
| 425 |
+
else:
|
| 426 |
+
records = payload if isinstance(payload, list) else [payload]
|
| 427 |
+
if not all(isinstance(record, Mapping) for record in records):
|
| 428 |
+
raise CanonicalDataError(f"{input_path}: every record must be a JSON object")
|
| 429 |
+
|
| 430 |
+
repository_root = (
|
| 431 |
+
Path(root).expanduser().resolve()
|
| 432 |
+
if root is not None
|
| 433 |
+
else _infer_root(input_path)
|
| 434 |
+
)
|
| 435 |
+
return [
|
| 436 |
+
adapt_canonical_row(record, repository_root, asset_root)
|
| 437 |
+
if is_canonical_row(record)
|
| 438 |
+
else copy.deepcopy(dict(record))
|
| 439 |
+
for record in records
|
| 440 |
+
]
|
| 441 |
+
|
| 442 |
+
|
| 443 |
+
__all__ = [
|
| 444 |
+
"CanonicalDataError",
|
| 445 |
+
"adapt_canonical_row",
|
| 446 |
+
"canonical_dataset_record",
|
| 447 |
+
"is_canonical_row",
|
| 448 |
+
"load_json_records",
|
| 449 |
+
"resolve_repository_path",
|
| 450 |
+
]
|
code/eval/task/eval.sh
ADDED
|
@@ -0,0 +1,1018 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# Thin shell entry for evaluation.
|
| 3 |
+
#
|
| 4 |
+
# Supported TASKS: videomme,videommev2,videommmu,mmvu,mvbench,videoholmes,longvideobench,lvbench,mlvu,vsi,mmsi,mindcube,revsi,spatial_grounding,tracking,stvg,temporal_grounding,segmentation,all
|
| 5 |
+
# Common env overrides:
|
| 6 |
+
# MODEL_PATH=/path/to/model_or_verl_actor
|
| 7 |
+
# TASKS=videomme,videommev2,vsi
|
| 8 |
+
# GPUS=0,1,2,3,4,5,6,7
|
| 9 |
+
# TP_SIZE=1
|
| 10 |
+
# Prefer explicit CLI args for new runs, which avoids stale env variables:
|
| 11 |
+
# bash eval/task/eval.sh --model /path/to/model --tasks videomme,videommev2,mvbench --gpus 0,1,2,3,4,5,6,7 --force-merge
|
| 12 |
+
#
|
| 13 |
+
# MODEL must be provided explicitly with --model.
|
| 14 |
+
|
| 15 |
+
# # —— Video QA / MC ——
|
| 16 |
+
# bash eval/task/eval.sh --model $MODEL --tasks mvbench --gpus $GPUS --force-merge
|
| 17 |
+
# bash eval/task/eval.sh --model $MODEL --tasks mmvu --gpus $GPUS --force-merge
|
| 18 |
+
# bash eval/task/eval.sh --model $MODEL --tasks videomme --gpus $GPUS --force-merge
|
| 19 |
+
# bash eval/task/eval.sh --model $MODEL --tasks videoholmes --gpus $GPUS --force-merge
|
| 20 |
+
# bash eval/task/eval.sh --model $MODEL --tasks longvideobench --gpus $GPUS --force-merge
|
| 21 |
+
# bash eval/task/eval.sh --model $MODEL --tasks mlvu --gpus $GPUS --force-merge
|
| 22 |
+
|
| 23 |
+
# # —— Spatial intelligence (MC) ——
|
| 24 |
+
# bash eval/task/eval.sh --model $MODEL --tasks vsi --gpus $GPUS --force-merge
|
| 25 |
+
# bash eval/task/eval.sh --model $MODEL --tasks mmsi --gpus $GPUS --force-merge
|
| 26 |
+
# bash eval/task/eval.sh --model $MODEL --tasks mindcube --gpus $GPUS --force-merge
|
| 27 |
+
# bash eval/task/eval.sh --model $MODEL --tasks revsi --gpus $GPUS --force-merge
|
| 28 |
+
|
| 29 |
+
# # —— Grounding / tracking / stvg / temporal ——
|
| 30 |
+
# bash eval/task/eval.sh --model $MODEL --tasks spatial_grounding --gpus $GPUS --force-merge
|
| 31 |
+
# bash eval/task/eval.sh --model $MODEL --tasks tracking --gpus $GPUS --force-merge
|
| 32 |
+
# bash eval/task/eval.sh --model $MODEL --tasks stvg --gpus $GPUS --force-merge
|
| 33 |
+
# charades(默认)
|
| 34 |
+
# TIMELENS_DATASETS=charades-timelens TIMELENS_FPS=4 bash eval/task/eval.sh --model $MODEL --tasks temporal_grounding --gpus $GPUS --force-merge
|
| 35 |
+
# TIMELENS_DATASETS=activitynet-timelens TIMELENS_FPS=2 bash eval/task/eval.sh --model $MODEL --tasks temporal_grounding --gpus $GPUS --force-merge
|
| 36 |
+
# TIMELENS_DATASETS=qvhighlights-timelens TIMELENS_FPS=2 bash eval/task/eval.sh --model $MODEL --tasks temporal_grounding --gpus $GPUS --force-merge
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
# # —— Segmentation(需要 SAM2 + OraRL 评测环境)——
|
| 41 |
+
# EVAL_CONDA_ENV=orarl SEGMENTATION_RUN_SAM2=true \
|
| 42 |
+
# bash eval/task/eval.sh --model $MODEL --tasks segmentation --gpus $GPUS --force-merge
|
| 43 |
+
|
| 44 |
+
set -euo pipefail
|
| 45 |
+
|
| 46 |
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
| 47 |
+
PROJECT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
| 48 |
+
DEFAULT_GPUS="0,1,2,3,4,5,6,7"
|
| 49 |
+
|
| 50 |
+
usage() {
|
| 51 |
+
cat <<'EOF'
|
| 52 |
+
Usage:
|
| 53 |
+
bash eval/task/eval.sh [options]
|
| 54 |
+
|
| 55 |
+
Options:
|
| 56 |
+
--model PATH Model or checkpoint path.
|
| 57 |
+
--tasks LIST Comma-separated task list, e.g. videomme,videommev2,mvbench,mlvu.
|
| 58 |
+
--gpus LIST Comma-separated GPU ids, e.g. 0,1,2,3,4,5,6,7.
|
| 59 |
+
--tp-size N Tensor parallel size.
|
| 60 |
+
--shard-mode MODE Sharding mode passed to vLLM task runner.
|
| 61 |
+
--video-cache-size N Video cache size.
|
| 62 |
+
--videomme-max-new-tokens N
|
| 63 |
+
Max new tokens for VideoMME generation.
|
| 64 |
+
--force-merge Force merging FSDP actor checkpoints.
|
| 65 |
+
--skip-merge Reuse existing merged HF model for actor checkpoints.
|
| 66 |
+
--merged-model PATH Explicit merged HF output/input path for actor checkpoints.
|
| 67 |
+
--base-model PATH Base model path used when merging actor checkpoints.
|
| 68 |
+
--env NAME Conda env to activate. Default: keep current env.
|
| 69 |
+
--segmentation-run-sam2
|
| 70 |
+
Enable SAM2 post-processing for segmentation.
|
| 71 |
+
--help Show this help.
|
| 72 |
+
|
| 73 |
+
Core launch settings intentionally ignore inherited environment variables to
|
| 74 |
+
avoid stale MODEL_PATH/TASKS/GPUS/etc. Use explicit CLI options instead.
|
| 75 |
+
EOF
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
# Avoid stale shell environment contaminating evaluation identity. Task-specific
|
| 79 |
+
# tuning envs below are still supported, but core launch identity is CLI/defaults.
|
| 80 |
+
unset MODEL_PATH TASKS GPUS TP_SIZE SHARD_MODE VIDEO_CACHE_SIZE
|
| 81 |
+
unset FORCE_MERGE SKIP_MERGE MERGED_MODEL_PATH BASE_MODEL_PATH EVAL_CONDA_ENV
|
| 82 |
+
|
| 83 |
+
while (($# > 0)); do
|
| 84 |
+
case "$1" in
|
| 85 |
+
--model|--model-path)
|
| 86 |
+
[[ $# -ge 2 ]] || { echo "ERROR: $1 requires a value" >&2; exit 1; }
|
| 87 |
+
MODEL_PATH="$2"
|
| 88 |
+
shift 2
|
| 89 |
+
;;
|
| 90 |
+
--tasks)
|
| 91 |
+
[[ $# -ge 2 ]] || { echo "ERROR: $1 requires a value" >&2; exit 1; }
|
| 92 |
+
TASKS="$2"
|
| 93 |
+
shift 2
|
| 94 |
+
;;
|
| 95 |
+
--gpus)
|
| 96 |
+
if [[ $# -ge 2 && "$2" != --* ]]; then
|
| 97 |
+
GPUS="$2"
|
| 98 |
+
shift 2
|
| 99 |
+
else
|
| 100 |
+
GPUS="$DEFAULT_GPUS"
|
| 101 |
+
shift
|
| 102 |
+
fi
|
| 103 |
+
;;
|
| 104 |
+
--tp-size)
|
| 105 |
+
[[ $# -ge 2 ]] || { echo "ERROR: $1 requires a value" >&2; exit 1; }
|
| 106 |
+
TP_SIZE="$2"
|
| 107 |
+
shift 2
|
| 108 |
+
;;
|
| 109 |
+
--shard-mode)
|
| 110 |
+
[[ $# -ge 2 ]] || { echo "ERROR: $1 requires a value" >&2; exit 1; }
|
| 111 |
+
SHARD_MODE="$2"
|
| 112 |
+
shift 2
|
| 113 |
+
;;
|
| 114 |
+
--video-cache-size)
|
| 115 |
+
[[ $# -ge 2 ]] || { echo "ERROR: $1 requires a value" >&2; exit 1; }
|
| 116 |
+
VIDEO_CACHE_SIZE="$2"
|
| 117 |
+
shift 2
|
| 118 |
+
;;
|
| 119 |
+
--videomme-max-new-tokens)
|
| 120 |
+
[[ $# -ge 2 ]] || { echo "ERROR: $1 requires a value" >&2; exit 1; }
|
| 121 |
+
VIDEOMME_MAX_NEW_TOKENS="$2"
|
| 122 |
+
shift 2
|
| 123 |
+
;;
|
| 124 |
+
--force-merge)
|
| 125 |
+
FORCE_MERGE=1
|
| 126 |
+
shift
|
| 127 |
+
;;
|
| 128 |
+
--skip-merge)
|
| 129 |
+
SKIP_MERGE=1
|
| 130 |
+
shift
|
| 131 |
+
;;
|
| 132 |
+
--merged-model|--merged-model-path)
|
| 133 |
+
[[ $# -ge 2 ]] || { echo "ERROR: $1 requires a value" >&2; exit 1; }
|
| 134 |
+
MERGED_MODEL_PATH="$2"
|
| 135 |
+
shift 2
|
| 136 |
+
;;
|
| 137 |
+
--base-model|--base-model-path)
|
| 138 |
+
[[ $# -ge 2 ]] || { echo "ERROR: $1 requires a value" >&2; exit 1; }
|
| 139 |
+
BASE_MODEL_PATH="$2"
|
| 140 |
+
shift 2
|
| 141 |
+
;;
|
| 142 |
+
--env|--conda-env)
|
| 143 |
+
[[ $# -ge 2 ]] || { echo "ERROR: $1 requires a value" >&2; exit 1; }
|
| 144 |
+
EVAL_CONDA_ENV="$2"
|
| 145 |
+
shift 2
|
| 146 |
+
;;
|
| 147 |
+
--segmentation-run-sam2)
|
| 148 |
+
SEGMENTATION_RUN_SAM2=true
|
| 149 |
+
shift
|
| 150 |
+
;;
|
| 151 |
+
--help|-h)
|
| 152 |
+
usage
|
| 153 |
+
exit 0
|
| 154 |
+
;;
|
| 155 |
+
--)
|
| 156 |
+
shift
|
| 157 |
+
break
|
| 158 |
+
;;
|
| 159 |
+
*)
|
| 160 |
+
echo "ERROR: unknown option '$1'" >&2
|
| 161 |
+
usage >&2
|
| 162 |
+
exit 1
|
| 163 |
+
;;
|
| 164 |
+
esac
|
| 165 |
+
done
|
| 166 |
+
|
| 167 |
+
# Require an explicit model so stale machine-local defaults cannot contaminate
|
| 168 |
+
# benchmark results.
|
| 169 |
+
if [[ -z "${MODEL_PATH:-}" ]]; then
|
| 170 |
+
echo "ERROR: --model PATH is required." >&2
|
| 171 |
+
usage >&2
|
| 172 |
+
exit 2
|
| 173 |
+
fi
|
| 174 |
+
|
| 175 |
+
MODEL_PATH="$(printf '%s' "$MODEL_PATH" | xargs)"
|
| 176 |
+
|
| 177 |
+
# Common launch defaults.
|
| 178 |
+
: "${TASKS:=all}"
|
| 179 |
+
: "${GPUS:=$DEFAULT_GPUS}"
|
| 180 |
+
: "${TP_SIZE:=1}"
|
| 181 |
+
: "${SHARD_MODE:=contiguous}"
|
| 182 |
+
: "${VIDEO_CACHE_SIZE:=2}"
|
| 183 |
+
: "${PREFETCH_BATCHES:=1}"
|
| 184 |
+
: "${FORCE_MERGE:=0}"
|
| 185 |
+
: "${SKIP_MERGE:=0}"
|
| 186 |
+
: "${MERGE_LOG_DIR:=${PROJECT_DIR}/logs/qwen3.5/eval/merge}"
|
| 187 |
+
|
| 188 |
+
if [[ ! "$GPUS" =~ ^[0-9]+(,[0-9]+)*$ ]]; then
|
| 189 |
+
echo "ERROR: --gpus must be a comma-separated list of GPU ids, got '${GPUS}'" >&2
|
| 190 |
+
exit 1
|
| 191 |
+
fi
|
| 192 |
+
|
| 193 |
+
# VideoMME defaults.
|
| 194 |
+
: "${VIDEOMME_VIDEO_BASE:=${PROJECT_DIR}/data/eval/videomme}"
|
| 195 |
+
: "${VIDEOMME_VIDEO_DIR:=${VIDEOMME_VIDEO_BASE}/videos}"
|
| 196 |
+
: "${VIDEOMME_VIDEO_MIN_PIXELS:=4096}"
|
| 197 |
+
: "${VIDEOMME_VIDEO_MAX_PIXELS:=262144}"
|
| 198 |
+
: "${VIDEOMME_VIDEO_TOTAL_PIXELS:=0}"
|
| 199 |
+
: "${VIDEOMME_MAX_FRAMES:=384}"
|
| 200 |
+
: "${VIDEOMME_FPS:=2}"
|
| 201 |
+
: "${VIDEOMME_SETTING:=all-qwen3_vl-sub0-f${VIDEOMME_MAX_FRAMES}-fps${VIDEOMME_FPS}-min${VIDEOMME_VIDEO_MIN_PIXELS}-max${VIDEOMME_VIDEO_MAX_PIXELS}-total${VIDEOMME_VIDEO_TOTAL_PIXELS}-videomme_preprocessed_384f_262k_total0}"
|
| 202 |
+
: "${VIDEOMME_DATA_FILE:=${PROJECT_DIR}/eval/data/valid_data/videomme_preprocessed_384f_262k_total0.jsonl}"
|
| 203 |
+
: "${VIDEOMME_PREPROCESSED_VIDEO_DIR:=${VIDEOMME_VIDEO_BASE}/preprocessed_videos_384f_262k_total0}"
|
| 204 |
+
: "${VIDEOMME_BATCH_SIZE:=1}"
|
| 205 |
+
: "${VIDEOMME_MAX_MODEL_LEN:=65536}"
|
| 206 |
+
: "${VIDEOMME_MAX_NEW_TOKENS:=128}"
|
| 207 |
+
: "${VIDEOMME_MAX_NUM_BATCHED_TOKENS:=65536}"
|
| 208 |
+
: "${VIDEOMME_GPU_MEMORY_UTILIZATION:=0.90}"
|
| 209 |
+
: "${VIDEOMME_MAX_SAMPLES:=0}"
|
| 210 |
+
|
| 211 |
+
# VideoMME-V2 defaults. Uses the same local schema and evaluator as VideoMME.
|
| 212 |
+
: "${VIDEOMMEV2_VIDEO_BASE:=${PROJECT_DIR}/data/eval/videommev2}"
|
| 213 |
+
: "${VIDEOMMEV2_VIDEO_DIR:=${VIDEOMMEV2_VIDEO_BASE}/videos}"
|
| 214 |
+
: "${VIDEOMMEV2_VIDEO_MIN_PIXELS:=4096}"
|
| 215 |
+
: "${VIDEOMMEV2_VIDEO_MAX_PIXELS:=262144}"
|
| 216 |
+
: "${VIDEOMMEV2_VIDEO_TOTAL_PIXELS:=0}"
|
| 217 |
+
: "${VIDEOMMEV2_MAX_FRAMES:=384}"
|
| 218 |
+
: "${VIDEOMMEV2_FPS:=2}"
|
| 219 |
+
: "${VIDEOMMEV2_SETTING:=all-qwen3_vl-sub0-f${VIDEOMMEV2_MAX_FRAMES}-fps${VIDEOMMEV2_FPS}-min${VIDEOMMEV2_VIDEO_MIN_PIXELS}-max${VIDEOMMEV2_VIDEO_MAX_PIXELS}-total${VIDEOMMEV2_VIDEO_TOTAL_PIXELS}-videommev2_preprocessed_384f_262k_total0}"
|
| 220 |
+
: "${VIDEOMMEV2_DATA_FILE:=${PROJECT_DIR}/eval/data/valid_data/videommev2_preprocessed_384f_262k_total0.jsonl}"
|
| 221 |
+
: "${VIDEOMMEV2_PREPROCESSED_VIDEO_DIR:=${VIDEOMMEV2_VIDEO_BASE}/preprocessed_videos_384f_262k_total0}"
|
| 222 |
+
: "${VIDEOMMEV2_BATCH_SIZE:=1}"
|
| 223 |
+
: "${VIDEOMMEV2_MAX_MODEL_LEN:=65536}"
|
| 224 |
+
: "${VIDEOMMEV2_MAX_NEW_TOKENS:=128}"
|
| 225 |
+
: "${VIDEOMMEV2_MAX_NUM_BATCHED_TOKENS:=65536}"
|
| 226 |
+
: "${VIDEOMMEV2_GPU_MEMORY_UTILIZATION:=0.90}"
|
| 227 |
+
: "${VIDEOMMEV2_PROMPT_MODE:=default}"
|
| 228 |
+
: "${VIDEOMMEV2_ANSWER_FILTER:=}"
|
| 229 |
+
: "${VIDEOMMEV2_MAX_SAMPLES:=0}"
|
| 230 |
+
|
| 231 |
+
# VideoMMMU defaults.
|
| 232 |
+
: "${VIDEOMMMU_SETTING:=videommmu-f128-fps2-min4096-max262144-total0-with-image}"
|
| 233 |
+
: "${VIDEOMMMU_DATA_FILE:=${PROJECT_DIR}/eval/data/valid_data/videommmu_with_image.json}"
|
| 234 |
+
: "${VIDEOMMMU_VIDEO_ROOT:=${PROJECT_DIR}/data/eval/videommmu}"
|
| 235 |
+
: "${VIDEOMMMU_BATCH_SIZE:=4}"
|
| 236 |
+
: "${VIDEOMMMU_MAX_MODEL_LEN:=65536}"
|
| 237 |
+
: "${VIDEOMMMU_MAX_NEW_TOKENS:=128}"
|
| 238 |
+
: "${VIDEOMMMU_MAX_NUM_BATCHED_TOKENS:=65536}"
|
| 239 |
+
: "${VIDEOMMMU_GPU_MEMORY_UTILIZATION:=0.90}"
|
| 240 |
+
: "${VIDEOMMMU_VIDEO_MIN_PIXELS:=4096}"
|
| 241 |
+
: "${VIDEOMMMU_VIDEO_MAX_PIXELS:=262144}"
|
| 242 |
+
: "${VIDEOMMMU_VIDEO_TOTAL_PIXELS:=0}"
|
| 243 |
+
: "${VIDEOMMMU_MAX_FRAMES:=128}"
|
| 244 |
+
: "${VIDEOMMMU_FPS:=2}"
|
| 245 |
+
# OneThinker-style Adaptation uses subject videos + PNGs extracted from
|
| 246 |
+
# Adaptation/test-00000-of-00001.parquet. Run once before eval:
|
| 247 |
+
# python3 eval/data/extract_videommmu_images.py
|
| 248 |
+
# prompt_mode=onethink reproduces OneThinker's CoT prompt; pair it with a large
|
| 249 |
+
# VIDEOMMMU_MAX_NEW_TOKENS (OneThinker uses 8192).
|
| 250 |
+
: "${VIDEOMMMU_PROMPT_MODE:=default}"
|
| 251 |
+
# Separate Adaptation image resolution cap (OneThinker uses 1024*32*32=1048576).
|
| 252 |
+
: "${VIDEOMMMU_IMAGE_MAX_PIXELS:=1048576}"
|
| 253 |
+
# Qwen3.5 native benchmarking: thinking on + recommended sampling + long output.
|
| 254 |
+
# To reproduce the official VideoMMMU number, run with:
|
| 255 |
+
# VIDEOMMMU_PROMPT_MODE=qwen VIDEOMMMU_ENABLE_THINKING=true
|
| 256 |
+
# VIDEOMMMU_MAX_NEW_TOKENS=32768 VIDEOMMMU_MAX_MODEL_LEN=131072
|
| 257 |
+
# VIDEOMMMU_TEMPERATURE=1.0 VIDEOMMMU_TOP_P=0.95 VIDEOMMMU_TOP_K=20 VIDEOMMMU_PRESENCE_PENALTY=1.5
|
| 258 |
+
: "${VIDEOMMMU_ENABLE_THINKING:=false}"
|
| 259 |
+
: "${VIDEOMMMU_TEMPERATURE:=0.0}"
|
| 260 |
+
: "${VIDEOMMMU_TOP_P:=1.0}"
|
| 261 |
+
: "${VIDEOMMMU_TOP_K:=-1}"
|
| 262 |
+
: "${VIDEOMMMU_PRESENCE_PENALTY:=0.0}"
|
| 263 |
+
: "${VIDEOMMMU_MIN_P:=0.0}"
|
| 264 |
+
|
| 265 |
+
# MMVU defaults (multiple-choice subset, raw video).
|
| 266 |
+
# Build the MC JSONL once: python scripts/build_mmvu_mc.py
|
| 267 |
+
: "${MMVU_DATA_FILE:=${PROJECT_DIR}/eval/data/valid_data/mmvu_mc.jsonl}"
|
| 268 |
+
: "${MMVU_VIDEO_ROOT:=${PROJECT_DIR}/data/eval/mmvu}"
|
| 269 |
+
: "${MMVU_BATCH_SIZE:=1}"
|
| 270 |
+
: "${MMVU_MAX_MODEL_LEN:=65536}"
|
| 271 |
+
: "${MMVU_MAX_NEW_TOKENS:=128}"
|
| 272 |
+
: "${MMVU_MAX_NUM_BATCHED_TOKENS:=65536}"
|
| 273 |
+
: "${MMVU_GPU_MEMORY_UTILIZATION:=0.90}"
|
| 274 |
+
: "${MMVU_VIDEO_MIN_PIXELS:=4096}"
|
| 275 |
+
: "${MMVU_VIDEO_MAX_PIXELS:=262144}"
|
| 276 |
+
: "${MMVU_VIDEO_TOTAL_PIXELS:=0}"
|
| 277 |
+
: "${MMVU_MAX_FRAMES:=384}"
|
| 278 |
+
: "${MMVU_FPS:=2}"
|
| 279 |
+
: "${MMVU_PROMPT_MODE:=default}"
|
| 280 |
+
# Qwen3.5 native: MMVU_PROMPT_MODE=qwen MMVU_ENABLE_THINKING=true with large
|
| 281 |
+
# MMVU_MAX_NEW_TOKENS / sampling, mirroring VideoMMMU.
|
| 282 |
+
: "${MMVU_ENABLE_THINKING:=false}"
|
| 283 |
+
: "${MMVU_TEMPERATURE:=0.0}"
|
| 284 |
+
: "${MMVU_TOP_P:=1.0}"
|
| 285 |
+
: "${MMVU_TOP_K:=-1}"
|
| 286 |
+
: "${MMVU_PRESENCE_PENALTY:=0.0}"
|
| 287 |
+
: "${MMVU_MIN_P:=0.0}"
|
| 288 |
+
: "${MMVU_MAX_SAMPLES:=0}"
|
| 289 |
+
: "${MMVU_SETTING:=mmvu-mc-f${MMVU_MAX_FRAMES}-fps${MMVU_FPS}-min${MMVU_VIDEO_MIN_PIXELS}-max${MMVU_VIDEO_MAX_PIXELS}-total${MMVU_VIDEO_TOTAL_PIXELS}-${MMVU_PROMPT_MODE}}"
|
| 290 |
+
|
| 291 |
+
# MVBench defaults (multi-task short-video multiple-choice).
|
| 292 |
+
: "${MVBENCH_DATA_FILE:=${PROJECT_DIR}/eval/data/valid_data/mvbench.json}"
|
| 293 |
+
: "${MVBENCH_VIDEO_ROOT:=${PROJECT_DIR}/data/eval/mvbench}"
|
| 294 |
+
: "${MVBENCH_BATCH_SIZE:=1}"
|
| 295 |
+
: "${MVBENCH_MAX_MODEL_LEN:=65536}"
|
| 296 |
+
: "${MVBENCH_MAX_NEW_TOKENS:=128}"
|
| 297 |
+
: "${MVBENCH_MAX_NUM_BATCHED_TOKENS:=65536}"
|
| 298 |
+
: "${MVBENCH_GPU_MEMORY_UTILIZATION:=0.90}"
|
| 299 |
+
: "${MVBENCH_VIDEO_MIN_PIXELS:=4096}"
|
| 300 |
+
: "${MVBENCH_VIDEO_MAX_PIXELS:=262144}"
|
| 301 |
+
: "${MVBENCH_VIDEO_TOTAL_PIXELS:=0}"
|
| 302 |
+
: "${MVBENCH_MAX_FRAMES:=384}"
|
| 303 |
+
: "${MVBENCH_FPS:=2}"
|
| 304 |
+
: "${MVBENCH_PROMPT_MODE:=default}"
|
| 305 |
+
: "${MVBENCH_ENABLE_THINKING:=false}"
|
| 306 |
+
: "${MVBENCH_TEMPERATURE:=0.0}"
|
| 307 |
+
: "${MVBENCH_TOP_P:=1.0}"
|
| 308 |
+
: "${MVBENCH_TOP_K:=-1}"
|
| 309 |
+
: "${MVBENCH_PRESENCE_PENALTY:=0.0}"
|
| 310 |
+
: "${MVBENCH_MIN_P:=0.0}"
|
| 311 |
+
: "${MVBENCH_MAX_SAMPLES:=0}"
|
| 312 |
+
: "${MVBENCH_SETTING:=mvbench-f${MVBENCH_MAX_FRAMES}-fps${MVBENCH_FPS}-min${MVBENCH_VIDEO_MIN_PIXELS}-max${MVBENCH_VIDEO_MAX_PIXELS}-total${MVBENCH_VIDEO_TOTAL_PIXELS}-${MVBENCH_PROMPT_MODE}}"
|
| 313 |
+
|
| 314 |
+
# Video-Holmes defaults (all multiple-choice, raw cropped videos).
|
| 315 |
+
# Download: huggingface-cli download TencentARC/Video-Holmes --repo-type dataset
|
| 316 |
+
# Build: python scripts/build_videoholmes.py
|
| 317 |
+
: "${VIDEOHOLMES_DATA_FILE:=${PROJECT_DIR}/eval/data/valid_data/videoholmes.jsonl}"
|
| 318 |
+
: "${VIDEOHOLMES_VIDEO_ROOT:=${PROJECT_DIR}/data/eval/videoholmes}"
|
| 319 |
+
: "${VIDEOHOLMES_BATCH_SIZE:=1}"
|
| 320 |
+
: "${VIDEOHOLMES_MAX_MODEL_LEN:=65536}"
|
| 321 |
+
: "${VIDEOHOLMES_MAX_NEW_TOKENS:=128}"
|
| 322 |
+
: "${VIDEOHOLMES_MAX_NUM_BATCHED_TOKENS:=65536}"
|
| 323 |
+
: "${VIDEOHOLMES_GPU_MEMORY_UTILIZATION:=0.90}"
|
| 324 |
+
: "${VIDEOHOLMES_VIDEO_MIN_PIXELS:=4096}"
|
| 325 |
+
: "${VIDEOHOLMES_VIDEO_MAX_PIXELS:=262144}"
|
| 326 |
+
: "${VIDEOHOLMES_VIDEO_TOTAL_PIXELS:=0}"
|
| 327 |
+
: "${VIDEOHOLMES_MAX_FRAMES:=384}"
|
| 328 |
+
: "${VIDEOHOLMES_FPS:=2}"
|
| 329 |
+
: "${VIDEOHOLMES_PROMPT_MODE:=default}"
|
| 330 |
+
# Official benchmark prompt is reasoning-style:
|
| 331 |
+
# VIDEOHOLMES_PROMPT_MODE=holmes VIDEOHOLMES_ENABLE_THINKING=true VIDEOHOLMES_MAX_NEW_TOKENS=1024
|
| 332 |
+
: "${VIDEOHOLMES_ENABLE_THINKING:=false}"
|
| 333 |
+
: "${VIDEOHOLMES_TEMPERATURE:=0.0}"
|
| 334 |
+
: "${VIDEOHOLMES_TOP_P:=1.0}"
|
| 335 |
+
: "${VIDEOHOLMES_TOP_K:=-1}"
|
| 336 |
+
: "${VIDEOHOLMES_PRESENCE_PENALTY:=0.0}"
|
| 337 |
+
: "${VIDEOHOLMES_MIN_P:=0.0}"
|
| 338 |
+
: "${VIDEOHOLMES_MAX_SAMPLES:=0}"
|
| 339 |
+
: "${VIDEOHOLMES_SETTING:=videoholmes-f${VIDEOHOLMES_MAX_FRAMES}-fps${VIDEOHOLMES_FPS}-min${VIDEOHOLMES_VIDEO_MIN_PIXELS}-max${VIDEOHOLMES_VIDEO_MAX_PIXELS}-total${VIDEOHOLMES_VIDEO_TOTAL_PIXELS}-${VIDEOHOLMES_PROMPT_MODE}}"
|
| 340 |
+
|
| 341 |
+
# LongVideoBench defaults. Main videos/subtitles repo is gated; after getting
|
| 342 |
+
# access, download/extract to LONGVIDEOBENCH_VIDEO_ROOT with videos/ + subtitles/.
|
| 343 |
+
# Build validation JSONL: python scripts/build_longvideobench.py
|
| 344 |
+
: "${LONGVIDEOBENCH_DATA_FILE:=${PROJECT_DIR}/eval/data/valid_data/longvideobench_val.jsonl}"
|
| 345 |
+
: "${LONGVIDEOBENCH_VIDEO_ROOT:=${PROJECT_DIR}/data/eval/longvideobench}"
|
| 346 |
+
: "${LONGVIDEOBENCH_SUBTITLE_ROOT:=$LONGVIDEOBENCH_VIDEO_ROOT}"
|
| 347 |
+
: "${LONGVIDEOBENCH_USE_SUBTITLES:=true}"
|
| 348 |
+
: "${LONGVIDEOBENCH_BATCH_SIZE:=1}"
|
| 349 |
+
: "${LONGVIDEOBENCH_MAX_MODEL_LEN:=65536}"
|
| 350 |
+
: "${LONGVIDEOBENCH_MAX_NEW_TOKENS:=128}"
|
| 351 |
+
: "${LONGVIDEOBENCH_MAX_NUM_BATCHED_TOKENS:=65536}"
|
| 352 |
+
: "${LONGVIDEOBENCH_GPU_MEMORY_UTILIZATION:=0.90}"
|
| 353 |
+
: "${LONGVIDEOBENCH_VIDEO_MIN_PIXELS:=4096}"
|
| 354 |
+
: "${LONGVIDEOBENCH_VIDEO_MAX_PIXELS:=262144}"
|
| 355 |
+
: "${LONGVIDEOBENCH_VIDEO_TOTAL_PIXELS:=0}"
|
| 356 |
+
: "${LONGVIDEOBENCH_MAX_FRAMES:=384}"
|
| 357 |
+
: "${LONGVIDEOBENCH_FPS:=2}"
|
| 358 |
+
: "${LONGVIDEOBENCH_PROMPT_MODE:=default}"
|
| 359 |
+
: "${LONGVIDEOBENCH_ENABLE_THINKING:=false}"
|
| 360 |
+
: "${LONGVIDEOBENCH_TEMPERATURE:=0.0}"
|
| 361 |
+
: "${LONGVIDEOBENCH_TOP_P:=1.0}"
|
| 362 |
+
: "${LONGVIDEOBENCH_TOP_K:=-1}"
|
| 363 |
+
: "${LONGVIDEOBENCH_PRESENCE_PENALTY:=0.0}"
|
| 364 |
+
: "${LONGVIDEOBENCH_MIN_P:=0.0}"
|
| 365 |
+
: "${LONGVIDEOBENCH_MAX_SAMPLES:=0}"
|
| 366 |
+
: "${LONGVIDEOBENCH_SETTING:=longvideobench-val-f${LONGVIDEOBENCH_MAX_FRAMES}-fps${LONGVIDEOBENCH_FPS}-min${LONGVIDEOBENCH_VIDEO_MIN_PIXELS}-max${LONGVIDEOBENCH_VIDEO_MAX_PIXELS}-total${LONGVIDEOBENCH_VIDEO_TOTAL_PIXELS}-${LONGVIDEOBENCH_PROMPT_MODE}}"
|
| 367 |
+
|
| 368 |
+
# LVBench defaults (long-video multiple-choice; independent from LongVideoBench).
|
| 369 |
+
: "${LVBENCH_DATA_FILE:=${PROJECT_DIR}/eval/data/valid_data/lvbench.json}"
|
| 370 |
+
: "${LVBENCH_VIDEO_ROOT:=${PROJECT_DIR}/data/eval/lvbench}"
|
| 371 |
+
: "${LVBENCH_BATCH_SIZE:=4}"
|
| 372 |
+
: "${LVBENCH_MAX_MODEL_LEN:=65536}"
|
| 373 |
+
: "${LVBENCH_MAX_NEW_TOKENS:=128}"
|
| 374 |
+
: "${LVBENCH_MAX_NUM_BATCHED_TOKENS:=65536}"
|
| 375 |
+
: "${LVBENCH_GPU_MEMORY_UTILIZATION:=0.90}"
|
| 376 |
+
: "${LVBENCH_VIDEO_MIN_PIXELS:=4096}"
|
| 377 |
+
: "${LVBENCH_VIDEO_MAX_PIXELS:=262144}"
|
| 378 |
+
: "${LVBENCH_VIDEO_TOTAL_PIXELS:=0}"
|
| 379 |
+
: "${LVBENCH_MAX_FRAMES:=128}"
|
| 380 |
+
: "${LVBENCH_FPS:=2}"
|
| 381 |
+
: "${LVBENCH_PROMPT_MODE:=default}"
|
| 382 |
+
: "${LVBENCH_ENABLE_THINKING:=false}"
|
| 383 |
+
: "${LVBENCH_TEMPERATURE:=0.0}"
|
| 384 |
+
: "${LVBENCH_TOP_P:=1.0}"
|
| 385 |
+
: "${LVBENCH_TOP_K:=-1}"
|
| 386 |
+
: "${LVBENCH_PRESENCE_PENALTY:=0.0}"
|
| 387 |
+
: "${LVBENCH_MIN_P:=0.0}"
|
| 388 |
+
: "${LVBENCH_SETTING:=lvbench-f${LVBENCH_MAX_FRAMES}-fps${LVBENCH_FPS}-min${LVBENCH_VIDEO_MIN_PIXELS}-max${LVBENCH_VIDEO_MAX_PIXELS}-total${LVBENCH_VIDEO_TOTAL_PIXELS}-${LVBENCH_PROMPT_MODE}}"
|
| 389 |
+
|
| 390 |
+
# MLVU defaults (dev multiple-choice subset = 7 tasks, M-Avg).
|
| 391 |
+
# Build the MC JSONL once: python scripts/build_mlvu_mc.py
|
| 392 |
+
: "${MLVU_DATA_FILE:=${PROJECT_DIR}/eval/data/valid_data/mlvu_mc.jsonl}"
|
| 393 |
+
: "${MLVU_VIDEO_ROOT:=${PROJECT_DIR}/data/eval/mlvu}"
|
| 394 |
+
: "${MLVU_BATCH_SIZE:=1}"
|
| 395 |
+
: "${MLVU_MAX_MODEL_LEN:=65536}"
|
| 396 |
+
: "${MLVU_MAX_NEW_TOKENS:=128}"
|
| 397 |
+
: "${MLVU_MAX_NUM_BATCHED_TOKENS:=65536}"
|
| 398 |
+
: "${MLVU_GPU_MEMORY_UTILIZATION:=0.90}"
|
| 399 |
+
: "${MLVU_VIDEO_MIN_PIXELS:=4096}"
|
| 400 |
+
: "${MLVU_VIDEO_MAX_PIXELS:=262144}"
|
| 401 |
+
: "${MLVU_VIDEO_TOTAL_PIXELS:=0}"
|
| 402 |
+
: "${MLVU_MAX_FRAMES:=384}"
|
| 403 |
+
: "${MLVU_FPS:=2}"
|
| 404 |
+
: "${MLVU_PROMPT_MODE:=default}"
|
| 405 |
+
: "${MLVU_ENABLE_THINKING:=false}"
|
| 406 |
+
: "${MLVU_TEMPERATURE:=0.0}"
|
| 407 |
+
: "${MLVU_TOP_P:=1.0}"
|
| 408 |
+
: "${MLVU_TOP_K:=-1}"
|
| 409 |
+
: "${MLVU_PRESENCE_PENALTY:=0.0}"
|
| 410 |
+
: "${MLVU_MIN_P:=0.0}"
|
| 411 |
+
: "${MLVU_MAX_SAMPLES:=0}"
|
| 412 |
+
: "${MLVU_SETTING:=mlvu-mc-f${MLVU_MAX_FRAMES}-fps${MLVU_FPS}-min${MLVU_VIDEO_MIN_PIXELS}-max${MLVU_VIDEO_MAX_PIXELS}-total${MLVU_VIDEO_TOTAL_PIXELS}-${MLVU_PROMPT_MODE}}"
|
| 413 |
+
|
| 414 |
+
# VSI-Bench defaults.
|
| 415 |
+
: "${VSI_SETTING:=video128-16M-video-f128-fps2-min65536-maxnone-total16777216-vsibench_preprocessed_128f_16M}"
|
| 416 |
+
: "${VSI_DATA_FILE:=${PROJECT_DIR}/eval/data/valid_data/vsibench_preprocessed_128f_16M.jsonl}"
|
| 417 |
+
: "${VSI_PREPROCESSED_VIDEO_DIR:=${PROJECT_DIR}/data/eval/vsi/preprocessed_videos}"
|
| 418 |
+
: "${VSI_BATCH_SIZE:=16}"
|
| 419 |
+
: "${VSI_MAX_MODEL_LEN:=32768}"
|
| 420 |
+
: "${VSI_MAX_NEW_TOKENS:=1024}"
|
| 421 |
+
: "${VSI_GPU_MEMORY_UTILIZATION:=0.90}"
|
| 422 |
+
: "${VSI_EXPECTED_SAMPLES:=5130}"
|
| 423 |
+
|
| 424 |
+
# MMSI-Bench defaults. Transformers is used so every image can be retained;
|
| 425 |
+
# set MMSI_BACKEND=vllm and MMSI_MAX_IMAGES=8 for the legacy path.
|
| 426 |
+
: "${MMSI_BACKEND:=transformers}"
|
| 427 |
+
: "${MMSI_DATA_FILE:=${PROJECT_DIR}/data/eval/mmsi/MMSI_bench.tsv}"
|
| 428 |
+
: "${MMSI_IMAGE_MIN_PIXELS:=4096}"
|
| 429 |
+
: "${MMSI_IMAGE_MAX_PIXELS:=262144}"
|
| 430 |
+
if [[ "${MMSI_BACKEND}" == "transformers" ]]; then
|
| 431 |
+
: "${MMSI_MAX_IMAGES:=0}"
|
| 432 |
+
else
|
| 433 |
+
: "${MMSI_MAX_IMAGES:=8}"
|
| 434 |
+
fi
|
| 435 |
+
: "${MMSI_IMAGE_COUNT_TAG:=$([[ "${MMSI_MAX_IMAGES}" -le 0 ]] && echo all || echo "${MMSI_MAX_IMAGES}")}"
|
| 436 |
+
: "${MMSI_SETTING:=${MMSI_BACKEND}-img-min${MMSI_IMAGE_MIN_PIXELS}-max${MMSI_IMAGE_MAX_PIXELS}-n${MMSI_IMAGE_COUNT_TAG}-all-MMSI_bench}"
|
| 437 |
+
: "${MMSI_BATCH_SIZE:=16}"
|
| 438 |
+
: "${MMSI_MAX_MODEL_LEN:=32768}"
|
| 439 |
+
: "${MMSI_MAX_NEW_TOKENS:=1024}"
|
| 440 |
+
: "${MMSI_MAX_NUM_BATCHED_TOKENS:=32768}"
|
| 441 |
+
: "${MMSI_GPU_MEMORY_UTILIZATION:=0.90}"
|
| 442 |
+
: "${MMSI_MAX_SAMPLES:=0}"
|
| 443 |
+
: "${MMSI_CATEGORY:=}"
|
| 444 |
+
: "${MMSI_ENABLE_THINKING:=false}"
|
| 445 |
+
: "${MMSI_ATTN_IMPLEMENTATION:=flash_attention_2}"
|
| 446 |
+
if [[ -n "${MMSI_CATEGORY}" || "${MMSI_MAX_SAMPLES}" != "0" ]]; then
|
| 447 |
+
: "${MMSI_EXPECTED_SAMPLES:=0}"
|
| 448 |
+
else
|
| 449 |
+
: "${MMSI_EXPECTED_SAMPLES:=1000}"
|
| 450 |
+
fi
|
| 451 |
+
|
| 452 |
+
# MindCube-Tiny defaults.
|
| 453 |
+
: "${MINDCUBE_SETTING:=img-min4096-max262144-n4-official-tiny1050}"
|
| 454 |
+
: "${MINDCUBE_DATA_FILE:=${PROJECT_DIR}/data/eval/mindcube_official/combined-00000-of-00001.parquet}"
|
| 455 |
+
: "${MINDCUBE_MAX_IMAGES:=4}"
|
| 456 |
+
: "${MINDCUBE_EXPECTED_SAMPLES:=1050}"
|
| 457 |
+
: "${MINDCUBE_HF_CACHE_DIR:=}"
|
| 458 |
+
: "${MINDCUBE_BATCH_SIZE:=16}"
|
| 459 |
+
: "${MINDCUBE_MAX_MODEL_LEN:=32768}"
|
| 460 |
+
: "${MINDCUBE_MAX_NEW_TOKENS:=1024}"
|
| 461 |
+
: "${MINDCUBE_MAX_NUM_BATCHED_TOKENS:=32768}"
|
| 462 |
+
: "${MINDCUBE_GPU_MEMORY_UTILIZATION:=0.90}"
|
| 463 |
+
|
| 464 |
+
# ReVSI defaults. The all-frame/native-frame profile reproduces the 58.2 result
|
| 465 |
+
# reported for Video-ORA-9B; ReVSI remains excluded from the three-benchmark
|
| 466 |
+
# spatial-intelligence average.
|
| 467 |
+
: "${REVSI_DATA_FILE:=${PROJECT_DIR}/data/eval/revsi/all_frame/test-00000-of-00001.parquet}"
|
| 468 |
+
: "${REVSI_VIDEO_ROOT:=${PROJECT_DIR}/data/eval/revsi}"
|
| 469 |
+
: "${REVSI_FRAME_BUDGET:=all}"
|
| 470 |
+
: "${REVSI_MAX_FRAMES:=128}"
|
| 471 |
+
: "${REVSI_EXACT_NFRAMES:=true}"
|
| 472 |
+
: "${REVSI_FPS:=2}"
|
| 473 |
+
: "${REVSI_VIDEO_MIN_PIXELS:=65536}"
|
| 474 |
+
: "${REVSI_VIDEO_MAX_PIXELS:=}"
|
| 475 |
+
: "${REVSI_VIDEO_TOTAL_PIXELS:=16777216}"
|
| 476 |
+
: "${REVSI_BATCH_SIZE:=16}"
|
| 477 |
+
: "${REVSI_MAX_MODEL_LEN:=32768}"
|
| 478 |
+
: "${REVSI_MAX_NEW_TOKENS:=64}"
|
| 479 |
+
: "${REVSI_GPU_MEMORY_UTILIZATION:=0.90}"
|
| 480 |
+
: "${REVSI_MAX_SAMPLES:=0}"
|
| 481 |
+
: "${REVSI_EXPECTED_SAMPLES:=6808}"
|
| 482 |
+
: "${REVSI_TASK_FILTER:=}"
|
| 483 |
+
: "${REVSI_ENABLE_THINKING:=false}"
|
| 484 |
+
: "${REVSI_SETTING:=native-all-f${REVSI_MAX_FRAMES}-exact${REVSI_EXACT_NFRAMES}-fps${REVSI_FPS}-min${REVSI_VIDEO_MIN_PIXELS}-max${REVSI_VIDEO_MAX_PIXELS:-none}-total${REVSI_VIDEO_TOTAL_PIXELS}}"
|
| 485 |
+
|
| 486 |
+
# Spatial grounding defaults, aligned to data/joint/sft_joint_all.jsonl.
|
| 487 |
+
: "${SPATIAL_GROUNDING_DATASETS:=refcoco-val,refcoco-testA,refcoco-testB,refcoco+-val,refcoco+-testA,refcoco+-testB,refcocog-val,refcocog-test}"
|
| 488 |
+
: "${SPATIAL_GROUNDING_BENCH_DIR:=${PROJECT_DIR}/eval/task/spatial_grounding}"
|
| 489 |
+
: "${SPATIAL_GROUNDING_IMAGE_ROOT:=${PROJECT_DIR}/data/eval/spatial_grounding/images}"
|
| 490 |
+
: "${SPATIAL_GROUNDING_PROCESSOR_PATH:=$MODEL_PATH}"
|
| 491 |
+
: "${SPATIAL_GROUNDING_PROMPT_STYLE:=qwen_native}"
|
| 492 |
+
: "${SPATIAL_GROUNDING_COORD_SYSTEM:=norm1000}"
|
| 493 |
+
: "${SPATIAL_GROUNDING_BBOX_SELECT:=first}"
|
| 494 |
+
: "${SPATIAL_GROUNDING_MIN_TOKENS:=64}"
|
| 495 |
+
: "${SPATIAL_GROUNDING_TOTAL_TOKENS:=1024}"
|
| 496 |
+
: "${SPATIAL_GROUNDING_BATCH_SIZE:=64}"
|
| 497 |
+
: "${SPATIAL_GROUNDING_MAX_MODEL_LEN:=32768}"
|
| 498 |
+
: "${SPATIAL_GROUNDING_MAX_NEW_TOKENS:=1024}"
|
| 499 |
+
: "${SPATIAL_GROUNDING_MAX_NUM_BATCHED_TOKENS:=32768}"
|
| 500 |
+
: "${SPATIAL_GROUNDING_GPU_MEMORY_UTILIZATION:=0.85}"
|
| 501 |
+
: "${SPATIAL_GROUNDING_ENABLE_THINKING:=false}"
|
| 502 |
+
: "${SPATIAL_GROUNDING_MAX_SAMPLES:=0}"
|
| 503 |
+
: "${SPATIAL_GROUNDING_SETTING:=spatial-grounding-${SPATIAL_GROUNDING_DATASETS//,/_}-${SPATIAL_GROUNDING_PROMPT_STYLE}-min${SPATIAL_GROUNDING_MIN_TOKENS}-total${SPATIAL_GROUNDING_TOTAL_TOKENS}-new${SPATIAL_GROUNDING_MAX_NEW_TOKENS}}"
|
| 504 |
+
|
| 505 |
+
# Tracking defaults, aligned to data/joint/sft_joint_all.jsonl tracking videos.
|
| 506 |
+
: "${TRACKING_DATASETS:=eval_got10k}"
|
| 507 |
+
: "${TRACKING_BENCH_DIR:=${PROJECT_DIR}/data/eval/tracking}"
|
| 508 |
+
: "${TRACKING_BASE_PREFIX:=$TRACKING_BENCH_DIR}"
|
| 509 |
+
: "${TRACKING_PROCESSOR_PATH:=$MODEL_PATH}"
|
| 510 |
+
: "${TRACKING_VIDEO_MIN_PIXELS:=4096}"
|
| 511 |
+
: "${TRACKING_VIDEO_MAX_PIXELS:=786432}"
|
| 512 |
+
: "${TRACKING_VIDEO_TOTAL_PIXELS:=8388608}"
|
| 513 |
+
: "${TRACKING_MAX_FRAMES:=32}"
|
| 514 |
+
: "${TRACKING_FPS:=1}"
|
| 515 |
+
: "${TRACKING_MAX_MODEL_LEN:=32768}"
|
| 516 |
+
: "${TRACKING_MAX_NEW_TOKENS:=8192}"
|
| 517 |
+
: "${TRACKING_MAX_NUM_BATCHED_TOKENS:=32768}"
|
| 518 |
+
: "${TRACKING_BATCH_SIZE:=64}"
|
| 519 |
+
: "${TRACKING_GPU_MEMORY_UTILIZATION:=0.95}"
|
| 520 |
+
: "${TRACKING_ENABLE_THINKING:=false}"
|
| 521 |
+
: "${TRACKING_PROMPT_MODE:=default}"
|
| 522 |
+
: "${TRACKING_CHUNKED_REPROMPT:=0}"
|
| 523 |
+
: "${TRACKING_REPROMPT_USE_GT_FIRST_BOX:=0}"
|
| 524 |
+
: "${TRACKING_MAX_SAMPLES:=0}"
|
| 525 |
+
: "${TRACKING_SETTING:=tracking-${TRACKING_DATASETS//,/_}-f${TRACKING_MAX_FRAMES}-fps${TRACKING_FPS}-min${TRACKING_VIDEO_MIN_PIXELS}-max${TRACKING_VIDEO_MAX_PIXELS}-total${TRACKING_VIDEO_TOTAL_PIXELS}-new${TRACKING_MAX_NEW_TOKENS}}"
|
| 526 |
+
|
| 527 |
+
# STVG defaults, aligned to data/joint/sft_joint_all.jsonl stvg videos.
|
| 528 |
+
: "${STVG_DATASETS:=eval_stvg}"
|
| 529 |
+
: "${STVG_BENCH_DIR:=${PROJECT_DIR}/data/eval/stvg}"
|
| 530 |
+
: "${STVG_BASE_PREFIX:=$STVG_BENCH_DIR}"
|
| 531 |
+
: "${STVG_PROCESSOR_PATH:=$MODEL_PATH}"
|
| 532 |
+
: "${STVG_VIDEO_MIN_PIXELS:=65536}"
|
| 533 |
+
: "${STVG_VIDEO_MAX_PIXELS:=393216}"
|
| 534 |
+
: "${STVG_VIDEO_TOTAL_PIXELS:=10485760}"
|
| 535 |
+
: "${STVG_MAX_FRAMES:=128}"
|
| 536 |
+
: "${STVG_FPS:=2}"
|
| 537 |
+
: "${STVG_MAX_MODEL_LEN:=65536}"
|
| 538 |
+
: "${STVG_MAX_NEW_TOKENS:=2048}"
|
| 539 |
+
: "${STVG_MAX_NUM_BATCHED_TOKENS:=65536}"
|
| 540 |
+
: "${STVG_BATCH_SIZE:=64}"
|
| 541 |
+
: "${STVG_GPU_MEMORY_UTILIZATION:=0.85}"
|
| 542 |
+
: "${STVG_ENABLE_THINKING:=false}"
|
| 543 |
+
: "${STVG_PROMPT_MODE:=train_stvg}"
|
| 544 |
+
: "${STVG_MAX_SAMPLES:=0}"
|
| 545 |
+
: "${STVG_SETTING:=stvg-${STVG_DATASETS//,/_}-f${STVG_MAX_FRAMES}-fps${STVG_FPS}-min${STVG_VIDEO_MIN_PIXELS}-max${STVG_VIDEO_MAX_PIXELS}-total${STVG_VIDEO_TOTAL_PIXELS}-new${STVG_MAX_NEW_TOKENS}-${STVG_PROMPT_MODE}}"
|
| 546 |
+
|
| 547 |
+
# Segmentation defaults.
|
| 548 |
+
# This task is dispatched to eval/task/segmentation/run_eval_vllm.sh,
|
| 549 |
+
# which performs vLLM inference plus optional SAM2 post-processing.
|
| 550 |
+
: "${SEGMENTATION_DATASETS:=eval_seg_refcoco,eval_seg_refcocop,eval_seg_refcocog,eval_seg_mevis,eval_seg_reasonvos}"
|
| 551 |
+
: "${SEGMENTATION_BENCH_DIR:=${PROJECT_DIR}/data/eval/segmentation}"
|
| 552 |
+
: "${SEGMENTATION_DATA_ROOT:=$SEGMENTATION_BENCH_DIR}"
|
| 553 |
+
: "${SEGMENTATION_PROCESSOR_PATH:=$MODEL_PATH}"
|
| 554 |
+
: "${SEGMENTATION_DATA_TYPE:=all}"
|
| 555 |
+
# train_seg mirrors the prompt used in joint SFT training data
|
| 556 |
+
# (data/train/seg_image_nothink_evalaware.jsonl, seg_video_nothink.jsonl).
|
| 557 |
+
: "${SEGMENTATION_PROMPT_MODE:=train_seg}"
|
| 558 |
+
: "${SEGMENTATION_ENABLE_THINKING:=false}"
|
| 559 |
+
: "${SEGMENTATION_BATCH_SIZE:=16}"
|
| 560 |
+
: "${SEGMENTATION_MAX_NEW_TOKENS:=1024}"
|
| 561 |
+
: "${SEGMENTATION_MAX_MODEL_LEN:=32768}"
|
| 562 |
+
: "${SEGMENTATION_MAX_PIXELS_IMAGE:=1048576}"
|
| 563 |
+
: "${SEGMENTATION_MIN_PIXELS_IMAGE:=4096}"
|
| 564 |
+
: "${SEGMENTATION_VIDEO_MAX_PIXELS:=262144}"
|
| 565 |
+
: "${SEGMENTATION_VIDEO_MIN_PIXELS:=4096}"
|
| 566 |
+
: "${SEGMENTATION_VIDEO_TOTAL_PIXELS:=16777216}"
|
| 567 |
+
: "${SEGMENTATION_MAX_FRAMES:=128}"
|
| 568 |
+
: "${SEGMENTATION_FPS:=2}"
|
| 569 |
+
: "${SEGMENTATION_VIDEO_READER:=decord}"
|
| 570 |
+
: "${SEGMENTATION_GPU_MEM_UTIL:=0.85}"
|
| 571 |
+
: "${SEGMENTATION_SEED:=42}"
|
| 572 |
+
: "${SEGMENTATION_MAX_SAMPLES:=}"
|
| 573 |
+
: "${SEGMENTATION_RUN_SAM2:=false}"
|
| 574 |
+
: "${SEGMENTATION_SAM2_CKPT:=${PROJECT_DIR}/models/sam2/sam2.1_hiera_large.pt}"
|
| 575 |
+
: "${SEGMENTATION_SAM2_CFG:=configs/sam2.1/sam2.1_hiera_l.yaml}"
|
| 576 |
+
: "${SEGMENTATION_POSTPROCESSOR_PATH:=${PROJECT_DIR}/third_party/OneThinker/Evaluation/Eval/seg_post_sam2.py}"
|
| 577 |
+
# SAM2 post-processing parallelism. world_size = NUM_GPUS * WORKERS_PER_GPU.
|
| 578 |
+
# With a large SAM2_EPOCH_SIZE (single epoch) + maxtasksperchild=1, EACH worker
|
| 579 |
+
# loads the SAM2 model exactly ONCE at startup and then streams its whole slice
|
| 580 |
+
# (no repeated reloads). So WORKERS_PER_GPU only trades GPU utilization vs total
|
| 581 |
+
# process count: too high (32 -> 256 procs) exhausts CPU/RAM/ffmpeg/handles and
|
| 582 |
+
# aborts; too low (1) underutilizes the GPU. 4 per GPU is a balance (8 GPUs -> 32
|
| 583 |
+
# persistent workers, 32 one-time loads); bump to 6-8 if GPU is still not full.
|
| 584 |
+
# Leave NUM_GPUS empty to auto-use all visible GPUs.
|
| 585 |
+
: "${SEGMENTATION_SAM2_NUM_GPUS:=}"
|
| 586 |
+
: "${SEGMENTATION_SAM2_WORKERS_PER_GPU:=8}"
|
| 587 |
+
: "${SEGMENTATION_SETTING:=segmentation-${SEGMENTATION_DATASETS//,/_}-${SEGMENTATION_PROMPT_MODE}-f${SEGMENTATION_MAX_FRAMES}-fps${SEGMENTATION_FPS}-min${SEGMENTATION_VIDEO_MIN_PIXELS}-max${SEGMENTATION_VIDEO_MAX_PIXELS}-total${SEGMENTATION_VIDEO_TOTAL_PIXELS}-new${SEGMENTATION_MAX_NEW_TOKENS}}"
|
| 588 |
+
|
| 589 |
+
# TimeLens-Bench temporal grounding defaults.
|
| 590 |
+
# This task uses HuggingFace transformers, not vLLM.
|
| 591 |
+
: "${TIMELENS_DATASETS:=charades-timelens}"
|
| 592 |
+
: "${TIMELENS_BENCH_DIR:=${PROJECT_DIR}/data/eval/temporal_grounding}"
|
| 593 |
+
: "${TIMELENS_ENABLE_THINKING:=false}"
|
| 594 |
+
: "${TIMELENS_MIN_TOKENS:=1}"
|
| 595 |
+
: "${TIMELENS_TOTAL_TOKENS:=128000}"
|
| 596 |
+
: "${TIMELENS_MAX_PIXELS:=409600}"
|
| 597 |
+
: "${TIMELENS_MAX_FRAMES:=2048}"
|
| 598 |
+
: "${TIMELENS_FPS:=4}"
|
| 599 |
+
: "${TIMELENS_MAX_NEW_TOKENS:=128}"
|
| 600 |
+
: "${TIMELENS_REPETITION_PENALTY:=1.0}"
|
| 601 |
+
: "${TIMELENS_STOP_AFTER_ANSWER:=true}"
|
| 602 |
+
: "${TIMELENS_PROMPT_MODE:=same}"
|
| 603 |
+
: "${TIMELENS_NUM_WORKERS:=2}"
|
| 604 |
+
: "${TIMELENS_MAX_SAMPLES:=0}"
|
| 605 |
+
|
| 606 |
+
export MODEL_PATH TASKS GPUS TP_SIZE SHARD_MODE VIDEO_CACHE_SIZE PREFETCH_BATCHES
|
| 607 |
+
export FORCE_MERGE SKIP_MERGE MERGE_LOG_DIR
|
| 608 |
+
export VIDEOMME_SETTING VIDEOMME_DATA_FILE VIDEOMME_VIDEO_BASE VIDEOMME_VIDEO_DIR
|
| 609 |
+
export VIDEOMME_PREPROCESSED_VIDEO_DIR VIDEOMME_VIDEO_MIN_PIXELS VIDEOMME_VIDEO_MAX_PIXELS
|
| 610 |
+
export VIDEOMME_VIDEO_TOTAL_PIXELS VIDEOMME_MAX_FRAMES VIDEOMME_FPS
|
| 611 |
+
export VIDEOMME_BATCH_SIZE VIDEOMME_MAX_MODEL_LEN VIDEOMME_MAX_NEW_TOKENS
|
| 612 |
+
export VIDEOMME_MAX_NUM_BATCHED_TOKENS VIDEOMME_GPU_MEMORY_UTILIZATION
|
| 613 |
+
export VIDEOMME_MAX_SAMPLES
|
| 614 |
+
export VIDEOMMEV2_SETTING VIDEOMMEV2_DATA_FILE VIDEOMMEV2_VIDEO_BASE VIDEOMMEV2_VIDEO_DIR
|
| 615 |
+
export VIDEOMMEV2_PREPROCESSED_VIDEO_DIR VIDEOMMEV2_VIDEO_MIN_PIXELS VIDEOMMEV2_VIDEO_MAX_PIXELS
|
| 616 |
+
export VIDEOMMEV2_VIDEO_TOTAL_PIXELS VIDEOMMEV2_MAX_FRAMES VIDEOMMEV2_FPS
|
| 617 |
+
export VIDEOMMEV2_BATCH_SIZE VIDEOMMEV2_MAX_MODEL_LEN VIDEOMMEV2_MAX_NEW_TOKENS
|
| 618 |
+
export VIDEOMMEV2_MAX_NUM_BATCHED_TOKENS VIDEOMMEV2_GPU_MEMORY_UTILIZATION
|
| 619 |
+
export VIDEOMMEV2_PROMPT_MODE VIDEOMMEV2_ANSWER_FILTER VIDEOMMEV2_MAX_SAMPLES
|
| 620 |
+
export VIDEOMMMU_SETTING VIDEOMMMU_DATA_FILE VIDEOMMMU_VIDEO_ROOT
|
| 621 |
+
export VIDEOMMMU_BATCH_SIZE VIDEOMMMU_MAX_MODEL_LEN VIDEOMMMU_MAX_NEW_TOKENS
|
| 622 |
+
export VIDEOMMMU_MAX_NUM_BATCHED_TOKENS VIDEOMMMU_GPU_MEMORY_UTILIZATION
|
| 623 |
+
export VIDEOMMMU_VIDEO_MIN_PIXELS VIDEOMMMU_VIDEO_MAX_PIXELS VIDEOMMMU_VIDEO_TOTAL_PIXELS
|
| 624 |
+
export VIDEOMMMU_MAX_FRAMES VIDEOMMMU_FPS VIDEOMMMU_PROMPT_MODE VIDEOMMMU_IMAGE_MAX_PIXELS
|
| 625 |
+
export VIDEOMMMU_ENABLE_THINKING VIDEOMMMU_TEMPERATURE VIDEOMMMU_TOP_P VIDEOMMMU_TOP_K
|
| 626 |
+
export VIDEOMMMU_PRESENCE_PENALTY VIDEOMMMU_MIN_P
|
| 627 |
+
export MMVU_SETTING MMVU_DATA_FILE MMVU_VIDEO_ROOT MMVU_BATCH_SIZE
|
| 628 |
+
export MMVU_MAX_MODEL_LEN MMVU_MAX_NEW_TOKENS MMVU_MAX_NUM_BATCHED_TOKENS
|
| 629 |
+
export MMVU_GPU_MEMORY_UTILIZATION MMVU_VIDEO_MIN_PIXELS MMVU_VIDEO_MAX_PIXELS
|
| 630 |
+
export MMVU_VIDEO_TOTAL_PIXELS MMVU_MAX_FRAMES MMVU_FPS MMVU_PROMPT_MODE
|
| 631 |
+
export MMVU_ENABLE_THINKING MMVU_TEMPERATURE MMVU_TOP_P MMVU_TOP_K
|
| 632 |
+
export MMVU_PRESENCE_PENALTY MMVU_MIN_P MMVU_MAX_SAMPLES
|
| 633 |
+
export MVBENCH_SETTING MVBENCH_DATA_FILE MVBENCH_VIDEO_ROOT MVBENCH_BATCH_SIZE
|
| 634 |
+
export MVBENCH_MAX_MODEL_LEN MVBENCH_MAX_NEW_TOKENS MVBENCH_MAX_NUM_BATCHED_TOKENS
|
| 635 |
+
export MVBENCH_GPU_MEMORY_UTILIZATION MVBENCH_VIDEO_MIN_PIXELS MVBENCH_VIDEO_MAX_PIXELS
|
| 636 |
+
export MVBENCH_VIDEO_TOTAL_PIXELS MVBENCH_MAX_FRAMES MVBENCH_FPS MVBENCH_PROMPT_MODE
|
| 637 |
+
export MVBENCH_ENABLE_THINKING MVBENCH_TEMPERATURE MVBENCH_TOP_P MVBENCH_TOP_K
|
| 638 |
+
export MVBENCH_PRESENCE_PENALTY MVBENCH_MIN_P MVBENCH_MAX_SAMPLES
|
| 639 |
+
export VIDEOHOLMES_SETTING VIDEOHOLMES_DATA_FILE VIDEOHOLMES_VIDEO_ROOT VIDEOHOLMES_BATCH_SIZE
|
| 640 |
+
export VIDEOHOLMES_MAX_MODEL_LEN VIDEOHOLMES_MAX_NEW_TOKENS VIDEOHOLMES_MAX_NUM_BATCHED_TOKENS
|
| 641 |
+
export VIDEOHOLMES_GPU_MEMORY_UTILIZATION VIDEOHOLMES_VIDEO_MIN_PIXELS VIDEOHOLMES_VIDEO_MAX_PIXELS
|
| 642 |
+
export VIDEOHOLMES_VIDEO_TOTAL_PIXELS VIDEOHOLMES_MAX_FRAMES VIDEOHOLMES_FPS VIDEOHOLMES_PROMPT_MODE
|
| 643 |
+
export VIDEOHOLMES_ENABLE_THINKING VIDEOHOLMES_TEMPERATURE VIDEOHOLMES_TOP_P VIDEOHOLMES_TOP_K
|
| 644 |
+
export VIDEOHOLMES_PRESENCE_PENALTY VIDEOHOLMES_MIN_P VIDEOHOLMES_MAX_SAMPLES
|
| 645 |
+
export LONGVIDEOBENCH_SETTING LONGVIDEOBENCH_DATA_FILE LONGVIDEOBENCH_VIDEO_ROOT
|
| 646 |
+
export LONGVIDEOBENCH_SUBTITLE_ROOT LONGVIDEOBENCH_USE_SUBTITLES LONGVIDEOBENCH_BATCH_SIZE
|
| 647 |
+
export LONGVIDEOBENCH_MAX_MODEL_LEN LONGVIDEOBENCH_MAX_NEW_TOKENS LONGVIDEOBENCH_MAX_NUM_BATCHED_TOKENS
|
| 648 |
+
export LONGVIDEOBENCH_GPU_MEMORY_UTILIZATION LONGVIDEOBENCH_VIDEO_MIN_PIXELS LONGVIDEOBENCH_VIDEO_MAX_PIXELS
|
| 649 |
+
export LONGVIDEOBENCH_VIDEO_TOTAL_PIXELS LONGVIDEOBENCH_MAX_FRAMES LONGVIDEOBENCH_FPS LONGVIDEOBENCH_PROMPT_MODE
|
| 650 |
+
export LONGVIDEOBENCH_ENABLE_THINKING LONGVIDEOBENCH_TEMPERATURE LONGVIDEOBENCH_TOP_P LONGVIDEOBENCH_TOP_K
|
| 651 |
+
export LONGVIDEOBENCH_PRESENCE_PENALTY LONGVIDEOBENCH_MIN_P LONGVIDEOBENCH_MAX_SAMPLES
|
| 652 |
+
export LVBENCH_SETTING LVBENCH_DATA_FILE LVBENCH_VIDEO_ROOT LVBENCH_BATCH_SIZE
|
| 653 |
+
export LVBENCH_MAX_MODEL_LEN LVBENCH_MAX_NEW_TOKENS LVBENCH_MAX_NUM_BATCHED_TOKENS
|
| 654 |
+
export LVBENCH_GPU_MEMORY_UTILIZATION LVBENCH_VIDEO_MIN_PIXELS LVBENCH_VIDEO_MAX_PIXELS
|
| 655 |
+
export LVBENCH_VIDEO_TOTAL_PIXELS LVBENCH_MAX_FRAMES LVBENCH_FPS LVBENCH_PROMPT_MODE
|
| 656 |
+
export LVBENCH_ENABLE_THINKING LVBENCH_TEMPERATURE LVBENCH_TOP_P LVBENCH_TOP_K
|
| 657 |
+
export LVBENCH_PRESENCE_PENALTY LVBENCH_MIN_P
|
| 658 |
+
export MLVU_SETTING MLVU_DATA_FILE MLVU_VIDEO_ROOT MLVU_BATCH_SIZE
|
| 659 |
+
export MLVU_MAX_MODEL_LEN MLVU_MAX_NEW_TOKENS MLVU_MAX_NUM_BATCHED_TOKENS
|
| 660 |
+
export MLVU_GPU_MEMORY_UTILIZATION MLVU_VIDEO_MIN_PIXELS MLVU_VIDEO_MAX_PIXELS
|
| 661 |
+
export MLVU_VIDEO_TOTAL_PIXELS MLVU_MAX_FRAMES MLVU_FPS MLVU_PROMPT_MODE
|
| 662 |
+
export MLVU_ENABLE_THINKING MLVU_TEMPERATURE MLVU_TOP_P MLVU_TOP_K
|
| 663 |
+
export MLVU_PRESENCE_PENALTY MLVU_MIN_P MLVU_MAX_SAMPLES
|
| 664 |
+
export VSI_SETTING VSI_DATA_FILE VSI_PREPROCESSED_VIDEO_DIR
|
| 665 |
+
export VSI_BATCH_SIZE VSI_MAX_MODEL_LEN VSI_MAX_NEW_TOKENS VSI_GPU_MEMORY_UTILIZATION
|
| 666 |
+
export VSI_EXPECTED_SAMPLES
|
| 667 |
+
export MMSI_BACKEND MMSI_SETTING MMSI_DATA_FILE MMSI_MAX_IMAGES MMSI_BATCH_SIZE
|
| 668 |
+
export MMSI_IMAGE_MIN_PIXELS MMSI_IMAGE_MAX_PIXELS MMSI_IMAGE_COUNT_TAG
|
| 669 |
+
export MMSI_MAX_MODEL_LEN MMSI_MAX_NEW_TOKENS MMSI_MAX_NUM_BATCHED_TOKENS
|
| 670 |
+
export MMSI_GPU_MEMORY_UTILIZATION MMSI_MAX_SAMPLES MMSI_CATEGORY
|
| 671 |
+
export MMSI_ENABLE_THINKING MMSI_ATTN_IMPLEMENTATION MMSI_EXPECTED_SAMPLES
|
| 672 |
+
export MINDCUBE_SETTING MINDCUBE_DATA_FILE MINDCUBE_MAX_IMAGES MINDCUBE_BATCH_SIZE
|
| 673 |
+
export MINDCUBE_MAX_MODEL_LEN MINDCUBE_MAX_NEW_TOKENS MINDCUBE_MAX_NUM_BATCHED_TOKENS
|
| 674 |
+
export MINDCUBE_GPU_MEMORY_UTILIZATION MINDCUBE_EXPECTED_SAMPLES MINDCUBE_HF_CACHE_DIR
|
| 675 |
+
export REVSI_SETTING REVSI_DATA_FILE REVSI_VIDEO_ROOT REVSI_FRAME_BUDGET
|
| 676 |
+
export REVSI_MAX_FRAMES REVSI_EXACT_NFRAMES REVSI_FPS
|
| 677 |
+
export REVSI_VIDEO_MIN_PIXELS REVSI_VIDEO_MAX_PIXELS REVSI_VIDEO_TOTAL_PIXELS
|
| 678 |
+
export REVSI_BATCH_SIZE REVSI_MAX_MODEL_LEN REVSI_MAX_NEW_TOKENS
|
| 679 |
+
export REVSI_GPU_MEMORY_UTILIZATION REVSI_MAX_SAMPLES REVSI_EXPECTED_SAMPLES
|
| 680 |
+
export REVSI_TASK_FILTER REVSI_ENABLE_THINKING
|
| 681 |
+
export SPATIAL_GROUNDING_SETTING SPATIAL_GROUNDING_DATASETS SPATIAL_GROUNDING_BENCH_DIR
|
| 682 |
+
export SPATIAL_GROUNDING_IMAGE_ROOT SPATIAL_GROUNDING_PROCESSOR_PATH SPATIAL_GROUNDING_PROMPT_STYLE
|
| 683 |
+
export SPATIAL_GROUNDING_COORD_SYSTEM SPATIAL_GROUNDING_BBOX_SELECT
|
| 684 |
+
export SPATIAL_GROUNDING_MIN_TOKENS SPATIAL_GROUNDING_TOTAL_TOKENS
|
| 685 |
+
export SPATIAL_GROUNDING_BATCH_SIZE SPATIAL_GROUNDING_MAX_MODEL_LEN
|
| 686 |
+
export SPATIAL_GROUNDING_MAX_NEW_TOKENS SPATIAL_GROUNDING_MAX_NUM_BATCHED_TOKENS
|
| 687 |
+
export SPATIAL_GROUNDING_GPU_MEMORY_UTILIZATION SPATIAL_GROUNDING_ENABLE_THINKING
|
| 688 |
+
export SPATIAL_GROUNDING_MAX_SAMPLES
|
| 689 |
+
export TRACKING_SETTING TRACKING_DATASETS TRACKING_BENCH_DIR TRACKING_BASE_PREFIX
|
| 690 |
+
export TRACKING_PROCESSOR_PATH TRACKING_VIDEO_MIN_PIXELS TRACKING_VIDEO_MAX_PIXELS
|
| 691 |
+
export TRACKING_VIDEO_TOTAL_PIXELS TRACKING_MAX_FRAMES TRACKING_FPS
|
| 692 |
+
export TRACKING_MAX_MODEL_LEN TRACKING_MAX_NEW_TOKENS TRACKING_MAX_NUM_BATCHED_TOKENS
|
| 693 |
+
export TRACKING_BATCH_SIZE TRACKING_GPU_MEMORY_UTILIZATION TRACKING_ENABLE_THINKING
|
| 694 |
+
export TRACKING_PROMPT_MODE TRACKING_CHUNKED_REPROMPT TRACKING_REPROMPT_USE_GT_FIRST_BOX
|
| 695 |
+
export TRACKING_MAX_SAMPLES
|
| 696 |
+
export STVG_SETTING STVG_DATASETS STVG_BENCH_DIR STVG_BASE_PREFIX STVG_PROCESSOR_PATH
|
| 697 |
+
export STVG_VIDEO_MIN_PIXELS STVG_VIDEO_MAX_PIXELS STVG_VIDEO_TOTAL_PIXELS
|
| 698 |
+
export STVG_MAX_FRAMES STVG_FPS STVG_MAX_MODEL_LEN STVG_MAX_NEW_TOKENS
|
| 699 |
+
export STVG_MAX_NUM_BATCHED_TOKENS STVG_BATCH_SIZE STVG_GPU_MEMORY_UTILIZATION
|
| 700 |
+
export STVG_ENABLE_THINKING STVG_PROMPT_MODE STVG_MAX_SAMPLES
|
| 701 |
+
export SEGMENTATION_SETTING SEGMENTATION_DATASETS SEGMENTATION_BENCH_DIR SEGMENTATION_DATA_ROOT
|
| 702 |
+
export SEGMENTATION_PROCESSOR_PATH SEGMENTATION_DATA_TYPE SEGMENTATION_PROMPT_MODE
|
| 703 |
+
export SEGMENTATION_ENABLE_THINKING SEGMENTATION_BATCH_SIZE SEGMENTATION_MAX_NEW_TOKENS
|
| 704 |
+
export SEGMENTATION_MAX_MODEL_LEN SEGMENTATION_MAX_PIXELS_IMAGE SEGMENTATION_MIN_PIXELS_IMAGE
|
| 705 |
+
export SEGMENTATION_VIDEO_MAX_PIXELS SEGMENTATION_VIDEO_MIN_PIXELS SEGMENTATION_VIDEO_TOTAL_PIXELS
|
| 706 |
+
export SEGMENTATION_MAX_FRAMES SEGMENTATION_FPS SEGMENTATION_VIDEO_READER
|
| 707 |
+
export SEGMENTATION_GPU_MEM_UTIL SEGMENTATION_SEED
|
| 708 |
+
export SEGMENTATION_MAX_SAMPLES
|
| 709 |
+
export SEGMENTATION_RUN_SAM2 SEGMENTATION_SAM2_CKPT SEGMENTATION_SAM2_CFG
|
| 710 |
+
export SEGMENTATION_SAM2_NUM_GPUS SEGMENTATION_SAM2_WORKERS_PER_GPU
|
| 711 |
+
export TIMELENS_BENCH_DIR TIMELENS_MAX_SAMPLES
|
| 712 |
+
|
| 713 |
+
# Conda env used to run eval. By default, keep the currently active environment.
|
| 714 |
+
# Override with EVAL_CONDA_ENV=xxx only when an explicit env switch is desired.
|
| 715 |
+
: "${EVAL_CONDA_ENV:=keep}"
|
| 716 |
+
if [[ "${EVAL_CONDA_ENV}" != "keep" && "${CONDA_DEFAULT_ENV:-}" != "${EVAL_CONDA_ENV}" ]]; then
|
| 717 |
+
if command -v conda >/dev/null 2>&1; then
|
| 718 |
+
eval "$(conda shell.bash hook)"
|
| 719 |
+
conda activate "${EVAL_CONDA_ENV}"
|
| 720 |
+
else
|
| 721 |
+
echo "ERROR: conda not found and CONDA_DEFAULT_ENV=${CONDA_DEFAULT_ENV:-<unset>}" >&2
|
| 722 |
+
exit 1
|
| 723 |
+
fi
|
| 724 |
+
fi
|
| 725 |
+
|
| 726 |
+
cd "${PROJECT_DIR}"
|
| 727 |
+
|
| 728 |
+
resolve_model_path() {
|
| 729 |
+
local input_path="${MODEL_PATH%/}"
|
| 730 |
+
|
| 731 |
+
if ls "${input_path}"/model_world_size_*_rank_0.pt >/dev/null 2>&1; then
|
| 732 |
+
local actor_dir="$input_path"
|
| 733 |
+
local hf_dir="${MERGED_MODEL_PATH:-${actor_dir}/huggingface}"
|
| 734 |
+
local need_merge=0
|
| 735 |
+
|
| 736 |
+
if [[ "${SKIP_MERGE}" = "1" ]]; then
|
| 737 |
+
need_merge=0
|
| 738 |
+
elif [[ "${FORCE_MERGE}" = "1" ]]; then
|
| 739 |
+
need_merge=1
|
| 740 |
+
elif [[ ! -f "${hf_dir}/model.safetensors" ]]; then
|
| 741 |
+
need_merge=1
|
| 742 |
+
fi
|
| 743 |
+
|
| 744 |
+
if [[ "${need_merge}" = "1" ]]; then
|
| 745 |
+
mkdir -p "${MERGE_LOG_DIR}"
|
| 746 |
+
local merge_log="${MERGE_LOG_DIR}/merge-$(basename "$(dirname "${actor_dir}")")-$(date +%Y%m%d_%H%M%S).log"
|
| 747 |
+
echo "[merge] FSDP actor checkpoint detected: ${actor_dir}"
|
| 748 |
+
echo "[merge] Merging shards -> ${hf_dir}"
|
| 749 |
+
if [[ -n "${BASE_MODEL_PATH:-}" ]]; then
|
| 750 |
+
python -u scripts/model_merger.py \
|
| 751 |
+
--local_dir "${actor_dir}" \
|
| 752 |
+
--base_model_path "${BASE_MODEL_PATH}" \
|
| 753 |
+
2>&1 | tee "${merge_log}"
|
| 754 |
+
else
|
| 755 |
+
python -u scripts/model_merger.py \
|
| 756 |
+
--local_dir "${actor_dir}" \
|
| 757 |
+
2>&1 | tee "${merge_log}"
|
| 758 |
+
fi
|
| 759 |
+
if [[ ! -f "${hf_dir}/model.safetensors" ]]; then
|
| 760 |
+
echo "ERROR: merge finished but ${hf_dir}/model.safetensors was not created. Log: ${merge_log}" >&2
|
| 761 |
+
exit 1
|
| 762 |
+
fi
|
| 763 |
+
else
|
| 764 |
+
echo "[merge] Using existing merged HF model: ${hf_dir}"
|
| 765 |
+
fi
|
| 766 |
+
|
| 767 |
+
MODEL_PATH="${hf_dir}"
|
| 768 |
+
export MODEL_PATH
|
| 769 |
+
|
| 770 |
+
if [[ "${TRACKING_PROCESSOR_PATH%/}" = "${input_path}" ]]; then
|
| 771 |
+
TRACKING_PROCESSOR_PATH="${MODEL_PATH}"
|
| 772 |
+
export TRACKING_PROCESSOR_PATH
|
| 773 |
+
fi
|
| 774 |
+
if [[ "${STVG_PROCESSOR_PATH%/}" = "${input_path}" ]]; then
|
| 775 |
+
STVG_PROCESSOR_PATH="${MODEL_PATH}"
|
| 776 |
+
export STVG_PROCESSOR_PATH
|
| 777 |
+
fi
|
| 778 |
+
if [[ "${SPATIAL_GROUNDING_PROCESSOR_PATH%/}" = "${input_path}" ]]; then
|
| 779 |
+
SPATIAL_GROUNDING_PROCESSOR_PATH="${MODEL_PATH}"
|
| 780 |
+
export SPATIAL_GROUNDING_PROCESSOR_PATH
|
| 781 |
+
fi
|
| 782 |
+
if [[ "${SEGMENTATION_PROCESSOR_PATH%/}" = "${input_path}" ]]; then
|
| 783 |
+
SEGMENTATION_PROCESSOR_PATH="${MODEL_PATH}"
|
| 784 |
+
export SEGMENTATION_PROCESSOR_PATH
|
| 785 |
+
fi
|
| 786 |
+
elif [[ -d "${input_path}/huggingface" && -f "${input_path}/huggingface/model.safetensors" && ! -f "${input_path}/config.json" ]]; then
|
| 787 |
+
MODEL_PATH="${input_path}/huggingface"
|
| 788 |
+
export MODEL_PATH
|
| 789 |
+
echo "[merge] MODEL_PATH points to a checkpoint wrapper; using ${MODEL_PATH}"
|
| 790 |
+
fi
|
| 791 |
+
}
|
| 792 |
+
|
| 793 |
+
resolve_model_path
|
| 794 |
+
|
| 795 |
+
VLLM_TASKS=""
|
| 796 |
+
RUN_TIMELENS=0
|
| 797 |
+
RUN_SEGMENTATION=0
|
| 798 |
+
RUN_MMSI_TRANSFORMERS=0
|
| 799 |
+
RUN_REVSI=0
|
| 800 |
+
|
| 801 |
+
if [[ "${MMSI_BACKEND}" != "transformers" && "${MMSI_BACKEND}" != "vllm" ]]; then
|
| 802 |
+
echo "ERROR: MMSI_BACKEND must be 'transformers' or 'vllm', got '${MMSI_BACKEND}'" >&2
|
| 803 |
+
exit 1
|
| 804 |
+
fi
|
| 805 |
+
|
| 806 |
+
append_vllm_task() {
|
| 807 |
+
local task="$1"
|
| 808 |
+
if [[ -z "$VLLM_TASKS" ]]; then
|
| 809 |
+
VLLM_TASKS="$task"
|
| 810 |
+
else
|
| 811 |
+
VLLM_TASKS="${VLLM_TASKS},${task}"
|
| 812 |
+
fi
|
| 813 |
+
}
|
| 814 |
+
|
| 815 |
+
model_family_tag() {
|
| 816 |
+
local model_path="${1%/}"
|
| 817 |
+
local name
|
| 818 |
+
name="$(basename "$model_path")"
|
| 819 |
+
local parent
|
| 820 |
+
local grandparent
|
| 821 |
+
parent="$(basename "$(dirname "$model_path")")"
|
| 822 |
+
grandparent="$(basename "$(dirname "$(dirname "$model_path")")")"
|
| 823 |
+
if [[ "$name" == "huggingface" && "$parent" == "actor" && "$grandparent" == global_step_* ]]; then
|
| 824 |
+
basename "$(dirname "$(dirname "$(dirname "$model_path")")")"
|
| 825 |
+
return
|
| 826 |
+
fi
|
| 827 |
+
if [[ "$name" == checkpoint-* ]]; then
|
| 828 |
+
basename "$(dirname "$(dirname "$model_path")")"
|
| 829 |
+
else
|
| 830 |
+
echo "$name"
|
| 831 |
+
fi
|
| 832 |
+
}
|
| 833 |
+
|
| 834 |
+
checkpoint_tag() {
|
| 835 |
+
local model_path="${1%/}"
|
| 836 |
+
local name
|
| 837 |
+
name="$(basename "$model_path")"
|
| 838 |
+
local parent
|
| 839 |
+
local grandparent
|
| 840 |
+
parent="$(basename "$(dirname "$model_path")")"
|
| 841 |
+
grandparent="$(basename "$(dirname "$(dirname "$model_path")")")"
|
| 842 |
+
if [[ "$name" == "huggingface" && "$parent" == "actor" && "$grandparent" == global_step_* ]]; then
|
| 843 |
+
basename "$(dirname "$(dirname "$model_path")")"
|
| 844 |
+
return
|
| 845 |
+
fi
|
| 846 |
+
if [[ "$name" == checkpoint-* ]]; then
|
| 847 |
+
echo "$name"
|
| 848 |
+
else
|
| 849 |
+
echo "base"
|
| 850 |
+
fi
|
| 851 |
+
}
|
| 852 |
+
|
| 853 |
+
IFS=',' read -ra TASK_LIST <<< "$TASKS"
|
| 854 |
+
for RAW_TASK in "${TASK_LIST[@]}"; do
|
| 855 |
+
TASK="$(echo "$RAW_TASK" | xargs)"
|
| 856 |
+
case "$TASK" in
|
| 857 |
+
all)
|
| 858 |
+
VLLM_TASKS="videomme,videommev2,videommmu,mmvu,mvbench,videoholmes,longvideobench,lvbench,mlvu,vsi,mindcube,spatial_grounding,tracking,stvg"
|
| 859 |
+
if [[ "${MMSI_BACKEND}" == "transformers" ]]; then
|
| 860 |
+
RUN_MMSI_TRANSFORMERS=1
|
| 861 |
+
else
|
| 862 |
+
append_vllm_task "mmsi"
|
| 863 |
+
fi
|
| 864 |
+
RUN_REVSI=1
|
| 865 |
+
;;
|
| 866 |
+
videomme|videommev2|videommmu|mmvu|mvbench|videoholmes|longvideobench|lvbench|mlvu|vsi|mindcube|spatial_grounding|tracking|stvg)
|
| 867 |
+
append_vllm_task "$TASK"
|
| 868 |
+
;;
|
| 869 |
+
mmsi)
|
| 870 |
+
if [[ "${MMSI_BACKEND}" == "transformers" ]]; then
|
| 871 |
+
RUN_MMSI_TRANSFORMERS=1
|
| 872 |
+
else
|
| 873 |
+
append_vllm_task "mmsi"
|
| 874 |
+
fi
|
| 875 |
+
;;
|
| 876 |
+
revsi)
|
| 877 |
+
RUN_REVSI=1
|
| 878 |
+
;;
|
| 879 |
+
spatial|grounding|refcoco|spatial_grounding_eval)
|
| 880 |
+
append_vllm_task "spatial_grounding"
|
| 881 |
+
;;
|
| 882 |
+
spatial_temporal_grounding)
|
| 883 |
+
append_vllm_task "stvg"
|
| 884 |
+
;;
|
| 885 |
+
temporal_grounding|timelens|timelens_bench)
|
| 886 |
+
RUN_TIMELENS=1
|
| 887 |
+
;;
|
| 888 |
+
segmentation|seg|reasonseg|refseg)
|
| 889 |
+
RUN_SEGMENTATION=1
|
| 890 |
+
;;
|
| 891 |
+
"")
|
| 892 |
+
;;
|
| 893 |
+
*)
|
| 894 |
+
echo "ERROR: unknown TASK '${TASK}'. Supported: videomme,videommev2,videommmu,mmvu,mvbench,videoholmes,longvideobench,lvbench,mlvu,vsi,mmsi,mindcube,revsi,spatial_grounding,tracking,stvg,temporal_grounding,segmentation,all" >&2
|
| 895 |
+
exit 1
|
| 896 |
+
;;
|
| 897 |
+
esac
|
| 898 |
+
done
|
| 899 |
+
|
| 900 |
+
if [[ -n "$VLLM_TASKS" ]]; then
|
| 901 |
+
TASKS="$VLLM_TASKS" python eval/task/eval_vllm.py "$@"
|
| 902 |
+
fi
|
| 903 |
+
|
| 904 |
+
if [[ "$RUN_MMSI_TRANSFORMERS" = "1" ]]; then
|
| 905 |
+
MMSI_FAMILY="$(model_family_tag "$MODEL_PATH")"
|
| 906 |
+
MMSI_CKPT="$(checkpoint_tag "$MODEL_PATH")"
|
| 907 |
+
MMSI_RUN_ROOT="${PROJECT_DIR}/outputs/${MMSI_FAMILY}/spatial_intelligence/mmsi/${MMSI_CKPT}/${MMSI_SETTING}/$(date +%Y%m%d_%H%M%S)"
|
| 908 |
+
CUDA_VISIBLE_DEVICES="$GPUS" \
|
| 909 |
+
PROCESSOR_PATH="$MODEL_PATH" \
|
| 910 |
+
DATA_FILE="$MMSI_DATA_FILE" \
|
| 911 |
+
OUTPUT_DIR="$MMSI_RUN_ROOT" \
|
| 912 |
+
IMAGE_MIN_PIXELS="$MMSI_IMAGE_MIN_PIXELS" \
|
| 913 |
+
IMAGE_MAX_PIXELS="$MMSI_IMAGE_MAX_PIXELS" \
|
| 914 |
+
MAX_IMAGES="$MMSI_MAX_IMAGES" \
|
| 915 |
+
MAX_NEW_TOKENS="$MMSI_MAX_NEW_TOKENS" \
|
| 916 |
+
MAX_SAMPLES="$MMSI_MAX_SAMPLES" \
|
| 917 |
+
CATEGORY="$MMSI_CATEGORY" \
|
| 918 |
+
ENABLE_THINKING="$MMSI_ENABLE_THINKING" \
|
| 919 |
+
ATTN_IMPLEMENTATION="$MMSI_ATTN_IMPLEMENTATION" \
|
| 920 |
+
EXPECTED_SAMPLES="$MMSI_EXPECTED_SAMPLES" \
|
| 921 |
+
bash eval/task/mmsi/run_eval_transformers.sh "$MODEL_PATH"
|
| 922 |
+
fi
|
| 923 |
+
|
| 924 |
+
if [[ "$RUN_REVSI" = "1" ]]; then
|
| 925 |
+
REVSI_FAMILY="$(model_family_tag "$MODEL_PATH")"
|
| 926 |
+
REVSI_CKPT="$(checkpoint_tag "$MODEL_PATH")"
|
| 927 |
+
REVSI_RUN_EXPECTED_SAMPLES="$REVSI_EXPECTED_SAMPLES"
|
| 928 |
+
if [[ "$REVSI_MAX_SAMPLES" != "0" || -n "$REVSI_TASK_FILTER" ]]; then
|
| 929 |
+
REVSI_RUN_EXPECTED_SAMPLES=0
|
| 930 |
+
fi
|
| 931 |
+
REVSI_RUN_ROOT="${PROJECT_DIR}/outputs/${REVSI_FAMILY}/spatial_intelligence/revsi/${REVSI_CKPT}/${REVSI_SETTING}/$(date +%Y%m%d_%H%M%S)"
|
| 932 |
+
CUDA_VISIBLE_DEVICES="$GPUS" \
|
| 933 |
+
REVSI_ROOT="$REVSI_VIDEO_ROOT" \
|
| 934 |
+
QA_FILE="$REVSI_DATA_FILE" \
|
| 935 |
+
VIDEO_ROOT="$REVSI_VIDEO_ROOT" \
|
| 936 |
+
FRAME_BUDGET="$REVSI_FRAME_BUDGET" \
|
| 937 |
+
OUTPUT_DIR="$REVSI_RUN_ROOT" \
|
| 938 |
+
MAX_FRAMES="$REVSI_MAX_FRAMES" \
|
| 939 |
+
EXACT_NFRAMES="$REVSI_EXACT_NFRAMES" \
|
| 940 |
+
FPS="$REVSI_FPS" \
|
| 941 |
+
VIDEO_MIN_PIXELS="$REVSI_VIDEO_MIN_PIXELS" \
|
| 942 |
+
VIDEO_MAX_PIXELS="$REVSI_VIDEO_MAX_PIXELS" \
|
| 943 |
+
VIDEO_TOTAL_PIXELS="$REVSI_VIDEO_TOTAL_PIXELS" \
|
| 944 |
+
BATCH_SIZE="$REVSI_BATCH_SIZE" \
|
| 945 |
+
MAX_MODEL_LEN="$REVSI_MAX_MODEL_LEN" \
|
| 946 |
+
MAX_NEW_TOKENS="$REVSI_MAX_NEW_TOKENS" \
|
| 947 |
+
GPU_MEM_UTIL="$REVSI_GPU_MEMORY_UTILIZATION" \
|
| 948 |
+
MAX_SAMPLES="$REVSI_MAX_SAMPLES" \
|
| 949 |
+
EXPECTED_SAMPLES="$REVSI_RUN_EXPECTED_SAMPLES" \
|
| 950 |
+
TASK_FILTER="$REVSI_TASK_FILTER" \
|
| 951 |
+
ENABLE_THINKING="$REVSI_ENABLE_THINKING" \
|
| 952 |
+
TP_SIZE="$TP_SIZE" \
|
| 953 |
+
bash eval/task/revsi/run_eval_vllm.sh "$MODEL_PATH"
|
| 954 |
+
fi
|
| 955 |
+
|
| 956 |
+
if [[ "$RUN_TIMELENS" = "1" ]]; then
|
| 957 |
+
TIMELENS_FAMILY="$(model_family_tag "$MODEL_PATH")"
|
| 958 |
+
TIMELENS_CKPT="$(checkpoint_tag "$MODEL_PATH")"
|
| 959 |
+
TIMELENS_SETTING="timelens-fps${TIMELENS_FPS}-min${TIMELENS_MIN_TOKENS}-total${TIMELENS_TOTAL_TOKENS}-new${TIMELENS_MAX_NEW_TOKENS}-${TIMELENS_DATASETS//,/_}"
|
| 960 |
+
TIMELENS_RUN_ROOT="${PROJECT_DIR}/outputs/${TIMELENS_FAMILY}/temporal_grounding/${TIMELENS_CKPT}/${TIMELENS_SETTING}/$(date +%Y%m%d_%H%M%S)"
|
| 961 |
+
CUDA_VISIBLE_DEVICES="$GPUS" \
|
| 962 |
+
DATASETS="$TIMELENS_DATASETS" \
|
| 963 |
+
SETTING="$TIMELENS_SETTING" \
|
| 964 |
+
OUTPUT_ROOT="$TIMELENS_RUN_ROOT" \
|
| 965 |
+
TIMELENS_USE_OUTPUT_ROOT_DIRECT=1 \
|
| 966 |
+
MIN_TOKENS="$TIMELENS_MIN_TOKENS" \
|
| 967 |
+
TOTAL_TOKENS="$TIMELENS_TOTAL_TOKENS" \
|
| 968 |
+
MAX_PIXELS="$TIMELENS_MAX_PIXELS" \
|
| 969 |
+
MAX_FRAMES="$TIMELENS_MAX_FRAMES" \
|
| 970 |
+
FPS="$TIMELENS_FPS" \
|
| 971 |
+
MAX_NEW_TOKENS="$TIMELENS_MAX_NEW_TOKENS" \
|
| 972 |
+
REPETITION_PENALTY="$TIMELENS_REPETITION_PENALTY" \
|
| 973 |
+
STOP_AFTER_ANSWER="$TIMELENS_STOP_AFTER_ANSWER" \
|
| 974 |
+
PROMPT_MODE="$TIMELENS_PROMPT_MODE" \
|
| 975 |
+
NUM_WORKERS="$TIMELENS_NUM_WORKERS" \
|
| 976 |
+
bash eval/task/temporal_grounding/run_eval.sh \
|
| 977 |
+
"$MODEL_PATH" \
|
| 978 |
+
"$TIMELENS_ENABLE_THINKING" \
|
| 979 |
+
"$TIMELENS_RUN_ROOT"
|
| 980 |
+
fi
|
| 981 |
+
|
| 982 |
+
if [[ "$RUN_SEGMENTATION" = "1" ]]; then
|
| 983 |
+
SEG_FAMILY="$(model_family_tag "$MODEL_PATH")"
|
| 984 |
+
SEG_CKPT="$(checkpoint_tag "$MODEL_PATH")"
|
| 985 |
+
SEG_RUN_ROOT="${PROJECT_DIR}/outputs/${SEG_FAMILY}/segmentation/${SEG_CKPT}/${SEGMENTATION_SETTING}/$(date +%Y%m%d_%H%M%S)"
|
| 986 |
+
CUDA_VISIBLE_DEVICES="${CUDA_VISIBLE_DEVICES:-$GPUS}" \
|
| 987 |
+
PROCESSOR_PATH="$SEGMENTATION_PROCESSOR_PATH" \
|
| 988 |
+
BENCH_DIR="$SEGMENTATION_BENCH_DIR" \
|
| 989 |
+
DATA_ROOT="$SEGMENTATION_DATA_ROOT" \
|
| 990 |
+
DATASETS="$SEGMENTATION_DATASETS" \
|
| 991 |
+
DATA_TYPE="$SEGMENTATION_DATA_TYPE" \
|
| 992 |
+
PROMPT_MODE="$SEGMENTATION_PROMPT_MODE" \
|
| 993 |
+
ENABLE_THINKING="$SEGMENTATION_ENABLE_THINKING" \
|
| 994 |
+
BATCH_SIZE="$SEGMENTATION_BATCH_SIZE" \
|
| 995 |
+
MAX_NEW_TOKENS="$SEGMENTATION_MAX_NEW_TOKENS" \
|
| 996 |
+
MAX_MODEL_LEN="$SEGMENTATION_MAX_MODEL_LEN" \
|
| 997 |
+
MAX_PIXELS_IMAGE="$SEGMENTATION_MAX_PIXELS_IMAGE" \
|
| 998 |
+
MIN_PIXELS_IMAGE="$SEGMENTATION_MIN_PIXELS_IMAGE" \
|
| 999 |
+
VIDEO_MAX_PIXELS="$SEGMENTATION_VIDEO_MAX_PIXELS" \
|
| 1000 |
+
VIDEO_MIN_PIXELS="$SEGMENTATION_VIDEO_MIN_PIXELS" \
|
| 1001 |
+
VIDEO_TOTAL_PIXELS="$SEGMENTATION_VIDEO_TOTAL_PIXELS" \
|
| 1002 |
+
MAX_FRAMES="$SEGMENTATION_MAX_FRAMES" \
|
| 1003 |
+
FPS="$SEGMENTATION_FPS" \
|
| 1004 |
+
VIDEO_READER="$SEGMENTATION_VIDEO_READER" \
|
| 1005 |
+
TP_SIZE="$TP_SIZE" \
|
| 1006 |
+
GPU_MEM_UTIL="$SEGMENTATION_GPU_MEM_UTIL" \
|
| 1007 |
+
SEED="$SEGMENTATION_SEED" \
|
| 1008 |
+
MAX_SAMPLES="$SEGMENTATION_MAX_SAMPLES" \
|
| 1009 |
+
RUN_SAM2="$SEGMENTATION_RUN_SAM2" \
|
| 1010 |
+
SAM2_CKPT="$SEGMENTATION_SAM2_CKPT" \
|
| 1011 |
+
SAM2_CFG="$SEGMENTATION_SAM2_CFG" \
|
| 1012 |
+
ONETHINKER_SEG_POST="$SEGMENTATION_POSTPROCESSOR_PATH" \
|
| 1013 |
+
SAM2_NUM_GPUS="$SEGMENTATION_SAM2_NUM_GPUS" \
|
| 1014 |
+
SAM2_WORKERS_PER_GPU="$SEGMENTATION_SAM2_WORKERS_PER_GPU" \
|
| 1015 |
+
bash eval/task/segmentation/run_eval_vllm.sh \
|
| 1016 |
+
"$MODEL_PATH" \
|
| 1017 |
+
"$SEG_RUN_ROOT"
|
| 1018 |
+
fi
|
code/eval/task/eval_image_mc_vllm.py
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Unified vLLM evaluator for image-sequence multiple-choice benchmarks.
|
| 3 |
+
|
| 4 |
+
Supported benchmarks:
|
| 5 |
+
- MMSI-Bench: TSV/parquet with base64 image list in `image`.
|
| 6 |
+
- MindCube-Tiny: official 1,050-sample Hugging Face split.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import argparse
|
| 12 |
+
import ast
|
| 13 |
+
import base64
|
| 14 |
+
import csv
|
| 15 |
+
import io
|
| 16 |
+
import json
|
| 17 |
+
import os
|
| 18 |
+
import re
|
| 19 |
+
import sys
|
| 20 |
+
import time
|
| 21 |
+
from collections import defaultdict
|
| 22 |
+
from typing import Any, Dict, List
|
| 23 |
+
|
| 24 |
+
from canonical_data import load_json_records
|
| 25 |
+
from mindcube.data_utils import (
|
| 26 |
+
OFFICIAL_DATA_SOURCE,
|
| 27 |
+
decode_mindcube_images,
|
| 28 |
+
load_mindcube_records,
|
| 29 |
+
mindcube_answer_value,
|
| 30 |
+
mindcube_group,
|
| 31 |
+
mindcube_prompt,
|
| 32 |
+
)
|
| 33 |
+
from PIL import Image
|
| 34 |
+
|
| 35 |
+
CHOICES = list("ABCDEFGH")
|
| 36 |
+
ANSWER_RE = re.compile(r"<answer>\s*(.*?)\s*</answer>", re.DOTALL | re.IGNORECASE)
|
| 37 |
+
TAG_RE = re.compile(r"</?think>", re.IGNORECASE)
|
| 38 |
+
FORMAT_PRIORITY = {
|
| 39 |
+
"start": 10,
|
| 40 |
+
"end": 9,
|
| 41 |
+
"phrase": 7,
|
| 42 |
+
"parentheses": 6,
|
| 43 |
+
"period": 5,
|
| 44 |
+
"colon": 4,
|
| 45 |
+
"right_paren": 3,
|
| 46 |
+
"space": 2,
|
| 47 |
+
"fallback": 0,
|
| 48 |
+
}
|
| 49 |
+
ANSWER_PHRASES = [
|
| 50 |
+
"the answer is",
|
| 51 |
+
"answer is",
|
| 52 |
+
"the correct answer is",
|
| 53 |
+
"correct answer is",
|
| 54 |
+
"the best answer is",
|
| 55 |
+
"best answer is",
|
| 56 |
+
"the correct option is",
|
| 57 |
+
"correct option is",
|
| 58 |
+
"i choose",
|
| 59 |
+
"i select",
|
| 60 |
+
"my answer is",
|
| 61 |
+
"答案是",
|
| 62 |
+
"答案为",
|
| 63 |
+
]
|
| 64 |
+
PROMPT_TAIL = (
|
| 65 |
+
"Choose the best answer from the options. "
|
| 66 |
+
"Put exactly one uppercase option letter inside <answer>...</answer> "
|
| 67 |
+
"Do not explain. Example: <answer>A</answer>"
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def strip_think_block(text: str) -> str:
|
| 72 |
+
text = (text or "").strip()
|
| 73 |
+
if not text:
|
| 74 |
+
return ""
|
| 75 |
+
parts = re.split(r"</think>", text, flags=re.IGNORECASE)
|
| 76 |
+
if len(parts) > 1:
|
| 77 |
+
text = parts[-1]
|
| 78 |
+
else:
|
| 79 |
+
text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL | re.IGNORECASE)
|
| 80 |
+
return TAG_RE.sub("", text).strip()
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def strip_answer_tags(text: str) -> str:
|
| 84 |
+
matches = ANSWER_RE.findall(text or "")
|
| 85 |
+
if matches:
|
| 86 |
+
return matches[-1].strip()
|
| 87 |
+
return (text or "").strip()
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def extract_mcq_answer(response: str, choices: List[str] | None = None) -> str:
|
| 91 |
+
if not response or not response.strip():
|
| 92 |
+
return ""
|
| 93 |
+
all_choices = choices or CHOICES
|
| 94 |
+
text = strip_answer_tags(strip_think_block(response)).strip()
|
| 95 |
+
if not text:
|
| 96 |
+
return ""
|
| 97 |
+
for char in [",", ".", "!", "?", ";", ":", "'", '"', "。", ":"]:
|
| 98 |
+
text = text.strip(char)
|
| 99 |
+
padded = " " + text + " "
|
| 100 |
+
candidates = []
|
| 101 |
+
|
| 102 |
+
for ch in all_choices:
|
| 103 |
+
for token, fmt in (
|
| 104 |
+
(f"({ch})", "parentheses"),
|
| 105 |
+
(f"{ch}.", "period"),
|
| 106 |
+
(f"{ch}:", "colon"),
|
| 107 |
+
(f"{ch})", "right_paren"),
|
| 108 |
+
(f"{ch} ", "space"),
|
| 109 |
+
):
|
| 110 |
+
pos = padded.rfind(token)
|
| 111 |
+
if pos != -1:
|
| 112 |
+
candidates.append((ch, pos, fmt))
|
| 113 |
+
|
| 114 |
+
lower = padded.lower()
|
| 115 |
+
for phrase in ANSWER_PHRASES:
|
| 116 |
+
idx = lower.rfind(phrase.lower())
|
| 117 |
+
if idx != -1:
|
| 118 |
+
after = idx + len(phrase)
|
| 119 |
+
for ch in all_choices:
|
| 120 |
+
m = re.search(rf"\b{re.escape(ch)}\b", padded[after:], flags=re.IGNORECASE)
|
| 121 |
+
if m:
|
| 122 |
+
candidates.append((ch, after + m.start(), "phrase"))
|
| 123 |
+
|
| 124 |
+
stripped = padded.strip()
|
| 125 |
+
for ch in all_choices:
|
| 126 |
+
if stripped.upper() == ch:
|
| 127 |
+
candidates.append((ch, 0, "start"))
|
| 128 |
+
elif stripped.startswith(ch) and (len(stripped) == 1 or not stripped[1].isalpha()):
|
| 129 |
+
candidates.append((ch, 0, "start"))
|
| 130 |
+
elif stripped.endswith(ch) and (len(stripped) == 1 or not stripped[-2].isalpha()):
|
| 131 |
+
candidates.append((ch, len(padded) - 1, "end"))
|
| 132 |
+
|
| 133 |
+
if not candidates:
|
| 134 |
+
for ch in all_choices:
|
| 135 |
+
m = re.search(rf"\b{re.escape(ch)}\b", padded)
|
| 136 |
+
if m:
|
| 137 |
+
candidates.append((ch, m.start(), "fallback"))
|
| 138 |
+
if not candidates:
|
| 139 |
+
return ""
|
| 140 |
+
candidates.sort(key=lambda x: (FORMAT_PRIORITY.get(x[2], 0), x[1]), reverse=True)
|
| 141 |
+
return candidates[0][0]
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def load_records(args) -> List[Dict[str, Any]]:
|
| 145 |
+
if args.bench == "mindcube":
|
| 146 |
+
return load_mindcube_records(
|
| 147 |
+
args.data_file,
|
| 148 |
+
chunk=args.chunk,
|
| 149 |
+
index=args.index,
|
| 150 |
+
expected_samples=args.expected_samples,
|
| 151 |
+
cache_dir=args.hf_cache_dir or None,
|
| 152 |
+
)
|
| 153 |
+
if args.data_file.endswith((".jsonl", ".json")):
|
| 154 |
+
return load_json_records(args.data_file)
|
| 155 |
+
if args.data_file.endswith(".parquet"):
|
| 156 |
+
import pandas as pd
|
| 157 |
+
|
| 158 |
+
return pd.read_parquet(args.data_file).to_dict("records")
|
| 159 |
+
|
| 160 |
+
csv.field_size_limit(sys.maxsize)
|
| 161 |
+
with open(args.data_file, encoding="utf-8", errors="replace", newline="") as f:
|
| 162 |
+
return list(csv.DictReader(f, delimiter="\t"))
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def shard(records: List[Dict[str, Any]], chunk: int, index: int) -> List[Dict[str, Any]]:
|
| 166 |
+
if chunk <= 1:
|
| 167 |
+
return records
|
| 168 |
+
return records[index::chunk]
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def decode_mmsi_images(value: Any, max_images: int) -> List[Image.Image]:
|
| 172 |
+
images_b64 = ast.literal_eval(value) if isinstance(value, str) else value
|
| 173 |
+
if not isinstance(images_b64, (list, tuple)):
|
| 174 |
+
images_b64 = [images_b64]
|
| 175 |
+
images = []
|
| 176 |
+
for item in images_b64[:max_images]:
|
| 177 |
+
if not item:
|
| 178 |
+
continue
|
| 179 |
+
path = ""
|
| 180 |
+
if isinstance(item, dict):
|
| 181 |
+
path = str(
|
| 182 |
+
item.get("path")
|
| 183 |
+
or item.get("image")
|
| 184 |
+
or item.get("image_path")
|
| 185 |
+
or ""
|
| 186 |
+
)
|
| 187 |
+
elif isinstance(item, str) and os.path.isfile(item):
|
| 188 |
+
path = item
|
| 189 |
+
if path:
|
| 190 |
+
images.append(Image.open(path).convert("RGB"))
|
| 191 |
+
continue
|
| 192 |
+
raw = item if isinstance(item, bytes) else base64.b64decode(str(item))
|
| 193 |
+
images.append(Image.open(io.BytesIO(raw)).convert("RGB"))
|
| 194 |
+
return images
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def normalize_choices(value: Any) -> List[str]:
|
| 198 |
+
if value is None:
|
| 199 |
+
return []
|
| 200 |
+
if hasattr(value, "tolist"):
|
| 201 |
+
value = value.tolist()
|
| 202 |
+
if isinstance(value, (list, tuple)):
|
| 203 |
+
return [str(v) for v in value]
|
| 204 |
+
return [str(value)]
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
def answer_choices(choices: List[Any] | None) -> List[str]:
|
| 208 |
+
n = len(choices) if choices is not None else 0
|
| 209 |
+
return CHOICES[:n] if n > 0 else list("ABCD")
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def choices_from_question(question: str) -> List[str]:
|
| 213 |
+
found = re.findall(r"(?:^|[\s,])([A-H])\s*[:\).]", question or "")
|
| 214 |
+
out = []
|
| 215 |
+
for ch in found:
|
| 216 |
+
if ch not in out:
|
| 217 |
+
out.append(ch)
|
| 218 |
+
return out or list("ABCD")
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def build_prompt(args, rec: Dict[str, Any]) -> tuple[str, List[str]]:
|
| 222 |
+
if args.bench == "mmsi":
|
| 223 |
+
question = str(rec.get("question") or "").strip()
|
| 224 |
+
labels = choices_from_question(question)
|
| 225 |
+
return f"{question}\n{PROMPT_TAIL}", labels
|
| 226 |
+
|
| 227 |
+
return mindcube_prompt(rec, PROMPT_TAIL)
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def get_answer(args, rec: Dict[str, Any]) -> str:
|
| 231 |
+
answer = (
|
| 232 |
+
mindcube_answer_value(rec)
|
| 233 |
+
if args.bench == "mindcube"
|
| 234 |
+
else rec.get("answer")
|
| 235 |
+
)
|
| 236 |
+
if answer is None:
|
| 237 |
+
return ""
|
| 238 |
+
if args.bench == "mindcube" and isinstance(answer, (int, float)) and not isinstance(answer, bool):
|
| 239 |
+
idx = int(answer)
|
| 240 |
+
labels = answer_choices(normalize_choices(rec.get("choices")))
|
| 241 |
+
if 0 <= idx < len(labels):
|
| 242 |
+
return labels[idx]
|
| 243 |
+
return extract_mcq_answer(str(answer), CHOICES) or str(answer).strip().upper()[:1]
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def get_group_key(args, rec: Dict[str, Any]) -> str:
|
| 247 |
+
if args.bench == "mmsi":
|
| 248 |
+
return str(rec.get("category") or "unknown")
|
| 249 |
+
return mindcube_group(rec)
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
def get_filter_key(args) -> str:
|
| 253 |
+
return "category" if args.bench == "mmsi" else "task"
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
def render_chat_prompt(messages, processor, enable_thinking: bool = False) -> str:
|
| 257 |
+
return processor.apply_chat_template(
|
| 258 |
+
messages,
|
| 259 |
+
tokenize=False,
|
| 260 |
+
add_generation_prompt=True,
|
| 261 |
+
enable_thinking=enable_thinking,
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
def prepare_for_vllm(images: List[Image.Image], prompt: str, processor, args):
|
| 266 |
+
from qwen_vl_utils import process_vision_info
|
| 267 |
+
|
| 268 |
+
content: List[Dict[str, Any]] = []
|
| 269 |
+
for image in images:
|
| 270 |
+
item: Dict[str, Any] = {
|
| 271 |
+
"type": "image",
|
| 272 |
+
"image": image,
|
| 273 |
+
"max_pixels": args.image_max_pixels,
|
| 274 |
+
}
|
| 275 |
+
if args.image_min_pixels > 0:
|
| 276 |
+
item["min_pixels"] = args.image_min_pixels
|
| 277 |
+
content.append(item)
|
| 278 |
+
content.append({"type": "text", "text": prompt})
|
| 279 |
+
messages = [{"role": "user", "content": content}]
|
| 280 |
+
text = render_chat_prompt(messages, processor, args.enable_thinking)
|
| 281 |
+
image_inputs, _video_inputs = process_vision_info(messages, image_patch_size=args.patch_size)
|
| 282 |
+
return {
|
| 283 |
+
"prompt": text,
|
| 284 |
+
"multi_modal_data": {"image": image_inputs},
|
| 285 |
+
"mm_processor_kwargs": {"do_resize": False},
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
def pct(correct: int, total: int) -> float:
|
| 290 |
+
return round(100.0 * correct / total, 2) if total else 0.0
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
def aggregate(args, results: List[Dict[str, Any]]) -> Dict[str, Any]:
|
| 294 |
+
correct = sum(int(r.get("score", 0)) for r in results)
|
| 295 |
+
parsed = sum(1 for r in results if r.get("pred_answer"))
|
| 296 |
+
groups = defaultdict(lambda: {"correct": 0, "total": 0})
|
| 297 |
+
for r in results:
|
| 298 |
+
groups[str(r.get("group") or "unknown")]["total"] += 1
|
| 299 |
+
groups[str(r.get("group") or "unknown")]["correct"] += int(r.get("score", 0))
|
| 300 |
+
group_name = "by_category" if args.bench == "mmsi" else "by_task"
|
| 301 |
+
return {
|
| 302 |
+
"num_samples": len(results),
|
| 303 |
+
"correct": correct,
|
| 304 |
+
"accuracy": pct(correct, len(results)),
|
| 305 |
+
"parse_rate": pct(parsed, len(results)),
|
| 306 |
+
group_name: {
|
| 307 |
+
k: {"accuracy": pct(v["correct"], v["total"]), "correct": v["correct"], "total": v["total"]}
|
| 308 |
+
for k, v in sorted(groups.items())
|
| 309 |
+
},
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
def decode_images_for_record(args, rec: Dict[str, Any]) -> List[Image.Image]:
|
| 314 |
+
if args.bench == "mmsi":
|
| 315 |
+
return decode_mmsi_images(
|
| 316 |
+
rec.get("images") or rec.get("image"),
|
| 317 |
+
args.max_images,
|
| 318 |
+
)
|
| 319 |
+
return decode_mindcube_images(rec, args.max_images, args.min_image_bytes)
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
def evaluate(llm, sampling_params, processor, args) -> Dict[str, Any]:
|
| 323 |
+
records = load_records(args)
|
| 324 |
+
filter_value = args.category if args.bench == "mmsi" else args.task
|
| 325 |
+
if filter_value:
|
| 326 |
+
if args.bench == "mindcube":
|
| 327 |
+
records = [
|
| 328 |
+
record
|
| 329 |
+
for record in records
|
| 330 |
+
if mindcube_group(record) == filter_value
|
| 331 |
+
]
|
| 332 |
+
else:
|
| 333 |
+
key = get_filter_key(args)
|
| 334 |
+
records = [
|
| 335 |
+
record
|
| 336 |
+
for record in records
|
| 337 |
+
if str(record.get(key) or "") == filter_value
|
| 338 |
+
]
|
| 339 |
+
if args.max_samples and args.max_samples > 0:
|
| 340 |
+
records = records[: args.max_samples]
|
| 341 |
+
if args.bench == "mmsi":
|
| 342 |
+
records = shard(records, args.chunk, args.index)
|
| 343 |
+
print(f"Loaded {len(records)} {args.bench} samples for shard {args.index}/{args.chunk}", flush=True)
|
| 344 |
+
|
| 345 |
+
results: List[Dict[str, Any]] = []
|
| 346 |
+
t0 = time.time()
|
| 347 |
+
for start in range(0, len(records), args.batch_size):
|
| 348 |
+
batch = records[start:start + args.batch_size]
|
| 349 |
+
inputs = []
|
| 350 |
+
keep = []
|
| 351 |
+
prompts: Dict[int, str] = {}
|
| 352 |
+
labels_by_j: Dict[int, List[str]] = {}
|
| 353 |
+
for j, rec in enumerate(batch):
|
| 354 |
+
try:
|
| 355 |
+
images = decode_images_for_record(args, rec)
|
| 356 |
+
if not images:
|
| 357 |
+
raise ValueError("no decoded images")
|
| 358 |
+
prompt, labels = build_prompt(args, rec)
|
| 359 |
+
inputs.append(prepare_for_vllm(images, prompt, processor, args))
|
| 360 |
+
keep.append(j)
|
| 361 |
+
prompts[j] = prompt
|
| 362 |
+
labels_by_j[j] = labels
|
| 363 |
+
except Exception as e:
|
| 364 |
+
print(f"[warn] skip sample {start + j}: {type(e).__name__}: {e}", flush=True)
|
| 365 |
+
|
| 366 |
+
outputs = llm.generate(inputs, sampling_params=sampling_params) if inputs else []
|
| 367 |
+
out_by_j = {j: out for j, out in zip(keep, outputs)}
|
| 368 |
+
|
| 369 |
+
for j, rec in enumerate(batch):
|
| 370 |
+
if j not in out_by_j:
|
| 371 |
+
continue
|
| 372 |
+
raw = out_by_j[j].outputs[0].text
|
| 373 |
+
labels = labels_by_j[j]
|
| 374 |
+
pred = extract_mcq_answer(raw, labels)
|
| 375 |
+
gt = get_answer(args, rec)
|
| 376 |
+
score = 1.0 if pred and gt and pred.upper() == gt.upper() else 0.0
|
| 377 |
+
results.append({
|
| 378 |
+
"id": str(rec.get("index") or rec.get("id") or rec.get("sample_id") or start + j),
|
| 379 |
+
"bench": args.bench,
|
| 380 |
+
"group": get_group_key(args, rec),
|
| 381 |
+
"question": rec.get("question"),
|
| 382 |
+
"prompt": prompts.get(j, ""),
|
| 383 |
+
"answer": gt,
|
| 384 |
+
"pred_answer": pred,
|
| 385 |
+
"raw_prediction": raw,
|
| 386 |
+
"score": score,
|
| 387 |
+
})
|
| 388 |
+
|
| 389 |
+
done = min(start + args.batch_size, len(records))
|
| 390 |
+
if done % 200 == 0 or done == len(records):
|
| 391 |
+
elapsed = max(time.time() - t0, 1e-6)
|
| 392 |
+
summary = aggregate(args, results)
|
| 393 |
+
print(
|
| 394 |
+
f"[{done}/{len(records)}] {elapsed:.1f}s "
|
| 395 |
+
f"acc={summary['accuracy']:.2f}% parse={summary['parse_rate']:.2f}%",
|
| 396 |
+
flush=True,
|
| 397 |
+
)
|
| 398 |
+
|
| 399 |
+
full_mindcube_eval = (
|
| 400 |
+
args.bench == "mindcube"
|
| 401 |
+
and args.chunk == 1
|
| 402 |
+
and not args.task
|
| 403 |
+
and not args.max_samples
|
| 404 |
+
)
|
| 405 |
+
if (
|
| 406 |
+
full_mindcube_eval
|
| 407 |
+
and args.expected_samples > 0
|
| 408 |
+
and len(results) != args.expected_samples
|
| 409 |
+
):
|
| 410 |
+
raise RuntimeError(
|
| 411 |
+
f"Expected {args.expected_samples} official MindCube-Tiny "
|
| 412 |
+
f"results, got {len(results)}."
|
| 413 |
+
)
|
| 414 |
+
|
| 415 |
+
out_name = f"results_{args.bench}"
|
| 416 |
+
if args.chunk > 1:
|
| 417 |
+
out_name += f"_shard{args.index}"
|
| 418 |
+
out_path = os.path.join(args.output_dir, out_name + ".json")
|
| 419 |
+
with open(out_path, "w", encoding="utf-8") as f:
|
| 420 |
+
json.dump(results, f, ensure_ascii=False, indent=2)
|
| 421 |
+
|
| 422 |
+
summary = aggregate(args, results)
|
| 423 |
+
if args.bench == "mindcube":
|
| 424 |
+
summary["data_source"] = args.data_file
|
| 425 |
+
if full_mindcube_eval and args.expected_samples > 0:
|
| 426 |
+
summary["expected_samples"] = args.expected_samples
|
| 427 |
+
summary["coverage"] = round(
|
| 428 |
+
100.0 * len(results) / args.expected_samples, 2
|
| 429 |
+
)
|
| 430 |
+
summary_name = f"summary_shard{args.index}.json" if args.chunk > 1 else "summary.json"
|
| 431 |
+
with open(os.path.join(args.output_dir, summary_name), "w", encoding="utf-8") as f:
|
| 432 |
+
json.dump(summary, f, ensure_ascii=False, indent=2)
|
| 433 |
+
print(json.dumps(summary, ensure_ascii=False, indent=2), flush=True)
|
| 434 |
+
return summary
|
| 435 |
+
|
| 436 |
+
|
| 437 |
+
def parse_args():
|
| 438 |
+
p = argparse.ArgumentParser(description="Unified image-sequence MC evaluation via vLLM.")
|
| 439 |
+
p.add_argument("--bench", required=True, choices=["mmsi", "mindcube"])
|
| 440 |
+
p.add_argument("--model_path", required=True)
|
| 441 |
+
p.add_argument("--processor_path", default="")
|
| 442 |
+
p.add_argument("--data_file", default="")
|
| 443 |
+
p.add_argument("--output_dir", required=True)
|
| 444 |
+
p.add_argument("--image_min_pixels", type=int, default=4096)
|
| 445 |
+
p.add_argument("--image_max_pixels", type=int, default=262144)
|
| 446 |
+
p.add_argument("--max_images", type=int, default=8)
|
| 447 |
+
p.add_argument("--min_image_bytes", type=int, default=1024)
|
| 448 |
+
p.add_argument("--patch_size", type=int, default=16)
|
| 449 |
+
p.add_argument("--tensor_parallel_size", type=int, default=1)
|
| 450 |
+
p.add_argument("--gpu_memory_utilization", type=float, default=0.95)
|
| 451 |
+
p.add_argument("--max_model_len", type=int, default=65536)
|
| 452 |
+
p.add_argument("--max_new_tokens", type=int, default=64)
|
| 453 |
+
p.add_argument("--max_num_batched_tokens", type=int, default=65536)
|
| 454 |
+
p.add_argument("--batch_size", type=int, default=16)
|
| 455 |
+
p.add_argument("--max_samples", type=int, default=0)
|
| 456 |
+
p.add_argument("--expected_samples", type=int, default=1050)
|
| 457 |
+
p.add_argument("--hf_cache_dir", default="")
|
| 458 |
+
p.add_argument("--category", default="")
|
| 459 |
+
p.add_argument("--task", default="")
|
| 460 |
+
p.add_argument("--temperature", type=float, default=0.0)
|
| 461 |
+
p.add_argument("--top_p", type=float, default=1.0)
|
| 462 |
+
p.add_argument("--top_k", type=int, default=-1)
|
| 463 |
+
p.add_argument("--chunk", type=int, default=1)
|
| 464 |
+
p.add_argument("--index", type=int, default=0)
|
| 465 |
+
p.add_argument("--enable_thinking", action="store_true")
|
| 466 |
+
return p.parse_args()
|
| 467 |
+
|
| 468 |
+
|
| 469 |
+
def main():
|
| 470 |
+
args = parse_args()
|
| 471 |
+
if args.bench == "mindcube" and not args.data_file:
|
| 472 |
+
args.data_file = OFFICIAL_DATA_SOURCE
|
| 473 |
+
if not args.data_file:
|
| 474 |
+
raise ValueError("--data_file is required for MMSI-Bench")
|
| 475 |
+
args.processor_path = args.processor_path or args.model_path
|
| 476 |
+
os.makedirs(args.output_dir, exist_ok=True)
|
| 477 |
+
|
| 478 |
+
from transformers import AutoProcessor, AutoTokenizer
|
| 479 |
+
from vllm import LLM, SamplingParams
|
| 480 |
+
|
| 481 |
+
processor = AutoProcessor.from_pretrained(
|
| 482 |
+
args.processor_path,
|
| 483 |
+
padding_side="left",
|
| 484 |
+
trust_remote_code=True,
|
| 485 |
+
min_pixels=args.image_min_pixels,
|
| 486 |
+
max_pixels=args.image_max_pixels,
|
| 487 |
+
)
|
| 488 |
+
tokenizer = AutoTokenizer.from_pretrained(args.processor_path, trust_remote_code=True)
|
| 489 |
+
tokenizer.padding_side = "left"
|
| 490 |
+
processor.tokenizer = tokenizer
|
| 491 |
+
|
| 492 |
+
llm = LLM(
|
| 493 |
+
model=args.model_path,
|
| 494 |
+
tensor_parallel_size=args.tensor_parallel_size,
|
| 495 |
+
gpu_memory_utilization=args.gpu_memory_utilization,
|
| 496 |
+
max_model_len=args.max_model_len,
|
| 497 |
+
max_num_batched_tokens=args.max_num_batched_tokens,
|
| 498 |
+
trust_remote_code=True,
|
| 499 |
+
limit_mm_per_prompt={"image": args.max_images},
|
| 500 |
+
)
|
| 501 |
+
sampling = SamplingParams(
|
| 502 |
+
max_tokens=args.max_new_tokens,
|
| 503 |
+
temperature=args.temperature,
|
| 504 |
+
top_p=args.top_p,
|
| 505 |
+
top_k=args.top_k,
|
| 506 |
+
)
|
| 507 |
+
evaluate(llm, sampling, processor, args)
|
| 508 |
+
|
| 509 |
+
|
| 510 |
+
if __name__ == "__main__":
|
| 511 |
+
main()
|
code/eval/task/eval_prompt.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Single source of truth for evaluation prompts.
|
| 3 |
+
|
| 4 |
+
Exactly ONE prompt per task — the one used by ``eval/task/eval.sh`` — kept
|
| 5 |
+
byte-for-byte aligned with the SFT training data in
|
| 6 |
+
``data/joint/sft_joint_all.jsonl``.
|
| 7 |
+
|
| 8 |
+
Task -> prompt mapping (verbatim from sft_joint_all.jsonl):
|
| 9 |
+
|
| 10 |
+
video_qa_mc (videomme, videommmu, mmvu, mvbench, videoholmes,
|
| 11 |
+
longvideobench, lvbench, mlvu)
|
| 12 |
+
"{question}\\nOptions:\\n{opts}\\n" + VIDEO_QA_MC_TAIL
|
| 13 |
+
|
| 14 |
+
spatial_intelligence (vsi)
|
| 15 |
+
MC -> "{q}\\nOptions:\\n{opts}\\n" + VSI_MC_TAIL
|
| 16 |
+
integer -> "{q} " + VSI_INTEGER_TAIL
|
| 17 |
+
meters -> "{q} " + VSI_METERS_TAIL
|
| 18 |
+
cm -> "{q} " + VSI_CENTIMETERS_TAIL
|
| 19 |
+
room m2 -> "{q} " + VSI_SQUARE_METERS_TAIL
|
| 20 |
+
(each prefixed with "These are frames of a video.\\n")
|
| 21 |
+
|
| 22 |
+
sensenova_si / image_sequence_mc (mmsi, mindcube)
|
| 23 |
+
"{q}\\n" + SENSENOVA_MC_TAIL (alias PROMPT_TAIL)
|
| 24 |
+
|
| 25 |
+
spatial grounding (spatial_grounding)
|
| 26 |
+
build_spatial_grounding_prompt(expr) == QWEN_NATIVE_PROMPT_SG
|
| 27 |
+
|
| 28 |
+
tracking
|
| 29 |
+
GROUNDING_QUESTION_TEMPLATE_NO_THINK.format(Question=q) + TRACKING_TAIL
|
| 30 |
+
|
| 31 |
+
stvg
|
| 32 |
+
GROUNDING_QUESTION_TEMPLATE_NO_THINK.format(
|
| 33 |
+
Question=TRAIN_STVG_QUESTION_PREFIX.format(query=q)) + STVG_TAIL
|
| 34 |
+
|
| 35 |
+
segmentation
|
| 36 |
+
"{question}\\n" + TRAIN_SEG_IMAGE_TAIL / TRAIN_SEG_VIDEO_TAIL
|
| 37 |
+
|
| 38 |
+
temporal grounding (temporal_grounding)
|
| 39 |
+
TEMPORAL_GROUNDING_PROMPT.format(event)
|
| 40 |
+
|
| 41 |
+
Keep this file dependency-free (stdlib only) so it can be imported from every
|
| 42 |
+
worker regardless of the runtime environment.
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
# ===========================================================================
|
| 46 |
+
# Multiple-choice / numeric answer tails
|
| 47 |
+
# ===========================================================================
|
| 48 |
+
|
| 49 |
+
# video_qa_mc — videomme, videommmu, mmvu, mvbench, videoholmes,
|
| 50 |
+
# longvideobench, lvbench, mlvu.
|
| 51 |
+
VIDEO_QA_MC_TAIL = (
|
| 52 |
+
"Answer with the option letter only within <answer>...</answer> tags. "
|
| 53 |
+
"Example: <answer>A</answer>"
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
# spatial_intelligence multiple choice — vsi (object_rel_distance,
|
| 57 |
+
# object_rel_direction, obj_appearance_order, route_plan, ...).
|
| 58 |
+
VSI_MC_TAIL = (
|
| 59 |
+
"Answer with the option letter within <answer>...</answer> tags. "
|
| 60 |
+
"Example: <answer>A</answer>"
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
# spatial_intelligence numeric — vsi.
|
| 64 |
+
VSI_INTEGER_TAIL = (
|
| 65 |
+
"Answer with an integer within <answer>...</answer> tags. Example: <answer>3</answer>"
|
| 66 |
+
)
|
| 67 |
+
VSI_METERS_TAIL = (
|
| 68 |
+
"Answer with a number in meters within <answer>...</answer> tags. Example: <answer>2.3</answer>"
|
| 69 |
+
)
|
| 70 |
+
VSI_CENTIMETERS_TAIL = (
|
| 71 |
+
"Answer with a number in centimeters within <answer>...</answer> tags. Example: <answer>120.5</answer>"
|
| 72 |
+
)
|
| 73 |
+
VSI_SQUARE_METERS_TAIL = (
|
| 74 |
+
"Answer with a number in square meters within <answer>...</answer> tags. Example: <answer>25.5</answer>"
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
# sensenova_si / image_sequence_mc_answer_only — mmsi, mindcube.
|
| 78 |
+
SENSENOVA_MC_TAIL = (
|
| 79 |
+
"Choose the best answer from the options. "
|
| 80 |
+
"Put exactly one uppercase option letter inside <answer>...</answer> "
|
| 81 |
+
"Do not explain. Example: <answer>A</answer>"
|
| 82 |
+
)
|
| 83 |
+
# Backwards-compatible alias used throughout eval_vllm.py.
|
| 84 |
+
PROMPT_TAIL = SENSENOVA_MC_TAIL
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
# ===========================================================================
|
| 88 |
+
# Tracking
|
| 89 |
+
# ===========================================================================
|
| 90 |
+
|
| 91 |
+
# Question scaffold shared by tracking + stvg (the tail carries the full
|
| 92 |
+
# answer-format spec).
|
| 93 |
+
GROUNDING_QUESTION_TEMPLATE_NO_THINK = (
|
| 94 |
+
"{Question}\n"
|
| 95 |
+
"Please answer this question based on the visual content. "
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
TRACKING_TAIL = (
|
| 99 |
+
"Please track the target object throughout the video and provide one bounding box per second, "
|
| 100 |
+
"ONLY up to 32 seconds, within the <answer>...</answer> tags.\n"
|
| 101 |
+
"Example:\n"
|
| 102 |
+
"<answer>{\"boxes\": {\"1\": [405, 230, 654, 463], \"2\": [435, 223, 678, 446], "
|
| 103 |
+
"\"32\": [415, 203, 691, 487]}}</answer>\n"
|
| 104 |
+
"Note: Each key in 'boxes' must correspond to a second (1, 2, 3, ..., 32) "
|
| 105 |
+
"and contain a 4-number bounding box [x1, y1, x2, y2]."
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
# ===========================================================================
|
| 110 |
+
# Spatial-temporal grounding (STVG)
|
| 111 |
+
# ===========================================================================
|
| 112 |
+
|
| 113 |
+
STVG_TAIL = (
|
| 114 |
+
"Please provide only the time span in seconds and bounding boxes as JSON "
|
| 115 |
+
"within the <answer>...</answer> tags.\n"
|
| 116 |
+
"You MUST output one bounding box for every integer second within the "
|
| 117 |
+
"given time span (inclusive).\n"
|
| 118 |
+
"Example:\n"
|
| 119 |
+
"<answer>{\"time\": [8.1, 13.5], \"boxes\": {\"9\": [317, 422, 582, 997], "
|
| 120 |
+
"\"10\": [332, 175, 442, 369], \"11\": [340, 180, 450, 370]}}</answer>\n"
|
| 121 |
+
"Note: Each key in 'boxes' must be an integer second within the span, "
|
| 122 |
+
"and its value must be a 4-number bounding box [x1, y1, x2, y2]."
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
# Rewrites an eval question into the exact wording used during STVG training.
|
| 126 |
+
TRAIN_STVG_QUESTION_PREFIX = (
|
| 127 |
+
'Given the query "{query}", when and where does the described content '
|
| 128 |
+
'occur in the video? please firstly give the start and end time, spatial '
|
| 129 |
+
'bounding box corresponding to each integer second.'
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
# ===========================================================================
|
| 134 |
+
# Segmentation
|
| 135 |
+
# ===========================================================================
|
| 136 |
+
|
| 137 |
+
# Matches sft_joint_all.jsonl seg_image / seg_video samples.
|
| 138 |
+
TRAIN_SEG_IMAGE_TAIL = (
|
| 139 |
+
"Please answer this question based on the visual content. "
|
| 140 |
+
"This task prepares inputs for image object segmentation with a specialized model (e.g., SAM2).\n"
|
| 141 |
+
"Please provide ONE bounding box, 3 positive points (clearly INSIDE the object), "
|
| 142 |
+
"and 3 negative points (clearly OUTSIDE the object) within the <answer>...</answer> tags.\n"
|
| 143 |
+
"Choose informative points that help distinguish object vs. background. Prefer negatives on clear non-object "
|
| 144 |
+
"pixels INSIDE the box when safe; otherwise place them just outside on obvious background. "
|
| 145 |
+
"Negatives must NEVER be on the object or on its boundary.\n"
|
| 146 |
+
"Example: <answer>{\"boxes\": [x1, y1, x2, y2], \"positive_points\": [[x,y],[x,y],[x,y]], "
|
| 147 |
+
"\"negative_points\": [[x,y],[x,y],[x,y]]}</answer>"
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
TRAIN_SEG_VIDEO_TAIL = (
|
| 151 |
+
"Please answer this question based on the visual content. "
|
| 152 |
+
"This task prepares inputs for video object segmentation with a specialized model (e.g., SAM2).\n"
|
| 153 |
+
"Please select ONE representative time (in seconds), and provide ONE bounding box, "
|
| 154 |
+
"3 positive points (clearly INSIDE the object), and 3 negative points (clearly OUTSIDE the object) "
|
| 155 |
+
"within the <answer>...</answer> tags.\n"
|
| 156 |
+
"Choose informative points that help distinguish object vs. background. Prefer negatives on clear non-object "
|
| 157 |
+
"pixels INSIDE the box when safe; otherwise place them just outside on obvious background. "
|
| 158 |
+
"Negatives must NEVER be on the object or on its boundary.\n"
|
| 159 |
+
"Example: <answer>{\"time\": <time_in_seconds>, \"boxes\": [x1, y1, x2, y2], "
|
| 160 |
+
"\"positive_points\": [[x,y],[x,y],[x,y]], \"negative_points\": [[x,y],[x,y],[x,y]]}</answer>"
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
# ===========================================================================
|
| 165 |
+
# Spatial grounding (RefCOCO / spatial_grounding)
|
| 166 |
+
# ===========================================================================
|
| 167 |
+
|
| 168 |
+
# Matches sft_joint_all.jsonl spatial grounding samples after the <image> token.
|
| 169 |
+
QWEN_NATIVE_PROMPT_SG = (
|
| 170 |
+
'Locate "{}" in the image. Output its bounding box in JSON format '
|
| 171 |
+
'within <answer>...</answer> tags. '
|
| 172 |
+
'Example: <answer>[{{"bbox_2d": [123, 30, 404, 846]}}]</answer>'
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
# ===========================================================================
|
| 177 |
+
# Temporal grounding (TimeLens / temporal_grounding)
|
| 178 |
+
# ===========================================================================
|
| 179 |
+
|
| 180 |
+
# Matches sft_joint_all.jsonl temporal grounding samples (after <video>).
|
| 181 |
+
TEMPORAL_GROUNDING_PROMPT = (
|
| 182 |
+
'To accurately pinpoint the event "{}" in the video, '
|
| 183 |
+
"determine the precise time period of the event. "
|
| 184 |
+
"Provide the start and end times (in seconds) "
|
| 185 |
+
'in the format "start time to end time" within <answer> </answer> tags. '
|
| 186 |
+
"Example: <answer> 12 to 18 </answer>"
|
| 187 |
+
)
|
| 188 |
+
# Exact prompt used by the official TimeLens evaluation.
|
| 189 |
+
TIMELENS_OFFICIAL_PROMPT = (
|
| 190 |
+
"Please find the visual event described by the sentence '{}', "
|
| 191 |
+
"determining its starting and ending times. "
|
| 192 |
+
"The format should be: 'The event happens in <start time> - <end time> seconds'."
|
| 193 |
+
)
|
| 194 |
+
# Alias used by eval_timelens_hf.py.
|
| 195 |
+
PROMPT_WO_THINK = TEMPORAL_GROUNDING_PROMPT
|
| 196 |
+
TEMPORAL_GROUNDING_PROMPT_MODES = {
|
| 197 |
+
"same": TEMPORAL_GROUNDING_PROMPT,
|
| 198 |
+
"timelens_official": TIMELENS_OFFICIAL_PROMPT,
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
# ===========================================================================
|
| 203 |
+
# Builder helpers
|
| 204 |
+
# ===========================================================================
|
| 205 |
+
|
| 206 |
+
def build_video_qa_mc_prompt(question, options_block):
|
| 207 |
+
"""`{question}\\nOptions:\\n{options_block}\\n{VIDEO_QA_MC_TAIL}`."""
|
| 208 |
+
return f"{question}\nOptions:\n{options_block}\n{VIDEO_QA_MC_TAIL}"
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
def build_sensenova_mc_prompt(question):
|
| 212 |
+
"""`{question}\\n{SENSENOVA_MC_TAIL}`."""
|
| 213 |
+
return f"{question}\n{SENSENOVA_MC_TAIL}"
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def build_spatial_grounding_prompt(expression):
|
| 217 |
+
"""Training-aligned spatial grounding prompt."""
|
| 218 |
+
expression = (expression or "").strip()
|
| 219 |
+
if expression and expression[-1] not in ".?!":
|
| 220 |
+
expression += "."
|
| 221 |
+
return QWEN_NATIVE_PROMPT_SG.format(expression)
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def build_tracking_prompt(question):
|
| 225 |
+
return GROUNDING_QUESTION_TEMPLATE_NO_THINK.format(Question=question) + TRACKING_TAIL
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def build_stvg_prompt(raw_query):
|
| 229 |
+
q = TRAIN_STVG_QUESTION_PREFIX.format(query=raw_query)
|
| 230 |
+
return GROUNDING_QUESTION_TEMPLATE_NO_THINK.format(Question=q) + STVG_TAIL
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
def build_seg_prompt(question, data_type):
|
| 234 |
+
tail = TRAIN_SEG_VIDEO_TAIL if str(data_type).strip().lower() == "video" else TRAIN_SEG_IMAGE_TAIL
|
| 235 |
+
return f"{question}\n{tail}"
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
def build_temporal_grounding_prompt(event, prompt_mode="same"):
|
| 239 |
+
try:
|
| 240 |
+
template = TEMPORAL_GROUNDING_PROMPT_MODES[prompt_mode]
|
| 241 |
+
except KeyError as exc:
|
| 242 |
+
choices = ", ".join(sorted(TEMPORAL_GROUNDING_PROMPT_MODES))
|
| 243 |
+
raise ValueError(
|
| 244 |
+
f"Unknown temporal grounding prompt mode {prompt_mode!r}; "
|
| 245 |
+
f"choose one of: {choices}"
|
| 246 |
+
) from exc
|
| 247 |
+
return template.format(event)
|
code/eval/task/eval_revsi_multinode.sh
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# ReVSI evaluation using the same vLLM multi-node sharding path as VSI-Bench.
|
| 3 |
+
set -euo pipefail
|
| 4 |
+
|
| 5 |
+
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
| 6 |
+
PROJECT_DIR=$(cd -- "${SCRIPT_DIR}/../.." && pwd)
|
| 7 |
+
RUNNER="${SCRIPT_DIR}/revsi/run_eval_vllm.sh"
|
| 8 |
+
MERGER="${SCRIPT_DIR}/revsi/merge_multinode_shards.py"
|
| 9 |
+
PYTHON_BIN=${PYTHON_BIN:-$(command -v python)}
|
| 10 |
+
HOSTFILE=${HOSTFILE:-}
|
| 11 |
+
NUM_NODES=${NUM_NODES:-4}
|
| 12 |
+
GPUS_PER_NODE=${GPUS_PER_NODE:-8}
|
| 13 |
+
|
| 14 |
+
MODEL=${MODEL:-}
|
| 15 |
+
MODEL=${MODEL%/}
|
| 16 |
+
REVSI_ROOT=${REVSI_ROOT:-}
|
| 17 |
+
FRAME_BUDGET=${REVSI_FRAME_BUDGET:-all}
|
| 18 |
+
FRAME_DIR="${REVSI_ROOT:+${REVSI_ROOT}/${FRAME_BUDGET}_frame}"
|
| 19 |
+
QA_FILE=${REVSI_QA_FILE:-"${FRAME_DIR:+${FRAME_DIR}/test-00000-of-00001.parquet}"}
|
| 20 |
+
VIDEO_ROOT=${REVSI_VIDEO_ROOT:-${REVSI_ROOT}}
|
| 21 |
+
TASK_FILTER=${REVSI_TASK_FILTER:-}
|
| 22 |
+
MAX_SAMPLES=${REVSI_MAX_SAMPLES:-0}
|
| 23 |
+
REMOTE_SETUP=${REMOTE_SETUP:-}
|
| 24 |
+
SSH_STRICT_HOST_KEY_CHECKING=${SSH_STRICT_HOST_KEY_CHECKING:-yes}
|
| 25 |
+
|
| 26 |
+
case "${FRAME_BUDGET}" in
|
| 27 |
+
16|32|64|all) ;;
|
| 28 |
+
*)
|
| 29 |
+
echo "[FATAL] REVSI_FRAME_BUDGET must be 16, 32, 64, or all." >&2
|
| 30 |
+
exit 2
|
| 31 |
+
;;
|
| 32 |
+
esac
|
| 33 |
+
[[ -n "${MODEL}" ]] || {
|
| 34 |
+
echo "[FATAL] Set MODEL to the model directory." >&2
|
| 35 |
+
exit 2
|
| 36 |
+
}
|
| 37 |
+
[[ -n "${QA_FILE}" ]] || {
|
| 38 |
+
echo "[FATAL] Set REVSI_QA_FILE or REVSI_ROOT." >&2
|
| 39 |
+
exit 2
|
| 40 |
+
}
|
| 41 |
+
[[ -n "${HOSTFILE}" ]] || {
|
| 42 |
+
echo "[FATAL] Set HOSTFILE to one host per line." >&2
|
| 43 |
+
exit 2
|
| 44 |
+
}
|
| 45 |
+
for path in \
|
| 46 |
+
"${RUNNER}" "${MERGER}" "${HOSTFILE}" "${MODEL}/config.json" "${QA_FILE}"; do
|
| 47 |
+
[[ -f "${path}" ]] || {
|
| 48 |
+
echo "[FATAL] Required input is unavailable: ${path}" >&2
|
| 49 |
+
exit 1
|
| 50 |
+
}
|
| 51 |
+
done
|
| 52 |
+
if [[ "${QA_FILE}" == *.parquet ]]; then
|
| 53 |
+
[[ -n "${FRAME_DIR}" ]] || {
|
| 54 |
+
echo "[FATAL] Parquet input requires REVSI_ROOT." >&2
|
| 55 |
+
exit 2
|
| 56 |
+
}
|
| 57 |
+
compgen -G "${FRAME_DIR}/*.mp4" >/dev/null || {
|
| 58 |
+
echo "[FATAL] No ReVSI videos found under ${FRAME_DIR}." >&2
|
| 59 |
+
exit 1
|
| 60 |
+
}
|
| 61 |
+
fi
|
| 62 |
+
|
| 63 |
+
if [[ -n "${REVSI_EXPECTED_SAMPLES:-}" ]]; then
|
| 64 |
+
EXPECTED_SAMPLES=${REVSI_EXPECTED_SAMPLES}
|
| 65 |
+
elif [[ -z "${TASK_FILTER}" && "${MAX_SAMPLES}" == "0" ]]; then
|
| 66 |
+
EXPECTED_SAMPLES=6808
|
| 67 |
+
else
|
| 68 |
+
EXPECTED_SAMPLES=0
|
| 69 |
+
fi
|
| 70 |
+
|
| 71 |
+
mapfile -t AVAILABLE_HOSTS < <(awk 'NF && !seen[$1]++ {print $1}' "${HOSTFILE}")
|
| 72 |
+
[[ "${NUM_NODES}" =~ ^[1-9][0-9]*$ ]] || {
|
| 73 |
+
echo "[FATAL] NUM_NODES must be a positive integer." >&2
|
| 74 |
+
exit 2
|
| 75 |
+
}
|
| 76 |
+
((NUM_NODES <= ${#AVAILABLE_HOSTS[@]})) || {
|
| 77 |
+
echo "[FATAL] Requested ${NUM_NODES} nodes, only ${#AVAILABLE_HOSTS[@]} available." >&2
|
| 78 |
+
exit 2
|
| 79 |
+
}
|
| 80 |
+
HOSTS=("${AVAILABLE_HOSTS[@]:0:NUM_NODES}")
|
| 81 |
+
GLOBAL_SHARDS=$((NUM_NODES * GPUS_PER_NODE))
|
| 82 |
+
|
| 83 |
+
checkpoint_dir=$(dirname "$(dirname "${MODEL%/}")")
|
| 84 |
+
checkpoint_tag=$(basename "${checkpoint_dir}")
|
| 85 |
+
experiment_tag=$(basename "$(dirname "${checkpoint_dir}")")
|
| 86 |
+
timestamp=$(date +%Y%m%d_%H%M%S)
|
| 87 |
+
EVAL_MAX_FRAMES=${REVSI_MAX_FRAMES:-${FRAME_BUDGET}}
|
| 88 |
+
EVAL_EXACT_NFRAMES=${REVSI_EXACT_NFRAMES:-1}
|
| 89 |
+
if [[ "${FRAME_BUDGET}" == "all" ]]; then
|
| 90 |
+
EVAL_MAX_FRAMES=${REVSI_MAX_FRAMES:-128}
|
| 91 |
+
fi
|
| 92 |
+
setting_tag="revsi-${FRAME_BUDGET}frame-multinode${NUM_NODES}x${GPUS_PER_NODE}-f${EVAL_MAX_FRAMES}-exact${EVAL_EXACT_NFRAMES}-fps${REVSI_FPS:-2}-total${REVSI_VIDEO_TOTAL_PIXELS:-16777216}"
|
| 93 |
+
OUTPUT_DIR=${OUTPUT_DIR:-"${PROJECT_DIR}/outputs/${experiment_tag}/spatial_intelligence/revsi/${checkpoint_tag}/${setting_tag}/${timestamp}"}
|
| 94 |
+
LOG_DIR=${LOG_DIR:-"${PROJECT_DIR}/logs/revsi_multinode/${timestamp}"}
|
| 95 |
+
mkdir -p "${OUTPUT_DIR}" "${LOG_DIR}"
|
| 96 |
+
|
| 97 |
+
SSH_OPTS=(-o "StrictHostKeyChecking=${SSH_STRICT_HOST_KEY_CHECKING}" -o BatchMode=yes -o ServerAliveInterval=60)
|
| 98 |
+
WORKER_PATTERN='eval/task/revsi/eval_revsi_vllm.py'
|
| 99 |
+
|
| 100 |
+
run_on_host() {
|
| 101 |
+
local node=$1
|
| 102 |
+
shift
|
| 103 |
+
if ((node == 0)); then
|
| 104 |
+
bash -s <<<"$*"
|
| 105 |
+
else
|
| 106 |
+
ssh "${SSH_OPTS[@]}" "${HOSTS[node]}" bash -s <<<"$*"
|
| 107 |
+
fi
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
kill_stale_workers() {
|
| 111 |
+
local node reap_pids=() command
|
| 112 |
+
command="pkill -f $(printf %q "${WORKER_PATTERN}") >/dev/null 2>&1 || true; sleep 5; pkill -9 -f $(printf %q "${WORKER_PATTERN}") >/dev/null 2>&1 || true; sleep 3"
|
| 113 |
+
for ((node = 0; node < NUM_NODES; node++)); do
|
| 114 |
+
run_on_host "${node}" "${command}" >/dev/null 2>&1 &
|
| 115 |
+
reap_pids+=("$!")
|
| 116 |
+
done
|
| 117 |
+
wait "${reap_pids[@]}" 2>/dev/null || true
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
require_free_gpus() {
|
| 121 |
+
local node free bad=0
|
| 122 |
+
local need_gib=${REVSI_MIN_FREE_GIB:-75}
|
| 123 |
+
for ((node = 0; node < NUM_NODES; node++)); do
|
| 124 |
+
free=$(run_on_host "${node}" \
|
| 125 |
+
"nvidia-smi --query-gpu=memory.free --format=csv,noheader,nounits | sort -n | awk 'NR==1 {print}'" 2>/dev/null |
|
| 126 |
+
tr -d '\r' | awk '/^[0-9]+$/ {value=$0} END {print value}')
|
| 127 |
+
if [[ -z "${free}" ]]; then
|
| 128 |
+
echo "[FATAL] node=${node} host=${HOSTS[node]}: cannot query GPU memory." >&2
|
| 129 |
+
bad=1
|
| 130 |
+
elif ((free / 1024 < need_gib)); then
|
| 131 |
+
echo "[FATAL] node=${node} host=${HOSTS[node]}: only $((free / 1024)) GiB free (need ${need_gib} GiB)." >&2
|
| 132 |
+
bad=1
|
| 133 |
+
fi
|
| 134 |
+
done
|
| 135 |
+
((bad == 0)) || exit 1
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
pids=()
|
| 139 |
+
cleanup() {
|
| 140 |
+
local status=$?
|
| 141 |
+
trap - EXIT INT TERM
|
| 142 |
+
for pid in "${pids[@]:-}"; do
|
| 143 |
+
[[ -n "${pid}" ]] && kill "${pid}" >/dev/null 2>&1 || true
|
| 144 |
+
done
|
| 145 |
+
((status == 0)) || kill_stale_workers
|
| 146 |
+
exit "${status}"
|
| 147 |
+
}
|
| 148 |
+
trap cleanup EXIT INT TERM
|
| 149 |
+
|
| 150 |
+
kill_stale_workers
|
| 151 |
+
require_free_gpus
|
| 152 |
+
|
| 153 |
+
dispatch() {
|
| 154 |
+
local node=$1
|
| 155 |
+
local offset=$((node * GPUS_PER_NODE))
|
| 156 |
+
local gpu_ids
|
| 157 |
+
gpu_ids=$(seq -s, 0 $((GPUS_PER_NODE - 1)))
|
| 158 |
+
local env_values=(
|
| 159 |
+
"CUDA_VISIBLE_DEVICES=${gpu_ids}"
|
| 160 |
+
"REVSI_ROOT=${REVSI_ROOT}"
|
| 161 |
+
"FRAME_BUDGET=${FRAME_BUDGET}"
|
| 162 |
+
"QA_FILE=${QA_FILE}"
|
| 163 |
+
"VIDEO_ROOT=${VIDEO_ROOT}"
|
| 164 |
+
"OUTPUT_DIR=${OUTPUT_DIR}"
|
| 165 |
+
"GLOBAL_SHARD_COUNT=${GLOBAL_SHARDS}"
|
| 166 |
+
"GLOBAL_SHARD_OFFSET=${offset}"
|
| 167 |
+
"MERGE_SHARDS=0"
|
| 168 |
+
"EXPECTED_SAMPLES=0"
|
| 169 |
+
"TASK_FILTER=${TASK_FILTER}"
|
| 170 |
+
"MAX_SAMPLES=${MAX_SAMPLES}"
|
| 171 |
+
"MEDIA_MODE=video"
|
| 172 |
+
"TP_SIZE=1"
|
| 173 |
+
"BATCH_SIZE=${REVSI_BATCH_SIZE:-16}"
|
| 174 |
+
"MAX_FRAMES=${EVAL_MAX_FRAMES}"
|
| 175 |
+
"EXACT_NFRAMES=${EVAL_EXACT_NFRAMES}"
|
| 176 |
+
"FPS=${REVSI_FPS:-2}"
|
| 177 |
+
"VIDEO_MIN_PIXELS=${REVSI_VIDEO_MIN_PIXELS:-65536}"
|
| 178 |
+
"VIDEO_MAX_PIXELS=${REVSI_VIDEO_MAX_PIXELS:-}"
|
| 179 |
+
"VIDEO_TOTAL_PIXELS=${REVSI_VIDEO_TOTAL_PIXELS:-16777216}"
|
| 180 |
+
"MAX_MODEL_LEN=${REVSI_MAX_MODEL_LEN:-32768}"
|
| 181 |
+
"MAX_NEW_TOKENS=${REVSI_MAX_NEW_TOKENS:-64}"
|
| 182 |
+
"GPU_MEM_UTIL=${REVSI_GPU_MEMORY_UTILIZATION:-0.90}"
|
| 183 |
+
"STRICT_NUMERIC_PROMPT=${REVSI_STRICT_NUMERIC_PROMPT:-0}"
|
| 184 |
+
"ENABLE_THINKING=${REVSI_ENABLE_THINKING:-false}"
|
| 185 |
+
"SCORE_LOG_INTERVAL=${REVSI_SCORE_LOG_INTERVAL:-50}"
|
| 186 |
+
"LAUNCH_DELAY=2"
|
| 187 |
+
"VLLM_BASE_PORT=49000"
|
| 188 |
+
)
|
| 189 |
+
local prefix="" value
|
| 190 |
+
for value in "${env_values[@]}"; do
|
| 191 |
+
prefix+="$(printf %q "${value}") "
|
| 192 |
+
done
|
| 193 |
+
local setup_prefix=""
|
| 194 |
+
if [[ -n "${REMOTE_SETUP}" ]]; then
|
| 195 |
+
setup_prefix="${REMOTE_SETUP} && "
|
| 196 |
+
fi
|
| 197 |
+
local command="${setup_prefix}cd $(printf %q "${PROJECT_DIR}") && ${prefix}bash $(printf %q "${RUNNER}") $(printf %q "${MODEL}")"
|
| 198 |
+
local log="${LOG_DIR}/node${node}.log"
|
| 199 |
+
echo "[revsi] node=${node}/${NUM_NODES} host=${HOSTS[node]} shards=${offset}-$((offset + GPUS_PER_NODE - 1)) log=${log}"
|
| 200 |
+
if ((node == 0)); then
|
| 201 |
+
bash -lc "${command}" 2>&1 | tee "${log}"
|
| 202 |
+
else
|
| 203 |
+
ssh "${SSH_OPTS[@]}" "${HOSTS[node]}" \
|
| 204 |
+
"bash -lc $(printf %q "${command}")" >"${log}" 2>&1 &
|
| 205 |
+
pids+=("$!")
|
| 206 |
+
fi
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
for ((node = 1; node < NUM_NODES; node++)); do
|
| 210 |
+
dispatch "${node}"
|
| 211 |
+
done
|
| 212 |
+
dispatch 0
|
| 213 |
+
|
| 214 |
+
status=0
|
| 215 |
+
for pid in "${pids[@]}"; do
|
| 216 |
+
if ! wait "${pid}"; then status=1; fi
|
| 217 |
+
done
|
| 218 |
+
pids=()
|
| 219 |
+
((status == 0)) || {
|
| 220 |
+
echo "[FATAL] One or more ReVSI nodes failed; inspect ${LOG_DIR}." >&2
|
| 221 |
+
exit "${status}"
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
"${PYTHON_BIN}" "${MERGER}" \
|
| 225 |
+
--output-dir "${OUTPUT_DIR}" \
|
| 226 |
+
--num-shards "${GLOBAL_SHARDS}" \
|
| 227 |
+
--expected-samples "${EXPECTED_SAMPLES}"
|
| 228 |
+
chmod -R a+rX "${OUTPUT_DIR}" 2>/dev/null || true
|
| 229 |
+
echo "[revsi] Multi-node evaluation complete: ${OUTPUT_DIR}/summary.json"
|
code/eval/task/eval_vllm.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
code/eval/task/mindcube/data_utils.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Official MindCube-Tiny data loading and record normalization."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import io
|
| 6 |
+
import re
|
| 7 |
+
import sys
|
| 8 |
+
from collections import Counter
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from typing import Any, Dict, List
|
| 11 |
+
|
| 12 |
+
from PIL import Image
|
| 13 |
+
|
| 14 |
+
_TASK_DIR = Path(__file__).resolve().parents[1]
|
| 15 |
+
if str(_TASK_DIR) not in sys.path:
|
| 16 |
+
sys.path.insert(0, str(_TASK_DIR))
|
| 17 |
+
|
| 18 |
+
from canonical_data import load_json_records # noqa: E402
|
| 19 |
+
|
| 20 |
+
OFFICIAL_DATA_SOURCE = (
|
| 21 |
+
"hf://datasets/oscarqjh/MindCube_lmmseval"
|
| 22 |
+
"@7dd2725d9bd4149f2aad00a9843f72a3824da003/tiny/combined"
|
| 23 |
+
)
|
| 24 |
+
OFFICIAL_EXPECTED_SAMPLES = 1050
|
| 25 |
+
OFFICIAL_GROUP_COUNTS = {
|
| 26 |
+
"rotation": 200,
|
| 27 |
+
"among": 600,
|
| 28 |
+
"around": 250,
|
| 29 |
+
}
|
| 30 |
+
HF_DATASET_PREFIX = "hf://datasets/"
|
| 31 |
+
CHOICES = list("ABCDEFGH")
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def parse_hf_dataset_source(
|
| 35 |
+
source: str,
|
| 36 |
+
) -> tuple[str, str | None, str, str]:
|
| 37 |
+
"""Parse hf://datasets/<org>/<name>[@revision]/<config>/<split>."""
|
| 38 |
+
if not source.startswith(HF_DATASET_PREFIX):
|
| 39 |
+
raise ValueError(f"Not a Hugging Face dataset source: {source}")
|
| 40 |
+
payload = source[len(HF_DATASET_PREFIX) :]
|
| 41 |
+
try:
|
| 42 |
+
dataset_spec, config, split = payload.rsplit("/", 2)
|
| 43 |
+
except ValueError as error:
|
| 44 |
+
raise ValueError(
|
| 45 |
+
"MindCube Hugging Face source must use "
|
| 46 |
+
"hf://datasets/<org>/<name>/<config>/<split>"
|
| 47 |
+
) from error
|
| 48 |
+
dataset_name, separator, revision = dataset_spec.rpartition("@")
|
| 49 |
+
if not separator:
|
| 50 |
+
dataset_name = dataset_spec
|
| 51 |
+
revision = None
|
| 52 |
+
if not dataset_name or "/" not in dataset_name or not config or not split:
|
| 53 |
+
raise ValueError(f"Invalid MindCube Hugging Face source: {source}")
|
| 54 |
+
return dataset_name, revision, config, split
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def mindcube_group(record: Dict[str, Any]) -> str:
|
| 58 |
+
"""Return one of the official Rotation/Among/Around settings."""
|
| 59 |
+
for key in ("task", "setting"):
|
| 60 |
+
value = str(record.get(key) or "").strip().lower()
|
| 61 |
+
if value in OFFICIAL_GROUP_COUNTS:
|
| 62 |
+
return value
|
| 63 |
+
|
| 64 |
+
sample_id = str(
|
| 65 |
+
record.get("id")
|
| 66 |
+
or record.get("index")
|
| 67 |
+
or record.get("sample_id")
|
| 68 |
+
or ""
|
| 69 |
+
).strip().lower()
|
| 70 |
+
match = re.match(r"^(rotation|among|around)(?:_|$)", sample_id)
|
| 71 |
+
return match.group(1) if match else "unknown"
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _validate_records(
|
| 75 |
+
records: Any,
|
| 76 |
+
expected_samples: int,
|
| 77 |
+
) -> None:
|
| 78 |
+
if expected_samples <= 0:
|
| 79 |
+
return
|
| 80 |
+
actual = len(records)
|
| 81 |
+
if actual != expected_samples:
|
| 82 |
+
raise RuntimeError(
|
| 83 |
+
f"Expected {expected_samples} official MindCube-Tiny samples, "
|
| 84 |
+
f"got {actual}. Refusing to evaluate a reduced or different split."
|
| 85 |
+
)
|
| 86 |
+
if expected_samples != OFFICIAL_EXPECTED_SAMPLES:
|
| 87 |
+
return
|
| 88 |
+
|
| 89 |
+
if hasattr(records, "column_names") and "id" in records.column_names:
|
| 90 |
+
groups = Counter(
|
| 91 |
+
mindcube_group({"id": sample_id}) for sample_id in records["id"]
|
| 92 |
+
)
|
| 93 |
+
else:
|
| 94 |
+
groups = Counter(mindcube_group(record) for record in records)
|
| 95 |
+
actual_groups = {
|
| 96 |
+
group: groups.get(group, 0) for group in OFFICIAL_GROUP_COUNTS
|
| 97 |
+
}
|
| 98 |
+
if actual_groups != OFFICIAL_GROUP_COUNTS or groups.get("unknown", 0):
|
| 99 |
+
raise RuntimeError(
|
| 100 |
+
"Official MindCube-Tiny must contain "
|
| 101 |
+
f"{OFFICIAL_GROUP_COUNTS}, got {dict(groups)}."
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def load_mindcube_records(
|
| 106 |
+
source: str,
|
| 107 |
+
*,
|
| 108 |
+
chunk: int = 1,
|
| 109 |
+
index: int = 0,
|
| 110 |
+
expected_samples: int = OFFICIAL_EXPECTED_SAMPLES,
|
| 111 |
+
cache_dir: str | None = None,
|
| 112 |
+
) -> List[Dict[str, Any]]:
|
| 113 |
+
"""Load and shard the official split before decoding its images."""
|
| 114 |
+
if chunk <= 0 or index < 0 or index >= chunk:
|
| 115 |
+
raise ValueError(f"Invalid shard index {index}/{chunk}")
|
| 116 |
+
|
| 117 |
+
if source.startswith(HF_DATASET_PREFIX):
|
| 118 |
+
from datasets import load_dataset
|
| 119 |
+
|
| 120 |
+
dataset_name, revision, config, split = parse_hf_dataset_source(
|
| 121 |
+
source
|
| 122 |
+
)
|
| 123 |
+
records = load_dataset(
|
| 124 |
+
dataset_name,
|
| 125 |
+
config,
|
| 126 |
+
split=split,
|
| 127 |
+
revision=revision,
|
| 128 |
+
cache_dir=cache_dir or None,
|
| 129 |
+
)
|
| 130 |
+
_validate_records(records, expected_samples)
|
| 131 |
+
if chunk > 1:
|
| 132 |
+
records = records.shard(
|
| 133 |
+
num_shards=chunk,
|
| 134 |
+
index=index,
|
| 135 |
+
contiguous=False,
|
| 136 |
+
)
|
| 137 |
+
return [records[row_index] for row_index in range(len(records))]
|
| 138 |
+
|
| 139 |
+
if source.endswith((".jsonl", ".json")):
|
| 140 |
+
records = load_json_records(source)
|
| 141 |
+
_validate_records(records, expected_samples)
|
| 142 |
+
return records[index::chunk]
|
| 143 |
+
|
| 144 |
+
import pandas as pd
|
| 145 |
+
|
| 146 |
+
if not source.endswith(".parquet"):
|
| 147 |
+
raise ValueError(
|
| 148 |
+
"MindCube data must be the official Hugging Face source or a "
|
| 149 |
+
f"local official parquet, got: {source}"
|
| 150 |
+
)
|
| 151 |
+
records = pd.read_parquet(source).to_dict("records")
|
| 152 |
+
_validate_records(records, expected_samples)
|
| 153 |
+
return records[index::chunk]
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def decode_hf_image(
|
| 157 |
+
value: Any,
|
| 158 |
+
min_image_bytes: int,
|
| 159 |
+
) -> Image.Image | None:
|
| 160 |
+
if value is None:
|
| 161 |
+
return None
|
| 162 |
+
if isinstance(value, Image.Image):
|
| 163 |
+
return value.convert("RGB")
|
| 164 |
+
|
| 165 |
+
raw = None
|
| 166 |
+
path = None
|
| 167 |
+
if isinstance(value, dict):
|
| 168 |
+
raw = value.get("bytes")
|
| 169 |
+
path = value.get("path")
|
| 170 |
+
elif isinstance(value, (bytes, bytearray, memoryview)):
|
| 171 |
+
raw = value
|
| 172 |
+
elif isinstance(value, str):
|
| 173 |
+
path = value
|
| 174 |
+
|
| 175 |
+
if raw is None and path:
|
| 176 |
+
return Image.open(path).convert("RGB")
|
| 177 |
+
if raw is None:
|
| 178 |
+
return None
|
| 179 |
+
raw = bytes(raw)
|
| 180 |
+
if min_image_bytes > 0 and len(raw) < min_image_bytes:
|
| 181 |
+
return None
|
| 182 |
+
return Image.open(io.BytesIO(raw)).convert("RGB")
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def decode_mindcube_images(
|
| 186 |
+
record: Dict[str, Any],
|
| 187 |
+
max_images: int,
|
| 188 |
+
min_image_bytes: int,
|
| 189 |
+
) -> List[Image.Image]:
|
| 190 |
+
values = record.get("images")
|
| 191 |
+
if values is not None:
|
| 192 |
+
if hasattr(values, "tolist"):
|
| 193 |
+
values = values.tolist()
|
| 194 |
+
if not isinstance(values, (list, tuple)):
|
| 195 |
+
values = [values]
|
| 196 |
+
else:
|
| 197 |
+
values = [
|
| 198 |
+
record.get(f"image_{idx}") for idx in range(max_images)
|
| 199 |
+
]
|
| 200 |
+
|
| 201 |
+
images = []
|
| 202 |
+
for value in values[:max_images]:
|
| 203 |
+
image = decode_hf_image(value, min_image_bytes)
|
| 204 |
+
if image is not None:
|
| 205 |
+
images.append(image)
|
| 206 |
+
return images
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def normalize_choices(value: Any) -> List[str]:
|
| 210 |
+
if value is None:
|
| 211 |
+
return []
|
| 212 |
+
if hasattr(value, "tolist"):
|
| 213 |
+
value = value.tolist()
|
| 214 |
+
if isinstance(value, (list, tuple)):
|
| 215 |
+
return [str(item) for item in value]
|
| 216 |
+
return [str(value)]
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def labels_from_question(question: str) -> List[str]:
|
| 220 |
+
labels = []
|
| 221 |
+
for label in re.findall(
|
| 222 |
+
r"(?:^|[\s,])([A-H])\s*[:\).]",
|
| 223 |
+
question or "",
|
| 224 |
+
):
|
| 225 |
+
if label not in labels:
|
| 226 |
+
labels.append(label)
|
| 227 |
+
return labels or list("ABCD")
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def mindcube_prompt(
|
| 231 |
+
record: Dict[str, Any],
|
| 232 |
+
prompt_tail: str,
|
| 233 |
+
) -> tuple[str, List[str]]:
|
| 234 |
+
question = str(record.get("question") or "").strip()
|
| 235 |
+
choices = normalize_choices(record.get("choices"))
|
| 236 |
+
labels = CHOICES[: len(choices)] if choices else labels_from_question(
|
| 237 |
+
question
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
+
official_prompt = str(record.get("input_prompt") or "").strip()
|
| 241 |
+
if official_prompt:
|
| 242 |
+
return official_prompt, labels
|
| 243 |
+
if choices:
|
| 244 |
+
options = "\n".join(
|
| 245 |
+
f"{label}. {text}"
|
| 246 |
+
for label, text in zip(labels, choices)
|
| 247 |
+
)
|
| 248 |
+
return f"{question}\nOptions:\n{options}\n{prompt_tail}", labels
|
| 249 |
+
return f"{question}\n{prompt_tail}", labels
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
def mindcube_answer_value(record: Dict[str, Any]) -> Any:
|
| 253 |
+
answer = record.get("gt_answer")
|
| 254 |
+
return record.get("answer") if answer is None else answer
|
code/eval/task/mmsi/eval_mmsi_transformers.py
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""MMSI-Bench evaluation with Hugging Face Transformers.
|
| 3 |
+
|
| 4 |
+
Unlike the vLLM evaluator, this backend can pass every image in a sample
|
| 5 |
+
without configuring a fixed multimodal-item limit.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import argparse
|
| 11 |
+
import ast
|
| 12 |
+
import base64
|
| 13 |
+
import io
|
| 14 |
+
import json
|
| 15 |
+
import logging
|
| 16 |
+
import os
|
| 17 |
+
import random
|
| 18 |
+
import sys
|
| 19 |
+
import time
|
| 20 |
+
import warnings
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
from typing import Any, Dict, List
|
| 23 |
+
|
| 24 |
+
import numpy as np
|
| 25 |
+
import torch
|
| 26 |
+
from PIL import Image
|
| 27 |
+
from qwen_vl_utils import process_vision_info
|
| 28 |
+
from transformers import AutoModelForImageTextToText, AutoProcessor
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
TASK_DIR = Path(__file__).resolve().parents[1]
|
| 32 |
+
if str(TASK_DIR) not in sys.path:
|
| 33 |
+
sys.path.insert(0, str(TASK_DIR))
|
| 34 |
+
|
| 35 |
+
from eval_image_mc_vllm import ( # noqa: E402
|
| 36 |
+
aggregate,
|
| 37 |
+
build_prompt,
|
| 38 |
+
extract_mcq_answer,
|
| 39 |
+
get_answer,
|
| 40 |
+
get_group_key,
|
| 41 |
+
load_records,
|
| 42 |
+
shard,
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
warnings.filterwarnings("ignore", message=".*pad_token_id.*")
|
| 47 |
+
logging.getLogger("transformers").setLevel(logging.ERROR)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def parse_args() -> argparse.Namespace:
|
| 51 |
+
parser = argparse.ArgumentParser(
|
| 52 |
+
description="MMSI-Bench evaluation via Hugging Face Transformers."
|
| 53 |
+
)
|
| 54 |
+
parser.add_argument("--model_path", required=True)
|
| 55 |
+
parser.add_argument("--processor_path", default="")
|
| 56 |
+
parser.add_argument("--data_file", required=True)
|
| 57 |
+
parser.add_argument("--output_dir", required=True)
|
| 58 |
+
parser.add_argument("--image_min_pixels", type=int, default=4096)
|
| 59 |
+
parser.add_argument("--image_max_pixels", type=int, default=262144)
|
| 60 |
+
parser.add_argument(
|
| 61 |
+
"--max_images",
|
| 62 |
+
type=int,
|
| 63 |
+
default=0,
|
| 64 |
+
help="Maximum images per sample; <=0 means use every image.",
|
| 65 |
+
)
|
| 66 |
+
parser.add_argument("--patch_size", type=int, default=16)
|
| 67 |
+
parser.add_argument("--max_new_tokens", type=int, default=1024)
|
| 68 |
+
parser.add_argument("--max_samples", type=int, default=0)
|
| 69 |
+
parser.add_argument("--category", default="")
|
| 70 |
+
parser.add_argument("--chunk", type=int, default=1)
|
| 71 |
+
parser.add_argument("--index", type=int, default=0)
|
| 72 |
+
parser.add_argument("--enable_thinking", action="store_true")
|
| 73 |
+
parser.add_argument("--seed", type=int, default=42)
|
| 74 |
+
parser.add_argument("--save_interval", type=int, default=20)
|
| 75 |
+
parser.add_argument(
|
| 76 |
+
"--attn_implementation",
|
| 77 |
+
default="flash_attention_2",
|
| 78 |
+
choices=["flash_attention_2", "sdpa", "eager"],
|
| 79 |
+
)
|
| 80 |
+
args = parser.parse_args()
|
| 81 |
+
args.bench = "mmsi"
|
| 82 |
+
args.processor_path = args.processor_path or args.model_path
|
| 83 |
+
return args
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def set_seed(seed: int) -> None:
|
| 87 |
+
random.seed(seed)
|
| 88 |
+
np.random.seed(seed)
|
| 89 |
+
torch.manual_seed(seed)
|
| 90 |
+
if torch.cuda.is_available():
|
| 91 |
+
torch.cuda.manual_seed_all(seed)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def decode_all_images(value: Any, max_images: int) -> List[Image.Image]:
|
| 95 |
+
values = ast.literal_eval(value) if isinstance(value, str) else value
|
| 96 |
+
if not isinstance(values, (list, tuple)):
|
| 97 |
+
values = [values]
|
| 98 |
+
if max_images > 0:
|
| 99 |
+
values = values[:max_images]
|
| 100 |
+
|
| 101 |
+
images: List[Image.Image] = []
|
| 102 |
+
for item in values:
|
| 103 |
+
if not item:
|
| 104 |
+
continue
|
| 105 |
+
path = ""
|
| 106 |
+
if isinstance(item, dict):
|
| 107 |
+
path = str(
|
| 108 |
+
item.get("path")
|
| 109 |
+
or item.get("image")
|
| 110 |
+
or item.get("image_path")
|
| 111 |
+
or ""
|
| 112 |
+
)
|
| 113 |
+
elif isinstance(item, str) and os.path.isfile(item):
|
| 114 |
+
path = item
|
| 115 |
+
if path:
|
| 116 |
+
with Image.open(path) as image:
|
| 117 |
+
images.append(image.convert("RGB"))
|
| 118 |
+
continue
|
| 119 |
+
raw = item if isinstance(item, bytes) else base64.b64decode(str(item))
|
| 120 |
+
with Image.open(io.BytesIO(raw)) as image:
|
| 121 |
+
images.append(image.convert("RGB"))
|
| 122 |
+
return images
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def prepare_inputs(
|
| 126 |
+
images: List[Image.Image],
|
| 127 |
+
prompt: str,
|
| 128 |
+
processor,
|
| 129 |
+
args: argparse.Namespace,
|
| 130 |
+
):
|
| 131 |
+
content: List[Dict[str, Any]] = []
|
| 132 |
+
for image in images:
|
| 133 |
+
content.append(
|
| 134 |
+
{
|
| 135 |
+
"type": "image",
|
| 136 |
+
"image": image,
|
| 137 |
+
"min_pixels": args.image_min_pixels,
|
| 138 |
+
"max_pixels": args.image_max_pixels,
|
| 139 |
+
}
|
| 140 |
+
)
|
| 141 |
+
content.append({"type": "text", "text": prompt})
|
| 142 |
+
messages = [{"role": "user", "content": content}]
|
| 143 |
+
text = processor.apply_chat_template(
|
| 144 |
+
messages,
|
| 145 |
+
tokenize=False,
|
| 146 |
+
add_generation_prompt=True,
|
| 147 |
+
enable_thinking=args.enable_thinking,
|
| 148 |
+
)
|
| 149 |
+
image_inputs, _ = process_vision_info(
|
| 150 |
+
messages,
|
| 151 |
+
image_patch_size=args.patch_size,
|
| 152 |
+
)
|
| 153 |
+
return processor(
|
| 154 |
+
text=[text],
|
| 155 |
+
images=image_inputs,
|
| 156 |
+
padding=True,
|
| 157 |
+
return_tensors="pt",
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def sample_id(record: Dict[str, Any], fallback: int) -> str:
|
| 162 |
+
for key in ("index", "id", "sample_id"):
|
| 163 |
+
value = record.get(key)
|
| 164 |
+
if value is not None and str(value) != "":
|
| 165 |
+
return str(value)
|
| 166 |
+
return str(fallback)
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def write_progress(
|
| 170 |
+
output_path: Path,
|
| 171 |
+
summary_path: Path,
|
| 172 |
+
args: argparse.Namespace,
|
| 173 |
+
results: List[Dict[str, Any]],
|
| 174 |
+
) -> None:
|
| 175 |
+
output_path.write_text(
|
| 176 |
+
json.dumps(results, ensure_ascii=False, indent=2) + "\n",
|
| 177 |
+
encoding="utf-8",
|
| 178 |
+
)
|
| 179 |
+
summary_path.write_text(
|
| 180 |
+
json.dumps(aggregate(args, results), ensure_ascii=False, indent=2) + "\n",
|
| 181 |
+
encoding="utf-8",
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def main() -> None:
|
| 186 |
+
args = parse_args()
|
| 187 |
+
if not torch.cuda.is_available():
|
| 188 |
+
raise RuntimeError("CUDA is required for MMSI Transformers evaluation.")
|
| 189 |
+
if args.chunk <= 0 or not 0 <= args.index < args.chunk:
|
| 190 |
+
raise ValueError(
|
| 191 |
+
f"Invalid shard index: index={args.index}, chunk={args.chunk}"
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
set_seed(args.seed)
|
| 195 |
+
output_dir = Path(args.output_dir)
|
| 196 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 197 |
+
result_path = output_dir / f"results_mmsi_shard{args.index}.json"
|
| 198 |
+
summary_path = output_dir / f"summary_shard{args.index}.json"
|
| 199 |
+
|
| 200 |
+
print(f"Model: {args.model_path}", flush=True)
|
| 201 |
+
print(f"Processor: {args.processor_path}", flush=True)
|
| 202 |
+
print(f"Data: {args.data_file}", flush=True)
|
| 203 |
+
print(f"Shard: {args.index}/{args.chunk}", flush=True)
|
| 204 |
+
print(
|
| 205 |
+
"Images: "
|
| 206 |
+
f"{'all' if args.max_images <= 0 else args.max_images} "
|
| 207 |
+
f"(min_px={args.image_min_pixels}, max_px={args.image_max_pixels})",
|
| 208 |
+
flush=True,
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
print("Loading Transformers model ...", flush=True)
|
| 212 |
+
model = AutoModelForImageTextToText.from_pretrained(
|
| 213 |
+
args.model_path,
|
| 214 |
+
dtype=torch.bfloat16,
|
| 215 |
+
attn_implementation=args.attn_implementation,
|
| 216 |
+
device_map={"": "cuda:0"},
|
| 217 |
+
trust_remote_code=True,
|
| 218 |
+
).eval()
|
| 219 |
+
processor = AutoProcessor.from_pretrained(
|
| 220 |
+
args.processor_path,
|
| 221 |
+
padding_side="left",
|
| 222 |
+
do_resize=False,
|
| 223 |
+
trust_remote_code=True,
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
records = load_records(args)
|
| 227 |
+
if args.category:
|
| 228 |
+
records = [
|
| 229 |
+
record
|
| 230 |
+
for record in records
|
| 231 |
+
if str(record.get("category") or "") == args.category
|
| 232 |
+
]
|
| 233 |
+
if args.max_samples > 0:
|
| 234 |
+
records = records[: args.max_samples]
|
| 235 |
+
indexed_records = list(enumerate(records))
|
| 236 |
+
indexed_records = shard(indexed_records, args.chunk, args.index)
|
| 237 |
+
print(f"Loaded {len(indexed_records)} samples for this shard.", flush=True)
|
| 238 |
+
|
| 239 |
+
results: List[Dict[str, Any]] = []
|
| 240 |
+
started = time.time()
|
| 241 |
+
for local_index, (source_index, record) in enumerate(indexed_records, 1):
|
| 242 |
+
images: List[Image.Image] = []
|
| 243 |
+
raw_prediction = ""
|
| 244 |
+
error = ""
|
| 245 |
+
prompt, labels = build_prompt(args, record)
|
| 246 |
+
ground_truth = get_answer(args, record)
|
| 247 |
+
try:
|
| 248 |
+
images = decode_all_images(
|
| 249 |
+
record.get("images") or record.get("image"),
|
| 250 |
+
args.max_images,
|
| 251 |
+
)
|
| 252 |
+
if not images:
|
| 253 |
+
raise ValueError("sample contains no decodable images")
|
| 254 |
+
inputs = prepare_inputs(images, prompt, processor, args).to(
|
| 255 |
+
"cuda", non_blocking=True
|
| 256 |
+
)
|
| 257 |
+
input_length = inputs.input_ids.shape[-1]
|
| 258 |
+
with torch.inference_mode():
|
| 259 |
+
generated = model.generate(
|
| 260 |
+
**inputs,
|
| 261 |
+
do_sample=False,
|
| 262 |
+
max_new_tokens=args.max_new_tokens,
|
| 263 |
+
use_cache=True,
|
| 264 |
+
)
|
| 265 |
+
generated = generated[:, input_length:]
|
| 266 |
+
raw_prediction = processor.batch_decode(
|
| 267 |
+
generated,
|
| 268 |
+
skip_special_tokens=True,
|
| 269 |
+
clean_up_tokenization_spaces=False,
|
| 270 |
+
)[0]
|
| 271 |
+
del inputs, generated
|
| 272 |
+
except Exception as exc:
|
| 273 |
+
error = f"{type(exc).__name__}: {exc}"
|
| 274 |
+
torch.cuda.empty_cache()
|
| 275 |
+
print(
|
| 276 |
+
f"[warn] sample={source_index} images={len(images)}: {error}",
|
| 277 |
+
flush=True,
|
| 278 |
+
)
|
| 279 |
+
finally:
|
| 280 |
+
for image in images:
|
| 281 |
+
image.close()
|
| 282 |
+
|
| 283 |
+
prediction = extract_mcq_answer(raw_prediction, labels)
|
| 284 |
+
score = float(
|
| 285 |
+
bool(prediction)
|
| 286 |
+
and bool(ground_truth)
|
| 287 |
+
and prediction.upper() == ground_truth.upper()
|
| 288 |
+
)
|
| 289 |
+
results.append(
|
| 290 |
+
{
|
| 291 |
+
"id": sample_id(record, source_index),
|
| 292 |
+
"bench": "mmsi",
|
| 293 |
+
"group": get_group_key(args, record),
|
| 294 |
+
"question": record.get("question"),
|
| 295 |
+
"prompt": prompt,
|
| 296 |
+
"answer": ground_truth,
|
| 297 |
+
"pred_answer": prediction,
|
| 298 |
+
"raw_prediction": raw_prediction,
|
| 299 |
+
"num_images": len(images),
|
| 300 |
+
"score": score,
|
| 301 |
+
"error": error,
|
| 302 |
+
}
|
| 303 |
+
)
|
| 304 |
+
|
| 305 |
+
if (
|
| 306 |
+
local_index % args.save_interval == 0
|
| 307 |
+
or local_index == len(indexed_records)
|
| 308 |
+
):
|
| 309 |
+
write_progress(result_path, summary_path, args, results)
|
| 310 |
+
summary = aggregate(args, results)
|
| 311 |
+
print(
|
| 312 |
+
f"[{local_index}/{len(indexed_records)}] "
|
| 313 |
+
f"{time.time() - started:.1f}s "
|
| 314 |
+
f"acc={summary['accuracy']:.2f}% "
|
| 315 |
+
f"parse={summary['parse_rate']:.2f}%",
|
| 316 |
+
flush=True,
|
| 317 |
+
)
|
| 318 |
+
|
| 319 |
+
print(json.dumps(aggregate(args, results), ensure_ascii=False, indent=2))
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
if __name__ == "__main__":
|
| 323 |
+
main()
|
code/eval/task/mmsi/run_eval_transformers.sh
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# MMSI-Bench evaluation with one Transformers model replica per GPU.
|
| 3 |
+
set -euo pipefail
|
| 4 |
+
|
| 5 |
+
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
| 6 |
+
PROJECT_DIR=$(cd -- "${SCRIPT_DIR}/../../.." && pwd)
|
| 7 |
+
EVALUATOR="${SCRIPT_DIR}/eval_mmsi_transformers.py"
|
| 8 |
+
|
| 9 |
+
MODEL_PATH=${1:-${MODEL_PATH:-}}
|
| 10 |
+
PROCESSOR_PATH=${PROCESSOR_PATH:-${MODEL_PATH}}
|
| 11 |
+
DATA_FILE=${DATA_FILE:-"${PROJECT_DIR}/data/eval/mmsi/MMSI_bench.tsv"}
|
| 12 |
+
IMAGE_MIN_PIXELS=${IMAGE_MIN_PIXELS:-4096}
|
| 13 |
+
IMAGE_MAX_PIXELS=${IMAGE_MAX_PIXELS:-262144}
|
| 14 |
+
MAX_IMAGES=${MAX_IMAGES:-0}
|
| 15 |
+
PATCH_SIZE=${PATCH_SIZE:-16}
|
| 16 |
+
MAX_NEW_TOKENS=${MAX_NEW_TOKENS:-1024}
|
| 17 |
+
MAX_SAMPLES=${MAX_SAMPLES:-0}
|
| 18 |
+
CATEGORY=${CATEGORY:-}
|
| 19 |
+
ENABLE_THINKING=${ENABLE_THINKING:-false}
|
| 20 |
+
ATTN_IMPLEMENTATION=${ATTN_IMPLEMENTATION:-flash_attention_2}
|
| 21 |
+
SAVE_INTERVAL=${SAVE_INTERVAL:-20}
|
| 22 |
+
if [[ -n "${CATEGORY}" || "${MAX_SAMPLES}" != "0" ]]; then
|
| 23 |
+
EXPECTED_SAMPLES=${EXPECTED_SAMPLES:-0}
|
| 24 |
+
else
|
| 25 |
+
EXPECTED_SAMPLES=${EXPECTED_SAMPLES:-1000}
|
| 26 |
+
fi
|
| 27 |
+
LAUNCH_DELAY=${LAUNCH_DELAY:-2}
|
| 28 |
+
OUTPUT_ROOT=${OUTPUT_ROOT:-"${PROJECT_DIR}/outputs/mmsi"}
|
| 29 |
+
|
| 30 |
+
[[ -n "${MODEL_PATH}" ]] || {
|
| 31 |
+
echo "[FATAL] MODEL_PATH is required." >&2
|
| 32 |
+
exit 1
|
| 33 |
+
}
|
| 34 |
+
for path in "${EVALUATOR}" "${MODEL_PATH}/config.json" "${DATA_FILE}"; do
|
| 35 |
+
[[ -f "${path}" ]] || {
|
| 36 |
+
echo "[FATAL] Required input is unavailable: ${path}" >&2
|
| 37 |
+
exit 1
|
| 38 |
+
}
|
| 39 |
+
done
|
| 40 |
+
compgen -G "${MODEL_PATH}/*.safetensors" >/dev/null || {
|
| 41 |
+
echo "[FATAL] No safetensors weights found under ${MODEL_PATH}." >&2
|
| 42 |
+
exit 1
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
IFS=',' read -ra GPULIST <<<"${CUDA_VISIBLE_DEVICES:-$(seq -s, 0 $(($(nvidia-smi -L | wc -l) - 1)))}"
|
| 46 |
+
NUM_GPUS=${#GPULIST[@]}
|
| 47 |
+
((NUM_GPUS > 0)) || {
|
| 48 |
+
echo "[FATAL] No visible GPUs." >&2
|
| 49 |
+
exit 1
|
| 50 |
+
}
|
| 51 |
+
unset CUDA_VISIBLE_DEVICES
|
| 52 |
+
|
| 53 |
+
model_name=$(basename "${MODEL_PATH%/}")
|
| 54 |
+
model_parent=$(basename "$(dirname "${MODEL_PATH%/}")")
|
| 55 |
+
model_grandparent=$(basename "$(dirname "$(dirname "${MODEL_PATH%/}")")")
|
| 56 |
+
if [[ "${model_name}" == "huggingface" && "${model_parent}" == "actor" && "${model_grandparent}" == global_step_* ]]; then
|
| 57 |
+
MODEL_FAMILY=$(basename "$(dirname "$(dirname "$(dirname "${MODEL_PATH%/}")")")")
|
| 58 |
+
CHECKPOINT_TAG=${model_grandparent}
|
| 59 |
+
elif [[ "${model_name}" == checkpoint-* ]]; then
|
| 60 |
+
MODEL_FAMILY=$(basename "$(dirname "$(dirname "${MODEL_PATH%/}")")")
|
| 61 |
+
CHECKPOINT_TAG=${model_name}
|
| 62 |
+
else
|
| 63 |
+
MODEL_FAMILY=${model_name}
|
| 64 |
+
CHECKPOINT_TAG=base
|
| 65 |
+
fi
|
| 66 |
+
|
| 67 |
+
DATA_TAG=$(basename "${DATA_FILE}")
|
| 68 |
+
DATA_TAG=${DATA_TAG%.*}
|
| 69 |
+
IMAGE_COUNT_TAG=$([[ "${MAX_IMAGES}" -le 0 ]] && echo all || echo "${MAX_IMAGES}")
|
| 70 |
+
SETTING_TAG="transformers-img-min${IMAGE_MIN_PIXELS}-max${IMAGE_MAX_PIXELS}-n${IMAGE_COUNT_TAG}-all-${DATA_TAG}"
|
| 71 |
+
OUTPUT_DIR=${OUTPUT_DIR:-"${OUTPUT_ROOT}/${MODEL_FAMILY}/${CHECKPOINT_TAG}/${SETTING_TAG}/$(date +%Y%m%d_%H%M%S)"}
|
| 72 |
+
mkdir -p "${OUTPUT_DIR}"
|
| 73 |
+
|
| 74 |
+
echo "============================================================"
|
| 75 |
+
echo "MMSI-Bench Evaluation (Transformers, data-parallel)"
|
| 76 |
+
echo "============================================================"
|
| 77 |
+
echo "Model: ${MODEL_PATH}"
|
| 78 |
+
echo "Processor: ${PROCESSOR_PATH}"
|
| 79 |
+
echo "Data: ${DATA_FILE}"
|
| 80 |
+
echo "GPUs: ${GPULIST[*]} (${NUM_GPUS} replicas)"
|
| 81 |
+
echo "Images: $([[ "${MAX_IMAGES}" -le 0 ]] && echo all || echo "${MAX_IMAGES}")"
|
| 82 |
+
echo "Pixels: min=${IMAGE_MIN_PIXELS} max=${IMAGE_MAX_PIXELS}"
|
| 83 |
+
echo "Max new: ${MAX_NEW_TOKENS}"
|
| 84 |
+
echo "Thinking: ${ENABLE_THINKING}"
|
| 85 |
+
echo "Output: ${OUTPUT_DIR}"
|
| 86 |
+
echo "============================================================"
|
| 87 |
+
|
| 88 |
+
pids=()
|
| 89 |
+
cleanup() {
|
| 90 |
+
local status=$?
|
| 91 |
+
trap - EXIT INT TERM
|
| 92 |
+
for pid in "${pids[@]:-}"; do
|
| 93 |
+
[[ -n "${pid}" ]] && kill "${pid}" >/dev/null 2>&1 || true
|
| 94 |
+
done
|
| 95 |
+
wait >/dev/null 2>&1 || true
|
| 96 |
+
exit "${status}"
|
| 97 |
+
}
|
| 98 |
+
trap cleanup EXIT INT TERM
|
| 99 |
+
|
| 100 |
+
for index in "${!GPULIST[@]}"; do
|
| 101 |
+
gpu=${GPULIST[index]}
|
| 102 |
+
args=(
|
| 103 |
+
--model_path "${MODEL_PATH}"
|
| 104 |
+
--processor_path "${PROCESSOR_PATH}"
|
| 105 |
+
--data_file "${DATA_FILE}"
|
| 106 |
+
--output_dir "${OUTPUT_DIR}"
|
| 107 |
+
--image_min_pixels "${IMAGE_MIN_PIXELS}"
|
| 108 |
+
--image_max_pixels "${IMAGE_MAX_PIXELS}"
|
| 109 |
+
--max_images "${MAX_IMAGES}"
|
| 110 |
+
--patch_size "${PATCH_SIZE}"
|
| 111 |
+
--max_new_tokens "${MAX_NEW_TOKENS}"
|
| 112 |
+
--max_samples "${MAX_SAMPLES}"
|
| 113 |
+
--category "${CATEGORY}"
|
| 114 |
+
--chunk "${NUM_GPUS}"
|
| 115 |
+
--index "${index}"
|
| 116 |
+
--attn_implementation "${ATTN_IMPLEMENTATION}"
|
| 117 |
+
--save_interval "${SAVE_INTERVAL}"
|
| 118 |
+
)
|
| 119 |
+
if [[ "${ENABLE_THINKING,,}" == "true" || "${ENABLE_THINKING}" == "1" ]]; then
|
| 120 |
+
args+=(--enable_thinking)
|
| 121 |
+
fi
|
| 122 |
+
CUDA_VISIBLE_DEVICES="${gpu}" PYTHONUNBUFFERED=1 \
|
| 123 |
+
python "${EVALUATOR}" "${args[@]}" \
|
| 124 |
+
>"${OUTPUT_DIR}/worker_${index}.log" 2>&1 &
|
| 125 |
+
pids+=("$!")
|
| 126 |
+
echo "Launched shard ${index}/${NUM_GPUS} on GPU ${gpu} (PID ${pids[-1]})"
|
| 127 |
+
if ((index + 1 < NUM_GPUS)) && ((LAUNCH_DELAY > 0)); then
|
| 128 |
+
sleep "${LAUNCH_DELAY}"
|
| 129 |
+
fi
|
| 130 |
+
done
|
| 131 |
+
|
| 132 |
+
failed=0
|
| 133 |
+
for index in "${!pids[@]}"; do
|
| 134 |
+
if ! wait "${pids[index]}"; then
|
| 135 |
+
echo "[FAIL] shard ${index}; inspect ${OUTPUT_DIR}/worker_${index}.log" >&2
|
| 136 |
+
failed=1
|
| 137 |
+
else
|
| 138 |
+
echo "[DONE] shard ${index}"
|
| 139 |
+
fi
|
| 140 |
+
done
|
| 141 |
+
pids=()
|
| 142 |
+
((failed == 0)) || exit 1
|
| 143 |
+
|
| 144 |
+
python - "${OUTPUT_DIR}" "${NUM_GPUS}" "${EXPECTED_SAMPLES}" "${MAX_IMAGES}" <<'PY'
|
| 145 |
+
import json
|
| 146 |
+
import sys
|
| 147 |
+
from collections import defaultdict
|
| 148 |
+
from pathlib import Path
|
| 149 |
+
|
| 150 |
+
output_dir = Path(sys.argv[1])
|
| 151 |
+
num_shards = int(sys.argv[2])
|
| 152 |
+
expected = int(sys.argv[3])
|
| 153 |
+
max_images = int(sys.argv[4])
|
| 154 |
+
records_by_id = {}
|
| 155 |
+
for shard in range(num_shards):
|
| 156 |
+
path = output_dir / f"results_mmsi_shard{shard}.json"
|
| 157 |
+
if not path.is_file():
|
| 158 |
+
raise RuntimeError(f"Missing MMSI result shard: {path}")
|
| 159 |
+
for record in json.loads(path.read_text(encoding="utf-8")):
|
| 160 |
+
records_by_id[str(record["id"])] = record
|
| 161 |
+
|
| 162 |
+
records = list(records_by_id.values())
|
| 163 |
+
if expected > 0 and len(records) != expected:
|
| 164 |
+
raise RuntimeError(
|
| 165 |
+
f"Expected {expected} unique MMSI samples, got {len(records)}"
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
correct = sum(int(record.get("score", 0)) for record in records)
|
| 169 |
+
parsed = sum(bool(record.get("pred_answer")) for record in records)
|
| 170 |
+
errors = sum(bool(record.get("error")) for record in records)
|
| 171 |
+
groups = defaultdict(lambda: {"correct": 0, "total": 0})
|
| 172 |
+
for record in records:
|
| 173 |
+
group = str(record.get("group") or "unknown")
|
| 174 |
+
groups[group]["total"] += 1
|
| 175 |
+
groups[group]["correct"] += int(record.get("score", 0))
|
| 176 |
+
|
| 177 |
+
def pct(value, total):
|
| 178 |
+
return round(100.0 * value / total, 2) if total else 0.0
|
| 179 |
+
|
| 180 |
+
summary = {
|
| 181 |
+
"backend": "transformers",
|
| 182 |
+
"image_policy": "all" if max_images <= 0 else f"first_{max_images}",
|
| 183 |
+
"num_samples": len(records),
|
| 184 |
+
"correct": correct,
|
| 185 |
+
"accuracy": pct(correct, len(records)),
|
| 186 |
+
"parse_rate": pct(parsed, len(records)),
|
| 187 |
+
"errors": errors,
|
| 188 |
+
"by_category": {
|
| 189 |
+
key: {
|
| 190 |
+
"accuracy": pct(value["correct"], value["total"]),
|
| 191 |
+
"correct": value["correct"],
|
| 192 |
+
"total": value["total"],
|
| 193 |
+
}
|
| 194 |
+
for key, value in sorted(groups.items())
|
| 195 |
+
},
|
| 196 |
+
}
|
| 197 |
+
(output_dir / "results_mmsi.json").write_text(
|
| 198 |
+
json.dumps(records, ensure_ascii=False, indent=2) + "\n",
|
| 199 |
+
encoding="utf-8",
|
| 200 |
+
)
|
| 201 |
+
(output_dir / "summary.json").write_text(
|
| 202 |
+
json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
|
| 203 |
+
encoding="utf-8",
|
| 204 |
+
)
|
| 205 |
+
print(
|
| 206 |
+
f"MMSI Transformers: n={len(records)} "
|
| 207 |
+
f"acc={summary['accuracy']:.2f}% "
|
| 208 |
+
f"parse={summary['parse_rate']:.2f}% errors={errors}"
|
| 209 |
+
)
|
| 210 |
+
print(f"Summary: {output_dir / 'summary.json'}")
|
| 211 |
+
PY
|
| 212 |
+
|
| 213 |
+
chmod -R a+rX "${OUTPUT_DIR}" 2>/dev/null || true
|
| 214 |
+
echo "MMSI Transformers evaluation complete: ${OUTPUT_DIR}"
|
code/eval/task/qwenvl_decord_patch.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Monkey-patch qwen_vl_utils to force the decord video backend.
|
| 2 |
+
|
| 3 |
+
Why this is needed
|
| 4 |
+
==================
|
| 5 |
+
|
| 6 |
+
* Evaluators call ``process_vision_info(..., return_video_metadata=True)``,
|
| 7 |
+
which is the **new-API** form of ``qwen_vl_utils``. New-API
|
| 8 |
+
``fetch_video`` expects the underlying backend reader to return a
|
| 9 |
+
3-tuple ``(video_tensor, video_metadata, sample_fps)``.
|
| 10 |
+
|
| 11 |
+
* Newer ``qwen_vl_utils`` (>= 0.0.10) hard-codes
|
| 12 |
+
``VIDEO_READER_BACKENDS["torchvision"](ele)`` inside ``fetch_video``, so
|
| 13 |
+
``FORCE_QWENVL_VIDEO_READER=decord`` no longer takes effect. The
|
| 14 |
+
torchvision reader on some machines fails with
|
| 15 |
+
``KeyError: 'video_fps'`` because the mp4 metadata is incomplete.
|
| 16 |
+
|
| 17 |
+
* Older ``_read_video_decord`` (<= 0.0.8) returns a single tensor; if we
|
| 18 |
+
just alias ``"torchvision" -> _read_video_decord``, the 3-value unpack
|
| 19 |
+
in ``fetch_video`` blows up with ``RuntimeError: Error reading <path>``.
|
| 20 |
+
|
| 21 |
+
Strategy
|
| 22 |
+
========
|
| 23 |
+
|
| 24 |
+
Replace ``vp.fetch_video`` with our own version that uses ``decord``
|
| 25 |
+
directly and returns the **new-API 3-tuple** the rest of qwen_vl_utils
|
| 26 |
+
expects. This way both old- and new-API call sites work, and we never
|
| 27 |
+
have to ship a patched copy of qwen_vl_utils itself.
|
| 28 |
+
|
| 29 |
+
Disable the patch with ``DISABLE_QWENVL_DECORD_PATCH=1`` (e.g. when
|
| 30 |
+
debugging on a machine where torchvision works fine).
|
| 31 |
+
"""
|
| 32 |
+
from __future__ import annotations
|
| 33 |
+
|
| 34 |
+
import math
|
| 35 |
+
import os
|
| 36 |
+
import sys
|
| 37 |
+
from typing import Any
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _smart_nframes(ele: dict, *, total_frames: int, video_fps: float) -> int:
|
| 41 |
+
"""Mirror qwen_vl_utils.vision_process.smart_nframes: pick the number
|
| 42 |
+
of frames to sample given fps / max_frames hints in the message dict.
|
| 43 |
+
|
| 44 |
+
We re-implement instead of importing because qwen_vl_utils versions
|
| 45 |
+
differ on the public name (``smart_nframes`` vs internal helper).
|
| 46 |
+
"""
|
| 47 |
+
FPS = float(ele.get("fps", 2.0))
|
| 48 |
+
MIN_FRAMES = int(ele.get("min_frames", 4))
|
| 49 |
+
MAX_FRAMES = int(ele.get("max_frames", 768))
|
| 50 |
+
nframes = ele.get("nframes")
|
| 51 |
+
if nframes is not None:
|
| 52 |
+
return max(1, min(int(nframes), total_frames))
|
| 53 |
+
duration = total_frames / max(1e-6, video_fps)
|
| 54 |
+
if FPS <= 0:
|
| 55 |
+
FPS = 2.0
|
| 56 |
+
n = int(round(duration * FPS))
|
| 57 |
+
n = max(MIN_FRAMES, min(MAX_FRAMES, n))
|
| 58 |
+
n = max(1, min(n, total_frames))
|
| 59 |
+
return n
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _resolve_video_path(video_path: str) -> str:
|
| 63 |
+
"""If the input path doesn't exist, try a few fallbacks before giving up.
|
| 64 |
+
|
| 65 |
+
Benchmark annotations join a base prefix with a relative media path, so a
|
| 66 |
+
layout that nests media one level differently than the annotation expects
|
| 67 |
+
(for example ``./Evaluation/got10k/<file>.mp4`` against an on-disk
|
| 68 |
+
``<base>/got10k/<file>.mp4``) would otherwise fail to decode.
|
| 69 |
+
|
| 70 |
+
Resolution order:
|
| 71 |
+
0. exact path (default)
|
| 72 |
+
1. drop ``/Evaluation/`` -> retry
|
| 73 |
+
2. drop leading ``Evaluation/`` from the raw component if EVAL_BASE_PREFIX_OVERRIDE
|
| 74 |
+
3. take basename + walk under override prefix (slow last resort)
|
| 75 |
+
"""
|
| 76 |
+
if os.path.isfile(video_path):
|
| 77 |
+
return video_path
|
| 78 |
+
|
| 79 |
+
candidates = []
|
| 80 |
+
|
| 81 |
+
# (1) Drop the spurious "/Evaluation/" component anywhere in the path.
|
| 82 |
+
if "/Evaluation/" in video_path:
|
| 83 |
+
candidates.append(video_path.replace("/Evaluation/", "/", 1))
|
| 84 |
+
|
| 85 |
+
# (2) Override-prefix-based rewrites.
|
| 86 |
+
override = os.getenv("EVAL_BASE_PREFIX_OVERRIDE", "").rstrip("/")
|
| 87 |
+
base_name = os.path.basename(video_path)
|
| 88 |
+
parent = os.path.dirname(video_path)
|
| 89 |
+
if override:
|
| 90 |
+
rel = video_path
|
| 91 |
+
for prefix in ("Evaluation/", "evaluation/", "./Evaluation/", "./"):
|
| 92 |
+
if rel.startswith(prefix):
|
| 93 |
+
rel = rel[len(prefix):]
|
| 94 |
+
break
|
| 95 |
+
candidates.append(os.path.join(override, rel))
|
| 96 |
+
candidates.append(os.path.join(override, base_name))
|
| 97 |
+
pdir = os.path.basename(parent)
|
| 98 |
+
if pdir:
|
| 99 |
+
candidates.append(os.path.join(override, pdir, base_name))
|
| 100 |
+
|
| 101 |
+
for c in candidates:
|
| 102 |
+
if os.path.isfile(c):
|
| 103 |
+
print(f"[qwenvl_decord_patch] path-resolved {video_path!r} → {c!r}",
|
| 104 |
+
file=sys.stderr)
|
| 105 |
+
return c
|
| 106 |
+
|
| 107 |
+
# (3) Last resort: walk under override (or the path's grandparent).
|
| 108 |
+
walk_root = override or os.path.dirname(parent) or parent
|
| 109 |
+
if walk_root and os.path.isdir(walk_root):
|
| 110 |
+
for root, dirs, files in os.walk(walk_root):
|
| 111 |
+
if base_name in files:
|
| 112 |
+
hit = os.path.join(root, base_name)
|
| 113 |
+
print(f"[qwenvl_decord_patch] walked-resolved {video_path!r} → {hit!r}",
|
| 114 |
+
file=sys.stderr)
|
| 115 |
+
return hit
|
| 116 |
+
depth = root[len(walk_root):].count(os.sep)
|
| 117 |
+
if depth >= 4:
|
| 118 |
+
dirs.clear()
|
| 119 |
+
|
| 120 |
+
return video_path
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def _decord_fetch_video_new_api(ele: dict, *args, **kwargs):
|
| 124 |
+
"""Drop-in replacement for ``qwen_vl_utils.vision_process.fetch_video``.
|
| 125 |
+
|
| 126 |
+
The exact signature of fetch_video varies across qwen_vl_utils
|
| 127 |
+
versions, but we observe the following calling conventions in the
|
| 128 |
+
wild:
|
| 129 |
+
|
| 130 |
+
v0.0.8 : fetch_video(ele, image_factor) → tensor
|
| 131 |
+
v0.0.10+ : fetch_video(ele, image_factor=…,
|
| 132 |
+
return_video_sample_fps=True,
|
| 133 |
+
return_video_metadata=False) → (video, sample_fps)
|
| 134 |
+
v0.0.10+ : fetch_video(ele, …, return_video_metadata=True) → (video, metadata, sample_fps)
|
| 135 |
+
|
| 136 |
+
We therefore inspect ``return_video_sample_fps`` /
|
| 137 |
+
``return_video_metadata`` kwargs and return whichever shape the
|
| 138 |
+
caller asked for. The actual decode is always decord-based.
|
| 139 |
+
"""
|
| 140 |
+
import torch
|
| 141 |
+
import decord
|
| 142 |
+
|
| 143 |
+
video_path = ele["video"]
|
| 144 |
+
if isinstance(video_path, str) and video_path.startswith("file://"):
|
| 145 |
+
video_path = video_path[len("file://"):]
|
| 146 |
+
|
| 147 |
+
# Multi-tier fallback path resolution (see _resolve_video_path docstring)
|
| 148 |
+
video_path = _resolve_video_path(video_path)
|
| 149 |
+
|
| 150 |
+
if "video_start" in ele or "video_end" in ele:
|
| 151 |
+
# Trim window. decord supports ranges via numeric indices, but no
|
| 152 |
+
# evaluated benchmark sets these, so we keep it simple.
|
| 153 |
+
raise NotImplementedError(
|
| 154 |
+
"video_start/video_end not implemented in decord patch; "
|
| 155 |
+
"the evaluated benchmarks do not need this."
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
vr = decord.VideoReader(video_path)
|
| 159 |
+
total_frames = len(vr)
|
| 160 |
+
video_fps = float(vr.get_avg_fps())
|
| 161 |
+
if total_frames <= 0 or not math.isfinite(video_fps) or video_fps <= 0:
|
| 162 |
+
raise RuntimeError(
|
| 163 |
+
f"decord-patch: invalid video {video_path!r} "
|
| 164 |
+
f"(total_frames={total_frames}, video_fps={video_fps})"
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
nframes = _smart_nframes(ele, total_frames=total_frames, video_fps=video_fps)
|
| 168 |
+
idx = torch.linspace(0, total_frames - 1, nframes).round().long().tolist()
|
| 169 |
+
arr = vr.get_batch(idx).asnumpy() # (T, H, W, C)
|
| 170 |
+
video = torch.from_numpy(arr).permute(0, 3, 1, 2).contiguous() # (T, C, H, W)
|
| 171 |
+
|
| 172 |
+
sample_fps = nframes / (total_frames / video_fps) if total_frames > 0 else float(video_fps)
|
| 173 |
+
|
| 174 |
+
# metadata fields are chosen to be compatible with transformers'
|
| 175 |
+
# VideoMetadata dataclass (qwen3-vl image processor instantiates this
|
| 176 |
+
# from the dict we return). Field set across qwen2/qwen3-vl versions:
|
| 177 |
+
# fps, total_num_frames, duration, frames_indices, video_backend
|
| 178 |
+
# We deliberately DO NOT include "video_fps" — that name causes
|
| 179 |
+
# ``VideoMetadata.__init__() got an unexpected keyword argument 'video_fps'``
|
| 180 |
+
# on newer transformers.
|
| 181 |
+
metadata = {
|
| 182 |
+
"fps": float(video_fps),
|
| 183 |
+
"total_num_frames": int(total_frames),
|
| 184 |
+
"duration": float(total_frames / video_fps),
|
| 185 |
+
"frames_indices": [int(i) for i in idx],
|
| 186 |
+
"video_backend": "decord",
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
# Decide return shape based on what the caller asked for.
|
| 190 |
+
# NOTE on shapes: qwen_vl_utils versions disagree.
|
| 191 |
+
# * Some versions: fetch_video(...) → (video, metadata, sample_fps)
|
| 192 |
+
# * Other versions: fetch_video(...) → ((video, metadata), sample_fps)
|
| 193 |
+
# (i.e. the wrapper bundles the metadata INSIDE the
|
| 194 |
+
# video return so that process_vision_info can keep
|
| 195 |
+
# a 2-tuple unpack.)
|
| 196 |
+
# The pinned qwen_vl_utils release uses the 2-tuple-with-bundled-metadata
|
| 197 |
+
# variant, so we default to that when both flags are set. Override with
|
| 198 |
+
# QWENVL_RETURN_SHAPE=3tuple if your stack expects the 3-element form.
|
| 199 |
+
want_metadata = bool(kwargs.get("return_video_metadata", False))
|
| 200 |
+
want_sample_fps = bool(kwargs.get("return_video_sample_fps", False))
|
| 201 |
+
return_shape = os.getenv("QWENVL_RETURN_SHAPE", "auto").strip().lower()
|
| 202 |
+
|
| 203 |
+
if want_metadata and want_sample_fps:
|
| 204 |
+
if return_shape == "3tuple":
|
| 205 |
+
return video, metadata, float(sample_fps)
|
| 206 |
+
# default "auto" / "bundled" → 2-tuple form
|
| 207 |
+
return (video, metadata), float(sample_fps)
|
| 208 |
+
if want_metadata:
|
| 209 |
+
return video, metadata
|
| 210 |
+
if want_sample_fps:
|
| 211 |
+
return video, float(sample_fps)
|
| 212 |
+
return video
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def _apply_patch() -> None:
|
| 216 |
+
if os.getenv("DISABLE_QWENVL_DECORD_PATCH", "0") == "1":
|
| 217 |
+
print("[qwenvl_decord_patch] disabled via env", file=sys.stderr)
|
| 218 |
+
return
|
| 219 |
+
|
| 220 |
+
try:
|
| 221 |
+
from qwen_vl_utils import vision_process as vp
|
| 222 |
+
except Exception as exc: # pragma: no cover
|
| 223 |
+
print(f"[qwenvl_decord_patch] qwen_vl_utils import failed: {exc}",
|
| 224 |
+
file=sys.stderr)
|
| 225 |
+
return
|
| 226 |
+
|
| 227 |
+
# Make sure decord is importable up front so we fail loudly here, not
|
| 228 |
+
# mid-eval, if it's not installed.
|
| 229 |
+
try:
|
| 230 |
+
import decord # noqa: F401
|
| 231 |
+
except Exception as exc:
|
| 232 |
+
print(f"[qwenvl_decord_patch] decord not installed: {exc}",
|
| 233 |
+
file=sys.stderr)
|
| 234 |
+
print(" Install with: pip install decord",
|
| 235 |
+
file=sys.stderr)
|
| 236 |
+
return
|
| 237 |
+
|
| 238 |
+
# Belt: try the env-based switch (no-op on new versions).
|
| 239 |
+
os.environ.setdefault("FORCE_QWENVL_VIDEO_READER", "decord")
|
| 240 |
+
|
| 241 |
+
# Suspenders: replace fetch_video itself.
|
| 242 |
+
if not hasattr(vp, "fetch_video"):
|
| 243 |
+
print("[qwenvl_decord_patch] vp.fetch_video missing, abort",
|
| 244 |
+
file=sys.stderr)
|
| 245 |
+
return
|
| 246 |
+
|
| 247 |
+
# Also alias the torchvision backend to our decord function in case any
|
| 248 |
+
# downstream code re-reads VIDEO_READER_BACKENDS directly.
|
| 249 |
+
backends = getattr(vp, "VIDEO_READER_BACKENDS", None)
|
| 250 |
+
if isinstance(backends, dict):
|
| 251 |
+
# Wrap to satisfy the SINGLE-tensor return convention used by some
|
| 252 |
+
# internal callers (the older ``_read_video_torchvision`` returns
|
| 253 |
+
# just a tensor in 0.0.8 path).
|
| 254 |
+
def _single_tensor_decord(ele):
|
| 255 |
+
v, _meta, _fps = _decord_fetch_video_new_api(ele)
|
| 256 |
+
return v
|
| 257 |
+
backends["torchvision"] = _single_tensor_decord
|
| 258 |
+
|
| 259 |
+
vp.fetch_video = _decord_fetch_video_new_api
|
| 260 |
+
|
| 261 |
+
print(
|
| 262 |
+
f"[qwenvl_decord_patch] applied. "
|
| 263 |
+
f"vp.fetch_video → decord (return shape auto-adapts to "
|
| 264 |
+
f"return_video_metadata / return_video_sample_fps kwargs; "
|
| 265 |
+
f"override with QWENVL_RETURN_SHAPE=3tuple). "
|
| 266 |
+
f"file={vp.__file__}",
|
| 267 |
+
file=sys.stderr,
|
| 268 |
+
)
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
_apply_patch()
|
code/eval/task/revsi/eval_revsi_vllm.py
ADDED
|
@@ -0,0 +1,1025 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""ReVSI evaluation with vLLM data-parallel sharding.
|
| 2 |
+
|
| 3 |
+
This keeps the existing OraRL VSI execution style (one vLLM process per GPU,
|
| 4 |
+
JSONL shards, centralized merge) while using ReVSI's official prompt,
|
| 5 |
+
question-type grouping, and macro-averaged metrics.
|
| 6 |
+
|
| 7 |
+
Metrics:
|
| 8 |
+
* numerical tasks: MRA (mean relative accuracy)
|
| 9 |
+
* multiple-choice tasks: exact-match ACC on the first token/letter
|
| 10 |
+
|
| 11 |
+
Outputs one JSONL shard per process and a summary JSON after launcher merge.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import argparse
|
| 15 |
+
import json
|
| 16 |
+
import os
|
| 17 |
+
import re
|
| 18 |
+
import sys
|
| 19 |
+
import time
|
| 20 |
+
from collections import defaultdict
|
| 21 |
+
from functools import lru_cache
|
| 22 |
+
from pathlib import Path
|
| 23 |
+
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
| 24 |
+
|
| 25 |
+
import numpy as np
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
TASK_DIR = Path(__file__).resolve().parents[1]
|
| 29 |
+
if str(TASK_DIR) not in sys.path:
|
| 30 |
+
sys.path.insert(0, str(TASK_DIR))
|
| 31 |
+
|
| 32 |
+
from canonical_data import load_json_records # noqa: E402
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
try:
|
| 36 |
+
from tqdm import tqdm
|
| 37 |
+
except ImportError:
|
| 38 |
+
def tqdm(iterable, **_kwargs):
|
| 39 |
+
return iterable
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
NUMERICAL_TASKS = {
|
| 43 |
+
"object_abs_distance",
|
| 44 |
+
"object_counting_single",
|
| 45 |
+
"object_counting_multiple",
|
| 46 |
+
"object_size_estimation",
|
| 47 |
+
"room_size_estimation_single",
|
| 48 |
+
"room_size_estimation_multiple",
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
MC_TASKS = {
|
| 52 |
+
"object_rel_direction_forward_easy",
|
| 53 |
+
"object_rel_direction_backward_easy",
|
| 54 |
+
"object_rel_direction_forward_hard",
|
| 55 |
+
"object_rel_direction_backward_hard",
|
| 56 |
+
"object_rel_distance_closest",
|
| 57 |
+
"object_rel_distance_farthest",
|
| 58 |
+
"route_planning",
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
DIRECTION_PREFIXES = ("object_rel_direction",)
|
| 62 |
+
|
| 63 |
+
REPORT_ORDER = [
|
| 64 |
+
("Abs. Dist", "object_abs_distance", "MRA"),
|
| 65 |
+
("Obj. Count", "object_counting", "MRA"),
|
| 66 |
+
("Rel. Dir", "object_rel_direction", "ACC"),
|
| 67 |
+
("Rel. Dis", "object_rel_distance", "ACC"),
|
| 68 |
+
("Obj. Size", "object_size_estimation", "MRA"),
|
| 69 |
+
("Room Size", "room_size_estimation", "MRA"),
|
| 70 |
+
("Route Plan", "route_planning", "ACC"),
|
| 71 |
+
]
|
| 72 |
+
|
| 73 |
+
COMPOSITE_METRICS = {
|
| 74 |
+
"object_counting": (
|
| 75 |
+
"object_counting_single",
|
| 76 |
+
"object_counting_multiple",
|
| 77 |
+
),
|
| 78 |
+
"object_rel_direction": (
|
| 79 |
+
"object_rel_direction_forward_easy",
|
| 80 |
+
"object_rel_direction_backward_easy",
|
| 81 |
+
"object_rel_direction_forward_hard",
|
| 82 |
+
"object_rel_direction_backward_hard",
|
| 83 |
+
),
|
| 84 |
+
"object_rel_distance": (
|
| 85 |
+
"object_rel_distance_closest",
|
| 86 |
+
"object_rel_distance_farthest",
|
| 87 |
+
),
|
| 88 |
+
"room_size_estimation": (
|
| 89 |
+
"room_size_estimation_single",
|
| 90 |
+
"room_size_estimation_multiple",
|
| 91 |
+
),
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
ANSWER_RE = re.compile(r"<answer>\s*(.*?)\s*</answer>", flags=re.DOTALL | re.IGNORECASE)
|
| 95 |
+
FINAL_ANSWER_RE = re.compile(
|
| 96 |
+
r"(?:the\s+)?(?:final\s+answer|answer|答案)\s*(?:is|=|是|为)?\s*[::]?\s*"
|
| 97 |
+
r"([-+]?\d+(?:\.\d+)?|[A-Da-d])\b",
|
| 98 |
+
flags=re.IGNORECASE,
|
| 99 |
+
)
|
| 100 |
+
LOOSE_NUMERIC_INSTRUCTION_RE = re.compile(
|
| 101 |
+
r"\s*Please answer the question using a single word or phrase\.?\s*",
|
| 102 |
+
flags=re.IGNORECASE,
|
| 103 |
+
)
|
| 104 |
+
NUMERIC_STRICT_INSTRUCTION = (
|
| 105 |
+
"Output only a number. Do not output option letters, units, explanations, "
|
| 106 |
+
"or punctuation."
|
| 107 |
+
)
|
| 108 |
+
MC_STRICT_INSTRUCTION = "Answer with the option letter only."
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def _strip_answer_tags(text: str) -> str:
|
| 112 |
+
text = text or ""
|
| 113 |
+
matches = ANSWER_RE.findall(text)
|
| 114 |
+
if matches:
|
| 115 |
+
return matches[-1].strip()
|
| 116 |
+
return text.strip()
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def strip_think_block(text: str) -> str:
|
| 120 |
+
text = (text or "").strip()
|
| 121 |
+
if not text:
|
| 122 |
+
return ""
|
| 123 |
+
# Prefer content after the last closing tag. This handles common Qwen
|
| 124 |
+
# no-think outputs such as "</think> B" and complete think blocks.
|
| 125 |
+
parts = re.split(r"</think>", text, flags=re.IGNORECASE)
|
| 126 |
+
if len(parts) > 1:
|
| 127 |
+
text = parts[-1]
|
| 128 |
+
else:
|
| 129 |
+
text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL | re.IGNORECASE)
|
| 130 |
+
text = re.sub(r"</?think>", "", text, flags=re.IGNORECASE)
|
| 131 |
+
return text.strip()
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def fuzzy_matching(pred: str) -> str:
|
| 135 |
+
pred = _strip_answer_tags(strip_think_block(pred))
|
| 136 |
+
pred = pred.strip()
|
| 137 |
+
if not pred:
|
| 138 |
+
return ""
|
| 139 |
+
m = FINAL_ANSWER_RE.search(pred)
|
| 140 |
+
if m:
|
| 141 |
+
return m.group(1).rstrip(".").strip()
|
| 142 |
+
# Official VSI code uses the first whitespace-separated token and strips
|
| 143 |
+
# trailing punctuation. After stripping dangling think tags, this recovers
|
| 144 |
+
# outputs like "</think> B" -> "B" and "</think> 2" -> "2".
|
| 145 |
+
return pred.split()[0].rstrip(".。,::").strip()
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def extract_numeric_prediction(pred: str) -> str:
|
| 149 |
+
"""Robust numeric extraction for VSI numerical tasks.
|
| 150 |
+
|
| 151 |
+
Some RL checkpoints emit option-like prefixes on numeric answers, e.g.
|
| 152 |
+
"A.2" or "A\n\n4". For numerical tasks, recover the number while keeping
|
| 153 |
+
MC parsing unchanged.
|
| 154 |
+
"""
|
| 155 |
+
text = _strip_answer_tags(strip_think_block(pred)).strip()
|
| 156 |
+
if not text:
|
| 157 |
+
return ""
|
| 158 |
+
m = FINAL_ANSWER_RE.search(text)
|
| 159 |
+
if m and re.search(r"\d", m.group(1)):
|
| 160 |
+
return m.group(1).rstrip(".").strip()
|
| 161 |
+
m = re.match(r"^\s*[A-Da-d]\s*[\.\)::\-]?\s*([-+]?\d+(?:\.\d+)?)\b", text)
|
| 162 |
+
if m:
|
| 163 |
+
return m.group(1).strip()
|
| 164 |
+
m = re.search(r"[-+]?\d+(?:\.\d+)?", text)
|
| 165 |
+
return m.group(0).strip() if m else fuzzy_matching(pred)
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def clean_prediction_for_task(question_type: str, pred: str) -> str:
|
| 169 |
+
if question_type in NUMERICAL_TASKS:
|
| 170 |
+
return extract_numeric_prediction(pred)
|
| 171 |
+
return fuzzy_matching(pred)
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def to_float(pred: Any) -> Optional[float]:
|
| 175 |
+
try:
|
| 176 |
+
return float(str(pred).strip())
|
| 177 |
+
except (ValueError, TypeError):
|
| 178 |
+
return None
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def exact_match(pred: str, target: Any) -> float:
|
| 182 |
+
return 1.0 if str(pred).strip().lower() == str(target).strip().lower() else 0.0
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def abs_dist_norm(pred: float, target: float) -> float:
|
| 186 |
+
if target == 0:
|
| 187 |
+
return float("inf")
|
| 188 |
+
return abs(pred - target) / abs(target)
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def mean_relative_accuracy(pred: float, target: float,
|
| 192 |
+
start: float = 0.5,
|
| 193 |
+
end: float = 0.95,
|
| 194 |
+
interval: float = 0.05) -> float:
|
| 195 |
+
num_pts = (end - start) / interval + 2
|
| 196 |
+
conf_intervs = np.linspace(start, end, int(num_pts))
|
| 197 |
+
accuracy = abs_dist_norm(pred, target) <= 1 - conf_intervs
|
| 198 |
+
return float(accuracy.mean())
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def is_direction_task(question_type: str) -> bool:
|
| 202 |
+
return any(question_type.startswith(p) for p in DIRECTION_PREFIXES)
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def report_key(question_type: str) -> str:
|
| 206 |
+
for key, members in COMPOSITE_METRICS.items():
|
| 207 |
+
if question_type in members:
|
| 208 |
+
return key
|
| 209 |
+
return question_type
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def strengthen_prompt(prompt: str, question_type: str) -> str:
|
| 213 |
+
prompt = str(prompt or "").strip()
|
| 214 |
+
if question_type in NUMERICAL_TASKS:
|
| 215 |
+
if NUMERIC_STRICT_INSTRUCTION in prompt:
|
| 216 |
+
return prompt
|
| 217 |
+
prompt = LOOSE_NUMERIC_INSTRUCTION_RE.sub("\n", prompt).strip()
|
| 218 |
+
return prompt.rstrip() + "\n" + NUMERIC_STRICT_INSTRUCTION
|
| 219 |
+
if question_type in MC_TASKS or is_direction_task(question_type):
|
| 220 |
+
lower = prompt.lower()
|
| 221 |
+
if "option" in lower and "letter" in lower:
|
| 222 |
+
return prompt
|
| 223 |
+
return prompt.rstrip() + "\n" + MC_STRICT_INSTRUCTION
|
| 224 |
+
return prompt
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def compute_sample_score(question_type: str, prediction: str,
|
| 228 |
+
ground_truth: Any) -> Tuple[str, float]:
|
| 229 |
+
cleaned = clean_prediction_for_task(question_type, prediction)
|
| 230 |
+
if question_type in NUMERICAL_TASKS:
|
| 231 |
+
pred_val = to_float(cleaned)
|
| 232 |
+
gt_val = to_float(ground_truth)
|
| 233 |
+
if pred_val is None or gt_val is None or gt_val == 0:
|
| 234 |
+
return "MRA", 0.0
|
| 235 |
+
return "MRA", mean_relative_accuracy(pred_val, gt_val)
|
| 236 |
+
if question_type in MC_TASKS or is_direction_task(question_type):
|
| 237 |
+
return "ACC", exact_match(cleaned, ground_truth)
|
| 238 |
+
return "UNK", 0.0
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def load_rows(path: str) -> List[Dict[str, Any]]:
|
| 242 |
+
if path.endswith(".parquet"):
|
| 243 |
+
import pandas as pd
|
| 244 |
+
|
| 245 |
+
return pd.read_parquet(path).to_dict(orient="records")
|
| 246 |
+
return load_json_records(path)
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
def get_image_list(item: Dict[str, Any]) -> List[str]:
|
| 250 |
+
imgs = item.get("image_list") or item.get("images") or item.get("image") or []
|
| 251 |
+
if isinstance(imgs, str):
|
| 252 |
+
imgs = [imgs]
|
| 253 |
+
return [str(p) for p in imgs if p]
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
def get_video_list(item: Dict[str, Any]) -> List[str]:
|
| 257 |
+
vids = item.get("videos") or item.get("video") or []
|
| 258 |
+
if isinstance(vids, str):
|
| 259 |
+
vids = [vids]
|
| 260 |
+
return [str(p) for p in vids if p]
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
def infer_video_path(item: Dict[str, Any], video_root: str = "") -> str:
|
| 264 |
+
"""Resolve ``ReVSI/<budget>_frame/<scene_id>.mp4``."""
|
| 265 |
+
vids = get_video_list(item)
|
| 266 |
+
if vids:
|
| 267 |
+
p = vids[0]
|
| 268 |
+
return os.path.join(video_root, p) if video_root and not os.path.isabs(p) else p
|
| 269 |
+
|
| 270 |
+
raw = item.get("raw") if isinstance(item.get("raw"), dict) else item
|
| 271 |
+
scene = str(
|
| 272 |
+
item.get("scene_id")
|
| 273 |
+
or item.get("scene_name")
|
| 274 |
+
or raw.get("scene_id")
|
| 275 |
+
or raw.get("scene_name")
|
| 276 |
+
or ""
|
| 277 |
+
).strip()
|
| 278 |
+
if not scene:
|
| 279 |
+
return ""
|
| 280 |
+
frame_budget = str(item.get("num_frames") or raw.get("num_frames") or "").strip()
|
| 281 |
+
frame_budget = re.sub(r"[_-]?frame$", "", frame_budget, flags=re.IGNORECASE)
|
| 282 |
+
if not frame_budget:
|
| 283 |
+
return ""
|
| 284 |
+
filename = scene if scene.endswith(".mp4") else f"{scene}.mp4"
|
| 285 |
+
relative = os.path.join(f"{frame_budget}_frame", filename)
|
| 286 |
+
return os.path.join(video_root, relative) if video_root else relative
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
def get_item_id(item: Dict[str, Any], idx: int) -> str:
|
| 290 |
+
for key in ("id", "item_id"):
|
| 291 |
+
if item.get(key) is not None:
|
| 292 |
+
return str(item[key])
|
| 293 |
+
return f"idx_{idx}"
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
# ---------------------------------------------------------------------------
|
| 297 |
+
# Train / Eval prompt alignment.
|
| 298 |
+
#
|
| 299 |
+
# We rewrite the eval prompts so they match the *new training* prompts
|
| 300 |
+
# byte-for-byte. The training prompts were rewritten by
|
| 301 |
+
# scripts/normalize_sft_prompts.py to:
|
| 302 |
+
#
|
| 303 |
+
# - drop the verbose "Please answer the question using a single word or
|
| 304 |
+
# phrase." instruction
|
| 305 |
+
# - use a concise, format-anchored sentence on numeric tasks:
|
| 306 |
+
# abs_distance : "...? Answer with a number in meters (e.g. 2.3)."
|
| 307 |
+
# counting : "...? Answer with an integer (e.g. 3)."
|
| 308 |
+
# size : "...? Answer with a number (e.g. 120)."
|
| 309 |
+
# room_size : "...? Answer with a number (e.g. 25.5)."
|
| 310 |
+
# - simplify size: "the length of the longest dimension (length, width, or
|
| 311 |
+
# height) of X, measured in centimeters" -> "the longest dimension
|
| 312 |
+
# (length, width, or height) of X in centimeters"
|
| 313 |
+
# - normalise rel_distance to the "(a, b, c, d)" + "If there are multiple
|
| 314 |
+
# instances ..." form (matches VSI-Bench eval).
|
| 315 |
+
# - keep route_planning steps on separate lines.
|
| 316 |
+
#
|
| 317 |
+
# All rewrites are idempotent.
|
| 318 |
+
# ---------------------------------------------------------------------------
|
| 319 |
+
|
| 320 |
+
# ----- numeric tasks --------------------------------------------------------
|
| 321 |
+
|
| 322 |
+
_NUM_TAIL_OLD = re.compile(
|
| 323 |
+
r"\s*Please answer the question using a single word or phrase\.\s*$",
|
| 324 |
+
flags=re.IGNORECASE,
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
+
# abs_distance
|
| 328 |
+
_ABS_DIST_NEW_TAIL = (
|
| 329 |
+
"? Answer with a number in meters within <answer>...</answer> tags. "
|
| 330 |
+
"e.g. <answer>2.3</answer>"
|
| 331 |
+
)
|
| 332 |
+
_ABS_DIST_RE = re.compile(
|
| 333 |
+
r"(Measuring from the closest point of each object, what is the )"
|
| 334 |
+
r"(?:direct )?(distance between the [^?]+? and the [^?]+?)"
|
| 335 |
+
r"(?:\s*\(in meters\))?\?",
|
| 336 |
+
flags=re.IGNORECASE,
|
| 337 |
+
)
|
| 338 |
+
|
| 339 |
+
# counting
|
| 340 |
+
_COUNTING_NEW_TAIL = (
|
| 341 |
+
"? Answer with an integer within <answer>...</answer> tags. "
|
| 342 |
+
"e.g. <answer>3</answer>"
|
| 343 |
+
)
|
| 344 |
+
_COUNTING_RE = re.compile(
|
| 345 |
+
r"(How many [^?]+ are in this room)\?",
|
| 346 |
+
flags=re.IGNORECASE,
|
| 347 |
+
)
|
| 348 |
+
|
| 349 |
+
# size
|
| 350 |
+
_SIZE_LONG_RE = re.compile(
|
| 351 |
+
r"What is the length of the longest dimension \(length, width, or height\) "
|
| 352 |
+
r"of (?P<obj>the [^,?]+?), measured in centimeters\?",
|
| 353 |
+
flags=re.IGNORECASE,
|
| 354 |
+
)
|
| 355 |
+
# Short form already used in training-rewritten samples.
|
| 356 |
+
_SIZE_SHORT_RE = re.compile(
|
| 357 |
+
r"What is the longest dimension \(length, width, or height\) "
|
| 358 |
+
r"of (?P<obj>the [^?]+?) in centimeters\?",
|
| 359 |
+
flags=re.IGNORECASE,
|
| 360 |
+
)
|
| 361 |
+
_SIZE_NEW_TAIL = (
|
| 362 |
+
" Answer with a number in centimeters within <answer>...</answer> tags. "
|
| 363 |
+
"e.g. <answer>120</answer>"
|
| 364 |
+
)
|
| 365 |
+
|
| 366 |
+
# room_size
|
| 367 |
+
_ROOM_SIZE_NEW = (
|
| 368 |
+
"What is the size of this room in square meters? "
|
| 369 |
+
"If multiple rooms are shown, estimate the combined size. "
|
| 370 |
+
"Answer with a number in square meters within <answer>...</answer> tags. "
|
| 371 |
+
"e.g. <answer>25.5</answer>"
|
| 372 |
+
)
|
| 373 |
+
_ROOM_SIZE_OLD_RE = re.compile(
|
| 374 |
+
r"What is the size of this room \(in square meters\)\?"
|
| 375 |
+
r"[ \t]*\n?[ \t]*"
|
| 376 |
+
r"(?:If multiple rooms are shown, estimate the size of the combined space\.)?",
|
| 377 |
+
flags=re.IGNORECASE,
|
| 378 |
+
)
|
| 379 |
+
|
| 380 |
+
# Generic detector for "already aligned with new tail" so all 4 numeric
|
| 381 |
+
# rewriters short-circuit.
|
| 382 |
+
_HAS_NEW_ANSWER_TAG_HINT_RE = re.compile(
|
| 383 |
+
r"<answer>\.\.\.</answer>\s*tags",
|
| 384 |
+
flags=re.IGNORECASE,
|
| 385 |
+
)
|
| 386 |
+
|
| 387 |
+
# MC: rewrite legacy direct-answer tails to the joint-SFT answer-only format.
|
| 388 |
+
_MC_OLD_TAIL_RE = re.compile(
|
| 389 |
+
r"\s*(?:"
|
| 390 |
+
r"Answer with the option'?s letter from the given choices directly\.?|"
|
| 391 |
+
r"Answer with the option letter within <answer>\.\.\.</answer> tags\.?\s*(?:e\.g\.|Example:)?\s*<answer>A</answer>|"
|
| 392 |
+
r"Output only the option letter inside <answer>\.\.\.</answer>\.?\s*Do not explain\.?|"
|
| 393 |
+
r"Choose the best answer from the options\. Put exactly one uppercase option letter inside <answer>\.\.\.</answer>\s*Do not explain\. Example: <answer>A</answer>"
|
| 394 |
+
r")\s*$",
|
| 395 |
+
flags=re.IGNORECASE,
|
| 396 |
+
)
|
| 397 |
+
_MC_NEW_TAIL = (
|
| 398 |
+
"Choose the best answer from the options. "
|
| 399 |
+
"Put exactly one uppercase option letter inside <answer>...</answer> "
|
| 400 |
+
"Do not explain. Example: <answer>A</answer>"
|
| 401 |
+
)
|
| 402 |
+
|
| 403 |
+
# ----- rel_distance / route_planning ----------------------------------------
|
| 404 |
+
|
| 405 |
+
_REL_DIST_TRIGGER_RE = re.compile(
|
| 406 |
+
r"which\s+of\s+these\s+objects\s*\([^)]*\)\s+is\s+the\s+closest\s+to\s+",
|
| 407 |
+
flags=re.IGNORECASE,
|
| 408 |
+
)
|
| 409 |
+
_REL_DIST_INSERT_RE = re.compile(
|
| 410 |
+
r"(\?)(\s*\n?\s*)(Options\s*:)", flags=re.IGNORECASE
|
| 411 |
+
)
|
| 412 |
+
_REL_DIST_MULTI_INSTANCE_LINE = (
|
| 413 |
+
"If there are multiple instances of an object category, measure to the closest."
|
| 414 |
+
)
|
| 415 |
+
|
| 416 |
+
_ROUTE_BLOCK_RE = re.compile(
|
| 417 |
+
r"(turn right\.'\)\s*:)\s*(.*?)(\s*You have reached the final destination\.)",
|
| 418 |
+
flags=re.IGNORECASE | re.DOTALL,
|
| 419 |
+
)
|
| 420 |
+
_ROUTE_STEP_FINDALL_RE = re.compile(r"\d+\.\s+\S")
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
def _fix_route_planning(prompt: str) -> str:
|
| 424 |
+
def repl(m):
|
| 425 |
+
head, body, tail = m.group(1), m.group(2), m.group(3)
|
| 426 |
+
positions = [mt.start() for mt in _ROUTE_STEP_FINDALL_RE.finditer(body)]
|
| 427 |
+
if not positions:
|
| 428 |
+
return m.group(0)
|
| 429 |
+
positions.append(len(body))
|
| 430 |
+
parts = [body[positions[i]:positions[i + 1]].strip()
|
| 431 |
+
for i in range(len(positions) - 1)]
|
| 432 |
+
parts = [p for p in parts if p]
|
| 433 |
+
if not parts:
|
| 434 |
+
return m.group(0)
|
| 435 |
+
return head + "\n" + "\n".join(parts) + "\n" + tail.lstrip()
|
| 436 |
+
|
| 437 |
+
return _ROUTE_BLOCK_RE.sub(repl, prompt, count=1)
|
| 438 |
+
|
| 439 |
+
|
| 440 |
+
def _rewrite_abs_distance(prompt: str) -> str:
|
| 441 |
+
"""Rewrite to "...what is the direct distance between X and Y?
|
| 442 |
+
Answer with a number in meters within <answer>...</answer> tags. ..."."""
|
| 443 |
+
if "Answer with a number in meters within <answer>" in prompt:
|
| 444 |
+
return prompt
|
| 445 |
+
# Strip any older tails (legacy "Please answer ..." or the previous
|
| 446 |
+
# format-anchored "e.g. 2.3" form).
|
| 447 |
+
prompt = _NUM_TAIL_OLD.sub("", prompt)
|
| 448 |
+
prompt = re.sub(r"\s*Answer with a number in meters \(e\.g\.[^)]*\)\.\s*$",
|
| 449 |
+
"", prompt, flags=re.IGNORECASE)
|
| 450 |
+
new_prompt = _ABS_DIST_RE.sub(
|
| 451 |
+
lambda m: m.group(1) + "direct " + m.group(2) + _ABS_DIST_NEW_TAIL,
|
| 452 |
+
prompt,
|
| 453 |
+
count=1,
|
| 454 |
+
)
|
| 455 |
+
return new_prompt
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
def _rewrite_counting(prompt: str) -> str:
|
| 459 |
+
if "Answer with an integer within <answer>" in prompt:
|
| 460 |
+
return prompt
|
| 461 |
+
prompt = _NUM_TAIL_OLD.sub("", prompt)
|
| 462 |
+
prompt = re.sub(r"\s*Answer with an integer \(e\.g\.[^)]*\)\.\s*$",
|
| 463 |
+
"", prompt, flags=re.IGNORECASE)
|
| 464 |
+
return _COUNTING_RE.sub(lambda m: m.group(1) + _COUNTING_NEW_TAIL,
|
| 465 |
+
prompt, count=1)
|
| 466 |
+
|
| 467 |
+
|
| 468 |
+
def _rewrite_size(prompt: str) -> str:
|
| 469 |
+
if "Answer with a number in centimeters within <answer>" in prompt:
|
| 470 |
+
return prompt
|
| 471 |
+
prompt = _NUM_TAIL_OLD.sub("", prompt)
|
| 472 |
+
prompt = re.sub(r"\s*Answer with a number \(e\.g\.[^)]*\)\.\s*$",
|
| 473 |
+
"", prompt, flags=re.IGNORECASE)
|
| 474 |
+
# Long form -> new short form with anchor.
|
| 475 |
+
new_prompt, n = _SIZE_LONG_RE.subn(
|
| 476 |
+
lambda m: ("What is the longest dimension (length, width, or height) "
|
| 477 |
+
"of " + m.group("obj") + " in centimeters?" + _SIZE_NEW_TAIL),
|
| 478 |
+
prompt,
|
| 479 |
+
count=1,
|
| 480 |
+
)
|
| 481 |
+
if n:
|
| 482 |
+
return new_prompt
|
| 483 |
+
# Short form (no "measured in centimeters") -> just add the anchor.
|
| 484 |
+
return _SIZE_SHORT_RE.sub(
|
| 485 |
+
lambda m: ("What is the longest dimension (length, width, or height) "
|
| 486 |
+
"of " + m.group("obj") + " in centimeters?" + _SIZE_NEW_TAIL),
|
| 487 |
+
prompt,
|
| 488 |
+
count=1,
|
| 489 |
+
)
|
| 490 |
+
|
| 491 |
+
|
| 492 |
+
def _rewrite_room_size(prompt: str) -> str:
|
| 493 |
+
if "Answer with a number in square meters within <answer>" in prompt:
|
| 494 |
+
return prompt
|
| 495 |
+
prompt = _NUM_TAIL_OLD.sub("", prompt)
|
| 496 |
+
prompt = re.sub(r"\s*Answer with a number \(e\.g\.[^)]*\)\.\s*$",
|
| 497 |
+
"", prompt, flags=re.IGNORECASE)
|
| 498 |
+
return _ROOM_SIZE_OLD_RE.sub(_ROOM_SIZE_NEW, prompt, count=1)
|
| 499 |
+
|
| 500 |
+
|
| 501 |
+
def _rewrite_mc_tail(prompt: str) -> str:
|
| 502 |
+
"""Replace "Answer with the option's letter from the given choices
|
| 503 |
+
directly." -> the new <answer>-tagged version."""
|
| 504 |
+
if _MC_NEW_TAIL in prompt:
|
| 505 |
+
return prompt
|
| 506 |
+
if _MC_OLD_TAIL_RE.search(prompt):
|
| 507 |
+
return _MC_OLD_TAIL_RE.sub("\n" + _MC_NEW_TAIL, prompt, count=1)
|
| 508 |
+
return prompt
|
| 509 |
+
|
| 510 |
+
|
| 511 |
+
def align_prompt_to_training(question_type: str, prompt: str) -> str:
|
| 512 |
+
"""Rewrite eval prompts to match the (new) training phrasing.
|
| 513 |
+
|
| 514 |
+
Idempotent: applying twice yields the same result.
|
| 515 |
+
"""
|
| 516 |
+
if not prompt:
|
| 517 |
+
return prompt
|
| 518 |
+
|
| 519 |
+
if question_type == "object_abs_distance":
|
| 520 |
+
return _rewrite_abs_distance(prompt)
|
| 521 |
+
|
| 522 |
+
if question_type == "object_counting":
|
| 523 |
+
return _rewrite_counting(prompt)
|
| 524 |
+
|
| 525 |
+
if question_type == "object_size_estimation":
|
| 526 |
+
return _rewrite_size(prompt)
|
| 527 |
+
|
| 528 |
+
if question_type == "room_size_estimation":
|
| 529 |
+
return _rewrite_room_size(prompt)
|
| 530 |
+
|
| 531 |
+
if question_type == "object_rel_distance":
|
| 532 |
+
if _REL_DIST_TRIGGER_RE.search(prompt) and (
|
| 533 |
+
_REL_DIST_MULTI_INSTANCE_LINE not in prompt
|
| 534 |
+
):
|
| 535 |
+
prompt = _REL_DIST_INSERT_RE.sub(
|
| 536 |
+
lambda m: m.group(1) + "\n" + _REL_DIST_MULTI_INSTANCE_LINE
|
| 537 |
+
+ "\n" + m.group(3),
|
| 538 |
+
prompt,
|
| 539 |
+
count=1,
|
| 540 |
+
)
|
| 541 |
+
return _rewrite_mc_tail(prompt)
|
| 542 |
+
|
| 543 |
+
if question_type == "route_planning":
|
| 544 |
+
if "turn right.')" in prompt.lower() and "\n1." not in prompt:
|
| 545 |
+
prompt = _fix_route_planning(prompt)
|
| 546 |
+
return _rewrite_mc_tail(prompt)
|
| 547 |
+
|
| 548 |
+
# All other MC tasks (rel_direction_*, obj_appearance_order) only need
|
| 549 |
+
# the tail rewrite.
|
| 550 |
+
return _rewrite_mc_tail(prompt)
|
| 551 |
+
|
| 552 |
+
|
| 553 |
+
# ---------------------------------------------------------------------------
|
| 554 |
+
# Optional strict-numeric instruction (path A diagnostic).
|
| 555 |
+
#
|
| 556 |
+
# Background: a long SFT on the 87k mixture (numeric ~17k vs MC/letter ~70k)
|
| 557 |
+
# pushes the model to emit single letters (mostly "B") even on numeric tasks,
|
| 558 |
+
# despite the training prompt's "Please answer ... single word or phrase."
|
| 559 |
+
# This helper appends an *additional* sentence that explicitly forbids
|
| 560 |
+
# letters, so we can A/B test whether the model still has numeric capability
|
| 561 |
+
# but is just being triggered into MC-mode.
|
| 562 |
+
#
|
| 563 |
+
# Toggled via the --strict_numeric_prompt CLI flag (default OFF).
|
| 564 |
+
# ---------------------------------------------------------------------------
|
| 565 |
+
STRICT_NUMERIC_SUFFIX = (
|
| 566 |
+
"Output ONLY a single integer or decimal number (no units, no letters, no "
|
| 567 |
+
"punctuation, no explanation)."
|
| 568 |
+
)
|
| 569 |
+
|
| 570 |
+
|
| 571 |
+
def apply_strict_numeric_suffix(prompt: str) -> str:
|
| 572 |
+
if not prompt:
|
| 573 |
+
return prompt
|
| 574 |
+
if STRICT_NUMERIC_SUFFIX in prompt:
|
| 575 |
+
return prompt
|
| 576 |
+
return prompt.rstrip() + "\n" + STRICT_NUMERIC_SUFFIX
|
| 577 |
+
|
| 578 |
+
|
| 579 |
+
def normalise_item(item: Dict[str, Any], idx: int,
|
| 580 |
+
strict_numeric: bool = False,
|
| 581 |
+
video_root: str = "") -> Dict[str, Any]:
|
| 582 |
+
question = str(item.get("question") or "").strip()
|
| 583 |
+
gt = item.get("ground_truth")
|
| 584 |
+
qtype = str(item.get("question_type") or "unknown")
|
| 585 |
+
raw_options = item.get("options")
|
| 586 |
+
if raw_options is None:
|
| 587 |
+
options = []
|
| 588 |
+
elif isinstance(raw_options, np.ndarray):
|
| 589 |
+
options = raw_options.tolist()
|
| 590 |
+
elif isinstance(raw_options, (list, tuple)):
|
| 591 |
+
options = list(raw_options)
|
| 592 |
+
else:
|
| 593 |
+
options = [raw_options]
|
| 594 |
+
options = [str(option) for option in options if option is not None]
|
| 595 |
+
|
| 596 |
+
prompt_parts = ["These are frames of a video.", question]
|
| 597 |
+
if qtype in MC_TASKS:
|
| 598 |
+
prompt_parts.append("Options:\n" + "\n".join(options))
|
| 599 |
+
prompt_parts.append(
|
| 600 |
+
"Answer with the option's letter from the given choices directly."
|
| 601 |
+
)
|
| 602 |
+
elif qtype in NUMERICAL_TASKS:
|
| 603 |
+
prompt_parts.append(
|
| 604 |
+
"Answer the question using a single integer or decimal number."
|
| 605 |
+
)
|
| 606 |
+
prompt = "\n".join(part for part in prompt_parts if part).strip()
|
| 607 |
+
if strict_numeric and qtype in NUMERICAL_TASKS:
|
| 608 |
+
prompt = apply_strict_numeric_suffix(prompt)
|
| 609 |
+
|
| 610 |
+
scene_id = str(item.get("scene_id") or "").strip()
|
| 611 |
+
out = {
|
| 612 |
+
"id": get_item_id(item, idx),
|
| 613 |
+
"dataset": item.get("dataset", "revsi"),
|
| 614 |
+
"scene_id": scene_id,
|
| 615 |
+
"scene_name": scene_id,
|
| 616 |
+
"num_frames": str(item.get("num_frames") or "").strip(),
|
| 617 |
+
"question_type": qtype,
|
| 618 |
+
"question": question,
|
| 619 |
+
"prompt": prompt,
|
| 620 |
+
"ground_truth": "" if gt is None else str(gt),
|
| 621 |
+
"options": options,
|
| 622 |
+
"image_list": get_image_list(item),
|
| 623 |
+
"videos": get_video_list(item),
|
| 624 |
+
"raw": item,
|
| 625 |
+
}
|
| 626 |
+
out["video_path"] = infer_video_path(out, video_root)
|
| 627 |
+
return out
|
| 628 |
+
|
| 629 |
+
|
| 630 |
+
def build_conversation(item: Dict[str, Any]) -> List[Dict[str, Any]]:
|
| 631 |
+
content = []
|
| 632 |
+
for img_path in item["image_list"]:
|
| 633 |
+
content.append({
|
| 634 |
+
"type": "image_url",
|
| 635 |
+
"image_url": {"url": f"file://{img_path}"},
|
| 636 |
+
})
|
| 637 |
+
content.append({"type": "text", "text": item["prompt"]})
|
| 638 |
+
return [
|
| 639 |
+
{"role": "system", "content": "You are a helpful assistant."},
|
| 640 |
+
{"role": "user", "content": content},
|
| 641 |
+
]
|
| 642 |
+
|
| 643 |
+
|
| 644 |
+
@lru_cache(maxsize=4096)
|
| 645 |
+
def get_video_frame_count(path: str) -> int:
|
| 646 |
+
"""Read frame-count metadata without decoding the full video."""
|
| 647 |
+
|
| 648 |
+
from decord import VideoReader, cpu
|
| 649 |
+
|
| 650 |
+
return len(VideoReader(path, ctx=cpu(0), num_threads=1))
|
| 651 |
+
|
| 652 |
+
|
| 653 |
+
def build_video_messages(item: Dict[str, Any], args) -> List[Dict[str, Any]]:
|
| 654 |
+
video_item: Dict[str, Any] = {
|
| 655 |
+
"type": "video",
|
| 656 |
+
"video": item["video_path"],
|
| 657 |
+
"total_pixels": args.video_total_pixels,
|
| 658 |
+
}
|
| 659 |
+
if args.exact_nframes:
|
| 660 |
+
total_frames = get_video_frame_count(item["video_path"])
|
| 661 |
+
# qwen_vl_utils requires nframes <= total_frames and a multiple of two.
|
| 662 |
+
# ReVSI all-frame videos are not uniformly long enough for a literal
|
| 663 |
+
# 128-frame request, so short videos use every available even frame.
|
| 664 |
+
nframes = min(args.max_frames, total_frames)
|
| 665 |
+
nframes -= nframes % 2
|
| 666 |
+
if nframes < 2:
|
| 667 |
+
raise ValueError(
|
| 668 |
+
f"Video has fewer than two usable frames: {item['video_path']}"
|
| 669 |
+
)
|
| 670 |
+
video_item["nframes"] = nframes
|
| 671 |
+
else:
|
| 672 |
+
video_item["max_frames"] = args.max_frames
|
| 673 |
+
video_item["fps"] = args.fps
|
| 674 |
+
if args.video_min_pixels is not None:
|
| 675 |
+
video_item["min_pixels"] = args.video_min_pixels
|
| 676 |
+
if args.video_max_pixels is not None:
|
| 677 |
+
video_item["max_pixels"] = args.video_max_pixels
|
| 678 |
+
return [
|
| 679 |
+
{"role": "system", "content": "You are a helpful assistant."},
|
| 680 |
+
{"role": "user", "content": [video_item, {"type": "text", "text": item["prompt"]}]},
|
| 681 |
+
]
|
| 682 |
+
|
| 683 |
+
|
| 684 |
+
def prepare_video_for_vllm(messages: List[Dict[str, Any]], processor, patch_size: int,
|
| 685 |
+
enable_thinking: bool = False) -> Dict[str, Any]:
|
| 686 |
+
"""Client-side video decoding path for vLLM.
|
| 687 |
+
|
| 688 |
+
Mirrors existing STVG/Timelens eval: apply chat template, decode video via
|
| 689 |
+
qwen_vl_utils, then pass multi_modal_data to vLLM.generate.
|
| 690 |
+
"""
|
| 691 |
+
from qwen_vl_utils import process_vision_info
|
| 692 |
+
|
| 693 |
+
text = processor.apply_chat_template(
|
| 694 |
+
messages,
|
| 695 |
+
tokenize=False,
|
| 696 |
+
add_generation_prompt=True,
|
| 697 |
+
enable_thinking=enable_thinking,
|
| 698 |
+
)
|
| 699 |
+
_images, video_inputs, video_kwargs = process_vision_info(
|
| 700 |
+
messages,
|
| 701 |
+
image_patch_size=patch_size,
|
| 702 |
+
return_video_kwargs=True,
|
| 703 |
+
return_video_metadata=True,
|
| 704 |
+
)
|
| 705 |
+
video_kwargs = video_kwargs or {}
|
| 706 |
+
video_kwargs["do_resize"] = False
|
| 707 |
+
|
| 708 |
+
llm_input: Dict[str, Any] = {"prompt": text}
|
| 709 |
+
if video_inputs:
|
| 710 |
+
llm_input["multi_modal_data"] = {"video": video_inputs}
|
| 711 |
+
llm_input["mm_processor_kwargs"] = video_kwargs
|
| 712 |
+
return llm_input
|
| 713 |
+
|
| 714 |
+
|
| 715 |
+
def summarise(records: Iterable[Dict[str, Any]]) -> Dict[str, Any]:
|
| 716 |
+
qtype_scores = defaultdict(list)
|
| 717 |
+
for r in records:
|
| 718 |
+
qtype = r.get("question_type", "unknown")
|
| 719 |
+
score = float(r.get("score", 0.0) or 0.0)
|
| 720 |
+
qtype_scores[qtype].append(score)
|
| 721 |
+
|
| 722 |
+
task_scores = {}
|
| 723 |
+
task_means = {}
|
| 724 |
+
for display, key, metric in REPORT_ORDER:
|
| 725 |
+
members = COMPOSITE_METRICS.get(key, (key,))
|
| 726 |
+
member_scores = [
|
| 727 |
+
float(np.mean(qtype_scores[member]))
|
| 728 |
+
for member in members
|
| 729 |
+
if qtype_scores.get(member)
|
| 730 |
+
]
|
| 731 |
+
if member_scores:
|
| 732 |
+
task_mean = float(np.mean(member_scores))
|
| 733 |
+
task_means[key] = task_mean
|
| 734 |
+
task_scores[key] = {
|
| 735 |
+
"display": display,
|
| 736 |
+
"metric": metric,
|
| 737 |
+
# ReVSI first averages each fine-grained question type, then
|
| 738 |
+
# macro-averages related types into the reported category.
|
| 739 |
+
"score": round(task_mean * 100, 2),
|
| 740 |
+
"count": sum(len(qtype_scores.get(member, [])) for member in members),
|
| 741 |
+
}
|
| 742 |
+
|
| 743 |
+
num_keys = {"object_counting", "object_abs_distance", "object_size_estimation", "room_size_estimation"}
|
| 744 |
+
mc_keys = {"object_rel_distance", "object_rel_direction", "route_planning"}
|
| 745 |
+
num_vals = [value for key, value in task_means.items() if key in num_keys]
|
| 746 |
+
mc_vals = [value for key, value in task_means.items() if key in mc_keys]
|
| 747 |
+
all_vals = list(task_means.values())
|
| 748 |
+
|
| 749 |
+
out = {
|
| 750 |
+
"total": sum(len(v) for v in qtype_scores.values()),
|
| 751 |
+
"task_scores": task_scores,
|
| 752 |
+
"question_type_scores": {
|
| 753 |
+
k: {"score": round(float(np.mean(v) * 100), 2), "count": len(v)}
|
| 754 |
+
for k, v in sorted(qtype_scores.items())
|
| 755 |
+
},
|
| 756 |
+
}
|
| 757 |
+
if num_vals:
|
| 758 |
+
out["numerical_avg"] = round(float(np.mean(num_vals) * 100), 2)
|
| 759 |
+
if mc_vals:
|
| 760 |
+
out["mc_avg"] = round(float(np.mean(mc_vals) * 100), 2)
|
| 761 |
+
if all_vals:
|
| 762 |
+
out["overall_avg"] = round(float(np.mean(all_vals) * 100), 2)
|
| 763 |
+
return out
|
| 764 |
+
|
| 765 |
+
|
| 766 |
+
def print_summary(summary: Dict[str, Any], prefix: str = "") -> None:
|
| 767 |
+
print(prefix + "=" * 58, flush=True)
|
| 768 |
+
print(prefix + f"ReVSI Results (samples={summary.get('total', 0)})", flush=True)
|
| 769 |
+
print(prefix + "=" * 58, flush=True)
|
| 770 |
+
print(prefix + f"{'Task':<16} {'Metric':<6} {'Score':>8} {'Count':>7}", flush=True)
|
| 771 |
+
print(prefix + "-" * 42, flush=True)
|
| 772 |
+
for _display, key, _metric in REPORT_ORDER:
|
| 773 |
+
if key not in summary.get("task_scores", {}):
|
| 774 |
+
continue
|
| 775 |
+
s = summary["task_scores"][key]
|
| 776 |
+
print(prefix + f"{s['display']:<16} {s['metric']:<6} {s['score']:>7.2f}% {s['count']:>7}", flush=True)
|
| 777 |
+
print(prefix + "-" * 42, flush=True)
|
| 778 |
+
if "numerical_avg" in summary:
|
| 779 |
+
print(prefix + f"{'Numerical Avg':<16} {'MRA':<6} {summary['numerical_avg']:>7.2f}%", flush=True)
|
| 780 |
+
if "mc_avg" in summary:
|
| 781 |
+
print(prefix + f"{'MC Avg':<16} {'ACC':<6} {summary['mc_avg']:>7.2f}%", flush=True)
|
| 782 |
+
if "overall_avg" in summary:
|
| 783 |
+
print(prefix + f"{'Overall Avg':<16} {'---':<6} {summary['overall_avg']:>7.2f}%", flush=True)
|
| 784 |
+
print(prefix + "=" * 58, flush=True)
|
| 785 |
+
|
| 786 |
+
|
| 787 |
+
def parse_args():
|
| 788 |
+
p = argparse.ArgumentParser(description="ReVSI evaluation via vLLM")
|
| 789 |
+
p.add_argument("--model_path", required=True)
|
| 790 |
+
p.add_argument("--qa_file", required=True)
|
| 791 |
+
p.add_argument("--output_json_path", required=True)
|
| 792 |
+
p.add_argument("--resume_dir", default="", help="Existing output dir; skip all ids already present in results_shard*.jsonl before re-sharding remaining samples.")
|
| 793 |
+
p.add_argument("--task_filter", default="", help="Comma-separated question_type filter.")
|
| 794 |
+
p.add_argument("--rank", type=int, default=0)
|
| 795 |
+
p.add_argument("--world_size", type=int, default=1)
|
| 796 |
+
p.add_argument("--tensor_parallel_size", type=int, default=1)
|
| 797 |
+
p.add_argument("--max_model_len", type=int, default=32768)
|
| 798 |
+
p.add_argument("--gpu_memory_utilization", type=float, default=0.90)
|
| 799 |
+
p.add_argument("--max_new_tokens", type=int, default=64)
|
| 800 |
+
p.add_argument("--batch_size", type=int, default=16)
|
| 801 |
+
p.add_argument("--score_log_interval", type=int, default=200)
|
| 802 |
+
p.add_argument("--temperature", type=float, default=0.0)
|
| 803 |
+
p.add_argument("--top_p", type=float, default=1.0)
|
| 804 |
+
p.add_argument("--max_samples", type=int, default=0)
|
| 805 |
+
p.add_argument("--media_mode", choices=["image", "video"], default="video",
|
| 806 |
+
help="image: use image_list; video: reconstruct/read mp4 and sample frames.")
|
| 807 |
+
p.add_argument("--video_root", default="",
|
| 808 |
+
help="Root directory for relative video paths, e.g. VSI-590K root.")
|
| 809 |
+
p.add_argument("--max_frames", type=int, default=128)
|
| 810 |
+
p.add_argument("--fps", type=float, default=2.0)
|
| 811 |
+
p.add_argument(
|
| 812 |
+
"--exact_nframes",
|
| 813 |
+
action="store_true",
|
| 814 |
+
help="Decode exactly max_frames frames; use for fixed ReVSI subsets.",
|
| 815 |
+
)
|
| 816 |
+
p.add_argument("--video_total_pixels", type=int, default=16777216)
|
| 817 |
+
p.add_argument("--video_min_pixels", type=int, default=65536)
|
| 818 |
+
p.add_argument("--video_max_pixels", type=int, default=None)
|
| 819 |
+
p.add_argument(
|
| 820 |
+
"--strict_numeric_prompt",
|
| 821 |
+
action="store_true",
|
| 822 |
+
help="Append a strict 'output only a number' suffix to numeric-task "
|
| 823 |
+
"prompts. Useful when a long SFT has biased the model toward "
|
| 824 |
+
"single-letter outputs on numeric questions.",
|
| 825 |
+
)
|
| 826 |
+
p.add_argument(
|
| 827 |
+
"--enable_thinking",
|
| 828 |
+
type=str,
|
| 829 |
+
default="false",
|
| 830 |
+
choices=["true", "false"],
|
| 831 |
+
help="Enable Qwen3 <think> block. Default: false (no_think). "
|
| 832 |
+
"When false, vLLM is told via chat_template_kwargs to skip the "
|
| 833 |
+
"<think>...</think> reasoning prefix so the model emits the "
|
| 834 |
+
"<answer>...</answer> answer directly.",
|
| 835 |
+
)
|
| 836 |
+
return p.parse_args()
|
| 837 |
+
|
| 838 |
+
|
| 839 |
+
def main():
|
| 840 |
+
args = parse_args()
|
| 841 |
+
rows = load_rows(args.qa_file)
|
| 842 |
+
items = [normalise_item(r, i, strict_numeric=args.strict_numeric_prompt,
|
| 843 |
+
video_root=args.video_root)
|
| 844 |
+
for i, r in enumerate(rows)]
|
| 845 |
+
if args.task_filter:
|
| 846 |
+
keep = {x.strip() for x in args.task_filter.split(",") if x.strip()}
|
| 847 |
+
items = [x for x in items if x["question_type"] in keep]
|
| 848 |
+
print(f"Task filter: {sorted(keep)} -> {len(items)} samples", flush=True)
|
| 849 |
+
if args.max_samples and len(items) > args.max_samples:
|
| 850 |
+
items = items[:args.max_samples]
|
| 851 |
+
|
| 852 |
+
# Global resume mode: remove all IDs already written by any previous shard,
|
| 853 |
+
# then redistribute the remaining samples across the current world_size.
|
| 854 |
+
if args.resume_dir:
|
| 855 |
+
done_global = set()
|
| 856 |
+
for name in os.listdir(args.resume_dir) if os.path.isdir(args.resume_dir) else []:
|
| 857 |
+
if not (name.startswith("results_shard") and name.endswith(".jsonl")):
|
| 858 |
+
continue
|
| 859 |
+
p = os.path.join(args.resume_dir, name)
|
| 860 |
+
with open(p, "r", encoding="utf-8") as f:
|
| 861 |
+
for line in f:
|
| 862 |
+
if not line.strip():
|
| 863 |
+
continue
|
| 864 |
+
try:
|
| 865 |
+
done_global.add(str(json.loads(line).get("id")))
|
| 866 |
+
except Exception:
|
| 867 |
+
pass
|
| 868 |
+
if done_global:
|
| 869 |
+
before = len(items)
|
| 870 |
+
items = [x for x in items if str(x["id"]) not in done_global]
|
| 871 |
+
print(f"Global resume: skip {before - len(items)} done ids from {args.resume_dir}; remaining={len(items)}", flush=True)
|
| 872 |
+
|
| 873 |
+
# Contiguous sharding: matches official VSI script.
|
| 874 |
+
total = len(items)
|
| 875 |
+
chunk_size = total // args.world_size
|
| 876 |
+
remainder = total % args.world_size
|
| 877 |
+
start = args.rank * chunk_size + min(args.rank, remainder)
|
| 878 |
+
end = start + chunk_size + (1 if args.rank < remainder else 0)
|
| 879 |
+
shard_items = items[start:end]
|
| 880 |
+
|
| 881 |
+
# Resume: skip IDs already present in the shard output.
|
| 882 |
+
done = set()
|
| 883 |
+
if os.path.exists(args.output_json_path):
|
| 884 |
+
with open(args.output_json_path, "r", encoding="utf-8") as f:
|
| 885 |
+
for line in f:
|
| 886 |
+
if line.strip():
|
| 887 |
+
try:
|
| 888 |
+
done.add(json.loads(line)["id"])
|
| 889 |
+
except Exception:
|
| 890 |
+
pass
|
| 891 |
+
shard_items = [x for x in shard_items if x["id"] not in done]
|
| 892 |
+
|
| 893 |
+
if not shard_items:
|
| 894 |
+
print(f"rank={args.rank}: no remaining samples", flush=True)
|
| 895 |
+
return
|
| 896 |
+
|
| 897 |
+
if args.media_mode == "video":
|
| 898 |
+
missing = [x for x in shard_items if not x.get("video_path") or not os.path.isfile(x["video_path"])]
|
| 899 |
+
if missing:
|
| 900 |
+
ex = missing[0]
|
| 901 |
+
raise FileNotFoundError(
|
| 902 |
+
f"video_mode missing {len(missing)} videos; first id={ex['id']} "
|
| 903 |
+
f"dataset={ex['dataset']} scene={ex['scene_name']} path={ex.get('video_path')!r}"
|
| 904 |
+
)
|
| 905 |
+
|
| 906 |
+
os.makedirs(os.path.dirname(args.output_json_path), exist_ok=True)
|
| 907 |
+
|
| 908 |
+
# vLLM local-media permission root for image-url mode.
|
| 909 |
+
all_img_paths = [p for item in shard_items for p in item["image_list"]]
|
| 910 |
+
common_prefix = os.path.commonpath(all_img_paths) if all_img_paths else "/"
|
| 911 |
+
if not os.path.isdir(common_prefix):
|
| 912 |
+
common_prefix = os.path.dirname(common_prefix)
|
| 913 |
+
num_images = max((len(x["image_list"]) for x in shard_items), default=0)
|
| 914 |
+
|
| 915 |
+
from vllm import LLM, SamplingParams
|
| 916 |
+
|
| 917 |
+
print("Initializing vLLM engine...", flush=True)
|
| 918 |
+
print(f"Model: {args.model_path}", flush=True)
|
| 919 |
+
print(f"QA: {args.qa_file}", flush=True)
|
| 920 |
+
print(f"Rank: {args.rank}/{args.world_size}, samples={len(shard_items)} / total={total}", flush=True)
|
| 921 |
+
print(f"Media mode: {args.media_mode}", flush=True)
|
| 922 |
+
if args.media_mode == "video":
|
| 923 |
+
print(f"Video: root={args.video_root} max_frames={args.max_frames} "
|
| 924 |
+
f"exact_nframes={args.exact_nframes} fps={args.fps} "
|
| 925 |
+
f"total_pixels={args.video_total_pixels}", flush=True)
|
| 926 |
+
else:
|
| 927 |
+
print(f"Images per prompt max: {num_images}, allowed_media={common_prefix}", flush=True)
|
| 928 |
+
enable_thinking = (str(args.enable_thinking).lower() == "true")
|
| 929 |
+
print(f"Thinking: {enable_thinking}", flush=True)
|
| 930 |
+
|
| 931 |
+
processor = None
|
| 932 |
+
patch_size = None
|
| 933 |
+
if args.media_mode == "video":
|
| 934 |
+
from transformers import AutoProcessor, AutoTokenizer
|
| 935 |
+
processor = AutoProcessor.from_pretrained(
|
| 936 |
+
args.model_path, padding_side="left", do_resize=False,
|
| 937 |
+
trust_remote_code=True)
|
| 938 |
+
tokenizer = AutoTokenizer.from_pretrained(args.model_path, trust_remote_code=True)
|
| 939 |
+
tokenizer.padding_side = "left"
|
| 940 |
+
processor.tokenizer = tokenizer
|
| 941 |
+
patch_size = processor.image_processor.patch_size
|
| 942 |
+
print(f"Patch size: {patch_size}", flush=True)
|
| 943 |
+
|
| 944 |
+
mm_limit = {"video": 1, "image": 1} if args.media_mode == "video" else {"image": num_images}
|
| 945 |
+
llm_kwargs = dict(
|
| 946 |
+
model=args.model_path,
|
| 947 |
+
tensor_parallel_size=args.tensor_parallel_size,
|
| 948 |
+
max_model_len=args.max_model_len,
|
| 949 |
+
gpu_memory_utilization=args.gpu_memory_utilization,
|
| 950 |
+
trust_remote_code=True,
|
| 951 |
+
limit_mm_per_prompt=mm_limit,
|
| 952 |
+
)
|
| 953 |
+
if args.media_mode == "image":
|
| 954 |
+
llm_kwargs["allowed_local_media_path"] = common_prefix
|
| 955 |
+
|
| 956 |
+
llm = LLM(**llm_kwargs)
|
| 957 |
+
sampling = SamplingParams(
|
| 958 |
+
max_tokens=args.max_new_tokens,
|
| 959 |
+
temperature=args.temperature,
|
| 960 |
+
top_p=args.top_p,
|
| 961 |
+
)
|
| 962 |
+
# Passed through to the tokenizer's apply_chat_template inside vLLM.
|
| 963 |
+
# Qwen3 chat templates expose `enable_thinking` to skip the
|
| 964 |
+
# `<think>...</think>` reasoning prefix when set to False.
|
| 965 |
+
chat_template_kwargs = {"enable_thinking": enable_thinking}
|
| 966 |
+
|
| 967 |
+
t0 = time.time()
|
| 968 |
+
local_records: List[Dict[str, Any]] = []
|
| 969 |
+
with open(args.output_json_path, "a", encoding="utf-8") as f:
|
| 970 |
+
for batch_start in tqdm(range(0, len(shard_items), args.batch_size),
|
| 971 |
+
desc=f"rank{args.rank}"):
|
| 972 |
+
batch = shard_items[batch_start:batch_start + args.batch_size]
|
| 973 |
+
if args.media_mode == "video":
|
| 974 |
+
conversations = [build_video_messages(x, args) for x in batch]
|
| 975 |
+
llm_inputs = [prepare_video_for_vllm(
|
| 976 |
+
m, processor, patch_size, enable_thinking=enable_thinking)
|
| 977 |
+
for m in conversations]
|
| 978 |
+
outputs = llm.generate(llm_inputs, sampling_params=sampling)
|
| 979 |
+
else:
|
| 980 |
+
conversations = [build_conversation(x) for x in batch]
|
| 981 |
+
outputs = llm.chat(
|
| 982 |
+
conversations,
|
| 983 |
+
sampling_params=sampling,
|
| 984 |
+
chat_template_kwargs=chat_template_kwargs,
|
| 985 |
+
)
|
| 986 |
+
for item, output in zip(batch, outputs):
|
| 987 |
+
pred = output.outputs[0].text
|
| 988 |
+
metric, score = compute_sample_score(
|
| 989 |
+
item["question_type"], pred, item["ground_truth"])
|
| 990 |
+
record = {
|
| 991 |
+
"id": item["id"],
|
| 992 |
+
"dataset": item["dataset"],
|
| 993 |
+
"scene_id": item["scene_id"],
|
| 994 |
+
"scene_name": item["scene_name"],
|
| 995 |
+
"num_frames": item["num_frames"],
|
| 996 |
+
"question_type": item["question_type"],
|
| 997 |
+
"question": item["question"],
|
| 998 |
+
"prompt": item["prompt"],
|
| 999 |
+
"pred": pred,
|
| 1000 |
+
"pred_clean": clean_prediction_for_task(item["question_type"], pred),
|
| 1001 |
+
"ground_truth": item["ground_truth"],
|
| 1002 |
+
"options": item.get("options"),
|
| 1003 |
+
"image_list": item["image_list"],
|
| 1004 |
+
"video_path": item.get("video_path"),
|
| 1005 |
+
"media_mode": args.media_mode,
|
| 1006 |
+
"score": score,
|
| 1007 |
+
"metric": metric,
|
| 1008 |
+
}
|
| 1009 |
+
f.write(json.dumps(record, ensure_ascii=False) + "\n")
|
| 1010 |
+
local_records.append(record)
|
| 1011 |
+
f.flush()
|
| 1012 |
+
|
| 1013 |
+
done_n = min(batch_start + args.batch_size, len(shard_items))
|
| 1014 |
+
if done_n % args.score_log_interval == 0 or done_n == len(shard_items):
|
| 1015 |
+
elapsed = max(time.time() - t0, 1e-6)
|
| 1016 |
+
print(f"[{done_n}/{len(shard_items)}] {elapsed:.1f}s "
|
| 1017 |
+
f"({done_n / elapsed:.2f} items/s)", flush=True)
|
| 1018 |
+
print_summary(summarise(local_records), prefix=" ")
|
| 1019 |
+
|
| 1020 |
+
print("FINAL SHARD SUMMARY", flush=True)
|
| 1021 |
+
print_summary(summarise(local_records))
|
| 1022 |
+
|
| 1023 |
+
|
| 1024 |
+
if __name__ == "__main__":
|
| 1025 |
+
main()
|
code/eval/task/revsi/merge_multinode_shards.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Merge, validate, and officially summarize ReVSI vLLM shards."""
|
| 3 |
+
|
| 4 |
+
import argparse
|
| 5 |
+
import json
|
| 6 |
+
import sys
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
| 11 |
+
from eval_revsi_vllm import print_summary, summarise # noqa: E402
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def main() -> None:
|
| 15 |
+
parser = argparse.ArgumentParser()
|
| 16 |
+
parser.add_argument("--output-dir", type=Path, required=True)
|
| 17 |
+
parser.add_argument("--num-shards", type=int, required=True)
|
| 18 |
+
parser.add_argument("--expected-samples", type=int, default=0)
|
| 19 |
+
args = parser.parse_args()
|
| 20 |
+
|
| 21 |
+
missing = [
|
| 22 |
+
shard
|
| 23 |
+
for shard in range(args.num_shards)
|
| 24 |
+
if not (args.output_dir / f"results_shard{shard}.jsonl").is_file()
|
| 25 |
+
]
|
| 26 |
+
if missing:
|
| 27 |
+
raise RuntimeError(
|
| 28 |
+
f"Missing {len(missing)}/{args.num_shards} ReVSI shards: {missing}"
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
records_by_id = {}
|
| 32 |
+
for shard in range(args.num_shards):
|
| 33 |
+
path = args.output_dir / f"results_shard{shard}.jsonl"
|
| 34 |
+
with path.open(encoding="utf-8") as handle:
|
| 35 |
+
for line_number, line in enumerate(handle, 1):
|
| 36 |
+
if not line.strip():
|
| 37 |
+
continue
|
| 38 |
+
record = json.loads(line)
|
| 39 |
+
sample_id = record.get("id")
|
| 40 |
+
if sample_id is None:
|
| 41 |
+
raise RuntimeError(f"{path}:{line_number}: record has no id")
|
| 42 |
+
records_by_id[str(sample_id)] = record
|
| 43 |
+
|
| 44 |
+
records = list(records_by_id.values())
|
| 45 |
+
records.sort(
|
| 46 |
+
key=lambda row: (
|
| 47 |
+
0,
|
| 48 |
+
int(row["id"]),
|
| 49 |
+
)
|
| 50 |
+
if str(row["id"]).isdigit()
|
| 51 |
+
else (1, str(row["id"]))
|
| 52 |
+
)
|
| 53 |
+
if args.expected_samples > 0 and len(records) != args.expected_samples:
|
| 54 |
+
raise RuntimeError(
|
| 55 |
+
f"Expected {args.expected_samples} unique ReVSI samples, "
|
| 56 |
+
f"got {len(records)}"
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
merged_path = args.output_dir / "merged_results.jsonl"
|
| 60 |
+
with merged_path.open("w", encoding="utf-8") as handle:
|
| 61 |
+
for record in records:
|
| 62 |
+
handle.write(json.dumps(record, ensure_ascii=False) + "\n")
|
| 63 |
+
|
| 64 |
+
summary = summarise(records)
|
| 65 |
+
summary["num_shards"] = args.num_shards
|
| 66 |
+
summary["frame_budgets"] = sorted(
|
| 67 |
+
{str(record.get("num_frames") or "") for record in records}
|
| 68 |
+
)
|
| 69 |
+
summary_path = args.output_dir / "summary.json"
|
| 70 |
+
summary_path.write_text(
|
| 71 |
+
json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
|
| 72 |
+
encoding="utf-8",
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
print_summary(summary)
|
| 76 |
+
print(f"Merged: {merged_path}")
|
| 77 |
+
print(f"Summary: {summary_path}")
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
if __name__ == "__main__":
|
| 81 |
+
main()
|
code/eval/task/revsi/run_eval_vllm.sh
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# =============================================================================
|
| 3 |
+
# ReVSI evaluation launcher — vLLM, data-parallel only.
|
| 4 |
+
#
|
| 5 |
+
# Usage:
|
| 6 |
+
# bash eval/task/revsi/run_eval_vllm.sh [MODEL_PATH]
|
| 7 |
+
#
|
| 8 |
+
# Env overrides:
|
| 9 |
+
# REVSI_ROOT root containing 16_frame/32_frame/64_frame/all_frame
|
| 10 |
+
# FRAME_BUDGET 16, 32, 64, or all (default: all)
|
| 11 |
+
# QA_FILE defaults to FRAME_BUDGET/test-00000-of-00001.parquet
|
| 12 |
+
# OUTPUT_ROOT default: outputs/revsi
|
| 13 |
+
# OUTPUT_DIR optional existing run dir; set this to resume an interrupted eval
|
| 14 |
+
# TASK_FILTER comma-separated question_type list, optional
|
| 15 |
+
# MAX_SAMPLES optional quick debug cap
|
| 16 |
+
# TP_SIZE default 1
|
| 17 |
+
# MAX_MODEL_LEN default 32768
|
| 18 |
+
# MAX_NEW_TOKENS default 64
|
| 19 |
+
# BATCH_SIZE default 16
|
| 20 |
+
# GPU_MEM_UTIL default 0.90
|
| 21 |
+
# MEDIA_MODE image (default) or video
|
| 22 |
+
# VIDEO_ROOT root for mp4s in video mode, e.g. VSI-590K root
|
| 23 |
+
# MAX_FRAMES/FPS/VIDEO_TOTAL_PIXELS video sampling controls
|
| 24 |
+
# =============================================================================
|
| 25 |
+
|
| 26 |
+
set -e
|
| 27 |
+
|
| 28 |
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
| 29 |
+
PROJECT_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
| 30 |
+
|
| 31 |
+
MODEL_PATH="${1:-${MODEL_PATH:-}}"
|
| 32 |
+
REVSI_ROOT="${REVSI_ROOT:-}"
|
| 33 |
+
FRAME_BUDGET="${FRAME_BUDGET:-all}"
|
| 34 |
+
FRAME_DIR="${REVSI_ROOT:+${REVSI_ROOT}/${FRAME_BUDGET}_frame}"
|
| 35 |
+
QA_FILE="${QA_FILE:-${FRAME_DIR:+${FRAME_DIR}/test-00000-of-00001.parquet}}"
|
| 36 |
+
OUTPUT_ROOT="${OUTPUT_ROOT:-${PROJECT_DIR}/outputs/revsi}"
|
| 37 |
+
TASK_FILTER="${TASK_FILTER:-}"
|
| 38 |
+
MAX_SAMPLES="${MAX_SAMPLES:-0}"
|
| 39 |
+
STRICT_NUMERIC_PROMPT="${STRICT_NUMERIC_PROMPT:-0}"
|
| 40 |
+
ENABLE_THINKING="${ENABLE_THINKING:-false}"
|
| 41 |
+
MEDIA_MODE="${MEDIA_MODE:-video}"
|
| 42 |
+
VIDEO_ROOT="${VIDEO_ROOT:-${REVSI_ROOT}}"
|
| 43 |
+
MAX_FRAMES="${MAX_FRAMES:-${FRAME_BUDGET/all/128}}"
|
| 44 |
+
EXACT_NFRAMES="${EXACT_NFRAMES:-1}"
|
| 45 |
+
FPS="${FPS:-2}"
|
| 46 |
+
VIDEO_TOTAL_PIXELS="${VIDEO_TOTAL_PIXELS:-16777216}"
|
| 47 |
+
VIDEO_MIN_PIXELS="${VIDEO_MIN_PIXELS:-65536}"
|
| 48 |
+
VIDEO_MAX_PIXELS="${VIDEO_MAX_PIXELS:-}"
|
| 49 |
+
|
| 50 |
+
TP_SIZE="${TP_SIZE:-1}"
|
| 51 |
+
MAX_MODEL_LEN="${MAX_MODEL_LEN:-32768}"
|
| 52 |
+
MAX_NEW_TOKENS="${MAX_NEW_TOKENS:-64}"
|
| 53 |
+
BATCH_SIZE="${BATCH_SIZE:-16}"
|
| 54 |
+
GPU_MEM_UTIL="${GPU_MEM_UTIL:-0.90}"
|
| 55 |
+
SCORE_LOG_INTERVAL="${SCORE_LOG_INTERVAL:-200}"
|
| 56 |
+
GLOBAL_SHARD_COUNT="${GLOBAL_SHARD_COUNT:-}"
|
| 57 |
+
GLOBAL_SHARD_OFFSET="${GLOBAL_SHARD_OFFSET:-0}"
|
| 58 |
+
MERGE_SHARDS="${MERGE_SHARDS:-1}"
|
| 59 |
+
EXPECTED_SAMPLES="${EXPECTED_SAMPLES:-0}"
|
| 60 |
+
VLLM_BASE_PORT="${VLLM_BASE_PORT:-}"
|
| 61 |
+
LAUNCH_DELAY="${LAUNCH_DELAY:-2}"
|
| 62 |
+
|
| 63 |
+
case "${FRAME_BUDGET}" in
|
| 64 |
+
16|32|64|all) ;;
|
| 65 |
+
*)
|
| 66 |
+
echo "ERROR: FRAME_BUDGET must be 16, 32, 64, or all." >&2
|
| 67 |
+
exit 2
|
| 68 |
+
;;
|
| 69 |
+
esac
|
| 70 |
+
[[ -n "${MODEL_PATH}" ]] || {
|
| 71 |
+
echo "ERROR: pass MODEL_PATH as the first argument or environment variable." >&2
|
| 72 |
+
exit 2
|
| 73 |
+
}
|
| 74 |
+
[[ -n "${QA_FILE}" ]] || {
|
| 75 |
+
echo "ERROR: set QA_FILE or REVSI_ROOT." >&2
|
| 76 |
+
exit 2
|
| 77 |
+
}
|
| 78 |
+
for path in "${MODEL_PATH}/config.json" "${QA_FILE}"; do
|
| 79 |
+
[[ -f "${path}" ]] || {
|
| 80 |
+
echo "ERROR: required input is unavailable: ${path}" >&2
|
| 81 |
+
exit 1
|
| 82 |
+
}
|
| 83 |
+
done
|
| 84 |
+
if [[ "${QA_FILE}" == *.parquet ]]; then
|
| 85 |
+
[[ -n "${FRAME_DIR}" ]] || {
|
| 86 |
+
echo "ERROR: parquet input requires REVSI_ROOT to locate frame videos." >&2
|
| 87 |
+
exit 2
|
| 88 |
+
}
|
| 89 |
+
compgen -G "${FRAME_DIR}/*.mp4" >/dev/null || {
|
| 90 |
+
echo "ERROR: no ReVSI videos found under ${FRAME_DIR}; extract video.zip first." >&2
|
| 91 |
+
exit 1
|
| 92 |
+
}
|
| 93 |
+
fi
|
| 94 |
+
|
| 95 |
+
# Prefer the CUDA runtime libraries installed alongside PyTorch. In particular,
|
| 96 |
+
# pip/conda CUDA 12.9 builds need their matching nvJitLink ahead of an older
|
| 97 |
+
# system CUDA toolkit that may already be present in LD_LIBRARY_PATH.
|
| 98 |
+
NVJITLINK_LIB="$(
|
| 99 |
+
python - <<'PY'
|
| 100 |
+
import site
|
| 101 |
+
from pathlib import Path
|
| 102 |
+
|
| 103 |
+
roots = [*site.getsitepackages(), site.getusersitepackages()]
|
| 104 |
+
for root in roots:
|
| 105 |
+
candidate = Path(root) / "nvidia" / "nvjitlink" / "lib"
|
| 106 |
+
if candidate.is_dir():
|
| 107 |
+
print(candidate)
|
| 108 |
+
break
|
| 109 |
+
PY
|
| 110 |
+
)"
|
| 111 |
+
if [[ -n "${NVJITLINK_LIB}" ]]; then
|
| 112 |
+
export LD_LIBRARY_PATH="${NVJITLINK_LIB}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}"
|
| 113 |
+
fi
|
| 114 |
+
if ! python -c "import torch; print(f'PyTorch preflight: {torch.__version__} CUDA {torch.version.cuda}')"; then
|
| 115 |
+
echo "ERROR: PyTorch CUDA libraries cannot be loaded in the active environment." >&2
|
| 116 |
+
exit 1
|
| 117 |
+
fi
|
| 118 |
+
|
| 119 |
+
IFS="," read -ra GPULIST <<< "${CUDA_VISIBLE_DEVICES:-$(seq -s, 0 $(($(nvidia-smi -L | wc -l)-1)))}"
|
| 120 |
+
NUM_GPUS=${#GPULIST[@]}
|
| 121 |
+
# Each vLLM subprocess must inherit only its worker-specific GPU mask.
|
| 122 |
+
unset CUDA_VISIBLE_DEVICES
|
| 123 |
+
if (( NUM_GPUS % TP_SIZE != 0 )); then
|
| 124 |
+
echo "ERROR: NUM_GPUS=$NUM_GPUS must be divisible by TP_SIZE=$TP_SIZE"
|
| 125 |
+
exit 1
|
| 126 |
+
fi
|
| 127 |
+
DP_SIZE=$(( NUM_GPUS / TP_SIZE ))
|
| 128 |
+
if [[ -z "${VLLM_BASE_PORT}" ]]; then
|
| 129 |
+
VLLM_BASE_PORT="$(
|
| 130 |
+
python - "$DP_SIZE" <<'PY'
|
| 131 |
+
import socket
|
| 132 |
+
import sys
|
| 133 |
+
|
| 134 |
+
count = int(sys.argv[1])
|
| 135 |
+
spacing = 16
|
| 136 |
+
for base in range(48000, 64000 - spacing * count, 128):
|
| 137 |
+
sockets = []
|
| 138 |
+
try:
|
| 139 |
+
for index in range(count):
|
| 140 |
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
| 141 |
+
sock.bind(("127.0.0.1", base + index * spacing))
|
| 142 |
+
sockets.append(sock)
|
| 143 |
+
except OSError:
|
| 144 |
+
for sock in sockets:
|
| 145 |
+
sock.close()
|
| 146 |
+
continue
|
| 147 |
+
for sock in sockets:
|
| 148 |
+
sock.close()
|
| 149 |
+
print(base)
|
| 150 |
+
break
|
| 151 |
+
else:
|
| 152 |
+
raise SystemExit("no free ReVSI vLLM port block found")
|
| 153 |
+
PY
|
| 154 |
+
)"
|
| 155 |
+
fi
|
| 156 |
+
|
| 157 |
+
MODEL_TAG=$(basename "${MODEL_PATH%/}")
|
| 158 |
+
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
| 159 |
+
RUN_TAG="${RUN_TAG:-eval_revsi_${FRAME_BUDGET}frame_vllm-${MODEL_TAG}-${TIMESTAMP}}"
|
| 160 |
+
# If OUTPUT_DIR points to an existing partial run, eval_revsi_vllm.py will skip
|
| 161 |
+
# IDs already present in results_shard*.jsonl and continue the remaining samples.
|
| 162 |
+
OUTPUT_DIR="${OUTPUT_DIR:-${OUTPUT_ROOT}/${RUN_TAG}}"
|
| 163 |
+
mkdir -p "$OUTPUT_DIR"
|
| 164 |
+
export MODEL_PATH QA_FILE VIDEO_ROOT FRAME_BUDGET MAX_FRAMES EXACT_NFRAMES FPS
|
| 165 |
+
export VIDEO_TOTAL_PIXELS VIDEO_MIN_PIXELS VIDEO_MAX_PIXELS
|
| 166 |
+
export MAX_MODEL_LEN MAX_NEW_TOKENS BATCH_SIZE TP_SIZE MAX_SAMPLES
|
| 167 |
+
export EXPECTED_SAMPLES ENABLE_THINKING VLLM_BASE_PORT
|
| 168 |
+
python - "$OUTPUT_DIR/run_config.json" <<'PY'
|
| 169 |
+
import json
|
| 170 |
+
import os
|
| 171 |
+
import sys
|
| 172 |
+
|
| 173 |
+
keys = (
|
| 174 |
+
"MODEL_PATH",
|
| 175 |
+
"QA_FILE",
|
| 176 |
+
"VIDEO_ROOT",
|
| 177 |
+
"FRAME_BUDGET",
|
| 178 |
+
"MAX_FRAMES",
|
| 179 |
+
"EXACT_NFRAMES",
|
| 180 |
+
"FPS",
|
| 181 |
+
"VIDEO_TOTAL_PIXELS",
|
| 182 |
+
"VIDEO_MIN_PIXELS",
|
| 183 |
+
"VIDEO_MAX_PIXELS",
|
| 184 |
+
"MAX_MODEL_LEN",
|
| 185 |
+
"MAX_NEW_TOKENS",
|
| 186 |
+
"BATCH_SIZE",
|
| 187 |
+
"TP_SIZE",
|
| 188 |
+
"MAX_SAMPLES",
|
| 189 |
+
"EXPECTED_SAMPLES",
|
| 190 |
+
"ENABLE_THINKING",
|
| 191 |
+
"VLLM_BASE_PORT",
|
| 192 |
+
)
|
| 193 |
+
payload = {key.lower(): os.environ.get(key, "") for key in keys}
|
| 194 |
+
with open(sys.argv[1], "w", encoding="utf-8") as handle:
|
| 195 |
+
json.dump(payload, handle, indent=2, sort_keys=True)
|
| 196 |
+
handle.write("\n")
|
| 197 |
+
PY
|
| 198 |
+
|
| 199 |
+
cat <<EOF
|
| 200 |
+
==============================================
|
| 201 |
+
ReVSI Evaluation (vLLM, data-parallel)
|
| 202 |
+
==============================================
|
| 203 |
+
Model: $MODEL_PATH
|
| 204 |
+
QA file: $QA_FILE
|
| 205 |
+
Output: $OUTPUT_DIR
|
| 206 |
+
GPUs: ${GPULIST[*]} (${NUM_GPUS} total, TP=${TP_SIZE}, DP=${DP_SIZE})
|
| 207 |
+
Max tokens: new=$MAX_NEW_TOKENS model_len=$MAX_MODEL_LEN
|
| 208 |
+
Batch: $BATCH_SIZE
|
| 209 |
+
Task filter: ${TASK_FILTER:-<none>}
|
| 210 |
+
Max samples: ${MAX_SAMPLES}
|
| 211 |
+
Strict num: ${STRICT_NUMERIC_PROMPT}
|
| 212 |
+
Thinking: ${ENABLE_THINKING}
|
| 213 |
+
Media mode: ${MEDIA_MODE}
|
| 214 |
+
Video root: ${VIDEO_ROOT:-<none>}
|
| 215 |
+
Base port: ${VLLM_BASE_PORT}
|
| 216 |
+
Frame budget: ${FRAME_BUDGET} (exact_nframes=${EXACT_NFRAMES})
|
| 217 |
+
Video: max_frames=${MAX_FRAMES} fps=${FPS} total_pixels=${VIDEO_TOTAL_PIXELS}
|
| 218 |
+
==============================================
|
| 219 |
+
EOF
|
| 220 |
+
|
| 221 |
+
PIDS=()
|
| 222 |
+
cleanup() {
|
| 223 |
+
echo ""; echo "Caught interrupt, killing workers ..."
|
| 224 |
+
for pid in "${PIDS[@]}"; do kill -TERM "$pid" 2>/dev/null || true; done
|
| 225 |
+
wait 2>/dev/null || true
|
| 226 |
+
exit 1
|
| 227 |
+
}
|
| 228 |
+
trap cleanup INT TERM
|
| 229 |
+
|
| 230 |
+
EFFECTIVE_SHARD_COUNT="${GLOBAL_SHARD_COUNT:-$DP_SIZE}"
|
| 231 |
+
for IDX in $(seq 0 $((DP_SIZE - 1))); do
|
| 232 |
+
START=$(( IDX * TP_SIZE ))
|
| 233 |
+
GLOBAL_IDX=$((GLOBAL_SHARD_OFFSET + IDX))
|
| 234 |
+
SHARD_PORT=$((VLLM_BASE_PORT + IDX * 16))
|
| 235 |
+
SHARD_GPUS=""
|
| 236 |
+
for j in $(seq 0 $((TP_SIZE - 1))); do
|
| 237 |
+
g=${GPULIST[$((START + j))]}
|
| 238 |
+
SHARD_GPUS="${SHARD_GPUS}${SHARD_GPUS:+,}${g}"
|
| 239 |
+
done
|
| 240 |
+
|
| 241 |
+
OUT_JSONL="${OUTPUT_DIR}/results_shard${GLOBAL_IDX}.jsonl"
|
| 242 |
+
STRICT_FLAG=""
|
| 243 |
+
if [ "$STRICT_NUMERIC_PROMPT" = "1" ] || [ "$STRICT_NUMERIC_PROMPT" = "true" ]; then
|
| 244 |
+
STRICT_FLAG="--strict_numeric_prompt"
|
| 245 |
+
fi
|
| 246 |
+
VIDEO_MAX_PIXELS_ARGS=()
|
| 247 |
+
if [ -n "$VIDEO_MAX_PIXELS" ]; then
|
| 248 |
+
VIDEO_MAX_PIXELS_ARGS=(--video_max_pixels "$VIDEO_MAX_PIXELS")
|
| 249 |
+
fi
|
| 250 |
+
EXACT_NFRAMES_ARGS=()
|
| 251 |
+
if [ "$EXACT_NFRAMES" = "1" ] || [ "$EXACT_NFRAMES" = "true" ]; then
|
| 252 |
+
EXACT_NFRAMES_ARGS=(--exact_nframes)
|
| 253 |
+
fi
|
| 254 |
+
CUDA_VISIBLE_DEVICES="$SHARD_GPUS" \
|
| 255 |
+
VLLM_PORT="$SHARD_PORT" \
|
| 256 |
+
VLLM_HOST_IP=127.0.0.1 \
|
| 257 |
+
MASTER_PORT="$SHARD_PORT" \
|
| 258 |
+
MASTER_ADDR=127.0.0.1 \
|
| 259 |
+
PYTHONUNBUFFERED=1 \
|
| 260 |
+
python "${SCRIPT_DIR}/eval_revsi_vllm.py" \
|
| 261 |
+
--output_json_path "$OUT_JSONL" \
|
| 262 |
+
--model_path "$MODEL_PATH" \
|
| 263 |
+
--qa_file "$QA_FILE" \
|
| 264 |
+
--rank "$GLOBAL_IDX" \
|
| 265 |
+
--world_size "$EFFECTIVE_SHARD_COUNT" \
|
| 266 |
+
--tensor_parallel_size "$TP_SIZE" \
|
| 267 |
+
--max_model_len "$MAX_MODEL_LEN" \
|
| 268 |
+
--gpu_memory_utilization "$GPU_MEM_UTIL" \
|
| 269 |
+
--max_new_tokens "$MAX_NEW_TOKENS" \
|
| 270 |
+
--batch_size "$BATCH_SIZE" \
|
| 271 |
+
--score_log_interval "$SCORE_LOG_INTERVAL" \
|
| 272 |
+
--task_filter "$TASK_FILTER" \
|
| 273 |
+
--max_samples "$MAX_SAMPLES" \
|
| 274 |
+
--media_mode "$MEDIA_MODE" \
|
| 275 |
+
--video_root "$VIDEO_ROOT" \
|
| 276 |
+
--max_frames "$MAX_FRAMES" \
|
| 277 |
+
--fps "$FPS" \
|
| 278 |
+
--video_total_pixels "$VIDEO_TOTAL_PIXELS" \
|
| 279 |
+
--video_min_pixels "$VIDEO_MIN_PIXELS" \
|
| 280 |
+
"${VIDEO_MAX_PIXELS_ARGS[@]}" \
|
| 281 |
+
"${EXACT_NFRAMES_ARGS[@]}" \
|
| 282 |
+
--enable_thinking "$ENABLE_THINKING" \
|
| 283 |
+
$STRICT_FLAG \
|
| 284 |
+
> "$OUTPUT_DIR/worker_${GLOBAL_IDX}.log" 2>&1 &
|
| 285 |
+
PIDS+=($!)
|
| 286 |
+
echo "Launched shard $GLOBAL_IDX/$EFFECTIVE_SHARD_COUNT on GPU $SHARD_GPUS (PID ${PIDS[-1]})"
|
| 287 |
+
if [ "$IDX" -lt $((DP_SIZE - 1)) ] && [ "$LAUNCH_DELAY" -gt 0 ]; then
|
| 288 |
+
sleep "$LAUNCH_DELAY"
|
| 289 |
+
fi
|
| 290 |
+
done
|
| 291 |
+
|
| 292 |
+
echo "Waiting for ${DP_SIZE} workers ..."
|
| 293 |
+
FAILED=0
|
| 294 |
+
for i in "${!PIDS[@]}"; do
|
| 295 |
+
RC=0
|
| 296 |
+
wait "${PIDS[$i]}" || RC=$?
|
| 297 |
+
if [ $RC -ne 0 ]; then
|
| 298 |
+
echo "[FAIL] shard $i (PID ${PIDS[$i]}) exit=$RC"
|
| 299 |
+
FAILED=1
|
| 300 |
+
else
|
| 301 |
+
echo "[DONE] shard $i (PID ${PIDS[$i]})"
|
| 302 |
+
fi
|
| 303 |
+
done
|
| 304 |
+
|
| 305 |
+
if [ $FAILED -ne 0 ]; then
|
| 306 |
+
echo "ERROR: some workers failed; not merging incomplete shards." >&2
|
| 307 |
+
echo "Logs: $OUTPUT_DIR/worker_*.log" >&2
|
| 308 |
+
exit 1
|
| 309 |
+
fi
|
| 310 |
+
|
| 311 |
+
if [ "$MERGE_SHARDS" != "1" ]; then
|
| 312 |
+
echo "Local ReVSI shard range completed; centralized merge deferred."
|
| 313 |
+
exit 0
|
| 314 |
+
fi
|
| 315 |
+
|
| 316 |
+
python "${SCRIPT_DIR}/merge_multinode_shards.py" \
|
| 317 |
+
--output-dir "$OUTPUT_DIR" \
|
| 318 |
+
--num-shards "$EFFECTIVE_SHARD_COUNT" \
|
| 319 |
+
--expected-samples "$EXPECTED_SAMPLES"
|
| 320 |
+
echo "Done: $OUTPUT_DIR"
|
| 321 |
+
exit $FAILED
|
code/eval/task/segmentation/eval_seg_vllm.py
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Segmentation prompt evaluation with vLLM.
|
| 4 |
+
|
| 5 |
+
This is the inference stage only. It reads OneThinker-eval style records,
|
| 6 |
+
asks the model for SAM2 prompts (`boxes`, `positive_points`, `negative_points`,
|
| 7 |
+
and optional `time`), and writes `predicted_answer_norm` for the SAM2
|
| 8 |
+
post-processing stage.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import argparse
|
| 14 |
+
import json
|
| 15 |
+
import os
|
| 16 |
+
import re
|
| 17 |
+
import sys
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
from typing import Any, Dict, Iterable, List, Optional
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
os.environ.setdefault("VLLM_USE_V1", "1")
|
| 23 |
+
os.environ.setdefault("VLLM_WORKER_MULTIPROC_METHOD", "spawn")
|
| 24 |
+
os.environ.setdefault("FORCE_QWENVL_VIDEO_READER", "decord")
|
| 25 |
+
os.environ.setdefault("DECORD_EOF_RETRY_MAX", "20480")
|
| 26 |
+
|
| 27 |
+
_PATCH_DIRS = (
|
| 28 |
+
Path(__file__).resolve().parents[1],
|
| 29 |
+
Path(__file__).resolve().parents[2] / "Eval-onethink",
|
| 30 |
+
)
|
| 31 |
+
if os.environ.get("FORCE_QWENVL_VIDEO_READER") == "decord":
|
| 32 |
+
for _patch_dir in _PATCH_DIRS:
|
| 33 |
+
if not (_patch_dir / "qwenvl_decord_patch.py").is_file():
|
| 34 |
+
continue
|
| 35 |
+
sys.path.insert(0, str(_patch_dir))
|
| 36 |
+
try:
|
| 37 |
+
import qwenvl_decord_patch # noqa: F401
|
| 38 |
+
except Exception as exc:
|
| 39 |
+
print(f"[warn] failed to import qwenvl_decord_patch: {exc}", file=sys.stderr)
|
| 40 |
+
break
|
| 41 |
+
|
| 42 |
+
import torch # noqa: E402
|
| 43 |
+
from qwen_vl_utils import process_vision_info # noqa: E402
|
| 44 |
+
from tqdm import tqdm # noqa: E402
|
| 45 |
+
from transformers import AutoProcessor # noqa: E402
|
| 46 |
+
from vllm import LLM, SamplingParams # noqa: E402
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
# Prompts — single source of truth in eval/task/eval_prompt.py
|
| 50 |
+
# (training tails match data/joint/sft_joint_all.jsonl seg_image / seg_video).
|
| 51 |
+
_EVAL_TASK_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 52 |
+
if _EVAL_TASK_DIR not in sys.path:
|
| 53 |
+
sys.path.insert(0, _EVAL_TASK_DIR)
|
| 54 |
+
|
| 55 |
+
from canonical_data import ( # noqa: E402
|
| 56 |
+
load_json_records,
|
| 57 |
+
repository_relative_output_path,
|
| 58 |
+
)
|
| 59 |
+
from eval_prompt import ( # noqa: E402
|
| 60 |
+
TRAIN_SEG_IMAGE_TAIL,
|
| 61 |
+
TRAIN_SEG_VIDEO_TAIL,
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
ANSWER_RE = re.compile(r"<answer>\s*(.*?)\s*</answer>", re.DOTALL | re.IGNORECASE)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def extract_answer(text: Optional[str]) -> str:
|
| 69 |
+
if not isinstance(text, str):
|
| 70 |
+
return ""
|
| 71 |
+
match = ANSWER_RE.search(text)
|
| 72 |
+
return match.group(1).strip() if match else text.strip()
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def load_json_or_jsonl(path: Path) -> List[Dict[str, Any]]:
|
| 76 |
+
if path.suffix == ".jsonl":
|
| 77 |
+
return load_json_records(path)
|
| 78 |
+
with path.open("r", encoding="utf-8") as f:
|
| 79 |
+
payload = json.load(f)
|
| 80 |
+
if isinstance(payload, dict) and isinstance(payload.get("results"), list):
|
| 81 |
+
return payload["results"]
|
| 82 |
+
if isinstance(payload, list):
|
| 83 |
+
return payload
|
| 84 |
+
raise ValueError(f"Unsupported input JSON format: {path}")
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def resolve_dataset_path(bench_dir: str, dataset: str) -> Path:
|
| 88 |
+
raw = Path(dataset)
|
| 89 |
+
if raw.is_file():
|
| 90 |
+
return raw
|
| 91 |
+
base = Path(bench_dir)
|
| 92 |
+
candidates = [base / dataset, base / f"{dataset}.json", base / f"{dataset}.jsonl"]
|
| 93 |
+
for path in candidates:
|
| 94 |
+
if path.is_file():
|
| 95 |
+
return path
|
| 96 |
+
raise FileNotFoundError(f"Cannot find dataset {dataset!r} under {bench_dir}")
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def clean_question(problem: str) -> str:
|
| 100 |
+
return re.sub(r"^\s*<(image|video)>\s*", "", problem or "", flags=re.IGNORECASE).strip()
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def build_prompt(example: Dict[str, Any], prompt_mode: str) -> List[Dict[str, Any]]:
|
| 104 |
+
# Match the joint SFT training prompt verbatim: the question is followed
|
| 105 |
+
# immediately by the training tail.
|
| 106 |
+
question = clean_question(str(example.get("problem") or example.get("question") or ""))
|
| 107 |
+
data_type = str(example.get("data_type") or "").strip().lower()
|
| 108 |
+
tail = TRAIN_SEG_VIDEO_TAIL if data_type == "video" else TRAIN_SEG_IMAGE_TAIL
|
| 109 |
+
text = f"{question}\n{tail}"
|
| 110 |
+
return [{"role": "user", "content": [{"type": "text", "text": text}]}]
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def resolve_media_path(base_prefix: str, raw_path: str) -> tuple[str, str]:
|
| 114 |
+
"""Return `(absolute_path_for_inference, relative_path_for_output)`.
|
| 115 |
+
|
| 116 |
+
OneThinker JSON often stores paths like `./Evaluation/Refcoco/...`, while
|
| 117 |
+
local mirrors may place media directly under `Refcoco/...`.
|
| 118 |
+
"""
|
| 119 |
+
if os.path.isabs(raw_path):
|
| 120 |
+
return raw_path, repository_relative_output_path(raw_path, base_prefix)
|
| 121 |
+
|
| 122 |
+
rel = raw_path.lstrip("./")
|
| 123 |
+
candidates = [rel]
|
| 124 |
+
if rel.startswith("Evaluation/"):
|
| 125 |
+
candidates.append(rel[len("Evaluation/"):])
|
| 126 |
+
|
| 127 |
+
for cand in candidates:
|
| 128 |
+
full = os.path.join(base_prefix, cand)
|
| 129 |
+
if os.path.exists(full):
|
| 130 |
+
return full, f"./{cand}"
|
| 131 |
+
|
| 132 |
+
return os.path.join(base_prefix, rel), f"./{rel}"
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def canonical_output_path(base_prefix: str, raw_path: str) -> str:
|
| 136 |
+
return resolve_media_path(base_prefix, raw_path)[1]
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def build_content(example: Dict[str, Any], args: argparse.Namespace) -> List[Dict[str, Any]]:
|
| 140 |
+
data_type = str(example.get("data_type") or "").strip().lower()
|
| 141 |
+
media_path, _ = resolve_media_path(args.base_prefix, str(example.get("path") or ""))
|
| 142 |
+
if data_type == "image":
|
| 143 |
+
return [{
|
| 144 |
+
"type": "image",
|
| 145 |
+
"image": media_path,
|
| 146 |
+
"max_pixels": args.max_pixels_image,
|
| 147 |
+
"min_pixels": args.min_pixels_image,
|
| 148 |
+
}]
|
| 149 |
+
if data_type == "video":
|
| 150 |
+
item = {
|
| 151 |
+
"type": "video",
|
| 152 |
+
"video": media_path,
|
| 153 |
+
"max_pixels": args.max_pixels_video,
|
| 154 |
+
"max_frames": args.max_frames,
|
| 155 |
+
"fps": args.fps,
|
| 156 |
+
}
|
| 157 |
+
if os.getenv("EVAL_VIDEO_ITEM_ONETHINKER", "0") != "1":
|
| 158 |
+
item["min_pixels"] = args.min_pixels_video
|
| 159 |
+
item["total_pixels"] = args.total_pixels_video
|
| 160 |
+
return [item]
|
| 161 |
+
raise ValueError(f"Unsupported data_type for segmentation: {data_type!r}")
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def prepare_vllm_input(
|
| 165 |
+
messages: List[Dict[str, Any]],
|
| 166 |
+
processor: Any,
|
| 167 |
+
patch_size: Optional[int],
|
| 168 |
+
enable_thinking: bool = False,
|
| 169 |
+
) -> Dict[str, Any]:
|
| 170 |
+
omit_thinking_kw = os.getenv("EVAL_OMIT_ENABLE_THINKING_KW", "0") == "1"
|
| 171 |
+
force_do_resize_false = os.getenv("EVAL_FORCE_DO_RESIZE_FALSE", "0") == "1"
|
| 172 |
+
|
| 173 |
+
if omit_thinking_kw:
|
| 174 |
+
# OneThinker eval_bench.py omits this kwarg entirely. Keep this
|
| 175 |
+
# switch for paper-parity runs with OneThinker checkpoints.
|
| 176 |
+
text = processor.apply_chat_template(
|
| 177 |
+
messages,
|
| 178 |
+
tokenize=False,
|
| 179 |
+
add_generation_prompt=True,
|
| 180 |
+
)
|
| 181 |
+
else:
|
| 182 |
+
text = processor.apply_chat_template(
|
| 183 |
+
messages,
|
| 184 |
+
tokenize=False,
|
| 185 |
+
add_generation_prompt=True,
|
| 186 |
+
enable_thinking=enable_thinking,
|
| 187 |
+
)
|
| 188 |
+
kwargs: Dict[str, Any] = {"return_video_kwargs": True, "return_video_metadata": True}
|
| 189 |
+
if patch_size is None:
|
| 190 |
+
patch_size = getattr(getattr(processor, "image_processor", None), "patch_size", None)
|
| 191 |
+
if patch_size is not None:
|
| 192 |
+
kwargs["image_patch_size"] = patch_size
|
| 193 |
+
image_inputs, video_inputs, video_kwargs = process_vision_info(messages, **kwargs)
|
| 194 |
+
video_kwargs = video_kwargs or {}
|
| 195 |
+
if force_do_resize_false:
|
| 196 |
+
# Diagnostic only. For Qwen3.5 video segmentation this can trigger
|
| 197 |
+
# processor shape mismatches when decoded frames are not patch-aligned.
|
| 198 |
+
video_kwargs["do_resize"] = False
|
| 199 |
+
mm_data: Dict[str, Any] = {}
|
| 200 |
+
if image_inputs is not None:
|
| 201 |
+
mm_data["image"] = image_inputs
|
| 202 |
+
if video_inputs is not None:
|
| 203 |
+
mm_data["video"] = video_inputs
|
| 204 |
+
return {"prompt": text, "multi_modal_data": mm_data, "mm_processor_kwargs": video_kwargs}
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
def parse_json_answer(answer: str) -> Optional[Dict[str, Any]]:
|
| 208 |
+
if not answer:
|
| 209 |
+
return None
|
| 210 |
+
try:
|
| 211 |
+
obj = json.loads(answer)
|
| 212 |
+
return obj if isinstance(obj, dict) else None
|
| 213 |
+
except Exception:
|
| 214 |
+
pass
|
| 215 |
+
start = answer.find("{")
|
| 216 |
+
if start < 0:
|
| 217 |
+
return None
|
| 218 |
+
depth = 0
|
| 219 |
+
for idx in range(start, len(answer)):
|
| 220 |
+
if answer[idx] == "{":
|
| 221 |
+
depth += 1
|
| 222 |
+
elif answer[idx] == "}":
|
| 223 |
+
depth -= 1
|
| 224 |
+
if depth == 0:
|
| 225 |
+
try:
|
| 226 |
+
obj = json.loads(answer[start:idx + 1])
|
| 227 |
+
return obj if isinstance(obj, dict) else None
|
| 228 |
+
except Exception:
|
| 229 |
+
return None
|
| 230 |
+
return None
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
def _is_number_list(value: Any, length: int) -> bool:
|
| 234 |
+
if not isinstance(value, list) or len(value) != length:
|
| 235 |
+
return False
|
| 236 |
+
try:
|
| 237 |
+
for item in value:
|
| 238 |
+
float(item)
|
| 239 |
+
return True
|
| 240 |
+
except Exception:
|
| 241 |
+
return False
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def _is_point_list(value: Any) -> bool:
|
| 245 |
+
if not isinstance(value, list) or len(value) < 1:
|
| 246 |
+
return False
|
| 247 |
+
return all(_is_number_list(point, 2) for point in value)
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
def normalize_seg_prompt(obj: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
| 251 |
+
"""Normalize common model variants to the SAM2 prompt schema.
|
| 252 |
+
|
| 253 |
+
The prompt asks for one bounding box but uses the plural key "boxes". Some
|
| 254 |
+
models naturally return a list-of-boxes (`[[x1, y1, x2, y2]]`), while the
|
| 255 |
+
downstream OneThinker SAM2 script expects the single box as a flat list.
|
| 256 |
+
"""
|
| 257 |
+
if not isinstance(obj, dict):
|
| 258 |
+
return obj
|
| 259 |
+
out = dict(obj)
|
| 260 |
+
boxes = out.get("boxes")
|
| 261 |
+
if (
|
| 262 |
+
isinstance(boxes, list)
|
| 263 |
+
and len(boxes) == 1
|
| 264 |
+
and _is_number_list(boxes[0], 4)
|
| 265 |
+
):
|
| 266 |
+
out["boxes"] = boxes[0]
|
| 267 |
+
return out
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
def valid_seg_prompt(obj: Optional[Dict[str, Any]], data_type: str) -> bool:
|
| 271 |
+
if not isinstance(obj, dict):
|
| 272 |
+
return False
|
| 273 |
+
boxes = obj.get("boxes")
|
| 274 |
+
if not _is_number_list(boxes, 4):
|
| 275 |
+
return False
|
| 276 |
+
for key in ("positive_points", "negative_points"):
|
| 277 |
+
if not _is_point_list(obj.get(key)):
|
| 278 |
+
return False
|
| 279 |
+
if data_type == "video":
|
| 280 |
+
try:
|
| 281 |
+
float(obj.get("time"))
|
| 282 |
+
except Exception:
|
| 283 |
+
return False
|
| 284 |
+
return True
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
def iter_filtered(data: Iterable[Dict[str, Any]], args: argparse.Namespace) -> List[Dict[str, Any]]:
|
| 288 |
+
rows = []
|
| 289 |
+
missing_media = 0
|
| 290 |
+
for rec in data:
|
| 291 |
+
if str(rec.get("problem_type", "")).strip().lower() != "segmentation":
|
| 292 |
+
continue
|
| 293 |
+
data_type = str(rec.get("data_type") or "").strip().lower()
|
| 294 |
+
if data_type not in {"image", "video"}:
|
| 295 |
+
continue
|
| 296 |
+
if args.data_type != "all" and data_type != args.data_type:
|
| 297 |
+
continue
|
| 298 |
+
media_path, _ = resolve_media_path(args.base_prefix, str(rec.get("path") or ""))
|
| 299 |
+
if args.skip_missing_media and not os.path.exists(media_path):
|
| 300 |
+
missing_media += 1
|
| 301 |
+
continue
|
| 302 |
+
rows.append(rec)
|
| 303 |
+
if missing_media:
|
| 304 |
+
print(f"[warn] skipped {missing_media} samples with missing media", file=sys.stderr)
|
| 305 |
+
if args.max_samples is not None:
|
| 306 |
+
rows = rows[:args.max_samples]
|
| 307 |
+
if args.chunk > 1:
|
| 308 |
+
rows = [r for i, r in enumerate(rows) if i % args.chunk == args.index]
|
| 309 |
+
return rows
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
def evaluate_dataset(dataset: str, args: argparse.Namespace, llm: LLM, processor: Any, sampling_params: SamplingParams) -> Dict[str, Any]:
|
| 313 |
+
data_path = resolve_dataset_path(args.bench_dir, dataset)
|
| 314 |
+
rows = iter_filtered(load_json_or_jsonl(data_path), args)
|
| 315 |
+
|
| 316 |
+
outputs: List[Dict[str, Any]] = []
|
| 317 |
+
for start in tqdm(range(0, len(rows), args.batch_size), desc=f"{dataset}"):
|
| 318 |
+
batch = rows[start:start + args.batch_size]
|
| 319 |
+
inputs = []
|
| 320 |
+
for example in batch:
|
| 321 |
+
messages = build_prompt(example, args.prompt_mode)
|
| 322 |
+
user_msg = messages[-1]
|
| 323 |
+
user_msg = dict(user_msg)
|
| 324 |
+
user_msg["content"] = build_content(example, args) + user_msg["content"]
|
| 325 |
+
run_messages = messages[:-1] + [user_msg]
|
| 326 |
+
inputs.append(prepare_vllm_input(run_messages, processor, args.patch_size, args.enable_thinking))
|
| 327 |
+
|
| 328 |
+
generated = llm.generate(inputs, sampling_params=sampling_params)
|
| 329 |
+
texts = [out.outputs[0].text for out in generated]
|
| 330 |
+
|
| 331 |
+
for example, text in zip(batch, texts):
|
| 332 |
+
answer = extract_answer(text)
|
| 333 |
+
parsed = parse_json_answer(answer)
|
| 334 |
+
parsed = normalize_seg_prompt(parsed)
|
| 335 |
+
data_type = str(example.get("data_type") or "").strip().lower()
|
| 336 |
+
sample = dict(example)
|
| 337 |
+
sample["path"] = canonical_output_path(args.base_prefix, str(example.get("path") or ""))
|
| 338 |
+
sample["output"] = text
|
| 339 |
+
sample["prediction"] = answer
|
| 340 |
+
sample["predicted_answer_norm"] = (
|
| 341 |
+
json.dumps(parsed, ensure_ascii=False) if isinstance(parsed, dict) else answer
|
| 342 |
+
)
|
| 343 |
+
sample["parsed_prediction"] = parsed
|
| 344 |
+
sample["parse_ok"] = valid_seg_prompt(parsed, data_type)
|
| 345 |
+
outputs.append(sample)
|
| 346 |
+
|
| 347 |
+
total = len(outputs)
|
| 348 |
+
parsed = sum(1 for row in outputs if row.get("parse_ok"))
|
| 349 |
+
by_type: Dict[str, Dict[str, Any]] = {}
|
| 350 |
+
for data_type in ("image", "video"):
|
| 351 |
+
part = [r for r in outputs if r.get("data_type") == data_type]
|
| 352 |
+
if part:
|
| 353 |
+
ok = sum(1 for r in part if r.get("parse_ok"))
|
| 354 |
+
by_type[data_type] = {
|
| 355 |
+
"num_samples": len(part),
|
| 356 |
+
"parse_rate": round(ok / len(part) * 100.0, 2),
|
| 357 |
+
}
|
| 358 |
+
return {
|
| 359 |
+
"dataset": dataset,
|
| 360 |
+
"input_file": str(data_path),
|
| 361 |
+
"results": outputs,
|
| 362 |
+
"metrics": {
|
| 363 |
+
"num_samples": total,
|
| 364 |
+
"parse_ok": parsed,
|
| 365 |
+
"parse_rate": round(parsed / total * 100.0, 2) if total else 0.0,
|
| 366 |
+
"by_data_type": by_type,
|
| 367 |
+
},
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
|
| 371 |
+
def main() -> None:
|
| 372 |
+
parser = argparse.ArgumentParser()
|
| 373 |
+
parser.add_argument("--model_path", required=True)
|
| 374 |
+
parser.add_argument("--processor_path", default=None)
|
| 375 |
+
parser.add_argument("--bench_dir", required=True)
|
| 376 |
+
parser.add_argument("--datasets", required=True, help="Comma-separated dataset names or JSON/JSONL paths.")
|
| 377 |
+
parser.add_argument("--output_dir", required=True)
|
| 378 |
+
parser.add_argument("--base_prefix", default="")
|
| 379 |
+
parser.add_argument("--data_type", choices=["all", "image", "video"], default="all")
|
| 380 |
+
parser.add_argument("--prompt_mode", choices=["think", "no_think", "bare", "onethink_system", "train_seg"], default="no_think")
|
| 381 |
+
parser.add_argument("--enable_thinking", action="store_true", default=False)
|
| 382 |
+
parser.add_argument("--max_samples", type=int, default=None)
|
| 383 |
+
parser.add_argument("--skip_missing_media", action="store_true")
|
| 384 |
+
parser.add_argument("--chunk", type=int, default=1)
|
| 385 |
+
parser.add_argument("--index", type=int, default=0)
|
| 386 |
+
parser.add_argument("--batch_size", type=int, default=16)
|
| 387 |
+
parser.add_argument("--temperature", type=float, default=0.0)
|
| 388 |
+
parser.add_argument("--top_k", type=int, default=-1)
|
| 389 |
+
parser.add_argument("--max_new_tokens", type=int, default=1024)
|
| 390 |
+
parser.add_argument("--tensor_parallel_size", type=int, default=1)
|
| 391 |
+
parser.add_argument("--gpu_memory_utilization", type=float, default=0.85)
|
| 392 |
+
parser.add_argument("--max_model_len", type=int, default=32768)
|
| 393 |
+
parser.add_argument("--seed", type=int, default=42)
|
| 394 |
+
parser.add_argument("--max_pixels_image", type=int, default=1024 * 32 * 32)
|
| 395 |
+
parser.add_argument("--min_pixels_image", type=int, default=4 * 32 * 32)
|
| 396 |
+
parser.add_argument("--max_pixels_video", type=int, default=256 * 32 * 32)
|
| 397 |
+
parser.add_argument("--min_pixels_video", type=int, default=4 * 32 * 32)
|
| 398 |
+
parser.add_argument("--total_pixels_video", type=int, default=256 * 64 * 32 * 32)
|
| 399 |
+
parser.add_argument("--max_frames", type=int, default=128)
|
| 400 |
+
parser.add_argument("--fps", type=int, default=2)
|
| 401 |
+
parser.add_argument("--patch_size", type=int, default=None)
|
| 402 |
+
args = parser.parse_args()
|
| 403 |
+
|
| 404 |
+
if args.chunk < 1 or not (0 <= args.index < args.chunk):
|
| 405 |
+
raise ValueError("--chunk must be >=1 and --index must be in [0, chunk)")
|
| 406 |
+
|
| 407 |
+
os.makedirs(args.output_dir, exist_ok=True)
|
| 408 |
+
torch.manual_seed(args.seed)
|
| 409 |
+
|
| 410 |
+
processor = AutoProcessor.from_pretrained(args.processor_path or args.model_path)
|
| 411 |
+
llm = LLM(
|
| 412 |
+
model=args.model_path,
|
| 413 |
+
tensor_parallel_size=args.tensor_parallel_size,
|
| 414 |
+
max_model_len=args.max_model_len,
|
| 415 |
+
gpu_memory_utilization=args.gpu_memory_utilization,
|
| 416 |
+
mm_encoder_tp_mode="data",
|
| 417 |
+
seed=args.seed,
|
| 418 |
+
)
|
| 419 |
+
sampling_params = SamplingParams(
|
| 420 |
+
temperature=args.temperature,
|
| 421 |
+
max_tokens=args.max_new_tokens,
|
| 422 |
+
top_k=args.top_k,
|
| 423 |
+
stop_token_ids=[],
|
| 424 |
+
)
|
| 425 |
+
|
| 426 |
+
summary: Dict[str, Any] = {}
|
| 427 |
+
for dataset in [x.strip() for x in args.datasets.split(",") if x.strip()]:
|
| 428 |
+
payload = evaluate_dataset(dataset, args, llm, processor, sampling_params)
|
| 429 |
+
dataset_key = Path(dataset).stem if Path(dataset).suffix else dataset
|
| 430 |
+
suffix = f"_shard{args.index}" if args.chunk > 1 else ""
|
| 431 |
+
out_path = Path(args.output_dir) / f"results_{dataset_key}{suffix}.json"
|
| 432 |
+
with out_path.open("w", encoding="utf-8") as f:
|
| 433 |
+
json.dump({"results": payload["results"], "metrics": payload["metrics"]}, f, ensure_ascii=False, indent=2)
|
| 434 |
+
summary[dataset_key] = payload["metrics"]
|
| 435 |
+
print(
|
| 436 |
+
f"{dataset_key}: n={payload['metrics']['num_samples']} "
|
| 437 |
+
f"parse={payload['metrics']['parse_rate']:.2f}% -> {out_path}"
|
| 438 |
+
)
|
| 439 |
+
|
| 440 |
+
summary_name = f"summary_shard{args.index}.json" if args.chunk > 1 else "summary.json"
|
| 441 |
+
with (Path(args.output_dir) / summary_name).open("w", encoding="utf-8") as f:
|
| 442 |
+
json.dump(summary, f, ensure_ascii=False, indent=2)
|
| 443 |
+
|
| 444 |
+
|
| 445 |
+
if __name__ == "__main__":
|
| 446 |
+
main()
|
code/eval/task/segmentation/post_sam2.py
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Thin project-local wrapper around OneThinker's SAM2 segmentation post-process.
|
| 4 |
+
|
| 5 |
+
The OneThinker script contains the full image/video mask generation and metric
|
| 6 |
+
logic. This wrapper only injects paths from CLI args so evaluation can be run
|
| 7 |
+
from this repository without editing the third-party checkout.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import argparse
|
| 13 |
+
import importlib.util
|
| 14 |
+
import json
|
| 15 |
+
import multiprocessing.spawn as mp_spawn
|
| 16 |
+
import os
|
| 17 |
+
import sys
|
| 18 |
+
import tempfile
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
from typing import Any
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
DEFAULT_ONETHINKER_SCRIPT = str(
|
| 24 |
+
Path(__file__).resolve().parents[3]
|
| 25 |
+
/ "third_party"
|
| 26 |
+
/ "OneThinker"
|
| 27 |
+
/ "Evaluation"
|
| 28 |
+
/ "Eval"
|
| 29 |
+
/ "seg_post_sam2.py"
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def sam2_config_name(
|
| 34 |
+
value: str,
|
| 35 |
+
package_root: str | Path | None = None,
|
| 36 |
+
) -> str:
|
| 37 |
+
"""Convert an installed SAM2 YAML path to Hydra's package-relative name."""
|
| 38 |
+
|
| 39 |
+
path = Path(value).expanduser()
|
| 40 |
+
if not path.is_absolute():
|
| 41 |
+
return path.as_posix()
|
| 42 |
+
if package_root is None:
|
| 43 |
+
try:
|
| 44 |
+
import sam2
|
| 45 |
+
except ImportError:
|
| 46 |
+
return str(path)
|
| 47 |
+
package_root = Path(sam2.__file__).resolve().parent
|
| 48 |
+
try:
|
| 49 |
+
return path.resolve().relative_to(Path(package_root).resolve()).as_posix()
|
| 50 |
+
except ValueError:
|
| 51 |
+
return str(path)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def load_module(script_path: str):
|
| 55 |
+
path = Path(script_path)
|
| 56 |
+
if not path.is_file():
|
| 57 |
+
raise FileNotFoundError(f"OneThinker SAM2 script not found: {path}")
|
| 58 |
+
# Use the real filename as the module name so multiprocessing "spawn"
|
| 59 |
+
# workers can import it from path.parent when unpickling worker_run.
|
| 60 |
+
module_name = path.stem
|
| 61 |
+
spec = importlib.util.spec_from_file_location(module_name, path)
|
| 62 |
+
if spec is None or spec.loader is None:
|
| 63 |
+
raise RuntimeError(f"Failed to load module spec from {path}")
|
| 64 |
+
module = importlib.util.module_from_spec(spec)
|
| 65 |
+
sys.path.insert(0, str(path.parent))
|
| 66 |
+
# multiprocessing pickles functions by module name; dynamic modules must be
|
| 67 |
+
# registered so child processes can resolve worker_run.
|
| 68 |
+
sys.modules[module_name] = module
|
| 69 |
+
spec.loader.exec_module(module)
|
| 70 |
+
return module
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def _results(payload: Any) -> list[dict[str, Any]]:
|
| 74 |
+
if isinstance(payload, dict) and isinstance(payload.get("results"), list):
|
| 75 |
+
return payload["results"]
|
| 76 |
+
if isinstance(payload, list):
|
| 77 |
+
return payload
|
| 78 |
+
raise ValueError("SAM2 input must be a list or an object containing `results`.")
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def _segmentation_output(sample: dict[str, Any]) -> dict[str, Any] | None:
|
| 82 |
+
output = sample.get("segmentation_output")
|
| 83 |
+
if isinstance(output, dict):
|
| 84 |
+
return output
|
| 85 |
+
task_payload = sample.get("task_payload")
|
| 86 |
+
if isinstance(task_payload, dict):
|
| 87 |
+
output = task_payload.get("segmentation_output")
|
| 88 |
+
if isinstance(output, dict):
|
| 89 |
+
sample["segmentation_output"] = output
|
| 90 |
+
return output
|
| 91 |
+
return None
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def _compressed_rle(
|
| 95 |
+
size: Any,
|
| 96 |
+
counts: list[Any],
|
| 97 |
+
cocomask: Any,
|
| 98 |
+
) -> dict[str, Any] | None:
|
| 99 |
+
if (
|
| 100 |
+
not isinstance(size, (list, tuple))
|
| 101 |
+
or len(size) != 2
|
| 102 |
+
or not all(isinstance(value, (int, float)) for value in size)
|
| 103 |
+
):
|
| 104 |
+
return None
|
| 105 |
+
height, width = (int(size[0]), int(size[1]))
|
| 106 |
+
if height <= 0 or width <= 0:
|
| 107 |
+
return None
|
| 108 |
+
encoded = cocomask.frPyObjects(
|
| 109 |
+
{"size": [height, width], "counts": counts},
|
| 110 |
+
height,
|
| 111 |
+
width,
|
| 112 |
+
)
|
| 113 |
+
if isinstance(encoded, list):
|
| 114 |
+
encoded = cocomask.merge(encoded)
|
| 115 |
+
encoded_counts = encoded["counts"]
|
| 116 |
+
if isinstance(encoded_counts, bytes):
|
| 117 |
+
encoded_counts = encoded_counts.decode("utf-8")
|
| 118 |
+
return {"size": [height, width], "counts": encoded_counts}
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def normalize_missing_rle_counts(payload: Any, cocomask: Any) -> int:
|
| 122 |
+
"""Repair mask annotations that encode empty masks or polygons without RLE counts."""
|
| 123 |
+
|
| 124 |
+
repaired = 0
|
| 125 |
+
for sample in _results(payload):
|
| 126 |
+
segmentation = _segmentation_output(sample)
|
| 127 |
+
if segmentation is None:
|
| 128 |
+
continue
|
| 129 |
+
rle = segmentation.get("segmentation_rle")
|
| 130 |
+
|
| 131 |
+
if sample.get("data_type") == "video":
|
| 132 |
+
if not isinstance(rle, dict):
|
| 133 |
+
continue
|
| 134 |
+
for frame, frame_rle in list(rle.items()):
|
| 135 |
+
if not isinstance(frame_rle, dict):
|
| 136 |
+
continue
|
| 137 |
+
counts = frame_rle.get("counts")
|
| 138 |
+
if counts is not None and not isinstance(counts, list):
|
| 139 |
+
continue
|
| 140 |
+
size = frame_rle.get("size")
|
| 141 |
+
if (
|
| 142 |
+
counts is None
|
| 143 |
+
and (
|
| 144 |
+
not isinstance(size, (list, tuple))
|
| 145 |
+
or len(size) != 2
|
| 146 |
+
)
|
| 147 |
+
):
|
| 148 |
+
continue
|
| 149 |
+
replacement = _compressed_rle(
|
| 150 |
+
size,
|
| 151 |
+
counts
|
| 152 |
+
if isinstance(counts, list)
|
| 153 |
+
else [int(size[0]) * int(size[1])],
|
| 154 |
+
cocomask,
|
| 155 |
+
)
|
| 156 |
+
if replacement is not None:
|
| 157 |
+
rle[frame] = replacement
|
| 158 |
+
repaired += 1
|
| 159 |
+
continue
|
| 160 |
+
|
| 161 |
+
if not isinstance(rle, dict):
|
| 162 |
+
continue
|
| 163 |
+
|
| 164 |
+
size = rle.get("size")
|
| 165 |
+
counts = rle.get("counts")
|
| 166 |
+
if isinstance(counts, list):
|
| 167 |
+
replacement = _compressed_rle(size, counts, cocomask)
|
| 168 |
+
if replacement is not None:
|
| 169 |
+
segmentation["segmentation_rle"] = replacement
|
| 170 |
+
repaired += 1
|
| 171 |
+
continue
|
| 172 |
+
if counts is not None:
|
| 173 |
+
continue
|
| 174 |
+
|
| 175 |
+
polygons = segmentation.get("segmentation_polygon")
|
| 176 |
+
if polygons:
|
| 177 |
+
if isinstance(polygons, list) and polygons and isinstance(
|
| 178 |
+
polygons[0], (int, float)
|
| 179 |
+
):
|
| 180 |
+
polygons = [polygons]
|
| 181 |
+
if (
|
| 182 |
+
isinstance(size, (list, tuple))
|
| 183 |
+
and len(size) == 2
|
| 184 |
+
and polygons
|
| 185 |
+
):
|
| 186 |
+
height, width = int(size[0]), int(size[1])
|
| 187 |
+
encoded = cocomask.frPyObjects(polygons, height, width)
|
| 188 |
+
if isinstance(encoded, list):
|
| 189 |
+
encoded = cocomask.merge(encoded)
|
| 190 |
+
counts = encoded["counts"]
|
| 191 |
+
if isinstance(counts, bytes):
|
| 192 |
+
counts = counts.decode("utf-8")
|
| 193 |
+
segmentation["segmentation_rle"] = {
|
| 194 |
+
"size": [height, width],
|
| 195 |
+
"counts": counts,
|
| 196 |
+
}
|
| 197 |
+
repaired += 1
|
| 198 |
+
continue
|
| 199 |
+
|
| 200 |
+
replacement = None
|
| 201 |
+
if isinstance(size, (list, tuple)) and len(size) == 2:
|
| 202 |
+
replacement = _compressed_rle(
|
| 203 |
+
size,
|
| 204 |
+
[int(size[0]) * int(size[1])],
|
| 205 |
+
cocomask,
|
| 206 |
+
)
|
| 207 |
+
if replacement is not None:
|
| 208 |
+
segmentation["segmentation_rle"] = replacement
|
| 209 |
+
repaired += 1
|
| 210 |
+
|
| 211 |
+
return repaired
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def _sam2_output_path(input_json: str | Path) -> Path:
|
| 215 |
+
path = Path(input_json)
|
| 216 |
+
return path.with_name(f"{path.stem}_sam2.json")
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def _aggregate_rewards(payload: dict[str, Any]) -> None:
|
| 220 |
+
ok_items = [
|
| 221 |
+
row
|
| 222 |
+
for row in _results(payload)
|
| 223 |
+
if row.get("status") == "ok" and isinstance(row.get("reward"), dict)
|
| 224 |
+
]
|
| 225 |
+
video_j: list[float] = []
|
| 226 |
+
video_f: list[float] = []
|
| 227 |
+
video_jf: list[float] = []
|
| 228 |
+
image_iou: list[float] = []
|
| 229 |
+
total_inter = 0
|
| 230 |
+
total_union = 0
|
| 231 |
+
|
| 232 |
+
for row in ok_items:
|
| 233 |
+
reward = row["reward"]
|
| 234 |
+
if all(key in reward for key in ("J", "F", "J&F")):
|
| 235 |
+
video_j.append(float(reward["J"]))
|
| 236 |
+
video_f.append(float(reward["F"]))
|
| 237 |
+
video_jf.append(float(reward["J&F"]))
|
| 238 |
+
if "IoU" in reward:
|
| 239 |
+
image_iou.append(float(reward["IoU"]))
|
| 240 |
+
total_inter += int(reward.get("inter", 0))
|
| 241 |
+
total_union += int(reward.get("union", 0))
|
| 242 |
+
|
| 243 |
+
averages: dict[str, float] = {}
|
| 244 |
+
if video_j:
|
| 245 |
+
averages.update(
|
| 246 |
+
{
|
| 247 |
+
"video/J": sum(video_j) / len(video_j),
|
| 248 |
+
"video/F": sum(video_f) / len(video_f),
|
| 249 |
+
"video/J&F": sum(video_jf) / len(video_jf),
|
| 250 |
+
}
|
| 251 |
+
)
|
| 252 |
+
if image_iou:
|
| 253 |
+
giou = sum(image_iou) / len(image_iou)
|
| 254 |
+
averages.update(
|
| 255 |
+
{
|
| 256 |
+
"image/IoU": giou,
|
| 257 |
+
"image/gIoU": giou,
|
| 258 |
+
"image/cIoU": (
|
| 259 |
+
float(total_inter) / float(total_union) if total_union else 0.0
|
| 260 |
+
),
|
| 261 |
+
}
|
| 262 |
+
)
|
| 263 |
+
payload["avg_rewards"] = averages
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def _write_json_atomic(path: Path, payload: dict[str, Any]) -> None:
|
| 267 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 268 |
+
fd, temporary = tempfile.mkstemp(
|
| 269 |
+
dir=path.parent,
|
| 270 |
+
prefix=f".{path.name}.",
|
| 271 |
+
suffix=".tmp",
|
| 272 |
+
)
|
| 273 |
+
try:
|
| 274 |
+
with os.fdopen(fd, "w", encoding="utf-8") as stream:
|
| 275 |
+
json.dump(payload, stream, ensure_ascii=False, indent=2)
|
| 276 |
+
os.replace(temporary, path)
|
| 277 |
+
except BaseException:
|
| 278 |
+
try:
|
| 279 |
+
os.unlink(temporary)
|
| 280 |
+
except FileNotFoundError:
|
| 281 |
+
pass
|
| 282 |
+
raise
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
def _run_upstream(
|
| 286 |
+
module: Any,
|
| 287 |
+
*,
|
| 288 |
+
input_json: Path,
|
| 289 |
+
viz_ratio: float,
|
| 290 |
+
normalize: bool,
|
| 291 |
+
) -> Path:
|
| 292 |
+
original_load = module.json.load
|
| 293 |
+
|
| 294 |
+
if normalize:
|
| 295 |
+
def load_and_normalize(*args: Any, **kwargs: Any) -> Any:
|
| 296 |
+
payload = original_load(*args, **kwargs)
|
| 297 |
+
repaired = normalize_missing_rle_counts(payload, module.cocomask)
|
| 298 |
+
if repaired:
|
| 299 |
+
print(f"[INFO] normalized {repaired} mask RLE entries without counts")
|
| 300 |
+
return payload
|
| 301 |
+
|
| 302 |
+
module.json.load = load_and_normalize
|
| 303 |
+
|
| 304 |
+
try:
|
| 305 |
+
sys.argv = [
|
| 306 |
+
str(Path(module.__file__).name),
|
| 307 |
+
"--input_json",
|
| 308 |
+
str(input_json),
|
| 309 |
+
"--viz_ratio",
|
| 310 |
+
str(viz_ratio),
|
| 311 |
+
]
|
| 312 |
+
module.main()
|
| 313 |
+
finally:
|
| 314 |
+
module.json.load = original_load
|
| 315 |
+
return _sam2_output_path(input_json)
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
def _retry_errors(
|
| 319 |
+
module: Any,
|
| 320 |
+
*,
|
| 321 |
+
output_path: Path,
|
| 322 |
+
attempts: int,
|
| 323 |
+
viz_ratio: float,
|
| 324 |
+
) -> tuple[dict[str, Any], int]:
|
| 325 |
+
with output_path.open(encoding="utf-8") as stream:
|
| 326 |
+
payload = json.load(stream)
|
| 327 |
+
|
| 328 |
+
retried = 0
|
| 329 |
+
result_fields = (
|
| 330 |
+
"reward",
|
| 331 |
+
"status",
|
| 332 |
+
"viz_key_pred",
|
| 333 |
+
"viz_key_gt",
|
| 334 |
+
"viz_pred_video",
|
| 335 |
+
"viz_gt_video",
|
| 336 |
+
)
|
| 337 |
+
module.WORKERS_PER_GPU = 1
|
| 338 |
+
|
| 339 |
+
for attempt in range(1, attempts + 1):
|
| 340 |
+
failed = [
|
| 341 |
+
row
|
| 342 |
+
for row in _results(payload)
|
| 343 |
+
if str(row.get("status", "")).startswith("error:")
|
| 344 |
+
]
|
| 345 |
+
if not failed:
|
| 346 |
+
break
|
| 347 |
+
retried += len(failed)
|
| 348 |
+
retry_payload = {"results": failed}
|
| 349 |
+
repaired = normalize_missing_rle_counts(retry_payload, module.cocomask)
|
| 350 |
+
if repaired:
|
| 351 |
+
print(f"[INFO] normalized {repaired} mask RLE entries without counts")
|
| 352 |
+
print(
|
| 353 |
+
f"[INFO] retrying {len(failed)} SAM2 errors "
|
| 354 |
+
f"(attempt {attempt}/{attempts}, workers_per_gpu=1)"
|
| 355 |
+
)
|
| 356 |
+
|
| 357 |
+
with tempfile.TemporaryDirectory(
|
| 358 |
+
dir=output_path.parent,
|
| 359 |
+
prefix=".sam2-retry-",
|
| 360 |
+
) as temporary_dir:
|
| 361 |
+
retry_input = Path(temporary_dir) / "retry.json"
|
| 362 |
+
with retry_input.open("w", encoding="utf-8") as stream:
|
| 363 |
+
json.dump(retry_payload, stream, ensure_ascii=False)
|
| 364 |
+
retry_output = _run_upstream(
|
| 365 |
+
module,
|
| 366 |
+
input_json=retry_input,
|
| 367 |
+
viz_ratio=viz_ratio,
|
| 368 |
+
normalize=False,
|
| 369 |
+
)
|
| 370 |
+
with retry_output.open(encoding="utf-8") as stream:
|
| 371 |
+
retry_result = json.load(stream)
|
| 372 |
+
|
| 373 |
+
retried_by_id = {
|
| 374 |
+
row.get("problem_id"): row for row in _results(retry_result)
|
| 375 |
+
}
|
| 376 |
+
for row in _results(payload):
|
| 377 |
+
retry_row = retried_by_id.get(row.get("problem_id"))
|
| 378 |
+
if retry_row is not None:
|
| 379 |
+
for field in result_fields:
|
| 380 |
+
row[field] = retry_row.get(field)
|
| 381 |
+
|
| 382 |
+
_aggregate_rewards(payload)
|
| 383 |
+
_write_json_atomic(output_path, payload)
|
| 384 |
+
return payload, retried
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
def main() -> None:
|
| 388 |
+
parser = argparse.ArgumentParser()
|
| 389 |
+
parser.add_argument("--input_json", required=True, help="Prediction JSON from eval_seg_vllm.py.")
|
| 390 |
+
parser.add_argument("--data_root", required=True, help="Root joined with each record's relative `path`.")
|
| 391 |
+
parser.add_argument("--sam2_ckpt", required=True)
|
| 392 |
+
parser.add_argument("--sam2_cfg", required=True)
|
| 393 |
+
parser.add_argument("--onethinker_script", default=DEFAULT_ONETHINKER_SCRIPT)
|
| 394 |
+
parser.add_argument("--num_gpus", type=int, default=None)
|
| 395 |
+
parser.add_argument("--workers_per_gpu", type=int, default=None)
|
| 396 |
+
parser.add_argument("--pre_extract_threads", type=int, default=None)
|
| 397 |
+
parser.add_argument("--epoch_size", type=int, default=None)
|
| 398 |
+
parser.add_argument("--viz_ratio", type=float, default=0.0)
|
| 399 |
+
parser.add_argument(
|
| 400 |
+
"--retry_attempts",
|
| 401 |
+
type=int,
|
| 402 |
+
default=1,
|
| 403 |
+
help="Retry per-sample SAM2 errors with one worker per GPU.",
|
| 404 |
+
)
|
| 405 |
+
parser.add_argument(
|
| 406 |
+
"--retry_existing_errors",
|
| 407 |
+
action="store_true",
|
| 408 |
+
help="Retry errors in an existing sibling *_sam2.json without rerunning all samples.",
|
| 409 |
+
)
|
| 410 |
+
args = parser.parse_args()
|
| 411 |
+
|
| 412 |
+
# Python's spawn start method calls os.getcwd() for every new worker pool.
|
| 413 |
+
# If the command was launched from a transient directory, later epochs can
|
| 414 |
+
# fail with FileNotFoundError. Pin cwd to the project root.
|
| 415 |
+
project_dir = Path(__file__).resolve().parents[3]
|
| 416 |
+
os.chdir(project_dir)
|
| 417 |
+
original_get_preparation_data = mp_spawn.get_preparation_data
|
| 418 |
+
|
| 419 |
+
def get_preparation_data(name):
|
| 420 |
+
try:
|
| 421 |
+
return original_get_preparation_data(name)
|
| 422 |
+
except FileNotFoundError:
|
| 423 |
+
os.chdir(project_dir)
|
| 424 |
+
return original_get_preparation_data(name)
|
| 425 |
+
|
| 426 |
+
mp_spawn.get_preparation_data = get_preparation_data
|
| 427 |
+
|
| 428 |
+
config_name = sam2_config_name(args.sam2_cfg)
|
| 429 |
+
os.environ["SAM2_DATA_ROOT"] = args.data_root
|
| 430 |
+
os.environ["SAM2_CKPT"] = args.sam2_ckpt
|
| 431 |
+
os.environ["SAM2_CFG"] = config_name
|
| 432 |
+
module = load_module(args.onethinker_script)
|
| 433 |
+
module.DATA_ROOT = args.data_root
|
| 434 |
+
module.SAM2_CKPT = args.sam2_ckpt
|
| 435 |
+
module.SAM2_CFG = config_name
|
| 436 |
+
if args.num_gpus is not None:
|
| 437 |
+
module.NUM_GPUS = args.num_gpus
|
| 438 |
+
if args.workers_per_gpu is not None:
|
| 439 |
+
module.WORKERS_PER_GPU = args.workers_per_gpu
|
| 440 |
+
if args.pre_extract_threads is not None:
|
| 441 |
+
module.PRE_EXTRACT_THREADS = args.pre_extract_threads
|
| 442 |
+
if args.epoch_size is not None:
|
| 443 |
+
module.EPOCH_SIZE = args.epoch_size
|
| 444 |
+
|
| 445 |
+
sys.argv = [
|
| 446 |
+
str(Path(args.onethinker_script).name),
|
| 447 |
+
"--input_json",
|
| 448 |
+
args.input_json,
|
| 449 |
+
"--viz_ratio",
|
| 450 |
+
str(args.viz_ratio),
|
| 451 |
+
]
|
| 452 |
+
os.environ.setdefault(
|
| 453 |
+
"PYTORCH_CUDA_ALLOC_CONF",
|
| 454 |
+
"expandable_segments:True,max_split_size_mb:64,garbage_collection_threshold:0.8",
|
| 455 |
+
)
|
| 456 |
+
module.mp.set_start_method("spawn", force=True)
|
| 457 |
+
|
| 458 |
+
input_path = Path(args.input_json).resolve()
|
| 459 |
+
output_path = _sam2_output_path(input_path)
|
| 460 |
+
if not args.retry_existing_errors:
|
| 461 |
+
output_path = _run_upstream(
|
| 462 |
+
module,
|
| 463 |
+
input_json=input_path,
|
| 464 |
+
viz_ratio=args.viz_ratio,
|
| 465 |
+
normalize=True,
|
| 466 |
+
)
|
| 467 |
+
elif not output_path.is_file():
|
| 468 |
+
raise FileNotFoundError(f"Existing SAM2 result not found: {output_path}")
|
| 469 |
+
|
| 470 |
+
payload, retried = _retry_errors(
|
| 471 |
+
module,
|
| 472 |
+
output_path=output_path,
|
| 473 |
+
attempts=max(0, args.retry_attempts),
|
| 474 |
+
viz_ratio=args.viz_ratio,
|
| 475 |
+
)
|
| 476 |
+
failed = [row for row in _results(payload) if row.get("status") != "ok"]
|
| 477 |
+
print(
|
| 478 |
+
f"[INFO] final SAM2 status: ok={len(_results(payload)) - len(failed)} "
|
| 479 |
+
f"/ total={len(_results(payload))}; retried={retried}"
|
| 480 |
+
)
|
| 481 |
+
if failed:
|
| 482 |
+
examples = ", ".join(
|
| 483 |
+
f"{row.get('problem_id')}={row.get('status')}" for row in failed[:5]
|
| 484 |
+
)
|
| 485 |
+
raise RuntimeError(
|
| 486 |
+
f"SAM2 post-processing left {len(failed)} failed samples: {examples}"
|
| 487 |
+
)
|
| 488 |
+
|
| 489 |
+
|
| 490 |
+
if __name__ == "__main__":
|
| 491 |
+
main()
|
code/eval/task/segmentation/run_eval_vllm.sh
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# =============================================================================
|
| 3 |
+
# Segmentation evaluation — vLLM inference + optional SAM2 post-processing.
|
| 4 |
+
#
|
| 5 |
+
# Usage:
|
| 6 |
+
# bash eval/task/segmentation/run_eval_vllm.sh [MODEL_PATH] [OUTPUT_DIR]
|
| 7 |
+
#
|
| 8 |
+
# Common overrides:
|
| 9 |
+
# BENCH_DIR=/path/to/OneThinker-eval DATASETS=reasonseg-val DATA_ROOT=/path/to/OneThinker-eval \
|
| 10 |
+
# RUN_SAM2=true SAM2_CKPT=/path/to/sam2.1_hiera_large.pt SAM2_CFG=/path/to/sam2.1_hiera_l.yaml \
|
| 11 |
+
# bash eval/task/segmentation/run_eval_vllm.sh /path/to/ckpt
|
| 12 |
+
# =============================================================================
|
| 13 |
+
|
| 14 |
+
set -eo pipefail
|
| 15 |
+
|
| 16 |
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
| 17 |
+
PROJECT_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
| 18 |
+
|
| 19 |
+
# ---------- paths ----------
|
| 20 |
+
MODEL_PATH="${1:-}"
|
| 21 |
+
PROCESSOR_PATH="${PROCESSOR_PATH:-$MODEL_PATH}"
|
| 22 |
+
BENCH_DIR="${BENCH_DIR:-${PROJECT_DIR}/data/eval/segmentation}"
|
| 23 |
+
DATA_ROOT="${DATA_ROOT:-$BENCH_DIR}"
|
| 24 |
+
DATASETS="${DATASETS:-eval_seg_refcoco,eval_seg_refcocop,eval_seg_refcocog,eval_seg_mevis,eval_seg_reasonvos}"
|
| 25 |
+
|
| 26 |
+
if [[ -z "$MODEL_PATH" || ! -d "$MODEL_PATH" ]]; then
|
| 27 |
+
echo "ERROR: MODEL_PATH must name an existing model directory." >&2
|
| 28 |
+
exit 2
|
| 29 |
+
fi
|
| 30 |
+
if [[ ! -d "$BENCH_DIR" || ! -d "$DATA_ROOT" ]]; then
|
| 31 |
+
echo "ERROR: BENCH_DIR and DATA_ROOT must name existing directories." >&2
|
| 32 |
+
exit 2
|
| 33 |
+
fi
|
| 34 |
+
|
| 35 |
+
# ---------- eval settings ----------
|
| 36 |
+
DATA_TYPE="${DATA_TYPE:-all}" # all | image | video
|
| 37 |
+
PROMPT_MODE="${PROMPT_MODE:-train_seg}" # think | no_think | bare | onethink_system | train_seg
|
| 38 |
+
ENABLE_THINKING="${ENABLE_THINKING:-false}"
|
| 39 |
+
MAX_SAMPLES="${MAX_SAMPLES:-}"
|
| 40 |
+
BATCH_SIZE="${BATCH_SIZE:-16}"
|
| 41 |
+
MAX_NEW_TOKENS="${MAX_NEW_TOKENS:-1024}"
|
| 42 |
+
MAX_PIXELS_IMAGE="${MAX_PIXELS_IMAGE:-1048576}"
|
| 43 |
+
MIN_PIXELS_IMAGE="${MIN_PIXELS_IMAGE:-4096}"
|
| 44 |
+
# Accept both the tracking-style VIDEO_* names and older MAX_PIXELS_VIDEO names.
|
| 45 |
+
VIDEO_MAX_PIXELS="${VIDEO_MAX_PIXELS:-${MAX_PIXELS_VIDEO:-262144}}"
|
| 46 |
+
VIDEO_MIN_PIXELS="${VIDEO_MIN_PIXELS:-${MIN_PIXELS_VIDEO:-4096}}"
|
| 47 |
+
VIDEO_TOTAL_PIXELS="${VIDEO_TOTAL_PIXELS:-${TOTAL_PIXELS_VIDEO:-16777216}}"
|
| 48 |
+
MAX_FRAMES="${MAX_FRAMES:-128}"
|
| 49 |
+
FPS="${FPS:-2}"
|
| 50 |
+
PATCH_SIZE="${PATCH_SIZE:-}"
|
| 51 |
+
VIDEO_READER="${VIDEO_READER:-decord}"
|
| 52 |
+
|
| 53 |
+
case "$VIDEO_READER" in
|
| 54 |
+
decord|torchcodec|torchvision) ;;
|
| 55 |
+
*)
|
| 56 |
+
echo "ERROR: VIDEO_READER must be decord, torchcodec, or torchvision; got '$VIDEO_READER'" >&2
|
| 57 |
+
exit 1
|
| 58 |
+
;;
|
| 59 |
+
esac
|
| 60 |
+
export FORCE_QWENVL_VIDEO_READER="$VIDEO_READER"
|
| 61 |
+
|
| 62 |
+
require_int() {
|
| 63 |
+
local name="$1"
|
| 64 |
+
local value="$2"
|
| 65 |
+
if ! [[ "$value" =~ ^[0-9]+$ ]]; then
|
| 66 |
+
echo "ERROR: $name must be a non-empty integer, got '$value'"
|
| 67 |
+
exit 1
|
| 68 |
+
fi
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
require_int MAX_PIXELS_IMAGE "$MAX_PIXELS_IMAGE"
|
| 72 |
+
require_int MIN_PIXELS_IMAGE "$MIN_PIXELS_IMAGE"
|
| 73 |
+
require_int VIDEO_MAX_PIXELS "$VIDEO_MAX_PIXELS"
|
| 74 |
+
require_int VIDEO_MIN_PIXELS "$VIDEO_MIN_PIXELS"
|
| 75 |
+
require_int VIDEO_TOTAL_PIXELS "$VIDEO_TOTAL_PIXELS"
|
| 76 |
+
require_int MAX_FRAMES "$MAX_FRAMES"
|
| 77 |
+
require_int FPS "$FPS"
|
| 78 |
+
require_int BATCH_SIZE "$BATCH_SIZE"
|
| 79 |
+
require_int MAX_NEW_TOKENS "$MAX_NEW_TOKENS"
|
| 80 |
+
|
| 81 |
+
# ---------- vLLM ----------
|
| 82 |
+
TP_SIZE="${TP_SIZE:-1}"
|
| 83 |
+
GPU_MEM_UTIL="${GPU_MEM_UTIL:-0.85}"
|
| 84 |
+
MAX_MODEL_LEN="${MAX_MODEL_LEN:-32768}"
|
| 85 |
+
SEED="${SEED:-42}"
|
| 86 |
+
VLLM_BASE_PORT="${VLLM_BASE_PORT:-}"
|
| 87 |
+
RESUME_SHARDS="${RESUME_SHARDS:-false}"
|
| 88 |
+
RETRY_FAILED_SHARDS="${RETRY_FAILED_SHARDS:-true}"
|
| 89 |
+
|
| 90 |
+
# ---------- SAM2 ----------
|
| 91 |
+
RUN_SAM2="${RUN_SAM2:-false}"
|
| 92 |
+
SAM2_CKPT="${SAM2_CKPT:-}"
|
| 93 |
+
SAM2_CFG="${SAM2_CFG:-}"
|
| 94 |
+
ONETHINKER_SEG_POST="${ONETHINKER_SEG_POST:-${PROJECT_DIR}/third_party/OneThinker/Evaluation/Eval/seg_post_sam2.py}"
|
| 95 |
+
SAM2_NUM_GPUS="${SAM2_NUM_GPUS:-}"
|
| 96 |
+
SAM2_WORKERS_PER_GPU="${SAM2_WORKERS_PER_GPU:-}"
|
| 97 |
+
PRE_EXTRACT_THREADS="${PRE_EXTRACT_THREADS:-4}"
|
| 98 |
+
# Each SAM2 epoch spawns `world_size` worker processes that EACH reload the SAM2
|
| 99 |
+
# model and then handle only their slice of the epoch. Total model loads =
|
| 100 |
+
# world_size * num_epochs. A small epoch with many workers means tiny slices and
|
| 101 |
+
# constant model reloading (the real bottleneck). Keep the epoch large so there
|
| 102 |
+
# is effectively one epoch and each worker amortizes its model load over a big
|
| 103 |
+
# contiguous slice.
|
| 104 |
+
SAM2_EPOCH_SIZE="${SAM2_EPOCH_SIZE:-100000}"
|
| 105 |
+
VIZ_RATIO="${VIZ_RATIO:-0.0}"
|
| 106 |
+
|
| 107 |
+
# ---------- output ----------
|
| 108 |
+
MODEL_TAG=$(basename "${MODEL_PATH%/}")
|
| 109 |
+
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
| 110 |
+
OUTPUT_DIR="${2:-${PROJECT_DIR}/outputs/segmentation/eval_seg_vllm-${MODEL_TAG}-${TIMESTAMP}}"
|
| 111 |
+
|
| 112 |
+
# ---------- hardware ----------
|
| 113 |
+
NVJITLINK_LIB="$(
|
| 114 |
+
python - <<'PY'
|
| 115 |
+
import site
|
| 116 |
+
from pathlib import Path
|
| 117 |
+
|
| 118 |
+
roots = [*site.getsitepackages(), site.getusersitepackages()]
|
| 119 |
+
for root in roots:
|
| 120 |
+
candidate = Path(root) / "nvidia" / "nvjitlink" / "lib"
|
| 121 |
+
if candidate.is_dir():
|
| 122 |
+
print(candidate)
|
| 123 |
+
break
|
| 124 |
+
PY
|
| 125 |
+
)"
|
| 126 |
+
if [[ -n "${NVJITLINK_LIB}" ]]; then
|
| 127 |
+
export LD_LIBRARY_PATH="${NVJITLINK_LIB}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}"
|
| 128 |
+
fi
|
| 129 |
+
if ! python -c "import torch; print(f'PyTorch preflight: {torch.__version__} CUDA {torch.version.cuda}')"; then
|
| 130 |
+
echo "ERROR: PyTorch CUDA libraries cannot be loaded in the active environment." >&2
|
| 131 |
+
exit 1
|
| 132 |
+
fi
|
| 133 |
+
|
| 134 |
+
if [ -n "${CUDA_VISIBLE_DEVICES:-}" ]; then
|
| 135 |
+
IFS="," read -ra GPULIST <<< "$CUDA_VISIBLE_DEVICES"
|
| 136 |
+
else
|
| 137 |
+
IFS="," read -ra GPULIST <<< "$(seq -s, 0 $(($(nvidia-smi -L | wc -l)-1)))"
|
| 138 |
+
fi
|
| 139 |
+
NUM_GPUS=${#GPULIST[@]}
|
| 140 |
+
if (( NUM_GPUS % TP_SIZE != 0 )); then
|
| 141 |
+
echo "ERROR: NUM_GPUS ($NUM_GPUS) must be divisible by TP_SIZE ($TP_SIZE)"
|
| 142 |
+
exit 1
|
| 143 |
+
fi
|
| 144 |
+
DP_SIZE=$(( NUM_GPUS / TP_SIZE ))
|
| 145 |
+
if [[ -z "${VLLM_BASE_PORT}" ]]; then
|
| 146 |
+
VLLM_BASE_PORT="$(
|
| 147 |
+
python - "$DP_SIZE" <<'PY'
|
| 148 |
+
import socket
|
| 149 |
+
import sys
|
| 150 |
+
|
| 151 |
+
count = int(sys.argv[1])
|
| 152 |
+
spacing = 16
|
| 153 |
+
for base in range(45000, 64000 - spacing * count, 128):
|
| 154 |
+
sockets = []
|
| 155 |
+
try:
|
| 156 |
+
for index in range(count):
|
| 157 |
+
for offset in (0, 1):
|
| 158 |
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
| 159 |
+
sock.bind(("127.0.0.1", base + index * spacing + offset))
|
| 160 |
+
sockets.append(sock)
|
| 161 |
+
except OSError:
|
| 162 |
+
for sock in sockets:
|
| 163 |
+
sock.close()
|
| 164 |
+
continue
|
| 165 |
+
for sock in sockets:
|
| 166 |
+
sock.close()
|
| 167 |
+
print(base)
|
| 168 |
+
break
|
| 169 |
+
else:
|
| 170 |
+
raise SystemExit("no free segmentation vLLM port block found")
|
| 171 |
+
PY
|
| 172 |
+
)"
|
| 173 |
+
fi
|
| 174 |
+
|
| 175 |
+
echo "=============================================="
|
| 176 |
+
echo "Segmentation Evaluation (vLLM)"
|
| 177 |
+
echo "=============================================="
|
| 178 |
+
echo "Model: $MODEL_PATH"
|
| 179 |
+
echo "Processor: $PROCESSOR_PATH"
|
| 180 |
+
echo "Bench dir: $BENCH_DIR"
|
| 181 |
+
echo "Data root: $DATA_ROOT"
|
| 182 |
+
echo "Datasets: $DATASETS"
|
| 183 |
+
echo "Data type: $DATA_TYPE"
|
| 184 |
+
echo "Prompt: $PROMPT_MODE (enable_thinking=$ENABLE_THINKING)"
|
| 185 |
+
echo "Video pix: min=$VIDEO_MIN_PIXELS max=$VIDEO_MAX_PIXELS total=$VIDEO_TOTAL_PIXELS frames=$MAX_FRAMES fps=$FPS"
|
| 186 |
+
echo "Video reader:$VIDEO_READER"
|
| 187 |
+
echo "GPUs: ${GPULIST[*]} (${NUM_GPUS} total, TP=${TP_SIZE}, DP=${DP_SIZE})"
|
| 188 |
+
echo "Base port: $VLLM_BASE_PORT"
|
| 189 |
+
echo "Output: $OUTPUT_DIR"
|
| 190 |
+
echo "SAM2: $RUN_SAM2"
|
| 191 |
+
echo "=============================================="
|
| 192 |
+
|
| 193 |
+
mkdir -p "$OUTPUT_DIR"
|
| 194 |
+
|
| 195 |
+
PY_ARGS=(
|
| 196 |
+
--model_path "$MODEL_PATH"
|
| 197 |
+
--processor_path "$PROCESSOR_PATH"
|
| 198 |
+
--bench_dir "$BENCH_DIR"
|
| 199 |
+
--datasets "$DATASETS"
|
| 200 |
+
--output_dir "$OUTPUT_DIR"
|
| 201 |
+
--base_prefix "$DATA_ROOT"
|
| 202 |
+
--data_type "$DATA_TYPE"
|
| 203 |
+
--prompt_mode "$PROMPT_MODE"
|
| 204 |
+
--batch_size "$BATCH_SIZE"
|
| 205 |
+
--max_new_tokens "$MAX_NEW_TOKENS"
|
| 206 |
+
--tensor_parallel_size "$TP_SIZE"
|
| 207 |
+
--gpu_memory_utilization "$GPU_MEM_UTIL"
|
| 208 |
+
--max_model_len "$MAX_MODEL_LEN"
|
| 209 |
+
--seed "$SEED"
|
| 210 |
+
--max_pixels_image "$MAX_PIXELS_IMAGE"
|
| 211 |
+
--min_pixels_image "$MIN_PIXELS_IMAGE"
|
| 212 |
+
--max_pixels_video "$VIDEO_MAX_PIXELS"
|
| 213 |
+
--min_pixels_video "$VIDEO_MIN_PIXELS"
|
| 214 |
+
--total_pixels_video "$VIDEO_TOTAL_PIXELS"
|
| 215 |
+
--max_frames "$MAX_FRAMES"
|
| 216 |
+
--fps "$FPS"
|
| 217 |
+
--skip_missing_media
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
if [ -n "$MAX_SAMPLES" ]; then
|
| 221 |
+
PY_ARGS+=(--max_samples "$MAX_SAMPLES")
|
| 222 |
+
fi
|
| 223 |
+
if [ -n "$PATCH_SIZE" ]; then
|
| 224 |
+
PY_ARGS+=(--patch_size "$PATCH_SIZE")
|
| 225 |
+
fi
|
| 226 |
+
if [ "$ENABLE_THINKING" = "true" ]; then
|
| 227 |
+
PY_ARGS+=(--enable_thinking)
|
| 228 |
+
fi
|
| 229 |
+
|
| 230 |
+
PIDS=()
|
| 231 |
+
PID_SHARDS=()
|
| 232 |
+
cleanup() {
|
| 233 |
+
echo ""
|
| 234 |
+
echo "Caught interrupt, killing workers ..."
|
| 235 |
+
for pid in "${PIDS[@]}"; do
|
| 236 |
+
kill -TERM "$pid" 2>/dev/null || true
|
| 237 |
+
done
|
| 238 |
+
wait 2>/dev/null || true
|
| 239 |
+
exit 1
|
| 240 |
+
}
|
| 241 |
+
trap cleanup INT TERM
|
| 242 |
+
|
| 243 |
+
dataset_key() {
|
| 244 |
+
python -c \
|
| 245 |
+
"from pathlib import Path; import sys; p=sys.argv[1]; print(Path(p).stem if Path(p).suffix else p)" \
|
| 246 |
+
"$1"
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
shard_is_complete() {
|
| 250 |
+
local shard_id="$1"
|
| 251 |
+
local dataset
|
| 252 |
+
local key
|
| 253 |
+
local datasets=()
|
| 254 |
+
IFS=',' read -ra datasets <<< "$DATASETS"
|
| 255 |
+
for dataset in "${datasets[@]}"; do
|
| 256 |
+
key="$(dataset_key "$dataset")"
|
| 257 |
+
if [[ ! -s "$OUTPUT_DIR/results_${key}_shard${shard_id}.json" ]]; then
|
| 258 |
+
return 1
|
| 259 |
+
fi
|
| 260 |
+
done
|
| 261 |
+
return 0
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
if [ "$DP_SIZE" -eq 1 ]; then
|
| 265 |
+
CUDA_VISIBLE_DEVICES=$(IFS=,; echo "${GPULIST[*]}") \
|
| 266 |
+
VLLM_PORT="$VLLM_BASE_PORT" \
|
| 267 |
+
VLLM_HOST_IP=127.0.0.1 \
|
| 268 |
+
MASTER_PORT="$((VLLM_BASE_PORT + 1))" \
|
| 269 |
+
MASTER_ADDR=127.0.0.1 \
|
| 270 |
+
PYTHONUNBUFFERED=1 python "$SCRIPT_DIR/eval_seg_vllm.py" "${PY_ARGS[@]}" \
|
| 271 |
+
2>&1 | tee "$OUTPUT_DIR/run.log"
|
| 272 |
+
else
|
| 273 |
+
echo ""
|
| 274 |
+
echo ">>> Launching $DP_SIZE vLLM workers (TP=$TP_SIZE each) ..."
|
| 275 |
+
for IDX in $(seq 0 $((DP_SIZE - 1))); do
|
| 276 |
+
if [[ "$RESUME_SHARDS" == "true" ]] && shard_is_complete "$IDX"; then
|
| 277 |
+
echo " Reusing completed shard $IDX"
|
| 278 |
+
continue
|
| 279 |
+
fi
|
| 280 |
+
START=$(( IDX * TP_SIZE ))
|
| 281 |
+
SHARD_PORT=$((VLLM_BASE_PORT + IDX * 16))
|
| 282 |
+
SHARD_MASTER_PORT=$((SHARD_PORT + 1))
|
| 283 |
+
SHARD_GPUS=""
|
| 284 |
+
for j in $(seq 0 $((TP_SIZE - 1))); do
|
| 285 |
+
g=${GPULIST[$((START + j))]}
|
| 286 |
+
SHARD_GPUS="${SHARD_GPUS}${SHARD_GPUS:+,}${g}"
|
| 287 |
+
done
|
| 288 |
+
|
| 289 |
+
CUDA_VISIBLE_DEVICES="$SHARD_GPUS" \
|
| 290 |
+
VLLM_PORT="$SHARD_PORT" \
|
| 291 |
+
VLLM_HOST_IP=127.0.0.1 \
|
| 292 |
+
MASTER_PORT="$SHARD_MASTER_PORT" \
|
| 293 |
+
MASTER_ADDR=127.0.0.1 \
|
| 294 |
+
PYTHONUNBUFFERED=1 \
|
| 295 |
+
python "$SCRIPT_DIR/eval_seg_vllm.py" "${PY_ARGS[@]}" \
|
| 296 |
+
--chunk "$DP_SIZE" --index "$IDX" \
|
| 297 |
+
> "$OUTPUT_DIR/worker_shard${IDX}.log" 2>&1 &
|
| 298 |
+
PIDS+=($!)
|
| 299 |
+
PID_SHARDS+=("$IDX")
|
| 300 |
+
echo " Launched shard $IDX on GPU $SHARD_GPUS (PID ${PIDS[-1]})"
|
| 301 |
+
done
|
| 302 |
+
|
| 303 |
+
FAILED_SHARDS=()
|
| 304 |
+
for i in "${!PIDS[@]}"; do
|
| 305 |
+
if ! wait "${PIDS[$i]}"; then
|
| 306 |
+
FAILED_SHARDS+=("${PID_SHARDS[$i]}")
|
| 307 |
+
fi
|
| 308 |
+
done
|
| 309 |
+
|
| 310 |
+
if (( ${#FAILED_SHARDS[@]} > 0 )) && [[ "$RETRY_FAILED_SHARDS" == "true" ]]; then
|
| 311 |
+
echo ""
|
| 312 |
+
echo ">>> Retrying failed shards sequentially with fresh ports ..."
|
| 313 |
+
RETRY_FAILURES=()
|
| 314 |
+
for IDX in "${FAILED_SHARDS[@]}"; do
|
| 315 |
+
START=$(( IDX * TP_SIZE ))
|
| 316 |
+
SHARD_GPUS=""
|
| 317 |
+
for j in $(seq 0 $((TP_SIZE - 1))); do
|
| 318 |
+
g=${GPULIST[$((START + j))]}
|
| 319 |
+
SHARD_GPUS="${SHARD_GPUS}${SHARD_GPUS:+,}${g}"
|
| 320 |
+
done
|
| 321 |
+
RETRY_PORT="$(
|
| 322 |
+
python - <<'PY'
|
| 323 |
+
import socket
|
| 324 |
+
|
| 325 |
+
for base in range(52000, 64000, 8):
|
| 326 |
+
sockets = []
|
| 327 |
+
try:
|
| 328 |
+
for offset in (0, 1):
|
| 329 |
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
| 330 |
+
sock.bind(("127.0.0.1", base + offset))
|
| 331 |
+
sockets.append(sock)
|
| 332 |
+
except OSError:
|
| 333 |
+
for sock in sockets:
|
| 334 |
+
sock.close()
|
| 335 |
+
continue
|
| 336 |
+
for sock in sockets:
|
| 337 |
+
sock.close()
|
| 338 |
+
print(base)
|
| 339 |
+
break
|
| 340 |
+
else:
|
| 341 |
+
raise SystemExit("no free vLLM retry ports found")
|
| 342 |
+
PY
|
| 343 |
+
)"
|
| 344 |
+
echo " Retrying shard $IDX on GPU $SHARD_GPUS (ports $RETRY_PORT/$((RETRY_PORT + 1)))"
|
| 345 |
+
if ! CUDA_VISIBLE_DEVICES="$SHARD_GPUS" \
|
| 346 |
+
VLLM_PORT="$RETRY_PORT" \
|
| 347 |
+
VLLM_HOST_IP=127.0.0.1 \
|
| 348 |
+
MASTER_PORT="$((RETRY_PORT + 1))" \
|
| 349 |
+
MASTER_ADDR=127.0.0.1 \
|
| 350 |
+
PYTHONUNBUFFERED=1 \
|
| 351 |
+
python "$SCRIPT_DIR/eval_seg_vllm.py" "${PY_ARGS[@]}" \
|
| 352 |
+
--chunk "$DP_SIZE" --index "$IDX" \
|
| 353 |
+
> "$OUTPUT_DIR/worker_shard${IDX}_retry.log" 2>&1; then
|
| 354 |
+
RETRY_FAILURES+=("$IDX")
|
| 355 |
+
fi
|
| 356 |
+
done
|
| 357 |
+
FAILED_SHARDS=("${RETRY_FAILURES[@]}")
|
| 358 |
+
fi
|
| 359 |
+
|
| 360 |
+
if (( ${#FAILED_SHARDS[@]} > 0 )); then
|
| 361 |
+
echo "ERROR: Failed shards: ${FAILED_SHARDS[*]}; not merging or running SAM2."
|
| 362 |
+
exit 1
|
| 363 |
+
fi
|
| 364 |
+
|
| 365 |
+
echo ""
|
| 366 |
+
echo ">>> Merging shard results ..."
|
| 367 |
+
IFS=',' read -ra DATASET_LIST <<< "$DATASETS"
|
| 368 |
+
for DATASET in "${DATASET_LIST[@]}"; do
|
| 369 |
+
DATASET_KEY=$(python -c "from pathlib import Path; import sys; p=sys.argv[1]; print(Path(p).stem if Path(p).suffix else p)" "$DATASET")
|
| 370 |
+
python - "$OUTPUT_DIR" "$DATASET_KEY" "$DP_SIZE" <<'PY'
|
| 371 |
+
import json
|
| 372 |
+
import os
|
| 373 |
+
import sys
|
| 374 |
+
|
| 375 |
+
out_dir, dataset, num_shards = sys.argv[1], sys.argv[2], int(sys.argv[3])
|
| 376 |
+
all_samples = []
|
| 377 |
+
for sid in range(num_shards):
|
| 378 |
+
path = os.path.join(out_dir, f"results_{dataset}_shard{sid}.json")
|
| 379 |
+
if os.path.isfile(path):
|
| 380 |
+
with open(path, encoding="utf-8") as f:
|
| 381 |
+
payload = json.load(f)
|
| 382 |
+
all_samples.extend(payload.get("results", []))
|
| 383 |
+
|
| 384 |
+
n = len(all_samples)
|
| 385 |
+
if n == 0:
|
| 386 |
+
print(f" No results for {dataset}")
|
| 387 |
+
sys.exit(0)
|
| 388 |
+
|
| 389 |
+
parse_ok = sum(1 for row in all_samples if row.get("parse_ok"))
|
| 390 |
+
summary = {
|
| 391 |
+
"num_samples": n,
|
| 392 |
+
"parse_ok": parse_ok,
|
| 393 |
+
"parse_rate": round(parse_ok / n * 100.0, 2),
|
| 394 |
+
"by_data_type": {},
|
| 395 |
+
}
|
| 396 |
+
for data_type in ("image", "video"):
|
| 397 |
+
part = [r for r in all_samples if r.get("data_type") == data_type]
|
| 398 |
+
if part:
|
| 399 |
+
ok = sum(1 for r in part if r.get("parse_ok"))
|
| 400 |
+
summary["by_data_type"][data_type] = {
|
| 401 |
+
"num_samples": len(part),
|
| 402 |
+
"parse_rate": round(ok / len(part) * 100.0, 2),
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
with open(os.path.join(out_dir, f"results_{dataset}.json"), "w", encoding="utf-8") as f:
|
| 406 |
+
json.dump({"results": all_samples, "metrics": summary}, f, ensure_ascii=False, indent=2)
|
| 407 |
+
|
| 408 |
+
summary_path = os.path.join(out_dir, "summary.json")
|
| 409 |
+
full_summary = json.load(open(summary_path, encoding="utf-8")) if os.path.isfile(summary_path) else {}
|
| 410 |
+
full_summary[dataset] = summary
|
| 411 |
+
with open(summary_path, "w", encoding="utf-8") as f:
|
| 412 |
+
json.dump(full_summary, f, ensure_ascii=False, indent=2)
|
| 413 |
+
|
| 414 |
+
print(f" {dataset}: n={n} parse={summary['parse_rate']:.2f}%")
|
| 415 |
+
PY
|
| 416 |
+
done
|
| 417 |
+
fi
|
| 418 |
+
|
| 419 |
+
if [ "$RUN_SAM2" = "true" ]; then
|
| 420 |
+
if [ -z "$SAM2_CKPT" ] || [ -z "$SAM2_CFG" ]; then
|
| 421 |
+
echo "ERROR: RUN_SAM2=true requires SAM2_CKPT and SAM2_CFG."
|
| 422 |
+
exit 1
|
| 423 |
+
fi
|
| 424 |
+
echo ""
|
| 425 |
+
echo ">>> Running SAM2 post-processing ..."
|
| 426 |
+
# SAM2 needs visible GPUs. The DP>1 inference path sets CUDA_VISIBLE_DEVICES
|
| 427 |
+
# only inside per-worker subshells, so the parent env may be empty here; an
|
| 428 |
+
# empty CUDA_VISIBLE_DEVICES makes torch.cuda.is_available() False and SAM2
|
| 429 |
+
# falls back to slow CPU serial. Re-derive a non-empty device list.
|
| 430 |
+
export CUDA_VISIBLE_DEVICES="$(IFS=,; echo "${GPULIST[*]}")"
|
| 431 |
+
# Default SAM2 GPU count to all visible GPUs unless caller overrides.
|
| 432 |
+
if [ -z "$SAM2_NUM_GPUS" ]; then
|
| 433 |
+
SAM2_NUM_GPUS="$NUM_GPUS"
|
| 434 |
+
fi
|
| 435 |
+
echo " SAM2 GPUs: $CUDA_VISIBLE_DEVICES (num_gpus=$SAM2_NUM_GPUS)"
|
| 436 |
+
IFS=',' read -ra DATASET_LIST <<< "$DATASETS"
|
| 437 |
+
for DATASET in "${DATASET_LIST[@]}"; do
|
| 438 |
+
DATASET_KEY=$(python -c "from pathlib import Path; import sys; p=sys.argv[1]; print(Path(p).stem if Path(p).suffix else p)" "$DATASET")
|
| 439 |
+
RESULT_JSON="$OUTPUT_DIR/results_${DATASET_KEY}.json"
|
| 440 |
+
if [ ! -f "$RESULT_JSON" ]; then
|
| 441 |
+
echo " Skip $DATASET_KEY: missing $RESULT_JSON"
|
| 442 |
+
continue
|
| 443 |
+
fi
|
| 444 |
+
SAM2_ARGS=(
|
| 445 |
+
--input_json "$RESULT_JSON"
|
| 446 |
+
--data_root "$DATA_ROOT"
|
| 447 |
+
--sam2_ckpt "$SAM2_CKPT"
|
| 448 |
+
--sam2_cfg "$SAM2_CFG"
|
| 449 |
+
--onethinker_script "$ONETHINKER_SEG_POST"
|
| 450 |
+
--pre_extract_threads "$PRE_EXTRACT_THREADS"
|
| 451 |
+
--epoch_size "$SAM2_EPOCH_SIZE"
|
| 452 |
+
--viz_ratio "$VIZ_RATIO"
|
| 453 |
+
)
|
| 454 |
+
if [ -n "$SAM2_NUM_GPUS" ]; then
|
| 455 |
+
SAM2_ARGS+=(--num_gpus "$SAM2_NUM_GPUS")
|
| 456 |
+
fi
|
| 457 |
+
if [ -n "$SAM2_WORKERS_PER_GPU" ]; then
|
| 458 |
+
SAM2_ARGS+=(--workers_per_gpu "$SAM2_WORKERS_PER_GPU")
|
| 459 |
+
fi
|
| 460 |
+
PYTHONUNBUFFERED=1 python "$SCRIPT_DIR/post_sam2.py" "${SAM2_ARGS[@]}" \
|
| 461 |
+
2>&1 | tee "$OUTPUT_DIR/sam2_${DATASET_KEY}.log"
|
| 462 |
+
|
| 463 |
+
# Merge SAM2 metrics (cIoU / gIoU / J&F) back into summary.json.
|
| 464 |
+
SAM2_JSON="$OUTPUT_DIR/results_${DATASET_KEY}_sam2.json"
|
| 465 |
+
if [ -f "$SAM2_JSON" ]; then
|
| 466 |
+
python - "$OUTPUT_DIR" "$DATASET_KEY" "$SAM2_JSON" <<'PY'
|
| 467 |
+
import json
|
| 468 |
+
import os
|
| 469 |
+
import sys
|
| 470 |
+
|
| 471 |
+
out_dir, dataset, sam2_json = sys.argv[1], sys.argv[2], sys.argv[3]
|
| 472 |
+
with open(sam2_json, encoding="utf-8") as f:
|
| 473 |
+
payload = json.load(f)
|
| 474 |
+
|
| 475 |
+
metrics = payload.get("metrics", {})
|
| 476 |
+
avg_rewards = payload.get("avg_rewards", {})
|
| 477 |
+
|
| 478 |
+
summary_path = os.path.join(out_dir, "summary.json")
|
| 479 |
+
full_summary = json.load(open(summary_path, encoding="utf-8")) if os.path.isfile(summary_path) else {}
|
| 480 |
+
entry = full_summary.get(dataset, {})
|
| 481 |
+
if isinstance(metrics, dict):
|
| 482 |
+
for k in ("num_samples", "parse_ok", "parse_rate"):
|
| 483 |
+
if k in metrics:
|
| 484 |
+
entry[k] = metrics[k]
|
| 485 |
+
entry["avg_rewards"] = avg_rewards
|
| 486 |
+
full_summary[dataset] = entry
|
| 487 |
+
with open(summary_path, "w", encoding="utf-8") as f:
|
| 488 |
+
json.dump(full_summary, f, ensure_ascii=False, indent=2)
|
| 489 |
+
|
| 490 |
+
parts = ", ".join(f"{k}={v:.4f}" for k, v in avg_rewards.items())
|
| 491 |
+
print(f" {dataset}: {parts}")
|
| 492 |
+
PY
|
| 493 |
+
fi
|
| 494 |
+
done
|
| 495 |
+
fi
|
| 496 |
+
|
| 497 |
+
echo ""
|
| 498 |
+
echo "=============================================="
|
| 499 |
+
echo "Done. Summary: $OUTPUT_DIR/summary.json"
|
| 500 |
+
echo "=============================================="
|
code/eval/task/spatial_grounding/_grounding_utils.py
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared helpers for RefCOCO / RefCOCO+ / RefCOCOg evaluation.
|
| 2 |
+
|
| 3 |
+
Pure-Python (no torch / vLLM / HF dependencies) so it can be imported by both
|
| 4 |
+
the inference script (``eval_refcoco_vllm.py``) and the offline rescorer
|
| 5 |
+
(``rescore_results.py``).
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import json
|
| 11 |
+
import os
|
| 12 |
+
import re
|
| 13 |
+
import sys
|
| 14 |
+
from typing import Any, Dict, List, Optional, Tuple
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
# ---------------------------------------------------------------------------
|
| 18 |
+
# Prompts — single source of truth in eval/task/eval_prompt.py
|
| 19 |
+
# (qwen_native matches data/joint/sft_joint_all.jsonl spatial grounding samples).
|
| 20 |
+
# ---------------------------------------------------------------------------
|
| 21 |
+
_EVAL_TASK_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 22 |
+
if _EVAL_TASK_DIR not in sys.path:
|
| 23 |
+
sys.path.insert(0, _EVAL_TASK_DIR)
|
| 24 |
+
|
| 25 |
+
from canonical_data import load_json_records # noqa: E402
|
| 26 |
+
from eval_prompt import QWEN_NATIVE_PROMPT_SG # noqa: E402
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _with_terminal_period(expression: str) -> str:
|
| 30 |
+
expression = (expression or "").strip()
|
| 31 |
+
if expression and expression[-1] not in ".?!":
|
| 32 |
+
expression += "."
|
| 33 |
+
return expression
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def build_qwen_native_prompt(expression: str) -> str:
|
| 37 |
+
# The 13k iou<0.95 training set uses the qwen-native JSON bbox format,
|
| 38 |
+
# wrapped in <answer> tags for consistency with the joint SFT mixture:
|
| 39 |
+
# <image>Locate "expr." in the image. Output its bounding box in JSON
|
| 40 |
+
# format within <answer>...</answer> tags. Example: ...
|
| 41 |
+
# Keep eval byte-for-byte aligned on the user text after the <image> token.
|
| 42 |
+
return QWEN_NATIVE_PROMPT_SG.format(_with_terminal_period(expression))
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# Alternate grounding prompt styles below are NOT used by the eval pipeline
|
| 46 |
+
# (eval.sh always runs qwen_native). They are retained only for the offline
|
| 47 |
+
# analysis / data-selection tools in this folder (compute_difficulty.py,
|
| 48 |
+
# select_train_data.py), so they live here rather than in eval_prompt.py.
|
| 49 |
+
_QWEN_OFFICIAL_PROMPT_SG = (
|
| 50 |
+
'Locate every object that matches the description "{}" in the image. '
|
| 51 |
+
"Report bbox coordinates in JSON format."
|
| 52 |
+
)
|
| 53 |
+
_EVAL_BENCH_SG_PROMPT = (
|
| 54 |
+
'Locate "{}" in the image. Output its bounding box in JSON format '
|
| 55 |
+
'within <answer>...</answer> tags. '
|
| 56 |
+
'Example: <answer>[{{"bbox_2d": [123, 30, 404, 846]}}]</answer>'
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def build_qwen_official_prompt(expression: str) -> str:
|
| 61 |
+
return _QWEN_OFFICIAL_PROMPT_SG.format(expression or "")
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def build_eval_bench_prompt(expression: str) -> str:
|
| 65 |
+
return _EVAL_BENCH_SG_PROMPT.format(expression or "")
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
# ---------------------------------------------------------------------------
|
| 69 |
+
# Data loading
|
| 70 |
+
# ---------------------------------------------------------------------------
|
| 71 |
+
|
| 72 |
+
DATASET_CONFIGS = {
|
| 73 |
+
"refcoco-val": {"anno": "refcoco_val.json"},
|
| 74 |
+
"refcoco-testA": {"anno": "refcoco_testA.json"},
|
| 75 |
+
"refcoco-testB": {"anno": "refcoco_testB.json"},
|
| 76 |
+
"refcoco-train": {"anno": "refcoco_train.json"},
|
| 77 |
+
"refcoco+-val": {"anno": "refcocop_val.json"},
|
| 78 |
+
"refcoco+-testA": {"anno": "refcocop_testA.json"},
|
| 79 |
+
"refcoco+-testB": {"anno": "refcocop_testB.json"},
|
| 80 |
+
"refcoco+-train": {"anno": "refcocop_train.json"},
|
| 81 |
+
"refcocop-val": {"anno": "refcocop_val.json"},
|
| 82 |
+
"refcocop-testA": {"anno": "refcocop_testA.json"},
|
| 83 |
+
"refcocop-testB": {"anno": "refcocop_testB.json"},
|
| 84 |
+
"refcocop-train": {"anno": "refcocop_train.json"},
|
| 85 |
+
"refcocog-val": {"anno": "refcocog_val.json"},
|
| 86 |
+
"refcocog-test": {"anno": "refcocog_test.json"},
|
| 87 |
+
"refcocog-train": {"anno": "refcocog_train.json"},
|
| 88 |
+
}
|
| 89 |
+
CANONICAL_DATASET_NAMES = {
|
| 90 |
+
"refcoco-val": "refcoco_val",
|
| 91 |
+
"refcoco-testA": "refcoco_test_a",
|
| 92 |
+
"refcoco-testB": "refcoco_test_b",
|
| 93 |
+
"refcoco+-val": "refcocop_val",
|
| 94 |
+
"refcoco+-testA": "refcocop_test_a",
|
| 95 |
+
"refcoco+-testB": "refcocop_test_b",
|
| 96 |
+
"refcocog-val": "refcocog_val",
|
| 97 |
+
"refcocog-test": "refcocog_test",
|
| 98 |
+
}
|
| 99 |
+
for _canonical_name in CANONICAL_DATASET_NAMES.values():
|
| 100 |
+
DATASET_CONFIGS.setdefault(
|
| 101 |
+
_canonical_name,
|
| 102 |
+
{"anno": f"{_canonical_name}.jsonl"},
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def _first_existing_path(paths: List[str]) -> Optional[str]:
|
| 107 |
+
for path in paths:
|
| 108 |
+
if path and os.path.isfile(path):
|
| 109 |
+
return path
|
| 110 |
+
return None
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def _resolve_annotation_path(bench_dir: str, anno_name: str, dataset_name: str) -> str:
|
| 114 |
+
here = os.path.dirname(os.path.abspath(__file__))
|
| 115 |
+
canonical_name = CANONICAL_DATASET_NAMES.get(dataset_name, dataset_name)
|
| 116 |
+
candidates = [
|
| 117 |
+
os.path.join(bench_dir, f"{canonical_name}.jsonl"),
|
| 118 |
+
os.path.join(bench_dir, "annotations", f"{canonical_name}.jsonl"),
|
| 119 |
+
os.path.join(bench_dir, anno_name),
|
| 120 |
+
os.path.join(bench_dir, "rec_jsons_processed", anno_name),
|
| 121 |
+
os.path.join(bench_dir, "annotations", anno_name),
|
| 122 |
+
os.path.join(bench_dir, "annotations", "rec_jsons_processed", anno_name),
|
| 123 |
+
os.path.join(bench_dir, f"{dataset_name}.json"),
|
| 124 |
+
os.path.join(bench_dir, "rec_jsons_processed", f"{dataset_name}.json"),
|
| 125 |
+
os.path.join(here, "rec_jsons_processed", anno_name),
|
| 126 |
+
]
|
| 127 |
+
anno_path = _first_existing_path(candidates)
|
| 128 |
+
if anno_path is None:
|
| 129 |
+
raise FileNotFoundError(
|
| 130 |
+
f"Could not find annotations for {dataset_name!r}. Tried: "
|
| 131 |
+
+ ", ".join(candidates)
|
| 132 |
+
)
|
| 133 |
+
return anno_path
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def _resolve_image_root(bench_dir: str, anno_path: str, sample_image_ref: str) -> str:
|
| 137 |
+
if not sample_image_ref or os.path.isabs(sample_image_ref):
|
| 138 |
+
return ""
|
| 139 |
+
|
| 140 |
+
override = os.getenv("SPATIAL_GROUNDING_IMAGE_ROOT", "").strip()
|
| 141 |
+
if override:
|
| 142 |
+
return override
|
| 143 |
+
|
| 144 |
+
anno_dir = os.path.dirname(os.path.abspath(anno_path))
|
| 145 |
+
bench_parent = os.path.dirname(os.path.abspath(bench_dir))
|
| 146 |
+
rel_dir = os.path.dirname(sample_image_ref)
|
| 147 |
+
candidates = [
|
| 148 |
+
bench_dir,
|
| 149 |
+
os.path.join(bench_dir, "images"),
|
| 150 |
+
os.path.join(bench_dir, "Spatial-Grounding"),
|
| 151 |
+
os.path.join(bench_parent, "Spatial-Grounding"),
|
| 152 |
+
anno_dir,
|
| 153 |
+
os.path.dirname(anno_dir),
|
| 154 |
+
]
|
| 155 |
+
for root in candidates:
|
| 156 |
+
probe = os.path.join(root, rel_dir) if rel_dir else root
|
| 157 |
+
if os.path.isdir(probe):
|
| 158 |
+
return root
|
| 159 |
+
return bench_dir
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def _resolve_image_path(image_ref: str, image_root: str) -> str:
|
| 163 |
+
if not image_ref:
|
| 164 |
+
return ""
|
| 165 |
+
if os.path.isabs(image_ref):
|
| 166 |
+
return image_ref
|
| 167 |
+
direct = os.path.join(image_root, image_ref)
|
| 168 |
+
if os.path.exists(direct):
|
| 169 |
+
return direct
|
| 170 |
+
basename_path = os.path.join(image_root, os.path.basename(image_ref))
|
| 171 |
+
if os.path.exists(basename_path):
|
| 172 |
+
return basename_path
|
| 173 |
+
return direct
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def _as_box(value: Any) -> Optional[List[float]]:
|
| 177 |
+
if not isinstance(value, (list, tuple)) or len(value) != 4:
|
| 178 |
+
return None
|
| 179 |
+
try:
|
| 180 |
+
return [float(x) for x in value]
|
| 181 |
+
except (TypeError, ValueError):
|
| 182 |
+
return None
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def load_annotations(bench_dir: str, dataset_name: str) -> List[Dict[str, Any]]:
|
| 186 |
+
cfg = DATASET_CONFIGS.get(
|
| 187 |
+
dataset_name,
|
| 188 |
+
{"anno": f"{dataset_name}.jsonl"},
|
| 189 |
+
)
|
| 190 |
+
anno_path = _resolve_annotation_path(bench_dir, cfg["anno"], dataset_name)
|
| 191 |
+
if anno_path.endswith(".jsonl"):
|
| 192 |
+
raw = load_json_records(anno_path)
|
| 193 |
+
else:
|
| 194 |
+
with open(anno_path, "r") as f:
|
| 195 |
+
raw = json.load(f)
|
| 196 |
+
|
| 197 |
+
rows = raw if isinstance(raw, list) else list(raw.values())
|
| 198 |
+
first_image_ref = ""
|
| 199 |
+
for item in rows:
|
| 200 |
+
first_image_ref = item.get("image_path") or item.get("image") or item.get("path") or ""
|
| 201 |
+
if first_image_ref:
|
| 202 |
+
break
|
| 203 |
+
image_root = _resolve_image_root(bench_dir, anno_path, first_image_ref)
|
| 204 |
+
|
| 205 |
+
annos: List[Dict[str, Any]] = []
|
| 206 |
+
for idx, item in enumerate(rows):
|
| 207 |
+
expression = (
|
| 208 |
+
item.get("expression")
|
| 209 |
+
or item.get("normal_caption")
|
| 210 |
+
or item.get("caption")
|
| 211 |
+
or item.get("query")
|
| 212 |
+
or item.get("problem")
|
| 213 |
+
or ""
|
| 214 |
+
)
|
| 215 |
+
bbox = (
|
| 216 |
+
_as_box(item.get("bbox"))
|
| 217 |
+
or _as_box(item.get("normalized_solution"))
|
| 218 |
+
or _as_box(item.get("gt_bbox"))
|
| 219 |
+
or _as_box(item.get("solution"))
|
| 220 |
+
)
|
| 221 |
+
image_ref = item.get("image_path") or item.get("image") or item.get("path") or ""
|
| 222 |
+
if not expression or bbox is None or not image_ref:
|
| 223 |
+
continue
|
| 224 |
+
|
| 225 |
+
annos.append({
|
| 226 |
+
"problem_id": item.get("problem_id", item.get("id", idx)),
|
| 227 |
+
"image_path": _resolve_image_path(image_ref, image_root),
|
| 228 |
+
"expression": re.sub(r"\s+", " ", str(expression)).strip().strip("."),
|
| 229 |
+
"bbox": bbox,
|
| 230 |
+
"width": item.get("width"),
|
| 231 |
+
"height": item.get("height"),
|
| 232 |
+
})
|
| 233 |
+
return annos
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
# ---------------------------------------------------------------------------
|
| 237 |
+
# Bounding-box extraction / scoring
|
| 238 |
+
# ---------------------------------------------------------------------------
|
| 239 |
+
|
| 240 |
+
ANSWER_RE = re.compile(r"<answer>\s*(.*?)\s*</answer>", re.DOTALL | re.IGNORECASE)
|
| 241 |
+
NUMBER_RE = r"-?\d+(?:\.\d+)?"
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def _clean_jsonish(text: str) -> str:
|
| 245 |
+
text = (text or "").strip()
|
| 246 |
+
if text.startswith("```"):
|
| 247 |
+
text = re.sub(r"^```(?:json)?\s*", "", text, flags=re.IGNORECASE)
|
| 248 |
+
text = re.sub(r"\s*```$", "", text)
|
| 249 |
+
return text.strip()
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
def _iter_json_values(text: str) -> List[Any]:
|
| 253 |
+
decoder = json.JSONDecoder()
|
| 254 |
+
values: List[Any] = []
|
| 255 |
+
for i, ch in enumerate(text):
|
| 256 |
+
if ch not in "[{":
|
| 257 |
+
continue
|
| 258 |
+
try:
|
| 259 |
+
value, _ = decoder.raw_decode(text[i:])
|
| 260 |
+
except json.JSONDecodeError:
|
| 261 |
+
continue
|
| 262 |
+
values.append(value)
|
| 263 |
+
return values
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def _append_candidate(
|
| 267 |
+
out: List[Dict[str, Any]],
|
| 268 |
+
box: Any,
|
| 269 |
+
label: Optional[str] = None,
|
| 270 |
+
) -> None:
|
| 271 |
+
parsed = _as_box(box)
|
| 272 |
+
if parsed is not None:
|
| 273 |
+
out.append({"bbox": parsed, "label": label})
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
def _extract_from_json_value(value: Any, out: List[Dict[str, Any]],
|
| 277 |
+
label: Optional[str] = None) -> None:
|
| 278 |
+
if isinstance(value, dict):
|
| 279 |
+
local_label = value.get("label") or value.get("name") or value.get("category") or label
|
| 280 |
+
for key in ("bbox", "bbox_2d", "box", "boxes", "bboxes"):
|
| 281 |
+
if key not in value:
|
| 282 |
+
continue
|
| 283 |
+
box_value = value[key]
|
| 284 |
+
if _as_box(box_value) is not None:
|
| 285 |
+
_append_candidate(out, box_value, local_label)
|
| 286 |
+
elif isinstance(box_value, list):
|
| 287 |
+
for entry in box_value:
|
| 288 |
+
if _as_box(entry) is not None:
|
| 289 |
+
_append_candidate(out, entry, local_label)
|
| 290 |
+
else:
|
| 291 |
+
_extract_from_json_value(entry, out, local_label)
|
| 292 |
+
for key in ("objects", "items", "regions", "detections", "predictions"):
|
| 293 |
+
if key in value:
|
| 294 |
+
_extract_from_json_value(value[key], out, local_label)
|
| 295 |
+
elif isinstance(value, list):
|
| 296 |
+
if _as_box(value) is not None:
|
| 297 |
+
_append_candidate(out, value, label)
|
| 298 |
+
else:
|
| 299 |
+
for entry in value:
|
| 300 |
+
_extract_from_json_value(entry, out, label)
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
def extract_bboxes(text: str) -> List[Dict[str, Any]]:
|
| 304 |
+
"""Extract candidate bboxes from Qwen JSON and legacy R1 answer formats."""
|
| 305 |
+
if not text:
|
| 306 |
+
return []
|
| 307 |
+
|
| 308 |
+
candidates: List[Dict[str, Any]] = []
|
| 309 |
+
chunks = [m.group(1) for m in ANSWER_RE.finditer(text)] or [text]
|
| 310 |
+
for chunk in chunks:
|
| 311 |
+
cleaned = _clean_jsonish(chunk)
|
| 312 |
+
for value in _iter_json_values(cleaned):
|
| 313 |
+
_extract_from_json_value(value, candidates)
|
| 314 |
+
|
| 315 |
+
pair_pat = re.compile(
|
| 316 |
+
rf"\(({NUMBER_RE})\s*,\s*({NUMBER_RE})\)\s*,?\s*"
|
| 317 |
+
rf"\(({NUMBER_RE})\s*,\s*({NUMBER_RE})\)"
|
| 318 |
+
)
|
| 319 |
+
for match in pair_pat.finditer(cleaned):
|
| 320 |
+
_append_candidate(candidates, [float(x) for x in match.groups()])
|
| 321 |
+
|
| 322 |
+
bracket_pat = re.compile(
|
| 323 |
+
rf"\[\s*({NUMBER_RE})\s*,\s*({NUMBER_RE})\s*,\s*"
|
| 324 |
+
rf"({NUMBER_RE})\s*,\s*({NUMBER_RE})\s*\]"
|
| 325 |
+
)
|
| 326 |
+
for match in bracket_pat.finditer(cleaned):
|
| 327 |
+
_append_candidate(candidates, [float(x) for x in match.groups()])
|
| 328 |
+
|
| 329 |
+
if candidates:
|
| 330 |
+
return _dedupe_candidates(candidates)
|
| 331 |
+
|
| 332 |
+
nums = re.findall(NUMBER_RE, ANSWER_RE.sub(r"\1", text))
|
| 333 |
+
if len(nums) >= 4:
|
| 334 |
+
_append_candidate(candidates, [float(x) for x in nums[:4]])
|
| 335 |
+
return _dedupe_candidates(candidates)
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
def _dedupe_candidates(candidates: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
| 339 |
+
seen = set()
|
| 340 |
+
unique: List[Dict[str, Any]] = []
|
| 341 |
+
for cand in candidates:
|
| 342 |
+
bbox = cand.get("bbox")
|
| 343 |
+
if bbox is None:
|
| 344 |
+
continue
|
| 345 |
+
key = tuple(round(float(x), 6) for x in bbox)
|
| 346 |
+
if key in seen:
|
| 347 |
+
continue
|
| 348 |
+
seen.add(key)
|
| 349 |
+
unique.append(cand)
|
| 350 |
+
return unique
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
def compute_iou_2d(box1: Any, box2: Any) -> float:
|
| 354 |
+
b1 = _as_box(box1)
|
| 355 |
+
b2 = _as_box(box2)
|
| 356 |
+
if b1 is None or b2 is None:
|
| 357 |
+
return 0.0
|
| 358 |
+
|
| 359 |
+
x1 = max(b1[0], b2[0])
|
| 360 |
+
y1 = max(b1[1], b2[1])
|
| 361 |
+
x2 = min(b1[2], b2[2])
|
| 362 |
+
y2 = min(b1[3], b2[3])
|
| 363 |
+
inter = max(0.0, x2 - x1) * max(0.0, y2 - y1)
|
| 364 |
+
area1 = max(0.0, b1[2] - b1[0]) * max(0.0, b1[3] - b1[1])
|
| 365 |
+
area2 = max(0.0, b2[2] - b2[0]) * max(0.0, b2[3] - b2[1])
|
| 366 |
+
union = area1 + area2 - inter
|
| 367 |
+
return inter / union if union > 1e-12 else 0.0
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
def select_pred_bbox(candidates: List[Dict[str, Any]], gt_box: Any,
|
| 371 |
+
mode: str = "first") -> Tuple[Optional[List[float]], Optional[str]]:
|
| 372 |
+
if not candidates:
|
| 373 |
+
return None, None
|
| 374 |
+
if mode == "best_iou":
|
| 375 |
+
best = max(candidates, key=lambda c: compute_iou_2d(gt_box, c.get("bbox")))
|
| 376 |
+
else:
|
| 377 |
+
best = candidates[0]
|
| 378 |
+
return best.get("bbox"), best.get("label")
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
def sanitize_video_kwargs(video_kwargs: Any, has_video: bool = False) -> Dict[str, Any]:
|
| 382 |
+
if not has_video:
|
| 383 |
+
return {}
|
| 384 |
+
if not isinstance(video_kwargs, dict):
|
| 385 |
+
return {}
|
| 386 |
+
return {k: v for k, v in video_kwargs.items() if v is not None}
|