TerryPei commited on
Commit
070f264
·
verified ·
1 Parent(s): dd8d465

sync QWENVL-PRIVATE from /opt/tiger/thothvl_pretrain (HF tokens redacted)

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 +24 -0
  2. QWENVL-PRIVATE/.gitignore +17 -0
  3. QWENVL-PRIVATE/LICENSE +352 -0
  4. QWENVL-PRIVATE/README.md +1015 -0
  5. QWENVL-PRIVATE/analysis/aaten_debug.py +393 -0
  6. QWENVL-PRIVATE/analysis/analysis_auto.py +588 -0
  7. QWENVL-PRIVATE/analysis/analysis_high_res_prediction.py +190 -0
  8. QWENVL-PRIVATE/analysis/check_gate_label.py +195 -0
  9. QWENVL-PRIVATE/analysis/check_pseudo_label.py +630 -0
  10. QWENVL-PRIVATE/analysis/check_textvqa_json.py +56 -0
  11. QWENVL-PRIVATE/analysis/convert_chartqa.py +84 -0
  12. QWENVL-PRIVATE/analysis/convert_json2qwen_training.py +220 -0
  13. QWENVL-PRIVATE/analysis/convert_seal_json.py +81 -0
  14. QWENVL-PRIVATE/analysis/denoise_textual_file.py +348 -0
  15. QWENVL-PRIVATE/analysis/denoise_textual_file_wdet.py +330 -0
  16. QWENVL-PRIVATE/analysis/failure_case_analysis.py +302 -0
  17. QWENVL-PRIVATE/analysis/filter_roi_confusion.py +92 -0
  18. QWENVL-PRIVATE/analysis/filter_roi_fails.py +438 -0
  19. QWENVL-PRIVATE/analysis/fix_stage1_image_paths.py +166 -0
  20. QWENVL-PRIVATE/analysis/gated_roi_analysis.py +899 -0
  21. QWENVL-PRIVATE/analysis/gen_roi_map_30b.py +200 -0
  22. QWENVL-PRIVATE/analysis/get_o2i_attn.py +107 -0
  23. QWENVL-PRIVATE/analysis/llm-as-judge-roi-samples.py +383 -0
  24. QWENVL-PRIVATE/analysis/making_base_model_response.py +544 -0
  25. QWENVL-PRIVATE/analysis/making_gated_training_samples.py +781 -0
  26. QWENVL-PRIVATE/analysis/making_roi_model_response.py +565 -0
  27. QWENVL-PRIVATE/analysis/matching_utils.py +257 -0
  28. QWENVL-PRIVATE/analysis/merge_jsonl.py +32 -0
  29. QWENVL-PRIVATE/analysis/merge_pkls.py +49 -0
  30. QWENVL-PRIVATE/analysis/merge_stage1_pkls.py +36 -0
  31. QWENVL-PRIVATE/analysis/plot_trade_off.py +148 -0
  32. QWENVL-PRIVATE/analysis/plot_vstar_tradeoff.py +101 -0
  33. QWENVL-PRIVATE/analysis/qwen3vl_debug.py +410 -0
  34. QWENVL-PRIVATE/analysis/qwen3vl_debug_30b.py +300 -0
  35. QWENVL-PRIVATE/analysis/qwen3vl_debug_32b.py +349 -0
  36. QWENVL-PRIVATE/analysis/qwen3vl_identify_heads_30b.py +202 -0
  37. QWENVL-PRIVATE/analysis/qwen3vl_identify_heads_32b.py +237 -0
  38. QWENVL-PRIVATE/analysis/strip_image_root_inplace.py +25 -0
  39. QWENVL-PRIVATE/analysis/tmp_plot.py +84 -0
  40. QWENVL-PRIVATE/cookbooks/assets/agent_function_call/mobile_en_example.png +3 -0
  41. QWENVL-PRIVATE/cookbooks/assets/agent_function_call/mobile_zh_example.jpg +3 -0
  42. QWENVL-PRIVATE/cookbooks/assets/computer_use/computer_use1.jpeg +3 -0
  43. QWENVL-PRIVATE/cookbooks/assets/computer_use/computer_use2.jpeg +3 -0
  44. QWENVL-PRIVATE/cookbooks/assets/document_parsing/docparsing_example1.jpg +3 -0
  45. QWENVL-PRIVATE/cookbooks/assets/document_parsing/docparsing_example2.jpg +3 -0
  46. QWENVL-PRIVATE/cookbooks/assets/document_parsing/docparsing_example3.jpg +3 -0
  47. QWENVL-PRIVATE/cookbooks/assets/document_parsing/docparsing_example4.jpg +3 -0
  48. QWENVL-PRIVATE/cookbooks/assets/document_parsing/docparsing_example5.png +3 -0
  49. QWENVL-PRIVATE/cookbooks/assets/document_parsing/docparsing_example6.png +3 -0
  50. QWENVL-PRIVATE/cookbooks/assets/document_parsing/docparsing_example7.jpg +3 -0
.gitattributes CHANGED
@@ -33,3 +33,27 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ QWENVL-PRIVATE/cookbooks/assets/agent_function_call/mobile_en_example.png filter=lfs diff=lfs merge=lfs -text
37
+ QWENVL-PRIVATE/cookbooks/assets/agent_function_call/mobile_zh_example.jpg filter=lfs diff=lfs merge=lfs -text
38
+ QWENVL-PRIVATE/cookbooks/assets/computer_use/computer_use1.jpeg filter=lfs diff=lfs merge=lfs -text
39
+ QWENVL-PRIVATE/cookbooks/assets/computer_use/computer_use2.jpeg filter=lfs diff=lfs merge=lfs -text
40
+ QWENVL-PRIVATE/cookbooks/assets/document_parsing/docparsing_example1.jpg filter=lfs diff=lfs merge=lfs -text
41
+ QWENVL-PRIVATE/cookbooks/assets/document_parsing/docparsing_example2.jpg filter=lfs diff=lfs merge=lfs -text
42
+ QWENVL-PRIVATE/cookbooks/assets/document_parsing/docparsing_example3.jpg filter=lfs diff=lfs merge=lfs -text
43
+ QWENVL-PRIVATE/cookbooks/assets/document_parsing/docparsing_example4.jpg filter=lfs diff=lfs merge=lfs -text
44
+ QWENVL-PRIVATE/cookbooks/assets/document_parsing/docparsing_example5.png filter=lfs diff=lfs merge=lfs -text
45
+ QWENVL-PRIVATE/cookbooks/assets/document_parsing/docparsing_example6.png filter=lfs diff=lfs merge=lfs -text
46
+ QWENVL-PRIVATE/cookbooks/assets/document_parsing/docparsing_example7.jpg filter=lfs diff=lfs merge=lfs -text
47
+ QWENVL-PRIVATE/cookbooks/assets/document_parsing/docparsing_example8.png filter=lfs diff=lfs merge=lfs -text
48
+ QWENVL-PRIVATE/cookbooks/assets/ocr/ocr_example1.jpg filter=lfs diff=lfs merge=lfs -text
49
+ QWENVL-PRIVATE/cookbooks/assets/ocr/ocr_example3.jpg filter=lfs diff=lfs merge=lfs -text
50
+ QWENVL-PRIVATE/cookbooks/assets/ocr/ocr_example5.jpg filter=lfs diff=lfs merge=lfs -text
51
+ QWENVL-PRIVATE/cookbooks/assets/spatial_understanding/Origamis.jpg filter=lfs diff=lfs merge=lfs -text
52
+ QWENVL-PRIVATE/cookbooks/assets/spatial_understanding/cakes.png filter=lfs diff=lfs merge=lfs -text
53
+ QWENVL-PRIVATE/cookbooks/assets/universal_recognition/unireco_bird_example.jpg filter=lfs diff=lfs merge=lfs -text
54
+ QWENVL-PRIVATE/cookbooks/assets/universal_recognition/unireco_birds_example.jpg filter=lfs diff=lfs merge=lfs -text
55
+ QWENVL-PRIVATE/cookbooks/assets/universal_recognition/unireco_celebrities_example.jpg filter=lfs diff=lfs merge=lfs -text
56
+ QWENVL-PRIVATE/cookbooks/assets/universal_recognition/unireco_landmarks_example.jpg filter=lfs diff=lfs merge=lfs -text
57
+ QWENVL-PRIVATE/qwen-vl-finetune/demo/videos/v_7bUu05RIksU.mp4 filter=lfs diff=lfs merge=lfs -text
58
+ QWENVL-PRIVATE/qwen-vl-finetune/demo/videos/v_TpB_zMG3XBA.mp4 filter=lfs diff=lfs merge=lfs -text
59
+ QWENVL-PRIVATE/qwen-vl-finetune/demo/videos/v_rBMQFpHspmo.mp4 filter=lfs diff=lfs merge=lfs -text
QWENVL-PRIVATE/.gitignore ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # python generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # venv
10
+ .venv
11
+ /exclude/
12
+ .tmp.driveupload
13
+ /excluded/
14
+ /output/
15
+ .idea/
16
+ logs/
17
+ lmms-eval-0.5.0/
QWENVL-PRIVATE/LICENSE ADDED
@@ -0,0 +1,352 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Creative Commons Attribution-NonCommercial 4.0 International
2
+
3
+ Creative Commons Corporation ("Creative Commons") is not a law firm and
4
+ does not provide legal services or legal advice. Distribution of
5
+ Creative Commons public licenses does not create a lawyer-client or
6
+ other relationship. Creative Commons makes its licenses and related
7
+ information available on an "as-is" basis. Creative Commons gives no
8
+ warranties regarding its licenses, any material licensed under their
9
+ terms and conditions, or any related information. Creative Commons
10
+ disclaims all liability for damages resulting from their use to the
11
+ fullest extent possible.
12
+
13
+ Using Creative Commons Public Licenses
14
+
15
+ Creative Commons public licenses provide a standard set of terms and
16
+ conditions that creators and other rights holders may use to share
17
+ original works of authorship and other material subject to copyright and
18
+ certain other rights specified in the public license below. The
19
+ following considerations are for informational purposes only, are not
20
+ exhaustive, and do not form part of our licenses.
21
+
22
+ - Considerations for licensors: Our public licenses are intended for
23
+ use by those authorized to give the public permission to use
24
+ material in ways otherwise restricted by copyright and certain other
25
+ rights. Our licenses are irrevocable. Licensors should read and
26
+ understand the terms and conditions of the license they choose
27
+ before applying it. Licensors should also secure all rights
28
+ necessary before applying our licenses so that the public can reuse
29
+ the material as expected. Licensors should clearly mark any material
30
+ not subject to the license. This includes other CC-licensed
31
+ material, or material used under an exception or limitation to
32
+ copyright. More considerations for licensors :
33
+ wiki.creativecommons.org/Considerations_for_licensors
34
+
35
+ - Considerations for the public: By using one of our public licenses,
36
+ a licensor grants the public permission to use the licensed material
37
+ under specified terms and conditions. If the licensor's permission
38
+ is not necessary for any reason–for example, because of any
39
+ applicable exception or limitation to copyright–then that use is not
40
+ regulated by the license. Our licenses grant only permissions under
41
+ copyright and certain other rights that a licensor has authority to
42
+ grant. Use of the licensed material may still be restricted for
43
+ other reasons, including because others have copyright or other
44
+ rights in the material. A licensor may make special requests, such
45
+ as asking that all changes be marked or described. Although not
46
+ required by our licenses, you are encouraged to respect those
47
+ requests where reasonable. More considerations for the public :
48
+ wiki.creativecommons.org/Considerations_for_licensees
49
+
50
+ Creative Commons Attribution-NonCommercial 4.0 International Public
51
+ License
52
+
53
+ By exercising the Licensed Rights (defined below), You accept and agree
54
+ to be bound by the terms and conditions of this Creative Commons
55
+ Attribution-NonCommercial 4.0 International Public License ("Public
56
+ License"). To the extent this Public License may be interpreted as a
57
+ contract, You are granted the Licensed Rights in consideration of Your
58
+ acceptance of these terms and conditions, and the Licensor grants You
59
+ such rights in consideration of benefits the Licensor receives from
60
+ making the Licensed Material available under these terms and conditions.
61
+
62
+ - Section 1 – Definitions.
63
+
64
+ - a. Adapted Material means material subject to Copyright and
65
+ Similar Rights that is derived from or based upon the Licensed
66
+ Material and in which the Licensed Material is translated,
67
+ altered, arranged, transformed, or otherwise modified in a
68
+ manner requiring permission under the Copyright and Similar
69
+ Rights held by the Licensor. For purposes of this Public
70
+ License, where the Licensed Material is a musical work,
71
+ performance, or sound recording, Adapted Material is always
72
+ produced where the Licensed Material is synched in timed
73
+ relation with a moving image.
74
+ - b. Adapter's License means the license You apply to Your
75
+ Copyright and Similar Rights in Your contributions to Adapted
76
+ Material in accordance with the terms and conditions of this
77
+ Public License.
78
+ - c. Copyright and Similar Rights means copyright and/or similar
79
+ rights closely related to copyright including, without
80
+ limitation, performance, broadcast, sound recording, and Sui
81
+ Generis Database Rights, without regard to how the rights are
82
+ labeled or categorized. For purposes of this Public License, the
83
+ rights specified in Section 2(b)(1)-(2) are not Copyright and
84
+ Similar Rights.
85
+ - d. Effective Technological Measures means those measures that,
86
+ in the absence of proper authority, may not be circumvented
87
+ under laws fulfilling obligations under Article 11 of the WIPO
88
+ Copyright Treaty adopted on December 20, 1996, and/or similar
89
+ international agreements.
90
+ - e. Exceptions and Limitations means fair use, fair dealing,
91
+ and/or any other exception or limitation to Copyright and
92
+ Similar Rights that applies to Your use of the Licensed
93
+ Material.
94
+ - f. Licensed Material means the artistic or literary work,
95
+ database, or other material to which the Licensor applied this
96
+ Public License.
97
+ - g. Licensed Rights means the rights granted to You subject to
98
+ the terms and conditions of this Public License, which are
99
+ limited to all Copyright and Similar Rights that apply to Your
100
+ use of the Licensed Material and that the Licensor has authority
101
+ to license.
102
+ - h. Licensor means the individual(s) or entity(ies) granting
103
+ rights under this Public License.
104
+ - i. NonCommercial means not primarily intended for or directed
105
+ towards commercial advantage or monetary compensation. For
106
+ purposes of this Public License, the exchange of the Licensed
107
+ Material for other material subject to Copyright and Similar
108
+ Rights by digital file-sharing or similar means is NonCommercial
109
+ provided there is no payment of monetary compensation in
110
+ connection with the exchange.
111
+ - j. Share means to provide material to the public by any means or
112
+ process that requires permission under the Licensed Rights, such
113
+ as reproduction, public display, public performance,
114
+ distribution, dissemination, communication, or importation, and
115
+ to make material available to the public including in ways that
116
+ members of the public may access the material from a place and
117
+ at a time individually chosen by them.
118
+ - k. Sui Generis Database Rights means rights other than copyright
119
+ resulting from Directive 96/9/EC of the European Parliament and
120
+ of the Council of 11 March 1996 on the legal protection of
121
+ databases, as amended and/or succeeded, as well as other
122
+ essentially equivalent rights anywhere in the world.
123
+ - l. You means the individual or entity exercising the Licensed
124
+ Rights under this Public License. Your has a corresponding
125
+ meaning.
126
+
127
+ - Section 2 – Scope.
128
+
129
+ - a. License grant.
130
+ - 1. Subject to the terms and conditions of this Public
131
+ License, the Licensor hereby grants You a worldwide,
132
+ royalty-free, non-sublicensable, non-exclusive, irrevocable
133
+ license to exercise the Licensed Rights in the Licensed
134
+ Material to:
135
+ - A. reproduce and Share the Licensed Material, in whole
136
+ or in part, for NonCommercial purposes only; and
137
+ - B. produce, reproduce, and Share Adapted Material for
138
+ NonCommercial purposes only.
139
+ - 2. Exceptions and Limitations. For the avoidance of doubt,
140
+ where Exceptions and Limitations apply to Your use, this
141
+ Public License does not apply, and You do not need to comply
142
+ with its terms and conditions.
143
+ - 3. Term. The term of this Public License is specified in
144
+ Section 6(a).
145
+ - 4. Media and formats; technical modifications allowed. The
146
+ Licensor authorizes You to exercise the Licensed Rights in
147
+ all media and formats whether now known or hereafter
148
+ created, and to make technical modifications necessary to do
149
+ so. The Licensor waives and/or agrees not to assert any
150
+ right or authority to forbid You from making technical
151
+ modifications necessary to exercise the Licensed Rights,
152
+ including technical modifications necessary to circumvent
153
+ Effective Technological Measures. For purposes of this
154
+ Public License, simply making modifications authorized by
155
+ this Section 2(a)(4) never produces Adapted Material.
156
+ - 5. Downstream recipients.
157
+ - A. Offer from the Licensor – Licensed Material. Every
158
+ recipient of the Licensed Material automatically
159
+ receives an offer from the Licensor to exercise the
160
+ Licensed Rights under the terms and conditions of this
161
+ Public License.
162
+ - B. No downstream restrictions. You may not offer or
163
+ impose any additional or different terms or conditions
164
+ on, or apply any Effective Technological Measures to,
165
+ the Licensed Material if doing so restricts exercise of
166
+ the Licensed Rights by any recipient of the Licensed
167
+ Material.
168
+ - 6. No endorsement. Nothing in this Public License
169
+ constitutes or may be construed as permission to assert or
170
+ imply that You are, or that Your use of the Licensed
171
+ Material is, connected with, or sponsored, endorsed, or
172
+ granted official status by, the Licensor or others
173
+ designated to receive attribution as provided in Section
174
+ 3(a)(1)(A)(i).
175
+ - b. Other rights.
176
+ - 1. Moral rights, such as the right of integrity, are not
177
+ licensed under this Public License, nor are publicity,
178
+ privacy, and/or other similar personality rights; however,
179
+ to the extent possible, the Licensor waives and/or agrees
180
+ not to assert any such rights held by the Licensor to the
181
+ limited extent necessary to allow You to exercise the
182
+ Licensed Rights, but not otherwise.
183
+ - 2. Patent and trademark rights are not licensed under this
184
+ Public License.
185
+ - 3. To the extent possible, the Licensor waives any right to
186
+ collect royalties from You for the exercise of the Licensed
187
+ Rights, whether directly or through a collecting society
188
+ under any voluntary or waivable statutory or compulsory
189
+ licensing scheme. In all other cases the Licensor expressly
190
+ reserves any right to collect such royalties, including when
191
+ the Licensed Material is used other than for NonCommercial
192
+ purposes.
193
+
194
+ - Section 3 – License Conditions.
195
+
196
+ Your exercise of the Licensed Rights is expressly made subject to
197
+ the following conditions.
198
+
199
+ - a. Attribution.
200
+ - 1. If You Share the Licensed Material (including in modified
201
+ form), You must:
202
+ - A. retain the following if it is supplied by the
203
+ Licensor with the Licensed Material:
204
+ - i. identification of the creator(s) of the Licensed
205
+ Material and any others designated to receive
206
+ attribution, in any reasonable manner requested by
207
+ the Licensor (including by pseudonym if designated);
208
+ - ii. a copyright notice;
209
+ - iii. a notice that refers to this Public License;
210
+ - iv. a notice that refers to the disclaimer of
211
+ warranties;
212
+ - v. a URI or hyperlink to the Licensed Material to
213
+ the extent reasonably practicable;
214
+ - B. indicate if You modified the Licensed Material and
215
+ retain an indication of any previous modifications; and
216
+ - C. indicate the Licensed Material is licensed under this
217
+ Public License, and include the text of, or the URI or
218
+ hyperlink to, this Public License.
219
+ - 2. You may satisfy the conditions in Section 3(a)(1) in any
220
+ reasonable manner based on the medium, means, and context in
221
+ which You Share the Licensed Material. For example, it may
222
+ be reasonable to satisfy the conditions by providing a URI
223
+ or hyperlink to a resource that includes the required
224
+ information.
225
+ - 3. If requested by the Licensor, You must remove any of the
226
+ information required by Section 3(a)(1)(A) to the extent
227
+ reasonably practicable.
228
+ - 4. If You Share Adapted Material You produce, the Adapter's
229
+ License You apply must not prevent recipients of the Adapted
230
+ Material from complying with this Public License.
231
+
232
+ - Section 4 – Sui Generis Database Rights.
233
+
234
+ Where the Licensed Rights include Sui Generis Database Rights that
235
+ apply to Your use of the Licensed Material:
236
+
237
+ - a. for the avoidance of doubt, Section 2(a)(1) grants You the
238
+ right to extract, reuse, reproduce, and Share all or a
239
+ substantial portion of the contents of the database for
240
+ NonCommercial purposes only;
241
+ - b. if You include all or a substantial portion of the database
242
+ contents in a database in which You have Sui Generis Database
243
+ Rights, then the database in which You have Sui Generis Database
244
+ Rights (but not its individual contents) is Adapted Material;
245
+ and
246
+ - c. You must comply with the conditions in Section 3(a) if You
247
+ Share all or a substantial portion of the contents of the
248
+ database.
249
+
250
+ For the avoidance of doubt, this Section 4 supplements and does not
251
+ replace Your obligations under this Public License where the
252
+ Licensed Rights include other Copyright and Similar Rights.
253
+
254
+ - Section 5 – Disclaimer of Warranties and Limitation of Liability.
255
+
256
+ - a. Unless otherwise separately undertaken by the Licensor, to
257
+ the extent possible, the Licensor offers the Licensed Material
258
+ as-is and as-available, and makes no representations or
259
+ warranties of any kind concerning the Licensed Material, whether
260
+ express, implied, statutory, or other. This includes, without
261
+ limitation, warranties of title, merchantability, fitness for a
262
+ particular purpose, non-infringement, absence of latent or other
263
+ defects, accuracy, or the presence or absence of errors, whether
264
+ or not known or discoverable. Where disclaimers of warranties
265
+ are not allowed in full or in part, this disclaimer may not
266
+ apply to You.
267
+ - b. To the extent possible, in no event will the Licensor be
268
+ liable to You on any legal theory (including, without
269
+ limitation, negligence) or otherwise for any direct, special,
270
+ indirect, incidental, consequential, punitive, exemplary, or
271
+ other losses, costs, expenses, or damages arising out of this
272
+ Public License or use of the Licensed Material, even if the
273
+ Licensor has been advised of the possibility of such losses,
274
+ costs, expenses, or damages. Where a limitation of liability is
275
+ not allowed in full or in part, this limitation may not apply to
276
+ You.
277
+ - c. The disclaimer of warranties and limitation of liability
278
+ provided above shall be interpreted in a manner that, to the
279
+ extent possible, most closely approximates an absolute
280
+ disclaimer and waiver of all liability.
281
+
282
+ - Section 6 – Term and Termination.
283
+
284
+ - a. This Public License applies for the term of the Copyright and
285
+ Similar Rights licensed here. However, if You fail to comply
286
+ with this Public License, then Your rights under this Public
287
+ License terminate automatically.
288
+ - b. Where Your right to use the Licensed Material has terminated
289
+ under Section 6(a), it reinstates:
290
+
291
+ - 1. automatically as of the date the violation is cured,
292
+ provided it is cured within 30 days of Your discovery of the
293
+ violation; or
294
+ - 2. upon express reinstatement by the Licensor.
295
+
296
+ For the avoidance of doubt, this Section 6(b) does not affect
297
+ any right the Licensor may have to seek remedies for Your
298
+ violations of this Public License.
299
+
300
+ - c. For the avoidance of doubt, the Licensor may also offer the
301
+ Licensed Material under separate terms or conditions or stop
302
+ distributing the Licensed Material at any time; however, doing
303
+ so will not terminate this Public License.
304
+ - d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
305
+ License.
306
+
307
+ - Section 7 – Other Terms and Conditions.
308
+
309
+ - a. The Licensor shall not be bound by any additional or
310
+ different terms or conditions communicated by You unless
311
+ expressly agreed.
312
+ - b. Any arrangements, understandings, or agreements regarding the
313
+ Licensed Material not stated herein are separate from and
314
+ independent of the terms and conditions of this Public License.
315
+
316
+ - Section 8 – Interpretation.
317
+
318
+ - a. For the avoidance of doubt, this Public License does not, and
319
+ shall not be interpreted to, reduce, limit, restrict, or impose
320
+ conditions on any use of the Licensed Material that could
321
+ lawfully be made without permission under this Public License.
322
+ - b. To the extent possible, if any provision of this Public
323
+ License is deemed unenforceable, it shall be automatically
324
+ reformed to the minimum extent necessary to make it enforceable.
325
+ If the provision cannot be reformed, it shall be severed from
326
+ this Public License without affecting the enforceability of the
327
+ remaining terms and conditions.
328
+ - c. No term or condition of this Public License will be waived
329
+ and no failure to comply consented to unless expressly agreed to
330
+ by the Licensor.
331
+ - d. Nothing in this Public License constitutes or may be
332
+ interpreted as a limitation upon, or waiver of, any privileges
333
+ and immunities that apply to the Licensor or You, including from
334
+ the legal processes of any jurisdiction or authority.
335
+
336
+ Creative Commons is not a party to its public licenses. Notwithstanding,
337
+ Creative Commons may elect to apply one of its public licenses to
338
+ material it publishes and in those instances will be considered the
339
+ "Licensor." The text of the Creative Commons public licenses is
340
+ dedicated to the public domain under the CC0 Public Domain Dedication.
341
+ Except for the limited purpose of indicating that material is shared
342
+ under a Creative Commons public license or as otherwise permitted by the
343
+ Creative Commons policies published at creativecommons.org/policies,
344
+ Creative Commons does not authorize the use of the trademark "Creative
345
+ Commons" or any other trademark or logo of Creative Commons without its
346
+ prior written consent including, without limitation, in connection with
347
+ any unauthorized modifications to any of its public licenses or any
348
+ other arrangements, understandings, or agreements concerning use of
349
+ licensed material. For the avoidance of doubt, this paragraph does not
350
+ form part of the public licenses.
351
+
352
+ Creative Commons may be contacted at creativecommons.org.
QWENVL-PRIVATE/README.md ADDED
@@ -0,0 +1,1015 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Qwen2.5-VL
2
+
3
+
4
+ <p align="center">
5
+ <img src="https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen2.5-VL/qwen2.5vl_logo.png" width="400"/>
6
+ <p>
7
+
8
+ <p align="center">
9
+ 💜 <a href="https://chat.qwenlm.ai/"><b>Qwen Chat</b></a>&nbsp&nbsp | &nbsp&nbsp🤗 <a href="https://huggingface.co/collections/Qwen/qwen25-vl-6795ffac22b334a837c0f9a5">Hugging Face</a>&nbsp&nbsp | &nbsp&nbsp🤖 <a href="https://modelscope.cn/organization/qwen">ModelScope</a>&nbsp&nbsp | &nbsp&nbsp📑 <a href="https://qwenlm.github.io/blog/qwen2.5-vl/">Blog</a>&nbsp&nbsp | &nbsp&nbsp📚 <a href="https://github.com/QwenLM/Qwen2.5-VL/tree/main/cookbooks">Cookbooks</a>&nbsp&nbsp | &nbsp&nbsp📑 <a href="https://arxiv.org/abs/2502.13923">Paper</a>&nbsp&nbsp
10
+ <br>
11
+ 🖥️ <a href="https://huggingface.co/spaces/Qwen/Qwen2.5-VL-72B-Instruct">Demo</a>&nbsp&nbsp | &nbsp&nbsp💬 <a href="https://github.com/QwenLM/Qwen/blob/main/assets/wechat.png">WeChat (微信)</a>&nbsp&nbsp | &nbsp&nbsp🫨 <a href="https://discord.gg/CV4E9rpNSD">Discord</a>&nbsp&nbsp | &nbsp&nbsp📑 <a href="https://help.aliyun.com/zh/model-studio/developer-reference/qwen-vl-api">API</a>&nbsp&nbsp | &nbsp&nbsp🖥️ <a href="https://gallery.pai-ml.com/#/preview/deepLearning/cv/qwen2.5-vl">PAI-DSW</a>
12
+ </p>
13
+
14
+
15
+
16
+
17
+ ## Introduction
18
+ In the past five months since Qwen2-VL's release, numerous developers have built new models on the Qwen2-VL vision-language models, providing us with valuable feedback. During this period, we focused on building more useful vision-language models. Today, we are excited to introduce the latest addition to the Qwen family: Qwen2.5-VL.
19
+
20
+
21
+ #### Key Enhancements:
22
+
23
+ * **Powerful Document Parsing Capabilities**: Upgrade text recognition to omnidocument parsing, excelling in processing multi-scene, multilingual, and various built-in (handwriting, tables, charts, chemical formulas, and music sheets) documents.
24
+
25
+ * **Precise Object Grounding Across Formats**: Unlock improved accuracy in detecting, pointing, and counting objects, accommodating absolute coordinate and JSON formats for advanced spatial reasoning.
26
+
27
+ * **Ultra-long Video Understanding and Fine-grained Video Grounding**: Extend native dynamic resolution to the temporal dimension, enhancing the ability to understand videos lasting hours while extracting event segments in seconds.
28
+
29
+ * **Enhanced Agent Functionality for Computer and Mobile Devices**: Leverage advanced grounding, reasoning, and decision-making abilities, boosting the model with superior agent functionality on smartphones and computers.
30
+
31
+
32
+ #### Model Architecture Updates:
33
+
34
+ * **Dynamic Resolution and Frame Rate Training for Video Understanding**:
35
+
36
+ We extend dynamic resolution to the temporal dimension by adopting dynamic FPS sampling, enabling the model to comprehend videos at various sampling rates. Accordingly, we update mRoPE in the time dimension with IDs and absolute time alignment, enabling the model to learn temporal sequence and speed, and ultimately acquire the ability to pinpoint specific moments.
37
+
38
+ <p align="center">
39
+ <img src="https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen2.5-VL/qwen2.5vl_arc.jpeg" width="80%"/>
40
+ <p>
41
+
42
+
43
+ * **Streamlined and Efficient Vision Encoder**
44
+
45
+ We enhance both training and inference speeds by strategically implementing window attention into the ViT. The ViT architecture is further optimized with SwiGLU and RMSNorm, aligning it with the structure of the Qwen2.5 LLM.
46
+
47
+
48
+ ## News
49
+ * 2025.04.08: We provide the [code](https://github.com/QwenLM/Qwen2.5-VL/tree/main/qwen-vl-finetune) for fine-tuning Qwen2-VL and Qwen2.5-VL.
50
+ * 2025.03.25: We have released the [Qwen2.5-VL-32B](https://huggingface.co/Qwen/Qwen2.5-VL-32B-Instruct). It is smarter and its responses align more closely with human preferences. For more details, please check our [blog](https://qwenlm.github.io/blog/qwen2.5-vl-32b/)!
51
+ * 2025.02.20: we have released the [Qwen2.5-VL Technical Report](https://arxiv.org/abs/2502.13923). Alongside the report, we have also released AWQ-quantized models for Qwen2.5-VL in three different sizes: [3B](https://huggingface.co/Qwen/Qwen2.5-VL-3B-Instruct-AWQ), [7B](https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct-AWQ) , and [72B](https://huggingface.co/Qwen/Qwen2.5-VL-72B-Instruct-AWQ) parameters.
52
+ * 2025.01.28: We have released the [Qwen2.5-VL series](https://huggingface.co/Qwen). For more details, please check our [blog](https://qwenlm.github.io/blog/qwen2.5-vl/)!
53
+ * 2024.12.25: We have released the [QvQ-72B-Preview](https://huggingface.co/Qwen/QVQ-72B-Preview). QvQ-72B-Preview is an experimental research model, focusing on enhancing visual reasoning capabilities. For more details, please check our [blog](https://qwenlm.github.io/blog/qvq-72b-preview/)!
54
+ * 2024.09.19: The instruction-tuned [Qwen2-VL-72B model](https://huggingface.co/Qwen/Qwen2-VL-72B-Instruct) and its quantized version [[AWQ](https://huggingface.co/Qwen/Qwen2-VL-72B-Instruct-AWQ), [GPTQ-Int4](https://huggingface.co/Qwen/Qwen2-VL-72B-Instruct-GPTQ-Int4), [GPTQ-Int8](https://huggingface.co/Qwen/Qwen2-VL-72B-Instruct-GPTQ-Int8)] are now available. We have also released the [Qwen2-VL paper](https://arxiv.org/pdf/2409.12191) simultaneously.
55
+ * 2024.08.30: We have released the [Qwen2-VL series](https://huggingface.co/collections/Qwen/qwen2-vl-66cee7455501d7126940800d). The 2B and 7B models are now available, and the 72B model for open source is coming soon. For more details, please check our [blog](https://qwenlm.github.io/blog/qwen2-vl/)!
56
+
57
+
58
+ ## Performance
59
+
60
+
61
+
62
+ | Dataset | Qwen2.5-VL-3B<br><sup>([🤗](https://huggingface.co/Qwen/Qwen2.5-VL-3B-Instruct)[🤖](https://modelscope.cn/models/qwen/Qwen2.5-VL-3B-Instruct)) | Qwen2.5-VL-7B<br><sup>([🤗](https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct)[🤖](https://modelscope.cn/models/qwen/Qwen2.5-VL-7B-Instruct)) | Qwen2.5-VL-32B<br><sup>([🤗](https://huggingface.co/Qwen/Qwen2.5-VL-32B-Instruct)[🤖](https://modelscope.cn/models/qwen/Qwen2.5-VL-32B-Instruct)) | Qwen2.5-VL-72B<br><sup>([🤗](https://huggingface.co/Qwen/Qwen2.5-VL-72B-Instruct)[🤖](https://modelscope.cn/models/qwen/Qwen2.5-VL-72B-Instruct)) | Gemini-2 Flash | GPT-4o | Claude3.5 Sonnet | Qwen2-VL 72B |
63
+ |--------------------|--------|--------|--------|----------------|----------------|--------|------------------|--------------|
64
+ | MMMU | 53.1 | 58.6 | 70.0 | **70.2** | **70.7** | 70.3 | 70.4 | 64.5 |
65
+ | MMMU Pro | 31.6 | 38.3 |49.5|**51.1** | **57** | 54.5 | 54.7 | 46.2 |
66
+ | DocVQA | 93.9 | 95.7 | 94.8 |**96.4** | 92.1 | 91.1 | 95.2 | **96.5** |
67
+ | InfoVQA | 77.1 | 82.6 | 83.4| **87.3** | 77.8 | 80.7 | 74.3 | 84.5 |
68
+ | CC-OCR | 74.5 | 77.8 | 77.1 |**79.8** | 73.0 | 66.6 | 62.7 | 68.7 |
69
+ | OCRBenchV2 | 54.3/52.1 | 56.3/57.2 | 57.2/59.1| **61.5/63.7** | - | 46.5/32.3 | 45.2/39.6 | 47.8/46.1 |
70
+ | MegaBench | 28.9 |36.8 | - |**51.3** | **55.2** | 54.2 | 52.1 | 46.8 |
71
+ | MMStar | 55.8|63.9| 69.5 |**70.8** | 69.4 | 64.7 | 65.1 | 68.3 |
72
+ | MMBench1.1 | 81.5 | 84.3 | 84.6|**88.0** | 83.0 | 82.1 | 83.4 | 86.6 |
73
+ | MathVista | 62.3 | 68.2 | 74.7 |**74.8** | 73.1 | 63.8 | 65.4 | 70.5 |
74
+ | MathVision | 21.2 | 25.1 | **38.4** |38.1 | **41.3** | 30.4 | 38.3 | 25.9 |
75
+ | VideoMME | 61.5/67.6 | 65.1/71.6 | 70.5/77.9 |**73.3/79.1** | -/- | 71.9/77.2 | 60/62.9 | 71.2/77.8 |
76
+ | MMBench-Video | 1.63 | 1.79 | 1.93 |**2.02** | - | 1.68 | 1.38 | 1.7 |
77
+ | LVBench | 43.3 | 45.3 | **49.0** | 47.3 | - | 30.8 | - | - |
78
+ | CharadesSTA | 38.8 | 43.6 | **54.2** | 50.9 | - | 35.7 | - | - |
79
+ | AITZ | 76.9|81.9 | 83.1 |**83.2** | - | 35.3 | - | - |
80
+ | Android Control | 63.7/90.8 | 60.1/91.4 | 69.6/93.3 |**67.36/93.7** | - | - | - | 66.4/84.4 |
81
+ | ScreenSpot | 55.5|84.7| **88.5** | 87.1 | 84.0 | 18.1 | 83.0 | - |
82
+ | ScreenSpot Pro | 23.9|29.0 | 39.4 |**43.6** | - | - | 17.1 | - |
83
+ | AndroidWorld | - | - | 22.0 |**35** | - | 34.5(SoM) | 27.9 | - |
84
+ | OSWorld | - | - | 5.92 |**8.83** | - | 5.03 | **14.9** | - |
85
+
86
+
87
+
88
+
89
+
90
+ ## Quickstart
91
+
92
+ Below, we provide simple examples to show how to use Qwen2.5-VL with 🤖 ModelScope and 🤗 Transformers.
93
+
94
+ The code of Qwen2.5-VL has been in the latest Hugging face transformers and we advise you to build from source with command:
95
+ ```
96
+ pip install transformers==4.51.3 accelerate
97
+ ```
98
+ or you might encounter the following error:
99
+ ```
100
+ KeyError: 'qwen2_5_vl'
101
+ ```
102
+
103
+
104
+ We offer a toolkit to help you handle various types of visual input more conveniently, as if you were using an API. This includes base64, URLs, and interleaved images and videos. You can install it using the following command:
105
+
106
+ ```bash
107
+ # It's highly recommended to use `[decord]` feature for faster video loading.
108
+ pip install qwen-vl-utils[decord]
109
+ ```
110
+
111
+ Currently, `qwen-vl-utils` supports three video decoding backends: `torchvision`, `decord`, and `torchcodec`. While `decord` and `torchcodec` generally offer significantly faster decoding speeds compared to `torchvision`, we recommend using `torchcodec`. This is because `decord` has known issues, such as decoding hangs, and its project is no longer actively maintained.
112
+
113
+ - For `decord`, if you are not using Linux, you might not be able to install `decord` from PyPI. In that case, you can use `pip install qwen-vl-utils` which will fall back to using torchvision for video processing. However, you can still [install decord from source](https://github.com/dmlc/decord?tab=readme-ov-file#install-from-source) to get decord used when loading video.
114
+
115
+ - To use `torchcodec` as the backend for video decoding, follow the installation instructions provided in the official [torchcodec repository](https://github.com/pytorch/torchcodec/tree/main?tab=readme-ov-file#installing-torchcodec) and install it manually. Note that `torchcodec` depends on FFmpeg for decoding functionality.
116
+
117
+ ## Cookbooks
118
+
119
+ We are preparing [cookbooks](https://github.com/QwenLM/Qwen2.5-VL/tree/main/cookbooks) for many capabilities, including recognition, localization, document parsing, video understanding, key information extraction, and more. Welcome to learn more!
120
+
121
+ | Cookbook | Description | Open |
122
+ | -------- | ----------- | ---- |
123
+ | [Universal Recognition](https://github.com/QwenLM/Qwen2.5-VL/blob/main/cookbooks/universal_recognition.ipynb) | Not only identify animals, plants, people, and scenic spots but also recognize various objects such as cars and merchandise. | [![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/QwenLM/Qwen2.5-VL/blob/main/cookbooks/universal_recognition.ipynb) |
124
+ | [Powerful Document Parsing Capabilities](https://github.com/QwenLM/Qwen2.5-VL/blob/main/cookbooks/document_parsing.ipynb) | The parsing of documents has reached a higher level, including not only text but also layout position information and our Qwen HTML format. | [![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/QwenLM/Qwen2.5-VL/blob/main/cookbooks/document_parsing.ipynb) |
125
+ | [Precise Object Grounding Across Formats](https://github.com/QwenLM/Qwen2.5-VL/blob/main/cookbooks/spatial_understanding.ipynb) | Using absolute position coordinates, it supports both boxes and points, allowing for diverse combinations of positioning and labeling tasks. | [![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/QwenLM/Qwen2.5-VL/blob/main/cookbooks/spatial_understanding.ipynb) |
126
+ | [General OCR and Key Information Extraction](https://github.com/QwenLM/Qwen2.5-VL/blob/main/cookbooks/ocr.ipynb) | Stronger text recognition capabilities in natural scenes and multiple languages, supporting diverse key information extraction needs. | [![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/QwenLM/Qwen2.5-VL/blob/main/cookbooks/ocr.ipynb) |
127
+ | [Video Understanding](https://github.com/QwenLM/Qwen2.5-VL/blob/main/cookbooks/video_understanding.ipynb) | Better video OCR, long video understanding, and video grounding. | [![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/QwenLM/Qwen2.5-VL/blob/main/cookbooks/video_understanding.ipynb) |
128
+ | [Mobile Agent](https://github.com/QwenLM/Qwen2.5-VL/blob/main/cookbooks/mobile_agent.ipynb) | Locate and think for mobile phone control. | [![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/QwenLM/Qwen2.5-VL/blob/main/cookbooks/mobile_agent.ipynb) |
129
+ | [Computer-Use Agent](https://github.com/QwenLM/Qwen2.5-VL/blob/main/cookbooks/computer_use.ipynb) | Locate and think for controlling computers and Web. | [![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/QwenLM/Qwen2.5-VL/blob/main/cookbooks/computer_use.ipynb) |
130
+
131
+
132
+ ### Using 🤗 Transformers to Chat
133
+
134
+ Here we show a code snippet to show you how to use the chat model with `transformers` and `qwen_vl_utils`:
135
+
136
+ ```python
137
+ from transformers import Qwen2_5_VLForConditionalGeneration, AutoProcessor
138
+ from qwen_vl_utils import process_vision_info
139
+
140
+ # default: Load the model on the available device(s)
141
+ model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
142
+ "Qwen/Qwen2.5-VL-7B-Instruct", torch_dtype="auto", device_map="auto"
143
+ )
144
+
145
+ # We recommend enabling flash_attention_2 for better acceleration and memory saving, especially in multi-image and video scenarios.
146
+ # model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
147
+ # "Qwen/Qwen2.5-VL-7B-Instruct",
148
+ # torch_dtype=torch.bfloat16,
149
+ # attn_implementation="flash_attention_2",
150
+ # device_map="auto",
151
+ # )
152
+
153
+ # default processor
154
+ processor = AutoProcessor.from_pretrained("Qwen/Qwen2.5-VL-7B-Instruct")
155
+
156
+ # The default range for the number of visual tokens per image in the model is 4-16384.
157
+ # You can set min_pixels and max_pixels according to your needs, such as a token range of 256-1280, to balance performance and cost.
158
+ # min_pixels = 256*28*28
159
+ # max_pixels = 1280*28*28
160
+ # processor = AutoProcessor.from_pretrained("Qwen/Qwen2.5-VL-7B-Instruct", min_pixels=min_pixels, max_pixels=max_pixels)
161
+
162
+ messages = [
163
+ {
164
+ "role": "user",
165
+ "content": [
166
+ {
167
+ "type": "image",
168
+ "image": "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen-VL/assets/demo.jpeg",
169
+ },
170
+ {"type": "text", "text": "Describe this image."},
171
+ ],
172
+ }
173
+ ]
174
+
175
+ # Preparation for inference
176
+ text = processor.apply_chat_template(
177
+ messages, tokenize=False, add_generation_prompt=True
178
+ )
179
+ image_inputs, video_inputs = process_vision_info(messages)
180
+ inputs = processor(
181
+ text=[text],
182
+ images=image_inputs,
183
+ videos=video_inputs,
184
+ padding=True,
185
+ return_tensors="pt",
186
+ )
187
+ inputs = inputs.to(model.device)
188
+
189
+ # Inference: Generation of the output
190
+ generated_ids = model.generate(**inputs, max_new_tokens=128)
191
+ generated_ids_trimmed = [
192
+ out_ids[len(in_ids) :] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
193
+ ]
194
+ output_text = processor.batch_decode(
195
+ generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
196
+ )
197
+ print(output_text)
198
+ ```
199
+
200
+ <details>
201
+ <summary>Minimum VRAM requirements</summary>
202
+
203
+ | Precision | Qwen2.5-VL-3B | Qwen2.5-VL-7B | Qwen2.5-VL-72B |
204
+ |-----------|------------| --------- | -------- |
205
+ | FP32 | 11.5 GB | 26.34 GB | 266.21 GB |
206
+ | BF16 | 5.75 GB | 13.17 GB | 133.11 GB |
207
+ | INT8 | 2.87 GB | 6.59 GB | 66.5 GB |
208
+ | INT4 | 1.44 GB | 3.29 GB | 33.28 GB |
209
+
210
+ Note: The table above presents the theoretical minimum video memory requirements for inference with `transformers`; however, in practice, the actual memory usage is typically at least 1.2 times higher. For more information, see the linked resource [here](https://huggingface.co/docs/accelerate/main/en/usage_guides/model_size_estimator).
211
+ </details>
212
+
213
+
214
+ <details>
215
+ <summary>Multi image inference</summary>
216
+
217
+ ```python
218
+ # Messages containing multiple images and a text query
219
+ messages = [
220
+ {
221
+ "role": "user",
222
+ "content": [
223
+ {"type": "image", "image": "file:///path/to/image1.jpg"},
224
+ {"type": "image", "image": "file:///path/to/image2.jpg"},
225
+ {"type": "text", "text": "Identify the similarities between these images."},
226
+ ],
227
+ }
228
+ ]
229
+
230
+ # Preparation for inference
231
+ text = processor.apply_chat_template(
232
+ messages, tokenize=False, add_generation_prompt=True
233
+ )
234
+ image_inputs, video_inputs = process_vision_info(messages)
235
+ inputs = processor(
236
+ text=[text],
237
+ images=image_inputs,
238
+ videos=video_inputs,
239
+ padding=True,
240
+ return_tensors="pt",
241
+ )
242
+ inputs = inputs.to("cuda")
243
+
244
+ # Inference
245
+ generated_ids = model.generate(**inputs, max_new_tokens=128)
246
+ generated_ids_trimmed = [
247
+ out_ids[len(in_ids) :] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
248
+ ]
249
+ output_text = processor.batch_decode(
250
+ generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
251
+ )
252
+ print(output_text)
253
+ ```
254
+ </details>
255
+
256
+ <details>
257
+ <summary>Video inference</summary>
258
+
259
+ ```python
260
+ # Messages containing a images list as a video and a text query
261
+ messages = [
262
+ {
263
+ "role": "user",
264
+ "content": [
265
+ {
266
+ "type": "video",
267
+ "video": [
268
+ "file:///path/to/frame1.jpg",
269
+ "file:///path/to/frame2.jpg",
270
+ "file:///path/to/frame3.jpg",
271
+ "file:///path/to/frame4.jpg",
272
+ ],
273
+ },
274
+ {"type": "text", "text": "Describe this video."},
275
+ ],
276
+ }
277
+ ]
278
+
279
+ # Messages containing a local video path and a text query
280
+ messages = [
281
+ {
282
+ "role": "user",
283
+ "content": [
284
+ {
285
+ "type": "video",
286
+ "video": "file:///path/to/video1.mp4",
287
+ "max_pixels": 360 * 420,
288
+ "fps": 1.0,
289
+ },
290
+ {"type": "text", "text": "Describe this video."},
291
+ ],
292
+ }
293
+ ]
294
+
295
+ # Messages containing a video url and a text query
296
+ messages = [
297
+ {
298
+ "role": "user",
299
+ "content": [
300
+ {
301
+ "type": "video",
302
+ "video": "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen2-VL/space_woaudio.mp4",
303
+ "min_pixels": 4 * 28 * 28,
304
+ "max_pixels": 256 * 28 * 28,
305
+ "total_pixels": 20480 * 28 * 28,
306
+ },
307
+ {"type": "text", "text": "Describe this video."},
308
+ ],
309
+ }
310
+ ]
311
+
312
+ # Preparation for inference
313
+ text = processor.apply_chat_template(
314
+ messages, tokenize=False, add_generation_prompt=True
315
+ )
316
+ image_inputs, video_inputs, video_kwargs = process_vision_info(messages, return_video_kwargs=True)
317
+ inputs = processor(
318
+ text=[text],
319
+ images=image_inputs,
320
+ videos=video_inputs,
321
+ fps=fps,
322
+ padding=True,
323
+ return_tensors="pt",
324
+ **video_kwargs,
325
+ )
326
+ inputs = inputs.to("cuda")
327
+
328
+ # Inference
329
+ generated_ids = model.generate(**inputs, max_new_tokens=128)
330
+ generated_ids_trimmed = [
331
+ out_ids[len(in_ids) :] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
332
+ ]
333
+ output_text = processor.batch_decode(
334
+ generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
335
+ )
336
+ print(output_text)
337
+ ```
338
+
339
+ #### Video URL compatibility
340
+
341
+ Video URL compatibility is primarily determined by the version of the third-party library being used. For more details, refer to the table below. If you prefer not to use the default backend, you can switch it by setting `FORCE_QWENVL_VIDEO_READER` to `torchvision`, `decord`, or `torchcodec`.
342
+
343
+ | Backend | HTTP | HTTPS |
344
+ |-------------|------|-------|
345
+ | torchvision >= 0.19.0 | ✅ | ✅ |
346
+ | torchvision < 0.19.0 | ❌ | ❌ |
347
+ | decord | ✅ | ❌ |
348
+ | torchcodec | ✅ | ✅ |
349
+
350
+ #### Configuration for adjusting video resolution
351
+
352
+ We recommend setting appropriate values for the `min_pixels` and `max_pixels` parameters based on available GPU memory and the specific application scenario to restrict the resolution of individual frames in the video. Alternatively, you can use the `total_pixels` parameter to limit the total number of tokens in the video (it is recommended to set this value below 24576 * 28 * 28 to avoid excessively long input sequences). For more details on parameter usage and processing logic, please refer to the `fetch_video` function in `qwen_vl_utils/vision_process.py`.
353
+
354
+ </details>
355
+
356
+ <details>
357
+ <summary>Batch inference</summary>
358
+
359
+ ```python
360
+ # Sample messages for batch inference
361
+ messages1 = [
362
+ {
363
+ "role": "user",
364
+ "content": [
365
+ {"type": "image", "image": "file:///path/to/image1.jpg"},
366
+ {"type": "image", "image": "file:///path/to/image2.jpg"},
367
+ {"type": "text", "text": "What are the common elements in these pictures?"},
368
+ ],
369
+ }
370
+ ]
371
+ messages2 = [
372
+ {"role": "system", "content": "You are a helpful assistant."},
373
+ {"role": "user", "content": "Who are you?"},
374
+ ]
375
+ # Combine messages for batch processing
376
+ messages = [messages1, messages2]
377
+
378
+ # Preparation for batch inference
379
+ texts = [
380
+ processor.apply_chat_template(msg, tokenize=False, add_generation_prompt=True)
381
+ for msg in messages
382
+ ]
383
+ image_inputs, video_inputs = process_vision_info(messages)
384
+ inputs = processor(
385
+ text=texts,
386
+ images=image_inputs,
387
+ videos=video_inputs,
388
+ padding=True,
389
+ return_tensors="pt",
390
+ )
391
+ inputs = inputs.to("cuda")
392
+
393
+ # Batch Inference
394
+ generated_ids = model.generate(**inputs, max_new_tokens=128)
395
+ generated_ids_trimmed = [
396
+ out_ids[len(in_ids) :] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
397
+ ]
398
+ output_texts = processor.batch_decode(
399
+ generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
400
+ )
401
+ print(output_texts)
402
+ ```
403
+ </details>
404
+
405
+ ### 🤖 ModelScope
406
+ We strongly advise users especially those in mainland China to use ModelScope. `snapshot_download` can help you solve issues concerning downloading checkpoints.
407
+
408
+ ### More Usage Tips
409
+
410
+ For input images, we support local files, base64, and URLs. For videos, we currently only support local files.
411
+
412
+ ```python
413
+ # You can directly insert a local file path, a URL, or a base64-encoded image into the position where you want in the text.
414
+ ## Local file path
415
+ messages = [
416
+ {
417
+ "role": "user",
418
+ "content": [
419
+ {"type": "image", "image": "file:///path/to/your/image.jpg"},
420
+ {"type": "text", "text": "Describe this image."},
421
+ ],
422
+ }
423
+ ]
424
+ ## Image URL
425
+ messages = [
426
+ {
427
+ "role": "user",
428
+ "content": [
429
+ {"type": "image", "image": "http://path/to/your/image.jpg"},
430
+ {"type": "text", "text": "Describe this image."},
431
+ ],
432
+ }
433
+ ]
434
+ ## Base64 encoded image
435
+ messages = [
436
+ {
437
+ "role": "user",
438
+ "content": [
439
+ {"type": "image", "image": "data:image;base64,/9j/..."},
440
+ {"type": "text", "text": "Describe this image."},
441
+ ],
442
+ }
443
+ ]
444
+ ```
445
+ #### Image Resolution for performance boost
446
+
447
+ The model supports a wide range of resolution inputs. By default, it uses the native resolution for input, but higher resolutions can enhance performance at the cost of more computation. Users can set the minimum and maximum number of pixels to achieve an optimal configuration for their needs, such as a token count range of 256-1280, to balance speed and memory usage.
448
+
449
+ ```python
450
+ min_pixels = 256 * 28 * 28
451
+ max_pixels = 1280 * 28 * 28
452
+ processor = AutoProcessor.from_pretrained(
453
+ "Qwen/Qwen2.5-VL-7B-Instruct", min_pixels=min_pixels, max_pixels=max_pixels
454
+ )
455
+ ```
456
+
457
+ Besides, We provide two methods for fine-grained control over the image size input to the model:
458
+
459
+ 1. Specify exact dimensions: Directly set `resized_height` and `resized_width`. These values will be rounded to the nearest multiple of 28.
460
+
461
+ 2. Define min_pixels and max_pixels: Images will be resized to maintain their aspect ratio within the range of min_pixels and max_pixels.
462
+
463
+ ```python
464
+ # resized_height and resized_width
465
+ messages = [
466
+ {
467
+ "role": "user",
468
+ "content": [
469
+ {
470
+ "type": "image",
471
+ "image": "file:///path/to/your/image.jpg",
472
+ "resized_height": 280,
473
+ "resized_width": 420,
474
+ },
475
+ {"type": "text", "text": "Describe this image."},
476
+ ],
477
+ }
478
+ ]
479
+ # min_pixels and max_pixels
480
+ messages = [
481
+ {
482
+ "role": "user",
483
+ "content": [
484
+ {
485
+ "type": "image",
486
+ "image": "file:///path/to/your/image.jpg",
487
+ "min_pixels": 50176,
488
+ "max_pixels": 50176,
489
+ },
490
+ {"type": "text", "text": "Describe this image."},
491
+ ],
492
+ }
493
+ ]
494
+ ```
495
+
496
+ #### Add ids for Multiple Image Inputs
497
+ By default, images and video content are directly included in the conversation. When handling multiple images, it's helpful to add labels to the images and videos for better reference. Users can control this behavior with the following settings:
498
+ <details>
499
+ <summary>Add vision ids</summary>
500
+
501
+ ```python
502
+ conversation = [
503
+ {
504
+ "role": "user",
505
+ "content": [{"type": "image"}, {"type": "text", "text": "Hello, how are you?"}],
506
+ },
507
+ {
508
+ "role": "assistant",
509
+ "content": "I'm doing well, thank you for asking. How can I assist you today?",
510
+ },
511
+ {
512
+ "role": "user",
513
+ "content": [
514
+ {"type": "text", "text": "Can you describe these images and video?"},
515
+ {"type": "image"},
516
+ {"type": "image"},
517
+ {"type": "video"},
518
+ {"type": "text", "text": "These are from my vacation."},
519
+ ],
520
+ },
521
+ {
522
+ "role": "assistant",
523
+ "content": "I'd be happy to describe the images and video for you. Could you please provide more context about your vacation?",
524
+ },
525
+ {
526
+ "role": "user",
527
+ "content": "It was a trip to the mountains. Can you see the details in the images and video?",
528
+ },
529
+ ]
530
+
531
+ # default:
532
+ prompt_without_id = processor.apply_chat_template(
533
+ conversation, add_generation_prompt=True
534
+ )
535
+ # Excepted output: '<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n<|im_start|>user\n<|vision_start|><|image_pad|><|vision_end|>Hello, how are you?<|im_end|>\n<|im_start|>assistant\nI'm doing well, thank you for asking. How can I assist you today?<|im_end|>\n<|im_start|>user\nCan you describe these images and video?<|vision_start|><|image_pad|><|vision_end|><|vision_start|><|image_pad|><|vision_end|><|vision_start|><|video_pad|><|vision_end|>These are from my vacation.<|im_end|>\n<|im_start|>assistant\nI'd be happy to describe the images and video for you. Could you please provide more context about your vacation?<|im_end|>\n<|im_start|>user\nIt was a trip to the mountains. Can you see the details in the images and video?<|im_end|>\n<|im_start|>assistant\n'
536
+
537
+
538
+ # add ids
539
+ prompt_with_id = processor.apply_chat_template(
540
+ conversation, add_generation_prompt=True, add_vision_id=True
541
+ )
542
+ # Excepted output: '<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n<|im_start|>user\nPicture 1: <|vision_start|><|image_pad|><|vision_end|>Hello, how are you?<|im_end|>\n<|im_start|>assistant\nI'm doing well, thank you for asking. How can I assist you today?<|im_end|>\n<|im_start|>user\nCan you describe these images and video?Picture 2: <|vision_start|><|image_pad|><|vision_end|>Picture 3: <|vision_start|><|image_pad|><|vision_end|>Video 1: <|vision_start|><|video_pad|><|vision_end|>These are from my vacation.<|im_end|>\n<|im_start|>assistant\nI'd be happy to describe the images and video for you. Could you please provide more context about your vacation?<|im_end|>\n<|im_start|>user\nIt was a trip to the mountains. Can you see the details in the images and video?<|im_end|>\n<|im_start|>assistant\n'
543
+ ```
544
+ </details>
545
+
546
+ #### Flash-Attention 2 to speed up generation
547
+
548
+ First, make sure to install the latest version of Flash Attention 2:
549
+
550
+ ```bash
551
+ pip install -U flash-attn --no-build-isolation
552
+ ```
553
+
554
+ Also, you should have a hardware that is compatible with Flash-Attention 2. Read more about it in the official documentation of the [flash attention repository](https://github.com/Dao-AILab/flash-attention). FlashAttention-2 can only be used when a model is loaded in `torch.float16` or `torch.bfloat16`.
555
+
556
+ To load and run a model using Flash Attention-2, simply add `attn_implementation="flash_attention_2"` when loading the model as follows:
557
+
558
+ ```python
559
+ from transformers import Qwen2_5_VLForConditionalGeneration
560
+
561
+ model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
562
+ "Qwen/Qwen2.5-VL-7B-Instruct",
563
+ torch_dtype=torch.bfloat16,
564
+ attn_implementation="flash_attention_2",
565
+ )
566
+ ```
567
+
568
+ ### Processing Long Texts
569
+
570
+ The current `config.json` is set for context length up to 32,768 tokens.
571
+ To handle extensive inputs exceeding 32,768 tokens, we utilize [YaRN](https://arxiv.org/abs/2309.00071), a technique for enhancing model length extrapolation, ensuring optimal performance on lengthy texts.
572
+
573
+ For supported frameworks, you could add the following to `config.json` to enable YaRN:
574
+
575
+ ```
576
+ {
577
+ ...,
578
+ "type": "yarn",
579
+ "mrope_section": [
580
+ 16,
581
+ 24,
582
+ 24
583
+ ],
584
+ "factor": 4,
585
+ "original_max_position_embeddings": 32768
586
+ }
587
+ ```
588
+
589
+ However, it should be noted that this method has a significant impact on the performance of temporal and spatial localization tasks, and is therefore not recommended for use.
590
+
591
+ At the same time, for long video inputs, since MRoPE itself is more economical with ids, the max_position_embeddings can be directly modified to a larger value, such as 64k.
592
+
593
+
594
+ ### Try Qwen2.5-VL-72B with API!
595
+
596
+ To explore Qwen2.5-VL-72B, a more fascinating multimodal model, we encourage you to test our cutting-edge API service. Let's start the exciting journey right now!
597
+
598
+ #### Installation
599
+ ```bash
600
+ pip install dashscope
601
+ ```
602
+
603
+ #### Examples
604
+ ```python
605
+ import dashscope
606
+
607
+
608
+ dashscope.api_key = "your_api_key"
609
+
610
+ messages = [{
611
+ 'role': 'user',
612
+ 'content': [
613
+ {
614
+ 'image': "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg"
615
+ },
616
+ {
617
+ 'text': 'What are in the image?'
618
+ },
619
+ ]
620
+ }]
621
+
622
+ response = dashscope.MultiModalConversation.call(model='qwen2.5-vl-72b-instruct', messages=messages)
623
+ print(response)
624
+ ```
625
+
626
+ For more usage, please refer to the tutorial at [aliyun](https://help.aliyun.com/zh/model-studio/developer-reference/qwen-vl-api).
627
+
628
+
629
+
630
+
631
+ ## Demo
632
+ ### Web UI Example
633
+
634
+ In this section, we provide instructions for users to build a web-based user interface (UI) demo. This UI demo allows users to interact with a predefined model or application through a web browser. Follow the steps below to get started.
635
+
636
+ #### Installation
637
+
638
+ Before you begin, ensure that you have the required dependencies installed on your system. You can install them by running the following command:
639
+
640
+ ```bash
641
+ pip install -r requirements_web_demo.txt
642
+ ```
643
+
644
+ #### Running the Demo with FlashAttention-2
645
+
646
+ Once the required packages are installed, you can launch the web demo using the following command. This command will start a web server and provide you with a link to access the UI in your web browser.
647
+
648
+ **Recommended**: For enhanced performance and efficiency, especially in multi-image and video processing scenarios, we strongly recommend using [FlashAttention-2](https://github.com/Dao-AILab/flash-attention). FlashAttention-2 provides significant improvements in memory usage and speed, making it ideal for handling large-scale models and data processing.
649
+
650
+ To enable FlashAttention-2, use the following command:
651
+
652
+ ```bash
653
+ python web_demo_mm.py --flash-attn2
654
+ ```
655
+
656
+ This will load the model with FlashAttention-2 enabled.
657
+
658
+ **Default Usage**: If you prefer to run the demo without FlashAttention-2 or if you do not specify the `--flash-attn2` option, the demo will load the model using the standard attention implementation:
659
+
660
+ ```bash
661
+ python web_demo_mm.py
662
+ ```
663
+
664
+ After running the command, you’ll see a link generated in the terminal similar to this:
665
+
666
+ ```
667
+ Running on local: http://127.0.0.1:7860/
668
+ ```
669
+
670
+ Copy this link and paste it into your browser to access the web UI, where you can interact with the model by inputting text, uploading images, or using any other provided functionalities.
671
+
672
+ ##### Running the Streaming Video Chat Demo
673
+ An experimental streaming video chat demo is also available in the ``web_demo_streaming`` directory.
674
+
675
+ To run the streaming video chat demo, use the following command:
676
+
677
+ ```bash
678
+ cd web_demo_streaming/
679
+ python app.py --flash-attn2
680
+ ```
681
+
682
+ If you prefer to run the demo without FlashAttention-2, use the following command:
683
+ ```bash
684
+ cd web_demo_streaming/
685
+ python app.py
686
+ ```
687
+
688
+ This demo supports webcam/screen capture as its video input source. To support screen capture video input, we use code snippet from the following hugginface space: [gstaff/gradio-screen-recorder](https://huggingface.co/spaces/gstaff/gradio-screen-recorder/tree/main).
689
+
690
+
691
+ ## Deployment
692
+
693
+ We recommend using vLLM for fast Qwen2.5-VL deployment and inference. You need to install `vllm>0.7.2` to enable Qwen2.5-VL support. You can also use our [official docker image](#-docker).
694
+
695
+ You can also check [vLLM official documentation](https://docs.vllm.ai/en/latest/serving/multimodal_inputs.html) for more details about online serving and offline inference.
696
+
697
+ ### Installation
698
+ ```bash
699
+ pip install git+https://github.com/huggingface/transformers@f3f6c86582611976e72be054675e2bf0abb5f775
700
+ pip install accelerate
701
+ pip install qwen-vl-utils
702
+ pip install 'vllm>0.7.2'
703
+
704
+ ```
705
+ ### Start an OpenAI API Service
706
+
707
+ Run the command below to start an OpenAI-compatible API service:
708
+
709
+ ```bash
710
+ vllm serve Qwen/Qwen2.5-VL-7B-Instruct --port 8000 --host 0.0.0.0 --dtype bfloat16 --limit-mm-per-prompt image=5,video=5
711
+ ```
712
+
713
+ Then you can use the chat API as below (via curl or Python API):
714
+
715
+ ```bash
716
+ curl http://localhost:8000/v1/chat/completions \
717
+ -H "Content-Type: application/json" \
718
+ -d '{
719
+ "model": "Qwen/Qwen2.5-VL-7B-Instruct",
720
+ "messages": [
721
+ {"role": "system", "content": "You are a helpful assistant."},
722
+ {"role": "user", "content": [
723
+ {"type": "image_url", "image_url": {"url": "https://modelscope.oss-cn-beijing.aliyuncs.com/resource/qwen.png"}},
724
+ {"type": "text", "text": "What is the text in the illustrate?"}
725
+ ]}
726
+ ]
727
+ }'
728
+ ```
729
+
730
+ ```python
731
+ from openai import OpenAI
732
+
733
+ # Set OpenAI's API key and API base to use vLLM's API server.
734
+ openai_api_key = "EMPTY"
735
+ openai_api_base = "http://localhost:8000/v1"
736
+
737
+ client = OpenAI(
738
+ api_key=openai_api_key,
739
+ base_url=openai_api_base,
740
+ )
741
+
742
+ chat_response = client.chat.completions.create(
743
+ model="Qwen/Qwen2.5-VL-7B-Instruct",
744
+ messages=[
745
+ {"role": "system", "content": "You are a helpful assistant."},
746
+ {
747
+ "role": "user",
748
+ "content": [
749
+ {
750
+ "type": "image_url",
751
+ "image_url": {
752
+ "url": "https://modelscope.oss-cn-beijing.aliyuncs.com/resource/qwen.png"
753
+ },
754
+ },
755
+ {"type": "text", "text": "What is the text in the illustrate?"},
756
+ ],
757
+ },
758
+ ],
759
+ )
760
+ print("Chat response:", chat_response)
761
+ ```
762
+
763
+ You can also upload base64-encoded local images (see [OpenAI API protocol document](https://platform.openai.com/docs/guides/vision/uploading-base-64-encoded-images) for more details):
764
+ ```python
765
+ import base64
766
+ from openai import OpenAI
767
+ # Set OpenAI's API key and API base to use vLLM's API server.
768
+ openai_api_key = "EMPTY"
769
+ openai_api_base = "http://localhost:8000/v1"
770
+ client = OpenAI(
771
+ api_key=openai_api_key,
772
+ base_url=openai_api_base,
773
+ )
774
+ image_path = "/path/to/local/image.png"
775
+ with open(image_path, "rb") as f:
776
+ encoded_image = base64.b64encode(f.read())
777
+ encoded_image_text = encoded_image.decode("utf-8")
778
+ base64_qwen = f"data:image;base64,{encoded_image_text}"
779
+ chat_response = client.chat.completions.create(
780
+ model="Qwen/Qwen2.5-VL-7B-Instruct",
781
+ messages=[
782
+ {"role": "system", "content": "You are a helpful assistant."},
783
+ {
784
+ "role": "user",
785
+ "content": [
786
+ {
787
+ "type": "image_url",
788
+ "image_url": {
789
+ "url": base64_qwen
790
+ },
791
+ },
792
+ {"type": "text", "text": "What is the text in the illustrate?"},
793
+ ],
794
+ },
795
+ ],
796
+ )
797
+ print("Chat response:", chat_response)
798
+ ```
799
+
800
+ For videos, you can use the chat API as follows:
801
+ ```python
802
+ import base64
803
+ import numpy as np
804
+ from PIL import Image
805
+ from io import BytesIO
806
+ from openai import OpenAI
807
+ from qwen_vl_utils import process_vision_info
808
+
809
+
810
+ # Set OpenAI's API key and API base to use vLLM's API server.
811
+ openai_api_key = "EMPTY"
812
+ openai_api_base = "http://localhost:8000/v1"
813
+
814
+ client = OpenAI(
815
+ api_key=openai_api_key,
816
+ base_url=openai_api_base,
817
+ )
818
+
819
+
820
+ video_messages = [
821
+ {"role": "system", "content": "You are a helpful assistant."},
822
+ {"role": "user", "content": [
823
+ {"type": "text", "text": "请用表格总结一下视频中的商品特点"},
824
+ {
825
+ "type": "video",
826
+ "video": "https://duguang-labelling.oss-cn-shanghai.aliyuncs.com/qiansun/video_ocr/videos/50221078283.mp4",
827
+ "total_pixels": 20480 * 28 * 28, "min_pixels": 16 * 28 * 2,
828
+ 'fps': 3.0 # The default value is 2.0, but for demonstration purposes, we set it to 3.0.
829
+ }]
830
+ },
831
+ ]
832
+
833
+
834
+ def prepare_message_for_vllm(content_messages):
835
+ """
836
+ The frame extraction logic for videos in `vLLM` differs from that of `qwen_vl_utils`.
837
+ Here, we utilize `qwen_vl_utils` to extract video frames, with the `media_typ`e of the video explicitly set to `video/jpeg`.
838
+ By doing so, vLLM will no longer attempt to extract frames from the input base64-encoded images.
839
+ """
840
+ vllm_messages, fps_list = [], []
841
+ for message in content_messages:
842
+ message_content_list = message["content"]
843
+ if not isinstance(message_content_list, list):
844
+ vllm_messages.append(message)
845
+ continue
846
+
847
+ new_content_list = []
848
+ for part_message in message_content_list:
849
+ if 'video' in part_message:
850
+ video_message = [{'content': [part_message]}]
851
+ image_inputs, video_inputs, video_kwargs = process_vision_info(video_message, return_video_kwargs=True)
852
+ assert video_inputs is not None, "video_inputs should not be None"
853
+ video_input = (video_inputs.pop()).permute(0, 2, 3, 1).numpy().astype(np.uint8)
854
+ fps_list.extend(video_kwargs.get('fps', []))
855
+
856
+ # encode image with base64
857
+ base64_frames = []
858
+ for frame in video_input:
859
+ img = Image.fromarray(frame)
860
+ output_buffer = BytesIO()
861
+ img.save(output_buffer, format="jpeg")
862
+ byte_data = output_buffer.getvalue()
863
+ base64_str = base64.b64encode(byte_data).decode("utf-8")
864
+ base64_frames.append(base64_str)
865
+
866
+ part_message = {
867
+ "type": "video_url",
868
+ "video_url": {"url": f"data:video/jpeg;base64,{','.join(base64_frames)}"}
869
+ }
870
+ new_content_list.append(part_message)
871
+ message["content"] = new_content_list
872
+ vllm_messages.append(message)
873
+ return vllm_messages, {'fps': fps_list}
874
+
875
+
876
+ video_messages, video_kwargs = prepare_message_for_vllm(video_messages)
877
+ chat_response = client.chat.completions.create(
878
+ model="Qwen/Qwen2.5-VL-7B-Instruct",
879
+ messages=video_messages,
880
+ extra_body={
881
+ "mm_processor_kwargs": video_kwargs
882
+ }
883
+ )
884
+ print("Chat response:", chat_response)
885
+ ```
886
+
887
+ ### Inference Locally
888
+
889
+ You can also use vLLM to inference Qwen2.5-VL locally:
890
+
891
+ ```python
892
+ from transformers import AutoProcessor
893
+ from vllm import LLM, SamplingParams
894
+ from qwen_vl_utils import process_vision_info
895
+
896
+ MODEL_PATH = "Qwen/Qwen2.5-VL-7B-Instruct"
897
+
898
+ llm = LLM(
899
+ model=MODEL_PATH,
900
+ limit_mm_per_prompt={"image": 10, "video": 10},
901
+ )
902
+
903
+ sampling_params = SamplingParams(
904
+ temperature=0.1,
905
+ top_p=0.001,
906
+ repetition_penalty=1.05,
907
+ max_tokens=256,
908
+ stop_token_ids=[],
909
+ )
910
+
911
+ image_messages = [
912
+ {"role": "system", "content": "You are a helpful assistant."},
913
+ {
914
+ "role": "user",
915
+ "content": [
916
+ {
917
+ "type": "image",
918
+ "image": "https://modelscope.oss-cn-beijing.aliyuncs.com/resource/qwen.png",
919
+ "min_pixels": 224 * 224,
920
+ "max_pixels": 1280 * 28 * 28,
921
+ },
922
+ {"type": "text", "text": "What is the text in the illustrate?"},
923
+ ],
924
+ },
925
+ ]
926
+
927
+
928
+ # For video input, you can pass following values instead:
929
+ # "type": "video",
930
+ # "video": "<video URL>",
931
+ video_messages = [
932
+ {"role": "system", "content": "You are a helpful assistant."},
933
+ {"role": "user", "content": [
934
+ {"type": "text", "text": "请用表格总结一下视频中的商品特点"},
935
+ {
936
+ "type": "video",
937
+ "video": "https://duguang-labelling.oss-cn-shanghai.aliyuncs.com/qiansun/video_ocr/videos/50221078283.mp4",
938
+ "total_pixels": 20480 * 28 * 28, "min_pixels": 16 * 28 * 28
939
+ }
940
+ ]
941
+ },
942
+ ]
943
+
944
+ # Here we use video messages as a demonstration
945
+ messages = video_messages
946
+
947
+ processor = AutoProcessor.from_pretrained(MODEL_PATH)
948
+ prompt = processor.apply_chat_template(
949
+ messages,
950
+ tokenize=False,
951
+ add_generation_prompt=True,
952
+ )
953
+ image_inputs, video_inputs, video_kwargs = process_vision_info(messages, return_video_kwargs=True)
954
+
955
+ mm_data = {}
956
+ if image_inputs is not None:
957
+ mm_data["image"] = image_inputs
958
+ if video_inputs is not None:
959
+ mm_data["video"] = video_inputs
960
+
961
+ llm_inputs = {
962
+ "prompt": prompt,
963
+ "multi_modal_data": mm_data,
964
+
965
+ # FPS will be returned in video_kwargs
966
+ "mm_processor_kwargs": video_kwargs,
967
+ }
968
+
969
+ outputs = llm.generate([llm_inputs], sampling_params=sampling_params)
970
+ generated_text = outputs[0].outputs[0].text
971
+
972
+ print(generated_text)
973
+ ```
974
+
975
+
976
+ ## 🐳 Docker
977
+
978
+ To simplify the deploy process, we provide docker images with pre-build environments: [qwenllm/qwenvl](https://hub.docker.com/r/qwenllm/qwenvl). You only need to install the driver and download model files to launch demos.
979
+
980
+ ```bash
981
+ docker run --gpus all --ipc=host --network=host --rm --name qwen2.5 -it qwenllm/qwenvl:2.5-cu121 bash
982
+ ```
983
+
984
+ ## Citation
985
+
986
+ If you find our paper and code useful in your research, please consider giving a star :star: and citation :pencil: :)
987
+
988
+
989
+
990
+
991
+ ```BibTeX
992
+
993
+ @article{Qwen2.5-VL,
994
+ title={Qwen2.5-VL Technical Report},
995
+ author={Bai, Shuai and Chen, Keqin and Liu, Xuejing and Wang, Jialin and Ge, Wenbin and Song, Sibo and Dang, Kai and Wang, Peng and Wang, Shijie and Tang, Jun and Zhong, Humen and Zhu, Yuanzhi and Yang, Mingkun and Li, Zhaohai and Wan, Jianqiang and Wang, Pengfei and Ding, Wei and Fu, Zheren and Xu, Yiheng and Ye, Jiabo and Zhang, Xi and Xie, Tianbao and Cheng, Zesen and Zhang, Hang and Yang, Zhibo and Xu, Haiyang and Lin, Junyang},
996
+ journal={arXiv preprint arXiv:2502.13923},
997
+ year={2025}
998
+ }
999
+
1000
+ @article{Qwen2-VL,
1001
+ title={Qwen2-VL: Enhancing Vision-Language Model's Perception of the World at Any Resolution},
1002
+ author={Wang, Peng and Bai, Shuai and Tan, Sinan and Wang, Shijie and Fan, Zhihao and Bai, Jinze and Chen, Keqin and Liu, Xuejing and Wang, Jialin and Ge, Wenbin and Fan, Yang and Dang, Kai and Du, Mengfei and Ren, Xuancheng and Men, Rui and Liu, Dayiheng and Zhou, Chang and Zhou, Jingren and Lin, Junyang},
1003
+ journal={arXiv preprint arXiv:2409.12191},
1004
+ year={2024}
1005
+ }
1006
+
1007
+ @article{Qwen-VL,
1008
+ title={Qwen-VL: A Versatile Vision-Language Model for Understanding, Localization, Text Reading, and Beyond},
1009
+ author={Bai, Jinze and Bai, Shuai and Yang, Shusheng and Wang, Shijie and Tan, Sinan and Wang, Peng and Lin, Junyang and Zhou, Chang and Zhou, Jingren},
1010
+ journal={arXiv preprint arXiv:2308.12966},
1011
+ year={2023}
1012
+ }
1013
+ ```
1014
+
1015
+ <br>
QWENVL-PRIVATE/analysis/aaten_debug.py ADDED
@@ -0,0 +1,393 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import torch
3
+ import numpy as np
4
+ import matplotlib.pyplot as plt
5
+ from transformers import AutoProcessor
6
+ from qwen_src.qwen3_vl import Qwen3VLForConditionalGeneration
7
+ from qwen_src.qwen3_vl.aura_attention import AuraDecoderLayer
8
+ import json
9
+ import os
10
+ from tqdm import tqdm
11
+ from PIL import Image, ImageDraw, ImageFont
12
+ from qwen_src.mm_utils import expand2square, create_pseudo_labels
13
+ from qwen_src.ana_utils import register_hooks, get_attn_weights_map, visualize_token_wise_attention
14
+ import matplotlib.patches as patches
15
+ from matplotlib.colors import ListedColormap, BoundaryNorm
16
+ from analysis.denoise_textual_file_wdet import process_single_sample_packed
17
+ PATCH_SIZE = 32 #32x32 patches For Qwen3-VL
18
+ import pickle
19
+ use_act = True
20
+ import math
21
+
22
+ def plot_pseudo_label(image_path, layer2_attn, grounding_attn_o2i, grounding_attn, is_sink_token = None,
23
+ save_path=None, figsize=(15, 4), fg_bbox = None):
24
+ """
25
+ Plot image and attention maps.
26
+
27
+ Args:
28
+ image_path: str, path to the image file
29
+ layer2_attn: ndarray (576,), layer 2 attention weights
30
+ grounding_attn_o2i: ndarray (576,), grounding attention object to image
31
+ grounding_attn: ndarray (576,), grounding attention
32
+ is_sink_token: ndarray (576,) in bool type to indicate whether this token is a sink token
33
+ save_path: str, optional path to save the plot
34
+ figsize: tuple, figure size
35
+ """
36
+ # Create figure with subplots
37
+ fig, axes = plt.subplots(1, 4, figsize=figsize, dpi=200)
38
+
39
+ # Plot 1: Original Image
40
+ if os.path.exists(image_path):
41
+ img = Image.open(image_path)
42
+ axes[0].imshow(img)
43
+ axes[0].set_title('Original Image')
44
+ axes[0].axis('off')
45
+ else:
46
+ axes[0].text(0.5, 0.5, f'Image not found:\n{image_path}',
47
+ ha='center', va='center', transform=axes[0].transAxes)
48
+ axes[0].set_title('Original Image')
49
+ axes[0].axis('off')
50
+
51
+ # Reshape attention arrays to 24x24
52
+ if layer2_attn.ndim == 1:
53
+ square_size_hw = int(math.sqrt(len(layer2_attn.flatten())))
54
+ square_size = (square_size_hw, square_size_hw)
55
+ else:
56
+ square_size = layer2_attn.shape
57
+ layer2_attn_2d = layer2_attn.reshape(square_size)
58
+ grounding_attn_o2i_2d = grounding_attn_o2i.reshape(square_size)
59
+ grounding_attn_2d = grounding_attn.reshape(square_size)
60
+ if is_sink_token is not None:
61
+ is_sink_token_2d = is_sink_token.reshape(square_size)
62
+
63
+
64
+ # Plot 2: Special handling for Pseudo-Label map vs. continuous attention
65
+ # Check if the data is likely discrete pseudo-labels (-1, 0, 1)
66
+ if -1 in layer2_attn:
67
+ # --- Discrete Colormap for Pseudo-Labels ---
68
+ axes[1].set_title('Pseudo Label Map')
69
+ # Define the colors: Ignore (-1)->Gray, Background (0)->Dark Purple, Foreground (1)->Yellow
70
+ colors = ['gray', 'black', '#FFE699']
71
+ cmap_discrete = ListedColormap(colors)
72
+ # Define the boundaries for the colormap: [-1.5, -0.5), [-0.5, 0.5), [0.5, 1.5]
73
+ boundaries = [-1.5, -0.5, 0.5, 1.5]
74
+ norm = BoundaryNorm(boundaries, cmap_discrete.N)
75
+ im1 = axes[1].imshow(layer2_attn_2d, cmap=cmap_discrete, norm=norm, aspect='auto')
76
+ # Customize the colorbar for discrete values
77
+ cbar = plt.colorbar(im1, ax=axes[1], shrink=0.6)
78
+ cbar.set_ticks([-1, 0, 1])
79
+ cbar.set_ticklabels(['Ignore', 'BG', 'FG'])
80
+ else:
81
+ # --- Continuous Colormap for standard attention ---
82
+ axes[1].set_title('Layer 2 Attention')
83
+ im1 = axes[1].imshow(layer2_attn_2d, cmap='viridis', aspect='auto')
84
+ plt.colorbar(im1, ax=axes[1], shrink=0.6)
85
+
86
+ # Plot 3: Grounding Attention O2I
87
+ im2 = axes[2].imshow(grounding_attn_o2i_2d, cmap='inferno', aspect='auto')
88
+ axes[2].set_title('Grounding Attn O2I')
89
+ axes[2].axis('off')
90
+ plt.colorbar(im2, ax=axes[2], shrink=0.6)
91
+
92
+ # Plot 4: Grounding Attention Q2I
93
+ im3 = axes[3].imshow(grounding_attn_2d, cmap='inferno', aspect='auto')
94
+ axes[3].set_title('Grounding Attn')
95
+ axes[3].axis('off')
96
+ plt.colorbar(im3, ax=axes[3], shrink=0.6)
97
+
98
+ # Highlight sink tokens on the attention maps
99
+ if is_sink_token is not None:
100
+ # Find the coordinates of all sink tokens
101
+ sink_coords = np.argwhere(is_sink_token_2d)
102
+ for row, col in sink_coords:
103
+ # Define the properties of the highlight box
104
+ rect_args = {
105
+ 'xy': (col - 0.5, row - 0.5), # Bottom-left corner of the cell
106
+ 'width': 1,
107
+ 'height': 1,
108
+ 'linewidth': 2.5,
109
+ 'edgecolor': '#7CFC00', # A bright, "LawnGreen" color
110
+ 'facecolor': 'none'
111
+ }
112
+ # Add a new rectangle patch to each of the three attention plots
113
+ #axes[1].add_patch(patches.Rectangle(**rect_args))
114
+ axes[2].add_patch(patches.Rectangle(**rect_args))
115
+ #axes[3].add_patch(patches.Rectangle(**rect_args))
116
+
117
+ if fg_bbox is not None:
118
+ fg_rect_arg = {
119
+ 'xy': (fg_bbox[1] - 0.5, fg_bbox[0] - 0.5), # Bottom-left corner of the cell
120
+ 'width': fg_bbox[3] - fg_bbox[1] + 1,
121
+ 'height': fg_bbox[2] - fg_bbox[0] + 1,
122
+ 'linewidth': 2.5,
123
+ 'edgecolor': '#FF4500', # A bright, "OrangeRed" color
124
+ 'facecolor': 'none'
125
+ }
126
+ axes[1].add_patch(patches.Rectangle(**fg_rect_arg))
127
+
128
+ plt.tight_layout()
129
+
130
+ if save_path:
131
+ plt.savefig(save_path, dpi=300, bbox_inches='tight')
132
+
133
+ plt.show()
134
+ pass
135
+
136
+ def replace_with_aura_layers(model, start_layer, num_layers):
137
+ """
138
+ Replace model.model.language_model.layers[start..start+num-1] in-place
139
+ with AuraDecoderLayer instances initialized from the original layers.
140
+
141
+ Since roi_mask=None is used during inference, forward output is
142
+ bit-for-bit identical to the original (Path A in AuraAttention).
143
+ """
144
+ layers = model.model.language_model.layers
145
+ total_layers = len(layers)
146
+ assert start_layer + num_layers <= total_layers, (
147
+ f"Cannot replace layers {start_layer}..{start_layer + num_layers - 1}: "
148
+ f"model only has {total_layers} layers"
149
+ )
150
+
151
+ print(f"Replacing layers {start_layer}..{start_layer + num_layers - 1} with AuraDecoderLayer")
152
+ for i in range(num_layers):
153
+ idx = start_layer + i
154
+ original_layer = layers[idx]
155
+ aura_layer = AuraDecoderLayer(model.config.text_config, layer_idx=idx)
156
+ missing = aura_layer.load_from_standard_layer(original_layer)
157
+ print(f" AuraLayer[{i}] <- layers[{idx}] (new params: {missing})")
158
+ # Move to same device/dtype as original
159
+ aura_layer = aura_layer.to(
160
+ device=next(original_layer.parameters()).device,
161
+ dtype=next(original_layer.parameters()).dtype,
162
+ )
163
+ layers[idx] = aura_layer
164
+ print("Layer replacement complete.")
165
+
166
+
167
+ def eval_model_qwen(model, processor, questions, image_folder,
168
+ ocr_cache_file=None, is_natural_image=False, save_path=None, debug_samples=10):
169
+
170
+ sample = 0
171
+
172
+ ocr_cache = {}
173
+ if ocr_cache_file and os.path.exists(ocr_cache_file):
174
+ print(f"Loading existing OCR cache from {ocr_cache_file}...")
175
+ try:
176
+ with open(ocr_cache_file, 'rb') as f:
177
+ ocr_cache = pickle.load(f)
178
+ print(f"Loaded {len(ocr_cache)} cached entries.")
179
+ except Exception as e:
180
+ print(f"Failed to load cache: {e}. Starting fresh.")
181
+
182
+
183
+ for line in tqdm(questions[sample:]):
184
+ if use_act: activations.clear()
185
+ image_file = line["image"]
186
+ qs = line["text"] if 'text' in line else line['prompt']
187
+
188
+ if is_natural_image:
189
+ qs = qs.replace('Answer the question using a single word or phrase.',
190
+ 'Output grounding bounding box related to the question in JSON.')
191
+ else:
192
+ if 'Answer the question using a single word or phrase.' not in qs:
193
+ qs += ' Answer the question using a single word or phrase.'
194
+
195
+ image = Image.open(os.path.join(image_folder, image_file)).convert('RGB')
196
+ processor.image_processor.max_pixels = 576 * PATCH_SIZE * PATCH_SIZE
197
+ processor.image_processor.min_pixels = 576 * PATCH_SIZE * PATCH_SIZE
198
+ image_size = image.size
199
+ image = expand2square(image, (127, 127, 127))
200
+ messages = [
201
+ {
202
+ "role": "user",
203
+ "content": [
204
+ {
205
+ "type": "text",
206
+ "text": f"{qs}"
207
+ },
208
+ {
209
+ "type": "image",
210
+ "image": image
211
+ }
212
+ ]
213
+ }
214
+ ]
215
+ # Preparation for inference
216
+ inputs = processor.apply_chat_template(
217
+ messages,
218
+ tokenize=True,
219
+ add_generation_prompt=True,
220
+ return_dict=True,
221
+ return_tensors="pt"
222
+ )
223
+ inputs = inputs.to(model.device)
224
+
225
+ # Inference: Generation of the output
226
+ generated_ids = model.generate(**inputs, max_new_tokens=128, output_attentions=True)
227
+ generated_ids_trimmed = [
228
+ out_ids[len(in_ids):] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
229
+ ]
230
+ output_text = processor.batch_decode(
231
+ generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
232
+ )
233
+ print("Question:", qs)
234
+ print(output_text)
235
+
236
+ if use_act and activations.get('visual_mask', None) is None:
237
+ vision_start_token_id = processor.tokenizer.convert_tokens_to_ids('<|vision_start|>')
238
+ vision_end_token_id = processor.tokenizer.convert_tokens_to_ids('<|vision_end|>')
239
+ pos = inputs['input_ids'].tolist()[0].index(vision_start_token_id) + 1
240
+ pos_end = inputs['input_ids'].tolist()[0].index(vision_end_token_id)
241
+ activations['visual_mask'] = torch.zeros((1, inputs['input_ids'].shape[1]), dtype=torch.bool, device=device)
242
+ activations['visual_mask'][0, pos:pos_end] = True
243
+ output_shape = inputs['image_grid_thw'].cpu().numpy().squeeze(0)[1:] / 2
244
+ output_shape = np.array(output_shape, dtype=np.int32)
245
+ attn_weights_reduction = 'mean' # 'none' or 'mean'
246
+ layers = model.config.text_config.num_hidden_layers
247
+ attn_weights_map = get_attn_weights_map(activations, layers=layers, reduction=attn_weights_reduction)
248
+
249
+
250
+ visualize_token_wise_attention(attn_weights_map, qk_type='output2images', attn_size=output_shape, layers=layers, rgb_image=image)
251
+
252
+ pass
253
+
254
+ if "Qwen3-VL-4B" in args.model_path and use_act:
255
+ if is_natural_image: ##TODO: find heads for natural image
256
+ required_heads = {20: [15, 21, 23], 21: [8, 10, 11]}
257
+ sink_heads = {20: [1, 24]}
258
+ else:
259
+ ##textual image
260
+ required_heads = {19: [24, 27], 24: [21, 23]}
261
+ sink_heads = {24: [6, 10]}
262
+ required_layers = list(required_heads.keys())
263
+ elif "Qwen3-VL-8B" in args.model_path and use_act:
264
+ if is_natural_image: ##TODO: find heads for natural image
265
+ required_heads = {20: [21, 23], 21: [8, 10, 11]}
266
+ sink_heads = {21: [7]}
267
+ else:
268
+ ##textual image
269
+ required_heads = {19: [24, 27], 24: [21, 23]}
270
+ sink_heads = {24: [1, 2]}
271
+ required_layers = list(required_heads.keys())
272
+
273
+ grounding_attn_o2i = np.zeros(output_shape)
274
+ total_grounding_heads = sum([len(heads) for heads in required_heads.values()])
275
+ for layer, heads in required_heads.items():
276
+ attn_weights = attn_weights_map[f'layer{layer}']['output2images'].reshape(-1, output_shape[0], output_shape[1])
277
+ for head in heads:
278
+ grounding_attn_o2i += attn_weights[head]
279
+ grounding_attn_o2i = grounding_attn_o2i / total_grounding_heads
280
+
281
+ sink_attn = np.zeros(output_shape)
282
+ total_sink_heads = sum([len(heads) for heads in sink_heads.values()])
283
+ if total_sink_heads > 0:
284
+ for layer, heads in sink_heads.items():
285
+ attn_weights = attn_weights_map[f'layer{layer}']['output2images'].reshape(-1, output_shape[0], output_shape[1])
286
+ for head in heads:
287
+ sink_attn += attn_weights[head]
288
+ sink_attn = sink_attn / total_sink_heads
289
+
290
+ ##denoise with ocr boxes
291
+ img_path = os.path.join(image_folder, image_file)
292
+ if not is_natural_image:
293
+ cached_boxes = ocr_cache.get(img_path, None)
294
+ item = {
295
+ 'image': img_path,
296
+ 'grounding_attn_o2i': grounding_attn_o2i,
297
+ }
298
+ new_item, _, _ = process_single_sample_packed((item, None, cached_boxes))
299
+ grounding_attn_o2i = new_item['grounding_attn_o2i']
300
+
301
+ pseudo_set_denoised = create_pseudo_labels(sink_attn, grounding_attn_o2i, sink_thresh=0.02, bg_coff=0.05, binary_coff=0.25,
302
+ original_image_size=image_size, for_vis=True, mask_known_bg = True)
303
+ plot_pseudo_label(
304
+ image_path=img_path,
305
+ layer2_attn=pseudo_set_denoised['labels'],
306
+ grounding_attn_o2i=grounding_attn_o2i,
307
+ grounding_attn=pseudo_set_denoised['grounding_attn'],
308
+ is_sink_token=pseudo_set_denoised['sink_token_mask'],
309
+ fg_bbox=pseudo_set_denoised['fg_bbox'],
310
+ save_path=f'{save_path}/{sample}_pseudo_label.png',
311
+ )
312
+ sample += 1
313
+ if sample >= debug_samples:
314
+ print('Debug samples limit reached. Stopping evaluation.')
315
+ break
316
+ pass
317
+
318
+ import argparse
319
+
320
+ if __name__ == "__main__":
321
+ parser = argparse.ArgumentParser()
322
+ parser.add_argument("--model_name", type=str, required=True, help="model name")
323
+ parser.add_argument("--model_path", type=str, required=True, help="model path")
324
+ parser.add_argument(
325
+ "--dataset", type=str, required=True,
326
+ choices=["ocr_vqa", "DocVQA", "textvqa", "gqa"],
327
+ help="ocr_vqa, DocVQA, textvqa, gqa"
328
+ )
329
+ parser.add_argument("--save_path", type=str, required=True, help="save directory")
330
+
331
+ parser.add_argument("--questions_path", type=str, required=True, help="path to .jsonl/.json questions file")
332
+ parser.add_argument("--images_path", type=str, required=True, help="path to images root folder, not include images/")
333
+ parser.add_argument("--ocr_cache_file", type=str, default=None, help="path to ocr cache .pkl (optional)")
334
+
335
+ parser.add_argument("--debug_samples", type=int, default=10, help="stop after N samples")
336
+ parser.add_argument("--aura_start_layer", type=int, default=24, help="first layer to replace with AuraDecoderLayer (twig_K)")
337
+ parser.add_argument("--aura_num_layers", type=int, default=3, help="number of layers to replace (twig_T)")
338
+ args = parser.parse_args()
339
+
340
+ device = "cuda"
341
+
342
+ model = Qwen3VLForConditionalGeneration.from_pretrained(
343
+ args.model_path,
344
+ torch_dtype=torch.bfloat16,
345
+ attn_implementation="eager",
346
+ ).eval().to(device)
347
+
348
+ processor = AutoProcessor.from_pretrained(
349
+ args.model_path,
350
+ trust_remote_code=True,
351
+ padding_side="left",
352
+ use_fast=True,
353
+ )
354
+
355
+ # Step 1: Replace middle layers with AuraDecoderLayer (roi_mask=None → bit-for-bit identical)
356
+ replace_with_aura_layers(model, args.aura_start_layer, args.aura_num_layers)
357
+
358
+ # Step 2: Register hooks AFTER replacement so they attach to AuraAttention.self_attn
359
+ if use_act:
360
+ activations, hooks = register_hooks(model, is_qwen3vl=True)
361
+
362
+ is_natural_image = (args.dataset == "gqa")
363
+
364
+ if args.dataset == "textvqa":
365
+ args.images_path = args.images_path + "/textvqa/train_images"
366
+
367
+ print("args.images_path: ", args.images_path)
368
+ ocr_cache_file = os.path.expanduser(args.ocr_cache_file) if args.ocr_cache_file else None
369
+
370
+ questions = [json.loads(q) for q in open(os.path.expanduser(args.questions_path), "r")]
371
+ print('Total questions loaded:', len(questions))
372
+ questions = [q for q in questions if args.dataset in os.path.join(args.images_path, q["image"])]
373
+ print('Questions after filtering by target dataset:', len(questions))
374
+
375
+
376
+ eval_model_qwen(
377
+ model=model,
378
+ processor=processor,
379
+ questions=questions,
380
+ image_folder=args.images_path,
381
+ ocr_cache_file=ocr_cache_file,
382
+ is_natural_image=is_natural_image,
383
+ save_path=args.save_path,
384
+ debug_samples=args.debug_samples,
385
+ )
386
+
387
+ print(f"dataset: {args.dataset}")
388
+ print(f"questions_path: {args.questions_path}")
389
+ print(f"images_path: {args.images_path}")
390
+ print(f"ocr_cache_file: {ocr_cache_file}")
391
+ print(f"is_natural_image: {is_natural_image}")
392
+ print(f"aura_start_layer: {args.aura_start_layer}")
393
+ print(f"aura_num_layers: {args.aura_num_layers}")
QWENVL-PRIVATE/analysis/analysis_auto.py ADDED
@@ -0,0 +1,588 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import glob
3
+ import pickle
4
+ import numpy as np
5
+ import matplotlib.pyplot as plt
6
+ import seaborn as sns
7
+
8
+
9
+ # ==========================================
10
+ # 1. DATA LOADING & PROCESSING
11
+ # ==========================================
12
+
13
+ def load_data_for_task(base_dir, task_name, model_pattern):
14
+ """
15
+ Loads pickle files matching the model_pattern for a specific task.
16
+ Returns:
17
+ - samples_data: dict of sample-level data (scores, visual tokens)
18
+ - metadata: dict containing 'e2e_latency', 'total_tokens', etc.
19
+ """
20
+ # Construct search pattern. logic handles wildcards if passed in model_pattern
21
+ search_pattern = os.path.join(base_dir, f"{task_name}_analysis_rank*_{model_pattern}*.pkl")
22
+ files = glob.glob(search_pattern)
23
+
24
+ if not files:
25
+ print(f" [Warning] No files found for task '{task_name}' with pattern '*{model_pattern}*'")
26
+ return {}, {}
27
+
28
+ merged_samples = {}
29
+ aggregated_metadata = {"e2e_latency": 0, "total_tokens": 0}
30
+ found_metadata = False
31
+
32
+ for file_path in files:
33
+ try:
34
+ with open(file_path, "rb") as f:
35
+ data_chunk = pickle.load(f)
36
+
37
+ # Separate metric_dict from sample data
38
+ if 'metric_dict' in data_chunk:
39
+ m_dict = data_chunk.pop('metric_dict')
40
+ # Sum latencies/tokens if split across multiple files (rare but safe)
41
+ aggregated_metadata['e2e_latency'] += m_dict.get('e2e_latency', 0)
42
+ aggregated_metadata['total_tokens'] += m_dict.get('total_tokens', 0)
43
+ found_metadata = True
44
+
45
+ merged_samples.update(data_chunk)
46
+ except Exception as e:
47
+ print(f" [Error] Failed to load {file_path}: {e}")
48
+
49
+ if not found_metadata:
50
+ print(f" [Info] No 'metric_dict' found in files for {task_name}.")
51
+
52
+ return merged_samples, aggregated_metadata
53
+
54
+
55
+ def extract_metrics(task_data_map, task_meta_map):
56
+ """
57
+ Calculates average metrics (Throughput, Avg Visual Tokens) from raw data.
58
+ """
59
+ visual_tokens = []
60
+
61
+ # 1. Extract Visual Token Counts
62
+ for doc_id, content in task_data_map.items():
63
+ if "visual_token_num" in content:
64
+ val = content["visual_token_num"]
65
+ # Handle if it's a tensor/array or scalar
66
+ if hasattr(val, 'item'):
67
+ val = val.item()
68
+ visual_tokens.append(float(val))
69
+
70
+ avg_vis_tokens = np.mean(visual_tokens) if visual_tokens else 0
71
+
72
+ # 2. Calculate Throughput (Samples / Second)
73
+ # Total samples in this dataset
74
+ n_samples = len(task_data_map)
75
+
76
+ # Total time (e2e_latency is usually in seconds, ensure your logger units match)
77
+ total_time = task_meta_map.get('e2e_latency', 0)
78
+
79
+ if total_time > 0:
80
+ throughput = n_samples / total_time
81
+ else:
82
+ throughput = 0
83
+
84
+ return {
85
+ "avg_visual_tokens": avg_vis_tokens,
86
+ "throughput": throughput,
87
+ "n_samples": n_samples
88
+ }
89
+
90
+
91
+ # ==========================================
92
+ # 2. PLOTTING FUNCTIONS
93
+ # ==========================================
94
+
95
+ def plot_tradeoff_curves(results, save_dir):
96
+ """
97
+ Plots Accuracy vs. Efficiency (Throughput) and Accuracy vs. Visual Tokens.
98
+ Handles Baseline, Ours, and RoI curves with distinct styles.
99
+ """
100
+ labels = list(results.keys())
101
+
102
+ # Calculate Macro Averages (across all tasks for each config)
103
+ avg_accs = []
104
+ avg_throughputs = []
105
+ avg_tokens = []
106
+
107
+ for label in labels:
108
+ metrics = results[label]
109
+ # Calculate mean of values; ensure we handle potential empty lists safely
110
+ acc_values = [m['acc'] for m in metrics.values()]
111
+ th_values = [m['throughput'] for m in metrics.values()]
112
+ tok_values = [m['avg_visual_tokens'] for m in metrics.values()]
113
+
114
+ avg_accs.append(np.mean(acc_values) if acc_values else 0)
115
+ avg_throughputs.append(np.mean(th_values) if th_values else 0)
116
+ avg_tokens.append(np.mean(tok_values) if tok_values else 0)
117
+
118
+ # --- STYLE DEFINITIONS ---
119
+ # Customize colors and markers here
120
+ styles = {
121
+ 'Baseline': {'marker': 'o', 'linestyle': '--', 'color': '#7f8c8d'}, # Grey, Dashed, Circle
122
+ 'Ours': {'marker': '*', 'linestyle': '-', 'color': '#e74c3c'}, # Red, Solid, Star
123
+ 'RoI': {'marker': 's', 'linestyle': '-', 'color': '#3498db'} # Blue, Solid, Square
124
+ }
125
+
126
+ # Helper function to generate both plots without code duplication
127
+ def create_plot(x_data, x_label, file_name, title):
128
+ plt.figure(figsize=(10, 7))
129
+ sns.set_theme(style="whitegrid") # Optional: makes grid look nice
130
+
131
+ # Identify unique prefixes (Baseline, Ours, RoI) from the config keys
132
+ prefixes = sorted(list(set([l.split('\n')[0] for l in labels])))
133
+
134
+ # Sort prefixes to ensure Legend order (e.g. Baseline -> Ours -> RoI) if desired
135
+ # You can manually force order: prefixes = ['Baseline', 'Ours', 'RoI']
136
+
137
+ for prefix in prefixes:
138
+ # Get style, default to black if unknown prefix
139
+ style = styles.get(prefix, {'marker': 'D', 'linestyle': '-.', 'color': 'black'})
140
+
141
+ # Filter data for this group
142
+ group_indices = [i for i, l in enumerate(labels) if l.startswith(prefix)]
143
+ if not group_indices: continue
144
+
145
+ g_x = [x_data[i] for i in group_indices]
146
+ g_y = [avg_accs[i] for i in group_indices]
147
+ g_labels = [labels[i] for i in group_indices]
148
+
149
+ # Sort points by X-axis (efficiency or tokens) to draw correct line
150
+ sorted_pairs = sorted(zip(g_x, g_y, g_labels), key=lambda x: x[0])
151
+ s_x, s_y, s_l = zip(*sorted_pairs)
152
+
153
+ # Plot Line
154
+ plt.plot(s_x, s_y,
155
+ marker=style['marker'],
156
+ linestyle=style['linestyle'],
157
+ color=style['color'],
158
+ label=prefix,
159
+ linewidth=2,
160
+ markersize=10 if style['marker'] != '*' else 14) # Stars need to be larger
161
+
162
+ # Annotate Points (Labels like "Max-256")
163
+ for x, y, lbl in zip(s_x, s_y, s_l):
164
+ clean_lbl = lbl.split('\n')[-1]
165
+
166
+ # Dynamic offsets to prevent overlapping labels
167
+ offset = (0, 10)
168
+ if prefix == 'RoI':
169
+ offset = (0, -15)
170
+ elif prefix == 'Ours':
171
+ offset = (-10, 10)
172
+
173
+ plt.annotate(clean_lbl, (x, y), xytext=offset, textcoords='offset points',
174
+ ha='center', fontsize=8, fontweight='bold',
175
+ bbox=dict(boxstyle="round,pad=0.2", fc="white", alpha=0.6, ec="none"))
176
+
177
+ plt.title(title, fontsize=14)
178
+ plt.xlabel(x_label, fontweight='bold')
179
+ plt.ylabel('Average Accuracy (%)', fontweight='bold')
180
+ plt.grid(True, linestyle=':', alpha=0.6)
181
+ plt.legend(fontsize=10)
182
+ plt.tight_layout()
183
+
184
+ save_path = os.path.join(save_dir, file_name)
185
+ plt.savefig(save_path, dpi=300)
186
+ plt.close()
187
+ print(f"Saved plot: {save_path}")
188
+
189
+ # 1. Plot Efficiency
190
+ create_plot(avg_throughputs,
191
+ 'Average Throughput (Samples / Second) \n← Slower | Faster →',
192
+ "acc_vs_efficiency.png",
193
+ "Accuracy vs. Efficiency Trade-off")
194
+
195
+ # 2. Plot Visual Tokens
196
+ create_plot(avg_tokens,
197
+ 'Average Visual Tokens per Image \n← Cheaper | More Detailed →',
198
+ "acc_vs_visual_tokens.png",
199
+ "Accuracy vs. Visual Cost")
200
+
201
+ # ==========================================
202
+ # 3. EXISTING UTILS (Preserved)
203
+ # ==========================================
204
+ def print_threshold_ratios(task_data_map, threshold):
205
+ print(f"\n{'=' * 20} Threshold Analysis (Score > {threshold}) {'=' * 20}")
206
+ print(f"{'Dataset Name':<20} | {'Total Samples':<15} | {'Above Thr':<10} | {'Ratio':<10}")
207
+ print("-" * 65)
208
+ for task_name, content in task_data_map.items():
209
+ scores = content['scores']
210
+ if len(scores) == 0: continue
211
+ above_mask = scores > threshold
212
+ count_above = np.sum(above_mask)
213
+ ratio = count_above / len(scores)
214
+ print(f"{task_name:<20} | {len(scores):<15} | {count_above:<10} | {ratio:.2%}")
215
+ print("-" * 65 + "\n")
216
+
217
+
218
+ def extract_scores_list(data_dict):
219
+ scores = []
220
+ for doc_id, content in data_dict.items():
221
+ if "high_res_pred_score" in content:
222
+ val = content["high_res_pred_score"]
223
+ if isinstance(val, np.ndarray): val = val.flatten()[0]
224
+ scores.append(float(val))
225
+ return np.array(scores)
226
+
227
+
228
+ def plot_cost_benefit_combo(results, task_name, save_dir):
229
+ """
230
+ Better Idea: Combines Relative Efficiency (Bars) with Accuracy Gain (Points/Lines)
231
+ on a dual-axis chart.
232
+ """
233
+ # 1. Parse Data (Similar to above, but also gets Accuracy)
234
+ data_map = {}
235
+ for label, metrics in results.items():
236
+ method, resolution = label.split('\n')
237
+ if resolution not in data_map: data_map[resolution] = {}
238
+
239
+ task_data = metrics.get(task_name, {'throughput': 0, 'acc': 0})
240
+ data_map[resolution][method] = {
241
+ 'speed': task_data['throughput'],
242
+ 'acc': task_data['acc']
243
+ }
244
+
245
+ resolutions = sorted(data_map.keys(), key=lambda x: int(x.split('-')[-1]))
246
+ methods = ['Baseline', 'Ours', 'RoI']
247
+ colors = {'Baseline': '#95a5a6', 'Ours': '#e74c3c', 'RoI': '#3498db'}
248
+
249
+ x = np.arange(len(resolutions))
250
+ width = 0.25
251
+
252
+ fig, ax1 = plt.subplots(figsize=(11, 7))
253
+ ax2 = ax1.twinx() # Create secondary y-axis for Accuracy Gain
254
+
255
+ # Loop for Bars (Efficiency) AND Points (Accuracy)
256
+ for i, method in enumerate(methods):
257
+ eff_ratios = []
258
+ acc_gains = []
259
+
260
+ for res in resolutions:
261
+ base_speed = data_map[res].get('Baseline', {}).get('speed', 1.0)
262
+ base_acc = data_map[res].get('Baseline', {}).get('acc', 0.0)
263
+
264
+ curr = data_map[res].get(method, {'speed': 0, 'acc': 0})
265
+
266
+ # Efficiency Ratio
267
+ ratio = (curr['speed'] / base_speed) * 100 if base_speed > 0 else 0
268
+ eff_ratios.append(ratio)
269
+
270
+ # Accuracy Delta (Gain over Baseline of SAME resolution)
271
+ delta = curr['acc'] - base_acc
272
+ acc_gains.append(delta)
273
+
274
+ offset = (i - 1) * width
275
+
276
+ # PLOT 1: Efficiency Bars (Left Axis)
277
+ ax1.bar(x + offset, eff_ratios, width, color=colors[method], alpha=0.3, label=f"{method} (Speed)")
278
+
279
+ # PLOT 2: Accuracy Gain Markers (Right Axis)
280
+ # We plot these slightly offset to align with bars, or connected with lines
281
+ if method == 'Baseline':
282
+ # Plot baseline as flat line at 0
283
+ ax2.plot(x + offset, acc_gains, color=colors[method], marker='_', markersize=20, linestyle='None')
284
+ else:
285
+ # Plot gains as distinct markers
286
+ ax2.plot(x + offset, acc_gains, color=colors[method], marker='D', markersize=8,
287
+ linestyle='-', linewidth=2, label=f"{method} (Acc Gain)")
288
+
289
+ # Annotate specific gains
290
+ for j, gain in enumerate(acc_gains):
291
+ ax2.annotate(f"+{gain:.1f}", (x[j] + offset, gain), xytext=(0, 8),
292
+ textcoords='offset points', ha='center', color=colors[method], fontweight='bold')
293
+
294
+ # Formatting
295
+ ax1.set_ylabel("Relative Efficiency (%)", fontsize=12, fontweight='bold')
296
+ ax1.set_ylim(0, 120)
297
+ ax1.set_xlabel("Resolution", fontsize=12)
298
+ ax1.set_xticks(x)
299
+ ax1.set_xticklabels(resolutions)
300
+
301
+ ax2.set_ylabel("Accuracy Gain over Baseline (Points)", fontsize=12, fontweight='bold')
302
+ # Set symmetric limits or auto limits for acc gain
303
+ ax2.axhline(0, color='gray', linestyle=':', linewidth=1)
304
+
305
+ plt.title(f"Cost-Benefit Analysis: {task_name}\n(Bars = Speed %, Points = Acc Gain)", fontsize=14)
306
+
307
+ # Custom Legend
308
+ lines, labels = ax1.get_legend_handles_labels()
309
+ lines2, labels2 = ax2.get_legend_handles_labels()
310
+ ax1.legend(lines + lines2, labels + labels2, loc='upper left')
311
+
312
+ plt.tight_layout()
313
+ save_path = os.path.join(save_dir, f"{task_name}_cost_benefit.png")
314
+ plt.savefig(save_path, dpi=300)
315
+ plt.close()
316
+ print(f"Saved Cost-Benefit plot: {save_path}")
317
+
318
+ # ==========================================
319
+ # 4. MAIN EXECUTION
320
+ # ==========================================
321
+
322
+ if __name__ == "__main__":
323
+ BASE_DIR = "../archive_data/"
324
+ SAVE_DIR = "./analysis_plots/"
325
+ os.makedirs(SAVE_DIR, exist_ok=True)
326
+
327
+ # --- CONFIGURATION: Define Models and Manual Accuracies ---
328
+ # Keys: Display Label (First line is Group, second is variant)
329
+ # Values: 'pattern': filename part to match, 'accs': dict of {task: accuracy}
330
+ TARGET_TASKS = [
331
+ "docvqa_val",
332
+ "infovqa_val",
333
+ "textvqa_val",
334
+ "chartqa", # Added from table
335
+ "ocrbench" # Added from table
336
+ # "vstar_bench",
337
+ # "mmerealworld_lite",
338
+ ]
339
+
340
+ # --- CONFIGURATION: Define Models and Manual Accuracies ---
341
+ # Patterns are derived from: Visual_Tokens * 28 * 28
342
+ # 128 tokens = 100352 pixels
343
+ # 256 tokens = 200704 pixels
344
+ # 576 tokens = 451584 pixels
345
+ # 1024 tokens = 802816 pixels
346
+ # 2048 tokens = 1605632 pixels
347
+
348
+ MODEL_CONFIGS = {
349
+ # --- BASELINE CONFIGURATIONS ---
350
+ # 'Baseline\nMax-256': {
351
+ # 'pattern': 'Qwen2.5-VL-7B-Instruct_min100352_max200704',
352
+ # 'accs': {
353
+ # 'docvqa_val': 81.2,
354
+ # 'chartqa': 76.8,
355
+ # 'ocrbench': 77.8,
356
+ # 'infovqa_val': 54.0,
357
+ # 'textvqa_val': 75.5,
358
+ # 'vstar_bench': 53.4,
359
+ # 'mmerealworld_lite': 27.6
360
+ # }
361
+ # },
362
+ 'Baseline\nMax-576': {
363
+ 'pattern': 'Qwen2.5-VL-7B-Instruct_min200704_max451584',
364
+ 'accs': {
365
+ 'docvqa_val': 92.0,
366
+ 'chartqa': 83.0,
367
+ 'ocrbench': 82.8,
368
+ 'infovqa_val': 70.1,
369
+ 'textvqa_val': 81.1,
370
+ 'vstar_bench': 64.4,
371
+ 'mmerealworld_lite': 35.4
372
+ }
373
+ },
374
+ 'Baseline\nMax-1024': {
375
+ 'pattern': 'Qwen2.5-VL-7B-Instruct_min200704_max802816',
376
+ 'accs': {
377
+ 'docvqa_val': 93.9,
378
+ 'chartqa': 84.6,
379
+ 'ocrbench': 84.4,
380
+ 'infovqa_val': 76.5,
381
+ 'textvqa_val': 82.8,
382
+ 'vstar_bench': 70.2,
383
+ 'mmerealworld_lite': 37.5
384
+ }
385
+ },
386
+ 'Baseline\nMax-2048': {
387
+ 'pattern': 'Qwen2.5-VL-7B-Instruct_min200704_max1605632',
388
+ 'accs': {
389
+ 'docvqa_val': 94.7,
390
+ 'chartqa': 84.5,
391
+ 'ocrbench': 84.8,
392
+ 'infovqa_val': 80.4,
393
+ 'textvqa_val': 82.9,
394
+ 'vstar_bench': 72.3,
395
+ 'mmerealworld_lite': 39.3
396
+ }
397
+ },
398
+ #
399
+ # # # # --- YOUR "OURS" CONFIGURATIONS (Update these patterns as needed) ---
400
+ # 'Ours\nMax-256': {
401
+ # 'pattern': 'qwen2_5vl-7b-roi-K18T3-134k-gatedv2PB-wGTwRoI-twiginit-filled_min100352_max200704',
402
+ # 'accs': {
403
+ # 'docvqa_val': 89.1,
404
+ # 'chartqa': 82.5,
405
+ # 'ocrbench': 83.8,
406
+ # 'infovqa_val': 69.6,
407
+ # 'textvqa_val': 81.8,
408
+ # 'vstar_bench': 67.5,
409
+ # 'mmerealworld_lite': 37.3
410
+ # }
411
+ # },
412
+ # 'Ours\nMax-576': {
413
+ # 'pattern': 'qwen2_5vl-7b-roi-K18T3-134k-gatedv2PB-wGTwRoI-twiginit-filled_min200704_max451584',
414
+ # 'accs': {
415
+ # 'docvqa_val': 93.9,
416
+ # 'chartqa': 84.2,
417
+ # 'ocrbench': 84.8,
418
+ # 'infovqa_val': 78.5,
419
+ # 'textvqa_val': 82.8,
420
+ # 'vstar_bench': 80.1,
421
+ # 'mmerealworld_lite': 43.4
422
+ # }
423
+ # },
424
+ # 'Ours\nMax-1024': {
425
+ # 'pattern': 'qwen2_5vl-7b-roi-K18T3-134k-gatedv2PB-wGTwRoI-twiginit-filled_min200704_max802816',
426
+ # 'accs': {
427
+ # 'docvqa_val': 94.6,
428
+ # 'chartqa': 85.4,
429
+ # 'ocrbench': 84.8,
430
+ # 'infovqa_val': 81.5,
431
+ # 'textvqa_val': 83.7,
432
+ # 'vstar_bench': 83.3,
433
+ # 'mmerealworld_lite': 46.5
434
+ # }
435
+ # },
436
+ # 'Ours\nMax-1536': {
437
+ # 'pattern': 'qwen2_5vl-7b-roi-K18T3-134k-gatedv2PB-wGTwRoI-twiginit-filled_min200704_max1204224',
438
+ # 'accs': {
439
+ # 'docvqa_val': 94.8,
440
+ # 'chartqa': 85.4,
441
+ # 'ocrbench': 85.4,
442
+ # 'infovqa_val': 82.4,
443
+ # 'textvqa_val': 83.6,
444
+ # 'vstar_bench': 88.0,
445
+ # 'mmerealworld_lite': 47.2
446
+ # }
447
+ # },
448
+ ###--- "OURS-RoI" CONFIGURATIONS ---
449
+ 'RoI\nMax-256': {
450
+ 'pattern': 'qwen2_5vl-7b-roi-K18T3-185k-v1bf16-denoisedv2-twiginit-filled_min100352_max200704',
451
+ 'accs': {
452
+ 'docvqa_val': 89.2, 'chartqa': 83.0, 'ocrbench': 83.6, 'infovqa_val': 69.9,
453
+ 'textvqa_val': 81.4, 'vstar_bench': 67.5, 'mmerealworld_lite': 37.6
454
+ }
455
+ },
456
+ 'RoI\nMax-576': {
457
+ 'pattern': 'qwen2_5vl-7b-roi-K18T3-185k-v1bf16-denoisedv2-twiginit-filled_min200704_max451584',
458
+ 'accs': {
459
+ 'docvqa_val': 94.1, 'chartqa': 85.8, 'ocrbench': 84.9, 'infovqa_val': 79.6,
460
+ 'textvqa_val': 82.9, 'vstar_bench': 80.6, 'mmerealworld_lite': 44.6
461
+ }
462
+ },
463
+ 'RoI\nMax-1024': {
464
+ 'pattern': 'qwen2_5vl-7b-roi-K18T3-185k-v1bf16-denoisedv2-twiginit-filled_min200704_max802816',
465
+ 'accs': {
466
+ 'docvqa_val': 94.7, 'chartqa': 86.3, 'ocrbench': 84.8, 'infovqa_val': 82.0,
467
+ 'textvqa_val': 83.9, 'vstar_bench': 82.2, 'mmerealworld_lite': 46.7
468
+ }
469
+ },
470
+ # 'RoI\nMax-1536': {
471
+ # 'pattern': 'qwen2_5vl-7b-roi-K18T3-185k-v1bf16-denoisedv2-twiginit-filled_min200704_max1204224',
472
+ # 'accs': {
473
+ # 'docvqa_val': 94.9, 'chartqa': 85.8, 'ocrbench': 84.8, 'infovqa_val': 83.2,
474
+ # 'textvqa_val': 83.9, 'vstar_bench': 88.0, 'mmerealworld_lite': 47.0
475
+ # }
476
+ # },
477
+
478
+ # # # --- YOUR "OURS" CONFIGURATIONS (Update these patterns as needed) ---
479
+ 'Ours\nMax-1024': {
480
+ 'pattern': 'qwen2_5vl-7b-roi-K18T3-134k-gatedv2PB-stage3_min200704_max802816',
481
+ 'accs': {
482
+ 'docvqa_val': 94.8,
483
+ 'chartqa': 86.4,
484
+ 'ocrbench': 85.4,
485
+ 'infovqa_val': 81.2,
486
+ 'textvqa_val': 84.1,
487
+ 'vstar_bench': 83.8,
488
+ 'mmerealworld_lite': 47.3
489
+ }
490
+ },
491
+ 'Ours\nMax-576': {
492
+ 'pattern': 'qwen2_5vl-7b-roi-K18T3-134k-gatedv2PB-stage3_min200704_max451584',
493
+ 'accs': {
494
+ 'docvqa_val': 94.0,
495
+ 'chartqa': 85.4,
496
+ 'ocrbench': 85.4,
497
+ 'infovqa_val': 79.4,
498
+ 'textvqa_val': 83.5,
499
+ 'vstar_bench': 79.6,
500
+ 'mmerealworld_lite': 45.7
501
+ }
502
+ },
503
+ 'Ours\nMax-384': {
504
+ 'pattern': 'qwen2_5vl-7b-roi-K18T3-134k-gatedv2PB-stage3_min100352_max301056',
505
+ 'accs': {
506
+ 'docvqa_val': 93.4,
507
+ 'chartqa': 84.2,
508
+ 'ocrbench': 85.3,
509
+ 'infovqa_val': 75.7,
510
+ 'textvqa_val': 82.7,
511
+ 'vstar_bench': 73.8,
512
+ 'mmerealworld_lite': 41.2
513
+ }
514
+ },
515
+ 'Ours\nMax-256': {
516
+ 'pattern': 'qwen2_5vl-7b-roi-K18T3-134k-gatedv2PB-stage3_min100352_max200704',
517
+ 'accs': {
518
+ 'docvqa_val': 90.4,
519
+ 'chartqa': 82.7,
520
+ 'ocrbench': 83.5,
521
+ 'infovqa_val': 71.7,
522
+ 'textvqa_val': 81.8,
523
+ 'vstar_bench': 68.6,
524
+ 'mmerealworld_lite': 37.9
525
+ }
526
+ },
527
+ # 'Ours\nMax-1536': {
528
+ # 'pattern': 'qwen2_5vl-7b-roi-K18T3-post7k-repos-full2_min200704_max1204224',
529
+ # 'accs': {
530
+ # 'docvqa_val': 95.0,
531
+ # 'chartqa': 86.4,
532
+ # 'ocrbench': 84.8,
533
+ # 'infovqa_val': 82.6,
534
+ # 'textvqa_val': 84.3,
535
+ # 'vstar_bench': 86.9,
536
+ # 'mmerealworld_lite': 48.2
537
+ # }
538
+ # },
539
+
540
+ }
541
+ print("--- Loading Data for Comparison ---")
542
+
543
+ # Store all results here: { "Baseline\nMax-256": { "textvqa": {throughput, tokens, acc}, ... } }
544
+ overall_results = {}
545
+
546
+ for label, config in MODEL_CONFIGS.items():
547
+ print(f"Processing config: {label.replace(chr(10), ' ')}") # chr(10) is newline
548
+ overall_results[label] = {}
549
+
550
+ # Load data for each task for this specific config
551
+ for task in TARGET_TASKS:
552
+ # 1. Load Raw Data
553
+ raw_data, metadata = load_data_for_task(BASE_DIR, task, config['pattern'])
554
+
555
+ if not raw_data:
556
+ # Fill dummy if missing to avoid crash, or skip
557
+ overall_results[label][task] = {'throughput': 0, 'avg_visual_tokens': 0, 'acc': 0}
558
+ continue
559
+
560
+ # 2. Extract Metrics (Speed, Tokens)
561
+ metrics = extract_metrics(raw_data, metadata)
562
+
563
+ # 3. Add Manual Accuracy
564
+ acc = config['accs'].get(task, 0.0) # Default 0 if task missing in config
565
+
566
+ # 4. Store Scores for Histograms (Optional, if you want detailed score plots per config)
567
+ # scores = extract_scores_list(raw_data)
568
+
569
+ # Save combined
570
+ overall_results[label][task] = {
571
+ 'throughput': metrics['throughput'],
572
+ 'avg_visual_tokens': metrics['avg_visual_tokens'],
573
+ 'acc': acc,
574
+ # 'scores': scores # Uncomment if you want to pass this to the histogram plotter
575
+ }
576
+
577
+ print(
578
+ f" > {task}: {metrics['throughput']:.1f} samp/s, {metrics['avg_visual_tokens']:.0f} tokens, Acc: {acc}")
579
+
580
+ # --- PLOT COMPARISONS ---
581
+ if overall_results:
582
+ print("\n--- Generating Comparison Plots ---")
583
+ plot_tradeoff_curves(overall_results, SAVE_DIR)
584
+
585
+ print("\nDone! Plots saved to:", SAVE_DIR)
586
+
587
+ for i in range(len(TARGET_TASKS)):
588
+ plot_cost_benefit_combo(overall_results, TARGET_TASKS[i], SAVE_DIR)
QWENVL-PRIVATE/analysis/analysis_high_res_prediction.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import glob
3
+ import pickle
4
+ import numpy as np
5
+ import matplotlib.pyplot as plt
6
+ import seaborn as sns
7
+
8
+
9
+ def print_threshold_ratios(task_data_map, threshold):
10
+ """
11
+ Calculates and prints the ratio of samples above the given threshold.
12
+ """
13
+ print(f"\n{'=' * 20} Threshold Analysis (Score > {threshold}) {'=' * 20}")
14
+ print(f"{'Dataset Name':<20} | {'Total Samples':<15} | {'Above Thr':<10} | {'Ratio':<10}")
15
+ print("-" * 65)
16
+
17
+ for task_name, scores in task_data_map.items():
18
+ if len(scores) == 0:
19
+ continue
20
+
21
+ # Boolean mask: True if score > threshold
22
+ above_mask = scores > threshold
23
+ count_above = np.sum(above_mask)
24
+ total_samples = len(scores)
25
+ ratio = count_above / total_samples
26
+
27
+ # Print formatted row
28
+ print(f"{task_name:<20} | {total_samples:<15} | {count_above:<10} | {ratio:.2%}")
29
+ print("-" * 65 + "\n")
30
+
31
+ def load_data_for_task(base_dir, task_name, min_pixels=200704, max_pixels=451584, model_name="qwen2_5vl-3b-roi-K24T3-stage3"):
32
+ """
33
+ Loads all pickle files for a specific task (merging different ranks).
34
+ """
35
+ search_pattern = os.path.join(base_dir, f"{task_name}_analysis_rank*_{model_name}_min{min_pixels}_max{max_pixels}.pkl")
36
+ print("Searching for files with pattern:", search_pattern)
37
+ files = glob.glob(search_pattern)
38
+
39
+ if not files:
40
+ print(f"Warning: No files found for task '{task_name}' in {base_dir}")
41
+ return {}
42
+
43
+ merged_data = {}
44
+ print(f"Loading {len(files)} files for task: {task_name}...")
45
+
46
+ for file_path in files:
47
+ try:
48
+ with open(file_path, "rb") as f:
49
+ data_chunk = pickle.load(f)
50
+ merged_data.update(data_chunk)
51
+ except Exception as e:
52
+ print(f"Error loading {file_path}: {e}")
53
+
54
+ return merged_data
55
+
56
+
57
+ def extract_scores(data_dict):
58
+ """
59
+ Extracts the 'high_res_pred_score' from the dictionary into a flat list.
60
+ """
61
+ scores = []
62
+ for doc_id, content in data_dict.items():
63
+ if "high_res_pred_score" in content:
64
+ val = content["high_res_pred_score"]
65
+ if isinstance(val, np.ndarray):
66
+ val = val.flatten()[0]
67
+ scores.append(float(val))
68
+ return np.array(scores)
69
+
70
+
71
+ def plot_single_task(task_name, scores, save_dir):
72
+ """
73
+ Plots the distribution for a SINGLE task and saves it to a separate file.
74
+ """
75
+ if len(scores) == 0:
76
+ return
77
+
78
+ plt.figure(figsize=(8, 6))
79
+ sns.set_theme(style="whitegrid")
80
+
81
+ # Plot specific color for individual plots
82
+ color = sns.color_palette("viridis", 1)[0]
83
+
84
+ sns.histplot(
85
+ scores,
86
+ kde=True,
87
+ stat="probability", # Normalized ratio
88
+ element="step",
89
+ bins=50,
90
+ color=color,
91
+ label=f"{task_name} (n={len(scores)})"
92
+ )
93
+
94
+ # Calculate stats for title
95
+ mean_val = np.mean(scores)
96
+ std_val = np.std(scores)
97
+
98
+ plt.title(f"Distribution: {task_name}\n(Mean={mean_val:.2f}, Std={std_val:.2f})")
99
+ plt.xlabel("Prediction Score")
100
+ plt.ylabel("Density (Normalized)")
101
+ plt.legend()
102
+ plt.tight_layout()
103
+ plt.show()
104
+
105
+ save_path = os.path.join(save_dir, f"{task_name}_dist.png")
106
+ plt.savefig(save_path, dpi=300)
107
+ plt.close() # Close figure to free memory
108
+ print(f"Saved individual plot: {save_path}")
109
+
110
+
111
+ def plot_overall_comparison(task_data_map, save_path):
112
+ """
113
+ Plots ALL tasks on the SAME figure for comparison.
114
+ """
115
+ plt.figure(figsize=(12, 7))
116
+ sns.set_theme(style="whitegrid")
117
+
118
+ # Use a distinct palette for multiple lines
119
+ palette = sns.color_palette("bright", len(task_data_map))
120
+
121
+ for i, (task_name, scores) in enumerate(task_data_map.items()):
122
+ if len(scores) == 0:
123
+ continue
124
+
125
+ sns.histplot(
126
+ scores,
127
+ label=f"{task_name}",
128
+ kde=True,
129
+ element="step",
130
+ stat="probability",
131
+ common_norm=False,
132
+ bins=50,
133
+ alpha=0.3,
134
+ color=palette[i]
135
+ )
136
+
137
+ plt.title("Overall Comparison: High Res Prediction Scores")
138
+ plt.xlabel("Prediction Score")
139
+ plt.ylabel("Ratio Percentage")
140
+ plt.legend(title="Datasets")
141
+ plt.tight_layout()
142
+ plt.show()
143
+ plt.savefig(save_path, dpi=300)
144
+ plt.close()
145
+ print(f"Saved overall comparison plot: {save_path}")
146
+
147
+
148
+ if __name__ == "__main__":
149
+ # 1. SETUP
150
+ BASE_DIR = "./excluded/"
151
+ # Directory to save the images
152
+ SAVE_DIR = "./excluded/analysis_plots/"
153
+ THRESHOLD = 0.1
154
+
155
+ # Ensure save directory exists
156
+ os.makedirs(SAVE_DIR, exist_ok=True)
157
+
158
+ TARGET_TASKS = ["textvqa_val", "infovqa_val", "chartqa", "ocrbench",
159
+ "docvqa_val","vstar_bench", "hrbench8k","mmerealworld_lite"]
160
+ # 2. LOAD DATA
161
+ task_scores_map = {}
162
+
163
+ print("--- Loading Data ---")
164
+ for task in TARGET_TASKS:
165
+ raw_dict = load_data_for_task(BASE_DIR, task)
166
+ if raw_dict:
167
+ scores = extract_scores(raw_dict)
168
+ task_scores_map[task] = scores
169
+
170
+ if not task_scores_map:
171
+ print("No data found. Exiting.")
172
+ exit()
173
+
174
+ # 3. PRINT RATIO STATISTICS (NEW FEATURE)
175
+ print_threshold_ratios(task_scores_map, THRESHOLD)
176
+ print_threshold_ratios(task_scores_map, 0.05)
177
+ print_threshold_ratios(task_scores_map, 0.025)
178
+
179
+
180
+ print("\n--- Generating Plots ---")
181
+
182
+ # 3. PLOT INDIVIDUAL (One file per task)
183
+ for task_name, scores in task_scores_map.items():
184
+ plot_single_task(task_name, scores, SAVE_DIR)
185
+
186
+ # # 4. PLOT OVERALL (One file with all tasks)
187
+ # overall_path = os.path.join(SAVE_DIR, "overall_comparison.png")
188
+ # plot_overall_comparison(task_scores_map, overall_path)
189
+ #
190
+ # print("\nDone! All plots saved to:", SAVE_DIR)
QWENVL-PRIVATE/analysis/check_gate_label.py ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pickle
2
+ import matplotlib.pyplot as plt
3
+ import os
4
+ import math
5
+ from PIL import Image
6
+ import textwrap
7
+ import string
8
+ from collections import Counter
9
+ import random
10
+
11
+ def _normalize_answer(text):
12
+ """Helper to normalize text for comparison (lower case, strip punctuation)."""
13
+ if not isinstance(text, str): return str(text)
14
+ text = text.lower().strip()
15
+ # Remove punctuation
16
+ text = text.translate(str.maketrans('', '', string.punctuation))
17
+ return text
18
+
19
+
20
+ def plot_single_sample(item, image_root_folder, save_path=None, show_plot=True):
21
+ """
22
+ Plots a single sample's image and resolution consistency chart.
23
+
24
+ Args:
25
+ item: A dictionary containing 'image', 'res', 'ms_answers', 'text' (GT), etc.
26
+ image_root_folder: Path to the root folder where images are stored.
27
+ save_path: If provided, saves the figure to this path.
28
+ show_plot: If True, calls plt.show().
29
+ """
30
+ high_res_group_num = 2
31
+ fig, axes = plt.subplots(1, 2, figsize=(16, 6))
32
+
33
+ # --- Left Side: The Image ---
34
+ img_filename = item['image']
35
+
36
+ # Handle cases where image path might be absolute or relative
37
+ if os.path.isabs(img_filename):
38
+ img_path = img_filename
39
+ else:
40
+ img_path = os.path.join(image_root_folder, img_filename)
41
+
42
+ try:
43
+ img = Image.open(img_path).convert('RGB')
44
+ axes[0].imshow(img)
45
+ axes[0].axis('off')
46
+ axes[0].set_title(f"ID: {item['id']}\nGT: {item['text']}", fontsize=10)
47
+ except Exception as e:
48
+ axes[0].text(0.5, 0.5, f"Image Not Found:\n{img_filename}",
49
+ ha='center', va='center', fontsize=12)
50
+ axes[0].axis('off')
51
+
52
+ # --- Right Side: Resolution vs Answer Flow ---
53
+ resolutions = item['res'] # e.g., [64, 128, 256...]
54
+ answers = item['ms_answers'] # e.g., ["dog", "dog", "cat"...]
55
+ # Normalize all answers for fair comparison
56
+ norm_answers = [_normalize_answer(a) for a in answers]
57
+ if len(norm_answers) >= high_res_group_num:
58
+ high_res_group = norm_answers[-high_res_group_num:]
59
+ else:
60
+ high_res_group = norm_answers # Fallback for short lists
61
+ # Find the consensus answer
62
+ counts = Counter(high_res_group)
63
+ target_answer, freq = counts.most_common(1)[0]
64
+
65
+ # --- RULE 3: UNSTABLE / HOPELESS ---
66
+ # If the high-res views strongly disagree (e.g., ["A", "B", "C"]), ignore sample.
67
+ # Logic: If frequency is 1 (all different), it's unstable.
68
+ if len(high_res_group) >= high_res_group_num and freq < 2:
69
+ print('Skipping unstable sample ID:', item['id'])
70
+ plt.close(fig)
71
+ return # Skip plotting
72
+
73
+ # --- STEP 2: Categorize Indices ---
74
+ # Which resolutions got it "Right" vs "Wrong"?
75
+ correct_indices = [i for i, ans in enumerate(norm_answers) if (target_answer in ans or ans in target_answer)]
76
+ wrong_indices = [i for i, ans in enumerate(norm_answers) if (target_answer not in ans and ans not in target_answer)]
77
+
78
+ # --- RULE 1: EASY / CONSISTENT CASE ---
79
+ # If EVERYTHING is correct (or almost everything, specifically low res),
80
+ # we want to teach efficiency.
81
+ if len(wrong_indices) == 0:
82
+ print('Skipping consistent sample ID:', item['id'])
83
+ plt.close(fig)
84
+ return
85
+
86
+ # Clean answers for display (wrap long text)
87
+ wrapped_answers = ['\n'.join(textwrap.wrap(str(a), width=20)) for a in answers]
88
+
89
+ # Assign colors: Green if matches Ground Truth, Red if not (if GT exists), else Blue
90
+ gt = str(item['text']).lower().strip() if item['text'] else None
91
+
92
+ # Simple heuristic to determine "Final Consensus" if GT is missing or complex
93
+ final_answer = str(answers[-1]).lower().strip()
94
+
95
+ if target_answer not in gt and gt not in target_answer:
96
+ print('Skipping sample with complex/no GT ID:', item['id'])
97
+ plt.close(fig)
98
+ return
99
+
100
+ bar_colors = []
101
+ for ans in answers:
102
+ ans_clean = str(ans).lower().strip()
103
+ # Your specific containment logic
104
+ if gt and (gt in ans_clean or ans_clean in gt):
105
+ bar_colors.append('#2ca02c') # Green (Correct)
106
+ elif (final_answer in ans_clean or ans_clean in final_answer):
107
+ bar_colors.append('#1f77b4') # Blue (Consistent with final)
108
+ else:
109
+ bar_colors.append('#d62728') # Red (Inconsistent/Wrong)
110
+
111
+ # Create a step plot or scatter plot
112
+ y_pos = range(len(resolutions))
113
+
114
+ # Draw connecting lines first
115
+ axes[1].plot(y_pos, [0] * len(y_pos), color='gray', alpha=0.3, zorder=1) # Baseline
116
+
117
+ # Draw scattered points with labels
118
+ axes[1].scatter(y_pos, [0] * len(y_pos), s=100, c=bar_colors, zorder=2)
119
+
120
+ # Annotate texts
121
+ for idx, (res, ans_text, color) in enumerate(zip(resolutions, wrapped_answers, bar_colors)):
122
+ # Alternate height to avoid overlapping text
123
+ vertical_offset = 0.2 if idx % 2 == 0 else -0.2
124
+
125
+ # Highlight changed answers
126
+ font_weight = 'bold' if idx > 0 and answers[idx] != answers[idx - 1] else 'normal'
127
+
128
+ axes[1].text(idx, vertical_offset, ans_text,
129
+ ha='center',
130
+ va='center' if vertical_offset == 0 else ('bottom' if vertical_offset > 0 else 'top'),
131
+ fontsize=9, fontweight=font_weight, color='black',
132
+ bbox=dict(boxstyle="round,pad=0.3", fc="white", ec=color, alpha=0.9))
133
+
134
+ axes[1].text(idx, -0.05, f"{res}px", ha='center', va='top', fontsize=8, color='gray')
135
+
136
+ # Formatting right ax
137
+ axes[1].set_title(f"Prompt: {item['prompt'][:60]}...", fontsize=10)
138
+ axes[1].set_xlim(-0.5, len(resolutions) - 0.5)
139
+ axes[1].set_ylim(-1, 1)
140
+ axes[1].axis('off') # Hide standard axes
141
+
142
+ # Add legend/info
143
+ info_text = "Logic:\nGreen = Matches GT\nBlue = Matches Final Res\nRed = Mismatch"
144
+ axes[1].text(0.95, 0.05, info_text, transform=axes[1].transAxes,
145
+ fontsize=8, ha='right', va='bottom', bbox=dict(facecolor='white', alpha=0.5))
146
+
147
+ plt.tight_layout()
148
+
149
+ if save_path:
150
+ plt.savefig(save_path)
151
+
152
+ if show_plot:
153
+ plt.show()
154
+
155
+ plt.close(fig)
156
+
157
+
158
+ def visualize_resolution_consistency(pkl_path, image_root_folder, save_dir='../excluded/vis_results', num_samples=10):
159
+ """
160
+ Visualizes samples from the pickle file to check consistency hypothesis.
161
+ """
162
+
163
+ # 1. Load Data
164
+ print(f"Loading data from {pkl_path}...")
165
+ try:
166
+ with open(pkl_path, 'rb') as f:
167
+ data = pickle.load(f)
168
+ except Exception as e:
169
+ print(f"Error loading pickle: {e}")
170
+ return
171
+
172
+ # Convert dict to list for easier slicing
173
+ # Note: Retained your specific slicing logic
174
+ samples = list(data.values())[150:]
175
+ selected_samples = samples
176
+
177
+ if not os.path.exists(save_dir):
178
+ os.makedirs(save_dir)
179
+
180
+ print(f"Generating plots for {len(selected_samples)} samples...")
181
+
182
+ # 3. Plotting Loop
183
+ for i, item in enumerate(selected_samples):
184
+ plot_single_sample(item, image_root_folder, save_path=None, show_plot=True)
185
+
186
+ print(f"Done! Check the '{save_dir}' folder.")
187
+
188
+ # --- Usage ---
189
+ if __name__ == "__main__":
190
+ # Update these paths to match your setup
191
+ PKL_FILE = "/home/yuheng/datasets/roi_vcot68k_ms.pkl"
192
+ IMAGE_ROOT = "/home/yuheng/datasets"
193
+
194
+ # Run visualization
195
+ visualize_resolution_consistency(PKL_FILE, IMAGE_ROOT, num_samples=50)
QWENVL-PRIVATE/analysis/check_pseudo_label.py ADDED
@@ -0,0 +1,630 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import numpy as np
4
+ from PIL import Image
5
+ import matplotlib.pyplot as plt
6
+ import matplotlib.patches as patches
7
+ from matplotlib.colors import ListedColormap, BoundaryNorm
8
+ import torch
9
+ import torchvision.transforms.functional as torchvision_F
10
+ from tqdm import tqdm
11
+ #from denoise_textual_file import refine_pseudo_labels_with_doctr
12
+ # Add the parent directory to sys.path to import qwen_src
13
+ import sys
14
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
15
+ from qwen_src.mm_utils import expand2square, create_pseudo_labels, get_foreground_mask
16
+
17
+ def plot_attention_analysis(image_path, sink_attn, grounding_attn_o2i, grounding_attn, is_sink_token = None,
18
+ save_path=None, figsize=(15, 4), fg_bbox = None):
19
+ """
20
+ Plot image and attention maps.
21
+
22
+ Args:
23
+ image_path: str, path to the image file
24
+ sink_attn: ndarray (576,), layer 2 attention weights
25
+ grounding_attn_o2i: ndarray (576,), grounding attention object to image
26
+ grounding_attn: ndarray (576,), grounding attention
27
+ is_sink_token: ndarray (576,) in bool type to indicate whether this token is a sink token
28
+ save_path: str, optional path to save the plot
29
+ figsize: tuple, figure size
30
+ """
31
+ # Create figure with subplots
32
+ fig, axes = plt.subplots(1, 4, figsize=figsize, dpi=200)
33
+ h,w = sink_attn.shape[0], sink_attn.shape[1] if len(sink_attn.shape) > 1 else 24
34
+ # Plot 1: Original Image
35
+ if os.path.exists(image_path):
36
+ img = Image.open(image_path)
37
+ axes[0].imshow(img)
38
+ axes[0].set_title('Original Image')
39
+ axes[0].axis('off')
40
+ else:
41
+ axes[0].text(0.5, 0.5, f'Image not found:\n{image_path}',
42
+ ha='center', va='center', transform=axes[0].transAxes)
43
+ axes[0].set_title('Original Image')
44
+ axes[0].axis('off')
45
+
46
+ # Reshape attention arrays to 24x24
47
+ sink_attn_2d = sink_attn.reshape(h, w)
48
+ grounding_attn_o2i_2d = grounding_attn_o2i.reshape(h, w)
49
+ grounding_attn_2d = grounding_attn.reshape(h, w)
50
+ if is_sink_token is not None:
51
+ is_sink_token_2d = is_sink_token.reshape(h, w)
52
+
53
+
54
+ # Plot 2: Special handling for Pseudo-Label map vs. continuous attention
55
+ # Check if the data is likely discrete pseudo-labels (-1, 0, 1)
56
+ if -1 in sink_attn:
57
+ # --- Discrete Colormap for Pseudo-Labels ---
58
+ axes[1].set_title('Pseudo Label Map')
59
+ # Define the colors: Ignore (-1)->Gray, Background (0)->Dark Purple, Foreground (1)->Yellow
60
+ colors = ['gray', 'black', '#FFE699']
61
+ cmap_discrete = ListedColormap(colors)
62
+ # Define the boundaries for the colormap: [-1.5, -0.5), [-0.5, 0.5), [0.5, 1.5]
63
+ boundaries = [-1.5, -0.5, 0.5, 1.5]
64
+ norm = BoundaryNorm(boundaries, cmap_discrete.N)
65
+ im1 = axes[1].imshow(sink_attn_2d, cmap=cmap_discrete, norm=norm, aspect='auto')
66
+ # Customize the colorbar for discrete values
67
+ cbar = plt.colorbar(im1, ax=axes[1], shrink=0.6)
68
+ cbar.set_ticks([-1, 0, 1])
69
+ cbar.set_ticklabels(['Ignore', 'BG', 'FG'])
70
+ else:
71
+ # --- Continuous Colormap for standard attention ---
72
+ axes[1].set_title('Layer 2 Attention')
73
+ im1 = axes[1].imshow(sink_attn_2d, cmap='viridis', aspect='auto')
74
+ plt.colorbar(im1, ax=axes[1], shrink=0.6)
75
+
76
+ # Plot 3: Grounding Attention O2I
77
+ im2 = axes[2].imshow(grounding_attn_o2i_2d, cmap='inferno', aspect='auto')
78
+ axes[2].set_title('Grounding Attn O2I')
79
+ axes[2].axis('off')
80
+ plt.colorbar(im2, ax=axes[2], shrink=0.6)
81
+
82
+ # Plot 4: Grounding Attention Q2I
83
+ im3 = axes[3].imshow(grounding_attn_2d, cmap='inferno', aspect='auto')
84
+ axes[3].set_title('Grounding Attn')
85
+ axes[3].axis('off')
86
+ plt.colorbar(im3, ax=axes[3], shrink=0.6)
87
+
88
+ # Highlight sink tokens on the attention maps
89
+ if is_sink_token is not None:
90
+ # Find the coordinates of all sink tokens
91
+ sink_coords = np.argwhere(is_sink_token_2d)
92
+ for row, col in sink_coords:
93
+ # Define the properties of the highlight box
94
+ rect_args = {
95
+ 'xy': (col - 0.5, row - 0.5), # Bottom-left corner of the cell
96
+ 'width': 1,
97
+ 'height': 1,
98
+ 'linewidth': 2.5,
99
+ 'edgecolor': '#7CFC00', # A bright, "LawnGreen" color
100
+ 'facecolor': 'none'
101
+ }
102
+ # Add a new rectangle patch to each of the three attention plots
103
+ #axes[1].add_patch(patches.Rectangle(**rect_args))
104
+ axes[2].add_patch(patches.Rectangle(**rect_args))
105
+ #axes[3].add_patch(patches.Rectangle(**rect_args))
106
+
107
+ if fg_bbox is not None:
108
+ fg_rect_arg = {
109
+ 'xy': (fg_bbox[1] - 0.5, fg_bbox[0] - 0.5), # Bottom-left corner of the cell
110
+ 'width': fg_bbox[3] - fg_bbox[1] + 1,
111
+ 'height': fg_bbox[2] - fg_bbox[0] + 1,
112
+ 'linewidth': 2.5,
113
+ 'edgecolor': '#FF4500', # A bright, "OrangeRed" color
114
+ 'facecolor': 'none'
115
+ }
116
+ axes[1].add_patch(patches.Rectangle(**fg_rect_arg))
117
+
118
+ plt.tight_layout()
119
+
120
+ if save_path:
121
+ plt.savefig(save_path, dpi=300, bbox_inches='tight')
122
+
123
+ plt.show()
124
+ pass
125
+
126
+
127
+ def save_attention_maps_separately(
128
+ image_path,
129
+ sink_attn,
130
+ grounding_attn_o2i,
131
+ grounding_attn,
132
+ save_dir,
133
+ is_sink_token=None,
134
+ fg_bbox=None
135
+ ):
136
+ """
137
+ Saves the original image and three attention maps as separate, clean image files
138
+ with no axes, titles, colorbars, or whitespace.
139
+
140
+ Args:
141
+ image_path (str): Path to the image file.
142
+ sink_attn (np.ndarray): Array (576,) for the first map (can be discrete or continuous).
143
+ grounding_attn_o2i (np.ndarray): Array (576,) for the second attention map.
144
+ grounding_attn (np.ndarray): Array (576,) for the third attention map.
145
+ save_dir (str): Directory where the output images will be saved.
146
+ is_sink_token (np.ndarray, optional): Boolean array (576,) to highlight sink tokens.
147
+ fg_bbox (list or tuple, optional): Bounding box [y_min, x_min, y_max, x_max] for the foreground.
148
+ """
149
+ # Ensure the save directory exists
150
+ if not os.path.exists(save_dir):
151
+ os.makedirs(save_dir)
152
+
153
+ # --- 1. Save Original Image ---
154
+ if os.path.exists(image_path):
155
+ img = Image.open(image_path)
156
+ img.save(os.path.join(save_dir, "original_image.png"))
157
+
158
+ # Reshape attention arrays and masks to 24x24
159
+ map1_data = sink_attn.reshape(h, w)
160
+ map2_data = grounding_attn_o2i.reshape(h, w)
161
+ map3_data = grounding_attn.reshape(h, w)
162
+
163
+ is_sink_token_2d = is_sink_token.reshape(h, w) if is_sink_token is not None else None
164
+
165
+ # --- 2. Process and Save the Three Attention Maps ---
166
+ map_data_list = [map1_data, map2_data, map3_data]
167
+ map_names = ["pseudo_label_or_attn_map", "grounding_attn_o2i", "grounding_attn"]
168
+ cmaps = ['viridis', 'inferno', 'inferno'] # Default cmaps
169
+
170
+ for i, (data, name) in enumerate(zip(map_data_list, map_names)):
171
+ fig, ax = plt.subplots(1, 1, figsize=(5, 5))
172
+ ax.axis('off')
173
+
174
+ # --- Plotting Logic ---
175
+ # Special handling for the first map if it's a discrete pseudo-label
176
+ if i == 0 and -1 in sink_attn:
177
+ colors = ['gray', 'black', '#FFE699']
178
+ cmap_discrete = ListedColormap(colors)
179
+ boundaries = [-1.5, -0.5, 0.5, 1.5]
180
+ norm = BoundaryNorm(boundaries, cmap_discrete.N)
181
+ ax.imshow(data, cmap=cmap_discrete, norm=norm, aspect='auto')
182
+ else:
183
+ ax.imshow(data, cmap=cmaps[i], aspect='auto')
184
+
185
+ # --- Overlay Logic ---
186
+ # Add sink token highlights (on all three maps)
187
+ if is_sink_token_2d is not None and i==1:
188
+ sink_coords = np.argwhere(is_sink_token_2d)
189
+ for row, col in sink_coords:
190
+ ax.add_patch(patches.Rectangle(
191
+ (col - 0.5, row - 0.5), 1, 1,
192
+ linewidth=2.5, edgecolor='#7CFC00', facecolor='none'
193
+ ))
194
+
195
+ # Add FG bounding box (only on the first map)
196
+ if i == 0 and fg_bbox is not None:
197
+ ax.add_patch(patches.Rectangle(
198
+ (fg_bbox[1] - 0.5, fg_bbox[0] - 0.5),
199
+ fg_bbox[3] - fg_bbox[1] + 1,
200
+ fg_bbox[2] - fg_bbox[0] + 1,
201
+ linewidth=2.5, edgecolor='#FF4500', facecolor='none'
202
+ ))
203
+
204
+ # --- Save the clean plot ---
205
+ save_path = os.path.join(save_dir, f"{name}.png")
206
+ fig.savefig(save_path, dpi=300, bbox_inches='tight', pad_inches=0)
207
+ plt.close(fig) # Close the figure to free memory
208
+
209
+ print(f"Successfully saved 4 images to '{save_dir}'")
210
+
211
+
212
+
213
+ def get_foreground_bbox(array_2d):
214
+ """
215
+ Get bounding box of foreground (non-zero) area in a 2D array.
216
+
217
+ Args:
218
+ array_2d: numpy array of shape (24, 24) or any 2D array
219
+
220
+ Returns:
221
+ tuple: (min_row, min_col, max_row, max_col) or None if no foreground
222
+ - min_row, min_col: top-left corner of bounding box
223
+ - max_row, max_col: bottom-right corner of bounding box (inclusive)
224
+ """
225
+ # Find all non-zero positions
226
+ fg_positions = np.where(array_2d > 0)
227
+ full_bbox = (0, 0, array_2d.shape[0] - 1, array_2d.shape[1] - 1)
228
+ # Check if there are any foreground pixels
229
+ if len(fg_positions[0]) == 0:
230
+ return full_bbox # return full image as bbox if no foreground
231
+
232
+ # Get bounding box coordinates
233
+ min_row = np.min(fg_positions[0])
234
+ max_row = np.max(fg_positions[0])
235
+ min_col = np.min(fg_positions[1])
236
+ max_col = np.max(fg_positions[1])
237
+
238
+ return (min_row, min_col, max_row, max_col)
239
+
240
+ def make_jsonl_file(json_path, save_path, save_name = 'llava_v1_5_selected_qa.jsonl', data_path='/home/yuheng/datasets', qa_pair='first'):
241
+ """
242
+ Save the json content to a jsonl file.
243
+ """
244
+ json_content = json.load(open(json_path, "r"))
245
+ os.makedirs(os.path.dirname(save_path), exist_ok=True)
246
+ save_file = os.path.join(save_path, save_name)
247
+ all_results = []
248
+ for idx, ele in enumerate(json_content):
249
+ if 'image' in ele.keys():# and 'coco' not in ele['image']:
250
+ question_id = str(idx)
251
+ image = os.path.join(data_path, ele['image'])
252
+ conversations = ele['conversations']
253
+ if qa_pair == 'first':
254
+ text = conversations[0]['value'].replace('<image>\n', '').replace('\n<image>', '')
255
+ else:
256
+ raise NotImplementedError(f"qa_pair={qa_pair} not implemented")
257
+ if '<image>' in text:
258
+ pass
259
+ result = {
260
+ "question_id": question_id,
261
+ "image": image,
262
+ "text": text,
263
+ "category": "default",
264
+ }
265
+ all_results.append(result)
266
+ else: continue
267
+ with open(save_file, "w") as f:
268
+ for result in all_results:
269
+ f.write(json.dumps(result) + "\n")
270
+ print(f"Saved {len(all_results)} results to {save_file}")
271
+
272
+ def replace_img_path(pkl_path='/home/yuheng/datasets/llava_v1_5_pseudo_roi.pkl', data_path='/home/yuheng/datasets/'):
273
+ """
274
+ Replace the image path in the json file with the data path.
275
+ """
276
+ import pickle
277
+ pkl_content = pickle.load(open(pkl_path, "rb"))
278
+ all_results = []
279
+ for sample in pkl_content:
280
+ sample['image'] = sample['image'].replace(data_path, '')
281
+ all_results.append(sample)
282
+ dst_pkl_path = pkl_path.replace('.pkl', '_replaced.pkl')
283
+ with open(dst_pkl_path, "wb") as f:
284
+ pickle.dump(all_results, f)
285
+
286
+ def replace_prompt(pkl_path='/home/yuheng/datasets/qwen2_5vl_pseudo_3b_576res_v5.pkl',
287
+ data_path='/home/yuheng/datasets/',
288
+ dest_pkl_path='/home/yuheng/datasets/qwen2_5vl_pseudo_3b_576res_release.pkl'):
289
+ """
290
+ Replace the image path in the json file with the data path.
291
+ """
292
+ import pickle
293
+ pkl_content = pickle.load(open(pkl_path, "rb"))
294
+ all_results = []
295
+ for sample in pkl_content:
296
+ sample['prompt'] = sample['image'].replace('Output grounding bounding box related to the question in JSON.',
297
+ 'Answer the question using a single word or phrase.')
298
+ all_results.append(sample)
299
+ if dest_pkl_path is None:
300
+ dst_pkl_path = pkl_path.replace('.pkl', '_replaced.pkl')
301
+ else:
302
+ dst_pkl_path = dest_pkl_path
303
+ with open(dst_pkl_path, "wb") as f:
304
+ pickle.dump(all_results, f)
305
+
306
+
307
+ def refine_pseudo_labels_with_debug(original_image, pseudo_labels, std_threshold=5.0, grid_size=(24, 24), debug=True):
308
+ # 1. Prepare Image
309
+ if isinstance(original_image, Image.Image):
310
+ img_arr = np.array(original_image)
311
+ else:
312
+ img_arr = original_image
313
+
314
+ # Create Intensity Map (Max of RGB)
315
+ if len(img_arr.shape) == 3:
316
+ intensity_map = np.max(img_arr, axis=2)
317
+ else:
318
+ intensity_map = img_arr
319
+
320
+ H, W = intensity_map.shape
321
+ h_grid, w_grid = grid_size
322
+
323
+ # Reshape labels if flat
324
+ if pseudo_labels.shape != grid_size:
325
+ pseudo_labels = pseudo_labels.reshape(h_grid, w_grid)
326
+
327
+ refined_labels = pseudo_labels.copy()
328
+
329
+ step_h = H // h_grid
330
+ step_w = W // w_grid
331
+
332
+ rows, cols = np.where(pseudo_labels == 1)
333
+
334
+ # --- DEBUG COUNTER ---
335
+ debug_count = 0
336
+ MAX_DEBUG_PLOTS = 5 # Stop after showing this many plots
337
+
338
+ print(f"Checking {len(rows)} foreground tokens...")
339
+
340
+ for r, c in zip(rows, cols):
341
+ y_start = r * step_h
342
+ y_end = min((r + 1) * step_h, H)
343
+ x_start = c * step_w
344
+ x_end = min((c + 1) * step_w, W)
345
+
346
+ # Extract patches
347
+ intensity_patch = intensity_map[y_start:y_end, x_start:x_end]
348
+
349
+ # We also extract the RGB patch just for visualization
350
+ rgb_patch = img_arr[y_start:y_end, x_start:x_end]
351
+
352
+ if intensity_patch.size > 0:
353
+ patch_std = np.std(intensity_patch)
354
+
355
+ # --- VISUALIZATION BLOCK ---
356
+ if debug and debug_count < MAX_DEBUG_PLOTS:
357
+ fig, axes = plt.subplots(1, 2, figsize=(8, 4))
358
+
359
+ # Plot 1: Original RGB Patch (What the human sees)
360
+ axes[0].imshow(rgb_patch)
361
+ axes[0].set_title(f"RGB Patch\nGrid: ({r},{c})")
362
+ axes[0].axis('off')
363
+
364
+ # Plot 2: Intensity Patch (What the algo sees)
365
+ # Using 'jet' or 'magma' to visualize intensity better than gray
366
+ im = axes[1].imshow(intensity_patch, cmap='magma', vmin=0, vmax=255)
367
+ axes[1].set_title(f"Max-RGB Intensity\nStd Dev: {patch_std:.2f}")
368
+ axes[1].axis('off')
369
+
370
+ # Add status text
371
+ status = "KEEP" if patch_std >= std_threshold else "DROP (Too Flat)"
372
+ plt.suptitle(f"Token Analysis: {status} (Thresh: {std_threshold})",
373
+ color='green' if patch_std >= std_threshold else 'red', weight='bold')
374
+
375
+ plt.tight_layout()
376
+ plt.show()
377
+
378
+ debug_count += 1
379
+ # ---------------------------
380
+
381
+ # 3. Apply Threshold
382
+ if patch_std < std_threshold:
383
+ refined_labels[r, c] = 0 # Revert to Background
384
+
385
+ return refined_labels
386
+ def check_pseudo_label(dst_pkl_path, dataset_path = None):
387
+ """
388
+ Check the pseudo label for different datasets.
389
+ """
390
+ import pickle #10, 26, 62, 71, 102
391
+ from doctr.models import ocr_predictor
392
+ predictor = ocr_predictor(det_arch='fast_base', reco_arch='crnn_vgg16_bn', pretrained=True).cuda()
393
+ tmp_pkl_content = pickle.load(open(dst_pkl_path, "rb"))
394
+ for i in range(0, len(tmp_pkl_content)):
395
+ img_path = tmp_pkl_content[i]['image']
396
+ if dataset_path is not None: img_path = os.path.join(dataset_path, img_path)
397
+ #if 'ocr' not in img_path: continue
398
+ prompt = tmp_pkl_content[i]['prompt']
399
+ # if not ('left' in prompt or 'right' in prompt or 'top' in prompt or 'bottom' in prompt):
400
+ # continue
401
+ response = tmp_pkl_content[i]['text']
402
+ print(f"Prompt: {prompt}\nResponse: {response}")
403
+ # if 'Is the bear sleeping?' not in prompt:
404
+ # continue # only check the specific prompt
405
+ grounding_attn_o2i = tmp_pkl_content[i]['grounding_attn_o2i']
406
+ sink_attn = tmp_pkl_content[i]['sink_attn']
407
+ # if 'Who is the author of this book?' not in prompt: continue
408
+ # if 'Suzanne Van Atten' not in tmp_pkl_content[i]['text']: continue
409
+ image = Image.open(img_path).convert('RGB')
410
+ image_size = image.size
411
+ known_fg_mask = get_foreground_mask(image_size, grounding_attn_o2i.shape[0], min_dimension_size=1)
412
+ tmp_attn = grounding_attn_o2i.copy()
413
+ tmp_attn[~known_fg_mask] = 0.
414
+ tmp_attn_binary = (tmp_attn > tmp_attn.max()*0.05).astype(np.float32)
415
+ # refined_labels = refine_pseudo_labels_with_debug(
416
+ # original_image=expand2square(image, background_color=(127,127,127)),
417
+ # pseudo_labels=tmp_attn_binary,
418
+ # std_threshold=4.0,
419
+ # grid_size=(24, 24),
420
+ # debug=False
421
+ # )
422
+ refined_labels = refine_pseudo_labels_with_doctr(
423
+ original_image=expand2square(image, background_color=(127,127,127)),
424
+ pseudo_labels=tmp_attn_binary,
425
+ grid_size=(24, 24),
426
+ predictor=predictor, # Pass the loaded model
427
+ debug=False
428
+ )
429
+
430
+ grounding_attn_o2i_refined = grounding_attn_o2i * refined_labels
431
+
432
+
433
+ # pseudo_set = create_pseudo_labels(sink_attn, grounding_attn_o2i_refined, sink_thresh=0.02, bg_coff=0.1, binary_coff=0.2,
434
+ # original_image_size=image_size, for_vis=True, max_ratio_limit=0.5)
435
+ #
436
+ # plot_attention_analysis(
437
+ # image_path=img_path,
438
+ # sink_attn=pseudo_set['labels'],
439
+ # grounding_attn_o2i=grounding_attn_o2i,
440
+ # grounding_attn=pseudo_set['grounding_attn'],
441
+ # is_sink_token=pseudo_set['sink_token_mask'],
442
+ # fg_bbox=pseudo_set['fg_bbox'],
443
+ # )
444
+ #
445
+ pseudo_set = create_pseudo_labels(sink_attn, grounding_attn_o2i, sink_thresh=0.02, bg_coff=0.05, binary_coff=0.2,
446
+ original_image_size=image_size, for_vis=True, max_ratio_limit=0.5)
447
+
448
+
449
+ plot_attention_analysis(
450
+ image_path=img_path,
451
+ sink_attn=pseudo_set['labels'],
452
+ grounding_attn_o2i=grounding_attn_o2i,
453
+ grounding_attn=pseudo_set['grounding_attn'],
454
+ is_sink_token=pseudo_set['sink_token_mask'],
455
+ fg_bbox=pseudo_set['fg_bbox'],
456
+ )
457
+
458
+ # save_attention_maps_separately(
459
+ # image_path=img_path,
460
+ # sink_attn=pseudo_labels,
461
+ # grounding_attn_o2i=grounding_attn_o2i,
462
+ # grounding_attn=grounding_attn,
463
+ # save_dir='/home/yuheng/code/llava/exclude/paper_plot',
464
+ # is_sink_token=sink_tokens,
465
+ # fg_bbox=pseudo_set['fg_bbox']
466
+ # )
467
+ pass
468
+
469
+ pass
470
+
471
+ def refine_base_attn_with_grounding(grounding_pkl_path, base_pkl_path, dst_pkl_path, dataset_path):
472
+ """
473
+ Refine the base attention with grounding attention.
474
+ """
475
+ import pickle
476
+ grounding_pkl_content = pickle.load(open(grounding_pkl_path, "rb"))
477
+ base_pkl_content = pickle.load(open(base_pkl_path, "rb"))
478
+ assert len(grounding_pkl_content) == len(base_pkl_content), "The length of grounding and base pkl files should be the same."
479
+ all_results = []
480
+ sink_thresh, bg_coff, binary_coff = 0.02, 0.1, 0.2
481
+ for i in tqdm(range(0, len(grounding_pkl_content))):
482
+ assert grounding_pkl_content[i]['question_id'] == base_pkl_content[i]['question_id'], "The question_id should be the same."
483
+ assert grounding_pkl_content[i]['image'] == base_pkl_content[i]['image'], "The image path should be the same."
484
+ img_path = base_pkl_content[i]['image']
485
+ if dataset_path is not None: img_path = os.path.join(dataset_path, img_path)
486
+ image = Image.open(img_path).convert('RGB')
487
+ image_size = image.size
488
+ grounding_sample = grounding_pkl_content[i]
489
+ grounding_pseudo_set = create_pseudo_labels(grounding_sample['sink_attn'],
490
+ grounding_sample['grounding_attn_o2i'],
491
+ sink_thresh=sink_thresh,
492
+ bg_coff=bg_coff,
493
+ binary_coff=binary_coff,
494
+ original_image_size=image_size,
495
+ for_vis=True,
496
+ max_ratio_limit=0.5,
497
+ use_smoothing=True
498
+ )
499
+ grounding_bg_mask = grounding_pseudo_set['labels'] == 0 # background mask
500
+ # plot_attention_analysis(
501
+ # image_path=img_path,
502
+ # sink_attn=grounding_pseudo_set['labels'],
503
+ # grounding_attn_o2i=grounding_sample['grounding_attn_o2i'],
504
+ # grounding_attn=grounding_pseudo_set['grounding_attn'],
505
+ # is_sink_token=grounding_pseudo_set['sink_token_mask'],
506
+ # fg_bbox=grounding_pseudo_set['fg_bbox'],
507
+ # )
508
+
509
+ # Refine the base attention with grounding background mask
510
+ base_sample = base_pkl_content[i]
511
+ base_grounding_attn_o2i = base_sample['grounding_attn_o2i']
512
+ feat_shape = base_grounding_attn_o2i.shape
513
+ base_grounding_attn_o2i = base_grounding_attn_o2i.flatten()
514
+ base_grounding_attn_o2i[grounding_bg_mask.flatten()] = 0.
515
+ base_grounding_attn_o2i = base_grounding_attn_o2i.reshape(feat_shape)
516
+ # base_pseudo_set = create_pseudo_labels(base_sample['sink_attn'],
517
+ # base_grounding_attn_o2i,
518
+ # sink_thresh=sink_thresh,
519
+ # bg_coff=bg_coff,
520
+ # binary_coff=binary_coff,
521
+ # original_image_size=image_size,
522
+ # for_vis=True,
523
+ # max_ratio_limit=0.5)
524
+ # plot_attention_analysis(
525
+ # image_path=img_path,
526
+ # sink_attn=base_pseudo_set['labels'],
527
+ # grounding_attn_o2i=base_grounding_attn_o2i,
528
+ # grounding_attn=base_pseudo_set['grounding_attn'],
529
+ # is_sink_token=base_pseudo_set['sink_token_mask'],
530
+ # fg_bbox=base_pseudo_set['fg_bbox'],
531
+ # )
532
+
533
+ refined_sample = {
534
+ "question_id": base_sample['question_id'],
535
+ "prompt": base_sample['prompt'],
536
+ "text": base_sample['text'],
537
+ "image": base_sample['image'],
538
+ "sink_attn": base_sample['sink_attn'],
539
+ "grounding_attn_o2i": base_grounding_attn_o2i
540
+ }
541
+
542
+ all_results.append(refined_sample)
543
+ pass
544
+
545
+ with open(dst_pkl_path, "wb") as f:
546
+ pickle.dump(all_results, f)
547
+ print(f"Saved {len(all_results)} results to {dst_pkl_path}")
548
+ return dst_pkl_path
549
+
550
+ def merge_pkls(pkl_list, dst_path, dst_pkl_name='llava_v1_5_pseudo_roi.pkl'):
551
+ """
552
+ Get the pkl files from the list and save them to a single pkl file.
553
+ """
554
+ import pickle
555
+ all_results = []
556
+ for pkl_path in pkl_list:
557
+ tmp_pkl_content = pickle.load(open(pkl_path, "rb"))
558
+ all_results.extend(tmp_pkl_content)
559
+ os.makedirs(dst_path, exist_ok=True)
560
+ dst_pkl_path = os.path.join(dst_path, dst_pkl_name)
561
+ with open(dst_pkl_path, "wb") as f:
562
+ pickle.dump(all_results, f)
563
+ print(f"Saved {len(all_results)} results to {dst_pkl_path}")
564
+ return dst_pkl_path
565
+
566
+
567
+ # dataset_dir = '/home/yuheng/datasets/'
568
+ # dst_json_path = os.path.join(dataset_dir,'llava_v1_5_mix665k.json') #"/home/yuheng/datasets/llava_v1_5_mix665k.json"
569
+ # tmp_json_content = json.load(open(dst_json_path, "r"))
570
+ # json_content_nococo = [ele for ele in tmp_json_content if 'image' in ele.keys() and 'coco' not in ele['image']]
571
+ # coco = [ele for ele in tmp_json_content if 'image' in ele.keys() and 'coco' in ele['image']]
572
+ # vg_100k = [ele for ele in json_content_nococo if 'image' in ele.keys() and 'VG_100K' in ele['image']]
573
+ # gqa = [ele for ele in json_content_nococo if 'image' in ele.keys() and 'gqa' in ele['image']]
574
+ # textvqa = [ele for ele in json_content_nococo if 'image' in ele.keys() and 'textvqa' in ele['image']]
575
+ # ocr_vqa = [ele for ele in json_content_nococo if 'image' in ele.keys() and 'ocr_vqa' in ele['image']]
576
+ # pass
577
+ #
578
+ #
579
+ # test_json_152k = os.path.join(dataset_dir,'llava_v1_5_mix152k_test.json') #'/home/yuheng/datasets/llava_v1_5_mix152k_test.json'
580
+ # test_json_152k_content = gqa + ocr_vqa
581
+ # with open(test_json_152k, "w") as f:
582
+ # json.dump(test_json_152k_content, f, indent=2)
583
+
584
+ # test_json_238k = os.path.join(dataset_dir,'llava_v1_5_mix238k_test.json') #'/home/yuheng/datasets/llava_v1_5_mix152k_test.json'
585
+ # test_json_238k_content = gqa + ocr_vqa + vg_100k
586
+ # with open(test_json_238k, "w") as f:
587
+ # json.dump(test_json_238k_content, f, indent=2)
588
+
589
+
590
+
591
+ # make_jsonl_file(json_path='/home/yuheng/datasets/llava_v1_5_mix665k.json',
592
+ # save_path='/home/yuheng/datasets',
593
+ # save_name='llava_v1_5_mix665k_selected_qa.jsonl',
594
+ # data_path='/home/yuheng/datasets',
595
+ # qa_pair='first')
596
+
597
+ if __name__ == "__main__":
598
+ # make_jsonl_file(json_path='/mnt/sda/yuheng/datasets/llava_v1_5_mix665k.json',
599
+ # save_path='/mnt/sda/yuheng/datasets',
600
+ # save_name='llava_v1_5_mix665k_selected_qa.jsonl',
601
+ # data_path='/mnt/sda/yuheng/datasets',
602
+ # qa_pair='first')
603
+ #replace_img_path('/home/yuheng/datasets/llava_v1_5_pseudo_roi_replaced.pkl')
604
+ #check_pseudo_label(dst_pkl_path='/mnt/sda/yuheng/datasets/llava_v1_5_pseudo_roi_replaced.pkl', dataset_path='/mnt/sda/yuheng/datasets/')
605
+ #check_pseudo_label(dst_pkl_path='/home/yuheng/datasets/qwen2_5vl_pseudo_7b_576res_229k_denoised.pkl', dataset_path='/home/yuheng/datasets/')
606
+ # check_pseudo_label(dst_pkl_path='/home/yuheng/datasets/qwen2_5vl_pseudo_7b_docvqa33k_fullPromptFix_denoised.pkl', dataset_path='/home/yuheng/datasets/')
607
+
608
+ # refine_base_attn_with_grounding(
609
+ # grounding_pkl_path='/home/yuheng/datasets/qwen2_5vl_pseudo_3b_gqa_576res_25k.pkl',
610
+ # base_pkl_path='/home/yuheng/datasets/qwen2_5vl_pseudo_3b_gqa_576res_v3_25k.pkl',
611
+ # dst_pkl_path='/home/yuheng/datasets/qwen_v2_5_pseudo_3b_gqa_refined.pkl',
612
+ # dataset_path='/home/yuheng/datasets/'
613
+ # )
614
+ # replace_prompt()
615
+ # pass
616
+
617
+ merge_pkls( pkl_list=[
618
+ '/root/epfs/sproject/datasets/qwen2_5vl_pseudo_7b_576res_release_denoisedV2.pkl',
619
+ '/root/epfs/sproject/datasets/qwen2_5vl_pseudo_7b_docvqa33k_fullPromptFix_denoised.pkl'
620
+ ],
621
+ dst_path='/root/epfs/sproject/datasets',
622
+ dst_pkl_name='qwen2_5vl_pseudo_7b_576res_185k_denoisedV2.pkl')
623
+ # # replace_img_path('/home/yuheng/datasets/llava_v1_5_13b_pseudo_roi.pkl')
624
+
625
+
626
+ # zero_array = np.zeros(576)
627
+ # plot_attention_analysis(os.path.join(dataset_dir,'gqa/images/2348106.jpg'),zero_array,zero_array,zero_array)
628
+ # pass
629
+ #{'from': 'human', 'value': 'Please provide the bounding box coordinate of the region this sentence describes: A black corner lamp.'}
630
+
QWENVL-PRIVATE/analysis/check_textvqa_json.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+
3
+ # --- 1. Load Source Data ---
4
+ # (Using your provided paths)
5
+ # textvqa_train_json = '/home/yuheng/datasets/textvqa/TextVQA_0.5.1_train.json'
6
+ # output_jsonl_file = '/home/yuheng/datasets/textvqa/converted_llava_style_train.jsonl'
7
+
8
+ textvqa_train_json = '/home/yuheng/code/llava/playground/data/eval/textvqa/TextVQA_0.5.1_val.json'
9
+ output_jsonl_file = '/home/yuheng/datasets/textvqa/converted_llava_style_textvqa_val.jsonl'
10
+
11
+ try:
12
+ # Load the entire JSON file
13
+ textvqa_train_data = json.load(open(textvqa_train_json))['data']
14
+
15
+ print(f"Loaded {len(textvqa_train_data)} items from {textvqa_train_json}")
16
+
17
+ converted_data = []
18
+
19
+ # --- 2. Iterate and Convert Each Item ---
20
+ for item in textvqa_train_data:
21
+ # Get the image_id (string) from the source
22
+ # This is used for both 'question_id' and 'image' in the target format
23
+ image_id = item.get('image_id')
24
+
25
+ # Get the raw question text
26
+ question_text = item.get('question')
27
+
28
+ # Create the new dictionary in the target format
29
+ new_item = {
30
+ "question_id": image_id,
31
+ "image": f"{image_id}.jpg",
32
+ "text": question_text, # See critical note below
33
+ "category": "default", # Hardcoded, as in your example
34
+ "answers": item.get('answers', []) # List of answers
35
+ }
36
+
37
+ converted_data.append(new_item)
38
+
39
+ # --- 3. Write to JSONL (JSON Lines) File ---
40
+ # The target format is a .jsonl file, where each line is a separate JSON object.
41
+ with open(output_jsonl_file, 'w') as f:
42
+ for entry in converted_data:
43
+ # Dump each dictionary as a string and add a newline
44
+ f.write(json.dumps(entry) + '\n')
45
+
46
+ print(f"Successfully converted and saved to {output_jsonl_file}")
47
+
48
+ # Print a sample of the first converted item
49
+ if converted_data:
50
+ print("\n--- Sample of first converted item ---")
51
+ print(json.dumps(converted_data[0], indent=2))
52
+
53
+ except FileNotFoundError:
54
+ print(f"Error: Source file not found at {textvqa_train_json}")
55
+ except Exception as e:
56
+ print(f"An error occurred: {e}")
QWENVL-PRIVATE/analysis/convert_chartqa.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ from datasets import load_dataset
4
+ from tqdm import tqdm
5
+
6
+
7
+ def process_chartqa_dataset(base_dataset_dir, output_image_dir, output_jsonl_path):
8
+ """
9
+ Downloads ChartQA, saves images, and writes JSONL with paths relative
10
+ to base_dataset_dir.
11
+ """
12
+
13
+ # 1. Create the output directory if it doesn't exist
14
+ if not os.path.exists(output_image_dir):
15
+ os.makedirs(output_image_dir)
16
+ print(f"Created directory: {output_image_dir}")
17
+
18
+ # 2. Load the dataset
19
+ print("Loading HuggingFaceM4/ChartQA dataset...")
20
+ try:
21
+ dataset = load_dataset("HuggingFaceM4/ChartQA", split="train")
22
+ except Exception as e:
23
+ print(f"Error loading dataset: {e}")
24
+ return
25
+
26
+ print(f"Processing {len(dataset)} items...")
27
+
28
+ # 3. Iterate and Process
29
+ with open(output_jsonl_path, 'w', encoding='utf-8') as f:
30
+ for idx, item in enumerate(tqdm(dataset)):
31
+ try:
32
+ # --- A. Save Image ---
33
+ image = item['image']
34
+
35
+ # Create filename
36
+ image_filename = f"chartqa_train_{idx}.png"
37
+ full_image_path = os.path.join(output_image_dir, image_filename)
38
+
39
+ # Save the image physically to the full path
40
+ image.save(full_image_path)
41
+
42
+ # --- B. Calculate Relative Path ---
43
+ # This converts /home/yuheng/datasets/ChartQA/images/img.png
44
+ # to ChartQA/images/img.png
45
+ relative_image_path = os.path.relpath(full_image_path, base_dataset_dir)
46
+
47
+ # --- C. Process Data Fields ---
48
+ question_text = item.get('query', item.get('question', ''))
49
+ raw_label = item.get('label', [])
50
+ answers_list = raw_label if isinstance(raw_label, list) else [str(raw_label)]
51
+ primary_answer = answers_list[0] if answers_list else ""
52
+
53
+ # --- D. Construct JSON Object ---
54
+ entry = {
55
+ "question_id": str(idx),
56
+ "image": relative_image_path, # <--- UPDATED: Uses relative path now
57
+ "prompt": question_text,
58
+ "category": "default",
59
+ "answers": answers_list,
60
+ "answer": primary_answer,
61
+ "dataset": "ChartQA"
62
+ }
63
+
64
+ f.write(json.dumps(entry) + "\n")
65
+
66
+ except Exception as e:
67
+ print(f"Skipping index {idx} due to error: {e}")
68
+
69
+ print(f"\nProcessing complete.")
70
+ print(f"Images saved to: {output_image_dir}")
71
+ print(f"Annotations saved to: {output_jsonl_path}")
72
+
73
+
74
+ # --- Configuration ---
75
+ if __name__ == "__main__":
76
+ # The root directory you want the paths to be relative to
77
+ BASE_ROOT = "/home/yuheng/datasets"
78
+
79
+ # Where the images and jsonl will actually be stored
80
+ # Ensure this path is inside BASE_ROOT if you want a clean relative path
81
+ IMAGE_SAVE_PATH = os.path.join(BASE_ROOT, "ChartQA/images")
82
+ JSONL_SAVE_PATH = os.path.join(BASE_ROOT, "chartqa_28k_qa.jsonl")
83
+
84
+ process_chartqa_dataset(BASE_ROOT, IMAGE_SAVE_PATH, JSONL_SAVE_PATH)
QWENVL-PRIVATE/analysis/convert_json2qwen_training.py ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import argparse
3
+ import os
4
+ import random
5
+ import glob
6
+
7
+
8
+ def fix_image_path(raw_image_id, data_root):
9
+ """
10
+ Tries to find the image in data_root.
11
+ If not found, applies dataset-specific folder corrections (like textvqa).
12
+ """
13
+ full_path = os.path.join(data_root, raw_image_id)
14
+ if os.path.exists(full_path):
15
+ return raw_image_id
16
+
17
+ # Heuristic fixes based on your previous logs
18
+ # Check if it needs 'textvqa/train_images' prefix
19
+ if "textvqa" in raw_image_id.lower() or len(raw_image_id) == 16: # e.g. 20de8a999becc896.jpg
20
+ # Try appending textvqa prefix if not present
21
+ potential_fix = os.path.join('textvqa/train_images', os.path.basename(raw_image_id))
22
+ if os.path.exists(os.path.join(data_root, potential_fix)):
23
+ print(f" Fixed path: {raw_image_id} -> {potential_fix}")
24
+ return potential_fix
25
+
26
+ # Add other dataset fixes here (DocVQA, ChartQA, etc.)
27
+ # Example for DocVQA:
28
+ # if "docvqa" in raw_image_id.lower(): ...
29
+
30
+ return raw_image_id
31
+
32
+
33
+ def convert_to_qwen_mixed(
34
+ reg_file, imp_file, output_file, data_root,
35
+ mixing_ratio=1.0, use_gt=False, seed=42,
36
+ use_short_prompt=False
37
+ ):
38
+ random.seed(seed)
39
+
40
+ # 1. Load Data
41
+ print(f"Loading Regressions: {reg_file}")
42
+ with open(reg_file, 'r', encoding='utf-8') as f:
43
+ reg_data = json.load(f)
44
+
45
+ imp_data = []
46
+ if imp_file and os.path.exists(imp_file):
47
+ print(f"Loading Improvements: {imp_file}")
48
+ with open(imp_file, 'r', encoding='utf-8') as f:
49
+ imp_data = json.load(f)
50
+ else:
51
+ print("No improvements file provided or found. Skipping mixing.")
52
+
53
+ # 2. Determine Counts based on Ratio
54
+ num_regs = len(reg_data)
55
+ target_imp_count = int(num_regs * mixing_ratio)
56
+
57
+ print(f"stat: {num_regs} Regressions found.")
58
+
59
+ selected_improvements = []
60
+ if imp_data:
61
+ if len(imp_data) > target_imp_count:
62
+ print(f"Sampling {target_imp_count} Improvements from {len(imp_data)} available (Ratio {mixing_ratio})")
63
+ selected_improvements = random.sample(imp_data, target_imp_count)
64
+ else:
65
+ print(f"Taking all {len(imp_data)} available Improvements (Target was {target_imp_count})")
66
+ selected_improvements = imp_data
67
+
68
+ # 3. Tag Sources and Assign Targets
69
+ # Regressions -> Target is Baseline (or GT)
70
+ # Improvements -> Target is ROI (or GT)
71
+
72
+ final_pool = []
73
+
74
+ # Process Regressions
75
+ for item in reg_data:
76
+ item['source_type'] = 'regression'
77
+ # For regression, Baseline is correct
78
+ item['target_response_key'] = 'baseline_response'
79
+ final_pool.append(item)
80
+
81
+ # Process Improvements
82
+ for item in selected_improvements:
83
+ item['source_type'] = 'improvement'
84
+ # For improvement, ROI is correct (Baseline is wrong!)
85
+ item['target_response_key'] = 'roi_response'
86
+ final_pool.append(item)
87
+
88
+ # Shuffle the pool
89
+ random.shuffle(final_pool)
90
+
91
+ print(f"Generating {len(final_pool)} training samples...")
92
+
93
+ # 4. Conversion Loop
94
+ with open(output_file, 'w', encoding='utf-8') as f_out:
95
+ for item in final_pool:
96
+ # A. Select Response
97
+ if use_gt:
98
+ # If forcing GT, ignore model responses
99
+ gt = item.get('ground_truth', '')
100
+ if isinstance(gt, list):
101
+ target_response = str(gt[0])
102
+ else:
103
+ target_response = str(gt)
104
+ else:
105
+ # Use the key determined above (Base for Reg, ROI for Imp)
106
+ key = item['target_response_key']
107
+ target_response = item.get(key, '')
108
+
109
+ # B. Format Question
110
+ question_text = item.get('question', '')
111
+ if "<image>" not in question_text:
112
+ question_text = "<image>\n" + question_text
113
+
114
+ if use_short_prompt and item['source_type']== 'improvement':
115
+ question_text = question_text + "\nAnswer the question using a single word or phrase."
116
+
117
+ # C. Fix Image Path
118
+ raw_image_id = item.get('image_id', '')
119
+ if not os.path.exists(os.path.join(data_root, raw_image_id)):
120
+ raw_image_id = os.path.join('textvqa/train_images', raw_image_id)
121
+ print("Adjusted image path to:", raw_image_id)
122
+ fixed_image_id = raw_image_id
123
+
124
+ # Optional: Warning if still missing
125
+ if not os.path.exists(os.path.join(data_root, fixed_image_id)):
126
+ # Just a warning, don't crash, maybe the path is absolute or handled elsewhere
127
+ print(f" Warning: Image not found at {os.path.join(data_root, fixed_image_id)}")
128
+ pass
129
+
130
+ # D. Construct Entry
131
+ entry = {
132
+ "id": str(item.get('question_id', 'unknown')),
133
+ "image": fixed_image_id,
134
+ "data_path": data_root,
135
+ "conversations": [
136
+ {
137
+ "from": "human",
138
+ "value": question_text
139
+ },
140
+ {
141
+ "from": "gpt",
142
+ "value": target_response
143
+ }
144
+ ]
145
+ }
146
+
147
+ f_out.write(json.dumps(entry) + '\n')
148
+
149
+ print(f"✅ Conversion complete.")
150
+ print(f"Saved to '{output_file}'")
151
+
152
+
153
+ if __name__ == "__main__":
154
+ parser = argparse.ArgumentParser(description="Convert Mixed Reg/Imp to Qwen Training Format")
155
+ parser.add_argument('--reg-file', type=str, required=True, help="Path to regressions.json")
156
+ parser.add_argument('--imp-file', type=str, default=None, help="Path to improvements.json")
157
+ parser.add_argument('--output', type=str, default="finetune_mixed.jsonl", help="Output JSONL path")
158
+ parser.add_argument('--data-root', type=str, default="./data", help="Root path for images")
159
+ parser.add_argument('--mixing-ratio', type=float, default=1.0,
160
+ help="Ratio of Improvements to keep relative to Regressions (e.g. 0.5 = 1 Imp for 2 Regs)")
161
+ parser.add_argument('--use-gt', action='store_true', help="Force using Ground Truth for all answers")
162
+ parser.add_argument('--use-short-prompt', action='store_true', help="Whether to use short prompt format")
163
+ args = parser.parse_args()
164
+
165
+ if os.path.exists(args.reg_file):
166
+ convert_to_qwen_mixed(
167
+ args.reg_file, args.imp_file, args.output,
168
+ args.data_root, args.mixing_ratio, args.use_gt,
169
+ use_short_prompt=args.use_short_prompt
170
+ )
171
+ else:
172
+ print(f"Error: Regression file '{args.reg_file}' not found.")
173
+ '''
174
+ python analysis/convert_json2qwen_training.py --input /home/yuheng/datasets/regressions-v2.json --output /home/yuheng/datasets/roi-post-sft7k.jsonl --data-root /home/yuheng/datasets/
175
+ python analysis/convert_json2qwen_training.py --input /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/regressions-v2.json --output /root/epfs/sproject/datasets/roi-post-sft7k.jsonl --data-root /root/epfs/sproject/datasets
176
+
177
+
178
+ python analysis/convert_json2qwen_training.py \
179
+ --reg-file /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/regressions-v2.json \
180
+ --imp-file /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/improvements-v2.json \
181
+ --output /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/mixed_finetune_v2.jsonl \
182
+ --data-root /root/epfs/sproject/datasets \
183
+ --mixing-ratio 1.0
184
+
185
+
186
+
187
+ python analysis/convert_json2qwen_training.py \
188
+ --reg-file /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/regressions-v2-short.json \
189
+ --imp-file /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/improvements-v2-short.json \
190
+ --output /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/mixed_finetune_v2-short.jsonl \
191
+ --data-root /root/epfs/sproject/datasets \
192
+ --use-short-prompt \
193
+ --mixing-ratio 1.0
194
+
195
+ python analysis/convert_json2qwen_training.py \
196
+ --reg-file /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/regressions-v2-short.json \
197
+ --imp-file /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/improvements-v2-short.json \
198
+ --output /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/roi-post-short-sft4k.jsonl \
199
+ --data-root /root/epfs/sproject/datasets \
200
+ --use-short-prompt \
201
+ --mixing-ratio 0.0
202
+
203
+
204
+ python analysis/convert_json2qwen_training.py \
205
+ --reg-file /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/regressions-v2.json \
206
+ --imp-file /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/improvements-v2-short.json \
207
+ --output /root/epfs/sproject/datasets/roi-post-mixed-sft14k.jsonl \
208
+ --data-root /root/epfs/sproject/datasets \
209
+ --use-short-prompt \
210
+ --mixing-ratio 1.0
211
+
212
+
213
+ python analysis/convert_json2qwen_training.py \
214
+ --reg-file /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/improvements-v2-clean-32b-judge-wo-spatial.json \
215
+ --imp-file /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/regressions-v2-32b-judge.json \
216
+ --output /root/epfs/sproject/datasets/roi-post-mixed-sft12k.jsonl \
217
+ --data-root /root/epfs/sproject/datasets \
218
+ --mixing-ratio 1.0
219
+
220
+ '''
QWENVL-PRIVATE/analysis/convert_seal_json.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import re
3
+ import os
4
+
5
+
6
+ def clean_prompt(text):
7
+ # Matches: <image> followed by newlines, then "Additional visual info..." ending with a dot and newline
8
+ pattern = r"<image>\s*\n\s*Additional visual information to focus on:.*?\.\n"
9
+ cleaned_text = re.sub(pattern, "", text, flags=re.DOTALL)
10
+ return cleaned_text.strip()
11
+
12
+
13
+ def convert_json_to_jsonl(input_file, output_file):
14
+ with open(input_file, 'r', encoding='utf-8') as f:
15
+ data = json.load(f)
16
+
17
+ with open(output_file, 'w', encoding='utf-8') as f_out:
18
+ total_extracted = 0
19
+ skipped_count = 0
20
+
21
+ for item_idx, item in enumerate(data):
22
+ original_path = item.get('image', '')
23
+
24
+ # 1. FILTER: Only process if it starts with 'coco2017'
25
+ if not original_path.startswith('coco2017'):
26
+ skipped_count += 1
27
+ continue
28
+
29
+ # 2. RENAME: Replace only the first occurrence of 'coco2017' with 'coco'
30
+ # e.g., "coco2017/train2017/..." -> "coco/train2017/..."
31
+ new_image_path = original_path.replace('coco2017', 'coco', 1)
32
+
33
+ # Generate ID
34
+ base_id = os.path.splitext(os.path.basename(original_path))[0]
35
+ if not base_id:
36
+ base_id = f"item_{item_idx}"
37
+
38
+ conversations = item.get('conversations', [])
39
+
40
+ # Process turns
41
+ for i in range(0, len(conversations), 2):
42
+ if i + 1 >= len(conversations):
43
+ break
44
+
45
+ human_turn = conversations[i]
46
+ gpt_turn = conversations[i + 1]
47
+
48
+ if human_turn['from'] != 'human' or gpt_turn['from'] != 'gpt':
49
+ continue
50
+
51
+ question_text = clean_prompt(human_turn['value'])
52
+ answer_text = gpt_turn['value']
53
+ q_id = f"{base_id}_{i // 2}"
54
+
55
+ new_entry = {
56
+ "question_id": q_id,
57
+ "image": new_image_path, # Uses the modified 'coco' path
58
+ "text": question_text,
59
+ "answers": [answer_text],
60
+ "category": "default"
61
+ }
62
+
63
+ f_out.write(json.dumps(new_entry) + '\n')
64
+ total_extracted += 1
65
+
66
+ print(f"Conversion complete.")
67
+ print(f"Skipped {skipped_count} items (not from coco2017).")
68
+ print(f"Generated {total_extracted} JSONL lines in '{output_file}'.")
69
+
70
+
71
+ # --- Usage ---
72
+ if __name__ == "__main__":
73
+ # Example usage
74
+ # Ensure you change these filenames to your actual paths
75
+ input_json = "/home/yuheng/datasets/spatial_relation_data.json"
76
+ output_jsonl = "/home/yuheng/datasets/vstar_coco_spatial_relation_data.jsonl"
77
+
78
+ if os.path.exists(input_json):
79
+ convert_json_to_jsonl(input_json, output_jsonl)
80
+ else:
81
+ print(f"File {input_json} not found. Please set the correct path.")
QWENVL-PRIVATE/analysis/denoise_textual_file.py ADDED
@@ -0,0 +1,348 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+
4
+ # Add the parent directory to sys.path to import qwen_src
5
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
6
+
7
+ import pickle
8
+ import numpy as np
9
+ from PIL import Image
10
+ from multiprocessing import Pool, cpu_count
11
+ from functools import partial
12
+ from tqdm import tqdm
13
+ import matplotlib.pyplot as plt # Needed for the debug function if enabled
14
+ import cv2
15
+
16
+ # Assuming these are available in your environment
17
+ from qwen_src.mm_utils import expand2square, create_pseudo_labels, get_foreground_mask
18
+
19
+
20
+ # Import Doctr
21
+ from doctr.models import ocr_predictor
22
+
23
+ # --- GLOBAL LOADER TO PREVENT RE-LOADING MODEL EVERY CALL ---
24
+ # In a production loop, initialize this ONCE outside your loop and pass it in.
25
+ _GLOBAL_PREDICTOR = None
26
+
27
+ def get_doctr_predictor():
28
+ global _GLOBAL_PREDICTOR
29
+ if _GLOBAL_PREDICTOR is None:
30
+ # We only need detection (det), recognition (reco) is not needed for masking
31
+ # causing a 2x speedup.
32
+ print("Loading docTR model (one-time setup)...")
33
+ _GLOBAL_PREDICTOR = ocr_predictor(det_arch='db_resnet50', reco_arch=None, pretrained=True)
34
+ return _GLOBAL_PREDICTOR
35
+
36
+
37
+ def refine_pseudo_labels_with_doctr(original_image, pseudo_labels, grid_size=(24, 24), overlap_threshold=0.15,
38
+ debug=False, predictor=None):
39
+ """
40
+ Refines pseudo-labels by intersecting them with text regions detected by docTR.
41
+
42
+ Args:
43
+ original_image: PIL Image or Numpy Array.
44
+ pseudo_labels: (H_grid, W_grid) binary mask.
45
+ grid_size: tuple (h_grid, w_grid).
46
+ overlap_threshold: float (0.0 to 1.0). If a grid token contains this percentage
47
+ of text pixels, it is kept.
48
+ predictor: Optional passed instance of ocr_predictor to save loading time.
49
+ """
50
+
51
+ # 1. Prepare Image
52
+ if isinstance(original_image, Image.Image):
53
+ img_arr = np.array(original_image)
54
+ else:
55
+ img_arr = original_image.copy()
56
+
57
+ H, W = img_arr.shape[:2]
58
+
59
+ # 2. Get Predictor
60
+ if predictor is None:
61
+ doc_model = get_doctr_predictor()
62
+ else:
63
+ doc_model = predictor
64
+
65
+ # 3. Run Text Detection (Inference)
66
+ # docTR expects list of numpy arrays
67
+ result = doc_model([img_arr])
68
+
69
+ # 4. Generate a "Text Mask" at full resolution
70
+ # We will draw white boxes on a black canvas where text is found.
71
+ text_mask = np.zeros((H, W), dtype=np.uint8)
72
+
73
+ # Extract boxes. Doctr returns relative coords (xmin, ymin, xmax, ymax) 0.0-1.0
74
+ # Structure: result.pages[0].blocks[i].lines[j].words[k]
75
+ # We use 'words' for tightest fit, or 'lines' for better connectivity.
76
+ # Let's use words to be precise.
77
+
78
+ boxes = []
79
+ for block in result.pages[0].blocks:
80
+ for line in block.lines:
81
+ for word in line.words:
82
+ # geometry is ((xmin, ymin), (xmax, ymax))
83
+ (min_x, min_y), (max_x, max_y) = word.geometry
84
+
85
+ # Convert to absolute pixels
86
+ abs_xmin = int(min_x * W)
87
+ abs_xmax = int(max_x * W)
88
+ abs_ymin = int(min_y * H)
89
+ abs_ymax = int(max_y * H)
90
+
91
+ boxes.append([abs_xmin, abs_ymin, abs_xmax, abs_ymax])
92
+
93
+ # Draw filled rectangle on the mask
94
+ cv2.rectangle(text_mask, (abs_xmin, abs_ymin), (abs_xmax, abs_ymax), 1, thickness=-1)
95
+
96
+ # 5. Intersect Grid with Text Mask
97
+ h_grid, w_grid = grid_size
98
+
99
+ # Ensure labels shape
100
+ if pseudo_labels.shape != grid_size:
101
+ pseudo_labels = pseudo_labels.reshape(h_grid, w_grid)
102
+
103
+ refined_labels = pseudo_labels.copy()
104
+ step_h = H // h_grid
105
+ step_w = W // w_grid
106
+
107
+ rows, cols = np.where(pseudo_labels == 1)
108
+
109
+ debug_count = 0
110
+ MAX_DEBUG_PLOTS = 3
111
+
112
+ for r, c in zip(rows, cols):
113
+ y_start = r * step_h
114
+ y_end = min((r + 1) * step_h, H)
115
+ x_start = c * step_w
116
+ x_end = min((c + 1) * step_w, W)
117
+
118
+ # Slice the text mask corresponding to this grid token
119
+ mask_patch = text_mask[y_start:y_end, x_start:x_end]
120
+
121
+ # Calculate Intersection: How much of this patch is actually text?
122
+ pixel_count = mask_patch.size
123
+ text_pixel_count = np.sum(mask_patch)
124
+
125
+ ratio = text_pixel_count / pixel_count if pixel_count > 0 else 0
126
+
127
+ # Logic: If the Grid Token (Attention) is ON, but there is NO text in that region, turn it OFF.
128
+ keep_token = ratio >= overlap_threshold
129
+
130
+ if not keep_token:
131
+ refined_labels[r, c] = 0
132
+
133
+ # --- DEBUG VISUALIZATION ---
134
+ if debug and debug_count < MAX_DEBUG_PLOTS:
135
+ # Only show borderline cases or drops to save time
136
+ # visualizing patches that were 1 but dropped to 0
137
+ if not keep_token:
138
+ fig, axes = plt.subplots(1, 3, figsize=(12, 4))
139
+
140
+ # RGB
141
+ axes[0].imshow(img_arr[y_start:y_end, x_start:x_end])
142
+ axes[0].set_title(f"RGB Patch ({r},{c})")
143
+
144
+ # Global Text Mask Context
145
+ # Show the whole mask with a red box around current patch
146
+ debug_mask = text_mask.copy() * 255
147
+ cv2.rectangle(debug_mask, (x_start, y_start), (x_end, y_end), 127, 2)
148
+ axes[1].imshow(debug_mask, cmap='gray')
149
+ axes[1].set_title("Global Text Detections")
150
+
151
+ # Local Intersection
152
+ axes[2].imshow(mask_patch, vmin=0, vmax=1)
153
+ axes[2].set_title(f"Overlap Ratio: {ratio:.2f}\nThresh: {overlap_threshold}")
154
+
155
+ plt.suptitle(f"Action: {'KEEP' if keep_token else 'DROP'}",
156
+ color='green' if keep_token else 'red', weight='bold')
157
+ plt.show()
158
+ debug_count += 1
159
+ # ---------------------------
160
+
161
+ return refined_labels
162
+
163
+
164
+ # --- Your existing refine function (kept as is) ---
165
+ def refine_pseudo_labels_with_debug(original_image, pseudo_labels, std_threshold=5.0, grid_size=(24, 24), debug=True):
166
+ # 1. Prepare Image
167
+ if isinstance(original_image, Image.Image):
168
+ img_arr = np.array(original_image)
169
+ else:
170
+ img_arr = original_image
171
+
172
+ # Create Intensity Map (Max of RGB)
173
+ if len(img_arr.shape) == 3:
174
+ intensity_map = np.max(img_arr, axis=2)
175
+ else:
176
+ intensity_map = img_arr
177
+
178
+ H, W = intensity_map.shape
179
+ h_grid, w_grid = grid_size
180
+
181
+ # Reshape labels if flat
182
+ if pseudo_labels.shape != grid_size:
183
+ pseudo_labels = pseudo_labels.reshape(h_grid, w_grid)
184
+
185
+ refined_labels = pseudo_labels.copy()
186
+
187
+ step_h = H // h_grid
188
+ step_w = W // w_grid
189
+
190
+ rows, cols = np.where(pseudo_labels == 1)
191
+
192
+ # --- DEBUG COUNTER ---
193
+ debug_count = 0
194
+ MAX_DEBUG_PLOTS = 5
195
+
196
+ for r, c in zip(rows, cols):
197
+ y_start = r * step_h
198
+ y_end = min((r + 1) * step_h, H)
199
+ x_start = c * step_w
200
+ x_end = min((c + 1) * step_w, W)
201
+
202
+ # Extract patches
203
+ intensity_patch = intensity_map[y_start:y_end, x_start:x_end]
204
+ rgb_patch = img_arr[y_start:y_end, x_start:x_end]
205
+
206
+ if intensity_patch.size > 0:
207
+ patch_std = np.std(intensity_patch)
208
+
209
+ # --- VISUALIZATION BLOCK ---
210
+ # Note: Debug plotting inside multiprocessing usually fails or is hidden.
211
+ # Ensure debug=False when running in parallel.
212
+ if debug and debug_count < MAX_DEBUG_PLOTS:
213
+ fig, axes = plt.subplots(1, 2, figsize=(8, 4))
214
+ axes[0].imshow(rgb_patch)
215
+ axes[0].set_title(f"RGB Patch\nGrid: ({r},{c})")
216
+ axes[0].axis('off')
217
+ im = axes[1].imshow(intensity_patch, cmap='magma', vmin=0, vmax=255)
218
+ axes[1].set_title(f"Max-RGB Intensity\nStd Dev: {patch_std:.2f}")
219
+ axes[1].axis('off')
220
+ status = "KEEP" if patch_std >= std_threshold else "DROP (Too Flat)"
221
+ plt.suptitle(f"Token Analysis: {status} (Thresh: {std_threshold})",
222
+ color='green' if patch_std >= std_threshold else 'red', weight='bold')
223
+ plt.tight_layout()
224
+ plt.show()
225
+ debug_count += 1
226
+ # ---------------------------
227
+
228
+ if patch_std < std_threshold:
229
+ refined_labels[r, c] = 0
230
+
231
+ return refined_labels
232
+
233
+
234
+ # --- Worker Function for Multiprocessing ---
235
+ def process_single_sample(item, dataset_path):
236
+ """
237
+ Worker function to process a single data item.
238
+ Returns the modified item or None if processing fails.
239
+ """
240
+ try:
241
+ img_path = item['image']
242
+ if 'gqa' in img_path.lower():
243
+ return item # Skip GQA images
244
+ if dataset_path is not None:
245
+ full_img_path = os.path.join(dataset_path, img_path)
246
+ else:
247
+ full_img_path = img_path
248
+
249
+ grounding_attn_o2i = item['grounding_attn_o2i']
250
+
251
+ # Load and convert image
252
+ try:
253
+ image = Image.open(full_img_path).convert('RGB')
254
+ except Exception as e:
255
+ # print(f"Error opening image {full_img_path}: {e}")
256
+ return None
257
+
258
+ image_size = image.size
259
+
260
+ # Calculate mask
261
+ known_fg_mask = get_foreground_mask(image_size, grounding_attn_o2i.shape[0], min_dimension_size=1)
262
+ tmp_attn = grounding_attn_o2i.copy()
263
+ tmp_attn[~known_fg_mask] = 0.
264
+
265
+ # Binary Threshold
266
+ tmp_attn_binary = (tmp_attn > tmp_attn.max() * 0.05).astype(np.float32)
267
+
268
+ # Denoise (Force debug=False for multiprocessing)
269
+ refined_labels = refine_pseudo_labels_with_debug(
270
+ original_image=expand2square(image, background_color=(127, 127, 127)),
271
+ pseudo_labels=tmp_attn_binary,
272
+ std_threshold=4.0,
273
+ grid_size=(grounding_attn_o2i.shape[0], grounding_attn_o2i.shape[1]),
274
+ debug=False
275
+ )
276
+
277
+ # Update attention
278
+ grounding_attn_o2i_refined = grounding_attn_o2i * refined_labels
279
+
280
+ # Create a copy to return (avoid modifying shared memory if that were an issue)
281
+ new_item = item.copy()
282
+ new_item['grounding_attn_o2i'] = grounding_attn_o2i_refined
283
+ return new_item
284
+
285
+ except Exception as e:
286
+ # Catch generic errors to prevent pool crash
287
+ # print(f"Error processing item: {e}")
288
+ return None
289
+
290
+
291
+ # --- Main Driver ---
292
+ def denoise_textual_attn_multiprocess(src_pkl_path, dataset_path, dst_pkl_path, num_workers=None):
293
+ print(f"Loading pickle from {src_pkl_path}...")
294
+ tmp_pkl_content = pickle.load(open(src_pkl_path, "rb"))
295
+
296
+ # Pre-filtering
297
+ original_len = len(tmp_pkl_content)
298
+ tmp_pkl_content = [item for item in tmp_pkl_content if "textvqa" not in item['image'].lower()]
299
+ print(f"Filtered {original_len} -> {len(tmp_pkl_content)} samples (Removed TextVQA).")
300
+
301
+ dst_pkl_content = []
302
+
303
+ # Determine workers (leave 2 cores free for system stability if possible)
304
+ if num_workers is None:
305
+ num_workers = max(1, cpu_count() - 2)
306
+
307
+ print(f"Starting multiprocessing pool with {num_workers} workers...")
308
+
309
+ # Partial function to fix the 'dataset_path' argument
310
+ worker_func = partial(process_single_sample, dataset_path=dataset_path)
311
+
312
+ with Pool(num_workers) as pool:
313
+ # imap allows us to use tqdm for a progress bar
314
+ # chunksize can be tuned, but default is usually fine for this data size
315
+ results = list(tqdm(pool.imap(worker_func, tmp_pkl_content), total=len(tmp_pkl_content)))
316
+
317
+ # Filter out failures (None returns)
318
+ valid_results = [res for res in results if res is not None]
319
+
320
+ print(f"Processing complete. Success: {len(valid_results)}, Failed/Skipped: {len(results) - len(valid_results)}")
321
+
322
+ print(f"Saving to {dst_pkl_path}...")
323
+ pickle.dump(valid_results, open(dst_pkl_path, "wb"))
324
+ print("Done.")
325
+
326
+
327
+ if __name__ == "__main__":
328
+ # Ensure this block is used to prevent recursive process spawning on some OS
329
+ # denoise_textual_attn_multiprocess(
330
+ # src_pkl_path='/home/yuheng/datasets/qwen2_5vl_pseudo_7b_576res_247k.pkl',
331
+ # dataset_path='/home/yuheng/datasets/',
332
+ # dst_pkl_path='/home/yuheng/datasets/qwen2_5vl_pseudo_7b_576res_229k_denoised.pkl',
333
+ # num_workers=16 # Adjust based on your machine (e.g., 8, 16, 32)
334
+ # )
335
+
336
+ denoise_textual_attn_multiprocess(
337
+ src_pkl_path='/root/epfs/sproject/datasets/qwen2_5vl_pseudo_7b_576res_release.pkl',
338
+ dataset_path='/root/epfs/sproject/datasets/',
339
+ dst_pkl_path='/root/epfs/sproject/datasets/qwen2_5vl_pseudo_7b_576res_release_denoised.pkl',
340
+ num_workers=16 # Adjust based on your machine (e.g., 8, 16, 32)
341
+ )
342
+
343
+ # denoise_textual_attn_multiprocess(
344
+ # src_pkl_path='/home/yuheng/datasets/qwen2_5vl_pseudo_7b_576res_release.pkl',
345
+ # dataset_path='//home/yuheng/datasets/',
346
+ # dst_pkl_path='/home/yuheng/datasets/qwen2_5vl_pseudo_7b_576res_release_denoised.pkl',
347
+ # num_workers=16 # Adjust based on your machine (e.g., 8, 16, 32)
348
+ # )
QWENVL-PRIVATE/analysis/denoise_textual_file_wdet.py ADDED
@@ -0,0 +1,330 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ import pickle
4
+ import numpy as np
5
+ import cv2
6
+ import warnings
7
+ from PIL import Image
8
+ import torch
9
+ import multiprocessing as mp
10
+ from multiprocessing import Pool, Manager
11
+ from functools import partial
12
+ from tqdm import tqdm
13
+
14
+ # Add the parent directory to sys.path to import qwen_src
15
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
16
+
17
+ from qwen_src.mm_utils import expand2square, get_foreground_mask
18
+ from doctr.models import ocr_predictor
19
+
20
+ # --- GLOBAL LOADER ---
21
+ _GLOBAL_PREDICTOR = None
22
+
23
+
24
+ def get_doctr_predictor():
25
+ """
26
+ Initializes the DocTR model once per worker process on the assigned GPU.
27
+ """
28
+ global _GLOBAL_PREDICTOR
29
+
30
+ # Suppress the specific torch load warning from doctr
31
+ warnings.filterwarnings("ignore", category=FutureWarning, module="doctr")
32
+ warnings.filterwarnings("ignore", category=FutureWarning, module="torch")
33
+
34
+ if _GLOBAL_PREDICTOR is None:
35
+ try:
36
+ # 1. Initialize the architecture (loads to CPU by default)
37
+ predictor = ocr_predictor(det_arch='fast_base', pretrained=True)
38
+
39
+ # 2. Force Move to GPU using the High-Level API
40
+ # We use 'cuda:0' because CUDA_VISIBLE_DEVICES restricts this process to seeing only 1 GPU.
41
+ if torch.cuda.is_available():
42
+ # .cuda() moves the model weights AND sets the internal flag to move input images to GPU
43
+ predictor.cuda()
44
+ predictor.det_predictor.model.eval()
45
+
46
+ # --- SANITY CHECK ---
47
+ # This prints the actual device of the model parameters to prove it's on GPU
48
+ param_device = next(predictor.det_predictor.model.parameters()).device
49
+ # print(f"[Worker {os.getpid()}] Model loaded. confirmed on: {param_device}")
50
+ else:
51
+ print(f"[Worker {os.getpid()}] WARNING: CUDA not available. Running on CPU.")
52
+
53
+ _GLOBAL_PREDICTOR = predictor
54
+
55
+ except Exception as e:
56
+ print(f"[Worker {os.getpid()}] Error loading model: {e}")
57
+ return None
58
+
59
+ return _GLOBAL_PREDICTOR
60
+
61
+
62
+ def refine_pseudo_labels_with_doctr(original_image, pseudo_labels,
63
+ precomputed_boxes=None,
64
+ grid_size=(24, 24),
65
+ overlap_threshold=0.15,
66
+ predictor=None):
67
+ if isinstance(original_image, Image.Image):
68
+ img_arr = np.array(original_image)
69
+ else:
70
+ img_arr = original_image.copy()
71
+
72
+ H, W = img_arr.shape[:2]
73
+
74
+ # --- Retrieve or Compute Boxes ---
75
+ boxes = []
76
+
77
+ if precomputed_boxes is not None:
78
+ boxes = precomputed_boxes
79
+ else:
80
+ # Initialize/Get Model
81
+ if predictor is None:
82
+ doc_model = get_doctr_predictor()
83
+ else:
84
+ doc_model = predictor
85
+
86
+ # Run Inference
87
+ if doc_model is not None:
88
+ # The predictor automatically handles moving numpy/cpu inputs to the GPU
89
+ # if .cuda() was called during initialization.
90
+ result = doc_model([img_arr])
91
+
92
+ for block in result.pages[0].blocks:
93
+ for line in block.lines:
94
+ for word in line.words:
95
+ (min_x, min_y), (max_x, max_y) = word.geometry
96
+ boxes.append([
97
+ int(min_x * W), int(min_y * H),
98
+ int(max_x * W), int(max_y * H)
99
+ ])
100
+
101
+ # --- Create Text Mask ---
102
+ text_mask = np.zeros((H, W), dtype=np.uint8)
103
+ for box in boxes:
104
+ cv2.rectangle(text_mask, (box[0], box[1]), (box[2], box[3]), 1, thickness=-1)
105
+
106
+ # -- -Refine Labels ---
107
+ h_grid, w_grid = grid_size
108
+ if pseudo_labels.shape != grid_size:
109
+ pseudo_labels = pseudo_labels.reshape(h_grid, w_grid)
110
+
111
+ refined_labels = pseudo_labels.copy()
112
+ step_h, step_w = H // h_grid, W // w_grid
113
+ rows, cols = np.where(pseudo_labels == 1)
114
+
115
+ for r, c in zip(rows, cols):
116
+ y_start, y_end = r * step_h, min((r + 1) * step_h, H)
117
+ x_start, x_end = c * step_w, min((c + 1) * step_w, W)
118
+
119
+ mask_patch = text_mask[y_start:y_end, x_start:x_end]
120
+ if mask_patch.size > 0:
121
+ ratio = np.sum(mask_patch) / mask_patch.size
122
+ if ratio < overlap_threshold:
123
+ refined_labels[r, c] = 0
124
+ else:
125
+ refined_labels[r, c] = 0
126
+
127
+ return refined_labels, boxes
128
+
129
+
130
+ # --- Worker Functions ---
131
+
132
+ def gpu_worker_init(queue):
133
+ """
134
+ Assigns a unique GPU to the worker process.
135
+ """
136
+ try:
137
+ gpu_id = queue.get()
138
+ # Mask the environment so this process only sees this specific GPU
139
+ os.environ["CUDA_VISIBLE_DEVICES"] = str(gpu_id)
140
+
141
+ # Suppress warnings inside the worker process as well
142
+ warnings.filterwarnings("ignore", category=FutureWarning)
143
+ except Exception as e:
144
+ print(f"Worker init failed: {e}")
145
+
146
+
147
+ def process_single_sample_packed(args):
148
+ """
149
+ Unpacks arguments and processes the sample.
150
+ args: (item, dataset_path, precomputed_boxes)
151
+ """
152
+ item, dataset_path, precomputed_boxes = args
153
+
154
+ try:
155
+ img_path = item['image']
156
+
157
+ # --- GQA / NATURAL IMAGE HANDLING ---
158
+ # Keep GQA samples intact, but return None for boxes (no cache update)
159
+ if 'gqa' in img_path.lower():
160
+ return item, img_path, None
161
+
162
+ # --- PROCESSING FOR TEXT IMAGES ---
163
+ full_img_path = os.path.join(dataset_path, img_path) if dataset_path else img_path
164
+ grounding_attn_o2i = item['grounding_attn_o2i']
165
+
166
+ try:
167
+ image = Image.open(full_img_path).convert('RGB')
168
+ except Exception:
169
+ return None, None, None
170
+
171
+ # Prepare Mask
172
+ known_fg_mask = get_foreground_mask(image.size, grounding_attn_o2i.shape[0], min_dimension_size=1)
173
+ tmp_attn = grounding_attn_o2i.copy()
174
+ tmp_attn[~known_fg_mask] = 0.
175
+ tmp_attn_binary = (tmp_attn > tmp_attn.max() * 0.05).astype(np.float32)
176
+
177
+ # Denoise
178
+ square_image = expand2square(image, background_color=(127, 127, 127))
179
+
180
+ refined_labels, final_boxes = refine_pseudo_labels_with_doctr(
181
+ original_image=square_image,
182
+ pseudo_labels=tmp_attn_binary,
183
+ precomputed_boxes=precomputed_boxes,
184
+ grid_size=(grounding_attn_o2i.shape[0], grounding_attn_o2i.shape[1]),
185
+ overlap_threshold=0.15
186
+ )
187
+
188
+ new_item = item.copy()
189
+ new_item['grounding_attn_o2i'] = grounding_attn_o2i * refined_labels
190
+
191
+ return new_item, img_path, final_boxes
192
+
193
+ except Exception as e:
194
+ # print(f"Error in worker: {e}")
195
+ return None, None, None
196
+
197
+
198
+ def denoise_textual_attn_multiprocess(
199
+ src_pkl_path,
200
+ dataset_path,
201
+ dst_pkl_path,
202
+ ocr_cache_file,
203
+ num_gpus=None,
204
+ filter_substr="docvqa",
205
+ ):
206
+ # 1. Setup GPUs
207
+ if num_gpus is None:
208
+ num_gpus = torch.cuda.device_count()
209
+ if num_gpus == 0: num_gpus = 1
210
+
211
+ print(f"Using {num_gpus} workers (GPUs).")
212
+
213
+ # 2. Load Dataset
214
+ print(f"Loading dataset from {src_pkl_path}...")
215
+ tmp_pkl_content = pickle.load(open(src_pkl_path, "rb"))
216
+
217
+ # Optional: Filter explicit datasets if needed
218
+ original_len = len(tmp_pkl_content)
219
+ # # Removing TextVQA as per previous requests, keeping GQA handled in worker
220
+ # tmp_pkl_content = [item for item in tmp_pkl_content if "textvqa" not in item['image'].lower()]
221
+ if filter_substr:
222
+ tmp_pkl_content = [item for item in tmp_pkl_content if filter_substr in item['image'].lower()]
223
+
224
+ print(f"Filtered {original_len} -> {len(tmp_pkl_content)} samples.")
225
+
226
+ # 3. Load/Init OCR Cache (Single File)
227
+ ocr_cache = {}
228
+ if ocr_cache_file and os.path.exists(ocr_cache_file):
229
+ print(f"Loading existing OCR cache from {ocr_cache_file}...")
230
+ try:
231
+ with open(ocr_cache_file, 'rb') as f:
232
+ ocr_cache = pickle.load(f)
233
+ print(f"Loaded {len(ocr_cache)} cached entries.")
234
+ except Exception as e:
235
+ print(f"Failed to load cache: {e}. Starting fresh.")
236
+
237
+ # 4. Prepare Tasks
238
+ tasks = []
239
+ for item in tmp_pkl_content:
240
+ img_path = item['image']
241
+ cached_boxes = ocr_cache.get(img_path, None)
242
+ tasks.append((item, dataset_path, cached_boxes))
243
+
244
+ # 5. Start Processing
245
+ m = Manager()
246
+ gpu_queue = m.Queue()
247
+ for i in range(num_gpus): gpu_queue.put(i)
248
+
249
+ print("Starting pool...")
250
+ valid_results = []
251
+ cache_updated = False
252
+
253
+ # --- FORCE SPAWN ---
254
+ # This prevents the parent process's state from corrupting the worker's CUDA context
255
+ try:
256
+ mp.set_start_method('spawn', force=True)
257
+ except RuntimeError:
258
+ pass
259
+
260
+ with Pool(processes=num_gpus, initializer=gpu_worker_init, initargs=(gpu_queue,)) as pool:
261
+ for result_item, img_path, boxes in tqdm(pool.imap(process_single_sample_packed, tasks), total=len(tasks)):
262
+
263
+ if result_item is not None:
264
+ valid_results.append(result_item)
265
+
266
+ # Only update cache if we actually have boxes (i.e. not GQA)
267
+ if img_path and boxes is not None:
268
+ if img_path not in ocr_cache:
269
+ ocr_cache[img_path] = boxes
270
+ cache_updated = True
271
+
272
+ print(f"Processing complete. Valid items: {len(valid_results)}")
273
+
274
+ # 6. Save Outputs
275
+ print(f"Saving denoised dataset to {dst_pkl_path}...")
276
+ with open(dst_pkl_path, "wb") as f:
277
+ pickle.dump(valid_results, f)
278
+
279
+ if ocr_cache_file:
280
+ if cache_updated or not os.path.exists(ocr_cache_file):
281
+ print(f"Saving updated OCR cache ({len(ocr_cache)} entries) to {ocr_cache_file}...")
282
+ with open(ocr_cache_file, "wb") as f:
283
+ pickle.dump(ocr_cache, f)
284
+ else:
285
+ print("OCR Cache was not modified, skipping save.")
286
+
287
+ print("Done.")
288
+
289
+ if __name__ == "__main__":
290
+ # --- CRITICAL FIX FOR GPU MULTIPROCESSING ---
291
+ # This must be the very first thing that happens in __main__
292
+ # 'spawn' creates a fresh process, ensuring CUDA is not corrupted by the parent.
293
+ try:
294
+ mp.set_start_method('spawn', force=True)
295
+ except RuntimeError as e:
296
+ print(f"Start method setting warning: {e}")
297
+
298
+ import argparse
299
+
300
+ parser = argparse.ArgumentParser(
301
+ description="Denoise textual-image pseudo labels with DocTR detection."
302
+ )
303
+ parser.add_argument("--src-pkl-path", type=str, required=True, help="Source pkl file path.")
304
+ parser.add_argument("--dataset-path", type=str, default=None, help="Root dataset path for image loading.")
305
+ parser.add_argument("--dst-pkl-path", type=str, required=True, help="Output pkl file path.")
306
+ parser.add_argument("--ocr-cache-file", type=str, default=None, help="Path to OCR cache pkl.")
307
+ parser.add_argument("--num-gpus", type=int, default=None, help="Number of GPUs to use.")
308
+ parser.add_argument(
309
+ "--filter-substr",
310
+ type=str,
311
+ default="docvqa",
312
+ help="Only keep samples whose image path contains this substring (case-insensitive).",
313
+ )
314
+ args = parser.parse_args()
315
+
316
+ denoise_textual_attn_multiprocess(
317
+ src_pkl_path=args.src_pkl_path,
318
+ dataset_path=args.dataset_path,
319
+ dst_pkl_path=args.dst_pkl_path,
320
+ ocr_cache_file=args.ocr_cache_file,
321
+ num_gpus=args.num_gpus,
322
+ filter_substr=args.filter_substr.lower() if args.filter_substr else "",
323
+ )
324
+ # denoise_textual_attn_multiprocess(
325
+ # src_pkl_path='/home/yuheng/datasets/qwen2_5vl_pseudo_7b_576res_release.pkl',
326
+ # dataset_path='/home/yuheng/datasets/',
327
+ # dst_pkl_path='/home/yuheng/datasets/qwen2_5vl_pseudo_7b_576res_release_denoised.pkl',
328
+ # ocr_cache_file='/home/yuheng/datasets/ocr_boxes_cache_full.pkl',
329
+ # num_gpus=4
330
+ # )
QWENVL-PRIVATE/analysis/failure_case_analysis.py ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+
4
+
5
+ def load_jsonl(path):
6
+ """
7
+ Loads a JSONL file into a dictionary keyed by 'doc_id'.
8
+ """
9
+ data = {}
10
+ try:
11
+ with open(path, 'r', encoding='utf-8') as f:
12
+ for i, line in enumerate(f):
13
+ try:
14
+ record = json.loads(line)
15
+ # Use 'doc_id' as the unique key for comparison
16
+ if 'doc_id' not in record:
17
+ print(f"Warning: Missing 'doc_id' in {path} at line {i + 1}. Skipping.")
18
+ continue
19
+ data[record['doc_id']] = record
20
+ except json.JSONDecodeError:
21
+ print(f"Error: Could not parse JSON in {path} at line {i + 1}. Skipping.")
22
+ except FileNotFoundError:
23
+ print(f"Error: File not found at {path}")
24
+ return None
25
+ except Exception as e:
26
+ print(f"Error opening file {path}: {e}")
27
+ return None
28
+
29
+ print(f"Successfully loaded {len(data)} records from {path}.")
30
+ return data
31
+
32
+
33
+ def find_failure_cases_textvqa(ours_output_path, baseline_output_path, score_key="exact_match"):
34
+ """
35
+ Compares two output files and finds items where 'ours' is wrong (0.0)
36
+ and 'baseline' is right (1.0).
37
+
38
+ Args:
39
+ ours_output_path (str): Path to your model's output JSONL.
40
+ baseline_output_path (str): Path to the baseline model's output JSONL.
41
+ score_key (str): The key in the JSON to find the score dictionary.
42
+ Default is "vstar_overall_acc".
43
+
44
+ Returns:
45
+ list: A list of dictionaries, each containing details of a failure case.
46
+ """
47
+ failure_cases = []
48
+
49
+ # Load the baseline results into a dictionary for fast lookup
50
+ baseline_results = load_jsonl(baseline_output_path)
51
+ if baseline_results is None:
52
+ return []
53
+
54
+ print(f"\nProcessing {ours_output_path} against baseline...")
55
+
56
+ try:
57
+ with open(ours_output_path, 'r', encoding='utf-8') as f:
58
+ for i, line in enumerate(f):
59
+ try:
60
+ our_record = json.loads(line)
61
+ if 'doc_id' not in our_record:
62
+ print(f"Warning: Missing 'doc_id' in 'ours' file at line {i + 1}. Skipping.")
63
+ continue
64
+ doc_id = our_record['doc_id']
65
+
66
+ # Check if this doc_id exists in the baseline
67
+ if doc_id not in baseline_results:
68
+ print(f"Warning: doc_id {doc_id} from 'ours' file not found in baseline. Skipping.")
69
+ continue
70
+ baseline_record = baseline_results[doc_id]
71
+ # Check if score keys exist before accessing
72
+ if score_key not in our_record :
73
+ print(f"Warning: Missing score key '{score_key}' in 'ours' file for doc_id {doc_id}. Skipping.")
74
+ continue
75
+ if score_key not in baseline_record:
76
+ print(
77
+ f"Warning: Missing score key '{score_key}' in baseline file for doc_id {doc_id}. Skipping.")
78
+ continue
79
+ # Get the scores
80
+ if type(our_record[score_key]) is not dict or type(baseline_record[score_key]) is not dict:
81
+ our_score = our_record[score_key]
82
+ baseline_score = baseline_record[score_key]
83
+ else:
84
+ our_score = our_record[score_key]['score']
85
+ baseline_score = baseline_record[score_key]['score']
86
+
87
+ # The core logic: Find where we failed and baseline succeeded
88
+ if our_score < 0.3 and baseline_score > 0.7:
89
+ # Format a clean dictionary for case study
90
+ failure_info = {
91
+ "doc_id": doc_id,
92
+ "question": our_record['input'],
93
+ "ground_truth": baseline_record['filtered_resps'][0],
94
+ "our_prediction": our_record['filtered_resps'][0],#[score_key].get('prediction', 'N/A'),
95
+ "baseline_prediction": baseline_record['filtered_resps'][0],
96
+ "full_our_record": our_record # Optional: include for full context
97
+ }
98
+ failure_cases.append(failure_info)
99
+
100
+ except json.JSONDecodeError:
101
+ print(f"Error: Could not parse JSON in 'ours' file at line {i + 1}. Skipping.")
102
+ except KeyError:
103
+ # This catches if a sub-key like 'score' is missing
104
+ print(f"Error: Missing expected key structure in 'ours' file at line {i + 1}. Skipping.")
105
+
106
+ except FileNotFoundError:
107
+ print(f"Error: File not found at {ours_output_path}")
108
+ return []
109
+ except Exception as e:
110
+ print(f"Error opening file {ours_output_path}: {e}")
111
+ return []
112
+
113
+ print(f"\n--- Analysis Complete ---")
114
+ print(f"Found {len(failure_cases)} failure cases where 'ours' was wrong and 'baseline' was right.")
115
+ return failure_cases
116
+
117
+
118
+ def find_failure_cases_vstar(ours_output_path, baseline_output_path, score_key="vstar_overall_acc"):
119
+ """
120
+ Compares two output files and finds items where 'ours' is wrong (0.0)
121
+ and 'baseline' is right (1.0).
122
+
123
+ Args:
124
+ ours_output_path (str): Path to your model's output JSONL.
125
+ baseline_output_path (str): Path to the baseline model's output JSONL.
126
+ score_key (str): The key in the JSON to find the score dictionary.
127
+ Default is "vstar_overall_acc".
128
+
129
+ Returns:
130
+ list: A list of dictionaries, each containing details of a failure case.
131
+ """
132
+ failure_cases = []
133
+
134
+ # Load the baseline results into a dictionary for fast lookup
135
+ baseline_results = load_jsonl(baseline_output_path)
136
+ if baseline_results is None:
137
+ return []
138
+
139
+ print(f"\nProcessing {ours_output_path} against baseline...")
140
+
141
+ try:
142
+ with open(ours_output_path, 'r', encoding='utf-8') as f:
143
+ for i, line in enumerate(f):
144
+ try:
145
+ our_record = json.loads(line)
146
+
147
+ if 'doc_id' not in our_record:
148
+ print(f"Warning: Missing 'doc_id' in 'ours' file at line {i + 1}. Skipping.")
149
+ continue
150
+
151
+ doc_id = our_record['doc_id']
152
+
153
+ # Check if this doc_id exists in the baseline
154
+ if doc_id not in baseline_results:
155
+ print(f"Warning: doc_id {doc_id} from 'ours' file not found in baseline. Skipping.")
156
+ continue
157
+
158
+ baseline_record = baseline_results[doc_id]
159
+
160
+ # Check if score keys exist before accessing
161
+ if score_key not in our_record or 'score' not in our_record[score_key]:
162
+ print(f"Warning: Missing score key '{score_key}' in 'ours' file for doc_id {doc_id}. Skipping.")
163
+ continue
164
+
165
+ if score_key not in baseline_record or 'score' not in baseline_record[score_key]:
166
+ print(
167
+ f"Warning: Missing score key '{score_key}' in baseline file for doc_id {doc_id}. Skipping.")
168
+ continue
169
+
170
+ # Get the scores
171
+ our_score = our_record[score_key]['score']
172
+ baseline_score = baseline_record[score_key]['score']
173
+
174
+ # The core logic: Find where we failed and baseline succeeded
175
+ if our_score == 0.0 and baseline_score == 1.0:
176
+ # Format a clean dictionary for case study
177
+ failure_info = {
178
+ "doc_id": doc_id,
179
+ "question": our_record.get('doc', {}).get('text', 'N/A'),
180
+ "ground_truth": our_record.get('target', 'N/A'),
181
+ "our_prediction": our_record[score_key].get('prediction', 'N/A'),
182
+ "baseline_prediction": baseline_record[score_key].get('prediction', 'N/A'),
183
+ "full_our_record": our_record # Optional: include for full context
184
+ }
185
+ failure_cases.append(failure_info)
186
+
187
+ except json.JSONDecodeError:
188
+ print(f"Error: Could not parse JSON in 'ours' file at line {i + 1}. Skipping.")
189
+ except KeyError:
190
+ # This catches if a sub-key like 'score' is missing
191
+ print(f"Error: Missing expected key structure in 'ours' file at line {i + 1}. Skipping.")
192
+
193
+ except FileNotFoundError:
194
+ print(f"Error: File not found at {ours_output_path}")
195
+ return []
196
+ except Exception as e:
197
+ print(f"Error opening file {ours_output_path}: {e}")
198
+ return []
199
+
200
+ print(f"\n--- Analysis Complete ---")
201
+ print(f"Found {len(failure_cases)} failure cases where 'ours' was wrong and 'baseline' was right.")
202
+ return failure_cases
203
+
204
+ # --- Example Usage ---
205
+ if __name__ == "__main__":
206
+
207
+ # --- 1. Create dummy data files for the example ---
208
+
209
+ # This example has 3 cases:
210
+ # 1. We fail (B), baseline succeeds (A). -> This should be found.
211
+ # 2. We succeed (A), baseline fails (C). -> This should be ignored.
212
+ # 3. Both succeed (A). -> This should be ignored.
213
+
214
+
215
+
216
+ # # Define file paths
217
+ # baseline_file = "./logs/liuhaotian__llava-v1.5-7b/20250902_190647_samples_textvqa_val.jsonl"
218
+ # ours_file = "./logs/checkpoints__llava-v1.5-7b-roi-K15T3-152k-v1bf16Mheads-bg0p05fg0p2-filled/20251113_113923_samples_textvqa_val.jsonl"
219
+ #
220
+ # #failures = find_failure_cases_textvqa(ours_file, baseline_file, 'exact_match')
221
+ # failures = find_failure_cases_textvqa(baseline_file, ours_file , 'exact_match')
222
+
223
+
224
+ #baseline_file = "./logs/liuhaotian__llava-v1.5-7b/20250902_201850_samples_vstar_bench.jsonl"
225
+ #ours_file = "./logs/checkpoints__llava-v1.5-7b-roi-K15T3-152k-v1bf16Mheads-bg0p05fg0p2-filled/20251113_130922_samples_vstar_bench.jsonl"
226
+
227
+ # baseline_file = "../Qwen2.5-VL/logs/Qwen__Qwen2.5-VL-7B-Instruct/20251113_135313_samples_vstar_bench.jsonl"
228
+ # ours_file = "../Qwen2.5-VL/logs/output__qwen2_5vl-7b-roi-K16T3-152k-v1bf16Mheads-twiginit-filled/20251113_134244_samples_vstar_bench.jsonl"
229
+ #
230
+ # failures = find_failure_cases_vstar(baseline_file, ours_file)
231
+ # failure_ids = [case['doc_id'] for case in failures]
232
+
233
+ ###### TextVQA Failure Case Analysis
234
+ #baseline_file_textvqa = "logs/output__qwen2_5vl-7b-roi-K18T3-185k-v1bf16-denoisedv2-twiginit-filled/20251209_181751_samples_textvqa_val.jsonl"
235
+ baseline_file_textvqa = "logs/Qwen__Qwen3-VL-4B-Instruct/20260122_115842_samples_textvqa_val.jsonl"
236
+
237
+ ours_file_textvqa = "logs/output__qwen3vl-4b-roi-K24T3-185k-v1bf16-twiginit/20260122_120855_samples_textvqa_val.jsonl"
238
+ failures_baseline_textvqa = find_failure_cases_textvqa(baseline_file_textvqa, ours_file_textvqa, 'exact_match')
239
+ failure_ids_baseline_textvqa = [case['doc_id'] for case in failures_baseline_textvqa]
240
+
241
+ failures_ours_textvqa = find_failure_cases_textvqa(ours_file_textvqa, baseline_file_textvqa, 'exact_match')
242
+ failure_ids_ours_textvqa = [case['doc_id'] for case in failures_ours_textvqa]
243
+ failure_ids_ours_textvqa_dict = {case['doc_id']: case for case in failures_ours_textvqa}
244
+ ### print some results
245
+ for i, case in enumerate(failures_ours_textvqa[:10]):
246
+ print(f"\nCase {i + 1} (doc_id: {case['doc_id']})")
247
+ print(f" Question: {case['question']}")
248
+ print(f" Ground Truth: {case['ground_truth']}")
249
+ print(f" Our (Wrong) Prediction: {case['our_prediction']}")
250
+ print(f" Baseline (Correct) Prediction: {case['baseline_prediction']}")
251
+
252
+
253
+
254
+ # ###### InfoVQA Failure Case Analysis
255
+ # baseline_file_infovqa = "../Qwen2.5-VL/logs/Qwen__Qwen2.5-VL-7B-Instruct/20251204_093838_samples_infovqa_val.jsonl"
256
+ # ours_file_infovqa = "../Qwen2.5-VL/logs/output__qwen2_5vl-7b-roi-K18T3-185k-v1bf16-denoisedv2-twiginit-filled/20251215_131620_samples_infovqa_val.jsonl"
257
+ #
258
+ # failures_baseline_infovqa = find_failure_cases_textvqa(baseline_file_infovqa, ours_file_infovqa, 'anls')
259
+ # failure_ids_baseline_infovqa = [case['doc_id'] for case in failures_baseline_infovqa]
260
+ #
261
+ # failures_ours_infovqa = find_failure_cases_textvqa(ours_file_infovqa, baseline_file_infovqa, 'anls')
262
+ # failure_ids_ours_infovqa = [case['doc_id'] for case in failures_ours_infovqa]
263
+ #
264
+ # ##### DocVQA Failure Case Analysis
265
+ # baseline_file_docvqa = "../Qwen2.5-VL/logs/Qwen__Qwen2.5-VL-7B-Instruct/20251204_095001_samples_docvqa_val.jsonl"
266
+ # ours_file_docvqa = "../Qwen2.5-VL/logs/output__qwen2_5vl-7b-roi-K18T3-134k-gatedv2PB-wGTwRoI-twiginit-filled/20251210_143202_samples_docvqa_val.jsonl"
267
+ #
268
+ # failures_baseline_docvqa = find_failure_cases_textvqa(baseline_file_docvqa, ours_file_docvqa, 'anls')
269
+ # failure_ids_baseline_docvqa = [case['doc_id'] for case in failures_baseline_docvqa]
270
+ # failures_ours_docvqa = find_failure_cases_textvqa(ours_file_docvqa, baseline_file_docvqa, 'anls')
271
+ # failure_ids_ours_docvqa = [case['doc_id'] for case in failures_ours_docvqa]
272
+ #
273
+ # ##### ChartQA Failure Case Analysis
274
+ # baseline_file_chartqa = "../Qwen2.5-VL/logs/Qwen__Qwen2.5-VL-7B-Instruct/20251204_094359_samples_chartqa.jsonl"
275
+ # ours_file_chartqa = "../Qwen2.5-VL/logs/output__qwen2_5vl-7b-roi-K18T3-134k-gatedv2PB-wGTwRoI-twiginit-filled/20251210_142453_samples_chartqa.jsonl"
276
+ #
277
+ # failures_baseline_chartqa = find_failure_cases_textvqa(baseline_file_chartqa, ours_file_chartqa, 'relaxed_overall')
278
+ # failure_ids_baseline_chartqa = [case['doc_id'] for case in failures_baseline_chartqa]
279
+ # failures_ours_chartqa = find_failure_cases_textvqa(ours_file_chartqa, baseline_file_chartqa, 'relaxed_overall')
280
+ # failure_ids_ours_chartqa = [case['doc_id'] for case in failures_ours_chartqa]
281
+ #
282
+ #
283
+ # ###### OCRBench Failure Case Analysis
284
+ # baseline_file_ocrbench = "../Qwen2.5-VL/logs/Qwen__Qwen2.5-VL-7B-Instruct/20251204_094800_samples_ocrbench.jsonl"
285
+ # ours_file_ocrbench = "../Qwen2.5-VL/logs/output__qwen2_5vl-7b-roi-K18T3-134k-gatedv2PB-wGTwRoI-twiginit-filled/20251210_142936_samples_ocrbench.jsonl"
286
+ # failures_baseline_ocrbench = find_failure_cases_textvqa(baseline_file_ocrbench, ours_file_ocrbench, 'ocrbench_accuracy')
287
+ # failure_ids_baseline_ocrbench = [case['doc_id'] for case in failures_baseline_ocrbench]
288
+ # failures_ours_ocrbench = find_failure_cases_textvqa(ours_file_ocrbench, baseline_file_ocrbench, 'ocrbench_accuracy')
289
+ # failure_ids_ours_ocrbench = [case['doc_id'] for case in failures_ours_ocrbench]
290
+ #
291
+
292
+ pass
293
+
294
+
295
+ # if failure_ids_ours:
296
+ # print("\n--- Failure Cases ---")
297
+ # for i, case in enumerate(failure_ids_ours):
298
+ # print(f"\nCase {i + 1} (doc_id: {case['doc_id']})")
299
+ # print(f" Question: {case['question']}")
300
+ # print(f" Ground Truth: {case['ground_truth']}")
301
+ # print(f" Our (Wrong) Prediction: {case['our_prediction']}")
302
+ # print(f" Baseline (Correct) Prediction: {case['baseline_prediction']}")
QWENVL-PRIVATE/analysis/filter_roi_confusion.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import argparse
3
+ import os
4
+
5
+
6
+ def filter_improvements(input_path, output_path, exclude_datasets):
7
+ # 1. Define keywords that indicate the model is confused about image count
8
+ EXCLUDE_KEYWORDS = [
9
+ "images",
10
+ "second image",
11
+ "image 1",
12
+ "image 2",
13
+ "two images"
14
+ ]
15
+
16
+ print(f"Loading data from: {input_path}")
17
+ print(f"Excluding datasets: {exclude_datasets}")
18
+
19
+ if not os.path.exists(input_path):
20
+ print(f"Error: File not found at {input_path}")
21
+ return
22
+
23
+ with open(input_path, 'r', encoding='utf-8') as f:
24
+ data = json.load(f)
25
+
26
+ original_count = len(data)
27
+ filtered_data = []
28
+
29
+ removed_keyword_count = 0
30
+ removed_dataset_count = 0
31
+
32
+ print(f"Processing {original_count} entries...")
33
+
34
+ for entry in data:
35
+ roi_resp = entry.get('roi_response', '').lower()
36
+ dataset_name = entry.get('dataset', '')
37
+
38
+ # Check 1: Is this dataset in the exclusion list?
39
+ if dataset_name in exclude_datasets:
40
+ removed_dataset_count += 1
41
+ continue
42
+
43
+ # Check 2: Does the response contain confusion keywords?
44
+ should_exclude_keyword = any(keyword in roi_resp for keyword in EXCLUDE_KEYWORDS)
45
+ if should_exclude_keyword:
46
+ removed_keyword_count += 1
47
+ continue
48
+
49
+ # Keep entry if it passes both checks
50
+ filtered_data.append(entry)
51
+
52
+ # 2. Save the cleaned data
53
+ with open(output_path, 'w', encoding='utf-8') as f:
54
+ json.dump(filtered_data, f, indent=2)
55
+
56
+ # 3. Print Statistics
57
+ total_removed = removed_keyword_count + removed_dataset_count
58
+ print("-" * 30)
59
+ print(f"Original entries: {original_count}")
60
+ print(f"Removed (Keywords): {removed_keyword_count}")
61
+ print(f"Removed (Dataset match): {removed_dataset_count}")
62
+ print(f"Total Removed: {total_removed}")
63
+ print(f"Remaining entries: {len(filtered_data)}")
64
+ print("-" * 30)
65
+ print(f"Filtered file saved to: {output_path}")
66
+
67
+
68
+ if __name__ == "__main__":
69
+ parser = argparse.ArgumentParser(description="Filter ROI confusion and specific datasets from improvements.json")
70
+ parser.add_argument('--input', type=str, required=True,
71
+ help="Path to the original improvements JSON file")
72
+ parser.add_argument('--output', type=str, required=True,
73
+ help="Path to save the filtered JSON file")
74
+
75
+ # New argument for dataset exclusion
76
+ parser.add_argument('--exclude-datasets', type=str, default="vstar_coco_spatial",
77
+ help="Comma-separated list of datasets to remove (e.g. 'vstar_coco_spatial,other_dataset')")
78
+
79
+ args = parser.parse_args()
80
+
81
+ # Convert comma-separated string to a list of strings
82
+ exclude_list = [d.strip() for d in args.exclude_datasets.split(',') if d.strip()]
83
+
84
+ filter_improvements(args.input, args.output, exclude_list)
85
+
86
+
87
+ '''
88
+ python analysis/filter_roi_confusion.py \
89
+ --input /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/improvements-v2-32b-judge.json \
90
+ --output /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/improvements-v2-clean-32b-judge-wo-spatial.json \
91
+ --exclude-datasets "vstar_coco_spatial"
92
+ '''
QWENVL-PRIVATE/analysis/filter_roi_fails.py ADDED
@@ -0,0 +1,438 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import pickle
4
+ import argparse
5
+ import math
6
+ import torch
7
+ import multiprocessing as mp
8
+ import re
9
+ from torch.utils.data import Dataset, DataLoader
10
+ from transformers import AutoProcessor
11
+ from qwen_src.qwen2_5_vl import Qwen2_5_VLForConditionalGeneration
12
+ try:
13
+ from qwen_src.qwen3_vl import Qwen3VLForConditionalGeneration
14
+ except Exception:
15
+ Qwen3VLForConditionalGeneration = None
16
+ from tqdm import tqdm
17
+ import glob
18
+
19
+
20
+ # --- 1. Dataset & Collator ---
21
+
22
+ class JudgeDataset(Dataset):
23
+ def __init__(self, items):
24
+ self.items = items
25
+
26
+ def __len__(self):
27
+ return len(self.items)
28
+
29
+ def __getitem__(self, idx):
30
+ item = self.items[idx]
31
+
32
+ # 1. Format Ground Truth
33
+ gt = item['ground_truth']
34
+ if isinstance(gt, list):
35
+ gt_str = ", ".join([str(x) for x in gt])
36
+ else:
37
+ gt_str = str(gt)
38
+
39
+ # 2. Judge Prompt
40
+ prompt_text = (
41
+ f"You are an intelligent evaluator for a Visual Question Answering task.\n"
42
+ f"Compare the Model Predictions against the Ground Truth.\n\n"
43
+
44
+ f"--- DATA ---\n"
45
+ f"Question: {item['question']}\n"
46
+ f"Ground Truth: {gt_str}\n"
47
+ f"Model A Prediction: {item['base_resp']}\n"
48
+ f"Model B Prediction: {item['roi_resp']}\n\n"
49
+
50
+ f"--- CRITERIA ---\n"
51
+ f"1. **Containment**: If the Ground Truth is a short value (e.g., '6000') and the Model Prediction contains it correctly within a sentence (e.g., 'The value is 6000'), mark it as **Yes**.\n"
52
+ f"2. **Synonyms**: Accept standard synonyms (e.g., 'Yes' = 'True', '10%' = '0.1').\n"
53
+ f"3. **Formatting**: Ignore punctuation or capitalization differences (e.g., 'april' = 'April').\n"
54
+ f"4. **Contradiction**: Only mark 'No' if the prediction is factually wrong or missing the key information.\n\n"
55
+
56
+ f"--- TASK ---\n"
57
+ f"Does Model A provide the correct answer? (Yes/No)\n"
58
+ f"Does Model B provide the correct answer? (Yes/No)\n\n"
59
+
60
+ f"Respond in this exact format:\n"
61
+ f"A: [Yes/No]\n"
62
+ f"B: [Yes/No]"
63
+ )
64
+
65
+ messages = [
66
+ {"role": "system",
67
+ "content": "You are a fair and robust evaluator. You focus on semantic correctness over strict string matching."},
68
+ {"role": "user", "content": prompt_text}
69
+ ]
70
+
71
+ return {
72
+ "messages": messages,
73
+ "original_item": item
74
+ }
75
+
76
+
77
+ def infer_model_type(model_path, model_type):
78
+ if model_type and model_type != "auto":
79
+ return model_type
80
+ lower = model_path.lower()
81
+ if "qwen3" in lower:
82
+ return "qwen3"
83
+ if "qwen2.5" in lower or "qwen2_5" in lower or "qwen2-5" in lower:
84
+ return "qwen2_5"
85
+ return "qwen2_5"
86
+
87
+
88
+ def resolve_processor_path(model_path, model_type, processor_path=None):
89
+ if processor_path:
90
+ return processor_path
91
+ lower = model_path.lower()
92
+ public_prefix = "/root/public/Qwen"
93
+
94
+ def to_repo_path(name):
95
+ if "/root/public" in model_path:
96
+ return f"{public_prefix}/{name}"
97
+ return f"Qwen/{name}"
98
+
99
+ if model_type == "qwen3":
100
+ if "8b" in lower:
101
+ return to_repo_path("Qwen3-VL-8B-Instruct")
102
+ if "4b" in lower:
103
+ return to_repo_path("Qwen3-VL-4B-Instruct")
104
+ if "7b" in lower:
105
+ return to_repo_path("Qwen2.5-VL-7B-Instruct")
106
+ return model_path
107
+
108
+
109
+ def resolve_model_class(model_type):
110
+ if model_type == "qwen3":
111
+ if Qwen3VLForConditionalGeneration is None:
112
+ raise RuntimeError("Qwen3VLForConditionalGeneration not available.")
113
+ return Qwen3VLForConditionalGeneration
114
+ return Qwen2_5_VLForConditionalGeneration
115
+
116
+
117
+ class JudgeCollator:
118
+ def __init__(self, processor_path):
119
+ self.processor_path = processor_path
120
+ self.processor = None
121
+
122
+ def __call__(self, batch):
123
+ if self.processor is None:
124
+ self.processor = AutoProcessor.from_pretrained(
125
+ self.processor_path,
126
+ trust_remote_code=True,
127
+ padding_side='left',
128
+ use_fast=True
129
+ )
130
+
131
+ raw_messages = [item['messages'] for item in batch]
132
+ original_items = [item['original_item'] for item in batch]
133
+
134
+ text_inputs = [
135
+ self.processor.apply_chat_template(msg, tokenize=False, add_generation_prompt=True)
136
+ for msg in raw_messages
137
+ ]
138
+
139
+ inputs = self.processor(
140
+ text=text_inputs,
141
+ padding=True,
142
+ return_tensors="pt"
143
+ )
144
+
145
+ return {
146
+ "inputs": inputs,
147
+ "items": original_items
148
+ }
149
+
150
+
151
+ # --- 2. Worker Functions ---
152
+
153
+ def worker_process(chunk, gpu_id, args):
154
+ device = f'cuda:{gpu_id}'
155
+
156
+ # Storage for results
157
+ regressions_found = [] # Base: Right, ROI: Wrong
158
+ improvements_found = [] # Base: Wrong, ROI: Right
159
+
160
+ # Setup Dataset & Loader
161
+ dataset = JudgeDataset(chunk)
162
+ collator = JudgeCollator(args.processor_path)
163
+
164
+ dataloader = DataLoader(
165
+ dataset,
166
+ batch_size=args.batch_size,
167
+ shuffle=False,
168
+ num_workers=args.num_workers,
169
+ collate_fn=collator,
170
+ pin_memory=True
171
+ )
172
+
173
+ if len(dataset) == 0:
174
+ return {"regressions": [], "improvements": []}
175
+
176
+ try:
177
+ # Load Model
178
+ print(f"Worker {gpu_id}: Loading Judge Model...")
179
+ model_class = resolve_model_class(args.model_type)
180
+ model = model_class.from_pretrained(
181
+ args.model_path,
182
+ torch_dtype=torch.bfloat16,
183
+ trust_remote_code=True,
184
+ device_map=device
185
+ )
186
+ model.eval()
187
+
188
+ tokenizer = collator.processor.tokenizer if collator.processor else None
189
+ if tokenizer is None:
190
+ tokenizer = AutoProcessor.from_pretrained(args.model_path, trust_remote_code=True).tokenizer
191
+
192
+ print(f"Worker {gpu_id}: Starting Judging...")
193
+
194
+ for batch_data in tqdm(dataloader, desc=f"GPU {gpu_id}", position=gpu_id):
195
+ inputs = batch_data['inputs']
196
+ items = batch_data['items']
197
+
198
+ inputs = inputs.to(device)
199
+
200
+ with torch.no_grad():
201
+ output_ids = model.generate(
202
+ **inputs,
203
+ max_new_tokens=32,
204
+ do_sample=False,
205
+ pad_token_id=tokenizer.eos_token_id
206
+ )
207
+
208
+ input_len = inputs.input_ids.shape[1]
209
+ generated_ids = output_ids[:, input_len:]
210
+ responses = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)
211
+
212
+ for item, resp in zip(items, responses):
213
+ clean_resp = resp.replace("\n", " ").strip()
214
+
215
+ # Parse A (Baseline)
216
+ base_match = re.search(r"A\s*[:\-\.]\s*(Yes|True|Correct)", clean_resp, re.IGNORECASE)
217
+ base_correct = bool(base_match)
218
+
219
+ # Parse B (ROI)
220
+ roi_match = re.search(r"B\s*[:\-\.]\s*(Yes|True|Correct)", clean_resp, re.IGNORECASE)
221
+ roi_correct = bool(roi_match)
222
+
223
+ # Helper to create result dict
224
+ def create_result_entry():
225
+ s_bbox = item.get('surrounding_bbox')
226
+ if hasattr(s_bbox, 'tolist'): s_bbox = s_bbox.tolist()
227
+
228
+ r_mask = item.get('roi_mask')
229
+ mask_shape = r_mask.shape if hasattr(r_mask, 'shape') else "N/A"
230
+
231
+ return {
232
+ "dataset": item.get('dataset', 'unknown'),
233
+ "line_number": item['line_number'],
234
+ "question_id": item['id'],
235
+ "image_id": item['image'],
236
+ "question": item['question'],
237
+ "ground_truth": item['ground_truth'],
238
+ "baseline_response": item['base_resp'],
239
+ "roi_response": item['roi_resp'],
240
+ "judge_raw_output": resp,
241
+ "surrounding_bbox": s_bbox,
242
+ "roi_mask_shape": mask_shape
243
+ }
244
+
245
+ # CASE 1: REGRESSION (Base Right, ROI Wrong)
246
+ if base_correct and not roi_correct:
247
+ regressions_found.append(create_result_entry())
248
+
249
+ # CASE 2: IMPROVEMENT (Base Wrong, ROI Right)
250
+ elif not base_correct and roi_correct:
251
+ improvements_found.append(create_result_entry())
252
+
253
+ del inputs, output_ids, generated_ids
254
+
255
+ except Exception as e:
256
+ print(f"Worker {gpu_id}: Critical Error: {e}")
257
+ import traceback
258
+ traceback.print_exc()
259
+
260
+ return {"regressions": regressions_found, "improvements": improvements_found}
261
+
262
+
263
+ def worker_wrapper(chunk, gpu_id, args, return_dict):
264
+ result = worker_process(chunk, gpu_id, args)
265
+ return_dict[gpu_id] = result
266
+
267
+
268
+ # --- 3. Main Execution ---
269
+
270
+ def load_all_paired_data(search_dir, file_pattern="*_qwen2.5vl-7B.pkl"):
271
+ all_base_data = {}
272
+ all_roi_data = {}
273
+
274
+ search_pattern = os.path.join(search_dir, file_pattern)
275
+ base_files = glob.glob(search_pattern)
276
+
277
+ print(f"Found {len(base_files)} baseline files in '{search_dir}'. Loading pairs...")
278
+
279
+ for base_file in base_files:
280
+ filename = os.path.basename(base_file)
281
+ suffix_name = file_pattern.replace("*", "")
282
+ dataset_name = filename.replace(suffix_name, "")
283
+ roi_file = base_file.replace(".pkl", "-roi.pkl")
284
+
285
+ if not os.path.exists(roi_file):
286
+ print(f"⚠️ Skipping {dataset_name}: ROI file not found ({roi_file})")
287
+ continue
288
+
289
+ try:
290
+ with open(base_file, 'rb') as f:
291
+ cur_base = pickle.load(f)
292
+ with open(roi_file, 'rb') as f:
293
+ cur_roi = pickle.load(f)
294
+
295
+ for k in cur_base.keys():
296
+ if k in cur_roi:
297
+ unique_key = f"{dataset_name}_{k}"
298
+ all_base_data[unique_key] = cur_base[k]
299
+ all_roi_data[unique_key] = cur_roi[k]
300
+ all_base_data[unique_key]['dataset'] = dataset_name
301
+ all_roi_data[unique_key]['dataset'] = dataset_name
302
+
303
+ print(f" + Loaded {dataset_name}: {len(cur_base)} samples")
304
+
305
+ except Exception as e:
306
+ print(f"❌ Error loading {dataset_name}: {e}")
307
+
308
+ return all_base_data, all_roi_data
309
+
310
+
311
+ def main():
312
+ parser = argparse.ArgumentParser()
313
+ parser.add_argument('--model-path', type=str, default='Qwen/Qwen2.5-VL-7B-Instruct')
314
+ parser.add_argument('--model-type', type=str, default="auto",
315
+ choices=["auto", "qwen2_5", "qwen3"],
316
+ help="Model family for loading (auto detects from model path).")
317
+ parser.add_argument('--processor-path', type=str, default="",
318
+ help="Override processor path if auto detection is incorrect.")
319
+ parser.add_argument('--data-dir', type=str, default='./', help="Directory containing .pkl files")
320
+ parser.add_argument('--save-path', type=str, default="regressions.json",
321
+ help="File for Regressions (Base Right, ROI Wrong)")
322
+ parser.add_argument('--save-path-improvements', type=str, default="improvements.json",
323
+ help="File for Improvements (Base Wrong, ROI Right)")
324
+ parser.add_argument('--gpu-ids', type=str, default='0,1,2,3')
325
+ parser.add_argument('--batch-size', type=int, default=8)
326
+ parser.add_argument('--num_workers', type=int, default=4)
327
+ parser.add_argument('--file-pattern', type=str, default="*_qwen2.5vl-7B.pkl",)
328
+ args = parser.parse_args()
329
+ args.model_type = infer_model_type(args.model_path, args.model_type)
330
+ args.processor_path = resolve_processor_path(
331
+ args.model_path, args.model_type, args.processor_path
332
+ )
333
+
334
+ # 1. Load All Data Pairs
335
+ base_data, roi_data = load_all_paired_data(args.data_dir, file_pattern=args.file_pattern)
336
+
337
+ if not base_data:
338
+ print("No paired data found. Exiting.")
339
+ return
340
+
341
+ # 2. Prepare Items
342
+ judge_items = []
343
+ print(f"Preparing {len(base_data)} items for judging...")
344
+
345
+ for k, base_item in base_data.items():
346
+ roi_item = roi_data[k]
347
+
348
+ # Optimization: Skip identical strings (Result is definitely same, so neither reg nor imp)
349
+ if str(base_item.get('response', '')).strip() == str(roi_item.get('response', '')).strip():
350
+ continue
351
+
352
+ judge_items.append({
353
+ 'unique_key': k,
354
+ 'dataset': base_item.get('dataset', 'unknown'),
355
+ 'line_number': base_item.get('line_number', 'unknown'),
356
+ 'id': base_item['id'],
357
+ 'image': base_item['image'],
358
+ 'question': base_item['prompt'],
359
+ 'ground_truth': base_item['text'],
360
+ 'base_resp': base_item.get('response', ''),
361
+ 'roi_resp': roi_item.get('response', ''),
362
+ 'surrounding_bbox': roi_item.get('surrounding_bbox'),
363
+ 'roi_mask': roi_item.get('roi_mask')
364
+ })
365
+
366
+ print(f"Items requiring judgement (non-identical): {len(judge_items)}")
367
+
368
+ # 3. Parallel Processing
369
+ try:
370
+ mp.set_start_method('spawn', force=True)
371
+ except RuntimeError:
372
+ pass
373
+
374
+ gpu_ids = [int(gid) for gid in args.gpu_ids.split(',')]
375
+ num_gpus = len(gpu_ids)
376
+
377
+ chunk_size = math.ceil(len(judge_items) / num_gpus)
378
+ chunks = [judge_items[i:i + chunk_size] for i in range(0, len(judge_items), chunk_size)]
379
+
380
+ manager = mp.Manager()
381
+ return_dict = manager.dict()
382
+ processes = []
383
+
384
+ print(f"Starting judging on {num_gpus} GPUs...")
385
+ for i in range(num_gpus):
386
+ chunk_data = chunks[i] if i < len(chunks) else []
387
+ p = mp.Process(target=worker_wrapper, args=(chunk_data, gpu_ids[i], args, return_dict))
388
+ p.start()
389
+ processes.append(p)
390
+
391
+ for p in processes:
392
+ p.join()
393
+
394
+ # 4. Aggregation
395
+ all_regressions = []
396
+ all_improvements = []
397
+
398
+ for gpu_id, res in return_dict.items():
399
+ all_regressions.extend(res['regressions'])
400
+ all_improvements.extend(res['improvements'])
401
+
402
+ print(f"\n✅ Analysis Complete.")
403
+ print(f" - Regressions (Worse): {len(all_regressions)}")
404
+ print(f" - Improvements (Better): {len(all_improvements)}")
405
+
406
+ # Save Regressions
407
+ with open(args.save_path, 'w', encoding='utf-8') as f:
408
+ json.dump(all_regressions, f, indent=2)
409
+ print(f"Saved regressions to {args.save_path}")
410
+
411
+ # Save Improvements
412
+ with open(args.save_path_improvements, 'w', encoding='utf-8') as f:
413
+ json.dump(all_improvements, f, indent=2)
414
+ print(f"Saved improvements to {args.save_path_improvements}")
415
+
416
+
417
+ if __name__ == "__main__":
418
+ main()
419
+
420
+ '''
421
+ python analysis/filter_roi_fails.py \
422
+ --model-path /root/public/Qwen/Qwen2.5-VL-7B-Instruct \
423
+ --data-dir /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining \
424
+ --save-path /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/regressions-v2-short.json \
425
+ --save-path-improvements /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/improvements-v2-short.json \
426
+ --batch-size 8 \
427
+ --gpu-ids 0,1
428
+
429
+ python analysis/filter_roi_fails.py \
430
+ --model-path /root/public/Qwen/Qwen2.5-VL-7B-Instruct \
431
+ --data-dir /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining \
432
+ --search-pattern "*_qwen2.5vl-3B.pkl" \
433
+ --save-path /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/regressions-3b.json \
434
+ --save-path-improvements /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/improvements-3b.json \
435
+ --batch-size 8 \
436
+ --gpu-ids 0,1,2,3
437
+
438
+ '''
QWENVL-PRIVATE/analysis/fix_stage1_image_paths.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import os
3
+ import pickle
4
+ import re
5
+ from pathlib import PureWindowsPath
6
+
7
+
8
+ def normalize_path(path_str):
9
+ return path_str.replace("\\", "/")
10
+
11
+
12
+ def is_abs_like(path_str):
13
+ if os.path.isabs(path_str):
14
+ return True
15
+ if re.match(r"^[A-Za-z]:\\\\", path_str):
16
+ return True
17
+ if re.match(r"^[A-Za-z]:/", path_str):
18
+ return True
19
+ return False
20
+
21
+
22
+ def maybe_make_relative(path_str, prefixes, fallback_basename=False):
23
+ if not isinstance(path_str, str):
24
+ return path_str, False
25
+
26
+ if not is_abs_like(path_str):
27
+ return path_str, False
28
+
29
+ raw = path_str
30
+ norm = normalize_path(path_str).rstrip("/")
31
+ for prefix in prefixes:
32
+ if not prefix:
33
+ continue
34
+ prefix_norm = normalize_path(prefix).rstrip("/")
35
+ if norm == prefix_norm:
36
+ return os.path.basename(norm), True
37
+ if norm.startswith(prefix_norm + "/"):
38
+ return norm[len(prefix_norm) + 1 :], True
39
+
40
+ if fallback_basename:
41
+ return os.path.basename(norm), True
42
+
43
+ return raw, False
44
+
45
+
46
+ def iter_item_dicts(obj):
47
+ if isinstance(obj, list):
48
+ for item in obj:
49
+ if isinstance(item, dict):
50
+ yield item
51
+ elif isinstance(obj, dict):
52
+ # Common cases: dict of items or dict with list values
53
+ for value in obj.values():
54
+ if isinstance(value, dict):
55
+ yield value
56
+ elif isinstance(value, list):
57
+ for item in value:
58
+ if isinstance(item, dict):
59
+ yield item
60
+
61
+
62
+ def process_pkl(path, prefixes, fallback_basename=False):
63
+ with open(path, "rb") as f:
64
+ try:
65
+ data = pickle.load(f)
66
+ except EOFError:
67
+ print(f"Warning: empty or corrupted pickle, skipping: {path}")
68
+ return None, 0, 0
69
+
70
+ updated = 0
71
+ total = 0
72
+ for item in iter_item_dicts(data):
73
+ if "image" not in item:
74
+ continue
75
+ total += 1
76
+ new_val, changed = maybe_make_relative(
77
+ item["image"], prefixes, fallback_basename=fallback_basename
78
+ )
79
+ if changed:
80
+ item["image"] = new_val
81
+ updated += 1
82
+
83
+ return data, total, updated
84
+
85
+
86
+ def main():
87
+ parser = argparse.ArgumentParser(
88
+ description="Replace absolute image paths with relative paths in stage1 pkls."
89
+ )
90
+ parser.add_argument(
91
+ "--input-dir",
92
+ type=str,
93
+ required=True,
94
+ help="Directory containing .pkl files to process.",
95
+ )
96
+ parser.add_argument(
97
+ "--prefix-to-strip",
98
+ type=str,
99
+ action="append",
100
+ default=[],
101
+ help="Prefix to strip from absolute image paths. Can be set multiple times.",
102
+ )
103
+ parser.add_argument(
104
+ "--dataset-root",
105
+ type=str,
106
+ default="",
107
+ help="Optional dataset root path to strip (added to prefix list if set).",
108
+ )
109
+ parser.add_argument(
110
+ "--fallback-basename",
111
+ action="store_true",
112
+ help="If no prefix matches, fallback to basename for absolute paths.",
113
+ )
114
+ parser.add_argument(
115
+ "--out-dir",
116
+ type=str,
117
+ default="",
118
+ help="Output directory for fixed pkls (default: input dir).",
119
+ )
120
+ parser.add_argument(
121
+ "--inplace",
122
+ action="store_true",
123
+ help="Overwrite pkls in place (ignores --out-dir).",
124
+ )
125
+ args = parser.parse_args()
126
+
127
+ prefixes = list(args.prefix_to_strip)
128
+ if args.dataset_root:
129
+ prefixes.append(args.dataset_root)
130
+
131
+ input_dir = os.path.abspath(args.input_dir)
132
+ out_dir = input_dir if args.inplace else (args.out_dir or input_dir)
133
+ os.makedirs(out_dir, exist_ok=True)
134
+
135
+ pkl_files = [
136
+ os.path.join(input_dir, name)
137
+ for name in os.listdir(input_dir)
138
+ if name.endswith(".pkl")
139
+ ]
140
+
141
+ if not pkl_files:
142
+ print(f"No .pkl files found in {input_dir}")
143
+ return
144
+
145
+ for pkl_path in pkl_files:
146
+ data, total, updated = process_pkl(
147
+ pkl_path, prefixes, fallback_basename=args.fallback_basename
148
+ )
149
+ if data is None:
150
+ continue
151
+ if args.inplace:
152
+ out_path = pkl_path
153
+ else:
154
+ base = os.path.basename(pkl_path)
155
+ out_path = os.path.join(out_dir, base.replace(".pkl", "_relpath.pkl"))
156
+
157
+ with open(out_path, "wb") as f:
158
+ pickle.dump(data, f)
159
+
160
+ print(
161
+ f"{os.path.basename(pkl_path)}: updated {updated}/{total} image entries -> {out_path}"
162
+ )
163
+
164
+
165
+ if __name__ == "__main__":
166
+ main()
QWENVL-PRIVATE/analysis/gated_roi_analysis.py ADDED
@@ -0,0 +1,899 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ script_dir = os.path.dirname(os.path.abspath(__file__))
4
+ project_root = os.path.dirname(script_dir)
5
+ sys.path.append(project_root)
6
+
7
+ from qwen_src.qwen2_5_vl import Qwen2_5_VLForConditionalGeneration
8
+ from qwen_src.qwen2_vl import Qwen2VLForConditionalGeneration
9
+ import torch
10
+ import matplotlib.pyplot as plt
11
+ from qwen_vl_utils import process_vision_info
12
+ from transformers import AutoProcessor
13
+ import math
14
+ from PIL import Image, ImageDraw, ImageFont
15
+ import numpy as np
16
+ from qwen_src.ana_utils import register_hooks, get_attn_weights_map, plot_attention_across_layers, \
17
+ plot_tensor_2d, visualize_token_wise_attention,get_visual_feat_norm, visualize_norm_values, plot_attention_analysis
18
+ import json
19
+ import os
20
+ from tqdm import tqdm
21
+ import torchvision.transforms.functional as torchvision_F
22
+ from qwen_src.mm_utils import expand2square, create_pseudo_labels
23
+ import matplotlib.patches as patches
24
+ from matplotlib.colors import ListedColormap, BoundaryNorm
25
+ import pickle
26
+ import argparse
27
+ import multiprocessing as mp
28
+ import pandas as pd # <-- Make sure to import pandas
29
+ from matching_utils import TextVQAAccuracyEvaluator
30
+ from qwen_vl_utils import process_vision_info
31
+ import requests
32
+ import io
33
+
34
+ def get_KL_divergence(logits_list,):
35
+ import torch.nn as nn
36
+ from torch.nn import functional as F
37
+ KL_div = nn.KLDivLoss(reduction='batchmean')
38
+ KL_div_list = [KL_div(F.log_softmax(logits_list[-1], dim=1), F.softmax(logits_list[i], dim=1)).item() for i in range(len(logits_list))]
39
+ return KL_div_list
40
+
41
+
42
+ def plot_kl_divergence_distribution(kl_div_pickle_file,
43
+ save_path,
44
+ trim_threshold=None,
45
+ bins=50,
46
+ title="Distribution of KL Divergence"):
47
+ print(f"Loading KL data from {kl_div_pickle_file}...")
48
+ try:
49
+ with open(kl_div_pickle_file, 'rb') as f:
50
+ KL_div_dict = pickle.load(f)
51
+ except FileNotFoundError:
52
+ print(f"Error: Pickle file not found at {kl_div_pickle_file}")
53
+ return
54
+ except Exception as e:
55
+ print(f"Error loading pickle file: {e}")
56
+ return
57
+
58
+ # --- 1. Load and process all data ---
59
+ processed_data = []
60
+ for line_num, s in KL_div_dict.items():
61
+ if 'KL_divergence' in s and 'base_answer' in s and 'two_stage_answer' in s:
62
+ processed_data.append({
63
+ 'kl': s['KL_divergence'],
64
+ 'different': str(s['base_answer']) != str(s['two_stage_answer'])
65
+ })
66
+ else:
67
+ print(f"Warning: Skipping item {line_num}, missing required keys.")
68
+
69
+ if not processed_data:
70
+ print("No valid data found in the pickle file.")
71
+ return
72
+
73
+ df = pd.DataFrame(processed_data)
74
+ original_count = len(df)
75
+ plot_title = title
76
+
77
+ # --- 2. Handle trimming (clipping) ---
78
+ # Apply your logic: clip values larger than the threshold
79
+ if trim_threshold is not None:
80
+ kl_for_hist = df['kl'].clip(upper=trim_threshold)
81
+ print(f"Original data points: {original_count}")
82
+ plot_title = f"{title}\n(Values > {trim_threshold} clipped to {trim_threshold})"
83
+ else:
84
+ kl_for_hist = df['kl']
85
+ print(f"Plotting all {original_count} data points. No trimming.")
86
+
87
+ # --- 3. Plot 1: KL Divergence Histogram ---
88
+ print("Generating Histogram plot...")
89
+ plt.figure(figsize=(10, 6))
90
+
91
+ # Create the histogram AND capture the bin edges
92
+ counts, bin_edges, _ = plt.hist(kl_for_hist, bins=bins,
93
+ edgecolor='k', alpha=0.75, color='skyblue')
94
+
95
+ # Add a vertical line for the mean
96
+ mean_val = np.mean(kl_for_hist)
97
+ plt.axvline(mean_val, color='r', linestyle='dashed', linewidth=2)
98
+
99
+ # Get current y-axis limits to position text
100
+ ymin, ymax = plt.ylim()
101
+ plt.text(mean_val * 1.05, ymax * 0.9, f'Mean: {mean_val:.3f}', color='r')
102
+
103
+ plt.xlabel("KL Divergence Score")
104
+ plt.ylabel("Frequency (Count)")
105
+ plt.title(plot_title)
106
+ plt.grid(True, linestyle='--', alpha=0.6)
107
+ plt.tight_layout()
108
+ plt.show()
109
+ plt.close() # Close the plot to free memory
110
+
111
+ # --- 4. Calculate Ratios for Second Plot ---
112
+ print("Calculating ratios for the second plot...")
113
+
114
+ # Use pandas.cut to put each sample into the bins created by plt.hist
115
+ # We use the *clipped* KL values to match the histogram
116
+ df['bin'] = pd.cut(kl_for_hist, bins=bin_edges,
117
+ include_lowest=True, right=True)
118
+
119
+ # Group by the bins and calculate the mean of 'different' (True=1, False=0)
120
+ bin_ratios = df.groupby('bin')['different'].mean()
121
+
122
+ # Get the center of each bin for plotting
123
+ bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
124
+
125
+ # --- 5. Plot 2: Answer Difference Ratio ---
126
+ print("Generating Answer Difference Ratio plot...")
127
+ plt.figure(figsize=(10, 6))
128
+
129
+ plt.bar(bin_centers, bin_ratios.fillna(0),
130
+ width=np.diff(bin_edges),
131
+ edgecolor='k', alpha=0.85, color='salmon')
132
+
133
+ plt.xlabel("KL Divergence Bin")
134
+ plt.ylabel("Ratio of Different Answers")
135
+ plt.title("Answer Difference Ratio per KL Divergence Bin")
136
+ plt.grid(True, linestyle='--', alpha=0.6)
137
+ plt.ylim(0, 1.0) # Ratio is always between 0 and 1
138
+ plt.tight_layout()
139
+ plt.show()
140
+ plt.close()
141
+ def case_study_KL_divergence(model, processor,
142
+ question_file, image_folder,
143
+ kl_div_pickle_file, threshold,
144
+ max_cases=10, max_new_tokens=1024):
145
+ """
146
+ Performs a case study on questions with high KL divergence between
147
+ base and two-stage model logits.
148
+
149
+ Args:
150
+ model: The multimodal model.
151
+ processor: The model's processor.
152
+ question_file (str): Path to the JSONL file with all questions.
153
+ image_folder (str): Path to the folder containing images.
154
+ kl_div_pickle_file (str): Path to the .pkl file containing the
155
+ dictionary of {question_id: kl_divergence}.
156
+ threshold (float): The minimum KL divergence score to include
157
+ in the case study.
158
+ max_cases (int, optional): The maximum number of cases to print.
159
+ Defaults to 10.
160
+ max_new_tokens (int, optional): The max tokens for generation.
161
+ Defaults to 1024.
162
+ """
163
+
164
+ print(f"🚀 Starting case study for KL Divergence > {threshold}")
165
+
166
+ # 1. Load KL divergence data
167
+ print(f"Loading KL divergence data from: {kl_div_pickle_file}")
168
+ with open(kl_div_pickle_file, 'rb') as f:
169
+ KL_div_dict = pickle.load(f)
170
+
171
+ # 2. Load and preprocess all questions
172
+ questions = [json.loads(q) for q in open(os.path.expanduser(question_file), "r")]
173
+
174
+ questions_dict = {}
175
+ for tmp_ques in questions:
176
+ qs = tmp_ques['text']
177
+ # Remove lines with 'Reference OCR' as in the original function
178
+ if 'Reference OCR' in qs:
179
+ qs = '\n'.join([ele for ele in qs.split('\n') if 'Reference OCR' not in ele])
180
+ tmp_ques['text'] = qs
181
+ questions_dict[tmp_ques['question_id']] = tmp_ques
182
+
183
+ # 3. Filter for high KL divergence questions
184
+ high_kl_cases = []
185
+ for q_id, kl_div_sample in KL_div_dict.items():
186
+ kl_div = kl_div_sample['KL_divergence']
187
+ if kl_div > threshold:
188
+ if q_id in questions_dict:
189
+ high_kl_cases.append((q_id, kl_div, questions_dict[q_id]))
190
+ else:
191
+ print(f"Warning: Question ID {q_id} from pickle file not found in question file.")
192
+
193
+ # # Sort by KL divergence (highest first)
194
+ # high_kl_cases.sort(key=lambda x: x[1], reverse=True)
195
+
196
+ if not high_kl_cases:
197
+ print("No questions found above the threshold.")
198
+ return
199
+
200
+ # Set processor image size limits (matching your eval function)
201
+ processor.image_processor.max_pixels = 576 * 28 * 28
202
+ processor.image_processor.min_pixels = 576 * 28 * 28
203
+
204
+ # 4. Loop through filtered cases and generate answers
205
+ print(f"Found {len(high_kl_cases)} cases above threshold. Analyzing top {min(len(high_kl_cases), max_cases)}...")
206
+
207
+ model.model.is_debug = True
208
+ case_count = 0
209
+ for q_id, kl_value, line in high_kl_cases:
210
+ if case_count >= max_cases:
211
+ print(f"Reached max_cases limit of {max_cases}.")
212
+ break
213
+
214
+ case_count += 1
215
+
216
+ try:
217
+ image_file = line["image"]
218
+ qs = "<image>\n" + line["text"]
219
+ image_path = os.path.join(image_folder, image_file)
220
+ image = Image.open(image_path).convert('RGB')
221
+
222
+ # Prepare inputs
223
+ messages = [
224
+ {
225
+ "role": "user",
226
+ "content": [
227
+ {"type": "text", "text": f"{qs}"},
228
+ {"image": image_path} # Note: path is used by template, image obj by processor
229
+ ]
230
+ }
231
+ ]
232
+ text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
233
+ text = text.replace('<|vision_start|><|image_pad|><|vision_end|>', '').replace('<image>',
234
+ '<image><|vision_start|><|image_pad|><|vision_end|>')
235
+ inputs = processor(text=[text], images=[image], padding=True, return_tensors="pt").to('cuda')
236
+ inputs.data['src_images'] = [image]
237
+ inputs.data['processor'] = processor
238
+ input_ids_len = inputs.input_ids.shape[1]
239
+
240
+ print(f"\n--- CASE STUDY {case_count} (Q_ID: {q_id}) ---")
241
+ print(f"Image File: {image_file}")
242
+ print(f"KL Divergence: {kl_value:.4f}")
243
+ print(f"Question: {line['text']}")
244
+ print("---")
245
+
246
+ # --- 4a. Generate with two_stage_flag = False (Base Model) ---
247
+ two_stage_flag = False
248
+ model.model.enable_twig = two_stage_flag
249
+ model.model.roi_enable2stage = two_stage_flag
250
+ model.model.two_stage_vanilla = two_stage_flag
251
+ model.roi_enable2stage = two_stage_flag
252
+ model.model.roi_conf_thresh = 0.05
253
+
254
+ with torch.no_grad():
255
+ output_ids = model.generate(**inputs, max_new_tokens=max_new_tokens, output_attentions=False,
256
+ pad_token_id=processor.tokenizer.eos_token_id)
257
+
258
+ generated_ids = output_ids[:, input_ids_len:]
259
+ base_answer = \
260
+ processor.batch_decode(generated_ids, skip_special_tokens=True, clean_up_tokenization_spaces=True)[0]
261
+ print(f"**Base Answer (RoI OFF):**\n{base_answer}\n")
262
+
263
+ # --- 4b. Generate with two_stage_flag = True (Two-Stage Model) ---
264
+ two_stage_flag = True
265
+ model.model.enable_twig = two_stage_flag
266
+ model.model.roi_enable2stage = two_stage_flag
267
+ model.model.two_stage_vanilla = two_stage_flag
268
+ model.roi_enable2stage = two_stage_flag
269
+ model.model.roi_conf_thresh = 0.05
270
+
271
+ with torch.no_grad():
272
+ output_ids = model.generate(**inputs, max_new_tokens=max_new_tokens, output_attentions=False,
273
+ pad_token_id=processor.tokenizer.eos_token_id)
274
+
275
+ generated_ids = output_ids[:, input_ids_len:]
276
+ two_stage_answer = processor.batch_decode(generated_ids, skip_special_tokens=True, clean_up_tokenization_spaces=True)[0]
277
+ print(f"**Two-Stage Answer (RoI ON):**\n{two_stage_answer}\n")
278
+ print("-" * 40)
279
+
280
+ except Exception as e:
281
+ print(f"Error processing question {q_id}: {e}")
282
+ continue
283
+
284
+ print("✅ Case study complete.")
285
+
286
+
287
+ def eval_roi_KLdivergence(model, processor, question_file, image_folder, dst_save_path, max_new_tokens=1024):
288
+
289
+ questions = [json.loads(q) for q in open(os.path.expanduser(question_file), "r")]
290
+ system_prompt= "You are a helpful assistant"
291
+ sample = 10
292
+ two_stage_flag = False
293
+
294
+ processor.image_processor.max_pixels = 576 * 28 * 28
295
+ processor.image_processor.min_pixels = 576 * 28 * 28
296
+
297
+ new_questions = []
298
+ for tmp_ques in questions:
299
+ qs = tmp_ques['text']
300
+ if 'Reference OCR' in qs:
301
+ # Remove lines with 'Reference OCR
302
+ qs = '\n'.join([ele for ele in qs.split('\n') if 'Reference OCR' not in ele])
303
+ tmp_ques['text'] = qs
304
+ new_questions.append(tmp_ques)
305
+ questions = new_questions
306
+ model.model.is_debug = False
307
+ KL_div_analysis_dict = {}
308
+ for line in tqdm(questions):
309
+ image_file = line["image"]
310
+ qs = "<image>\n" + line["text"]
311
+ image = Image.open(os.path.join(image_folder, image_file)).convert('RGB')
312
+ messages = [
313
+ {
314
+ "role": "user",
315
+ "content": [
316
+ {
317
+ "type": "text",
318
+ "text": f"{qs}"
319
+ },
320
+ {
321
+ "image": os.path.join(image_folder, image_file)
322
+ }
323
+ ]
324
+ }
325
+ ]
326
+ text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
327
+ text = text.replace('<|vision_start|><|image_pad|><|vision_end|>', '').replace('<image>', '<image><|vision_start|><|image_pad|><|vision_end|>')
328
+ inputs = processor(text=[text], images=[image], padding=True, return_tensors="pt").to('cuda')
329
+ inputs.data['src_images'] = [image]
330
+ inputs.data['processor'] = processor
331
+ input_ids_len = inputs.input_ids.shape[1]
332
+
333
+ two_stage_flag = False
334
+ model.model.enable_twig = two_stage_flag
335
+ model.model.roi_enable2stage = two_stage_flag
336
+ model.model.two_stage_vanilla = two_stage_flag
337
+ model.roi_enable2stage = two_stage_flag
338
+ model.model.roi_conf_thresh = 0.05
339
+ with torch.no_grad():
340
+ output_dicts = model.forward(**inputs.data,
341
+ output_attentions=True,
342
+ return_dict=False,
343
+ output_hidden_states=False
344
+ )
345
+ base_logits = output_dicts[0][:,-1]
346
+ with torch.no_grad():
347
+ output_ids = model.generate(**inputs, max_new_tokens=max_new_tokens, output_attentions=False,
348
+ pad_token_id=processor.tokenizer.eos_token_id)
349
+
350
+ generated_ids = output_ids[:, input_ids_len:]
351
+ base_answer = processor.batch_decode(generated_ids, skip_special_tokens=True, clean_up_tokenization_spaces=True)[0]
352
+
353
+ two_stage_flag = True
354
+ model.model.enable_twig = two_stage_flag
355
+ model.model.roi_enable2stage = two_stage_flag
356
+ model.model.two_stage_vanilla = two_stage_flag
357
+ model.roi_enable2stage = two_stage_flag
358
+ model.model.roi_conf_thresh = 0.05
359
+ with torch.no_grad():
360
+ output_dicts = model.forward(**inputs.data,
361
+ output_attentions=False,
362
+ return_dict=False,
363
+ output_hidden_states=False
364
+ )
365
+ two_stage_logits = output_dicts[0][:, -1]
366
+
367
+ logits_list = [base_logits, two_stage_logits]
368
+ KL_div_list = get_KL_divergence(logits_list)
369
+
370
+ with torch.no_grad():
371
+ output_ids = model.generate(**inputs, max_new_tokens=max_new_tokens, output_attentions=False,
372
+ pad_token_id=processor.tokenizer.eos_token_id)
373
+
374
+ generated_ids = output_ids[:, input_ids_len:]
375
+ two_stage_answer = processor.batch_decode(generated_ids, skip_special_tokens=True, clean_up_tokenization_spaces=True)[0]
376
+ print(f"**Two-Stage Answer (RoI ON):**\n{two_stage_answer}\n")
377
+
378
+ print(f"\n--- Processing sample: {image_file} ---")
379
+ print(f"Question: {qs}")
380
+ print("KL Divergence between RoI on and off:", KL_div_list[0])
381
+ sample += 1
382
+ tmp_sample = {
383
+ 'image_file': image_file,
384
+ 'question': qs,
385
+ 'base_answer': base_answer,
386
+ 'two_stage_answer': two_stage_answer,
387
+ 'KL_divergence': KL_div_list[0]
388
+ }
389
+ KL_div_analysis_dict[line['question_id']] = tmp_sample
390
+ pass
391
+
392
+ print(f'saving KL divergence analysis dict to:{dst_save_path}')
393
+ with open(dst_save_path, 'wb') as f:
394
+ pickle.dump(KL_div_analysis_dict, f)
395
+
396
+
397
+ def worker_process_kl_divergence(questions_chunk, gpu_id, args):
398
+ """
399
+ This is the worker function that runs on a single GPU.
400
+ It loads the model and processes its assigned chunk of questions.
401
+ """
402
+
403
+ device = f'cuda:{gpu_id}'
404
+ KL_div_analysis_dict = {}
405
+ max_new_tokens = 1024
406
+ if not questions_chunk:
407
+ print(f"Worker {gpu_id}: No questions in chunk. Exiting.")
408
+ return KL_div_analysis_dict
409
+
410
+ try:
411
+ # 1. Load Model and Processor INSIDE the worker
412
+ print(f"Worker {gpu_id}: Loading processor from {args.model_path}...")
413
+ processor = AutoProcessor.from_pretrained(
414
+ 'Qwen/Qwen2.5-VL-7B-Instruct',
415
+ trust_remote_code=True,
416
+ padding_side='left', use_fast=True
417
+ )
418
+
419
+ print(f"Worker {gpu_id}: Loading model from {args.model_path}...")
420
+ model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
421
+ args.model_path,
422
+ torch_dtype=torch.bfloat16,
423
+ trust_remote_code=True,
424
+ device_map=device # Assign model directly to the correct device
425
+ )
426
+ model.eval()
427
+
428
+ # Set processor image size limits (matching your original function)
429
+ processor.image_processor.max_pixels = 576 * 28 * 28
430
+ processor.image_processor.min_pixels = 576 * 28 * 28
431
+
432
+ # 2. Process the chunk of questions
433
+ print(f"Worker {gpu_id}: Starting processing of {len(questions_chunk)} items.")
434
+
435
+ # Use tqdm for progress, disable for all but the first worker if too noisy
436
+ progress_bar = tqdm(questions_chunk, desc=f"GPU {gpu_id}", position=gpu_id)
437
+
438
+ for line in progress_bar:
439
+ try:
440
+ # --- MODIFIED: Handle different dataset formats ---
441
+ image_file_str = ''
442
+ ground_truth = None
443
+
444
+ if args.question_file is not None: # Case 1: Old format with "text" key
445
+ qs_text = line["text"]
446
+ qs_text = qs_text + " Answer the question using a single word or phrase." if "Answer the question using a single word or phrase." not in qs_text else qs_text
447
+ image_file = line["image"]
448
+ image_path = os.path.join(args.image_folder, image_file)
449
+ image = Image.open(image_path).convert('RGB')
450
+ image_file_str = image_file
451
+
452
+ elif args.hf_dataset is not None: # Case 2: New HF Dataset format (e.g., vstar_bench)
453
+ if 'vstar' in args.hf_dataset:
454
+ qs_text = line["text"]
455
+ qs_text = qs_text + " Answer the question using a single word or phrase." if "Answer the question using a single word or phrase." not in qs_text else qs_text
456
+ image = line["image"].convert('RGB')
457
+ ground_truth = line.get("label") # Store the ground truth
458
+ image_file_str = f"hf_dataset_{line.get('question_id', 'unknown')}"
459
+ elif 'POPE' in args.hf_dataset:
460
+ qs_text = line["question"] + " Answer the question using a single word or phrase."
461
+ image = line["image"].convert('RGB')
462
+ ground_truth = line.get("answer")
463
+ image_file_str = f"hf_dataset_{line.get('question_id', 'unknown')}"
464
+
465
+ else:
466
+ print(f"Worker {gpu_id}: Skipping line {line.get('original_line_number')}, unknown format.")
467
+ continue
468
+ # --- END MODIFICATION ---
469
+ qs = "<image>\n" + qs_text
470
+ messages = [
471
+ {"role": "user", "content": [{"type": "text", "text": f"{qs}"}, {"image": image}]}
472
+ ]
473
+ text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
474
+ text = text.replace('<|vision_start|><|image_pad|><|vision_end|>', '').replace('<image>',
475
+ '<image><|vision_start|><|image_pad|><|vision_end|>')
476
+
477
+ inputs = processor(text=[text], images=[image], padding=True, return_tensors="pt").to(device)
478
+
479
+ # We don't need these for KL div, but keeping if your model.forward needs them
480
+ inputs.data['src_images'] = [image]
481
+ inputs.data['processor'] = processor
482
+ input_ids_len = inputs.input_ids.shape[1]
483
+
484
+ # --- Run with two_stage_flag = False (Base) ---
485
+ two_stage_flag = False
486
+ model.model.enable_twig = two_stage_flag
487
+ model.model.roi_enable2stage = two_stage_flag
488
+ model.model.two_stage_vanilla = two_stage_flag
489
+ model.roi_enable2stage = two_stage_flag
490
+ model.model.roi_conf_thresh = 0.05
491
+ with torch.no_grad():
492
+ output_dicts = model.forward(**inputs.data,
493
+ output_attentions=True,
494
+ return_dict=False,
495
+ output_hidden_states=False
496
+ )
497
+ base_logits = output_dicts[0][:, -1]
498
+ with torch.no_grad():
499
+ output_ids = model.generate(**inputs, max_new_tokens=max_new_tokens, output_attentions=False,
500
+ pad_token_id=processor.tokenizer.eos_token_id)
501
+
502
+ generated_ids = output_ids[:, input_ids_len:]
503
+ base_answer = \
504
+ processor.batch_decode(generated_ids, skip_special_tokens=True, clean_up_tokenization_spaces=True)[
505
+ 0]
506
+
507
+ # --- Run with two_stage_flag = True (Two-Stage) ---
508
+ two_stage_flag = True
509
+ model.model.enable_twig = two_stage_flag
510
+ model.model.roi_enable2stage = two_stage_flag
511
+ model.model.two_stage_vanilla = two_stage_flag
512
+ model.roi_enable2stage = two_stage_flag
513
+ model.model.roi_conf_thresh = 0.05
514
+ with torch.no_grad():
515
+ output_dicts = model.forward(**inputs.data,
516
+ output_attentions=False,
517
+ return_dict=False,
518
+ output_hidden_states=False
519
+ )
520
+ two_stage_logits = output_dicts[0][:, -1]
521
+
522
+ logits_list = [base_logits, two_stage_logits]
523
+ KL_div_list = get_KL_divergence(logits_list)
524
+
525
+ with torch.no_grad():
526
+ output_ids = model.generate(**inputs, max_new_tokens=max_new_tokens, output_attentions=False,
527
+ pad_token_id=processor.tokenizer.eos_token_id)
528
+
529
+ generated_ids = output_ids[:, input_ids_len:]
530
+ two_stage_answer = \
531
+ processor.batch_decode(generated_ids, skip_special_tokens=True, clean_up_tokenization_spaces=True)[
532
+ 0]
533
+
534
+ tmp_sample = {
535
+ 'image_file': image_file_str,
536
+ 'question': qs_text, # Store the clean question
537
+ 'base_answer': base_answer,
538
+ 'two_stage_answer': two_stage_answer,
539
+ 'KL_divergence': KL_div_list[0],
540
+ 'ground_truth': ground_truth # This will be None for old data, or the answer
541
+ }
542
+
543
+ # *** MODIFIED LINE ***
544
+ # Use the original_line_number as the unique key
545
+ line_number = line['original_line_number']
546
+ if line_number in KL_div_analysis_dict:
547
+ print(
548
+ f"Worker {gpu_id}: Warning - Duplicate line_number {line_number} found. Overwriting previous entry.")
549
+ KL_div_analysis_dict[line_number] = tmp_sample
550
+
551
+ except Exception as e:
552
+ # *** MODIFIED LINE ***
553
+ # Update error logging to use line_number
554
+ print(f"Worker {gpu_id}: Error processing {line.get('original_line_number', 'unknown line')}: {e}")
555
+ continue
556
+
557
+ progress_bar.close()
558
+
559
+ except Exception as e:
560
+ print(f"Worker {gpu_id}: A critical error occurred: {e}")
561
+ finally:
562
+ # Clean up model from GPU memory
563
+ del model
564
+ del processor
565
+ torch.cuda.empty_cache()
566
+ print(f"Worker {gpu_id}: Finished and cleaned up.")
567
+
568
+ return KL_div_analysis_dict
569
+
570
+
571
+ def evaluate_accuracy_with_kl_bins(kl_div_pickle_file,
572
+ plot_save_path,
573
+ annotation_file=None, # <-- Made optional
574
+ trim_threshold=None,
575
+ bins=20):
576
+ """
577
+ Evaluates accuracy for base and two-stage answers, and plots
578
+ accuracy vs. KL divergence bins.
579
+
580
+ If annotation_file is provided, it's used as the GT source.
581
+ If None, 'ground_truth' key from the pickle file is used.
582
+ """
583
+
584
+ gt_answers_by_line = {}
585
+ use_external_gt = False
586
+
587
+ # --- 1. Load Ground Truth (Conditionally) ---
588
+ if annotation_file:
589
+ use_external_gt = True
590
+ print(f"Loading external annotations from {annotation_file}...")
591
+ try:
592
+ # Your logic for loading the .json file's 'data' list
593
+ if annotation_file.endswith('.jsonl'):
594
+ annotations = [json.loads(line) for line in open(os.path.expanduser(annotation_file), 'r')]
595
+ else:
596
+ annotations = json.load(open(os.path.expanduser(annotation_file)))['data']
597
+ gt_answers_by_line = {i: ann['answers'] for i, ann in enumerate(annotations)}
598
+ if not gt_answers_by_line:
599
+ print(f"Warning: No annotations loaded from {annotation_file}.")
600
+ except Exception as e:
601
+ print(f"Error loading annotation file: {e}")
602
+ return
603
+ else:
604
+ print("No annotation file provided. Using 'ground_truth' from pickle file.")
605
+
606
+ # --- 2. Load Prediction Data (from your pickle file) ---
607
+ print(f"Loading KL data from {kl_div_pickle_file}...")
608
+ try:
609
+ with open(kl_div_pickle_file, 'rb') as f:
610
+ KL_div_dict = pickle.load(f)
611
+ except FileNotFoundError:
612
+ print(f"Error: Pickle file not found at {kl_div_pickle_file}")
613
+ return
614
+ except Exception as e:
615
+ print(f"Error loading pickle file: {e}")
616
+ return
617
+
618
+ # --- 3. Match Data and Calculate Single-Sample Accuracy ---
619
+ print("Matching predictions to ground truth and calculating scores...")
620
+ evaluator = TextVQAAccuracyEvaluator() # Use the same evaluator for both paths
621
+ data_for_df = []
622
+
623
+ for line_number, sample in tqdm(KL_div_dict.items(), desc="Evaluating samples"):
624
+
625
+ gt_answers = None
626
+
627
+ if use_external_gt:
628
+ # Path 1: Get GT from the external file
629
+ if line_number not in gt_answers_by_line:
630
+ print(f"Warning: Line {line_number} from pickle not found in annotations. Skipping.")
631
+ continue
632
+ gt_answers = gt_answers_by_line[line_number]
633
+ else:
634
+ # Path 2: Get GT from the pickle file itself
635
+ gt = sample.get('ground_truth') if 'ground_truth' in sample else sample['text']
636
+ if gt is None:
637
+ # print(f"Warning: Line {line_number} in pickle has no 'ground_truth' key. Skipping.")
638
+ continue
639
+
640
+ # Ensure gt_answers is a list, as the evaluator expects
641
+ gt_answers = [gt.lower()] if isinstance(gt, str) else gt
642
+
643
+ if gt_answers is None:
644
+ continue
645
+
646
+ # We can now use the same evaluation logic for both paths
647
+ if annotation_file is not None:
648
+ base_acc = evaluator.eval_pred_list([
649
+ {"pred_answer": sample['base_answer'], "gt_answers": gt_answers}
650
+ ])
651
+
652
+ two_stage_acc = evaluator.eval_pred_list([
653
+ {"pred_answer": sample['two_stage_answer'], "gt_answers": gt_answers}
654
+ ])
655
+ else:
656
+ base_acc = sample['base_answer'].lower() in gt_answers
657
+ two_stage_acc = sample['two_stage_answer'].lower() in gt_answers
658
+ data_for_df.append({
659
+ 'kl': sample['KL_divergence'],
660
+ 'base_acc': base_acc,
661
+ 'two_stage_acc': two_stage_acc
662
+ })
663
+
664
+ if not data_for_df:
665
+ print("No matching data found to evaluate. Exiting.")
666
+ return
667
+
668
+ df = pd.DataFrame(data_for_df)
669
+
670
+ # --- 4. Print Overall Accuracy (Unchanged) ---
671
+ overall_base_acc = df['base_acc'].mean() * 100.0
672
+ overall_two_stage_acc = df['two_stage_acc'].mean() * 100.0
673
+
674
+ print("\n" + "=" * 30)
675
+ print("📊 Overall Accuracy Results:")
676
+ print(f" Base Answer Accuracy: {overall_base_acc:.2f}%")
677
+ print(f" Two-Stage Answer Accuracy: {overall_two_stage_acc:.2f}%")
678
+ print("=" * 30 + "\n")
679
+
680
+ # --- 5. Bin Data and Prepare for Plotting (Unchanged) ---
681
+ print("Binning data and preparing plot...")
682
+
683
+ if trim_threshold is not None:
684
+ kl_for_binning = df['kl'].clip(upper=trim_threshold)
685
+ plot_title = f"Accuracy vs. KL Divergence (Values > {trim_threshold} clipped)"
686
+ else:
687
+ kl_for_binning = df['kl']
688
+ plot_title = "Accuracy vs. KL Divergence"
689
+
690
+ df['bin'] = pd.cut(kl_for_binning, bins=bins, include_lowest=True, right=True)
691
+
692
+ binned_acc = df.groupby('bin', observed=False)[['base_acc', 'two_stage_acc']].mean()
693
+ binned_acc = binned_acc.fillna(0)
694
+
695
+ bin_centers = (binned_acc.index.categories.left + binned_acc.index.categories.right) / 2
696
+ bin_widths = (binned_acc.index.categories.right - binned_acc.index.categories.left)
697
+ bar_width = bin_widths * 0.4
698
+
699
+ # --- 6. Plot the Grouped Bar Chart (Unchanged, except save path) ---
700
+ plt.figure(figsize=(15, 7))
701
+
702
+ plt.bar(bin_centers - bar_width / 2,
703
+ binned_acc['base_acc'],
704
+ width=bar_width,
705
+ label='Base Accuracy',
706
+ color='skyblue',
707
+ edgecolor='k')
708
+
709
+ plt.bar(bin_centers + bar_width / 2,
710
+ binned_acc['two_stage_acc'],
711
+ width=bar_width,
712
+ label='Two-Stage Accuracy',
713
+ color='salmon',
714
+ edgecolor='k')
715
+
716
+ plt.xlabel("KL Divergence Bin")
717
+ plt.ylabel("Mean Accuracy")
718
+ plt.title(plot_title)
719
+ plt.legend()
720
+ plt.grid(True, linestyle='--', alpha=0.6, axis='y')
721
+ plt.xticks(bin_centers, [f"{b.left:.2f}" for b in binned_acc.index.categories], rotation=45, ha='right')
722
+ plt.ylim(0, 1.0)
723
+ plt.tight_layout()
724
+
725
+ # try:
726
+ # plt.savefig(plot_save_path)
727
+ # print(f"✅ Accuracy plot saved successfully to {plot_save_path}")
728
+ # except Exception as e:
729
+ # print(f"Error saving plot: {e}")
730
+
731
+ plt.show()
732
+ plt.close()
733
+
734
+ if __name__ == "__main__":
735
+ parser = argparse.ArgumentParser(description="Parallel KL Divergence Evaluation")
736
+ parser.add_argument('--model-path', type=str, default=None,
737
+ help="Path to the model and processor")
738
+ parser.add_argument('--question-file', type=str, default=None,
739
+ help="Path to the JSONL question file (E.g., TextVQA)")
740
+ parser.add_argument('--image-folder', type=str, default=None,
741
+ help="Path to the image directory (for JSONL file)")
742
+ parser.add_argument('--dst-save-path', type=str, required=True,
743
+ help="Path to save the output .pkl file")
744
+ parser.add_argument('--gpu-ids', type=str, default='0,1',
745
+ help="Comma-separated list of GPU IDs to use (e.g., '0,1,2,3')")
746
+ # --- ADDED: New HF Dataset args ---
747
+ parser.add_argument('--hf_dataset', type=str, default=None,
748
+ help="Name of the Hugging Face dataset (e.g., 'craigwu/vstar_bench')")
749
+ parser.add_argument('--dataset_split', type=str, default='test',
750
+ help="Split to use from the Hugging Face dataset (e.g., 'test')")
751
+ ### --- END ADDITION ---
752
+ args = parser.parse_args()
753
+
754
+ # # 1. Prepare Data and Split into Chunks
755
+ # gpu_ids = [int(gid) for gid in args.gpu_ids.split(',')]
756
+ # num_gpus = len(gpu_ids)
757
+ # questions = []
758
+ #
759
+ # if args.hf_dataset:
760
+ # print(f"Loading Hugging Face dataset {args.hf_dataset}, split {args.dataset_split}")
761
+ # # Import only if needed
762
+ # from datasets import load_dataset
763
+ # from datasets import Image as HFImage
764
+ #
765
+ # dataset = load_dataset(args.hf_dataset, split=args.dataset_split)
766
+ # print("Casting 'image' column to load from cache...")
767
+ # questions_list = list(dataset)[:3000]
768
+ #
769
+ # new_questions = []
770
+ # for i, tmp_ques in enumerate(questions_list):
771
+ # tmp_ques['original_line_number'] = i
772
+ # new_questions.append(tmp_ques)
773
+ # questions = new_questions
774
+ # print(f"Loaded {len(questions)} preprocessed questions from Hugging Face.")
775
+ #
776
+ # elif args.question_file:
777
+ # print("Loading and preprocessing questions from JSONL file...")
778
+ # if not args.image_folder:
779
+ # raise ValueError("--image-folder is required when using --question-file")
780
+ #
781
+ # questions_list = [json.loads(q) for q in open(os.path.expanduser(args.question_file), "r")][:3000]
782
+ #
783
+ # new_questions = []
784
+ # for i, tmp_ques in enumerate(questions_list):
785
+ # qs = tmp_ques['text']
786
+ # if 'Reference OCR' in qs:
787
+ # qs = '\n'.join([ele for ele in qs.split('\n') if 'Reference OCR' not in ele])
788
+ # tmp_ques['text'] = qs
789
+ # tmp_ques['original_line_number'] = i # Add the line number as a new field
790
+ # new_questions.append(tmp_ques)
791
+ # questions = new_questions
792
+ # print(f"Loaded {len(questions)} preprocessed questions from local file.")
793
+ #
794
+ # else:
795
+ # raise ValueError("You must provide either --question-file or --hf_dataset")
796
+ # chunk_size = math.ceil(len(questions) / num_gpus)
797
+ # chunks = [questions[i:i + chunk_size] for i in range(0, len(questions), chunk_size)]
798
+ # # 2. Set up arguments for each worker process
799
+ # worker_args = []
800
+ # for i in range(num_gpus):
801
+ # chunk_data = chunks[i] if i < len(chunks) else []
802
+ # worker_args.append((chunk_data, gpu_ids[i], args))
803
+ #
804
+ # # 3. Run processes in parallel
805
+ # print(f"Starting parallel processing on {num_gpus} GPUs: {gpu_ids}")
806
+ # # Use 'spawn' start method for CUDA compatibility
807
+ # ctx = mp.get_context('spawn')
808
+ # with ctx.Pool(processes=num_gpus) as pool:
809
+ # results_from_pool = pool.starmap(worker_process_kl_divergence, worker_args)
810
+ #
811
+ # # 4. Gather and Save Results
812
+ # print("All chunks processed. Gathering results...")
813
+ # final_KL_div_dict = {}
814
+ # for sub_dict in results_from_pool:
815
+ # final_KL_div_dict.update(sub_dict)
816
+ #
817
+ # with open(os.path.expanduser(args.dst_save_path), 'wb') as f:
818
+ # pickle.dump(final_KL_div_dict, f)
819
+ # print(f"✅ Successfully processed and saved {len(final_KL_div_dict)} KL divergence scores to {args.dst_save_path}.")
820
+
821
+ # #output_text, input_height, input_width = inference()
822
+ # use_act = False
823
+ # device = 'cuda'
824
+ # # # model_path = 'Qwen/Qwen2.5-VL-3B-Instruct'
825
+ # # # model_path = 'Qwen/Qwen2.5-VL-7B-Instruct'
826
+ # model_path = '/home/yuheng/code/Qwen2.5-VL/output/qwen2_5vl-7b-roi-K16T3-152k-v1bf16Mheads-twiginit-filled'
827
+ # model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
828
+ # model_path,
829
+ # torch_dtype=torch.bfloat16,
830
+ # attn_implementation="eager",
831
+ # ).eval().to(device)
832
+ # processor = AutoProcessor.from_pretrained('Qwen/Qwen2.5-VL-7B-Instruct', trust_remote_code=True, padding_side='left', use_fast=True)
833
+
834
+ # # # Run the case study on questions with KL divergence > 1.0
835
+ # case_study_KL_divergence(
836
+ # model,
837
+ # processor,
838
+ # QUESTION_FILE,
839
+ # IMAGE_FOLDER,
840
+ # KL_PICKLE_PATH,
841
+ # threshold=1.0, # <-- Set your desired threshold here
842
+ # max_cases=100 # <-- Set how many examples you want to see
843
+ # )
844
+
845
+
846
+ #KL_PICKLE_PATH = '../excluded/roi_KLdiv_textvqa_576res.pkl'
847
+ # QUESTION_FILE = '.../../llava/playground/data/eval/textvqa/llava_textvqa_val_v051_ocr.jsonl'
848
+ # IMAGE_FOLDER = '../../llava/playground/data/eval/textvqa/train_images'
849
+ ANNO_FILE = "/home/yuheng/datasets/textvqa/converted_llava_style_train.jsonl" #'../../llava/playground/data/eval/textvqa/TextVQA_0.5.1_val.json'
850
+
851
+ KL_PICKLE_PATH = args.dst_save_path
852
+ ANNO_FILE = None if args.hf_dataset else ANNO_FILE
853
+
854
+ plot_kl_divergence_distribution(
855
+ kl_div_pickle_file=KL_PICKLE_PATH,
856
+ save_path=None,
857
+ trim_threshold=5.0,
858
+ bins=50,
859
+ title="Distribution of KL Divergence (RoI On vs Off) on Puseudo Train Set"
860
+ )
861
+
862
+ ###check accuracy vs KL divergence bins
863
+ evaluate_accuracy_with_kl_bins(
864
+ kl_div_pickle_file=KL_PICKLE_PATH,
865
+ annotation_file=None,
866
+ plot_save_path=None,
867
+ trim_threshold=5.0,
868
+ bins=50
869
+ )
870
+ pass
871
+
872
+ '''
873
+ CUDA_VISIBLE_DEVICES=1,3 python analysis/gated_roi_analysis.py \
874
+ --model-path /home/yuheng/code/Qwen2.5-VL/output/qwen2_5vl-7b-roi-K16T3-152k-v1bf16Mheads-twiginit-filled \
875
+ --question-file ../llava/playground/data/eval/textvqa/llava_textvqa_val_v051_ocr.jsonl \
876
+ --image-folder ../llava/playground/data/eval/textvqa/train_images \
877
+ --dst-save-path ./excluded/roi_KLdiv_textvqa_576res.pkl \
878
+ --gpu-ids 0,1
879
+
880
+
881
+ CUDA_VISIBLE_DEVICES=0,1,2,3 python analysis/gated_roi_analysis.py \
882
+ --model-path /home/yuheng/code/Qwen2.5-VL/output/qwen2_5vl-7b-roi-K16T3-152k-v1bf16Mheads-twiginit-filled \
883
+ --hf_dataset "lmms-lab/vstar-bench" --dataset_split "test" \
884
+ --dst-save-path ./excluded/vstar_analysis.pkl --gpu-ids 0,1,2,3
885
+
886
+ CUDA_VISIBLE_DEVICES=0,1,2,3 python analysis/gated_roi_analysis.py \
887
+ --model-path /home/yuheng/code/Qwen2.5-VL/output/qwen2_5vl-7b-roi-K16T3-152k-v1bf16Mheads-twiginit-filled \
888
+ --hf_dataset "lmms-lab/POPE" --dataset_split "test" \
889
+ --dst-save-path ./excluded/pope_analysis.pkl --gpu-ids 0,1,2,3
890
+
891
+
892
+ CUDA_VISIBLE_DEVICES=1,2,3 python analysis/gated_roi_analysis.py \
893
+ --model-path /home/yuheng/code/Qwen2.5-VL/output/qwen2_5vl-7b-roi-K16T3-152k-v1bf16Mheads-twiginit-filled \
894
+ --question-file /home/yuheng/datasets/textvqa/converted_llava_style_train.jsonl \
895
+ --image-folder ../llava/playground/data/eval/textvqa/train_images \
896
+ --dst-save-path ./excluded/roi_KLdiv_textvqa_train_576res.pkl \
897
+ --gpu-ids 0,1,2
898
+
899
+ '''
QWENVL-PRIVATE/analysis/gen_roi_map_30b.py ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Generate ROI attention maps for Qwen3-VL-30B-A3B.
3
+ Produces 10 GQA samples with grounding attention visualization.
4
+
5
+ Usage:
6
+ CUDA_VISIBLE_DEVICES=0 python3 analysis/gen_roi_map_30b.py \
7
+ --model_path /mnt/bn/leonworkspace/terry/model/Qwen3-VL-30B-A3B-Instruct \
8
+ --num_samples 10
9
+
10
+ Output: stage0/<model_name>/ with ROI map PNGs
11
+ """
12
+
13
+ import os
14
+ import sys
15
+ import json
16
+ import math
17
+ import torch
18
+ import numpy as np
19
+ import matplotlib.pyplot as plt
20
+ from PIL import Image
21
+ from tqdm import tqdm
22
+ from transformers import AutoProcessor
23
+ import argparse
24
+
25
+ # Add project root to path
26
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
27
+ from qwen_src.qwen3_vl import Qwen3VLForConditionalGeneration
28
+ from qwen_src.mm_utils import expand2square
29
+ from qwen_src.ana_utils import register_hooks, get_attn_weights_map
30
+
31
+ PATCH_SIZE = 32
32
+
33
+
34
+ def plot_roi_map(image_path, grounding_attn, output_shape, question, answer, save_path):
35
+ """Plot image + ROI grounding attention map side by side."""
36
+ fig, axes = plt.subplots(1, 2, figsize=(10, 4), dpi=200)
37
+
38
+ # Original image
39
+ if os.path.exists(image_path):
40
+ img = Image.open(image_path)
41
+ axes[0].imshow(img)
42
+ axes[0].set_title('Image', fontsize=10)
43
+ axes[0].axis('off')
44
+
45
+ # Grounding attention map
46
+ attn_2d = grounding_attn.reshape(output_shape)
47
+ im = axes[1].imshow(attn_2d, cmap='inferno', aspect='auto')
48
+ axes[1].set_title('ROI Grounding Attention', fontsize=10)
49
+ axes[1].axis('off')
50
+ plt.colorbar(im, ax=axes[1], shrink=0.6)
51
+
52
+ fig.suptitle(f'Q: {question[:80]}...\nA: {answer}', fontsize=8, y=0.02)
53
+ plt.tight_layout()
54
+ plt.savefig(save_path, dpi=200, bbox_inches='tight')
55
+ plt.close()
56
+
57
+
58
+ def main():
59
+ parser = argparse.ArgumentParser()
60
+ parser.add_argument("--model_path", type=str, required=True)
61
+ parser.add_argument("--num_samples", type=int, default=10)
62
+ parser.add_argument("--questions_path", type=str,
63
+ default="/mnt/bn/leonworkspace/terry/data/llava_v1_5_mix665k_selected_qa.jsonl")
64
+ parser.add_argument("--images_root", type=str,
65
+ default="/mnt/bn/leonworkspace/terry/data")
66
+ args = parser.parse_args()
67
+
68
+ device = "cuda"
69
+ model_name = os.path.basename(args.model_path)
70
+ save_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "stage0", model_name)
71
+ os.makedirs(save_dir, exist_ok=True)
72
+ print(f"Output dir: {save_dir}")
73
+
74
+ # Load model
75
+ print(f"Loading {model_name}...")
76
+ model = Qwen3VLForConditionalGeneration.from_pretrained(
77
+ args.model_path,
78
+ torch_dtype=torch.bfloat16,
79
+ attn_implementation="eager", # need eager for attention weights
80
+ device_map="auto", # auto device_map for 30B
81
+ ).eval()
82
+ print(f"Model loaded. Device map: {model.hf_device_map if hasattr(model, 'hf_device_map') else 'single GPU'}")
83
+
84
+ processor = AutoProcessor.from_pretrained(
85
+ args.model_path,
86
+ trust_remote_code=True,
87
+ padding_side="left",
88
+ use_fast=True,
89
+ )
90
+
91
+ # Register hooks for attention extraction
92
+ activations, hooks = register_hooks(model, is_qwen3vl=True)
93
+
94
+ # Load GQA questions
95
+ print(f"Loading questions from {args.questions_path}...")
96
+ questions = [json.loads(l) for l in open(args.questions_path)]
97
+ gqa_questions = [q for q in questions if 'gqa' in q.get('image', '')]
98
+ print(f"Total: {len(questions)}, GQA: {len(gqa_questions)}")
99
+
100
+ # Model config
101
+ layers = model.config.text_config.num_hidden_layers
102
+ print(f"Model layers: {layers}")
103
+
104
+ # Process samples
105
+ for idx, line in enumerate(tqdm(gqa_questions[:args.num_samples], desc="Generating ROI maps")):
106
+ activations.clear()
107
+
108
+ image_file = line["image"]
109
+ question = line["text"] if "text" in line else line["prompt"]
110
+ image_path = os.path.join(args.images_root, image_file)
111
+
112
+ if not os.path.exists(image_path):
113
+ print(f" Skip: {image_path} not found")
114
+ continue
115
+
116
+ # Load and prepare image
117
+ image = Image.open(image_path).convert('RGB')
118
+ processor.image_processor.max_pixels = 576 * PATCH_SIZE * PATCH_SIZE
119
+ processor.image_processor.min_pixels = 576 * PATCH_SIZE * PATCH_SIZE
120
+ image_size = image.size
121
+ image = expand2square(image, (127, 127, 127))
122
+
123
+ messages = [{
124
+ "role": "user",
125
+ "content": [
126
+ {"type": "text", "text": question},
127
+ {"type": "image", "image": image},
128
+ ]
129
+ }]
130
+
131
+ inputs = processor.apply_chat_template(
132
+ messages, tokenize=True, add_generation_prompt=True,
133
+ return_dict=True, return_tensors="pt"
134
+ )
135
+ inputs = inputs.to(device)
136
+
137
+ # Generate with attention output
138
+ with torch.no_grad():
139
+ generated_ids = model.generate(**inputs, max_new_tokens=128, output_attentions=True)
140
+
141
+ generated_ids_trimmed = [
142
+ out_ids[len(in_ids):] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
143
+ ]
144
+ answer = processor.batch_decode(
145
+ generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
146
+ )[0]
147
+
148
+ # Build visual mask if not captured by hooks
149
+ if activations.get('visual_mask', None) is None:
150
+ vision_start_id = processor.tokenizer.convert_tokens_to_ids('<|vision_start|>')
151
+ vision_end_id = processor.tokenizer.convert_tokens_to_ids('<|vision_end|>')
152
+ input_ids_list = inputs['input_ids'].tolist()[0]
153
+ pos = input_ids_list.index(vision_start_id) + 1
154
+ pos_end = input_ids_list.index(vision_end_id)
155
+ activations['visual_mask'] = torch.zeros(
156
+ (1, inputs['input_ids'].shape[1]), dtype=torch.bool, device=device)
157
+ activations['visual_mask'][0, pos:pos_end] = True
158
+
159
+ output_shape = inputs['image_grid_thw'].cpu().numpy().squeeze(0)[1:] // 2
160
+ output_shape = tuple(output_shape.astype(int))
161
+
162
+ # Extract attention maps
163
+ attn_weights_map = get_attn_weights_map(activations, layers=layers, reduction='mean')
164
+
165
+ # Average grounding attention across middle-to-late layers
166
+ # For 30B (48 layers), use layers around 60-80% depth
167
+ grounding_layers = list(range(int(layers * 0.5), int(layers * 0.8)))
168
+ grounding_attn = np.zeros(output_shape)
169
+ count = 0
170
+ for layer_idx in grounding_layers:
171
+ key = f'layer{layer_idx}'
172
+ if key in attn_weights_map and 'output2images' in attn_weights_map[key]:
173
+ attn = attn_weights_map[key]['output2images']
174
+ if attn.size == np.prod(output_shape):
175
+ grounding_attn += attn.reshape(output_shape)
176
+ count += 1
177
+ else:
178
+ # Multi-head: average across heads
179
+ n_heads = attn.shape[0] // np.prod(output_shape)
180
+ if n_heads > 0:
181
+ attn_reshaped = attn.reshape(n_heads, *output_shape)
182
+ grounding_attn += attn_reshaped.mean(axis=0)
183
+ count += 1
184
+ if count > 0:
185
+ grounding_attn /= count
186
+
187
+ # Save visualization
188
+ save_path = os.path.join(save_dir, f"{idx}_roi_map.png")
189
+ plot_roi_map(image_path, grounding_attn, output_shape, question, answer, save_path)
190
+ print(f" [{idx}] Q: {question[:60]}... A: {answer[:30]}...")
191
+
192
+ # Cleanup hooks
193
+ for h in hooks:
194
+ h.remove()
195
+
196
+ print(f"\nDone! {args.num_samples} ROI maps saved to {save_dir}/")
197
+
198
+
199
+ if __name__ == "__main__":
200
+ main()
QWENVL-PRIVATE/analysis/get_o2i_attn.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from qwen_src.qwen2_5_vl import Qwen2_5_VLForConditionalGeneration
2
+ import torch
3
+ import matplotlib.pyplot as plt
4
+ from qwen_vl_utils import process_vision_info
5
+ from transformers import AutoProcessor
6
+ import math
7
+
8
+ def calculate_plt_size(attention_layer_num):
9
+ num_layers = attention_layer_num
10
+ cols = math.ceil(math.sqrt(num_layers))
11
+ rows = math.ceil(num_layers / cols)
12
+ return rows, cols
13
+
14
+ device = 'cuda'
15
+ model_path = 'Qwen/Qwen2.5-VL-3B-Instruct'
16
+
17
+ model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
18
+ model_path,
19
+ torch_dtype=torch.bfloat16,
20
+ attn_implementation="eager",
21
+ ).eval().to(device)
22
+ processor = AutoProcessor.from_pretrained(model_path, trust_remote_code=True, padding_side='left', use_fast=True)
23
+
24
+ image_path = './images/demo1.png'
25
+ question = 'what is the date of the photo?'
26
+
27
+ messages_query = [
28
+ {
29
+ "role": "user",
30
+ "content": [
31
+ {"type": "image", "image": image_path, "max_pixels": 512*28*28},
32
+ {"type": "text", "text": f"{question} Answer the question using a single word or phrase."},
33
+ ],
34
+ }
35
+ ]
36
+
37
+ image_inputs, _ = process_vision_info(messages_query)
38
+
39
+ text_query = processor.apply_chat_template(
40
+ messages_query,
41
+ tokenize=False,
42
+ add_generation_prompt=True
43
+ )
44
+
45
+ inputs = processor(
46
+ text=[text_query],
47
+ images=image_inputs,
48
+ padding=True,
49
+ return_tensors="pt",
50
+ ).to(device)
51
+
52
+ messages_general = [
53
+ {
54
+ "role": "user",
55
+ "content": [
56
+ {"type": "image","image": image_path,"max_pixels": 512*28*28},
57
+ {"type": "text", "text": "Write a general description of the image. Answer the question using a single word or phrase."},
58
+ ],
59
+ }
60
+ ]
61
+
62
+ text_general = processor.apply_chat_template(
63
+ messages_general,
64
+ tokenize=False,
65
+ add_generation_prompt=True
66
+ )
67
+
68
+ general_inputs = processor(
69
+ text=[text_general],
70
+ images=image_inputs,
71
+ padding=True,
72
+ return_tensors="pt",
73
+ ).to(device)
74
+
75
+ image_inputs_aux = processor.image_processor(images=image_inputs)
76
+ output_shape = image_inputs_aux["image_grid_thw"].numpy().squeeze(0)[1:]/2
77
+ output_shape = output_shape.astype(int)
78
+
79
+ with torch.no_grad():
80
+ vision_start_token_id = processor.tokenizer.convert_tokens_to_ids('<|vision_start|>')
81
+ vision_end_token_id = processor.tokenizer.convert_tokens_to_ids('<|vision_end|>')
82
+ pos = inputs['input_ids'].tolist()[0].index(vision_start_token_id) + 1
83
+ pos_end = inputs['input_ids'].tolist()[0].index(vision_end_token_id)
84
+
85
+ output = model(**inputs, output_attentions=True)
86
+ general_output = model(**general_inputs, output_attentions=True)
87
+ rows, cols = calculate_plt_size(len(output.attentions))
88
+ fig, axes = plt.subplots(rows, cols, figsize=(10.8, 16))
89
+ for i, ax in enumerate(axes.flatten()):
90
+ if i < len(output.attentions):
91
+ att = output.attentions[i][0, :, -1, pos:pos_end].mean(dim=0)
92
+ att = att.to(torch.float32).detach().cpu().numpy()
93
+
94
+ general_att = general_output.attentions[i][0, :, -1, pos:pos_end].mean(dim=0)
95
+ general_att = general_att.to(torch.float32).detach().cpu().numpy()
96
+
97
+ att = att / general_att
98
+
99
+ ax.imshow(att.reshape(output_shape), cmap="viridis", interpolation="nearest")
100
+ ax.set_title(f"Layer {i+1}")
101
+ ax.axis("off")
102
+ else:
103
+ ax.axis("off")
104
+
105
+ plt.tight_layout()
106
+ plt.show()
107
+ pass
QWENVL-PRIVATE/analysis/llm-as-judge-roi-samples.py ADDED
@@ -0,0 +1,383 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import pickle
4
+ import argparse
5
+ import math
6
+ import torch
7
+ import multiprocessing as mp
8
+ import re
9
+ from torch.utils.data import Dataset, DataLoader
10
+ # CHANGED: Import standard LLM classes instead of VL classes
11
+ from transformers import AutoTokenizer, AutoModelForCausalLM
12
+ from tqdm import tqdm
13
+ import glob
14
+
15
+
16
+ # --- 1. Dataset & Collator ---
17
+
18
+ class JudgeDataset(Dataset):
19
+ def __init__(self, items):
20
+ self.items = items
21
+
22
+ def __len__(self):
23
+ return len(self.items)
24
+
25
+ def __getitem__(self, idx):
26
+ item = self.items[idx]
27
+
28
+ # 1. Format Ground Truth
29
+ gt = item['ground_truth']
30
+ if isinstance(gt, list):
31
+ gt_str = ", ".join([str(x) for x in gt])
32
+ else:
33
+ gt_str = str(gt)
34
+
35
+ # 2. Judge Prompt
36
+ prompt_text = (
37
+ f"You are an intelligent evaluator for a Visual Question Answering task.\n"
38
+ f"Compare the Model Predictions against the Ground Truth.\n\n"
39
+
40
+ f"--- DATA ---\n"
41
+ f"Question: {item['question']}\n"
42
+ f"Ground Truth: {gt_str}\n"
43
+ f"Model A Prediction: {item['base_resp']}\n"
44
+ f"Model B Prediction: {item['roi_resp']}\n\n"
45
+
46
+ f"--- CRITERIA ---\n"
47
+ f"1. **Containment**: If the Ground Truth is a short value (e.g., '6000') and the Model Prediction contains it correctly within a sentence (e.g., 'The value is 6000'), mark it as **Yes**.\n"
48
+ f"2. **Synonyms**: Accept standard synonyms (e.g., 'Yes' = 'True', '10%' = '0.1').\n"
49
+ f"3. **Formatting**: Ignore punctuation or capitalization differences (e.g., 'april' = 'April').\n"
50
+ f"4. **Contradiction**: Only mark 'No' if the prediction is factually wrong or missing the key information.\n\n"
51
+
52
+ f"--- TASK ---\n"
53
+ f"Does Model A provide the correct answer? (Yes/No)\n"
54
+ f"Does Model B provide the correct answer? (Yes/No)\n\n"
55
+
56
+ f"Respond in this exact format:\n"
57
+ f"A: [Yes/No]\n"
58
+ f"B: [Yes/No]"
59
+ )
60
+
61
+ messages = [
62
+ {"role": "system",
63
+ "content": "You are a fair and robust evaluator. You focus on semantic correctness over strict string matching."},
64
+ {"role": "user", "content": prompt_text}
65
+ ]
66
+
67
+ return {
68
+ "messages": messages,
69
+ "original_item": item
70
+ }
71
+
72
+
73
+ class JudgeCollator:
74
+ def __init__(self, model_path):
75
+ self.model_path = model_path
76
+ self.tokenizer = None
77
+
78
+ def __call__(self, batch):
79
+ # CHANGED: Initialize AutoTokenizer for pure LLM
80
+ if self.tokenizer is None:
81
+ self.tokenizer = AutoTokenizer.from_pretrained(
82
+ self.model_path,
83
+ trust_remote_code=True,
84
+ padding_side='left',
85
+ use_fast=True
86
+ )
87
+ # Ensure pad token exists for batched inference
88
+ if self.tokenizer.pad_token is None:
89
+ self.tokenizer.pad_token = self.tokenizer.eos_token
90
+
91
+ raw_messages = [item['messages'] for item in batch]
92
+ original_items = [item['original_item'] for item in batch]
93
+
94
+ # CHANGED: Apply chat template for text only
95
+ text_inputs = [
96
+ self.tokenizer.apply_chat_template(msg, tokenize=False, add_generation_prompt=True)
97
+ for msg in raw_messages
98
+ ]
99
+
100
+ inputs = self.tokenizer(
101
+ text_inputs,
102
+ padding=True,
103
+ return_tensors="pt"
104
+ )
105
+
106
+ return {
107
+ "inputs": inputs,
108
+ "items": original_items
109
+ }
110
+
111
+
112
+ # --- 2. Worker Functions ---
113
+
114
+ def worker_process(chunk, gpu_id, args):
115
+ device = f'cuda:{gpu_id}'
116
+
117
+ # Storage for results
118
+ regressions_found = [] # Base: Right, ROI: Wrong
119
+ improvements_found = [] # Base: Wrong, ROI: Right
120
+
121
+ # Setup Dataset & Loader
122
+ dataset = JudgeDataset(chunk)
123
+ collator = JudgeCollator(args.model_path)
124
+
125
+ dataloader = DataLoader(
126
+ dataset,
127
+ # NOTE: 32B model requires more VRAM. You might need to lower batch_size manually if OOM occurs.
128
+ batch_size=args.batch_size,
129
+ shuffle=False,
130
+ num_workers=args.num_workers,
131
+ collate_fn=collator,
132
+ pin_memory=True
133
+ )
134
+
135
+ if len(dataset) == 0:
136
+ return {"regressions": [], "improvements": []}
137
+
138
+ try:
139
+ # Load Model
140
+ print(f"Worker {gpu_id}: Loading Judge Model (LLM)...")
141
+ # CHANGED: Use AutoModelForCausalLM
142
+ model = AutoModelForCausalLM.from_pretrained(
143
+ args.model_path,
144
+ torch_dtype=torch.bfloat16,
145
+ trust_remote_code=True,
146
+ device_map=device
147
+ )
148
+ model.eval()
149
+
150
+ # Access tokenizer from collator
151
+ tokenizer = collator.tokenizer if collator.tokenizer else None
152
+ if tokenizer is None:
153
+ tokenizer = AutoTokenizer.from_pretrained(args.model_path, trust_remote_code=True)
154
+
155
+ print(f"Worker {gpu_id}: Starting Judging...")
156
+
157
+ for batch_data in tqdm(dataloader, desc=f"GPU {gpu_id}", position=gpu_id):
158
+ inputs = batch_data['inputs']
159
+ items = batch_data['items']
160
+
161
+ # Move inputs to device (AutoTokenizer outputs input_ids and attention_mask)
162
+ inputs = {k: v.to(device) for k, v in inputs.items()}
163
+
164
+ with torch.no_grad():
165
+ output_ids = model.generate(
166
+ **inputs,
167
+ max_new_tokens=32,
168
+ do_sample=False,
169
+ pad_token_id=tokenizer.eos_token_id
170
+ )
171
+
172
+ # CHANGED: Slice outputs correctly for LLM (remove prompt tokens)
173
+ input_len = inputs['input_ids'].shape[1]
174
+ generated_ids = output_ids[:, input_len:]
175
+ responses = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)
176
+
177
+ for item, resp in zip(items, responses):
178
+ clean_resp = resp.replace("\n", " ").strip()
179
+
180
+ # Parse A (Baseline)
181
+ base_match = re.search(r"A\s*[:\-\.]\s*(Yes|True|Correct)", clean_resp, re.IGNORECASE)
182
+ base_correct = bool(base_match)
183
+
184
+ # Parse B (ROI)
185
+ roi_match = re.search(r"B\s*[:\-\.]\s*(Yes|True|Correct)", clean_resp, re.IGNORECASE)
186
+ roi_correct = bool(roi_match)
187
+
188
+ # Helper to create result dict
189
+ def create_result_entry():
190
+ s_bbox = item.get('surrounding_bbox')
191
+ if hasattr(s_bbox, 'tolist'): s_bbox = s_bbox.tolist()
192
+
193
+ r_mask = item.get('roi_mask')
194
+ mask_shape = r_mask.shape if hasattr(r_mask, 'shape') else "N/A"
195
+
196
+ return {
197
+ "dataset": item.get('dataset', 'unknown'),
198
+ "line_number": item['line_number'],
199
+ "question_id": item['id'],
200
+ "image_id": item['image'],
201
+ "question": item['question'],
202
+ "ground_truth": item['ground_truth'],
203
+ "baseline_response": item['base_resp'],
204
+ "roi_response": item['roi_resp'],
205
+ "judge_raw_output": resp,
206
+ "surrounding_bbox": s_bbox,
207
+ "roi_mask_shape": mask_shape
208
+ }
209
+
210
+ # CASE 1: REGRESSION (Base Right, ROI Wrong)
211
+ if base_correct and not roi_correct:
212
+ regressions_found.append(create_result_entry())
213
+
214
+ # CASE 2: IMPROVEMENT (Base Wrong, ROI Right)
215
+ elif not base_correct and roi_correct:
216
+ improvements_found.append(create_result_entry())
217
+
218
+ del inputs, output_ids, generated_ids
219
+
220
+ except Exception as e:
221
+ print(f"Worker {gpu_id}: Critical Error: {e}")
222
+ import traceback
223
+ traceback.print_exc()
224
+
225
+ return {"regressions": regressions_found, "improvements": improvements_found}
226
+
227
+
228
+ def worker_wrapper(chunk, gpu_id, args, return_dict):
229
+ result = worker_process(chunk, gpu_id, args)
230
+ return_dict[gpu_id] = result
231
+
232
+
233
+ # --- 3. Main Execution ---
234
+
235
+ def load_all_paired_data(search_dir):
236
+ all_base_data = {}
237
+ all_roi_data = {}
238
+
239
+ search_pattern = os.path.join(search_dir, "*_qwen2.5vl-7B.pkl")
240
+ base_files = glob.glob(search_pattern)
241
+
242
+ print(f"Found {len(base_files)} baseline files in '{search_dir}'. Loading pairs...")
243
+
244
+ for base_file in base_files:
245
+ filename = os.path.basename(base_file)
246
+ dataset_name = filename.replace("_qwen2.5vl-7B.pkl", "")
247
+ roi_file = base_file.replace(".pkl", "-roi.pkl")
248
+
249
+ if not os.path.exists(roi_file):
250
+ print(f"⚠️ Skipping {dataset_name}: ROI file not found ({roi_file})")
251
+ continue
252
+
253
+ try:
254
+ with open(base_file, 'rb') as f:
255
+ cur_base = pickle.load(f)
256
+ with open(roi_file, 'rb') as f:
257
+ cur_roi = pickle.load(f)
258
+
259
+ for k in cur_base.keys():
260
+ if k in cur_roi:
261
+ unique_key = f"{dataset_name}_{k}"
262
+ all_base_data[unique_key] = cur_base[k]
263
+ all_roi_data[unique_key] = cur_roi[k]
264
+ all_base_data[unique_key]['dataset'] = dataset_name
265
+ all_roi_data[unique_key]['dataset'] = dataset_name
266
+
267
+ print(f" + Loaded {dataset_name}: {len(cur_base)} samples")
268
+
269
+ except Exception as e:
270
+ print(f"❌ Error loading {dataset_name}: {e}")
271
+
272
+ return all_base_data, all_roi_data
273
+
274
+
275
+ def main():
276
+ parser = argparse.ArgumentParser()
277
+ # CHANGED: Default model path now points to a pure LLM path example
278
+ parser.add_argument('--model-path', type=str, default='/root/public/Qwen/Qwen2.5-32B-Instruct')
279
+ parser.add_argument('--data-dir', type=str, default='./', help="Directory containing .pkl files")
280
+ parser.add_argument('--save-path', type=str, default="regressions.json",
281
+ help="File for Regressions (Base Right, ROI Wrong)")
282
+ parser.add_argument('--save-path-improvements', type=str, default="improvements.json",
283
+ help="File for Improvements (Base Wrong, ROI Right)")
284
+ parser.add_argument('--gpu-ids', type=str, default='0,1,2,3')
285
+ parser.add_argument('--batch-size', type=int, default=8)
286
+ parser.add_argument('--num_workers', type=int, default=4)
287
+ args = parser.parse_args()
288
+
289
+ # 1. Load All Data Pairs
290
+ base_data, roi_data = load_all_paired_data(args.data_dir)
291
+
292
+ if not base_data:
293
+ print("No paired data found. Exiting.")
294
+ return
295
+
296
+ # 2. Prepare Items
297
+ judge_items = []
298
+ print(f"Preparing {len(base_data)} items for judging...")
299
+
300
+ for k, base_item in base_data.items():
301
+ roi_item = roi_data[k]
302
+
303
+ # Optimization: Skip identical strings (Result is definitely same, so neither reg nor imp)
304
+ if str(base_item.get('response', '')).strip() == str(roi_item.get('response', '')).strip():
305
+ continue
306
+
307
+ judge_items.append({
308
+ 'unique_key': k,
309
+ 'dataset': base_item.get('dataset', 'unknown'),
310
+ 'line_number': base_item.get('line_number', 'unknown'),
311
+ 'id': base_item['id'],
312
+ 'image': base_item['image'],
313
+ 'question': base_item['prompt'],
314
+ 'ground_truth': base_item['text'],
315
+ 'base_resp': base_item.get('response', ''),
316
+ 'roi_resp': roi_item.get('response', ''),
317
+ 'surrounding_bbox': roi_item.get('surrounding_bbox'),
318
+ 'roi_mask': roi_item.get('roi_mask')
319
+ })
320
+
321
+ print(f"Items requiring judgement (non-identical): {len(judge_items)}")
322
+
323
+ # 3. Parallel Processing
324
+ try:
325
+ mp.set_start_method('spawn', force=True)
326
+ except RuntimeError:
327
+ pass
328
+
329
+ gpu_ids = [int(gid) for gid in args.gpu_ids.split(',')]
330
+ num_gpus = len(gpu_ids)
331
+
332
+ chunk_size = math.ceil(len(judge_items) / num_gpus)
333
+ chunks = [judge_items[i:i + chunk_size] for i in range(0, len(judge_items), chunk_size)]
334
+
335
+ manager = mp.Manager()
336
+ return_dict = manager.dict()
337
+ processes = []
338
+
339
+ print(f"Starting judging on {num_gpus} GPUs...")
340
+ for i in range(num_gpus):
341
+ chunk_data = chunks[i] if i < len(chunks) else []
342
+ p = mp.Process(target=worker_wrapper, args=(chunk_data, gpu_ids[i], args, return_dict))
343
+ p.start()
344
+ processes.append(p)
345
+
346
+ for p in processes:
347
+ p.join()
348
+
349
+ # 4. Aggregation
350
+ all_regressions = []
351
+ all_improvements = []
352
+
353
+ for gpu_id, res in return_dict.items():
354
+ all_regressions.extend(res['regressions'])
355
+ all_improvements.extend(res['improvements'])
356
+
357
+ print(f"\n✅ Analysis Complete.")
358
+ print(f" - Regressions (Worse): {len(all_regressions)}")
359
+ print(f" - Improvements (Better): {len(all_improvements)}")
360
+
361
+ # Save Regressions
362
+ with open(args.save_path, 'w', encoding='utf-8') as f:
363
+ json.dump(all_regressions, f, indent=2)
364
+ print(f"Saved regressions to {args.save_path}")
365
+
366
+ # Save Improvements
367
+ with open(args.save_path_improvements, 'w', encoding='utf-8') as f:
368
+ json.dump(all_improvements, f, indent=2)
369
+ print(f"Saved improvements to {args.save_path_improvements}")
370
+
371
+
372
+ if __name__ == "__main__":
373
+ main()
374
+
375
+ '''
376
+ python analysis/llm-as-judge-roi-samples.py \
377
+ --model-path /root/public/Qwen/Qwen2.5-32B-Instruct \
378
+ --data-dir /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining \
379
+ --save-path /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/regressions-v2-32b-judge.json \
380
+ --save-path-improvements /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/improvements-v2-32b-judge.json \
381
+ --batch-size 8 \
382
+ --gpu-ids 0,1,2,3
383
+ '''
QWENVL-PRIVATE/analysis/making_base_model_response.py ADDED
@@ -0,0 +1,544 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ script_dir = os.path.dirname(os.path.abspath(__file__))
4
+ project_root = os.path.dirname(script_dir)
5
+ sys.path.append(project_root)
6
+
7
+ from qwen_src.qwen2_5_vl import Qwen2_5_VLForConditionalGeneration
8
+ from qwen_src.qwen2_vl import Qwen2VLForConditionalGeneration
9
+ try:
10
+ from qwen_src.qwen3_vl.modeling_qwen3_vl_batch import Qwen3VLForConditionalGeneration
11
+ except Exception:
12
+ Qwen3VLForConditionalGeneration = None
13
+ import torch
14
+ import matplotlib.pyplot as plt
15
+ from qwen_vl_utils import process_vision_info
16
+ from transformers import AutoProcessor
17
+ import math
18
+ from PIL import Image, ImageDraw, ImageFont
19
+ import numpy as np
20
+
21
+
22
+ import json
23
+ import os
24
+ from tqdm import tqdm
25
+
26
+ import pickle
27
+ import argparse
28
+ import multiprocessing as mp
29
+ import pandas as pd # <-- Make sure to import pandas
30
+ from matching_utils import TextVQAAccuracyEvaluator
31
+ from check_gate_label import plot_single_sample
32
+
33
+
34
+ def infer_model_type(model_path, model_type):
35
+ if model_type and model_type != "auto":
36
+ return model_type
37
+ lower = model_path.lower()
38
+ if "qwen3" in lower:
39
+ return "qwen3"
40
+ if "qwen2.5" in lower or "qwen2_5" in lower or "qwen2-5" in lower:
41
+ return "qwen2_5"
42
+ if "qwen2" in lower:
43
+ return "qwen2"
44
+ return "qwen2_5"
45
+
46
+
47
+ def resolve_processor_path(model_path, model_type, processor_path=None):
48
+ if processor_path:
49
+ return processor_path
50
+ lower = model_path.lower()
51
+ public_prefix = "/root/public/Qwen"
52
+
53
+ def to_repo_path(name):
54
+ if "/root/public" in model_path:
55
+ return f"{public_prefix}/{name}"
56
+ return f"Qwen/{name}"
57
+
58
+ if model_type == "qwen3":
59
+ if "8b" in lower:
60
+ return to_repo_path("Qwen3-VL-8B-Instruct")
61
+ if "4b" in lower:
62
+ return to_repo_path("Qwen3-VL-4B-Instruct")
63
+ if model_type == "qwen2_5":
64
+ if "7b" in lower:
65
+ return to_repo_path("Qwen2.5-VL-7B-Instruct")
66
+ if "3b" in lower:
67
+ return to_repo_path("Qwen2.5-VL-3B-Instruct")
68
+ if model_type == "qwen2" and "7b" in lower:
69
+ return to_repo_path("Qwen2-VL-7B-Instruct")
70
+
71
+ return model_path
72
+
73
+
74
+ def resolve_model_class(model_type):
75
+ if model_type == "qwen3":
76
+ if Qwen3VLForConditionalGeneration is None:
77
+ raise RuntimeError("Qwen3VLForConditionalGeneration not available.")
78
+ return Qwen3VLForConditionalGeneration
79
+ if model_type == "qwen2":
80
+ return Qwen2VLForConditionalGeneration
81
+ return Qwen2_5_VLForConditionalGeneration
82
+
83
+
84
+ def set_twig_flags(model, model_type, two_stage_flag):
85
+ if model_type == "qwen3":
86
+ if hasattr(model, "enable_twig"):
87
+ model.enable_twig = two_stage_flag
88
+ if hasattr(model, "roi_enable2stage"):
89
+ model.roi_enable2stage = two_stage_flag
90
+ if hasattr(model, "language_model"):
91
+ if hasattr(model.language_model, "enable_twig"):
92
+ model.language_model.enable_twig = two_stage_flag
93
+ if hasattr(model.language_model, "roi_enable2stage"):
94
+ model.language_model.roi_enable2stage = two_stage_flag
95
+ return
96
+
97
+ if hasattr(model, "model"):
98
+ if hasattr(model.model, "enable_twig"):
99
+ model.model.enable_twig = two_stage_flag
100
+ if hasattr(model.model, "roi_enable2stage"):
101
+ model.model.roi_enable2stage = two_stage_flag
102
+ if hasattr(model.model, "two_stage_vanilla"):
103
+ model.model.two_stage_vanilla = two_stage_flag
104
+ if hasattr(model, "roi_enable2stage"):
105
+ model.roi_enable2stage = two_stage_flag
106
+ def get_KL_divergence(logits_list,):
107
+ import torch.nn as nn
108
+ from torch.nn import functional as F
109
+ KL_div = nn.KLDivLoss(reduction='batchmean')
110
+ KL_div_list = [KL_div(F.log_softmax(logits_list[-1], dim=1), F.softmax(logits_list[i], dim=1)).item() for i in range(len(logits_list))]
111
+ return KL_div_list
112
+
113
+
114
+ from torch.utils.data import Dataset, DataLoader
115
+
116
+
117
+ def worker_wrapper(chunk, gpu_id, args, return_dict):
118
+ """
119
+ Wraps the main worker function to handle return values via shared dictionary.
120
+ """
121
+
122
+ result = worker_process(chunk, gpu_id, args)
123
+ # Store result in the shared dictionary using gpu_id as key
124
+ return_dict[gpu_id] = result
125
+
126
+
127
+
128
+ class QwenEvalDataset(Dataset):
129
+ def __init__(self, questions_chunk, args):
130
+ self.data = questions_chunk
131
+ self.args = args
132
+ self.image_folder = args.image_folder
133
+ self.use_short_prompt = getattr(args, 'use_short_prompt', False)
134
+
135
+
136
+ def __len__(self):
137
+ return len(self.data)
138
+
139
+ def __getitem__(self, idx):
140
+ line = self.data[idx]
141
+ image_file_str = ''
142
+ ground_truth = None
143
+ qs_text = ""
144
+ image = None
145
+
146
+ try:
147
+
148
+ qs_text = line["text"]
149
+ qs_text = qs_text
150
+ image_file = line["image"]
151
+ image_path = os.path.join(self.image_folder, image_file)
152
+ image = Image.open(image_path).convert('RGB')
153
+ image_file_str = image_file
154
+ ground_truth = line.get("answer") if "answer" in line else line.get("answers", None)
155
+
156
+ if "<image>" in qs_text:
157
+ qs_text = qs_text.replace("<image>", "") # Remove it from the text body
158
+
159
+ qs = "<image>\n" + qs_text
160
+
161
+ if self.use_short_prompt:
162
+ qs = qs + "\nAnswer the question using a single word or phrase."
163
+
164
+ # Return a dict containing everything needed for inference
165
+ return {
166
+ 'image': image,
167
+ 'prompt_text': qs,
168
+ 'meta': {
169
+ 'image_id': image_file_str,
170
+ 'prompt': qs_text,
171
+ 'ground_truth': ground_truth,
172
+ 'line_number': line['original_line_number'],
173
+ 'id': line.get('question_id', line.get('original_line_number', 'unknown'))
174
+ },
175
+ 'valid': True
176
+ }
177
+
178
+ except Exception as e:
179
+ print(f"Error loading item {idx}: {e}")
180
+ # Return dummy invalid item
181
+ return {'valid': False, 'meta': {'line_number': line.get('original_line_number', -1)}}
182
+
183
+
184
+ class SingleResCollator:
185
+ def __init__(self, processor_path, patch_size):
186
+ self.processor_path = processor_path
187
+ self.processor = None
188
+ # Define the requested limits
189
+ self.min_pixels = 128 * patch_size * patch_size
190
+ self.max_pixels = 576 * patch_size * patch_size
191
+
192
+ def __call__(self, batch):
193
+ if self.processor is None:
194
+ # Initialize processor once per worker
195
+ self.processor = AutoProcessor.from_pretrained(
196
+ self.processor_path,
197
+ trust_remote_code=True,
198
+ padding_side='left',
199
+ use_fast=True
200
+ )
201
+ # Set the resolution constraints globally for this processor
202
+ self.processor.image_processor.min_pixels = self.min_pixels
203
+ self.processor.image_processor.max_pixels = self.max_pixels
204
+
205
+ batch = [item for item in batch if item.get('valid', False)]
206
+ if not batch:
207
+ return None
208
+
209
+ # 1. Extract raw data
210
+ raw_images = [item['image'] for item in batch]
211
+ raw_prompts = [item['prompt_text'] for item in batch]
212
+ metas = [item['meta'] for item in batch]
213
+
214
+ # 2. Prepare text and images
215
+ current_texts = []
216
+ for prompt_str, img in zip(raw_prompts, raw_images):
217
+ # Qwen-VL-Chat template construction
218
+ messages = [{"role": "user", "content": [{"type": "text", "text": prompt_str}, {"image": img}]}]
219
+ text = self.processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
220
+
221
+ # Specific replacement logic for Qwen2.5-VL
222
+ text = text.replace('<|vision_start|><|image_pad|><|vision_end|>', '') \
223
+ .replace('<image>', '<image><|vision_start|><|image_pad|><|vision_end|>')
224
+ current_texts.append(text)
225
+
226
+ try:
227
+ # 3. Process Batch (Single Resolution Setting)
228
+ inputs = self.processor(
229
+ text=current_texts,
230
+ images=raw_images,
231
+ padding=True,
232
+ return_tensors="pt"
233
+ )
234
+ # Attach extra data needed later if necessary (or just return inputs)
235
+ inputs.data['src_images'] = raw_images
236
+
237
+ return {
238
+ 'metas': metas,
239
+ 'inputs': inputs
240
+ }
241
+
242
+ except Exception as e:
243
+ print(f"Error processing batch: {e}")
244
+ return None
245
+
246
+
247
+ def worker_process(questions_chunk, gpu_id, args):
248
+ device = f'cuda:{gpu_id}'
249
+ answer_analysis_dict = {} # Renamed from ms_answer_analysis_dict
250
+ max_new_tokens = 256
251
+
252
+ # --- SETUP DATALOADER ---
253
+ batch_size = getattr(args, 'batch_size', 8)
254
+ num_workers = getattr(args, 'num_workers', 4)
255
+
256
+ dataset = QwenEvalDataset(questions_chunk, args)
257
+
258
+ # Initialize the NEW single resolution collator
259
+ collator = SingleResCollator(args.processor_path, args.patch_size)
260
+
261
+ dataloader = DataLoader(
262
+ dataset,
263
+ batch_size=batch_size,
264
+ shuffle=False,
265
+ num_workers=num_workers,
266
+ collate_fn=collator,
267
+ pin_memory=True
268
+ )
269
+
270
+ if len(dataset) == 0: return answer_analysis_dict
271
+
272
+ try:
273
+ # Load Model
274
+ print(f"Worker {gpu_id}: Loading model...")
275
+ model_class = resolve_model_class(args.model_type)
276
+ model = model_class.from_pretrained(
277
+ args.model_path,
278
+ torch_dtype=torch.bfloat16,
279
+ trust_remote_code=True,
280
+ device_map=device
281
+ )
282
+ model.eval()
283
+
284
+ # Flags (keeping your original config)
285
+ two_stage_flag = False
286
+ set_twig_flags(model, args.model_type, two_stage_flag)
287
+
288
+ # Tokenizer for decoding
289
+ tokenizer = AutoProcessor.from_pretrained(args.processor_path, trust_remote_code=True).tokenizer
290
+
291
+ print(f"Worker {gpu_id}: Starting inference...")
292
+
293
+ # --- MAIN INFERENCE LOOP ---
294
+ progress_bar = tqdm(dataloader, desc=f"GPU {gpu_id}", position=gpu_id)
295
+
296
+ for batch_data in progress_bar:
297
+ if batch_data is None: continue
298
+
299
+ metas = batch_data['metas']
300
+ inputs = batch_data['inputs'] # Now getting direct inputs, not inputs_by_res
301
+
302
+ try:
303
+ # Move to GPU
304
+ inputs = inputs.to(device)
305
+
306
+ with torch.no_grad():
307
+ output_ids = model.generate(
308
+ **inputs,
309
+ max_new_tokens=max_new_tokens,
310
+ output_attentions=False,
311
+ pad_token_id=tokenizer.eos_token_id
312
+ )
313
+
314
+ # Decode
315
+ input_ids_len = inputs.input_ids.shape[1]
316
+ generated_ids = output_ids[:, input_ids_len:]
317
+ batch_answers = tokenizer.batch_decode(
318
+ generated_ids, skip_special_tokens=True, clean_up_tokenization_spaces=True
319
+ )
320
+
321
+ # --- SAVE RESULTS ---
322
+ for idx, (meta, answer) in enumerate(zip(metas, batch_answers)):
323
+ line_number = meta['line_number']
324
+
325
+ tmp_sample = {
326
+ 'image': meta['image_id'],
327
+ 'prompt': meta['prompt'],
328
+ 'text': meta['ground_truth'],
329
+ 'id': meta['id'],
330
+ 'response': answer # Storing single response
331
+ }
332
+ answer_analysis_dict[line_number] = tmp_sample
333
+
334
+ # Cleanup GPU memory for this batch
335
+ del inputs, output_ids, generated_ids
336
+
337
+ except RuntimeError as e:
338
+ msg = "ERROR_OOM" if "out of memory" in str(e) else f"ERROR_RUNTIME: {e}"
339
+ print(f"Worker {gpu_id}: {msg}")
340
+ torch.cuda.empty_cache()
341
+ # Log errors for this batch
342
+ for meta in metas:
343
+ line_number = meta['line_number']
344
+ answer_analysis_dict[line_number] = {
345
+ 'id': meta['id'],
346
+ 'response': msg
347
+ }
348
+
349
+ except Exception as e:
350
+ print(f"Worker {gpu_id}: Critical Error: {e}")
351
+ import traceback
352
+ traceback.print_exc()
353
+ finally:
354
+ torch.cuda.empty_cache()
355
+
356
+ return answer_analysis_dict
357
+
358
+
359
+ if __name__ == "__main__":
360
+ parser = argparse.ArgumentParser(description="Parallel KL Divergence Evaluation")
361
+ parser.add_argument('--model-path', type=str, required=True,
362
+ help="Path to the model and processor")
363
+ parser.add_argument('--model-type', type=str, default="auto",
364
+ choices=["auto", "qwen2_5", "qwen2", "qwen3"],
365
+ help="Model family for loading (auto detects from model path).")
366
+ parser.add_argument('--processor-path', type=str, default="",
367
+ help="Override processor path if auto detection is incorrect.")
368
+ parser.add_argument('--question-file', type=str, default=None,
369
+ help="Path to the JSONL question file (E.g., TextVQA)")
370
+ parser.add_argument('--image-folder', type=str, default=None,
371
+ help="Path to the image directory (for JSONL file)")
372
+ parser.add_argument('--dst-save-path', type=str, required=True,
373
+ help="Path to save the output .pkl file")
374
+ parser.add_argument('--gpu-ids', type=str, default='0,1',
375
+ help="Comma-separated list of GPU IDs to use (e.g., '0,1,2,3')")
376
+ # --- ADDED: New HF Dataset args ---
377
+ parser.add_argument('--hf_dataset', type=str, default=None,
378
+ help="Name of the Hugging Face dataset (e.g., 'craigwu/vstar_bench')")
379
+ parser.add_argument('--dataset_split', type=str, default='test',
380
+ help="Split to use from the Hugging Face dataset (e.g., 'test')")
381
+ parser.add_argument('--is_check_pred', type=bool, default=False,
382
+ help="Whether to check high-res predictions")
383
+ ### --- END ADDITION ---
384
+ parser.add_argument('--use-short-prompt', action='store_true',
385
+ help="Whether to use short prompt format")
386
+ args = parser.parse_args()
387
+ args.model_type = infer_model_type(args.model_path, args.model_type)
388
+ args.processor_path = resolve_processor_path(
389
+ args.model_path, args.model_type, args.processor_path
390
+ )
391
+ args.patch_size = 32 if args.model_type == "qwen3" else 28
392
+
393
+
394
+ # --- 1. Define a Wrapper Function ---
395
+ # This helper must be defined at the top level (global scope) so it can be pickled.
396
+ # --- 2. The Main Execution Block ---
397
+ # Ensure start method is spawn (Crucial for PyTorch+CUDA)
398
+ try:
399
+ mp.set_start_method('spawn', force=True)
400
+ except RuntimeError:
401
+ pass
402
+
403
+ # 1. Prepare Data and Split into Chunks
404
+ gpu_ids = [int(gid) for gid in args.gpu_ids.split(',')]
405
+ num_gpus = len(gpu_ids)
406
+ questions = []
407
+
408
+ if args.hf_dataset:
409
+ print(f"Loading Hugging Face dataset {args.hf_dataset}, split {args.dataset_split}")
410
+ from datasets import load_dataset
411
+
412
+ dataset = load_dataset(args.hf_dataset, split=args.dataset_split)
413
+ print("Casting 'image' column to load from cache...")
414
+ questions_list = list(dataset)
415
+
416
+ new_questions = []
417
+ for i, tmp_ques in enumerate(questions_list):
418
+ tmp_ques['original_line_number'] = i
419
+ new_questions.append(tmp_ques)
420
+ questions = new_questions
421
+ print(f"Loaded {len(questions)} preprocessed questions from Hugging Face.")
422
+
423
+ elif args.question_file:
424
+ print("Loading and preprocessing questions from JSONL file...")
425
+ if not args.image_folder:
426
+ raise ValueError("--image-folder is required when using --question-file")
427
+
428
+ questions_list = [json.loads(q) for q in open(os.path.expanduser(args.question_file), "r")]
429
+
430
+ new_questions = []
431
+ for i, tmp_ques in enumerate(questions_list):
432
+ qs = tmp_ques['text'] if 'text' in tmp_ques else tmp_ques['prompt']
433
+ if 'Reference OCR' in qs:
434
+ qs = '\n'.join([ele for ele in qs.split('\n') if 'Reference OCR' not in ele])
435
+ tmp_ques['text'] = qs
436
+ tmp_ques['original_line_number'] = i
437
+ new_questions.append(tmp_ques)
438
+ questions = new_questions
439
+ print(f"Loaded {len(questions)} preprocessed questions from local file.")
440
+
441
+ else:
442
+ raise ValueError("You must provide either --question-file or --hf_dataset")
443
+
444
+ # Create chunks
445
+ chunk_size = math.ceil(len(questions) / num_gpus)
446
+ chunks = [questions[i:i + chunk_size] for i in range(0, len(questions), chunk_size)]
447
+
448
+ # 2. Run processes in parallel using mp.Process (Manual Management)
449
+ print(f"Starting parallel processing on {num_gpus} GPUs: {gpu_ids}")
450
+
451
+ # Create a Manager to hold the results safely across processes
452
+ manager = mp.Manager()
453
+ return_dict = manager.dict()
454
+ processes = []
455
+
456
+ for i in range(num_gpus):
457
+ chunk_data = chunks[i] if i < len(chunks) else []
458
+
459
+ # We pass the shared 'return_dict' to the wrapper
460
+ p = mp.Process(
461
+ target=worker_wrapper,
462
+ args=(chunk_data, gpu_ids[i], args, return_dict)
463
+ )
464
+ p.start()
465
+ processes.append(p)
466
+
467
+ # Wait for all processes to finish
468
+ for p in processes:
469
+ p.join()
470
+
471
+ # 3. Gather and Save Results
472
+ print("All chunks processed. Gathering results...")
473
+ final_dict = {}
474
+
475
+ # Extract results from the shared return_dict
476
+ for gpu_id, sub_dict in return_dict.items():
477
+ final_dict.update(sub_dict)
478
+
479
+ #check save directory
480
+ os.makedirs(os.path.dirname(os.path.expanduser(args.dst_save_path)), exist_ok=True)
481
+ with open(os.path.expanduser(args.dst_save_path), 'wb') as f:
482
+ pickle.dump(final_dict, f)
483
+
484
+ print(f"✅ Successfully processed and saved {len(final_dict)} answers to {args.dst_save_path}.")
485
+
486
+
487
+ '''
488
+ CUDA_VISIBLE_DEVICES=2,3 python analysis/making_base_model_response.py \
489
+ --model-path Qwen/Qwen2.5-VL-7B-Instruct \
490
+ --question-file /home/yuheng/datasets/textvqa/converted_llava_style_train.jsonl \
491
+ --image-folder ../llava/playground/data/eval/textvqa/train_images \
492
+ --dst-save-path /home/yuheng/datasets/SFT_SDRPN_PostTraining/textvqa_qwen2.5vl-7B.pkl \
493
+ --gpu-ids 0,1
494
+
495
+ CUDA_VISIBLE_DEVICES=2,3 python analysis/making_base_model_response.py \
496
+ --model-path Qwen/Qwen2.5-VL-7B-Instruct \
497
+ --question-file /home/yuheng/datasets/vstar_coco_spatial_relation_data.jsonl \
498
+ --image-folder /home/yuheng/datasets \
499
+ --dst-save-path /home/yuheng/datasets/SFT_SDRPN_PostTraining/vstar_coco_spatial_qwen2.5vl-7B.pkl \
500
+ --gpu-ids 0,1
501
+
502
+
503
+
504
+
505
+ python analysis/making_base_model_response.py \
506
+ --model-path /root/epfs/sproject/code/Qwen2.5-VL/output/qwen2_5vl-7b-roi-K18T3-185k-v1bf16-denoisedv2-twiginit-filled \
507
+ --question-file /root/epfs/sproject/datasets/textvqa/converted_llava_style_train.jsonl \
508
+ --image-folder /root/epfs/sproject/datasets/textvqa/train_images \
509
+ --dst-save-path /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/textvqa_qwen2.5vl-7B-short-prompt.pkl \
510
+ --use-short-prompt \
511
+ --gpu-ids 0,1
512
+
513
+ python analysis/making_base_model_response.py \
514
+ --model-path /root/epfs/sproject/code/Qwen2.5-VL/output/qwen2_5vl-7b-roi-K18T3-185k-v1bf16-denoisedv2-twiginit-filled \
515
+ --question-file /root/epfs/sproject/datasets/chartqa_28k_qa.jsonl \
516
+ --image-folder /root/epfs/sproject/datasets \
517
+ --dst-save-path /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/chartqa_qwen2.5vl-7B-short-prompt.pkl \\
518
+ --use-short-prompt \
519
+ --gpu-ids 0,1
520
+
521
+ python analysis/making_base_model_response.py \
522
+ --model-path /root/epfs/sproject/code/Qwen2.5-VL/output/qwen2_5vl-7b-roi-K18T3-185k-v1bf16-denoisedv2-twiginit-filled \
523
+ --question-file /root/epfs/sproject/datasets/visual_cot_infovqa_subset15k.jsonl \
524
+ --image-folder /root/epfs/sproject/datasets \
525
+ --dst-save-path /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/visual_cot_infovqa_qwen2.5vl-7B-short-prompt.pkl \
526
+ --use-short-prompt \
527
+ --gpu-ids 0,1
528
+
529
+ python analysis/making_base_model_response.py \
530
+ --model-path /root/epfs/sproject/code/Qwen2.5-VL/output/qwen2_5vl-7b-roi-K18T3-185k-v1bf16-denoisedv2-twiginit-filled \
531
+ --question-file /root/epfs/sproject/datasets/visual_cot_docvqa_subset33k.jsonl \
532
+ --image-folder /root/epfs/sproject/datasets \
533
+ --dst-save-path /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/visual_cot_docvqa_qwen2.5vl-7B-short-prompt.pkl \
534
+ --use-short-prompt \
535
+ --gpu-ids 0,1
536
+
537
+ python analysis/making_base_model_response.py \
538
+ --model-path /root/epfs/sproject/code/Qwen2.5-VL/output/qwen2_5vl-7b-roi-K18T3-185k-v1bf16-denoisedv2-twiginit-filled \
539
+ --question-file /root/epfs/sproject/datasets/vstar_coco_spatial_relation_data.jsonl \
540
+ --image-folder /root/epfs/sproject/datasets/ \
541
+ --dst-save-path /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/vstar_coco_spatial_qwen2.5vl-7B-short-prompt.pkl \
542
+ --use-short-prompt \
543
+ --gpu-ids 0,1
544
+ '''
QWENVL-PRIVATE/analysis/making_gated_training_samples.py ADDED
@@ -0,0 +1,781 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ script_dir = os.path.dirname(os.path.abspath(__file__))
4
+ project_root = os.path.dirname(script_dir)
5
+ sys.path.append(project_root)
6
+
7
+ from qwen_src.qwen2_5_vl import Qwen2_5_VLForConditionalGeneration
8
+ from qwen_src.qwen2_vl import Qwen2VLForConditionalGeneration
9
+ import torch
10
+ import matplotlib.pyplot as plt
11
+ from qwen_vl_utils import process_vision_info
12
+ from transformers import AutoProcessor
13
+ import math
14
+ from PIL import Image, ImageDraw, ImageFont
15
+ import numpy as np
16
+
17
+
18
+ import json
19
+ import os
20
+ from tqdm import tqdm
21
+
22
+ import pickle
23
+ import argparse
24
+ import multiprocessing as mp
25
+ import pandas as pd # <-- Make sure to import pandas
26
+ from matching_utils import TextVQAAccuracyEvaluator
27
+ from check_gate_label import plot_single_sample
28
+ def get_KL_divergence(logits_list,):
29
+ import torch.nn as nn
30
+ from torch.nn import functional as F
31
+ KL_div = nn.KLDivLoss(reduction='batchmean')
32
+ KL_div_list = [KL_div(F.log_softmax(logits_list[-1], dim=1), F.softmax(logits_list[i], dim=1)).item() for i in range(len(logits_list))]
33
+ return KL_div_list
34
+
35
+
36
+ from torch.utils.data import Dataset, DataLoader
37
+
38
+
39
+ def worker_wrapper(chunk, gpu_id, args, return_dict):
40
+ """
41
+ Wraps the main worker function to handle return values via shared dictionary.
42
+ """
43
+ if args.is_check_pred:
44
+ result = check_high_res_prediction(chunk, gpu_id, args)
45
+ else:
46
+ result = worker_process_ms(chunk, gpu_id, args)
47
+ # Store result in the shared dictionary using gpu_id as key
48
+ return_dict[gpu_id] = result
49
+
50
+
51
+ def worker_process_kl_divergence(questions_chunk, gpu_id, args):
52
+ """
53
+ This is the worker function that runs on a single GPU.
54
+ It loads the model and processes its assigned chunk of questions.
55
+ """
56
+
57
+ device = f'cuda:{gpu_id}'
58
+ KL_div_analysis_dict = {}
59
+ max_new_tokens = 1024
60
+ if not questions_chunk:
61
+ print(f"Worker {gpu_id}: No questions in chunk. Exiting.")
62
+ return KL_div_analysis_dict
63
+
64
+ try:
65
+ # 1. Load Model and Processor INSIDE the worker
66
+ print(f"Worker {gpu_id}: Loading processor from {args.model_path}...")
67
+ processor = AutoProcessor.from_pretrained(
68
+ 'Qwen/Qwen2.5-VL-7B-Instruct',
69
+ trust_remote_code=True,
70
+ padding_side='left', use_fast=True
71
+ )
72
+
73
+ print(f"Worker {gpu_id}: Loading model from {args.model_path}...")
74
+ model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
75
+ args.model_path,
76
+ torch_dtype=torch.bfloat16,
77
+ trust_remote_code=True,
78
+ device_map=device # Assign model directly to the correct device
79
+ )
80
+ model.eval()
81
+
82
+ # Set processor image size limits (matching your original function)
83
+ processor.image_processor.max_pixels = 576 * 28 * 28
84
+ processor.image_processor.min_pixels = 576 * 28 * 28
85
+
86
+ # 2. Process the chunk of questions
87
+ print(f"Worker {gpu_id}: Starting processing of {len(questions_chunk)} items.")
88
+
89
+ # Use tqdm for progress, disable for all but the first worker if too noisy
90
+ progress_bar = tqdm(questions_chunk, desc=f"GPU {gpu_id}", position=gpu_id)
91
+ base_answer = two_stage_answer = "" # Initialize to avoid reference before assignment
92
+ for line in progress_bar:
93
+ try:
94
+ # --- MODIFIED: Handle different dataset formats ---
95
+ image_file_str = ''
96
+ ground_truth = None
97
+ if args.question_file is not None and 'textvqa' in args.question_file: # Case 1: Old format with "text" key
98
+ qs_text = line["text"]
99
+ qs_text = qs_text + " Answer the question using a single word or phrase." if "Answer the question using a single word or phrase." not in qs_text else qs_text
100
+ image_file = line["image"]
101
+ image_path = os.path.join(args.image_folder, image_file)
102
+ image = Image.open(image_path).convert('RGB')
103
+ image_file_str = image_file
104
+ ground_truth = line.get("answers") # Store the ground truth
105
+ assert ground_truth is not None, "Ground truth answers missing in the data."
106
+ elif args.question_file is not None and 'visual_cot' in args.question_file: # Case 1.5: Old format with "question" key
107
+ qs_text = line["text"]
108
+ qs_text = qs_text + " Answer the question using a single word or phrase." if "Answer the question using a single word or phrase." not in qs_text else qs_text
109
+ image_file = line["image"]
110
+ image_path = os.path.join(args.image_folder, image_file)
111
+ image = Image.open(image_path).convert('RGB')
112
+ image_file_str = image_file
113
+ ground_truth = line.get("answer")
114
+ elif args.hf_dataset is not None: # Case 2: New HF Dataset format (e.g., vstar_bench)
115
+ if 'vstar' in args.hf_dataset:
116
+ qs_text = line["text"]
117
+ image = line["image"].convert('RGB')
118
+ ground_truth = line.get("label") # Store the ground truth
119
+ image_file_str = f"hf_dataset_{line.get('question_id', 'unknown')}"
120
+ elif 'POPE' in args.hf_dataset:
121
+ qs_text = line["question"] + " Answer the question using a single word or phrase."
122
+ image = line["image"].convert('RGB')
123
+ ground_truth = line.get("answer")
124
+ image_file_str = f"hf_dataset_{line.get('question_id', 'unknown')}"
125
+
126
+ else:
127
+ print(f"Worker {gpu_id}: Skipping line {line.get('original_line_number')}, unknown format.")
128
+ continue
129
+ # --- END MODIFICATION ---
130
+ qs = "<image>\n" + qs_text
131
+ messages = [
132
+ {"role": "user", "content": [{"type": "text", "text": f"{qs}"}, {"image": image}]}
133
+ ]
134
+ text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
135
+ text = text.replace('<|vision_start|><|image_pad|><|vision_end|>', '').replace('<image>',
136
+ '<image><|vision_start|><|image_pad|><|vision_end|>')
137
+
138
+ inputs = processor(text=[text], images=[image], padding=True, return_tensors="pt").to(device)
139
+
140
+ # We don't need these for KL div, but keeping if your model.forward needs them
141
+ inputs.data['src_images'] = [image]
142
+ inputs.data['processor'] = processor
143
+ input_ids_len = inputs.input_ids.shape[1]
144
+
145
+ # --- Run with two_stage_flag = False (Base) ---
146
+ two_stage_flag = False
147
+ model.model.enable_twig = two_stage_flag
148
+ model.model.roi_enable2stage = two_stage_flag
149
+ model.model.two_stage_vanilla = two_stage_flag
150
+ model.roi_enable2stage = two_stage_flag
151
+ model.model.roi_conf_thresh = 0.1
152
+ with torch.no_grad():
153
+ output_dicts = model.forward(**inputs.data,
154
+ output_attentions=True,
155
+ return_dict=False,
156
+ output_hidden_states=False
157
+ )
158
+ base_logits = output_dicts[0][:, -1]
159
+
160
+ with torch.no_grad():
161
+ output_ids = model.generate(**inputs, max_new_tokens=max_new_tokens, output_attentions=False,
162
+ pad_token_id=processor.tokenizer.eos_token_id)
163
+
164
+ generated_ids = output_ids[:, input_ids_len:]
165
+ base_answer = \
166
+ processor.batch_decode(generated_ids, skip_special_tokens=True, clean_up_tokenization_spaces=True)[
167
+ 0]
168
+
169
+ # --- Run with two_stage_flag = True (Two-Stage) ---
170
+ two_stage_flag = True
171
+ model.model.enable_twig = two_stage_flag
172
+ model.model.roi_enable2stage = two_stage_flag
173
+ model.model.two_stage_vanilla = two_stage_flag
174
+ model.roi_enable2stage = two_stage_flag
175
+ model.model.roi_conf_thresh = 0.1
176
+ with torch.no_grad():
177
+ output_dicts = model.forward(**inputs.data,
178
+ output_attentions=False,
179
+ return_dict=False,
180
+ output_hidden_states=False
181
+ )
182
+ two_stage_logits = output_dicts[0][:, -1]
183
+
184
+ logits_list = [base_logits, two_stage_logits]
185
+ KL_div_list = get_KL_divergence(logits_list)
186
+
187
+ with torch.no_grad():
188
+ output_ids = model.generate(**inputs, max_new_tokens=max_new_tokens, output_attentions=False,
189
+ pad_token_id=processor.tokenizer.eos_token_id)
190
+
191
+ generated_ids = output_ids[:, input_ids_len:]
192
+ two_stage_answer = \
193
+ processor.batch_decode(generated_ids, skip_special_tokens=True, clean_up_tokenization_spaces=True)[
194
+ 0]
195
+
196
+ tmp_sample = {
197
+ 'image': image_file_str,
198
+ 'prompt': qs_text, # Store the clean question
199
+ 'base_answer': base_answer,
200
+ 'two_stage_answer': two_stage_answer,
201
+ 'KL_divergence': KL_div_list[0],
202
+ 'text': ground_truth, # This will be None for old data, or the answer
203
+ 'id': line.get('question_id', line.get('original_line_number', 'unknown'))
204
+ }
205
+
206
+ # *** MODIFIED LINE ***
207
+ # Use the original_line_number as the unique key
208
+ line_number = line['original_line_number']
209
+ if line_number in KL_div_analysis_dict:
210
+ print(
211
+ f"Worker {gpu_id}: Warning - Duplicate line_number {line_number} found. Overwriting previous entry.")
212
+ KL_div_analysis_dict[line_number] = tmp_sample
213
+
214
+ except Exception as e:
215
+ # *** MODIFIED LINE ***
216
+ # Update error logging to use line_number
217
+ print(f"Worker {gpu_id}: Error processing {line.get('original_line_number', 'unknown line')}: {e}")
218
+ continue
219
+
220
+ progress_bar.close()
221
+
222
+ except Exception as e:
223
+ print(f"Worker {gpu_id}: A critical error occurred: {e}")
224
+ finally:
225
+ # Clean up model from GPU memory
226
+ del model
227
+ del processor
228
+ torch.cuda.empty_cache()
229
+ print(f"Worker {gpu_id}: Finished and cleaned up.")
230
+
231
+ return KL_div_analysis_dict
232
+
233
+
234
+
235
+ class QwenEvalDataset(Dataset):
236
+ def __init__(self, questions_chunk, args):
237
+ self.data = questions_chunk
238
+ self.args = args
239
+ self.image_folder = args.image_folder
240
+
241
+ def __len__(self):
242
+ return len(self.data)
243
+
244
+ def __getitem__(self, idx):
245
+ line = self.data[idx]
246
+ image_file_str = ''
247
+ ground_truth = None
248
+ qs_text = ""
249
+ image = None
250
+
251
+ try:
252
+ # --- Logic to parse different dataset formats ---
253
+ if self.args.question_file is not None and 'textvqa' in self.args.question_file or 'chartqa' in self.args.question_file:
254
+ qs_text = line["text"]
255
+ qs_text = qs_text + " Answer the question using a single word or phrase." if "Answer the question using a single word or phrase." not in qs_text else qs_text
256
+ image_file = line["image"]
257
+ image_path = os.path.join(self.image_folder, image_file)
258
+ image = Image.open(image_path).convert('RGB')
259
+ image_file_str = image_file
260
+ ground_truth = line.get("answers")
261
+
262
+ elif self.args.question_file is not None and 'visual_cot' in self.args.question_file:
263
+ qs_text = line["text"]
264
+ qs_text = qs_text + " Answer the question using a single word or phrase." if "Answer the question using a single word or phrase." not in qs_text else qs_text
265
+ image_file = line["image"]
266
+ image_path = os.path.join(self.image_folder, image_file)
267
+ image = Image.open(image_path).convert('RGB')
268
+ image_file_str = image_file
269
+ ground_truth = line.get("answer")
270
+
271
+ elif self.args.hf_dataset is not None:
272
+ if 'vstar' in self.args.hf_dataset:
273
+ qs_text = line["text"]
274
+ image = line["image"].convert('RGB')
275
+ ground_truth = line.get("label")
276
+ image_file_str = f"hf_dataset_{line.get('question_id', 'unknown')}"
277
+ elif 'POPE' in self.args.hf_dataset:
278
+ qs_text = line["question"] + " Answer the question using a single word or phrase."
279
+ image = line["image"].convert('RGB')
280
+ ground_truth = line.get("answer")
281
+ image_file_str = f"hf_dataset_{line.get('question_id', 'unknown')}"
282
+
283
+ if "<image>" in qs_text:
284
+ qs_text = qs_text.replace("<image>", "") # Remove it from the text body
285
+
286
+ qs = "<image>\n" + qs_text
287
+
288
+ # Return a dict containing everything needed for inference
289
+ return {
290
+ 'image': image,
291
+ 'prompt_text': qs,
292
+ 'meta': {
293
+ 'image_id': image_file_str,
294
+ 'prompt': qs_text,
295
+ 'ground_truth': ground_truth,
296
+ 'line_number': line['original_line_number'],
297
+ 'id': line.get('question_id', line.get('original_line_number', 'unknown'))
298
+ },
299
+ 'valid': True
300
+ }
301
+
302
+ except Exception as e:
303
+ print(f"Error loading item {idx}: {e}")
304
+ # Return dummy invalid item
305
+ return {'valid': False, 'meta': {'line_number': line.get('original_line_number', -1)}}
306
+
307
+
308
+ class MultiResCollator:
309
+ def __init__(self, model_path, resolution_bases=[64, 128, 256, 576, 768, 1024]):
310
+ self.model_path = model_path
311
+ self.resolution_bases = resolution_bases
312
+ self.processor = None
313
+
314
+ def __call__(self, batch):
315
+ if self.processor is None:
316
+ self.processor = AutoProcessor.from_pretrained(
317
+ self.model_path,
318
+ trust_remote_code=True,
319
+ padding_side='left',
320
+ use_fast=True
321
+ )
322
+
323
+ batch = [item for item in batch if item.get('valid', False)]
324
+ if not batch:
325
+ return None
326
+
327
+ # 1. Extract raw data
328
+ raw_images = [item['image'] for item in batch]
329
+ raw_prompts = [item['prompt_text'] for item in batch]
330
+ metas = [item['meta'] for item in batch]
331
+
332
+ inputs_by_res = {}
333
+
334
+ # 2. Process for each resolution INDEPENDENTLY
335
+ for res_base in self.resolution_bases:
336
+ # Set resolution for this pass
337
+ target_pixels = res_base * 28 * 28
338
+ self.processor.image_processor.min_pixels = target_pixels
339
+ self.processor.image_processor.max_pixels = target_pixels
340
+
341
+ # --- PARANOID DATA PREPARATION ---
342
+ # A. Deep copy images to ensure no in-place resizing affects other loops
343
+ current_images = [img.copy() for img in raw_images]
344
+
345
+ # B. Re-construct text from scratch for this loop.
346
+ # This prevents the processor from modifying the list or string states across loops.
347
+ current_texts = []
348
+ for prompt_str, img in zip(raw_prompts, current_images):
349
+ # Sanity check: Ensure prompt doesn't already have duplicate image tags
350
+ # which would confuse the processor into expecting more images.
351
+ clean_prompt = prompt_str
352
+
353
+ messages = [{"role": "user", "content": [{"type": "text", "text": clean_prompt}, {"image": img}]}]
354
+ text = self.processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
355
+
356
+ # Qwen-VL specific replacement logic
357
+ text = text.replace('<|vision_start|><|image_pad|><|vision_end|>', '') \
358
+ .replace('<image>', '<image><|vision_start|><|image_pad|><|vision_end|>')
359
+ current_texts.append(text)
360
+
361
+ try:
362
+ # C. Process
363
+ inputs = self.processor(
364
+ text=current_texts,
365
+ images=current_images,
366
+ padding=True,
367
+ return_tensors="pt"
368
+ )
369
+ inputs.data['src_images'] = current_images
370
+ inputs.data['processor'] = self.processor
371
+ inputs_by_res[res_base] = inputs
372
+
373
+ except Exception as e:
374
+ # Log specific error but don't crash the whole worker
375
+ # We save the error message to put into the results dict later
376
+ print(f"Error processing resolution {res_base}: {e}")
377
+ inputs_by_res[res_base] = None
378
+
379
+ return {
380
+ 'metas': metas,
381
+ 'inputs_by_res': inputs_by_res
382
+ }
383
+
384
+
385
+ def check_high_res_prediction(questions_chunk, gpu_id, args):
386
+ device = f'cuda:{gpu_id}'
387
+ ms_answer_analysis_dict = {}
388
+ max_new_tokens = 128
389
+ resolution_bases = [64, 128, 256, 576, 768, 1024]
390
+ from qwen_src.qwen2_5_vl import Qwen2_5_VLForConditionalGeneration
391
+
392
+ # --- SETUP DATALOADER ---
393
+ batch_size = getattr(args, 'batch_size', 1)
394
+ num_workers = getattr(args, 'num_workers', 4)
395
+ image_root_folder = getattr(args, 'image_folder', '/home/yuheng/datasets')
396
+ dataset = QwenEvalDataset(questions_chunk, args)
397
+ collator = MultiResCollator(args.model_path, resolution_bases)
398
+ dataloader = DataLoader(
399
+ dataset,
400
+ batch_size=batch_size,
401
+ shuffle=False,
402
+ num_workers=num_workers,
403
+ collate_fn=collator,
404
+ pin_memory=True # Faster transfer to GPU
405
+ )
406
+
407
+ try:
408
+ # Load Model (No processor needed here anymore!)
409
+ print(f"Worker {gpu_id}: Loading model...")
410
+ model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
411
+ args.model_path,
412
+ torch_dtype=torch.bfloat16,
413
+ trust_remote_code=True,
414
+ device_map=device
415
+ )
416
+ model.eval()
417
+
418
+ # Flags
419
+ roi_flag = False
420
+ high_res_flag = True
421
+ model.model.enable_twig = high_res_flag or roi_flag
422
+ model.model.roi_enable2stage = roi_flag
423
+ model.model.two_stage_vanilla = roi_flag
424
+ model.roi_enable2stage = roi_flag
425
+ model.model.is_debug = True
426
+
427
+ tokenizer = AutoProcessor.from_pretrained(args.model_path, trust_remote_code=True).tokenizer
428
+
429
+ print(f"Worker {gpu_id}: Starting inference...")
430
+
431
+ # --- MAIN INFERENCE LOOP ---
432
+ progress_bar = tqdm(dataloader, desc=f"GPU {gpu_id}", position=gpu_id)
433
+
434
+ for batch_data in progress_bar:
435
+ metas = batch_data['metas']
436
+ inputs_by_res = batch_data['inputs_by_res']
437
+ # Temporary storage for this batch
438
+ batch_results = {i: {} for i in range(len(metas))}
439
+ batch_pred = {i: {} for i in range(len(metas))}
440
+ # Loop through pre-processed resolutions
441
+ for res_base in resolution_bases:
442
+ inputs = inputs_by_res.get(res_base)
443
+ if inputs is None:
444
+ # Handle preprocessing failure
445
+ for idx in range(len(metas)): batch_results[idx][res_base] = "ERROR_PREPROC"
446
+ continue
447
+
448
+ # Move pre-processed tensors to GPU
449
+ inputs = inputs.to(device)
450
+ with torch.no_grad():
451
+ output_ids = model.generate(
452
+ **inputs,
453
+ max_new_tokens=max_new_tokens,
454
+ output_attentions=False,
455
+ pad_token_id=tokenizer.eos_token_id
456
+ )
457
+
458
+ # Decode
459
+ input_ids_len = inputs.input_ids.shape[1]
460
+ generated_ids = output_ids[:, input_ids_len:]
461
+ batch_answers = tokenizer.batch_decode(
462
+ generated_ids, skip_special_tokens=True, clean_up_tokenization_spaces=True
463
+ )
464
+
465
+ for idx, answer in enumerate(batch_answers):
466
+ batch_results[idx][res_base] = answer
467
+ batch_pred[idx][res_base] = model.model.high_res_pred.flatten().float().cpu()
468
+ # Cleanup GPU memory for this resolution immediately
469
+ del inputs, output_ids, generated_ids
470
+
471
+ # --- Show RESULTS ---
472
+ for idx, meta in enumerate(metas):
473
+ line_number = meta['line_number']
474
+ ordered_answers = [batch_results[idx].get(r, "ERROR") for r in resolution_bases]
475
+
476
+ tmp_sample = {
477
+ 'image': meta['image_id'],
478
+ 'prompt': meta['prompt'],
479
+ 'text': meta['ground_truth'],
480
+ 'id': meta['id'],
481
+ 'res': resolution_bases,
482
+ 'ms_answers': ordered_answers
483
+ }
484
+ ms_answer_analysis_dict[line_number] = tmp_sample
485
+
486
+ plot_single_sample(ms_answer_analysis_dict[line_number], image_root_folder)
487
+ pass
488
+
489
+ except Exception as e:
490
+ print(f"Worker {gpu_id}: Critical Error: {e}")
491
+ import traceback
492
+ traceback.print_exc()
493
+ finally:
494
+ torch.cuda.empty_cache()
495
+
496
+ def worker_process_ms(questions_chunk, gpu_id, args):
497
+ device = f'cuda:{gpu_id}'
498
+ ms_answer_analysis_dict = {}
499
+ max_new_tokens = 128
500
+ resolution_bases = [64, 128, 256, 576, 768, 1024]
501
+
502
+ # --- SETUP DATALOADER ---
503
+ batch_size = getattr(args, 'batch_size', 8)
504
+ # Use 2-4 workers to ensure CPU preprocessing keeps up with GPU
505
+ num_workers = getattr(args, 'num_workers', 4)
506
+
507
+ dataset = QwenEvalDataset(questions_chunk, args)
508
+
509
+ # Initialize our custom collator
510
+ collator = MultiResCollator(args.model_path, resolution_bases)
511
+
512
+ dataloader = DataLoader(
513
+ dataset,
514
+ batch_size=batch_size,
515
+ shuffle=False,
516
+ num_workers=num_workers,
517
+ collate_fn=collator,
518
+ pin_memory=True # Faster transfer to GPU
519
+ )
520
+
521
+ if len(dataset) == 0: return ms_answer_analysis_dict
522
+
523
+ try:
524
+ # Load Model (No processor needed here anymore!)
525
+ print(f"Worker {gpu_id}: Loading model...")
526
+ model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
527
+ args.model_path,
528
+ torch_dtype=torch.bfloat16,
529
+ trust_remote_code=True,
530
+ device_map=device
531
+ )
532
+ model.eval()
533
+
534
+ # Flags
535
+ two_stage_flag = False
536
+ model.model.enable_twig = two_stage_flag
537
+ model.model.roi_enable2stage = two_stage_flag
538
+ model.model.two_stage_vanilla = two_stage_flag
539
+ model.roi_enable2stage = two_stage_flag
540
+
541
+ tokenizer = AutoProcessor.from_pretrained(args.model_path, trust_remote_code=True).tokenizer
542
+
543
+ print(f"Worker {gpu_id}: Starting inference...")
544
+
545
+ # --- MAIN INFERENCE LOOP ---
546
+ progress_bar = tqdm(dataloader, desc=f"GPU {gpu_id}", position=gpu_id)
547
+
548
+ for batch_data in progress_bar:
549
+ if batch_data is None: continue
550
+
551
+ metas = batch_data['metas']
552
+ inputs_by_res = batch_data['inputs_by_res']
553
+
554
+ # Temporary storage for this batch
555
+ batch_results = {i: {} for i in range(len(metas))}
556
+
557
+ # Loop through pre-processed resolutions
558
+ for res_base in resolution_bases:
559
+ inputs = inputs_by_res.get(res_base)
560
+
561
+ if inputs is None:
562
+ # Handle preprocessing failure
563
+ for idx in range(len(metas)): batch_results[idx][res_base] = "ERROR_PREPROC"
564
+ continue
565
+
566
+ try:
567
+ # Move pre-processed tensors to GPU
568
+ inputs = inputs.to(device)
569
+
570
+ with torch.no_grad():
571
+ output_ids = model.generate(
572
+ **inputs,
573
+ max_new_tokens=max_new_tokens,
574
+ output_attentions=False,
575
+ pad_token_id=tokenizer.eos_token_id
576
+ )
577
+
578
+ # Decode
579
+ input_ids_len = inputs.input_ids.shape[1]
580
+ generated_ids = output_ids[:, input_ids_len:]
581
+ batch_answers = tokenizer.batch_decode(
582
+ generated_ids, skip_special_tokens=True, clean_up_tokenization_spaces=True
583
+ )
584
+
585
+ for idx, answer in enumerate(batch_answers):
586
+ batch_results[idx][res_base] = answer
587
+
588
+ except RuntimeError as e:
589
+ msg = "ERROR_OOM" if "out of memory" in str(e) else "ERROR_RUNTIME"
590
+ print(f"Worker {gpu_id}: {msg} at res {res_base}")
591
+ torch.cuda.empty_cache()
592
+ for idx in range(len(metas)): batch_results[idx][res_base] = msg
593
+
594
+ # Cleanup GPU memory for this resolution immediately
595
+ del inputs, output_ids, generated_ids
596
+
597
+ # --- SAVE RESULTS ---
598
+ for idx, meta in enumerate(metas):
599
+ line_number = meta['line_number']
600
+ ordered_answers = [batch_results[idx].get(r, "ERROR") for r in resolution_bases]
601
+
602
+ tmp_sample = {
603
+ 'image': meta['image_id'],
604
+ 'prompt': meta['prompt'],
605
+ 'text': meta['ground_truth'],
606
+ 'id': meta['id'],
607
+ 'res': resolution_bases,
608
+ 'ms_answers': ordered_answers
609
+ }
610
+ ms_answer_analysis_dict[line_number] = tmp_sample
611
+
612
+ except Exception as e:
613
+ print(f"Worker {gpu_id}: Critical Error: {e}")
614
+ import traceback
615
+ traceback.print_exc()
616
+ finally:
617
+ torch.cuda.empty_cache()
618
+
619
+ return ms_answer_analysis_dict
620
+ if __name__ == "__main__":
621
+ parser = argparse.ArgumentParser(description="Parallel KL Divergence Evaluation")
622
+ parser.add_argument('--model-path', type=str, required=True,
623
+ help="Path to the model and processor")
624
+ parser.add_argument('--question-file', type=str, default=None,
625
+ help="Path to the JSONL question file (E.g., TextVQA)")
626
+ parser.add_argument('--image-folder', type=str, default=None,
627
+ help="Path to the image directory (for JSONL file)")
628
+ parser.add_argument('--dst-save-path', type=str, required=True,
629
+ help="Path to save the output .pkl file")
630
+ parser.add_argument('--gpu-ids', type=str, default='0,1',
631
+ help="Comma-separated list of GPU IDs to use (e.g., '0,1,2,3')")
632
+ # --- ADDED: New HF Dataset args ---
633
+ parser.add_argument('--hf_dataset', type=str, default=None,
634
+ help="Name of the Hugging Face dataset (e.g., 'craigwu/vstar_bench')")
635
+ parser.add_argument('--dataset_split', type=str, default='test',
636
+ help="Split to use from the Hugging Face dataset (e.g., 'test')")
637
+ parser.add_argument('--is_check_pred', type=bool, default=False,
638
+ help="Whether to check high-res predictions")
639
+ ### --- END ADDITION ---
640
+ args = parser.parse_args()
641
+
642
+
643
+ # --- 1. Define a Wrapper Function ---
644
+ # This helper must be defined at the top level (global scope) so it can be pickled.
645
+ # --- 2. The Main Execution Block ---
646
+ # Ensure start method is spawn (Crucial for PyTorch+CUDA)
647
+ try:
648
+ mp.set_start_method('spawn', force=True)
649
+ except RuntimeError:
650
+ pass
651
+
652
+ # 1. Prepare Data and Split into Chunks
653
+ gpu_ids = [int(gid) for gid in args.gpu_ids.split(',')]
654
+ num_gpus = len(gpu_ids)
655
+ questions = []
656
+
657
+ if args.hf_dataset:
658
+ print(f"Loading Hugging Face dataset {args.hf_dataset}, split {args.dataset_split}")
659
+ from datasets import load_dataset
660
+
661
+ dataset = load_dataset(args.hf_dataset, split=args.dataset_split)
662
+ print("Casting 'image' column to load from cache...")
663
+ questions_list = list(dataset)
664
+
665
+ new_questions = []
666
+ for i, tmp_ques in enumerate(questions_list):
667
+ tmp_ques['original_line_number'] = i
668
+ new_questions.append(tmp_ques)
669
+ questions = new_questions
670
+ print(f"Loaded {len(questions)} preprocessed questions from Hugging Face.")
671
+
672
+ elif args.question_file:
673
+ print("Loading and preprocessing questions from JSONL file...")
674
+ if not args.image_folder:
675
+ raise ValueError("--image-folder is required when using --question-file")
676
+
677
+ questions_list = [json.loads(q) for q in open(os.path.expanduser(args.question_file), "r")]
678
+
679
+ new_questions = []
680
+ for i, tmp_ques in enumerate(questions_list):
681
+ qs = tmp_ques['text'] if 'text' in tmp_ques else tmp_ques['prompt']
682
+ if 'Reference OCR' in qs:
683
+ qs = '\n'.join([ele for ele in qs.split('\n') if 'Reference OCR' not in ele])
684
+ tmp_ques['text'] = qs
685
+ tmp_ques['original_line_number'] = i
686
+ new_questions.append(tmp_ques)
687
+ questions = new_questions
688
+ print(f"Loaded {len(questions)} preprocessed questions from local file.")
689
+
690
+ else:
691
+ raise ValueError("You must provide either --question-file or --hf_dataset")
692
+
693
+ # Create chunks
694
+ chunk_size = math.ceil(len(questions) / num_gpus)
695
+ chunks = [questions[i:i + chunk_size] for i in range(0, len(questions), chunk_size)]
696
+
697
+ # 2. Run processes in parallel using mp.Process (Manual Management)
698
+ print(f"Starting parallel processing on {num_gpus} GPUs: {gpu_ids}")
699
+
700
+ # Create a Manager to hold the results safely across processes
701
+ manager = mp.Manager()
702
+ return_dict = manager.dict()
703
+ processes = []
704
+
705
+ for i in range(num_gpus):
706
+ chunk_data = chunks[i] if i < len(chunks) else []
707
+
708
+ # We pass the shared 'return_dict' to the wrapper
709
+ p = mp.Process(
710
+ target=worker_wrapper,
711
+ args=(chunk_data, gpu_ids[i], args, return_dict)
712
+ )
713
+ p.start()
714
+ processes.append(p)
715
+
716
+ # Wait for all processes to finish
717
+ for p in processes:
718
+ p.join()
719
+
720
+ # 3. Gather and Save Results
721
+ print("All chunks processed. Gathering results...")
722
+ final_ms_dict = {}
723
+
724
+ # Extract results from the shared return_dict
725
+ for gpu_id, sub_dict in return_dict.items():
726
+ final_ms_dict.update(sub_dict)
727
+
728
+ with open(os.path.expanduser(args.dst_save_path), 'wb') as f:
729
+ pickle.dump(final_ms_dict, f)
730
+
731
+ print(f"✅ Successfully processed and saved {len(final_ms_dict)} multi-resolution answers to {args.dst_save_path}.")
732
+
733
+
734
+ '''
735
+ CUDA_VISIBLE_DEVICES=2,3 python analysis/making_gated_training_samples.py \
736
+ --model-path /home/yuheng/code/Qwen2.5-VL/output/qwen2_5vl-7b-roi-K16T3-152k-v1bf16Mheads-twiginit-filled \
737
+ --question-file /home/yuheng/datasets/textvqa/converted_llava_style_train.jsonl \
738
+ --image-folder ../llava/playground/data/eval/textvqa/train_images \
739
+ --dst-save-path /home/yuheng/datasets/gated_roi_datasets/roi_KLdiv_textvqa_576res.pkl \
740
+ --gpu-ids 0,1
741
+
742
+ CUDA_VISIBLE_DEVICES=3 python analysis/making_gated_training_samples.py \
743
+ --model-path /home/yuheng/code/Qwen2.5-VL/output/qwen2_5vl-7b-roi-K16T3-152k-v1bf16Mheads-twiginit-filled \
744
+ --question-file /home/yuheng/datasets/visual_cot_llava_subset68k.jsonl \
745
+ --image-folder /home/yuheng/datasets \
746
+ --dst-save-path /home/yuheng/datasets/gated_roi_datasets/roi_KLdiv_vcot68k_576res.pkl \
747
+ --gpu-ids 0
748
+
749
+ python analysis/making_gated_training_samples.py \
750
+ --model-path /root/public/Qwen/Qwen2.5-VL-7B-Instruct \
751
+ --question-file /root/epfs/sproject/datasets/visual_cot_docvqa_subset33k.jsonl \
752
+ --image-folder /root/epfs/sproject/datasets \
753
+ --dst-save-path /root/epfs/sproject/datasets/gated_roi_datasets/roi_vcot_docvqa33k_ms.pkl \
754
+ --gpu-ids 0,1
755
+
756
+
757
+ python analysis/making_gated_training_samples.py \
758
+ --model-path /home/yuheng/code/Qwen2.5-VL/output/qwen2_5vl-7b-roi-K16T3-152k-v1bf16Mheads-twiginit-filled \
759
+ --question-file /home/yuheng/datasets/visual_cot_llava_subset68k.jsonl \
760
+ --image-folder /home/yuheng/datasets \
761
+ --dst-save-path /home/yuheng/datasets/gated_roi_datasets/roi_KLdiv_vcot68k_576res_wprediction.pkl \
762
+ --gpu-ids 0,1,2,3
763
+
764
+ CUDA_VISIBLE_DEVICES=0,1,2,3 python analysis/making_gated_training_samples.py \
765
+ --model-path /home/yuheng/code/Qwen2.5-VL/output/qwen2_5vl-7b-roi-K16T3-152k-v1bf16Mheads-twiginit-filled \
766
+ --hf_dataset "lmms-lab/vstar-bench" --dataset_split "test" \
767
+ --dst-save-path ./excluded/vstar_analysis.pkl --gpu-ids 0,1,2,3
768
+
769
+ CUDA_VISIBLE_DEVICES=0,1,2,3 python analysis/making_gated_training_samples.py \
770
+ --model-path /home/yuheng/code/Qwen2.5-VL/output/qwen2_5vl-7b-roi-K16T3-152k-v1bf16Mheads-twiginit-filled \
771
+ --hf_dataset "lmms-lab/POPE" --dataset_split "test" \
772
+ --dst-save-path ./excluded/pope_analysis.pkl --gpu-ids 0,1,2,3
773
+
774
+ python analysis/making_gated_training_samples.py \
775
+ --model-path /root/public/Qwen/Qwen2.5-VL-7B-Instruct \
776
+ --question-file /root/epfs/sproject/datasets/chartqa_28k_qa.jsonl \
777
+ --image-folder /root/epfs/sproject/datasets \
778
+ --dst-save-path /root/epfs/sproject/datasets/roi_chartqa28k_ms.pkl \
779
+ --gpu-ids 0,1,2,3
780
+
781
+ '''
QWENVL-PRIVATE/analysis/making_roi_model_response.py ADDED
@@ -0,0 +1,565 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ script_dir = os.path.dirname(os.path.abspath(__file__))
4
+ project_root = os.path.dirname(script_dir)
5
+ sys.path.append(project_root)
6
+
7
+ from qwen_src.qwen2_5_vl import Qwen2_5_VLForConditionalGeneration
8
+ #from qwen_src.qwen2_vl import Qwen2VLForConditionalGeneration
9
+ from qwen_src.qwen3_vl.modeling_qwen3_vl_batch import Qwen3VLForConditionalGeneration
10
+ import torch
11
+ import matplotlib.pyplot as plt
12
+ from qwen_vl_utils import process_vision_info
13
+ from transformers import AutoProcessor
14
+ import math
15
+ from PIL import Image, ImageDraw, ImageFont
16
+ import numpy as np
17
+
18
+
19
+ import json
20
+ import os
21
+ from tqdm import tqdm
22
+
23
+ import pickle
24
+ import argparse
25
+ import multiprocessing as mp
26
+ import pandas as pd # <-- Make sure to import pandas
27
+ from matching_utils import TextVQAAccuracyEvaluator
28
+ from check_gate_label import plot_single_sample
29
+
30
+
31
+ def infer_model_type(model_path, model_type):
32
+ if model_type and model_type != "auto":
33
+ return model_type
34
+ lower = model_path.lower()
35
+ if "qwen3" in lower:
36
+ return "qwen3"
37
+ if "qwen2.5" in lower or "qwen2_5" in lower or "qwen2-5" in lower:
38
+ return "qwen2_5"
39
+ if "qwen2" in lower:
40
+ return "qwen2"
41
+ return "qwen2_5"
42
+
43
+
44
+ def resolve_processor_path(model_path, model_type, processor_path=None):
45
+ if processor_path:
46
+ return processor_path
47
+ lower = model_path.lower()
48
+ public_prefix = "/root/public/Qwen"
49
+
50
+ def to_repo_path(name):
51
+ if "/root/public" in model_path:
52
+ return f"{public_prefix}/{name}"
53
+ return f"Qwen/{name}"
54
+
55
+ if model_type == "qwen3":
56
+ if "8b" in lower:
57
+ return to_repo_path("Qwen3-VL-8B-Instruct")
58
+ if "4b" in lower:
59
+ return to_repo_path("Qwen3-VL-4B-Instruct")
60
+ if model_type == "qwen2_5":
61
+ if "7b" in lower:
62
+ return to_repo_path("Qwen2.5-VL-7B-Instruct")
63
+ if "3b" in lower:
64
+ return to_repo_path("Qwen2.5-VL-3B-Instruct")
65
+ if model_type == "qwen2" and "7b" in lower:
66
+ return to_repo_path("Qwen2-VL-7B-Instruct")
67
+
68
+ return model_path
69
+
70
+
71
+ def resolve_model_class(model_type):
72
+ if model_type == "qwen3":
73
+ if Qwen3VLForConditionalGeneration is None:
74
+ raise RuntimeError("Qwen3VLForConditionalGeneration not available.")
75
+ return Qwen3VLForConditionalGeneration
76
+ if model_type == "qwen2":
77
+ return Qwen2VLForConditionalGeneration
78
+ return Qwen2_5_VLForConditionalGeneration
79
+
80
+
81
+ def set_twig_flags(model, model_type, two_stage_flag):
82
+ if model_type == "qwen3":
83
+ if hasattr(model, "enable_twig"):
84
+ model.enable_twig = two_stage_flag
85
+ if hasattr(model, "roi_enable2stage"):
86
+ model.roi_enable2stage = two_stage_flag
87
+ if hasattr(model, "language_model"):
88
+ if hasattr(model.language_model, "enable_twig"):
89
+ model.language_model.enable_twig = two_stage_flag
90
+ if hasattr(model.language_model, "roi_enable2stage"):
91
+ model.language_model.roi_enable2stage = two_stage_flag
92
+ if hasattr(model.language_model, "roi_conf_thresh"):
93
+ model.language_model.roi_conf_thresh = 0.15
94
+ return
95
+
96
+ if hasattr(model, "model"):
97
+ if hasattr(model.model, "enable_twig"):
98
+ model.model.enable_twig = two_stage_flag
99
+ if hasattr(model.model, "roi_enable2stage"):
100
+ model.model.roi_enable2stage = two_stage_flag
101
+ if hasattr(model.model, "two_stage_vanilla"):
102
+ model.model.two_stage_vanilla = two_stage_flag
103
+ if hasattr(model.model, "roi_conf_thresh"):
104
+ model.model.roi_conf_thresh = 0.15
105
+ if hasattr(model, "roi_enable2stage"):
106
+ model.roi_enable2stage = two_stage_flag
107
+ def get_KL_divergence(logits_list,):
108
+ import torch.nn as nn
109
+ from torch.nn import functional as F
110
+ KL_div = nn.KLDivLoss(reduction='batchmean')
111
+ KL_div_list = [KL_div(F.log_softmax(logits_list[-1], dim=1), F.softmax(logits_list[i], dim=1)).item() for i in range(len(logits_list))]
112
+ return KL_div_list
113
+
114
+
115
+ from torch.utils.data import Dataset, DataLoader
116
+
117
+
118
+ def worker_wrapper(chunk, gpu_id, args, return_dict):
119
+ """
120
+ Wraps the main worker function to handle return values via shared dictionary.
121
+ """
122
+
123
+ result = worker_process(chunk, gpu_id, args)
124
+ # Store result in the shared dictionary using gpu_id as key
125
+ return_dict[gpu_id] = result
126
+
127
+
128
+
129
+ class QwenEvalDataset(Dataset):
130
+ def __init__(self, questions_chunk, args):
131
+ self.data = questions_chunk
132
+ self.args = args
133
+ self.image_folder = args.image_folder
134
+ self.use_short_prompt = getattr(args, 'use_short_prompt', False)
135
+
136
+ def __len__(self):
137
+ return len(self.data)
138
+
139
+ def __getitem__(self, idx):
140
+ line = self.data[idx]
141
+ image_file_str = ''
142
+ ground_truth = None
143
+ qs_text = ""
144
+ image = None
145
+
146
+ try:
147
+
148
+ qs_text = line["text"]
149
+ qs_text = qs_text
150
+ image_file = line["image"]
151
+ image_path = os.path.join(self.image_folder, image_file)
152
+ image = Image.open(image_path).convert('RGB')
153
+ image_file_str = image_file
154
+ ground_truth = line.get("answer") if "answer" in line else line.get("answers", None)
155
+
156
+ if "<image>" in qs_text:
157
+ qs_text = qs_text.replace("<image>", "") # Remove it from the text body
158
+
159
+ qs = "<image>\n" + qs_text
160
+
161
+ if self.use_short_prompt:
162
+ qs = qs + "\nAnswer the question using a single word or phrase."
163
+
164
+ # Return a dict containing everything needed for inference
165
+ return {
166
+ 'image': image,
167
+ 'prompt_text': qs,
168
+ 'meta': {
169
+ 'image_id': image_file_str,
170
+ 'prompt': qs_text,
171
+ 'ground_truth': ground_truth,
172
+ 'line_number': line['original_line_number'],
173
+ 'id': line.get('question_id', line.get('original_line_number', 'unknown'))
174
+ },
175
+ 'valid': True
176
+ }
177
+
178
+ except Exception as e:
179
+ print(f"Error loading item {idx}: {e}")
180
+ # Return dummy invalid item
181
+ return {'valid': False, 'meta': {'line_number': line.get('original_line_number', -1)}}
182
+
183
+
184
+ class SingleResCollator:
185
+ def __init__(self, processor_path, patch_size):
186
+ self.processor_path = processor_path
187
+ self.processor = None
188
+ # Define the requested limits
189
+ self.min_pixels = 128 * patch_size * patch_size
190
+ self.max_pixels = 576 * patch_size * patch_size
191
+
192
+ def __call__(self, batch):
193
+ if self.processor is None:
194
+ # Initialize processor once per worker
195
+ self.processor = AutoProcessor.from_pretrained(
196
+ self.processor_path,
197
+ trust_remote_code=True,
198
+ padding_side='left',
199
+ use_fast=True
200
+ )
201
+ # Set the resolution constraints globally for this processor
202
+ self.processor.image_processor.min_pixels = self.min_pixels
203
+ self.processor.image_processor.max_pixels = self.max_pixels
204
+
205
+
206
+ batch = [item for item in batch if item.get('valid', False)]
207
+ if not batch:
208
+ return None
209
+
210
+ # 1. Extract raw data
211
+ raw_images = [item['image'] for item in batch]
212
+ raw_prompts = [item['prompt_text'] for item in batch]
213
+ metas = [item['meta'] for item in batch]
214
+
215
+ # 2. Prepare text and images
216
+ current_texts = []
217
+ for prompt_str, img in zip(raw_prompts, raw_images):
218
+ # Qwen-VL-Chat template construction
219
+ messages = [{"role": "user", "content": [{"type": "text", "text": prompt_str}, {"image": img}]}]
220
+ text = self.processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
221
+
222
+ # Specific replacement logic for Qwen2.5-VL
223
+ text = text.replace('<|vision_start|><|image_pad|><|vision_end|>', '') \
224
+ .replace('<image>', '<image><|vision_start|><|image_pad|><|vision_end|>')
225
+ current_texts.append(text)
226
+
227
+ try:
228
+ # 3. Process Batch (Single Resolution Setting)
229
+ inputs = self.processor(
230
+ text=current_texts,
231
+ images=raw_images,
232
+ padding=True,
233
+ return_tensors="pt"
234
+ )
235
+ # Attach extra data needed later if necessary (or just return inputs)
236
+ inputs.data['src_images'] = raw_images
237
+
238
+ return {
239
+ 'metas': metas,
240
+ 'inputs': inputs
241
+ }
242
+
243
+ except Exception as e:
244
+ print(f"Error processing batch: {e}")
245
+ return None
246
+
247
+
248
+ def worker_process(questions_chunk, gpu_id, args):
249
+ device = f'cuda:{gpu_id}'
250
+ answer_analysis_dict = {} # Renamed from ms_answer_analysis_dict
251
+ max_new_tokens = 128
252
+
253
+ # --- SETUP DATALOADER ---
254
+ batch_size = getattr(args, 'batch_size', 1)
255
+ num_workers = getattr(args, 'num_workers', 4)
256
+
257
+ dataset = QwenEvalDataset(questions_chunk, args)
258
+
259
+ # Initialize the NEW single resolution collator
260
+ collator = SingleResCollator(args.processor_path, args.patch_size)
261
+
262
+ dataloader = DataLoader(
263
+ dataset,
264
+ batch_size=batch_size,
265
+ shuffle=False,
266
+ num_workers=num_workers,
267
+ collate_fn=collator,
268
+ pin_memory=True
269
+ )
270
+
271
+ if len(dataset) == 0: return answer_analysis_dict
272
+
273
+ try:
274
+ # Load Model
275
+ print(f"Worker {gpu_id}: Loading model...")
276
+ model_class = resolve_model_class(args.model_type)
277
+ model = model_class.from_pretrained(
278
+ args.model_path,
279
+ torch_dtype=torch.bfloat16,
280
+ trust_remote_code=True,
281
+ device_map=device
282
+ )
283
+ model.eval()
284
+
285
+ # Flags (keeping your original config)
286
+ two_stage_flag = True
287
+ set_twig_flags(model, args.model_type, two_stage_flag)
288
+
289
+ # Tokenizer for decoding
290
+ tokenizer = AutoProcessor.from_pretrained(args.processor_path, trust_remote_code=True).tokenizer
291
+
292
+ print(f"Worker {gpu_id}: Starting inference...")
293
+
294
+ # --- MAIN INFERENCE LOOP ---
295
+ progress_bar = tqdm(dataloader, desc=f"GPU {gpu_id}", position=gpu_id)
296
+
297
+ for batch_data in progress_bar:
298
+ if batch_data is None: continue
299
+
300
+ metas = batch_data['metas']
301
+ inputs = batch_data['inputs'] # Now getting direct inputs, not inputs_by_res
302
+
303
+ try:
304
+ # Move to GPU
305
+ inputs = inputs.to(device)
306
+ batch_size = inputs.input_ids.shape[0]
307
+ assert batch_size == 1, "Batch size greater than 1 is not supported in this script."
308
+ inputs.data['processor'] = collator.processor
309
+ with torch.no_grad():
310
+ output_ids = model.generate(
311
+ **inputs,
312
+ max_new_tokens=max_new_tokens,
313
+ output_attentions=False,
314
+ pad_token_id=tokenizer.eos_token_id
315
+ )
316
+
317
+ # Decode
318
+ input_ids_len = inputs.input_ids.shape[1]
319
+ generated_ids = output_ids[:, input_ids_len:]
320
+ batch_answers = tokenizer.batch_decode(
321
+ generated_ids, skip_special_tokens=True, clean_up_tokenization_spaces=True
322
+ )
323
+ surrounding_bbox = model.model.surrounding_bbox if hasattr(model.model, 'surrounding_bbox') else None
324
+ roi_mask = model.model.roi_mask if hasattr(model.model, 'roi_mask') else None
325
+ if surrounding_bbox is not None and roi_mask is not None:
326
+ ###convert them to cpu numpy
327
+ surrounding_bbox = surrounding_bbox.cpu().numpy()
328
+ roi_mask = roi_mask.cpu().numpy()
329
+
330
+ # --- SAVE RESULTS ---
331
+ for idx, (meta, answer) in enumerate(zip(metas, batch_answers)):
332
+ line_number = meta['line_number']
333
+
334
+ tmp_sample = {
335
+ 'image': meta['image_id'],
336
+ 'prompt': meta['prompt'],
337
+ 'text': meta['ground_truth'],
338
+ 'id': meta['id'],
339
+ 'response': answer, # Storing single response
340
+ 'surrounding_bbox': surrounding_bbox,
341
+ 'roi_mask': roi_mask
342
+ }
343
+ answer_analysis_dict[line_number] = tmp_sample
344
+
345
+ # Cleanup GPU memory for this batch
346
+ del inputs, output_ids, generated_ids
347
+
348
+ except RuntimeError as e:
349
+ msg = "ERROR_OOM" if "out of memory" in str(e) else f"ERROR_RUNTIME: {e}"
350
+ print(f"Worker {gpu_id}: {msg}")
351
+ torch.cuda.empty_cache()
352
+ # Log errors for this batch
353
+ for meta in metas:
354
+ line_number = meta['line_number']
355
+ answer_analysis_dict[line_number] = {
356
+ 'id': meta['id'],
357
+ 'response': msg
358
+ }
359
+
360
+ except Exception as e:
361
+ print(f"Worker {gpu_id}: Critical Error: {e}")
362
+ import traceback
363
+ traceback.print_exc()
364
+ finally:
365
+ torch.cuda.empty_cache()
366
+
367
+ return answer_analysis_dict
368
+
369
+
370
+ if __name__ == "__main__":
371
+ parser = argparse.ArgumentParser(description="Parallel KL Divergence Evaluation")
372
+ parser.add_argument('--model-path', type=str, required=True,
373
+ help="Path to the model and processor")
374
+ parser.add_argument('--model-type', type=str, default="auto",
375
+ choices=["auto", "qwen2_5", "qwen2", "qwen3"],
376
+ help="Model family for loading (auto detects from model path).")
377
+ parser.add_argument('--processor-path', type=str, default="",
378
+ help="Override processor path if auto detection is incorrect.")
379
+ parser.add_argument('--question-file', type=str, default=None,
380
+ help="Path to the JSONL question file (E.g., TextVQA)")
381
+ parser.add_argument('--image-folder', type=str, default=None,
382
+ help="Path to the image directory (for JSONL file)")
383
+ parser.add_argument('--dst-save-path', type=str, required=True,
384
+ help="Path to save the output .pkl file")
385
+ parser.add_argument('--gpu-ids', type=str, default='0,1',
386
+ help="Comma-separated list of GPU IDs to use (e.g., '0,1,2,3')")
387
+ # --- ADDED: New HF Dataset args ---
388
+ parser.add_argument('--hf_dataset', type=str, default=None,
389
+ help="Name of the Hugging Face dataset (e.g., 'craigwu/vstar_bench')")
390
+ parser.add_argument('--dataset_split', type=str, default='test',
391
+ help="Split to use from the Hugging Face dataset (e.g., 'test')")
392
+ parser.add_argument('--is_check_pred', type=bool, default=False,
393
+ help="Whether to check high-res predictions")
394
+ ### --- END ADDITION ---
395
+ parser.add_argument('--use-short-prompt', action='store_true',
396
+ help="Whether to use short prompt format")
397
+ args = parser.parse_args()
398
+ args.model_type = infer_model_type(args.model_path, args.model_type)
399
+ args.processor_path = resolve_processor_path(
400
+ args.model_path, args.model_type, args.processor_path
401
+ )
402
+ args.patch_size = 32 if args.model_type == "qwen3" else 28
403
+
404
+
405
+ # --- 1. Define a Wrapper Function ---
406
+ # This helper must be defined at the top level (global scope) so it can be pickled.
407
+ # --- 2. The Main Execution Block ---
408
+ # Ensure start method is spawn (Crucial for PyTorch+CUDA)
409
+ try:
410
+ mp.set_start_method('spawn', force=True)
411
+ except RuntimeError:
412
+ pass
413
+
414
+ # 1. Prepare Data and Split into Chunks
415
+ gpu_ids = [int(gid) for gid in args.gpu_ids.split(',')]
416
+ num_gpus = len(gpu_ids)
417
+ questions = []
418
+
419
+ if args.hf_dataset:
420
+ print(f"Loading Hugging Face dataset {args.hf_dataset}, split {args.dataset_split}")
421
+ from datasets import load_dataset
422
+
423
+ dataset = load_dataset(args.hf_dataset, split=args.dataset_split)
424
+ print("Casting 'image' column to load from cache...")
425
+ questions_list = list(dataset)
426
+
427
+ new_questions = []
428
+ for i, tmp_ques in enumerate(questions_list):
429
+ tmp_ques['original_line_number'] = i
430
+ new_questions.append(tmp_ques)
431
+ questions = new_questions
432
+ print(f"Loaded {len(questions)} preprocessed questions from Hugging Face.")
433
+
434
+ elif args.question_file:
435
+ print("Loading and preprocessing questions from JSONL file...")
436
+ if not args.image_folder:
437
+ raise ValueError("--image-folder is required when using --question-file")
438
+
439
+ questions_list = [json.loads(q) for q in open(os.path.expanduser(args.question_file), "r")]
440
+
441
+ new_questions = []
442
+ for i, tmp_ques in enumerate(questions_list):
443
+ qs = tmp_ques['text'] if 'text' in tmp_ques else tmp_ques['prompt']
444
+ if 'Reference OCR' in qs:
445
+ qs = '\n'.join([ele for ele in qs.split('\n') if 'Reference OCR' not in ele])
446
+ tmp_ques['text'] = qs
447
+ tmp_ques['original_line_number'] = i
448
+ new_questions.append(tmp_ques)
449
+ questions = new_questions
450
+ print(f"Loaded {len(questions)} preprocessed questions from local file.")
451
+
452
+ else:
453
+ raise ValueError("You must provide either --question-file or --hf_dataset")
454
+
455
+ # Create chunks
456
+ chunk_size = math.ceil(len(questions) / num_gpus)
457
+ chunks = [questions[i:i + chunk_size] for i in range(0, len(questions), chunk_size)]
458
+
459
+ # 2. Run processes in parallel using mp.Process (Manual Management)
460
+ print(f"Starting parallel processing on {num_gpus} GPUs: {gpu_ids}")
461
+
462
+ # Create a Manager to hold the results safely across processes
463
+ manager = mp.Manager()
464
+ return_dict = manager.dict()
465
+ processes = []
466
+
467
+ for i in range(num_gpus):
468
+ chunk_data = chunks[i] if i < len(chunks) else []
469
+
470
+ # We pass the shared 'return_dict' to the wrapper
471
+ p = mp.Process(
472
+ target=worker_wrapper,
473
+ args=(chunk_data, gpu_ids[i], args, return_dict)
474
+ )
475
+ p.start()
476
+ processes.append(p)
477
+
478
+ # Wait for all processes to finish
479
+ for p in processes:
480
+ p.join()
481
+
482
+ # 3. Gather and Save Results
483
+ print("All chunks processed. Gathering results...")
484
+ final_dict = {}
485
+
486
+ # Extract results from the shared return_dict
487
+ for gpu_id, sub_dict in return_dict.items():
488
+ final_dict.update(sub_dict)
489
+
490
+ #check save directory
491
+ os.makedirs(os.path.dirname(os.path.expanduser(args.dst_save_path)), exist_ok=True)
492
+ with open(os.path.expanduser(args.dst_save_path), 'wb') as f:
493
+ pickle.dump(final_dict, f)
494
+
495
+ print(f"✅ Successfully processed and saved {len(final_dict)} answers to {args.dst_save_path}.")
496
+
497
+
498
+ '''
499
+ CUDA_VISIBLE_DEVICES=2,3 python analysis/making_roi_model_response.py \
500
+ --model-path ./output/qwen2_5vl-7b-roi-K18T3-185k-v1bf16-denoisedv2-twiginit-filled \
501
+ --question-file /home/yuheng/datasets/textvqa/converted_llava_style_train.jsonl \
502
+ --image-folder ../llava/playground/data/eval/textvqa/train_images \
503
+ --dst-save-path /home/yuheng/datasets/SFT_SDRPN_PostTraining/textvqa_qwen2.5vl-7B-roi.pkl \
504
+ --use-short-prompt \
505
+ --gpu-ids 0,1
506
+
507
+ CUDA_VISIBLE_DEVICES=2,3 python analysis/making_roi_model_response.py \
508
+ --model-path ./output/qwen2_5vl-7b-roi-K18T3-185k-v1bf16-denoisedv2-twiginit-filled \
509
+ --question-file /home/yuheng/datasets/vstar_coco_spatial_relation_data.jsonl \
510
+ --image-folder /home/yuheng/datasets \
511
+ --dst-save-path /home/yuheng/datasets/SFT_SDRPN_PostTraining/vstar_coco_spatial_qwen2.5vl-7B-roi.pkl \
512
+ --gpu-ids 0,1
513
+
514
+
515
+
516
+ python analysis/making_roi_model_response.py \
517
+ --model-path /root/epfs/sproject/code/Qwen2.5-VL/output/qwen2_5vl-7b-roi-K18T3-185k-v1bf16-denoisedv2-twiginit-filled \
518
+ --question-file /root/epfs/sproject/datasets/textvqa/converted_llava_style_train.jsonl \
519
+ --image-folder /root/epfs/sproject/datasets/textvqa/train_images \
520
+ --dst-save-path /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/textvqa_qwen2.5vl-7B-roi-short-prompt.pkl \
521
+ --use-short-prompt \
522
+ --gpu-ids 0,1
523
+
524
+ python analysis/making_roi_model_response.py \
525
+ --model-path /root/epfs/sproject/code/Qwen2.5-VL/output/qwen2_5vl-7b-roi-K18T3-185k-v1bf16-denoisedv2-twiginit-filled \
526
+ --question-file /root/epfs/sproject/datasets/chartqa_28k_qa.jsonl \
527
+ --image-folder /root/epfs/sproject/datasets \
528
+ --dst-save-path /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/chartqa_qwen2.5vl-7B-roi-short-prompt.pkl \\
529
+ --use-short-prompt \
530
+ --gpu-ids 0,1
531
+
532
+ python analysis/making_roi_model_response.py \
533
+ --model-path /root/epfs/sproject/code/Qwen2.5-VL/output/qwen2_5vl-7b-roi-K18T3-185k-v1bf16-denoisedv2-twiginit-filled \
534
+ --question-file /root/epfs/sproject/datasets/visual_cot_infovqa_subset15k.jsonl \
535
+ --image-folder /root/epfs/sproject/datasets \
536
+ --dst-save-path /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/visual_cot_infovqa_qwen2.5vl-7B-roi-short-prompt.pkl \
537
+ --use-short-prompt \
538
+ --gpu-ids 0,1
539
+
540
+ python analysis/making_roi_model_response.py \
541
+ --model-path /root/epfs/sproject/code/Qwen2.5-VL/output/qwen2_5vl-7b-roi-K18T3-185k-v1bf16-denoisedv2-twiginit-filled \
542
+ --question-file /root/epfs/sproject/datasets/visual_cot_docvqa_subset33k.jsonl \
543
+ --image-folder /root/epfs/sproject/datasets \
544
+ --dst-save-path /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/visual_cot_docvqa_qwen2.5vl-7B-roi-short-prompt.pkl \
545
+ --use-short-prompt \
546
+ --gpu-ids 0,1
547
+
548
+ python analysis/making_roi_model_response.py \
549
+ --model-path /root/epfs/sproject/code/Qwen2.5-VL/output/qwen2_5vl-7b-roi-K18T3-185k-v1bf16-denoisedv2-twiginit-filled \
550
+ --question-file /root/epfs/sproject/datasets/vstar_coco_spatial_relation_data.jsonl \
551
+ --image-folder /root/epfs/sproject/datasets/ \
552
+ --dst-save-path /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/vstar_coco_spatial_qwen2.5vl-7B-roi-short-prompt.pkl \
553
+ --use-short-prompt \
554
+ --gpu-ids 0,1
555
+
556
+
557
+
558
+
559
+ python analysis/making_base_model_response.py \
560
+ --model-path /root/epfs/sproject/code/Qwen2.5-VL/output/qwen2_5vl-7b-roi-K18T3-185k-v1bf16-denoisedv2-twiginit-filled \
561
+ --question-file /root/epfs/sproject/datasets/vstar_coco_spatial_relation_data.jsonl \
562
+ --image-folder /root/epfs/sproject/datasets \
563
+ --dst-save-path /root/epfs/sproject/datasets/SFT_SDRPN_PostTraining/vstar_coco_spatial_qwen2.5vl-7B-roi-short-prompt.pkl \
564
+ --gpu-ids 0,1,2,3
565
+ '''
QWENVL-PRIVATE/analysis/matching_utils.py ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Facebook, Inc. and its affiliates.
2
+ import re
3
+
4
+ from tqdm import tqdm
5
+
6
+
7
+ class EvalAIAnswerProcessor:
8
+ """
9
+ Processes an answer similar to Eval AI
10
+ copied from
11
+ https://github.com/facebookresearch/mmf/blob/c46b3b3391275b4181567db80943473a89ab98ab/pythia/tasks/processors.py#L897
12
+ """
13
+
14
+ CONTRACTIONS = {
15
+ "aint": "ain't",
16
+ "arent": "aren't",
17
+ "cant": "can't",
18
+ "couldve": "could've",
19
+ "couldnt": "couldn't",
20
+ "couldn'tve": "couldn't've",
21
+ "couldnt've": "couldn't've",
22
+ "didnt": "didn't",
23
+ "doesnt": "doesn't",
24
+ "dont": "don't",
25
+ "hadnt": "hadn't",
26
+ "hadnt've": "hadn't've",
27
+ "hadn'tve": "hadn't've",
28
+ "hasnt": "hasn't",
29
+ "havent": "haven't",
30
+ "hed": "he'd",
31
+ "hed've": "he'd've",
32
+ "he'dve": "he'd've",
33
+ "hes": "he's",
34
+ "howd": "how'd",
35
+ "howll": "how'll",
36
+ "hows": "how's",
37
+ "Id've": "I'd've",
38
+ "I'dve": "I'd've",
39
+ "Im": "I'm",
40
+ "Ive": "I've",
41
+ "isnt": "isn't",
42
+ "itd": "it'd",
43
+ "itd've": "it'd've",
44
+ "it'dve": "it'd've",
45
+ "itll": "it'll",
46
+ "let's": "let's",
47
+ "maam": "ma'am",
48
+ "mightnt": "mightn't",
49
+ "mightnt've": "mightn't've",
50
+ "mightn'tve": "mightn't've",
51
+ "mightve": "might've",
52
+ "mustnt": "mustn't",
53
+ "mustve": "must've",
54
+ "neednt": "needn't",
55
+ "notve": "not've",
56
+ "oclock": "o'clock",
57
+ "oughtnt": "oughtn't",
58
+ "ow's'at": "'ow's'at",
59
+ "'ows'at": "'ow's'at",
60
+ "'ow'sat": "'ow's'at",
61
+ "shant": "shan't",
62
+ "shed've": "she'd've",
63
+ "she'dve": "she'd've",
64
+ "she's": "she's",
65
+ "shouldve": "should've",
66
+ "shouldnt": "shouldn't",
67
+ "shouldnt've": "shouldn't've",
68
+ "shouldn'tve": "shouldn't've",
69
+ "somebody'd": "somebodyd",
70
+ "somebodyd've": "somebody'd've",
71
+ "somebody'dve": "somebody'd've",
72
+ "somebodyll": "somebody'll",
73
+ "somebodys": "somebody's",
74
+ "someoned": "someone'd",
75
+ "someoned've": "someone'd've",
76
+ "someone'dve": "someone'd've",
77
+ "someonell": "someone'll",
78
+ "someones": "someone's",
79
+ "somethingd": "something'd",
80
+ "somethingd've": "something'd've",
81
+ "something'dve": "something'd've",
82
+ "somethingll": "something'll",
83
+ "thats": "that's",
84
+ "thered": "there'd",
85
+ "thered've": "there'd've",
86
+ "there'dve": "there'd've",
87
+ "therere": "there're",
88
+ "theres": "there's",
89
+ "theyd": "they'd",
90
+ "theyd've": "they'd've",
91
+ "they'dve": "they'd've",
92
+ "theyll": "they'll",
93
+ "theyre": "they're",
94
+ "theyve": "they've",
95
+ "twas": "'twas",
96
+ "wasnt": "wasn't",
97
+ "wed've": "we'd've",
98
+ "we'dve": "we'd've",
99
+ "weve": "we've",
100
+ "werent": "weren't",
101
+ "whatll": "what'll",
102
+ "whatre": "what're",
103
+ "whats": "what's",
104
+ "whatve": "what've",
105
+ "whens": "when's",
106
+ "whered": "where'd",
107
+ "wheres": "where's",
108
+ "whereve": "where've",
109
+ "whod": "who'd",
110
+ "whod've": "who'd've",
111
+ "who'dve": "who'd've",
112
+ "wholl": "who'll",
113
+ "whos": "who's",
114
+ "whove": "who've",
115
+ "whyll": "why'll",
116
+ "whyre": "why're",
117
+ "whys": "why's",
118
+ "wont": "won't",
119
+ "wouldve": "would've",
120
+ "wouldnt": "wouldn't",
121
+ "wouldnt've": "wouldn't've",
122
+ "wouldn'tve": "wouldn't've",
123
+ "yall": "y'all",
124
+ "yall'll": "y'all'll",
125
+ "y'allll": "y'all'll",
126
+ "yall'd've": "y'all'd've",
127
+ "y'alld've": "y'all'd've",
128
+ "y'all'dve": "y'all'd've",
129
+ "youd": "you'd",
130
+ "youd've": "you'd've",
131
+ "you'dve": "you'd've",
132
+ "youll": "you'll",
133
+ "youre": "you're",
134
+ "youve": "you've",
135
+ }
136
+
137
+ NUMBER_MAP = {
138
+ "none": "0",
139
+ "zero": "0",
140
+ "one": "1",
141
+ "two": "2",
142
+ "three": "3",
143
+ "four": "4",
144
+ "five": "5",
145
+ "six": "6",
146
+ "seven": "7",
147
+ "eight": "8",
148
+ "nine": "9",
149
+ "ten": "10",
150
+ }
151
+ ARTICLES = ["a", "an", "the"]
152
+ PERIOD_STRIP = re.compile(r"(?!<=\d)(\.)(?!\d)")
153
+ COMMA_STRIP = re.compile(r"(?<=\d)(\,)+(?=\d)")
154
+ PUNCTUATIONS = [
155
+ ";",
156
+ r"/",
157
+ "[",
158
+ "]",
159
+ '"',
160
+ "{",
161
+ "}",
162
+ "(",
163
+ ")",
164
+ "=",
165
+ "+",
166
+ "\\",
167
+ "_",
168
+ "-",
169
+ ">",
170
+ "<",
171
+ "@",
172
+ "`",
173
+ ",",
174
+ "?",
175
+ "!",
176
+ ]
177
+
178
+ def __init__(self, *args, **kwargs):
179
+ pass
180
+
181
+ def word_tokenize(self, word):
182
+ word = word.lower()
183
+ word = word.replace(",", "").replace("?", "").replace("'s", " 's")
184
+ return word.strip()
185
+
186
+ def process_punctuation(self, in_text):
187
+ out_text = in_text
188
+ for p in self.PUNCTUATIONS:
189
+ if (p + " " in in_text or " " + p in in_text) or (
190
+ re.search(self.COMMA_STRIP, in_text) is not None
191
+ ):
192
+ out_text = out_text.replace(p, "")
193
+ else:
194
+ out_text = out_text.replace(p, " ")
195
+ out_text = self.PERIOD_STRIP.sub("", out_text, re.UNICODE)
196
+ return out_text
197
+
198
+ def process_digit_article(self, in_text):
199
+ out_text = []
200
+ temp_text = in_text.lower().split()
201
+ for word in temp_text:
202
+ word = self.NUMBER_MAP.setdefault(word, word)
203
+ if word not in self.ARTICLES:
204
+ out_text.append(word)
205
+ else:
206
+ pass
207
+ for word_id, word in enumerate(out_text):
208
+ if word in self.CONTRACTIONS:
209
+ out_text[word_id] = self.CONTRACTIONS[word]
210
+ out_text = " ".join(out_text)
211
+ return out_text
212
+
213
+ def __call__(self, item):
214
+ item = self.word_tokenize(item)
215
+ item = item.replace("\n", " ").replace("\t", " ").strip()
216
+ item = self.process_punctuation(item)
217
+ item = self.process_digit_article(item)
218
+ return item
219
+
220
+
221
+ class TextVQAAccuracyEvaluator:
222
+ def __init__(self):
223
+ self.answer_processor = EvalAIAnswerProcessor()
224
+
225
+ def _compute_answer_scores(self, raw_answers):
226
+ """
227
+ compute the accuracy (soft score) of human answers
228
+ """
229
+ answers = [self.answer_processor(a) for a in raw_answers]
230
+ #assert len(answers) == 10
231
+ gt_answers = list(enumerate(answers))
232
+ unique_answers = set(answers)
233
+ unique_answer_scores = {}
234
+
235
+ for unique_answer in unique_answers:
236
+ accs = []
237
+ for gt_answer in gt_answers:
238
+ other_answers = [item for item in gt_answers if item != gt_answer]
239
+ matching_answers = [
240
+ item for item in other_answers if item[1] == unique_answer
241
+ ]
242
+ acc = min(1, float(len(matching_answers)) / 3)
243
+ accs.append(acc)
244
+ unique_answer_scores[unique_answer] = sum(accs) / len(accs)
245
+
246
+ return unique_answer_scores
247
+
248
+ def eval_pred_list(self, pred_list):
249
+ pred_scores = []
250
+ for entry in tqdm(pred_list):
251
+ pred_answer = self.answer_processor(entry["pred_answer"])
252
+ unique_answer_scores = self._compute_answer_scores(entry["gt_answers"])
253
+ score = unique_answer_scores.get(pred_answer, 0.0)
254
+ pred_scores.append(score)
255
+
256
+ accuracy = sum(pred_scores) / len(pred_scores)
257
+ return accuracy
QWENVL-PRIVATE/analysis/merge_jsonl.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ # 1. Define your source files
4
+ jsonl_file_list = [
5
+ '/home/yuheng/datasets/visual_cot_infovqa_subset15k.jsonl',
6
+ '/home/yuheng/datasets/visual_cot_docvqa_subset33k.jsonl',
7
+ '/home/yuheng/datasets/visual_cot_textvqa_subset18k.jsonl',
8
+ '/home/yuheng/datasets/chartqa_28k_qa.jsonl'
9
+ ]
10
+
11
+ # 2. Define your destination
12
+ dst_jsonl_path = '/home/yuheng/datasets/visual_cot_textual_subset94k.jsonl'
13
+
14
+ print(f"Merging {len(jsonl_file_list)} files into {dst_jsonl_path}...")
15
+
16
+ # 3. Merging Logic
17
+ with open(dst_jsonl_path, 'w', encoding='utf-8') as outfile:
18
+ for file_path in jsonl_file_list:
19
+ print(f"Reading: {file_path}")
20
+ try:
21
+ with open(file_path, 'r', encoding='utf-8') as infile:
22
+ for line in infile:
23
+ # stripping whitespace and re-adding \n ensures we don't
24
+ # accidentally merge two JSON objects onto the same line
25
+ # if a file was missing an EOF newline.
26
+ clean_line = line.strip()
27
+ if clean_line:
28
+ outfile.write(clean_line + '\n')
29
+ except FileNotFoundError:
30
+ print(f"Warning: File not found {file_path}")
31
+
32
+ print("Merge complete.")
QWENVL-PRIVATE/analysis/merge_pkls.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pickle
2
+
3
+ #only contains dict in the form of {question_id: item_dict} in each pkl
4
+ pickle_file_list = [
5
+ '/root/epfs/sproject/datasets/stage3results/gated_vcot_docvqa33k_qwen2.5vl_3b.pkl',
6
+ '/root/epfs/sproject/datasets/stage3results/gated_chartqa28k_qwen2.5vl_3b.pkl',
7
+ '/root/epfs/sproject/datasets/stage3results/gated_vcot_llava68k_qwen2.5vl_3b.pkl'
8
+ ]
9
+
10
+ dst_pkl_path = '/root/epfs/sproject/datasets/qwen2.5vl_3b_stage3.pkl'
11
+
12
+ #pickle_file_list = [
13
+ # '/root/epfs/sproject/datasets/roi_vcot68k_ms.pkl',
14
+ # '/root/epfs/sproject/datasets/roi_vcot_docvqa33k_ms.pkl'
15
+ # ]
16
+ #
17
+ # dst_pkl_path = '/root/epfs/sproject/datasets/roi_vcot101k_ms.pkl'
18
+
19
+
20
+ merged_data = {}
21
+ cur_data_idx = 0
22
+ for pkl_path in pickle_file_list:
23
+ print(f"Loading data from {pkl_path}...")
24
+ try:
25
+ with open(pkl_path, 'rb') as f:
26
+ data = pickle.load(f)
27
+ if merged_data =={}:
28
+ merged_data = data
29
+ cur_data_idx = len(merged_data)
30
+ else:
31
+ for key, value in data.items():
32
+ if type(value['text']) is list:
33
+ value['text'] = value['text'][0]
34
+ if key in merged_data:
35
+ updated_key = cur_data_idx + key
36
+ merged_data[updated_key] = value
37
+ else:
38
+ merged_data[key] = value
39
+ cur_data_idx = len(merged_data)
40
+ except Exception as e:
41
+ print(f"Error loading pickle {pkl_path}: {e}")
42
+
43
+ print(f"Saving merged data to {dst_pkl_path}...")
44
+ try:
45
+ with open(dst_pkl_path, 'wb') as f:
46
+ pickle.dump(merged_data, f)
47
+ print("Merge and save completed successfully.")
48
+ except Exception as e:
49
+ print(f"Error saving merged pickle: {e}")
QWENVL-PRIVATE/analysis/merge_stage1_pkls.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import pickle
3
+ import argparse
4
+
5
+
6
+ def merge_pkls(pkl_list, dst_path, dst_pkl_name='llava_v1_5_pseudo_roi.pkl'):
7
+ """
8
+ Get the pkl files from the list and save them to a single pkl file.
9
+ """
10
+ all_results = []
11
+ for pkl_path in pkl_list:
12
+ tmp_pkl_content = pickle.load(open(pkl_path, "rb"))
13
+ all_results.extend(tmp_pkl_content)
14
+ os.makedirs(dst_path, exist_ok=True)
15
+ dst_pkl_path = os.path.join(dst_path, dst_pkl_name)
16
+ with open(dst_pkl_path, "wb") as f:
17
+ pickle.dump(all_results, f)
18
+ print(f"Saved {len(all_results)} results to {dst_pkl_path}")
19
+ return dst_pkl_path
20
+
21
+
22
+ if __name__ == "__main__":
23
+ parser = argparse.ArgumentParser(description="Merge stage1 pkl files")
24
+ parser.add_argument("--pkl-files", nargs="+", required=True,
25
+ help="List of pkl files to merge")
26
+ parser.add_argument("--dst-path", type=str, required=True,
27
+ help="Destination directory for merged pkl")
28
+ parser.add_argument("--dst-pkl-name", type=str, required=True,
29
+ help="Output pkl filename")
30
+ args = parser.parse_args()
31
+
32
+ merge_pkls(
33
+ pkl_list=args.pkl_files,
34
+ dst_path=args.dst_path,
35
+ dst_pkl_name=args.dst_pkl_name,
36
+ )
QWENVL-PRIVATE/analysis/plot_trade_off.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import matplotlib.pyplot as plt
2
+ import numpy as np
3
+
4
+ # --- 1. Data Definition ---
5
+ # Benchmarks and their sample counts
6
+ # Note: TextVQA uses 1250 samples as per user specification (val set subset or specific config)
7
+ bench_keys = ['DocVQA', 'InfoVQA', 'TextVQA', 'V*', 'RW-Lite']
8
+ samples = {
9
+ 'DocVQA': 5349,
10
+ 'InfoVQA': 701,
11
+ 'TextVQA': 1250,
12
+ 'V*': 191,
13
+ 'RW-Lite': 960
14
+ }
15
+
16
+ # Raw Data: {Label: {'accs': [Doc, Info, Text, V*, RW], 'times': [Doc, Info, Text, V*, RW]}}
17
+ # Data extracted from provided experimental results table
18
+
19
+ raw_data_baseline = {
20
+ 'Base\nMax-256': {
21
+ 'accs': [81.2, 54.0, 75.5, 53.4, 27.6],
22
+ 'times': [1330, 567, 938, 33, 292]
23
+ },
24
+ 'Base\nMax-576': {
25
+ 'accs': [92.0, 70.1, 81.1, 64.4, 35.4],
26
+ 'times': [1990, 904, 1567, 56, 532]
27
+ },
28
+ 'Base\nMax-1024': {
29
+ 'accs': [93.9, 76.5, 82.8, 70.2, 37.5],
30
+ 'times': [3066, 1423, 2471, 94, 896]
31
+ },
32
+ 'Base\nMax-2048': {
33
+ 'accs': [94.7, 80.4, 82.9, 72.3, 39.3],
34
+ 'times': [6053, 2047, 2600, 213, 1925]
35
+ }
36
+ }
37
+
38
+ raw_data_ours = {
39
+ 'Gated\nMax-256': {
40
+ 'accs': [89.1, 69.6, 81.8, 67.5, 37.3],
41
+ # Note: RW-Lite time estimated as 435s based on avg ~1.5x slowdown from baseline (292s)
42
+ 'times': [1919, 880, 1285, 53, 435]
43
+ },
44
+ 'Gated\nMax-576': {
45
+ 'accs': [93.8, 78.6, 82.8, 80.1, 43.4],
46
+ 'times': [2430, 1277, 1878, 85, 813]
47
+ },
48
+ 'Gated\nMax-1024': {
49
+ 'accs': [94.6, 80.8, 83.9, 82.2, 46.6],
50
+ 'times': [3481, 1801, 2677, 130, 1420]
51
+ }
52
+ }
53
+
54
+
55
+ # --- 2. Data Processing ---
56
+ def calculate_metrics(raw_data):
57
+ avg_speeds = []
58
+ avg_accs = []
59
+ labels = []
60
+
61
+ for label, data in raw_data.items():
62
+ accs = data['accs']
63
+ times = data['times']
64
+
65
+ # Calculate samples/second for each benchmark
66
+ task_speeds = []
67
+ for i, task in enumerate(bench_keys):
68
+ n_samples = samples[task]
69
+ t_seconds = times[i]
70
+ if t_seconds > 0:
71
+ task_speeds.append(n_samples / t_seconds)
72
+ else:
73
+ task_speeds.append(0)
74
+
75
+ # Weighted Average Speed (Average of speeds)
76
+ avg_speed = np.mean(task_speeds)
77
+
78
+ # Average Accuracy
79
+ avg_acc = np.mean(accs)
80
+
81
+ avg_speeds.append(avg_speed)
82
+ avg_accs.append(avg_acc)
83
+ labels.append(label)
84
+
85
+ return avg_speeds, avg_accs, labels
86
+
87
+
88
+ base_x, base_y, base_labels = calculate_metrics(raw_data_baseline)
89
+ ours_x, ours_y, ours_labels = calculate_metrics(raw_data_ours)
90
+
91
+ # --- 3. Plotting ---
92
+ plt.figure(figsize=(10, 7))
93
+
94
+ # Plot Baseline Curve
95
+ plt.plot(base_x, base_y, marker='o', linestyle='--', color='#7f8c8d', label='Baseline', linewidth=2, markersize=9,
96
+ markerfacecolor='white', markeredgewidth=2)
97
+
98
+ # Plot Ours Curve
99
+ plt.plot(ours_x, ours_y, marker='*', linestyle='-', color='#e74c3c', label='Ours-Gated', linewidth=2, markersize=14,
100
+ markerfacecolor='#e74c3c')
101
+
102
+ # Annotate Points (Baseline)
103
+ for x, y, label in zip(base_x, base_y, base_labels):
104
+ offset = (0, -20)
105
+ if '2048' in label:
106
+ offset = (0, 10)
107
+ ha_align = 'right'
108
+ else:
109
+ ha_align = 'center'
110
+
111
+ plt.annotate(label, (x, y), xytext=offset, textcoords='offset points',
112
+ ha=ha_align, fontsize=9, color='#555', weight='normal',
113
+ bbox=dict(boxstyle="round,pad=0.3", fc="white", ec="none", alpha=0.7))
114
+
115
+ # Annotate Points (Ours)
116
+ for x, y, label in zip(ours_x, ours_y, ours_labels):
117
+ offset = (0, 10)
118
+ if '1024' in label:
119
+ offset = (10, 0)
120
+ ha_align = 'left'
121
+ else:
122
+ ha_align = 'center'
123
+
124
+ plt.annotate(label, (x, y), xytext=offset, textcoords='offset points',
125
+ ha=ha_align, fontsize=9, fontweight='bold', color='#c0392b',
126
+ bbox=dict(boxstyle="round,pad=0.3", fc="white", ec="none", alpha=0.7))
127
+
128
+ # --- 4. Styling ---
129
+ plt.title('Accuracy vs. Efficiency Trade-off\n(Average across DocVQA, InfoVQA, TextVQA, V*, RW-Lite)', fontsize=14,
130
+ pad=15)
131
+ plt.xlabel('Average Throughput (Samples / Second) \n← Slower | Faster →', fontsize=12, fontweight='bold')
132
+ plt.ylabel('Average Accuracy (%)', fontsize=12, fontweight='bold')
133
+ plt.grid(True, linestyle=':', alpha=0.6)
134
+ plt.legend(fontsize=11, loc='lower right')
135
+
136
+ # Add Arrows for interpretation
137
+ plt.annotate('', xy=(max(base_x) * 0.95, min(base_y) * 1.02), xytext=(max(base_x) * 0.85, min(base_y) * 1.02),
138
+ arrowprops=dict(arrowstyle="->", color='green', lw=1.5))
139
+ plt.text(max(base_x) * 0.90, min(base_y) * 1.01, 'Higher Efficiency', color='green', ha='center', fontsize=9)
140
+
141
+ plt.annotate('', xy=(min(base_x) * 1.05, max(ours_y) * 0.98), xytext=(min(base_x) * 1.05, max(ours_y) * 0.90),
142
+ arrowprops=dict(arrowstyle="->", color='green', lw=1.5))
143
+ plt.text(min(base_x) * 1.06, max(ours_y) * 0.94, 'Higher Accuracy', color='green', rotation=90, va='center', fontsize=9)
144
+
145
+ plt.tight_layout()
146
+ plt.show()
147
+ # plt.savefig('acc_efficiency_tradeoff_updated.png', dpi=300)
148
+ # print("Plot saved as 'acc_efficiency_tradeoff_updated.png'")
QWENVL-PRIVATE/analysis/plot_vstar_tradeoff.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import matplotlib.pyplot as plt
2
+ import matplotlib as mpl
3
+ import numpy as np
4
+
5
+ # --- Aesthetic Setup (Apply principles from reference figures) ---
6
+ # Use a serif font similar to LaTeX/academic papers
7
+ mpl.rcParams['font.family'] = 'Times New Roman'
8
+ mpl.rcParams['mathtext.fontset'] = 'stix' # Ensures math text matches
9
+ mpl.rcParams['axes.labelweight'] = 'bold'
10
+ mpl.rcParams['axes.titleweight'] = 'bold'
11
+
12
+ # --- Data Extraction (same as before) ---
13
+ data = {
14
+ 'Qwen2.5-VL-3B': {
15
+ 'Baseline': {
16
+ 'perf': [47.1, 52.9, 59.2, 65.4, 68.1, 70.2, 71.2, 73.3],
17
+ 'throughput': [24.0, 17.4, 9.5, 5.7, 3.5, 2.2, 1.2, 0.8]
18
+ },
19
+ '+Ours': {
20
+ 'perf': [53.9, 63.9, 70.7, 75.4, 76.4, 79.1, 78.5, 81.2],
21
+ 'throughput': [16.1, 9.5, 5.2, 3.3, 2.0, 1.4, 1.0, 0.6]
22
+ }
23
+ },
24
+ 'Qwen2.5-VL-7B': {
25
+ 'Baseline': {
26
+ 'perf': [42.9, 53.4, 64.4, 70.2, 72.8, 72.3, 74.9, 78.0],
27
+ 'throughput': [21.1, 15.5, 9.1, 5.4, 3.4, 2.4, 1.4, 1.0]
28
+ },
29
+ '+Ours': {
30
+ 'perf': [59.7, 70.7, 77.5, 85.9, 86.4, 83.2, 89.5, 87.9],
31
+ 'throughput': [13.0, 8.6, 5.1, 3.0, 2.5, 2.1, 1.3, 0.9]
32
+ }
33
+ }
34
+ }
35
+ resolutions = [128, 256, 576, 1024, 1536, 2048, 3072, 4096]
36
+
37
+ # --- Plotting ---
38
+ fig, axs = plt.subplots(1, 2, figsize=(15, 5), sharey=False, dpi=300)
39
+
40
+ # NEW: Professional color palette (colorblind-friendly)
41
+ colors = {'Baseline': '#FF9900', '+Ours': '#00BFAE'}
42
+ markers = {'Baseline': 'o', '+Ours': 's'}
43
+ text_offsets = {'Baseline': (0, -2.5), '+Ours': (0, 2.5)} # Adjusted offsets for new fonts/sizes
44
+
45
+ model_names = ['Qwen2.5-VL-3B', 'Qwen2.5-VL-7B']
46
+
47
+ for i, model_name in enumerate(model_names):
48
+ ax = axs[i]
49
+ for method in ['Baseline', '+Ours']:
50
+ perf = data[model_name][method]['perf']
51
+ throughput = data[model_name][method]['throughput']
52
+
53
+ # MODIFIED: Added zorder, markeredgecolor, and other style params
54
+ ax.plot(throughput, perf, marker=markers[method], color=colors[method],
55
+ linestyle='--', label=method, markersize=9,
56
+ markeredgewidth=1.0, markeredgecolor=colors[method], # Add edge to markers
57
+ linewidth=2.0, zorder=3) # zorder places plot lines above grid
58
+
59
+ # Annotate points with their resolution
60
+ for j, res in enumerate(resolutions):
61
+ # Only annotate key resolutions to avoid clutter
62
+ if res in [128, 256, 576, 1024, 1536, 2048, 3072, 4096]:
63
+ ax.text(throughput[j], perf[j] + text_offsets[method][1], str(res),
64
+ fontsize=12, color=colors[method], ha='center', va='center', zorder=4, weight='bold')
65
+
66
+
67
+ # --- Major Aesthetic Overhaul for each subplot ---
68
+ ax.set_title(model_name, fontsize=18, pad=10)
69
+ ax.set_xscale('log')
70
+ from matplotlib.ticker import ScalarFormatter
71
+
72
+ ax.xaxis.set_major_formatter(ScalarFormatter())
73
+ ax.set_xticks([1, 2, 5, 10, 20])
74
+
75
+ ax.set_xlabel('Throughput (tokens/s)', fontsize=16)
76
+ if i == 0:
77
+ ax.set_ylabel('V* Performance', fontsize=16)
78
+
79
+ # NEW: Remove top and right spines for a cleaner look
80
+ ax.spines['top'].set_visible(False)
81
+ ax.spines['right'].set_visible(False)
82
+
83
+ # NEW: Make remaining spines and ticks thicker and black
84
+ ax.spines['left'].set_linewidth(1.5)
85
+ ax.spines['bottom'].set_linewidth(1.5)
86
+ ax.spines['left'].set_color('black')
87
+ ax.spines['bottom'].set_color('black')
88
+ ax.tick_params(width=1.5, colors='black', direction='out', length=6)
89
+
90
+ # NEW: Make grid more subtle
91
+ ax.grid(True, which="both", ls="--", linewidth=0.7, color='lightgray', zorder=0)
92
+
93
+ # MODIFIED: Make legend cleaner (no frame)
94
+ ax.legend(prop={'weight': 'bold', 'size': 14}, loc='best', frameon=False)
95
+ ax.tick_params(axis='both', which='major', labelsize=14)
96
+
97
+ # --- Final Touches ---
98
+ # fig.suptitle('Performance-Throughput Trade-off on V* Benchmark', fontsize=22, y=0.95)
99
+ plt.tight_layout(rect=[0, 0, 1, 0.96])
100
+ plt.savefig('performance_throughput_tradeoff_final.pdf', bbox_inches='tight')
101
+ plt.show()
QWENVL-PRIVATE/analysis/qwen3vl_debug.py ADDED
@@ -0,0 +1,410 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import torch
3
+ import numpy as np
4
+ import matplotlib.pyplot as plt
5
+ from transformers import AutoProcessor
6
+ from qwen_src.qwen3_vl import Qwen3VLForConditionalGeneration
7
+ import json
8
+ import os
9
+ from tqdm import tqdm
10
+ from PIL import Image, ImageDraw, ImageFont
11
+ from qwen_src.mm_utils import expand2square, create_pseudo_labels
12
+ from qwen_src.ana_utils import register_hooks, get_attn_weights_map, visualize_token_wise_attention
13
+ import matplotlib.patches as patches
14
+ from matplotlib.colors import ListedColormap, BoundaryNorm
15
+ from analysis.denoise_textual_file_wdet import process_single_sample_packed
16
+ PATCH_SIZE = 32 #32x32 patches For Qwen3-VL
17
+ import pickle
18
+ use_act = True
19
+ import math
20
+
21
+ def plot_pseudo_label(image_path, layer2_attn, grounding_attn_o2i, grounding_attn, is_sink_token = None,
22
+ save_path=None, figsize=(15, 4), fg_bbox = None):
23
+ """
24
+ Plot image and attention maps.
25
+
26
+ Args:
27
+ image_path: str, path to the image file
28
+ layer2_attn: ndarray (576,), layer 2 attention weights
29
+ grounding_attn_o2i: ndarray (576,), grounding attention object to image
30
+ grounding_attn: ndarray (576,), grounding attention
31
+ is_sink_token: ndarray (576,) in bool type to indicate whether this token is a sink token
32
+ save_path: str, optional path to save the plot
33
+ figsize: tuple, figure size
34
+ """
35
+ # Create figure with subplots
36
+ fig, axes = plt.subplots(1, 4, figsize=figsize, dpi=200)
37
+
38
+ # Plot 1: Original Image
39
+ if os.path.exists(image_path):
40
+ img = Image.open(image_path)
41
+ axes[0].imshow(img)
42
+ axes[0].set_title('Original Image')
43
+ axes[0].axis('off')
44
+ else:
45
+ axes[0].text(0.5, 0.5, f'Image not found:\n{image_path}',
46
+ ha='center', va='center', transform=axes[0].transAxes)
47
+ axes[0].set_title('Original Image')
48
+ axes[0].axis('off')
49
+
50
+ # Reshape attention arrays to 24x24
51
+ if layer2_attn.ndim == 1:
52
+ square_size_hw = int(math.sqrt(len(layer2_attn.flatten())))
53
+ square_size = (square_size_hw, square_size_hw)
54
+ else:
55
+ square_size = layer2_attn.shape
56
+ layer2_attn_2d = layer2_attn.reshape(square_size)
57
+ grounding_attn_o2i_2d = grounding_attn_o2i.reshape(square_size)
58
+ grounding_attn_2d = grounding_attn.reshape(square_size)
59
+ if is_sink_token is not None:
60
+ is_sink_token_2d = is_sink_token.reshape(square_size)
61
+
62
+
63
+ # Plot 2: Special handling for Pseudo-Label map vs. continuous attention
64
+ # Check if the data is likely discrete pseudo-labels (-1, 0, 1)
65
+ if -1 in layer2_attn:
66
+ # --- Discrete Colormap for Pseudo-Labels ---
67
+ axes[1].set_title('Pseudo Label Map')
68
+ # Define the colors: Ignore (-1)->Gray, Background (0)->Dark Purple, Foreground (1)->Yellow
69
+ colors = ['gray', 'black', '#FFE699']
70
+ cmap_discrete = ListedColormap(colors)
71
+ # Define the boundaries for the colormap: [-1.5, -0.5), [-0.5, 0.5), [0.5, 1.5]
72
+ boundaries = [-1.5, -0.5, 0.5, 1.5]
73
+ norm = BoundaryNorm(boundaries, cmap_discrete.N)
74
+ im1 = axes[1].imshow(layer2_attn_2d, cmap=cmap_discrete, norm=norm, aspect='auto')
75
+ # Customize the colorbar for discrete values
76
+ cbar = plt.colorbar(im1, ax=axes[1], shrink=0.6)
77
+ cbar.set_ticks([-1, 0, 1])
78
+ cbar.set_ticklabels(['Ignore', 'BG', 'FG'])
79
+ else:
80
+ # --- Continuous Colormap for standard attention ---
81
+ axes[1].set_title('Layer 2 Attention')
82
+ im1 = axes[1].imshow(layer2_attn_2d, cmap='viridis', aspect='auto')
83
+ plt.colorbar(im1, ax=axes[1], shrink=0.6)
84
+
85
+ # Plot 3: Grounding Attention O2I
86
+ im2 = axes[2].imshow(grounding_attn_o2i_2d, cmap='inferno', aspect='auto')
87
+ axes[2].set_title('Grounding Attn O2I')
88
+ axes[2].axis('off')
89
+ plt.colorbar(im2, ax=axes[2], shrink=0.6)
90
+
91
+ # Plot 4: Grounding Attention Q2I
92
+ im3 = axes[3].imshow(grounding_attn_2d, cmap='inferno', aspect='auto')
93
+ axes[3].set_title('Grounding Attn')
94
+ axes[3].axis('off')
95
+ plt.colorbar(im3, ax=axes[3], shrink=0.6)
96
+
97
+ # Highlight sink tokens on the attention maps
98
+ if is_sink_token is not None:
99
+ # Find the coordinates of all sink tokens
100
+ sink_coords = np.argwhere(is_sink_token_2d)
101
+ for row, col in sink_coords:
102
+ # Define the properties of the highlight box
103
+ rect_args = {
104
+ 'xy': (col - 0.5, row - 0.5), # Bottom-left corner of the cell
105
+ 'width': 1,
106
+ 'height': 1,
107
+ 'linewidth': 2.5,
108
+ 'edgecolor': '#7CFC00', # A bright, "LawnGreen" color
109
+ 'facecolor': 'none'
110
+ }
111
+ # Add a new rectangle patch to each of the three attention plots
112
+ #axes[1].add_patch(patches.Rectangle(**rect_args))
113
+ axes[2].add_patch(patches.Rectangle(**rect_args))
114
+ #axes[3].add_patch(patches.Rectangle(**rect_args))
115
+
116
+ if fg_bbox is not None:
117
+ fg_rect_arg = {
118
+ 'xy': (fg_bbox[1] - 0.5, fg_bbox[0] - 0.5), # Bottom-left corner of the cell
119
+ 'width': fg_bbox[3] - fg_bbox[1] + 1,
120
+ 'height': fg_bbox[2] - fg_bbox[0] + 1,
121
+ 'linewidth': 2.5,
122
+ 'edgecolor': '#FF4500', # A bright, "OrangeRed" color
123
+ 'facecolor': 'none'
124
+ }
125
+ axes[1].add_patch(patches.Rectangle(**fg_rect_arg))
126
+
127
+ plt.tight_layout()
128
+
129
+ if save_path:
130
+ plt.savefig(save_path, dpi=300, bbox_inches='tight')
131
+
132
+ plt.show()
133
+ pass
134
+
135
+ def eval_model_qwen(model, processor, questions, image_folder,
136
+ ocr_cache_file=None, is_natural_image=False, save_path=None, debug_samples=10):
137
+
138
+ sample = 0
139
+ # questions = [q for q in questions if os.path.exists(os.path.join(image_folder, q['image']))]
140
+
141
+ ocr_cache = {}
142
+ if ocr_cache_file and os.path.exists(ocr_cache_file):
143
+ print(f"Loading existing OCR cache from {ocr_cache_file}...")
144
+ try:
145
+ with open(ocr_cache_file, 'rb') as f:
146
+ ocr_cache = pickle.load(f)
147
+ print(f"Loaded {len(ocr_cache)} cached entries.")
148
+ except Exception as e:
149
+ print(f"Failed to load cache: {e}. Starting fresh.")
150
+
151
+
152
+ for line in tqdm(questions[sample:]):
153
+ if use_act: activations.clear()
154
+ image_file = line["image"]
155
+ qs = line["text"] if 'text' in line else line['prompt']
156
+
157
+ if is_natural_image:
158
+ qs = qs.replace('Answer the question using a single word or phrase.',
159
+ 'Output grounding bounding box related to the question in JSON.')
160
+ else:
161
+ if 'Answer the question using a single word or phrase.' not in qs:
162
+ qs += ' Answer the question using a single word or phrase.'
163
+
164
+ image = Image.open(os.path.join(image_folder, image_file)).convert('RGB')
165
+ processor.image_processor.max_pixels = 576 * PATCH_SIZE * PATCH_SIZE
166
+ processor.image_processor.min_pixels = 576 * PATCH_SIZE * PATCH_SIZE
167
+ image_size = image.size
168
+ image = expand2square(image, (127, 127, 127))
169
+ messages = [
170
+ {
171
+ "role": "user",
172
+ "content": [
173
+ {
174
+ "type": "text",
175
+ "text": f"{qs}"
176
+ },
177
+ # {
178
+ # "type": "image",
179
+ # "image": image#os.path.join(image_folder, image_file)
180
+ # }, # 测试多图 done!
181
+ {
182
+ "type": "image",
183
+ "image": image # os.path.join(image_folder, image_file)
184
+ }
185
+ ]
186
+ }
187
+ ]
188
+ # Preparation for inference
189
+ inputs = processor.apply_chat_template(
190
+ messages,
191
+ tokenize=True,
192
+ add_generation_prompt=True,
193
+ return_dict=True,
194
+ return_tensors="pt"
195
+ )
196
+ inputs = inputs.to(model.device)
197
+
198
+ # Inference: Generation of the output
199
+ generated_ids = model.generate(**inputs, max_new_tokens=128, output_attentions=True)
200
+ generated_ids_trimmed = [
201
+ out_ids[len(in_ids):] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
202
+ ]
203
+ output_text = processor.batch_decode(
204
+ generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
205
+ )
206
+ print("Question:", qs)
207
+ print(output_text)
208
+
209
+ if use_act and activations.get('visual_mask', None) is None:
210
+ vision_start_token_id = processor.tokenizer.convert_tokens_to_ids('<|vision_start|>')
211
+ vision_end_token_id = processor.tokenizer.convert_tokens_to_ids('<|vision_end|>')
212
+ pos = inputs['input_ids'].tolist()[0].index(vision_start_token_id) + 1
213
+ pos_end = inputs['input_ids'].tolist()[0].index(vision_end_token_id)
214
+ activations['visual_mask'] = torch.zeros((1, inputs['input_ids'].shape[1]), dtype=torch.bool, device=device)
215
+ activations['visual_mask'][0, pos:pos_end] = True
216
+ output_shape = inputs['image_grid_thw'].cpu().numpy().squeeze(0)[1:] / 2
217
+ output_shape = np.array(output_shape, dtype=np.int32)
218
+ attn_weights_reduction = 'mean' # 'none' or 'mean'
219
+ layers = model.config.text_config.num_hidden_layers
220
+ attn_weights_map = get_attn_weights_map(activations, layers=layers, reduction=attn_weights_reduction)
221
+
222
+
223
+ # visualize_token_wise_attention(attn_weights_map, qk_type='output2images', attn_size=output_shape, layers=layers, rgb_image=image,
224
+ # save_path=f'{save_path}/{sample}_layer_attn.png')
225
+ # ###4B: 19, 23 ,24
226
+ # ##textual image heads
227
+ # visualize_token_wise_attention(attn_weights_map, qk_type='output2images', avg_heads=False, rgb_image=image,
228
+ # layer_idx=13, attn_size=output_shape,layers=layers, save_path=f'{save_path}/{sample}_13_layer_attn.png')#head: 24, 27
229
+ # visualize_token_wise_attention(attn_weights_map, qk_type='output2images', avg_heads=False, rgb_image=image,
230
+ # layer_idx=14, attn_size=output_shape,layers=layers, save_path=f'{save_path}/{sample}_14_layer_attn.png')#head: 21, 23; sink_heads:6, 10
231
+ # ####natural image heads
232
+ # visualize_token_wise_attention(attn_weights_map, qk_type='output2images', avg_heads=False, rgb_image=image,
233
+ # layer_idx=14, attn_size=output_shape,layers=layers,save_path=f'{save_path}/{sample}_14_layer_attn.png')#head:
234
+ # visualize_token_wise_attention(attn_weights_map, qk_type='output2images', avg_heads=False, rgb_image=image,
235
+ # layer_idx=17, attn_size=output_shape,layers=layers,save_path=f'{save_path}/{sample}_17_layer_attn.png')#head
236
+ # visualize_token_wise_attention(attn_weights_map, qk_type='output2images', avg_heads=False, rgb_image=image,
237
+ # layer_idx=25, attn_size=output_shape, layers=layers) # head
238
+ ###8B
239
+ ###textual image heads
240
+ # visualize_token_wise_attention(attn_weights_map, qk_type='output2images', avg_heads=False, rgb_image=image,
241
+ # layer_idx=19, attn_size=output_shape,layers=layers)#head:
242
+ # visualize_token_wise_attention(attn_weights_map, qk_type='output2images', avg_heads=False, rgb_image=image,
243
+ # layer_idx=24, attn_size=output_shape,layers=layers)#head
244
+ # ####natural image heads
245
+ # visualize_token_wise_attention(attn_weights_map, qk_type='output2images', avg_heads=False, rgb_image=image,
246
+ # layer_idx=20, attn_size=output_shape,layers=layers)#head:
247
+ # visualize_token_wise_attention(attn_weights_map, qk_type='output2images', avg_heads=False, rgb_image=image,
248
+ # layer_idx=21, attn_size=output_shape,layers=layers)#head
249
+
250
+ required_heads = None
251
+
252
+ sink_heads = None
253
+
254
+ if "xxx" in args.model_path and use_act:
255
+ if is_natural_image: # gqa
256
+ required_heads = {14: [14, 15]}
257
+ sink_heads = {14: [3, 5]}
258
+ else: # textvqa
259
+ required_heads = {13: [4, 6, 7, 14, 15], 14:[6, 7, 14, 13, 15]}
260
+ sink_heads = {}
261
+
262
+ if "Qwen3-VL-4B" in args.model_path and use_act:
263
+ if is_natural_image: ##TODO: find heads for natural image
264
+ required_heads = {20: [15, 21, 23], 21: [8, 10, 11]}
265
+ sink_heads = {20: [1, 24]}
266
+ else:
267
+ ##textual image
268
+ required_heads = {19: [24, 27], 24: [21, 23]}
269
+ sink_heads = {24: [6, 10]}
270
+ required_layers = list(required_heads.keys())
271
+ elif "Qwen3-VL-8B" in args.model_path and use_act:
272
+ if is_natural_image: ##TODO: find heads for natural image
273
+ required_heads = {20: [15, 21, 23], 21: [8, 10, 11]}
274
+ sink_heads = {21: [7]}
275
+ else:
276
+ ##textual image
277
+ required_heads = {19: [24, 27], 24: [21, 23]}
278
+ sink_heads = {24: [1, 2]}
279
+ required_layers = list(required_heads.keys())
280
+
281
+ if required_heads is None:
282
+ print(f"[WARN] No predefined required_heads for model '{args.model_path}'. "
283
+ f"Only per-layer attention visualizations are saved. "
284
+ f"Check {save_path}/{sample}_layer_attn.png to identify grounding heads.")
285
+ sample += 1
286
+ if sample >= debug_samples:
287
+ print('Debug samples limit reached. Stopping evaluation.')
288
+ break
289
+ continue
290
+
291
+ grounding_attn_o2i = np.zeros(output_shape)
292
+ total_grounding_heads = sum([len(heads) for heads in required_heads.values()])
293
+ for layer, heads in required_heads.items():
294
+ attn_weights = attn_weights_map[f'layer{layer}']['output2images'].reshape(-1, output_shape[0], output_shape[1])
295
+ for head in heads:
296
+ grounding_attn_o2i += attn_weights[head]
297
+ grounding_attn_o2i = grounding_attn_o2i / total_grounding_heads
298
+
299
+ sink_attn = np.zeros(output_shape)
300
+ total_sink_heads = sum([len(heads) for heads in sink_heads.values()])
301
+ if total_sink_heads > 0:
302
+ for layer, heads in sink_heads.items():
303
+ attn_weights = attn_weights_map[f'layer{layer}']['output2images'].reshape(-1, output_shape[0], output_shape[1])
304
+ for head in heads:
305
+ sink_attn += attn_weights[head]
306
+ sink_attn = sink_attn / total_sink_heads
307
+
308
+ ##denoise with ocr boxes
309
+ img_path = os.path.join(image_folder, image_file)
310
+ # cached_boxes = ocr_cache.get(img_path, None)
311
+ # item = {
312
+ # 'image': img_path,
313
+ # 'grounding_attn_o2i': grounding_attn_o2i,
314
+ # }
315
+ # new_item, _, _ = process_single_sample_packed((item, None, cached_boxes))
316
+ # grounding_attn_o2i = new_item['grounding_attn_o2i']
317
+ if not is_natural_image:
318
+ cached_boxes = ocr_cache.get(img_path, None)
319
+ item = {
320
+ 'image': img_path,
321
+ 'grounding_attn_o2i': grounding_attn_o2i,
322
+ }
323
+ new_item, _, _ = process_single_sample_packed((item, None, cached_boxes))
324
+ grounding_attn_o2i = new_item['grounding_attn_o2i']
325
+
326
+ pseudo_set_denoised = create_pseudo_labels(sink_attn, grounding_attn_o2i, sink_thresh=0.02, bg_coff=0.05, binary_coff=0.25,
327
+ original_image_size=image_size, for_vis=True, mask_known_bg = True)
328
+ plot_pseudo_label(
329
+ image_path=img_path,
330
+ layer2_attn=pseudo_set_denoised['labels'],
331
+ grounding_attn_o2i=grounding_attn_o2i,
332
+ grounding_attn=pseudo_set_denoised['grounding_attn'],
333
+ is_sink_token=pseudo_set_denoised['sink_token_mask'],
334
+ fg_bbox=pseudo_set_denoised['fg_bbox'],
335
+ save_path=f'{save_path}/{sample}_pseudo_label.png',
336
+ )
337
+ sample += 1
338
+ if sample >= debug_samples:
339
+ print('Debug samples limit reached. Stopping evaluation.')
340
+ break
341
+ pass
342
+
343
+ import argparse
344
+
345
+ if __name__ == "__main__":
346
+ parser = argparse.ArgumentParser()
347
+ parser.add_argument("--model_name", type=str, required=True, help="model name")
348
+ parser.add_argument("--model_path", type=str, required=True, help="model path")
349
+ parser.add_argument(
350
+ "--dataset", type=str, required=True,
351
+ choices=["ocr_vqa", "DocVQA", "textvqa", "gqa"],
352
+ help="ocr_vqa, DocVQA, textvqa, gqa"
353
+ )
354
+ parser.add_argument("--save_path", type=str, required=True, help="save directory")
355
+
356
+ parser.add_argument("--questions_path", type=str, required=True, help="path to .jsonl/.json questions file")
357
+ parser.add_argument("--images_path", type=str, required=True, help="path to images root folder, not include images/")
358
+ parser.add_argument("--ocr_cache_file", type=str, default=None, help="path to ocr cache .pkl (optional)")
359
+
360
+ parser.add_argument("--debug_samples", type=int, default=100000, help="stop after N samples")
361
+ args = parser.parse_args()
362
+
363
+ device = "cuda"
364
+
365
+ model = Qwen3VLForConditionalGeneration.from_pretrained(
366
+ args.model_path,
367
+ torch_dtype=torch.bfloat16,
368
+ attn_implementation="eager",
369
+ ).eval().to(device)
370
+
371
+ processor = AutoProcessor.from_pretrained(
372
+ args.model_path,
373
+ trust_remote_code=True,
374
+ padding_side="left",
375
+ use_fast=True,
376
+ )
377
+
378
+ if use_act:
379
+ activations, hooks = register_hooks(model, is_qwen3vl=True)
380
+
381
+ is_natural_image = (args.dataset == "gqa")
382
+
383
+ if args.dataset == "textvqa":
384
+ args.images_path = args.images_path + "/textvqa/train_images"
385
+
386
+ print("args.images_path: ", args.images_path)
387
+ ocr_cache_file = os.path.expanduser(args.ocr_cache_file) if args.ocr_cache_file else None
388
+
389
+ questions = [json.loads(q) for q in open(os.path.expanduser(args.questions_path), "r")]
390
+ print('Total questions loaded:', len(questions))
391
+ questions = [q for q in questions if args.dataset in os.path.join(args.images_path, q["image"])]
392
+ print('Questions after filtering by target dataset:', len(questions))
393
+
394
+
395
+ eval_model_qwen(
396
+ model=model,
397
+ processor=processor,
398
+ questions=questions,
399
+ image_folder=args.images_path,
400
+ ocr_cache_file=ocr_cache_file,
401
+ is_natural_image=is_natural_image,
402
+ save_path=args.save_path,
403
+ debug_samples=args.debug_samples,
404
+ )
405
+
406
+ print(f"dataset: {args.dataset}")
407
+ print(f"questions_path: {args.questions_path}")
408
+ print(f"images_path: {args.images_path}")
409
+ print(f"ocr_cache_file: {ocr_cache_file}")
410
+ print(f"is_natural_image: {is_natural_image}")
QWENVL-PRIVATE/analysis/qwen3vl_debug_30b.py ADDED
@@ -0,0 +1,300 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Generate ROI pseudo label maps for Qwen3-VL-30B-A3B (MoE).
3
+ Same pipeline as qwen3vl_debug.py but adapted for:
4
+ 1. Qwen3VLMoeForConditionalGeneration (MoE architecture)
5
+ 2. device_map="auto" (58GB model, single 80GB GPU)
6
+ 3. 48 layers (vs 36 for 4B/8B)
7
+
8
+ Output: <save_path>/<sample_id>_pseudo_label.png (4-panel: image + pseudo + grounding_o2i + grounding)
9
+ """
10
+
11
+ import sys
12
+ import torch
13
+ import numpy as np
14
+ import matplotlib.pyplot as plt
15
+ from transformers import AutoProcessor, Qwen3VLMoeForConditionalGeneration
16
+ import json
17
+ import os
18
+ from tqdm import tqdm
19
+ from PIL import Image
20
+ import math
21
+ import pickle
22
+
23
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
24
+ from qwen_src.mm_utils import expand2square, create_pseudo_labels
25
+ from qwen_src.ana_utils import register_hooks, get_attn_weights_map
26
+
27
+ PATCH_SIZE = 32
28
+ use_act = True
29
+
30
+
31
+ def plot_pseudo_label(image_path, layer2_attn, grounding_attn_o2i, grounding_attn,
32
+ is_sink_token=None, save_path=None, figsize=(15, 4), fg_bbox=None):
33
+ """Same visualization as qwen3vl_debug.py"""
34
+ from matplotlib.colors import ListedColormap, BoundaryNorm
35
+ import matplotlib.patches as patches
36
+
37
+ fig, axes = plt.subplots(1, 4, figsize=figsize, dpi=200)
38
+
39
+ if os.path.exists(image_path):
40
+ img = Image.open(image_path)
41
+ axes[0].imshow(img)
42
+ axes[0].set_title('Original Image')
43
+ axes[0].axis('off')
44
+
45
+ if layer2_attn.ndim == 1:
46
+ sq = int(math.sqrt(len(layer2_attn.flatten())))
47
+ square_size = (sq, sq)
48
+ else:
49
+ square_size = layer2_attn.shape
50
+ layer2_attn_2d = layer2_attn.reshape(square_size)
51
+ grounding_attn_o2i_2d = grounding_attn_o2i.reshape(square_size)
52
+ grounding_attn_2d = grounding_attn.reshape(square_size)
53
+
54
+ if -1 in layer2_attn:
55
+ colors = ['gray', 'black', '#FFE699']
56
+ cmap_discrete = ListedColormap(colors)
57
+ boundaries = [-1.5, -0.5, 0.5, 1.5]
58
+ norm = BoundaryNorm(boundaries, cmap_discrete.N)
59
+ im1 = axes[1].imshow(layer2_attn_2d, cmap=cmap_discrete, norm=norm, aspect='auto')
60
+ axes[1].set_title('Pseudo Label Map')
61
+ cbar = plt.colorbar(im1, ax=axes[1], shrink=0.6)
62
+ cbar.set_ticks([-1, 0, 1])
63
+ cbar.set_ticklabels(['Ignore', 'BG', 'FG'])
64
+ else:
65
+ im1 = axes[1].imshow(layer2_attn_2d, cmap='viridis', aspect='auto')
66
+ axes[1].set_title('Attention')
67
+ plt.colorbar(im1, ax=axes[1], shrink=0.6)
68
+
69
+ im2 = axes[2].imshow(grounding_attn_o2i_2d, cmap='inferno', aspect='auto')
70
+ axes[2].set_title('Grounding Attn O2I')
71
+ axes[2].axis('off')
72
+ plt.colorbar(im2, ax=axes[2], shrink=0.6)
73
+
74
+ im3 = axes[3].imshow(grounding_attn_2d, cmap='inferno', aspect='auto')
75
+ axes[3].set_title('Grounding Attn')
76
+ axes[3].axis('off')
77
+ plt.colorbar(im3, ax=axes[3], shrink=0.6)
78
+
79
+ if is_sink_token is not None:
80
+ is_sink_token_2d = is_sink_token.reshape(square_size)
81
+ sink_coords = np.argwhere(is_sink_token_2d)
82
+ for row, col in sink_coords:
83
+ rect_args = {'xy': (col - 0.5, row - 0.5), 'width': 1, 'height': 1,
84
+ 'linewidth': 2.5, 'edgecolor': '#7CFC00', 'facecolor': 'none'}
85
+ axes[2].add_patch(patches.Rectangle(**rect_args))
86
+
87
+ if fg_bbox is not None:
88
+ fg_rect_arg = {'xy': (fg_bbox[1] - 0.5, fg_bbox[0] - 0.5),
89
+ 'width': fg_bbox[3] - fg_bbox[1] + 1,
90
+ 'height': fg_bbox[2] - fg_bbox[0] + 1,
91
+ 'linewidth': 2.5, 'edgecolor': '#FF4500', 'facecolor': 'none'}
92
+ axes[1].add_patch(patches.Rectangle(**fg_rect_arg))
93
+
94
+ plt.tight_layout()
95
+ if save_path:
96
+ plt.savefig(save_path, dpi=300, bbox_inches='tight')
97
+ plt.close()
98
+
99
+
100
+ def eval_model_30b(model, processor, questions, image_folder,
101
+ ocr_cache_file=None, is_natural_image=False, save_path=None, debug_samples=10):
102
+ sample = 0
103
+ ocr_cache = {}
104
+ if ocr_cache_file and os.path.exists(ocr_cache_file):
105
+ print(f"Loading OCR cache from {ocr_cache_file}...")
106
+ try:
107
+ ocr_cache = pickle.load(open(ocr_cache_file, 'rb'))
108
+ print(f"Loaded {len(ocr_cache)} cached entries.")
109
+ except Exception as e:
110
+ print(f"Failed to load cache: {e}")
111
+
112
+ device = next(model.parameters()).device # get actual device from model
113
+
114
+ for line in tqdm(questions[sample:]):
115
+ if use_act:
116
+ activations.clear()
117
+ image_file = line["image"]
118
+ qs = line["text"] if 'text' in line else line['prompt']
119
+
120
+ if is_natural_image:
121
+ qs = qs.replace('Answer the question using a single word or phrase.',
122
+ 'Output grounding bounding box related to the question in JSON.')
123
+ else:
124
+ if 'Answer the question using a single word or phrase.' not in qs:
125
+ qs += ' Answer the question using a single word or phrase.'
126
+
127
+ img_path = os.path.join(image_folder, image_file)
128
+ if not os.path.exists(img_path):
129
+ continue
130
+
131
+ image = Image.open(img_path).convert('RGB')
132
+ processor.image_processor.max_pixels = 576 * PATCH_SIZE * PATCH_SIZE
133
+ processor.image_processor.min_pixels = 576 * PATCH_SIZE * PATCH_SIZE
134
+ image_size = image.size
135
+ image = expand2square(image, (127, 127, 127))
136
+ messages = [{
137
+ "role": "user",
138
+ "content": [
139
+ {"type": "text", "text": f"{qs}"},
140
+ {"type": "image", "image": image}
141
+ ]
142
+ }]
143
+ inputs = processor.apply_chat_template(
144
+ messages, tokenize=True, add_generation_prompt=True,
145
+ return_dict=True, return_tensors="pt"
146
+ )
147
+ inputs = inputs.to(device)
148
+
149
+ with torch.no_grad():
150
+ generated_ids = model.generate(**inputs, max_new_tokens=128, output_attentions=True)
151
+ generated_ids_trimmed = [
152
+ out_ids[len(in_ids):] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
153
+ ]
154
+ output_text = processor.batch_decode(
155
+ generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
156
+ )
157
+ print(f"Q: {qs[:60]}...")
158
+ print(f"A: {output_text[0][:60]}...")
159
+
160
+ if use_act and activations.get('visual_mask', None) is None:
161
+ vision_start_id = processor.tokenizer.convert_tokens_to_ids('<|vision_start|>')
162
+ vision_end_id = processor.tokenizer.convert_tokens_to_ids('<|vision_end|>')
163
+ input_ids_list = inputs['input_ids'].tolist()[0]
164
+ pos = input_ids_list.index(vision_start_id) + 1
165
+ pos_end = input_ids_list.index(vision_end_id)
166
+ activations['visual_mask'] = torch.zeros(
167
+ (1, inputs['input_ids'].shape[1]), dtype=torch.bool, device=device)
168
+ activations['visual_mask'][0, pos:pos_end] = True
169
+
170
+ output_shape = inputs['image_grid_thw'].cpu().numpy().squeeze(0)[1:] / 2
171
+ output_shape = np.array(output_shape, dtype=np.int32)
172
+ layers = model.config.text_config.num_hidden_layers # 48 for 30B
173
+ attn_weights_map = get_attn_weights_map(activations, layers=layers, reduction='mean')
174
+
175
+ # For 30B, no predefined heads yet. Use all-layer average as initial exploration.
176
+ # The user will identify grounding heads from these maps.
177
+ # Use layers 60-80% depth as initial guess (layers 28-38 for 48 layers)
178
+ required_heads = None
179
+
180
+ # First pass: save per-layer attention for head identification
181
+ # Use all layers average for grounding attention
182
+ grounding_attn_o2i = np.zeros(output_shape)
183
+ count = 0
184
+ for layer_idx in range(layers):
185
+ key = f'layer{layer_idx}'
186
+ if key in attn_weights_map and 'output2images' in attn_weights_map[key]:
187
+ attn = attn_weights_map[key]['output2images']
188
+ try:
189
+ n_heads = attn.shape[0] // np.prod(output_shape)
190
+ if n_heads > 0:
191
+ attn_reshaped = attn.reshape(n_heads, *output_shape)
192
+ grounding_attn_o2i += attn_reshaped.mean(axis=0)
193
+ count += 1
194
+ except:
195
+ pass
196
+ if count > 0:
197
+ grounding_attn_o2i /= count
198
+
199
+ # Sink attention: use first few layers
200
+ sink_attn = np.zeros(output_shape)
201
+ sink_count = 0
202
+ for layer_idx in range(min(5, layers)):
203
+ key = f'layer{layer_idx}'
204
+ if key in attn_weights_map and 'output2images' in attn_weights_map[key]:
205
+ attn = attn_weights_map[key]['output2images']
206
+ try:
207
+ n_heads = attn.shape[0] // np.prod(output_shape)
208
+ if n_heads > 0:
209
+ attn_reshaped = attn.reshape(n_heads, *output_shape)
210
+ sink_attn += attn_reshaped.mean(axis=0)
211
+ sink_count += 1
212
+ except:
213
+ pass
214
+ if sink_count > 0:
215
+ sink_attn /= sink_count
216
+
217
+ # Denoise with OCR boxes (for textual images)
218
+ if not is_natural_image:
219
+ try:
220
+ from analysis.denoise_textual_file_wdet import process_single_sample_packed
221
+ cached_boxes = ocr_cache.get(img_path, None)
222
+ item = {'image': img_path, 'grounding_attn_o2i': grounding_attn_o2i}
223
+ new_item, _, _ = process_single_sample_packed((item, None, cached_boxes))
224
+ grounding_attn_o2i = new_item['grounding_attn_o2i']
225
+ except Exception as e:
226
+ print(f" Denoise skipped: {e}")
227
+
228
+ # Create pseudo labels
229
+ pseudo_set = create_pseudo_labels(
230
+ sink_attn, grounding_attn_o2i,
231
+ sink_thresh=0.02, bg_coff=0.05, binary_coff=0.25,
232
+ original_image_size=image_size, for_vis=True, mask_known_bg=True
233
+ )
234
+
235
+ plot_pseudo_label(
236
+ image_path=img_path,
237
+ layer2_attn=pseudo_set['labels'],
238
+ grounding_attn_o2i=grounding_attn_o2i,
239
+ grounding_attn=pseudo_set['grounding_attn'],
240
+ is_sink_token=pseudo_set['sink_token_mask'],
241
+ fg_bbox=pseudo_set['fg_bbox'],
242
+ save_path=f'{save_path}/{sample}_pseudo_label.png',
243
+ )
244
+ sample += 1
245
+ if sample >= debug_samples:
246
+ print('Debug samples limit reached.')
247
+ break
248
+
249
+
250
+ if __name__ == "__main__":
251
+ import argparse
252
+ parser = argparse.ArgumentParser()
253
+ parser.add_argument("--model_name", type=str, default="Qwen3-VL-30B-A3B-Instruct")
254
+ parser.add_argument("--model_path", type=str,
255
+ default="/mnt/bn/leonworkspace/terry/model/Qwen3-VL-30B-A3B-Instruct")
256
+ parser.add_argument("--dataset", type=str, default="gqa", choices=["gqa", "textvqa", "ocr_vqa"])
257
+ parser.add_argument("--save_path", type=str,
258
+ default="/mnt/bn/leonworkspace/terry/data/stage0/Qwen3-VL-30B-A3B-Instruct/gqa")
259
+ parser.add_argument("--questions_path", type=str,
260
+ default="/mnt/bn/leonworkspace/terry/data/llava_v1_5_mix665k_selected_qa.jsonl")
261
+ parser.add_argument("--images_path", type=str,
262
+ default="/mnt/bn/leonworkspace/terry/data/")
263
+ parser.add_argument("--ocr_cache_file", type=str, default=None)
264
+ parser.add_argument("--debug_samples", type=int, default=10)
265
+ args = parser.parse_args()
266
+
267
+ os.makedirs(args.save_path, exist_ok=True)
268
+ device = "cuda"
269
+
270
+ print(f"Loading {args.model_name} (MoE: 128 experts, 48 layers)...")
271
+ model = Qwen3VLMoeForConditionalGeneration.from_pretrained(
272
+ args.model_path,
273
+ torch_dtype=torch.bfloat16,
274
+ attn_implementation="eager",
275
+ device_map="auto",
276
+ ).eval()
277
+ print(f"Model loaded on device_map: {getattr(model, 'hf_device_map', 'unknown')}")
278
+
279
+ processor = AutoProcessor.from_pretrained(
280
+ args.model_path, trust_remote_code=True, padding_side="left", use_fast=True,
281
+ )
282
+
283
+ activations, hooks = register_hooks(model, is_qwen3vl=True)
284
+
285
+ is_natural_image = (args.dataset == "gqa")
286
+ if args.dataset == "textvqa":
287
+ args.images_path = args.images_path + "/textvqa/train_images"
288
+
289
+ questions = [json.loads(q) for q in open(args.questions_path)]
290
+ questions = [q for q in questions if args.dataset in os.path.join(args.images_path, q["image"])]
291
+ print(f"GQA questions: {len(questions)}")
292
+
293
+ eval_model_30b(
294
+ model=model, processor=processor, questions=questions,
295
+ image_folder=args.images_path, ocr_cache_file=args.ocr_cache_file,
296
+ is_natural_image=is_natural_image, save_path=args.save_path,
297
+ debug_samples=args.debug_samples,
298
+ )
299
+ for h in hooks:
300
+ h.remove()
QWENVL-PRIVATE/analysis/qwen3vl_debug_32b.py ADDED
@@ -0,0 +1,349 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Generate ROI pseudo label maps for Qwen3-VL-32B-Instruct (DENSE).
3
+
4
+ 32B is the dense variant (model_type=qwen3_vl), NOT MoE — so the pipeline
5
+ is fully aligned with 4B/8B dense (qwen3vl_debug.py). Only differences:
6
+ 1. 64 layers (vs 36 for 4B/8B, 48 for 30B MoE)
7
+ 2. 64 attention heads (vs varying for 4B/8B)
8
+ 3. hidden_size=5120 (vs 2560 for 4B, 3584 for 8B)
9
+ 4. ~64GB in bf16 → device_map="auto" required (won't fit on single 80GB GPU
10
+ when accounting for activations + KV cache during generation)
11
+ 5. DeepStack indexes [8, 16, 24] (same as 30B/8B)
12
+
13
+ Required grounding/sink heads for 32B are not yet identified — first pass
14
+ saves per-layer attention maps (set DEBUG_LAYER_VIS=1) so the user can
15
+ inspect and pick heads. Second pass uses the identified heads.
16
+
17
+ Output: <save_path>/<sample_id>_pseudo_label.png (4-panel figure)
18
+ """
19
+
20
+ import sys
21
+ import os
22
+ import math
23
+ import json
24
+ import pickle
25
+
26
+ import torch
27
+ import numpy as np
28
+ import matplotlib.pyplot as plt
29
+ from tqdm import tqdm
30
+ from PIL import Image
31
+ from transformers import AutoProcessor
32
+
33
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
34
+ from qwen_src.qwen3_vl import Qwen3VLForConditionalGeneration
35
+ from qwen_src.mm_utils import expand2square, create_pseudo_labels
36
+ from qwen_src.ana_utils import register_hooks, get_attn_weights_map, visualize_token_wise_attention
37
+
38
+ PATCH_SIZE = 32 # 32x32 patches for Qwen3-VL
39
+ use_act = True
40
+
41
+
42
+ def plot_pseudo_label(image_path, layer2_attn, grounding_attn_o2i, grounding_attn,
43
+ is_sink_token=None, save_path=None, figsize=(15, 4), fg_bbox=None):
44
+ """4-panel figure: original image | pseudo label | grounding o2i | grounding."""
45
+ from matplotlib.colors import ListedColormap, BoundaryNorm
46
+ import matplotlib.patches as patches
47
+
48
+ fig, axes = plt.subplots(1, 4, figsize=figsize, dpi=200)
49
+
50
+ if os.path.exists(image_path):
51
+ img = Image.open(image_path)
52
+ axes[0].imshow(img)
53
+ axes[0].set_title('Original Image')
54
+ axes[0].axis('off')
55
+
56
+ if layer2_attn.ndim == 1:
57
+ sq = int(math.sqrt(len(layer2_attn.flatten())))
58
+ square_size = (sq, sq)
59
+ else:
60
+ square_size = layer2_attn.shape
61
+ layer2_attn_2d = layer2_attn.reshape(square_size)
62
+ grounding_attn_o2i_2d = grounding_attn_o2i.reshape(square_size)
63
+ grounding_attn_2d = grounding_attn.reshape(square_size)
64
+
65
+ if -1 in layer2_attn:
66
+ colors = ['gray', 'black', '#FFE699']
67
+ cmap_discrete = ListedColormap(colors)
68
+ boundaries = [-1.5, -0.5, 0.5, 1.5]
69
+ norm = BoundaryNorm(boundaries, cmap_discrete.N)
70
+ im1 = axes[1].imshow(layer2_attn_2d, cmap=cmap_discrete, norm=norm, aspect='auto')
71
+ axes[1].set_title('Pseudo Label Map')
72
+ cbar = plt.colorbar(im1, ax=axes[1], shrink=0.6)
73
+ cbar.set_ticks([-1, 0, 1])
74
+ cbar.set_ticklabels(['Ignore', 'BG', 'FG'])
75
+ else:
76
+ im1 = axes[1].imshow(layer2_attn_2d, cmap='viridis', aspect='auto')
77
+ axes[1].set_title('Attention')
78
+ plt.colorbar(im1, ax=axes[1], shrink=0.6)
79
+
80
+ im2 = axes[2].imshow(grounding_attn_o2i_2d, cmap='inferno', aspect='auto')
81
+ axes[2].set_title('Grounding Attn O2I')
82
+ axes[2].axis('off')
83
+ plt.colorbar(im2, ax=axes[2], shrink=0.6)
84
+
85
+ im3 = axes[3].imshow(grounding_attn_2d, cmap='inferno', aspect='auto')
86
+ axes[3].set_title('Grounding Attn')
87
+ axes[3].axis('off')
88
+ plt.colorbar(im3, ax=axes[3], shrink=0.6)
89
+
90
+ if is_sink_token is not None:
91
+ is_sink_token_2d = is_sink_token.reshape(square_size)
92
+ sink_coords = np.argwhere(is_sink_token_2d)
93
+ for row, col in sink_coords:
94
+ rect_args = {'xy': (col - 0.5, row - 0.5), 'width': 1, 'height': 1,
95
+ 'linewidth': 2.5, 'edgecolor': '#7CFC00', 'facecolor': 'none'}
96
+ axes[2].add_patch(patches.Rectangle(**rect_args))
97
+
98
+ if fg_bbox is not None:
99
+ fg_rect_arg = {'xy': (fg_bbox[1] - 0.5, fg_bbox[0] - 0.5),
100
+ 'width': fg_bbox[3] - fg_bbox[1] + 1,
101
+ 'height': fg_bbox[2] - fg_bbox[0] + 1,
102
+ 'linewidth': 2.5, 'edgecolor': '#FF4500', 'facecolor': 'none'}
103
+ axes[1].add_patch(patches.Rectangle(**fg_rect_arg))
104
+
105
+ plt.tight_layout()
106
+ if save_path:
107
+ plt.savefig(save_path, dpi=300, bbox_inches='tight')
108
+ plt.close()
109
+
110
+
111
+ def eval_model_32b(model, processor, questions, image_folder,
112
+ ocr_cache_file=None, is_natural_image=False, save_path=None,
113
+ debug_samples=10, save_per_layer_vis=False):
114
+ sample = 0
115
+ ocr_cache = {}
116
+ if ocr_cache_file and os.path.exists(ocr_cache_file):
117
+ print(f"Loading OCR cache from {ocr_cache_file}...")
118
+ try:
119
+ ocr_cache = pickle.load(open(ocr_cache_file, 'rb'))
120
+ print(f"Loaded {len(ocr_cache)} cached entries.")
121
+ except Exception as e:
122
+ print(f"Failed to load cache: {e}")
123
+
124
+ # On multi-GPU device_map="auto", model.device may not exist. Use first param.
125
+ device = next(model.parameters()).device
126
+
127
+ for line in tqdm(questions[sample:]):
128
+ if use_act:
129
+ activations.clear()
130
+ image_file = line["image"]
131
+ qs = line["text"] if 'text' in line else line['prompt']
132
+
133
+ if is_natural_image:
134
+ qs = qs.replace('Answer the question using a single word or phrase.',
135
+ 'Output grounding bounding box related to the question in JSON.')
136
+ else:
137
+ if 'Answer the question using a single word or phrase.' not in qs:
138
+ qs += ' Answer the question using a single word or phrase.'
139
+
140
+ img_path = os.path.join(image_folder, image_file)
141
+ if not os.path.exists(img_path):
142
+ continue
143
+
144
+ image = Image.open(img_path).convert('RGB')
145
+ processor.image_processor.max_pixels = 576 * PATCH_SIZE * PATCH_SIZE
146
+ processor.image_processor.min_pixels = 576 * PATCH_SIZE * PATCH_SIZE
147
+ image_size = image.size
148
+ image = expand2square(image, (127, 127, 127))
149
+ messages = [{
150
+ "role": "user",
151
+ "content": [
152
+ {"type": "text", "text": f"{qs}"},
153
+ {"type": "image", "image": image}
154
+ ]
155
+ }]
156
+ inputs = processor.apply_chat_template(
157
+ messages, tokenize=True, add_generation_prompt=True,
158
+ return_dict=True, return_tensors="pt"
159
+ )
160
+ inputs = inputs.to(device)
161
+
162
+ with torch.no_grad():
163
+ generated_ids = model.generate(**inputs, max_new_tokens=128, output_attentions=True)
164
+ generated_ids_trimmed = [
165
+ out_ids[len(in_ids):] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
166
+ ]
167
+ output_text = processor.batch_decode(
168
+ generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
169
+ )
170
+ print(f"Q: {qs[:80]}")
171
+ print(f"A: {output_text[0][:80]}")
172
+
173
+ if use_act and activations.get('visual_mask', None) is None:
174
+ vision_start_id = processor.tokenizer.convert_tokens_to_ids('<|vision_start|>')
175
+ vision_end_id = processor.tokenizer.convert_tokens_to_ids('<|vision_end|>')
176
+ input_ids_list = inputs['input_ids'].tolist()[0]
177
+ pos = input_ids_list.index(vision_start_id) + 1
178
+ pos_end = input_ids_list.index(vision_end_id)
179
+ activations['visual_mask'] = torch.zeros(
180
+ (1, inputs['input_ids'].shape[1]), dtype=torch.bool, device=device)
181
+ activations['visual_mask'][0, pos:pos_end] = True
182
+
183
+ output_shape = inputs['image_grid_thw'].cpu().numpy().squeeze(0)[1:] / 2
184
+ output_shape = np.array(output_shape, dtype=np.int32)
185
+ layers = model.config.text_config.num_hidden_layers # 64 for 32B
186
+ attn_weights_map = get_attn_weights_map(activations, layers=layers, reduction='mean')
187
+
188
+ # Optional: dump per-layer attention maps for head identification.
189
+ # Set save_per_layer_vis=True (or env DEBUG_LAYER_VIS=1) on the first
190
+ # exploratory run, then update required_heads/sink_heads below from
191
+ # the visualizations and rerun with save_per_layer_vis=False.
192
+ if save_per_layer_vis:
193
+ visualize_token_wise_attention(
194
+ attn_weights_map, qk_type='output2images', attn_size=output_shape,
195
+ layers=layers, rgb_image=image,
196
+ save_path=f'{save_path}/{sample}_layer_attn.png',
197
+ )
198
+
199
+ # Required heads for 32B — TBD. Run with --save_per_layer_vis to identify
200
+ # the strongest grounding heads from the per-layer visualization, then
201
+ # populate the dicts below. For 4B/8B/30B-MoE these are hard-coded after
202
+ # exploration. For 32B (64 layers, 64 heads), use mean-of-mid-layers as
203
+ # a sensible default until proper heads are identified.
204
+ required_heads = None
205
+ sink_heads = None
206
+
207
+ if "Qwen3-VL-32B" in args.model_path and use_act:
208
+ if is_natural_image:
209
+ # TODO: identify natural-image grounding heads for 32B
210
+ # 32B mid-depth layers (~64% depth) are good candidates;
211
+ # use 38-40 as exploratory default until properly identified.
212
+ required_heads = {38: list(range(64)), 39: list(range(64)), 40: list(range(64))}
213
+ sink_heads = {}
214
+ else:
215
+ # TODO: identify textual grounding heads for 32B
216
+ # Mirror 8B structure (mid-late depth) as starting point;
217
+ # adjust after running with --save_per_layer_vis.
218
+ required_heads = {38: list(range(64)), 39: list(range(64)), 40: list(range(64))}
219
+ sink_heads = {}
220
+
221
+ if required_heads is None:
222
+ print(f"[WARN] No predefined required_heads for model '{args.model_path}'. "
223
+ f"Per-layer attention visualizations saved if --save_per_layer_vis. "
224
+ f"Inspect them at {save_path}/{sample}_layer_attn.png to pick grounding heads.")
225
+ sample += 1
226
+ if sample >= debug_samples:
227
+ print('Debug samples limit reached.')
228
+ break
229
+ continue
230
+
231
+ grounding_attn_o2i = np.zeros(output_shape)
232
+ total_grounding_heads = sum(len(h) for h in required_heads.values())
233
+ for layer, heads in required_heads.items():
234
+ attn = attn_weights_map[f'layer{layer}']['output2images'].reshape(-1, output_shape[0], output_shape[1])
235
+ for head in heads:
236
+ grounding_attn_o2i += attn[head]
237
+ grounding_attn_o2i = grounding_attn_o2i / total_grounding_heads
238
+
239
+ sink_attn = np.zeros(output_shape)
240
+ total_sink_heads = sum(len(h) for h in sink_heads.values()) if sink_heads else 0
241
+ if total_sink_heads > 0:
242
+ for layer, heads in sink_heads.items():
243
+ attn = attn_weights_map[f'layer{layer}']['output2images'].reshape(-1, output_shape[0], output_shape[1])
244
+ for head in heads:
245
+ sink_attn += attn[head]
246
+ sink_attn = sink_attn / total_sink_heads
247
+
248
+ # Denoise with OCR boxes for textual images
249
+ if not is_natural_image:
250
+ try:
251
+ from analysis.denoise_textual_file_wdet import process_single_sample_packed
252
+ cached_boxes = ocr_cache.get(img_path, None)
253
+ item = {'image': img_path, 'grounding_attn_o2i': grounding_attn_o2i}
254
+ new_item, _, _ = process_single_sample_packed((item, None, cached_boxes))
255
+ grounding_attn_o2i = new_item['grounding_attn_o2i']
256
+ except Exception as e:
257
+ print(f" Denoise skipped: {e}")
258
+
259
+ pseudo_set = create_pseudo_labels(
260
+ sink_attn, grounding_attn_o2i,
261
+ sink_thresh=0.02, bg_coff=0.05, binary_coff=0.25,
262
+ original_image_size=image_size, for_vis=True, mask_known_bg=True,
263
+ )
264
+ plot_pseudo_label(
265
+ image_path=img_path,
266
+ layer2_attn=pseudo_set['labels'],
267
+ grounding_attn_o2i=grounding_attn_o2i,
268
+ grounding_attn=pseudo_set['grounding_attn'],
269
+ is_sink_token=pseudo_set['sink_token_mask'],
270
+ fg_bbox=pseudo_set['fg_bbox'],
271
+ save_path=f'{save_path}/{sample}_pseudo_label.png',
272
+ )
273
+ sample += 1
274
+ if sample >= debug_samples:
275
+ print('Debug samples limit reached.')
276
+ break
277
+
278
+
279
+ if __name__ == "__main__":
280
+ import argparse
281
+
282
+ parser = argparse.ArgumentParser()
283
+ parser.add_argument("--model_name", type=str, default="Qwen3-VL-32B-Instruct")
284
+ parser.add_argument("--model_path", type=str,
285
+ default="/mnt/bn/leonworkspace/terry/model/Qwen3-VL-32B-Instruct")
286
+ parser.add_argument("--dataset", type=str, default="gqa",
287
+ choices=["ocr_vqa", "DocVQA", "textvqa", "gqa"])
288
+ parser.add_argument("--save_path", type=str,
289
+ default="/mnt/bn/leonworkspace/terry/data/stage0/Qwen3-VL-32B-Instruct/gqa")
290
+ parser.add_argument("--questions_path", type=str,
291
+ default="/mnt/bn/leonworkspace/terry/data/llava_v1_5_mix665k_selected_qa.jsonl")
292
+ parser.add_argument("--images_path", type=str,
293
+ default="/mnt/bn/leonworkspace/terry/data/")
294
+ parser.add_argument("--ocr_cache_file", type=str, default=None)
295
+ parser.add_argument("--debug_samples", type=int, default=10)
296
+ parser.add_argument("--save_per_layer_vis", action="store_true",
297
+ help="Save per-layer attention maps for head identification (slow).")
298
+ args = parser.parse_args()
299
+
300
+ os.makedirs(args.save_path, exist_ok=True)
301
+
302
+ print(f"Loading {args.model_name} (DENSE: 64 layers, hidden=5120, ~64GB bf16)...")
303
+ # device_map="auto" — 64GB model needs 2+ GPUs.
304
+ model = Qwen3VLForConditionalGeneration.from_pretrained(
305
+ args.model_path,
306
+ torch_dtype=torch.bfloat16,
307
+ attn_implementation="eager",
308
+ device_map="auto",
309
+ ).eval()
310
+ print(f"Model loaded on device_map: {getattr(model, 'hf_device_map', 'unknown')}")
311
+
312
+ processor = AutoProcessor.from_pretrained(
313
+ args.model_path, trust_remote_code=True, padding_side="left", use_fast=True,
314
+ )
315
+
316
+ if use_act:
317
+ activations, hooks = register_hooks(model, is_qwen3vl=True)
318
+
319
+ is_natural_image = (args.dataset == "gqa")
320
+ if args.dataset == "textvqa":
321
+ args.images_path = args.images_path + "/textvqa/train_images"
322
+
323
+ # Allow override via env var as well
324
+ save_per_layer_vis = args.save_per_layer_vis or os.environ.get("DEBUG_LAYER_VIS", "0") == "1"
325
+
326
+ print("args.images_path:", args.images_path)
327
+ ocr_cache_file = os.path.expanduser(args.ocr_cache_file) if args.ocr_cache_file else None
328
+
329
+ questions = [json.loads(q) for q in open(os.path.expanduser(args.questions_path), "r")]
330
+ print('Total questions loaded:', len(questions))
331
+ questions = [q for q in questions if args.dataset in os.path.join(args.images_path, q["image"])]
332
+ print('Questions after filtering by target dataset:', len(questions))
333
+
334
+ eval_model_32b(
335
+ model=model, processor=processor, questions=questions,
336
+ image_folder=args.images_path, ocr_cache_file=ocr_cache_file,
337
+ is_natural_image=is_natural_image, save_path=args.save_path,
338
+ debug_samples=args.debug_samples, save_per_layer_vis=save_per_layer_vis,
339
+ )
340
+
341
+ if use_act:
342
+ for h in hooks:
343
+ h.remove()
344
+
345
+ print(f"dataset: {args.dataset}")
346
+ print(f"questions_path: {args.questions_path}")
347
+ print(f"images_path: {args.images_path}")
348
+ print(f"save_path: {args.save_path}")
349
+ print(f"is_natural_image: {is_natural_image}")
QWENVL-PRIVATE/analysis/qwen3vl_identify_heads_30b.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Identify grounding/sink attention heads for Qwen3-VL-30B-A3B (MoE).
3
+ Mirrors qwen3vl_debug.py but:
4
+ 1. Uses Qwen3VLMoeForConditionalGeneration (MoE architecture)
5
+ 2. Uses device_map="auto" (58GB model on 80GB GPU)
6
+ 3. Generates per-layer attention maps for ALL 48 layers (head identification)
7
+ 4. Also generates per-head maps for selected layers
8
+
9
+ Output per sample:
10
+ <id>_layer_attn.png — all layers averaged over heads (identify which layers matter)
11
+ <id>_layer<L>_heads.png — all heads for layer L (identify which heads within a layer)
12
+
13
+ Usage:
14
+ CUDA_VISIBLE_DEVICES=0 python analysis/qwen3vl_identify_heads_30b.py \
15
+ --dataset gqa --debug_samples 10
16
+ """
17
+
18
+ import sys
19
+ import torch
20
+ import numpy as np
21
+ import matplotlib.pyplot as plt
22
+ from transformers import AutoProcessor, Qwen3VLMoeForConditionalGeneration
23
+ import json
24
+ import os
25
+ from tqdm import tqdm
26
+ from PIL import Image
27
+ import math
28
+ import pickle
29
+ import argparse
30
+
31
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
32
+ from qwen_src.mm_utils import expand2square
33
+ from qwen_src.ana_utils import register_hooks, get_attn_weights_map, visualize_token_wise_attention
34
+
35
+ PATCH_SIZE = 32
36
+ use_act = True
37
+
38
+
39
+ def main():
40
+ parser = argparse.ArgumentParser()
41
+ parser.add_argument("--model_name", type=str, default="Qwen3-VL-30B-A3B-Instruct")
42
+ parser.add_argument("--model_path", type=str,
43
+ default="/mnt/bn/leonworkspace/terry/model/Qwen3-VL-30B-A3B-Instruct")
44
+ parser.add_argument("--dataset", type=str, default="gqa", choices=["gqa", "textvqa", "ocr_vqa"])
45
+ parser.add_argument("--save_path", type=str, default=None)
46
+ parser.add_argument("--questions_path", type=str,
47
+ default="/mnt/bn/leonworkspace/terry/data/llava_v1_5_mix665k_selected_qa.jsonl")
48
+ parser.add_argument("--images_path", type=str,
49
+ default="/mnt/bn/leonworkspace/terry/data/")
50
+ parser.add_argument("--ocr_cache_file", type=str, default=None)
51
+ parser.add_argument("--debug_samples", type=int, default=10)
52
+ parser.add_argument("--inspect_layers", type=str, default=None,
53
+ help="Comma-separated layer indices for per-head visualization, e.g. '24,30,36'")
54
+ args = parser.parse_args()
55
+
56
+ if args.save_path is None:
57
+ args.save_path = f"/mnt/bn/leonworkspace/terry/data/stage0/{args.model_name}/{args.dataset}"
58
+ os.makedirs(args.save_path, exist_ok=True)
59
+
60
+ device = "cuda"
61
+ is_natural_image = (args.dataset == "gqa")
62
+
63
+ print(f"Loading {args.model_name} (MoE, 48 layers, 32 heads)...")
64
+ model = Qwen3VLMoeForConditionalGeneration.from_pretrained(
65
+ args.model_path,
66
+ torch_dtype=torch.bfloat16,
67
+ attn_implementation="eager",
68
+ device_map="auto",
69
+ ).eval()
70
+ print(f"Model loaded.")
71
+
72
+ processor = AutoProcessor.from_pretrained(
73
+ args.model_path, trust_remote_code=True, padding_side="left", use_fast=True,
74
+ )
75
+
76
+ activations, hooks = register_hooks(model, is_qwen3vl=True)
77
+
78
+ layers = model.config.text_config.num_hidden_layers # 48
79
+
80
+ # Parse inspect_layers
81
+ inspect_layers = None
82
+ if args.inspect_layers:
83
+ inspect_layers = [int(x) for x in args.inspect_layers.split(',')]
84
+ print(f"Will generate per-head maps for layers: {inspect_layers}")
85
+
86
+ # Adjust image path for textvqa
87
+ images_path = args.images_path
88
+ if args.dataset == "textvqa":
89
+ images_path = images_path + "/textvqa/train_images"
90
+
91
+ # Load questions
92
+ questions = [json.loads(q) for q in open(args.questions_path)]
93
+ questions = [q for q in questions if args.dataset in os.path.join(images_path, q["image"])]
94
+ print(f"{args.dataset} questions: {len(questions)}")
95
+
96
+ sample = 0
97
+ for line in tqdm(questions):
98
+ activations.clear()
99
+
100
+ image_file = line["image"]
101
+ qs = line["text"] if "text" in line else line["prompt"]
102
+
103
+ if is_natural_image:
104
+ qs = qs.replace('Answer the question using a single word or phrase.',
105
+ 'Output grounding bounding box related to the question in JSON.')
106
+ else:
107
+ if 'Answer the question using a single word or phrase.' not in qs:
108
+ qs += ' Answer the question using a single word or phrase.'
109
+
110
+ img_path = os.path.join(images_path, image_file)
111
+ if not os.path.exists(img_path):
112
+ continue
113
+
114
+ image = Image.open(img_path).convert('RGB')
115
+ processor.image_processor.max_pixels = 576 * PATCH_SIZE * PATCH_SIZE
116
+ processor.image_processor.min_pixels = 576 * PATCH_SIZE * PATCH_SIZE
117
+ image = expand2square(image, (127, 127, 127))
118
+
119
+ messages = [{
120
+ "role": "user",
121
+ "content": [
122
+ {"type": "text", "text": qs},
123
+ {"type": "image", "image": image},
124
+ ]
125
+ }]
126
+
127
+ inputs = processor.apply_chat_template(
128
+ messages, tokenize=True, add_generation_prompt=True,
129
+ return_dict=True, return_tensors="pt"
130
+ )
131
+ inputs = inputs.to(device)
132
+
133
+ with torch.no_grad():
134
+ generated_ids = model.generate(**inputs, max_new_tokens=128, output_attentions=True)
135
+
136
+ generated_ids_trimmed = [
137
+ out_ids[len(in_ids):] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
138
+ ]
139
+ answer = processor.batch_decode(
140
+ generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
141
+ )[0]
142
+ print(f" [{sample}] Q: {qs[:60]}...")
143
+ print(f" A: {answer[:60]}...")
144
+
145
+ # Build visual mask
146
+ if activations.get('visual_mask', None) is None:
147
+ vision_start_id = processor.tokenizer.convert_tokens_to_ids('<|vision_start|>')
148
+ vision_end_id = processor.tokenizer.convert_tokens_to_ids('<|vision_end|>')
149
+ input_ids_list = inputs['input_ids'].tolist()[0]
150
+ pos = input_ids_list.index(vision_start_id) + 1
151
+ pos_end = input_ids_list.index(vision_end_id)
152
+ activations['visual_mask'] = torch.zeros(
153
+ (1, inputs['input_ids'].shape[1]), dtype=torch.bool, device=device)
154
+ activations['visual_mask'][0, pos:pos_end] = True
155
+
156
+ output_shape = inputs['image_grid_thw'].cpu().numpy().squeeze(0)[1:] / 2
157
+ output_shape = np.array(output_shape, dtype=np.int32)
158
+
159
+ attn_weights_map = get_attn_weights_map(activations, layers=layers, reduction='mean')
160
+
161
+ # 1. Per-layer average attention map (all layers in one grid)
162
+ visualize_token_wise_attention(
163
+ attn_weights_map,
164
+ qk_type='output2images',
165
+ attn_size=output_shape,
166
+ layers=layers,
167
+ rgb_image=image,
168
+ save_path=f'{args.save_path}/{sample}_layer_attn.png'
169
+ )
170
+ print(f" Saved: {sample}_layer_attn.png")
171
+
172
+ # 2. Per-head attention for specified layers
173
+ if inspect_layers:
174
+ for layer_idx in inspect_layers:
175
+ if layer_idx >= layers:
176
+ continue
177
+ visualize_token_wise_attention(
178
+ attn_weights_map,
179
+ qk_type='output2images',
180
+ avg_heads=False,
181
+ rgb_image=image,
182
+ layer_idx=layer_idx,
183
+ attn_size=output_shape,
184
+ layers=layers,
185
+ save_path=f'{args.save_path}/{sample}_layer{layer_idx}_heads.png'
186
+ )
187
+ print(f" Saved: {sample}_layer{layer_idx}_heads.png")
188
+
189
+ sample += 1
190
+ if sample >= args.debug_samples:
191
+ print('Debug samples limit reached.')
192
+ break
193
+
194
+ for h in hooks:
195
+ h.remove()
196
+ print(f"\nDone! {sample} samples saved to {args.save_path}/")
197
+ print(f"Next step: inspect *_layer_attn.png to find grounding layers,")
198
+ print(f"then re-run with --inspect_layers <L1,L2,...> for per-head maps.")
199
+
200
+
201
+ if __name__ == "__main__":
202
+ main()
QWENVL-PRIVATE/analysis/qwen3vl_identify_heads_32b.py ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Identify grounding/sink attention heads for Qwen3-VL-32B-Instruct (DENSE).
3
+
4
+ 32B is the dense variant (model_type=qwen3_vl), NOT MoE — so the pipeline
5
+ mirrors qwen3vl_debug.py exactly (same code path as 4B/8B dense). Relative to
6
+ qwen3vl_identify_heads_30b.py:
7
+ 1. Uses Qwen3VLForConditionalGeneration (dense, not MoE)
8
+ 2. Uses device_map="auto" (64GB model — won't fit on single 80GB with
9
+ activations + KV cache, so shard across all visible GPUs)
10
+ 3. 64 hidden layers, 64 attention heads, 8 KV heads (GQA 8:1),
11
+ hidden_size=5120 — auto-detected from config
12
+ 4. Generates per-layer attention maps for ALL 64 layers
13
+ 5. Also generates per-head maps for selected layers via --inspect_layers
14
+
15
+ Output per sample:
16
+ <id>_layer_attn.png — all 64 layers averaged over heads (find grounding layers)
17
+ <id>_layer<L>_heads.png — all 64 heads for layer L (find grounding heads within)
18
+
19
+ Usage (single-GPU is fine — device_map=auto handles sharding if multiple visible):
20
+ CUDA_VISIBLE_DEVICES=0,1 python analysis/qwen3vl_identify_heads_32b.py \\
21
+ --dataset gqa --debug_samples 10
22
+
23
+ # After first pass, re-run with chosen layers for per-head maps:
24
+ CUDA_VISIBLE_DEVICES=0,1 python analysis/qwen3vl_identify_heads_32b.py \\
25
+ --dataset gqa --debug_samples 10 --inspect_layers 38,40,42,44
26
+
27
+ Next step after this: plug the identified grounding/sink heads into
28
+ qwen3vl_debug_32b.py (required_heads / sink_heads dicts).
29
+ """
30
+
31
+ import sys
32
+ import torch
33
+ import numpy as np
34
+ import matplotlib.pyplot as plt
35
+ import json
36
+ import os
37
+ from tqdm import tqdm
38
+ from PIL import Image
39
+ import argparse
40
+
41
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
42
+ from qwen_src.qwen3_vl import Qwen3VLForConditionalGeneration
43
+ from qwen_src.mm_utils import expand2square
44
+ from qwen_src.ana_utils import register_hooks, get_attn_weights_map, visualize_token_wise_attention
45
+ from transformers import AutoProcessor
46
+
47
+ PATCH_SIZE = 32
48
+ use_act = True
49
+
50
+
51
+ def main():
52
+ parser = argparse.ArgumentParser()
53
+ parser.add_argument("--model_name", type=str, default="Qwen3-VL-32B-Instruct")
54
+ parser.add_argument("--model_path", type=str,
55
+ default="/mnt/bn/leonworkspace/terry/model/Qwen3-VL-32B-Instruct")
56
+ parser.add_argument("--dataset", type=str, default="gqa",
57
+ choices=["gqa", "textvqa", "ocr_vqa", "gqa_cot", "textvqa_cot", "docvqa_cot"])
58
+ parser.add_argument("--save_path", type=str, default=None)
59
+ parser.add_argument("--questions_path", type=str,
60
+ default="/mnt/bn/leonworkspace/terry/data/llava_v1_5_mix665k_selected_qa.jsonl")
61
+ parser.add_argument("--images_path", type=str,
62
+ default="/mnt/bn/leonworkspace/terry/data/")
63
+ parser.add_argument("--ocr_cache_file", type=str, default=None)
64
+ parser.add_argument("--debug_samples", type=int, default=10)
65
+ parser.add_argument("--inspect_layers", type=str, default=None,
66
+ help="Comma-separated layer indices for per-head visualization, e.g. '38,40,42,44'")
67
+ args = parser.parse_args()
68
+
69
+ if args.save_path is None:
70
+ args.save_path = f"/mnt/bn/leonworkspace/terry/data/stage0/{args.model_name}/{args.dataset}"
71
+ os.makedirs(args.save_path, exist_ok=True)
72
+
73
+ device = "cuda"
74
+ is_natural_image = args.dataset in ("gqa", "gqa_cot")
75
+ is_visualcot = args.dataset.endswith("_cot")
76
+
77
+ print(f"Loading {args.model_name} (dense, 64 layers, 64 heads, GQA 8:1)...")
78
+ model = Qwen3VLForConditionalGeneration.from_pretrained(
79
+ args.model_path,
80
+ torch_dtype=torch.bfloat16,
81
+ attn_implementation="eager",
82
+ device_map="auto",
83
+ ).eval()
84
+ print("Model loaded.")
85
+
86
+ processor = AutoProcessor.from_pretrained(
87
+ args.model_path, trust_remote_code=True, padding_side="left", use_fast=True,
88
+ )
89
+
90
+ activations, hooks = register_hooks(model, is_qwen3vl=True)
91
+
92
+ layers = model.config.text_config.num_hidden_layers # 64
93
+ heads = model.config.text_config.num_attention_heads # 64
94
+ print(f"Config: {layers} layers, {heads} heads")
95
+
96
+ inspect_layers = None
97
+ if args.inspect_layers:
98
+ inspect_layers = [int(x) for x in args.inspect_layers.split(',')]
99
+ print(f"Will generate per-head maps for layers: {inspect_layers}")
100
+
101
+ images_path = args.images_path
102
+ if args.dataset == "textvqa" or args.dataset == "textvqa_cot":
103
+ images_path = images_path + "/textvqa/train_images"
104
+ elif args.dataset == "gqa_cot":
105
+ images_path = images_path + "/gqa/images"
106
+ elif args.dataset == "docvqa_cot":
107
+ images_path = images_path + "/DocVQA"
108
+
109
+ questions = [json.loads(q) for q in open(args.questions_path)]
110
+ # Visual-CoT jsonls are single-dataset → skip substring filter.
111
+ if not is_visualcot:
112
+ questions = [q for q in questions if args.dataset in os.path.join(images_path, q["image"])]
113
+ print(f"{args.dataset} questions: {len(questions)}")
114
+
115
+ sample = 0
116
+ for line in tqdm(questions):
117
+ activations.clear()
118
+
119
+ image_file = line["image"]
120
+ # Support LLaVA ("text"/"prompt") and Visual-CoT ("question") schemas.
121
+ qs = line.get("text") or line.get("question") or line.get("prompt") or ""
122
+
123
+ if is_natural_image:
124
+ # Always ensure the grounding-bbox instruction is present.
125
+ if 'Output grounding bounding box' not in qs:
126
+ if 'Answer the question using a single word or phrase.' in qs:
127
+ qs = qs.replace('Answer the question using a single word or phrase.',
128
+ 'Output grounding bounding box related to the question in JSON.')
129
+ else:
130
+ qs = qs.rstrip() + ' Output grounding bounding box related to the question in JSON.'
131
+ else:
132
+ if 'Answer the question using a single word or phrase.' not in qs:
133
+ qs = qs.rstrip() + ' Answer the question using a single word or phrase.'
134
+
135
+ img_path = os.path.join(images_path, image_file)
136
+ if not os.path.exists(img_path):
137
+ continue
138
+
139
+ image = Image.open(img_path).convert('RGB')
140
+ processor.image_processor.max_pixels = 576 * PATCH_SIZE * PATCH_SIZE
141
+ processor.image_processor.min_pixels = 576 * PATCH_SIZE * PATCH_SIZE
142
+ image = expand2square(image, (127, 127, 127))
143
+
144
+ messages = [{
145
+ "role": "user",
146
+ "content": [
147
+ {"type": "text", "text": qs},
148
+ {"type": "image", "image": image},
149
+ ]
150
+ }]
151
+
152
+ inputs = processor.apply_chat_template(
153
+ messages, tokenize=True, add_generation_prompt=True,
154
+ return_dict=True, return_tensors="pt"
155
+ )
156
+ inputs = inputs.to(device)
157
+
158
+ with torch.no_grad():
159
+ generated_ids = model.generate(**inputs, max_new_tokens=128, output_attentions=True)
160
+
161
+ generated_ids_trimmed = [
162
+ out_ids[len(in_ids):] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
163
+ ]
164
+ answer = processor.batch_decode(
165
+ generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
166
+ )[0]
167
+ print(f" [{sample}] Q: {qs[:60]}...")
168
+ print(f" A: {answer[:60]}...")
169
+
170
+ # Build visual mask from <|vision_start|> ... <|vision_end|>
171
+ if activations.get('visual_mask', None) is None:
172
+ vision_start_id = processor.tokenizer.convert_tokens_to_ids('<|vision_start|>')
173
+ vision_end_id = processor.tokenizer.convert_tokens_to_ids('<|vision_end|>')
174
+ input_ids_list = inputs['input_ids'].tolist()[0]
175
+ pos = input_ids_list.index(vision_start_id) + 1
176
+ pos_end = input_ids_list.index(vision_end_id)
177
+ activations['visual_mask'] = torch.zeros(
178
+ (1, inputs['input_ids'].shape[1]), dtype=torch.bool, device=device)
179
+ activations['visual_mask'][0, pos:pos_end] = True
180
+
181
+ output_shape = inputs['image_grid_thw'].cpu().numpy().squeeze(0)[1:] / 2
182
+ output_shape = np.array(output_shape, dtype=np.int32)
183
+
184
+ attn_weights_map = get_attn_weights_map(activations, layers=layers, reduction='mean')
185
+
186
+ # Build a title_prefix from the question (truncated for readability on the figure).
187
+ qs_title = qs.replace("\n", " ").strip()
188
+ if len(qs_title) > 120:
189
+ qs_title = qs_title[:117] + "..."
190
+ title_prefix = f"[s{sample}] Q: {qs_title}\n"
191
+
192
+ # 1. Per-layer average attention map (all 64 layers in one grid)
193
+ visualize_token_wise_attention(
194
+ attn_weights_map,
195
+ qk_type='output2images',
196
+ attn_size=output_shape,
197
+ layers=layers,
198
+ rgb_image=image,
199
+ title_prefix=title_prefix,
200
+ save_path=f'{args.save_path}/{sample}_layer_attn.png'
201
+ )
202
+ print(f" Saved: {sample}_layer_attn.png")
203
+
204
+ # 2. Per-head attention for specified layers (all 64 heads for each)
205
+ if inspect_layers:
206
+ for layer_idx in inspect_layers:
207
+ if layer_idx >= layers:
208
+ print(f" [WARN] layer {layer_idx} >= {layers}, skipping")
209
+ continue
210
+ visualize_token_wise_attention(
211
+ attn_weights_map,
212
+ qk_type='output2images',
213
+ avg_heads=False,
214
+ rgb_image=image,
215
+ layer_idx=layer_idx,
216
+ attn_size=output_shape,
217
+ layers=layers,
218
+ title_prefix=title_prefix,
219
+ save_path=f'{args.save_path}/{sample}_layer{layer_idx}_heads.png'
220
+ )
221
+ print(f" Saved: {sample}_layer{layer_idx}_heads.png")
222
+
223
+ sample += 1
224
+ if sample >= args.debug_samples:
225
+ print('Debug samples limit reached.')
226
+ break
227
+
228
+ for h in hooks:
229
+ h.remove()
230
+ print(f"\nDone! {sample} samples saved to {args.save_path}/")
231
+ print(f"Next: inspect *_layer_attn.png for grounding layers in 64-layer stack,")
232
+ print(f"then re-run with --inspect_layers <L1,L2,...> for per-head maps,")
233
+ print(f"finally update required_heads/sink_heads in qwen3vl_debug_32b.py.")
234
+
235
+
236
+ if __name__ == "__main__":
237
+ main()
QWENVL-PRIVATE/analysis/strip_image_root_inplace.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+
3
+ SRC = "/home/yuheng/datasets/llava_v1_5_mix665k_selected_qa.jsonl"
4
+ DATA_ROOT = "/home/yuheng/datasets/"
5
+
6
+
7
+ def main():
8
+ rows = []
9
+ with open(SRC, "r", encoding="utf-8") as f:
10
+ for line in f:
11
+ if not line.strip():
12
+ continue
13
+ obj = json.loads(line)
14
+ img = obj.get("image", "")
15
+ if img.startswith(DATA_ROOT):
16
+ obj["image"] = img[len(DATA_ROOT) :]
17
+ rows.append(obj)
18
+
19
+ with open(SRC, "w", encoding="utf-8") as f:
20
+ for obj in rows:
21
+ f.write(json.dumps(obj, ensure_ascii=False) + "\n")
22
+
23
+
24
+ if __name__ == "__main__":
25
+ main()
QWENVL-PRIVATE/analysis/tmp_plot.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import matplotlib.pyplot as plt
2
+ import numpy as np
3
+
4
+ # Benchmarks included: DocVQA, ChartQA, OCRBench, InfoVQA, TextVQA, V*, POPE
5
+ models = ['Baseline-576', '+Ours (Full)', '+Ours-Gate', 'Baseline-1024']
6
+ colors = ['#95a5a6', '#e74c3c', '#2ecc71', '#3498db'] # Grey, Red, Green, Blue
7
+ markers = ['o', '^', '*', 's']
8
+
9
+ # Data Structure: {Benchmark: (Score_List, Time_List)}
10
+ # Order matches 'models' list
11
+ data = {
12
+ 'DocVQA': ([92.0, 93.6, 93.4, 93.9], [586, 806, 722, 799]),
13
+ 'ChartQA': ([82.6, 85.6, 83.9, 84.4], [227, 396, 266, 355]),
14
+ 'OCRBench': ([81.5, 84.1, 83.5, 83.1], [125, 193, 155, 172]),
15
+ 'InfoVQA': ([70.2, 78.7, 77.2, 76.6], [251, 454, 355, 375]),
16
+ 'TextVQA': ([81.1, 83.1, 82.7, 82.8], [436, 806, 506, 678]),
17
+ 'V*': ([64.4, 76.4, 77.5, 70.2], [15, 29, 26, 24]),
18
+ 'POPE': ([86.7, 88.6, 87.4, 86.7], [675, 1299, 788, 1115])
19
+ }
20
+
21
+ # Lists to store aggregated data
22
+ avg_abs_scores = []
23
+ avg_rel_times = []
24
+
25
+ # Calculate Averages for each model
26
+ num_models = len(models)
27
+ for i in range(num_models):
28
+ total_score = 0
29
+ total_rel_time = 0
30
+ count = 0
31
+
32
+ for bench, (scores, times) in data.items():
33
+ # Sum absolute scores
34
+ total_score += scores[i]
35
+
36
+ # Sum relative time (Current Time / Baseline-576 Time)
37
+ base_time = times[0]
38
+ total_rel_time += times[i] / base_time
39
+
40
+ count += 1
41
+
42
+ avg_abs_scores.append(total_score / count)
43
+ avg_rel_times.append(total_rel_time / count)
44
+
45
+ # Create Plot
46
+ plt.figure(figsize=(11, 8))
47
+ plt.grid(True, linestyle='--', alpha=0.6)
48
+
49
+ # Plot Points
50
+ for i, model in enumerate(models):
51
+ plt.scatter(avg_rel_times[i], avg_abs_scores[i],
52
+ color=colors[i], s=250, label=model, marker=markers[i], zorder=10, edgecolors='k')
53
+
54
+ # Dynamic annotation placement to avoid overlap
55
+ xytext_offset = (0, 15)
56
+ if 'Baseline-1024' in model:
57
+ xytext_offset = (0, -25) # Move label down
58
+
59
+ plt.annotate(f"{model}\n({avg_rel_times[i]:.2f}x Time, {avg_abs_scores[i]:.1f}% Acc)",
60
+ (avg_rel_times[i], avg_abs_scores[i]),
61
+ xytext=xytext_offset, textcoords='offset points', ha='center',
62
+ fontsize=10, fontweight='bold',
63
+ bbox=dict(boxstyle="round,pad=0.3", fc="white", alpha=0.7, ec="none"))
64
+
65
+ # Axis Limits (optional, for aesthetics)
66
+ plt.xlim(0.9, 1.8)
67
+ plt.ylim(78, 86) # Adjust based on data range to zoom in on differences
68
+
69
+ # Axis Labels
70
+ plt.xlabel('Relative Time Cost (Normalized to Baseline-576)\n← Faster | Slower →', fontsize=12, fontweight='bold')
71
+ plt.ylabel('Average Absolute Accuracy (%)\nHigher is Better ↑', fontsize=12, fontweight='bold')
72
+ plt.title('Performance vs. Efficiency Trade-off\n(Average across 7 Benchmarks)', fontsize=14, pad=15)
73
+
74
+ # Reference Lines
75
+ # Vertical line at Baseline-576 time (1.0)
76
+ plt.axvline(x=1.0, color='#95a5a6', linestyle=':', linewidth=2, label='Baseline Speed')
77
+
78
+ # Legend
79
+ plt.legend(loc='lower right', fontsize=11, frameon=True, framealpha=0.9, shadow=True)
80
+
81
+ plt.tight_layout()
82
+ plt.show()
83
+ # plt.savefig('trade_off_plot_abs.png')
84
+ # print("Plot saved as 'trade_off_plot_abs.png'")
QWENVL-PRIVATE/cookbooks/assets/agent_function_call/mobile_en_example.png ADDED

Git LFS Details

  • SHA256: 2595c50aed60e9d27e96f47629caea17b2d7d36c756af7ed47a3f5ae5c3c0cbd
  • Pointer size: 131 Bytes
  • Size of remote file: 574 kB
QWENVL-PRIVATE/cookbooks/assets/agent_function_call/mobile_zh_example.jpg ADDED

Git LFS Details

  • SHA256: 831ee086eb48550bb2c9419f3d1fe4716012b752ad4027fdd694475642e0fc85
  • Pointer size: 131 Bytes
  • Size of remote file: 230 kB
QWENVL-PRIVATE/cookbooks/assets/computer_use/computer_use1.jpeg ADDED

Git LFS Details

  • SHA256: 348de316a8038e1374927670ba8c320af4abbf40a9f152ef756882548f95420a
  • Pointer size: 131 Bytes
  • Size of remote file: 770 kB
QWENVL-PRIVATE/cookbooks/assets/computer_use/computer_use2.jpeg ADDED

Git LFS Details

  • SHA256: ac7619eba480998b9da1e47eebd78d341aed5940b9979f28a1bfdaec50c33915
  • Pointer size: 131 Bytes
  • Size of remote file: 290 kB
QWENVL-PRIVATE/cookbooks/assets/document_parsing/docparsing_example1.jpg ADDED

Git LFS Details

  • SHA256: a5832a6241d2eb6df0adc95f633c0aeab593f2a29a755d742fc4db3b4bccef4e
  • Pointer size: 131 Bytes
  • Size of remote file: 977 kB
QWENVL-PRIVATE/cookbooks/assets/document_parsing/docparsing_example2.jpg ADDED

Git LFS Details

  • SHA256: f60221f9acee04a7f8cb53dd350744054f46ab7e5e44d0fa053512146526c16b
  • Pointer size: 131 Bytes
  • Size of remote file: 413 kB
QWENVL-PRIVATE/cookbooks/assets/document_parsing/docparsing_example3.jpg ADDED

Git LFS Details

  • SHA256: e3793e56a15b854a193f73b909c2aa5e86a5b9bbb17877a85d6cf63604af08ef
  • Pointer size: 131 Bytes
  • Size of remote file: 546 kB
QWENVL-PRIVATE/cookbooks/assets/document_parsing/docparsing_example4.jpg ADDED

Git LFS Details

  • SHA256: 9a0edd644f858d13e2aa457aac3574bb804454d157d30b33cb26c9823f456f5e
  • Pointer size: 131 Bytes
  • Size of remote file: 454 kB
QWENVL-PRIVATE/cookbooks/assets/document_parsing/docparsing_example5.png ADDED

Git LFS Details

  • SHA256: b1945329896be56970fbb11e8c1b9e62a5bc01a263cbe8bc85a780b424ba0a4c
  • Pointer size: 131 Bytes
  • Size of remote file: 562 kB
QWENVL-PRIVATE/cookbooks/assets/document_parsing/docparsing_example6.png ADDED

Git LFS Details

  • SHA256: 8166d1df34d070ab1378bdb34fecadfc4b08e46f2d80af2f4e8b1c5f98ae9dd4
  • Pointer size: 131 Bytes
  • Size of remote file: 453 kB
QWENVL-PRIVATE/cookbooks/assets/document_parsing/docparsing_example7.jpg ADDED

Git LFS Details

  • SHA256: 02a60d2068db310fa6b7bb9004d3da26eae9cc7260aab8dc02df625d3173e62e
  • Pointer size: 131 Bytes
  • Size of remote file: 211 kB