lyhisme commited on
Commit
53c10a4
·
verified ·
1 Parent(s): 00798e7

Add OraRL training and evaluation code

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +41 -0
  2. code/.gitignore +80 -0
  3. code/LICENSE +201 -0
  4. code/MANIFEST.in +20 -0
  5. code/NOTICE +7 -0
  6. code/README.md +201 -0
  7. code/README_zh.md +193 -0
  8. code/assets/orarl-data-scaling.gif +3 -0
  9. code/assets/orarl-hero.gif +3 -0
  10. code/assets/orarl-method.gif +3 -0
  11. code/assets/orarl-model-scaling.gif +3 -0
  12. code/assets/orarl-teaser.mp4 +3 -0
  13. code/assets/paper-data-scaling.png +3 -0
  14. code/assets/paper-framework.png +3 -0
  15. code/assets/paper-model-scaling.png +3 -0
  16. code/assets/paper-results.png +3 -0
  17. code/assets/video_ora_benchmark_matrix_dark.svg +362 -0
  18. code/assets/video_ora_benchmark_matrix_light.svg +362 -0
  19. code/configs/data_sources.example.yaml +90 -0
  20. code/configs/grpo_4b.yaml +139 -0
  21. code/configs/grpo_9b.yaml +139 -0
  22. code/configs/orarl_4b.yaml +162 -0
  23. code/configs/orarl_9b.yaml +162 -0
  24. code/data/eval/README.md +25 -0
  25. code/data/eval/datasets.jsonl +29 -0
  26. code/docs/environment.md +60 -0
  27. code/docs/environment_zh.md +57 -0
  28. code/docs/evaluation.md +121 -0
  29. code/docs/evaluation_zh.md +116 -0
  30. code/docs/training.md +162 -0
  31. code/docs/training_zh.md +151 -0
  32. code/environment.yml +18 -0
  33. code/eval/README.md +72 -0
  34. code/eval/task/canonical_data.py +450 -0
  35. code/eval/task/eval.sh +1018 -0
  36. code/eval/task/eval_image_mc_vllm.py +511 -0
  37. code/eval/task/eval_prompt.py +247 -0
  38. code/eval/task/eval_revsi_multinode.sh +229 -0
  39. code/eval/task/eval_vllm.py +0 -0
  40. code/eval/task/mindcube/data_utils.py +254 -0
  41. code/eval/task/mmsi/eval_mmsi_transformers.py +323 -0
  42. code/eval/task/mmsi/run_eval_transformers.sh +214 -0
  43. code/eval/task/qwenvl_decord_patch.py +271 -0
  44. code/eval/task/revsi/eval_revsi_vllm.py +1025 -0
  45. code/eval/task/revsi/merge_multinode_shards.py +81 -0
  46. code/eval/task/revsi/run_eval_vllm.sh +321 -0
  47. code/eval/task/segmentation/eval_seg_vllm.py +446 -0
  48. code/eval/task/segmentation/post_sam2.py +491 -0
  49. code/eval/task/segmentation/run_eval_vllm.sh +500 -0
  50. 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
+ &nbsp;&nbsp;·&nbsp;&nbsp;
17
+ <a href="https://orarl.github.io/">🌐 Project Page</a>
18
+ &nbsp;&nbsp;·&nbsp;&nbsp;
19
+ <a href="#models">🤗 Models (4B / 9B)</a>
20
+ </p>
21
+ <p>
22
+ <a href="docs/environment.md">⚙️ Environment</a>
23
+ &nbsp;&nbsp;·&nbsp;&nbsp;
24
+ <a href="docs/training.md">🚀 Training</a>
25
+ &nbsp;&nbsp;·&nbsp;&nbsp;
26
+ <a href="docs/evaluation.md">📊 Evaluation</a>
27
+ &nbsp;&nbsp;·&nbsp;&nbsp;
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
+ &nbsp;&nbsp;·&nbsp;&nbsp;
17
+ <a href="https://orarl.github.io/">🌐 项目主页</a>
18
+ &nbsp;&nbsp;·&nbsp;&nbsp;
19
+ <a href="#models">🤗 模型(4B / 9B)</a>
20
+ </p>
21
+ <p>
22
+ <a href="docs/environment_zh.md">⚙️ 环境</a>
23
+ &nbsp;&nbsp;·&nbsp;&nbsp;
24
+ <a href="docs/training_zh.md">🚀 训练</a>
25
+ &nbsp;&nbsp;·&nbsp;&nbsp;
26
+ <a href="docs/evaluation_zh.md">📊 评测</a>
27
+ &nbsp;&nbsp;·&nbsp;&nbsp;
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

  • SHA256: f63cbb854c1c9f3867dce9300b44f01b9d6d27a018947478fb927ac9c932e57e
  • Pointer size: 131 Bytes
  • Size of remote file: 240 kB
code/assets/orarl-hero.gif ADDED

Git LFS Details

  • SHA256: c9445d37d764a433e6743df9595d497819a994d27e4d074e875f23cae44e5ef8
  • Pointer size: 131 Bytes
  • Size of remote file: 339 kB
code/assets/orarl-method.gif ADDED

Git LFS Details

  • SHA256: 8e43a40e4ea1d241e5ee230090ca39f84ce972587808b7ac11cef8ffb7c6ccd7
  • Pointer size: 132 Bytes
  • Size of remote file: 2.25 MB
code/assets/orarl-model-scaling.gif ADDED

Git LFS Details

  • SHA256: 5463916e2ec28a459bee1bed561867ad6de98b8a37ad9dee3944e43d43761595
  • Pointer size: 131 Bytes
  • Size of remote file: 268 kB
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

  • SHA256: 9f9243c616b35fea4bc5aa25b86b6c7e501d1766ba10f3db493aa1fcdf08b4ec
  • Pointer size: 131 Bytes
  • Size of remote file: 145 kB
code/assets/paper-framework.png ADDED

Git LFS Details

  • SHA256: aa39c05c95b1b9c6fac5615d4babe206c96c103d6c3d9c70d69bb3002a3fbe17
  • Pointer size: 131 Bytes
  • Size of remote file: 186 kB
code/assets/paper-model-scaling.png ADDED

Git LFS Details

  • SHA256: c79ab0337be5ee65a4e203e8060d7515b394660409b4fd256230c5a5b32a2dc5
  • Pointer size: 131 Bytes
  • Size of remote file: 138 kB
code/assets/paper-results.png ADDED

Git LFS Details

  • SHA256: 8fd05c8ff68f5a864d1d113eb5060c48e5c482e5d8ff387e0308b80a87752d0c
  • Pointer size: 131 Bytes
  • Size of remote file: 317 kB
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}