npario Soul-Code commited on
Commit
eaa92e0
·
0 Parent(s):

Duplicate from SoMarkAI/OXR-1.0

Browse files

Co-authored-by: SoulCode <Soul-Code@users.noreply.huggingface.co>

.eval_results/olmocrbench.yaml ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ - dataset:
2
+ id: allenai/olmOCR-bench
3
+ task_id: overall
4
+ value: 87.84
5
+
6
+ - dataset:
7
+ id: allenai/olmOCR-bench
8
+ task_id: arxiv_math
9
+ value: 92.76
10
+
11
+ - dataset:
12
+ id: allenai/olmOCR-bench
13
+ task_id: baseline
14
+ value: 97.35
15
+
16
+ - dataset:
17
+ id: allenai/olmOCR-bench
18
+ task_id: headers_footers
19
+ value: 94.34
20
+
21
+ - dataset:
22
+ id: allenai/olmOCR-bench
23
+ task_id: long_tiny_text
24
+ value: 90.95
25
+
26
+ - dataset:
27
+ id: allenai/olmOCR-bench
28
+ task_id: multi_column
29
+ value: 85.18
30
+
31
+ - dataset:
32
+ id: allenai/olmOCR-bench
33
+ task_id: old_scans
34
+ value: 59.32
35
+
36
+ - dataset:
37
+ id: allenai/olmOCR-bench
38
+ task_id: old_scans_math
39
+ value: 89.74
40
+
41
+ - dataset:
42
+ id: allenai/olmOCR-bench
43
+ task_id: table_tests
44
+ value: 93.05
.gitattributes ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
2
+ *.pth filter=lfs diff=lfs merge=lfs -text
3
+ tokenizer.json filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ignore non-release weights; release checkpoints are tracked with Git LFS.
2
+ *.safetensors
3
+ *.bin
4
+ *.pt
5
+ *.pth
6
+ *.ckpt
7
+ *.gguf
8
+ *.onnx
9
+
10
+ # Weight index — references .safetensors, meaningless without them
11
+ model.safetensors.index.json
12
+
13
+ !model.safetensors
14
+ !layout_analysis/last.pth
15
+
16
+ # Training artifacts (not part of the modeling spec)
17
+ trainer_state.json
18
+ training_args.bin
19
+ zero_to_fp32.py
20
+ optimizer.pt
21
+ scheduler.pt
22
+ rng_state.pth
23
+
24
+ # Misc
25
+ *.bak
26
+ .DS_Store
27
+ __pycache__/
28
+ *.pyc
29
+ *.egg-info/
30
+ build/
31
+ dist/
32
+ .pytest_cache/
33
+ .venv/
34
+
35
+ # Local coding-assistant instructions are not model-release assets.
36
+ [Aa][Gg][Ee][Nn][Tt][Ss].md
37
+ [Aa][Gg][Ee][Nn][Tt][Ss].override.md
38
+ [Cc][Ll][Aa][Uu][Dd][Ee].md
39
+ [Gg][Ee][Mm][Ii][Nn][Ii].md
40
+ copilot-instructions.md
41
+ .claude/
42
+ .codex/
43
+ .agents/
44
+ .cursor/
LICENSE ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # OXR License
2
+
3
+ Copyright (c) 2026 SoMark Team
4
+
5
+ OXR is licensed under the Apache License, Version 2.0, subject to the
6
+ additional terms below. These additional terms take precedence in the event
7
+ of a conflict; otherwise, Apache License 2.0 applies.
8
+
9
+ ## 1. Commercial License and Thresholds
10
+
11
+ Commercial use of OXR and its Derivative Works is permitted without a
12
+ separate commercial license only while you and your Affiliates, on a
13
+ consolidated basis, do not exceed either of the following thresholds:
14
+
15
+ (a) USD 2,000,000 in Total Funding; or
16
+ (b) USD 2,000,000 in Annual Revenue.
17
+
18
+ If either threshold is exceeded, you must obtain a separate commercial
19
+ license from SoMark Team before commencing or continuing commercial use.
20
+ Being below these thresholds does not waive any other term of this License,
21
+ including the competing hosted API restriction in Section 3.
22
+
23
+ ## 2. Online Service Attribution
24
+
25
+ When providing an online service to third parties using OXR or its Derivative
26
+ Works, you must prominently identify SoMark OXR in the service's user interface or
27
+ publicly accessible documentation.
28
+
29
+ ## 3. Competing Hosted API Services
30
+
31
+ Unless expressly authorized in advance by a separate written license from
32
+ SoMark Team, you may not use OXR or its Derivative Works to offer, operate,
33
+ or make available to third parties a hosted API service that competes with
34
+ a hosted API service offered by SoMark Team or its Affiliates.
35
+
36
+ This restriction includes services offered through a reseller, a white-label
37
+ arrangement, or an intermediary interface, whether paid or free. A competing
38
+ hosted API service is one whose principal functionality provides document
39
+ parsing, optical character recognition (OCR), document layout analysis, or
40
+ document content extraction as a substitute for substantially similar
41
+ functionality of a hosted API service offered by SoMark Team or its Affiliates.
42
+
43
+ This restriction applies regardless of Total Funding or Annual Revenue.
44
+ Use solely within your organization and its Affiliates, without offering such
45
+ a service to third parties, is not prohibited by this Section; all other
46
+ terms of this License still apply.
47
+
48
+ ## 4. Termination
49
+
50
+ Your rights under this License terminate automatically, without notice from
51
+ SoMark Team, if you fail to obtain a commercial license as required by
52
+ Section 1, fail to comply with Section 2, or violate Section 3.
53
+
54
+ ## 5. Definitions and Scope
55
+
56
+ "Affiliates" are entities that directly or indirectly control you, are
57
+ controlled by you, or share common control with you. "Control" means the
58
+ power to direct an entity's management or operations through ownership,
59
+ voting rights, contractual rights, or other means.
60
+
61
+ "Total Funding" means cumulative equity and debt financing raised from all
62
+ sources by you and your Affiliates, on a consolidated basis, excluding
63
+ intra-group transactions to avoid double counting.
64
+
65
+ "Annual Revenue" means the gross revenue of you and your Affiliates for the
66
+ most recently completed fiscal year, on a consolidated basis, excluding
67
+ intra-group transactions to avoid double counting. Amounts in other
68
+ currencies are measured at their US dollar equivalent.
69
+
70
+ "Derivative Works" has the meaning given in Apache License 2.0. When
71
+ redistributing OXR or its Derivative Works, you must include these additional
72
+ terms together with Apache License 2.0. Third-party components separately
73
+ identified as being under other licenses remain governed by those licenses.
74
+
75
+ The full text of Apache License 2.0 is available at
76
+ https://www.apache.org/licenses/LICENSE-2.0 .
77
+
78
+ ---
79
+
80
+ # OXR 许可协议
81
+
82
+ 版权所有 (c) 2026 SoMark Team
83
+
84
+ OXR 依据 Apache License 2.0 进行许可,同时受下列附加条款约束。若存在冲突,
85
+ 以本附加条款为准;其他事项适用 Apache License 2.0。
86
+
87
+ ## 1. 商业许可与门槛
88
+
89
+ 仅当您及您的关联方按合并口径计算,均未超过以下任一门槛时,方可无需另行
90
+ 取得商业许可而将 OXR 及其衍生作品用于商业用途:
91
+
92
+ (a) 累计融资总额 2,000,000 美元;或
93
+ (b) 年度总收入 2,000,000 美元。
94
+
95
+ 一旦超过任一门槛,您必须在开始或继续商业使用之前,从 SoMark Team 取得
96
+ 单独的商业许可。未超过上述门槛并不免除本许可中的其他义务,包括第 3 条
97
+ 关于竞争性托管 API 服务的限制。
98
+
99
+ ## 2. 在线服务标识义务
100
+
101
+ 如果您使用 OXR 或其衍生作品向第三方提供在线服务,必须在该服务的用户界面
102
+ 或公开可访问的文档中,显著标明使用了 SoMark OXR。
103
+
104
+ ## 3. 竞争性托管 API 服务限制
105
+
106
+ 除非事先通过与 SoMark Team 签订单独的书面许可取得明确授权,否则您不得
107
+ 使用 OXR 或其衍生作品,向第三方提供、运营或开放与 SoMark Team 或其关联方
108
+ 所提供的托管 API 服务构成竞争的托管 API 服务。
109
+
110
+ 本限制包括通过转售、白标或中间接口提供的服务,无论收费或免费。竞争性
111
+ 托管 API 服务,是指以文档解析、光学字符识别(OCR)、文档版面分析或文档
112
+ 内容提取为主要功能,并能够替代 SoMark Team 或其关联方提供的托管 API
113
+ 服务中实质相似功能的服务。
114
+
115
+ 本限制不受累计融资总额或年度总收入门槛影响。仅在您所属组织及其关联方内部
116
+ 使用,且不向第三方提供上述服务的,不属于本条禁止的行为;本许可的其他
117
+ 条款仍然适用。
118
+
119
+ ## 4. 许可终止
120
+
121
+ 如果您未按第 1 条要求取得商业许可、未遵守第 2 条标识义务,或违反第 3 条,
122
+ 您在本许可项下的权利将自动终止,SoMark Team 无须另行通知。
123
+
124
+ ## 5. 定义与适用范围
125
+
126
+ “关联方”是指直接或间接控制您、受您控制,或与您处于共同控制之下的实体。
127
+ “控制”是指通过所有权、表决权、合同权利或其他方式,决定某一实体管理或
128
+ 经营事项的能力。
129
+
130
+ “累计融资总额”是指您及您的关联方按合并口径计算,从所有来源累计取得的
131
+ 股权融资和债务融资总额,剔除集团内部交易以避免重复计算。
132
+
133
+ “年度总收入”是指您及您的关联方最近一个已结束财年的总收入,按合并口径
134
+ 计算,并剔除集团内部交易以避免重复计算。其他币种金额按其等值美元计算。
135
+
136
+ “衍生作品”的含义与 Apache License 2.0 中的定义一致。再分发 OXR 或其
137
+ 衍生作品时,必须同时附上本附加条款和 Apache License 2.0。单独标明适用
138
+ 其他许可的第三方组件,仍受各自许可约束。
139
+
140
+ Apache License 2.0 全文见:https://www.apache.org/licenses/LICENSE-2.0 。
README.md ADDED
@@ -0,0 +1,350 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ tags:
3
+ - document-parsing
4
+ - ocr
5
+ ---
6
+
7
+ <div align="center">
8
+
9
+ <img src="https://raw.githubusercontent.com/SoMarkAI/image_bed/main/OXR_README/assets/header.png" alt="" />
10
+
11
+ <h1>SoMark|O<span style="color: #DE7802;">X</span>R</h1>
12
+
13
+ An open-source document parsing model with SOTA accuracy and speed, using an RSI loop during post-training.
14
+
15
+ ---
16
+
17
+ **Turn documents into Agent-Friendly data.**
18
+
19
+ [🌐 Online Demo](https://free-oxr.somark.ai) | [🔥 🆓 Free API](#free-api)
20
+
21
+ [![GitHub | OXR](https://img.shields.io/badge/GitHub-OXR-181717?style=flat-square&logo=github)](https://github.com/SoMarkAI/OXR) [![Hugging Face | OXR-1.0](https://img.shields.io/badge/Hugging%20Face-OXR--1.0-F59E0B?style=flat-square)](https://huggingface.co/SoMarkAI/OXR-1.0) [![ModelScope | OXR-1.0](https://img.shields.io/badge/ModelScope-OXR--1.0-624AFF?style=flat-square)](https://modelscope.cn/models/SoMark/OXR-1.0)
22
+ ![arXiv | Coming Soon](https://img.shields.io/badge/arXiv-Coming%20Soon-B31B1B?style=flat-square)
23
+
24
+ **English** · [简体中文](https://github.com/SoMarkAI/OXR/blob/main/README_CN.md)
25
+
26
+ </div>
27
+
28
+ OXR (Optical Everything Recognition) is an open-source model from the SoMark team. It turns PDFs and document images into Agent-Friendly data in Markdown or JSON, with layout coordinates included.
29
+
30
+ Pretrained from scratch, OXR uses a recursive self-improvement (RSI) loop during post-training—a first in document parsing. With just 1.3B parameters, OXR ranks #1 on olmOCR-Bench, OmniDocBench, and DocParsingBench while delivering industry-leading parsing speed.
31
+
32
+ OXR is the first to make its layout analysis module independent and pluggable. This design accelerates parsing and supports replacing the built-in layout model with a custom one. OXR continues to handle content recognition and reading order, enabling adaptation across diverse scenarios with minimal integration effort.
33
+
34
+ ![](https://raw.githubusercontent.com/SoMarkAI/image_bed/main/OXR_README/assets/three-benchmark-leaderboards-olm-main.png)
35
+
36
+ ![](https://raw.githubusercontent.com/SoMarkAI/image_bed/main/OXR_README/assets/speed-score-overview.png)
37
+
38
+ ---
39
+
40
+ > If this project helps you, please click ⭐ Star in the top-right corner to support us. It's the best encouragement you can give the developers!
41
+
42
+
43
+
44
+ ## Why OXR?
45
+
46
+ - 🎯 SOTA Accuracy. #1 Across Three Benchmarks.
47
+
48
+ - OXR ranks 🏆#1 on olmOCR-Bench, OmniDocBench, and DocParsingBench, with a particularly wide lead in table parsing.
49
+ - OXR can recognize 13 document element types, from formulas and tables to chemical equations, chemical structures, and code blocks, with bounding box coordinates included. It also performs well on challenging documents and layouts, including A3 exam papers, newspapers, vertical text, and handwriting.
50
+ - See [docs/benchmark.md](https://github.com/SoMarkAI/OXR/blob/main/docs/benchmark.md) for the full results and instructions to reproduce the evaluations.
51
+ - 🚀 1.3B Parameters. Top-Tier Parsing Speed.
52
+
53
+ - With only 1.3B parameters, OXR ranks 🏆#1 among open-source document parsing models in speed tests on the RTX PRO 6000.
54
+ - OXR redesign its processing architecture at the task level to improve parsing speed while maintaining accuracy.
55
+ - See [docs/benchmark.md](https://github.com/SoMarkAI/OXR/blob/main/docs/benchmark.md) for the full results and instructions to reproduce the evaluations.
56
+ - 🧩 Pluggable Layout Analysis. Document Structure on Your Terms.
57
+
58
+ - Layout analysis requirements vary widely across domains. Even for the same document, element types, region boundaries, and grouping rules can differ depending on the business use case.
59
+ - OXR is the first to make layout analysis independent and pluggable. Its built-in layout model can be replaced while retaining OXR’s recognition and reading order capabilities, enabling adaptation across document scenarios with minimal integration effort.
60
+ - See the [How it works](#how-to-works) section below for details.
61
+
62
+
63
+ <details>
64
+ <summary>Expand to see all 13 element types</summary>
65
+
66
+ | Name | JSON type value | Content or output format |
67
+ |-|-|-|
68
+ | Caption | Caption | Plain text |
69
+ | Footnote | Footnote | Plain text |
70
+ | Formula | Formula | LaTeX |
71
+ | Text | Text | Plain text |
72
+ | Footer | Page-footer | Plain text (filtered out by default) |
73
+ | Header | Page-header | Plain text (filtered out by default) |
74
+ | Figure | Picture | Image |
75
+ | Title | Title | Plain text |
76
+ | Table | Table | HTML / Markdown |
77
+ | Stamp | Stamp | Plain text / Image |
78
+ | Chemical Equation | Chemical Equation | LaTeX (starting with \ce, using mhchem syntax) |
79
+ | Code Block | Code Block | Markdown code block |
80
+ | Chemical Structure | Chemical Structure | SMILES / Image |
81
+
82
+
83
+ </details>
84
+
85
+
86
+ ## Parsing Examples
87
+
88
+ The animation shows OXR's parsing process.
89
+
90
+ ![Animated OXR parsing example](https://raw.githubusercontent.com/SoMarkAI/image_bed/main/OXR_README/assets/oxr_parsing_demo.gif)
91
+
92
+ The examples below show OXR parsing different document types. Click an image to view the original.
93
+
94
+ <table>
95
+ <tr>
96
+ <td align="center" valign="top"><strong>A3 Exam Paper</strong><br/><a href="https://raw.githubusercontent.com/SoMarkAI/image_bed/main/OXR_README/assets/parsing_examples/a3_exam_paper.png"><img src="https://raw.githubusercontent.com/SoMarkAI/image_bed/main/OXR_README/assets/parsing_examples/a3_exam_paper.png" alt="A3 Exam Paper" width="280" /></a></td>
97
+ <td align="center" valign="top"><strong>Chinese Handwriting</strong><br/><a href="https://raw.githubusercontent.com/SoMarkAI/image_bed/main/OXR_README/assets/parsing_examples/chinese_handwriting.png"><img src="https://raw.githubusercontent.com/SoMarkAI/image_bed/main/OXR_README/assets/parsing_examples/chinese_handwriting.png" alt="Chinese Handwriting" width="280" /></a></td>
98
+ <td align="center" valign="top"><strong>Code Block</strong><br/><a href="https://raw.githubusercontent.com/SoMarkAI/image_bed/main/OXR_README/assets/parsing_examples/code_block.png"><img src="https://raw.githubusercontent.com/SoMarkAI/image_bed/main/OXR_README/assets/parsing_examples/code_block.png" alt="Code Block" width="280" /></a></td>
99
+ </tr>
100
+ <tr>
101
+ <td align="center" valign="top"><strong>Chemical Equation</strong><br/><a href="https://raw.githubusercontent.com/SoMarkAI/image_bed/main/OXR_README/assets/parsing_examples/chemical_equation.png"><img src="https://raw.githubusercontent.com/SoMarkAI/image_bed/main/OXR_README/assets/parsing_examples/chemical_equation.png" alt="Chemical Equation" width="280" /></a></td>
102
+ <td align="center" valign="top"><strong>Chemical Structure</strong><br/><a href="https://raw.githubusercontent.com/SoMarkAI/image_bed/main/OXR_README/assets/parsing_examples/chemical_structure.png"><img src="https://raw.githubusercontent.com/SoMarkAI/image_bed/main/OXR_README/assets/parsing_examples/chemical_structure.png" alt="Chemical Structure" width="280" /></a></td>
103
+ <td align="center" valign="top"><strong>Complex Formula</strong><br/><a href="https://raw.githubusercontent.com/SoMarkAI/image_bed/main/OXR_README/assets/parsing_examples/complex_formula.png"><img src="https://raw.githubusercontent.com/SoMarkAI/image_bed/main/OXR_README/assets/parsing_examples/complex_formula.png" alt="Complex Formula" width="280" /></a></td>
104
+ </tr>
105
+ <tr>
106
+ <td align="center" valign="top"><strong>Complex Table</strong><br/><a href="https://raw.githubusercontent.com/SoMarkAI/image_bed/main/OXR_README/assets/parsing_examples/complex_table.png"><img src="https://raw.githubusercontent.com/SoMarkAI/image_bed/main/OXR_README/assets/parsing_examples/complex_table.png" alt="Complex Table" width="280" /></a></td>
107
+ <td align="center" valign="top"><strong>Academic Paper</strong><br/><a href="https://raw.githubusercontent.com/SoMarkAI/image_bed/main/OXR_README/assets/parsing_examples/academic_paper.png"><img src="https://raw.githubusercontent.com/SoMarkAI/image_bed/main/OXR_README/assets/parsing_examples/academic_paper.png" alt="Academic Paper" width="280" /></a></td>
108
+ <td align="center" valign="top"><strong>Newspaper</strong><br/><a href="https://raw.githubusercontent.com/SoMarkAI/image_bed/main/OXR_README/assets/parsing_examples/newspaper.png"><img src="https://raw.githubusercontent.com/SoMarkAI/image_bed/main/OXR_README/assets/parsing_examples/newspaper.png" alt="Newspaper" width="280" /></a></td>
109
+ </tr>
110
+ <tr>
111
+ <td align="center" valign="top"><strong>Research Report</strong><br/><a href="https://raw.githubusercontent.com/SoMarkAI/image_bed/main/OXR_README/assets/parsing_examples/research_report.png"><img src="https://raw.githubusercontent.com/SoMarkAI/image_bed/main/OXR_README/assets/parsing_examples/research_report.png" alt="Research Report" width="280" /></a></td>
112
+ <td align="center" valign="top"><strong>Resume</strong><br/><a href="https://raw.githubusercontent.com/SoMarkAI/image_bed/main/OXR_README/assets/parsing_examples/resume.png"><img src="https://raw.githubusercontent.com/SoMarkAI/image_bed/main/OXR_README/assets/parsing_examples/resume.png" alt="Resume" width="280" /></a></td>
113
+ <td align="center" valign="top"><strong>English Handwriting</strong><br/><a href="https://raw.githubusercontent.com/SoMarkAI/image_bed/main/OXR_README/assets/parsing_examples/english_handwriting.png"><img src="https://raw.githubusercontent.com/SoMarkAI/image_bed/main/OXR_README/assets/parsing_examples/english_handwriting.png" alt="English Handwriting" width="280" /></a></td>
114
+ </tr>
115
+ </table>
116
+
117
+ ## How it works
118
+
119
+ <a id="how-to-works"></a>
120
+
121
+ ### Technical Architecture
122
+
123
+ The system consists of two models: a Layout Analysis model and OXR. We integrate Reading Order into the Recognition stage, with both tasks handled by the same model. This design improves both speed and accuracy.
124
+
125
+ We also designed the system this way to make customization easier. We have found that Layout Analysis varies more across document domains than any other module, and it is typically the part that document parsing providers need to customize most. Existing document parsing tools either assign it to the Recognition model or use a dedicated model that also handles Reading Order. OXR's design achieves the simplest access method for Layout Analysis, and any Layout Analysis model can work well with the OXR model.
126
+
127
+ ![](https://raw.githubusercontent.com/SoMarkAI/image_bed/main/OXR_README/assets/oxr_pipeline.png)
128
+
129
+
130
+
131
+ ### Improvement Through the RSI Loop
132
+
133
+ For a task like document parsing, where ground-truth answers exist, we have long wanted an engineering system that helps the core model improve itself. The diagram below shows OXR's RSI approach. It targets samples the model gets wrong or handles weakly, improving performance without human annotation. This has made our iteration faster than ever.
134
+
135
+ ![](https://raw.githubusercontent.com/SoMarkAI/image_bed/main/OXR_README/assets/oxr_rsi_loop.png)
136
+
137
+
138
+
139
+ ## Quick Start
140
+
141
+ <a id="free-api"></a>
142
+
143
+ ### 0. 🔥 🆓 Free API
144
+
145
+ OXR provides a free API so you can test parsing on your own documents without registering or requesting an API key.
146
+
147
+ We'll keep the free API online until we can no longer afford to run it.
148
+
149
+ ⚠️ Note: We do not recommend using the Free OXR API in production. We do not guarantee service stability, and requests are queued, so processing speed may vary slightly.
150
+
151
+ | Online Demo | https://free-oxr.somark.ai | https://free-oxr.somark.cn |
152
+ |-|-|-|
153
+ | API URL | https://free-oxr.somark.ai/api/v1 | https://free-oxr.somark.cn/api/v1 |
154
+ | API documentation | [https://docs.somark.ai/api-reference](https://docs.somark.ai/api-reference) | https://docs.somark.cn/api-reference |
155
+
156
+ > somark.cn is a mirror of somark.ai. Choose the domain that works best for your region.
157
+
158
+ OXR's document parsing API is compatible with SoMark's official document parsing API. You can use SoMark's official document parsing CLI tools, somark-py and somark-js, by changing the **Base URL**. See Section 1.4 for supported endpoints and output formats.
159
+
160
+ ```Bash
161
+ # using python
162
+ pip install somark
163
+ # or using js
164
+ npm install -g somark-js
165
+
166
+ somark --base-url https://free-oxr.somark.ai/api/v1 --api-key dummy \
167
+ parse document.pdf --formats md,json --out ./parsed-output/
168
+ ```
169
+
170
+
171
+
172
+ ### 1. Run Locally
173
+
174
+ ```Plain Text
175
+ Operating system: Linux
176
+ GPU: NVIDIA (with BF16 support)
177
+ Driver: NVIDIA driver (compatible with CUDA 13.0)
178
+ Python: 3.12 (for model downloads / running from source)
179
+ GPU memory to get started: 24GB on a single GPU (for low-concurrency testing)
180
+ Recommended GPU memory: ≥48GB on a single GPU (for long documents / higher concurrency)
181
+ ```
182
+
183
+ #### 1.1 Download the Code and Models
184
+
185
+ | Model | Download links | Precision | Includes a lightweight layout analysis model? |
186
+ |-|-|-|-|
187
+ | OXR-1.0 | [🤗 Hugging Face ](https://huggingface.co/SoMarkAI/OXR-1.0) <br/>[🤖 ModelScope](https://modelscope.cn/models/SoMark/OXR-1.0) | BF16 | Yes, in the layout_analysis folder |
188
+
189
+ First, get the code and prepare the environment for downloading the models:
190
+
191
+ ```Bash
192
+ git clone https://github.com/SoMarkAI/OXR.git
193
+ cd OXR
194
+
195
+ # pip install -U huggingface_hub
196
+ hf download SoMarkAI/OXR-1.0 --local-dir ./models/OXR-1.0
197
+
198
+ # or using ModelScope
199
+ # pip install -U modelscope-hub
200
+ modelscope download SoMark/OXR-1.0 --local_dir ./models/OXR-1.0
201
+ ```
202
+
203
+ #### 1.2 Start the Services
204
+
205
+ ##### 1.2.1 Run with Docker (Recommended)
206
+
207
+ We recommend Docker to configure and start all services together.
208
+
209
+ ```Bash
210
+ docker build -f docker/Dockerfile.allinone -t oxr-1.0 .
211
+ docker run --rm --gpus all --ipc=host \
212
+ -p 127.0.0.1:8000:8000 \
213
+ -v "$PWD/models/OXR-1.0:/models/oxr-model:ro" \
214
+ oxr-1.0
215
+ ```
216
+
217
+ Once the startup logs show that the services are ready, open http://localhost:8000/demo or http://localhost:8000/docs.
218
+ The first startup may take longer. The default startup timeout is 900 seconds.
219
+ See How to use OXR for more advanced options and usage examples.
220
+
221
+
222
+ ##### 1.2.2 Run from Source
223
+
224
+ After downloading the code and models, start the following three services separately. Each service uses its own process and virtual environment. Run all commands from the OXR repository root. The document parsing API depends on both the OXR model service and the Layout Analysis model service, so start the two model APIs first.
225
+
226
+ **OXR Model Service (Port 8080)**
227
+
228
+ OXR supports vLLM, but the official vLLM release does not yet support OXR inference. We provide an out-of-tree plugin and a loading script in the model weights folder on Hugging Face. No recompilation is needed: you can use the plugin directly with the official vLLM Docker image or CLI.
229
+
230
+ ```Bash
231
+ # pip install vllm==0.25.1 transformers==5.13.1
232
+
233
+ MODEL_PATH="$PWD/models/OXR-1.0" \
234
+ bash services/oxr/start_vllm.sh \
235
+ --gpu-memory-utilization 0.70 --max-num-seqs 16
236
+ ```
237
+
238
+ **Layout Analysis Model Service (Port 8081)**
239
+
240
+ The Layout Analysis model weights are included in the OXR model weights folder downloaded earlier.
241
+
242
+ ```Bash
243
+ # pip install torch==2.11.0 torchvision==0.26.0 --index-url https://download.pytorch.org/whl/cu130
244
+ # pip install -r services/layout_analyze/requirements.txt
245
+
246
+ MODEL_PATH="$PWD/models/OXR-1.0/layout_analysis/last.pth" \
247
+ DFINE_DEVICE=cuda \
248
+ python -m uvicorn dfine_la.api:app \
249
+ --app-dir services/layout_analyze \
250
+ --host 127.0.0.1 --port 8081 --workers 1
251
+ ```
252
+
253
+ **Document Parsing API (Port 8000)**
254
+
255
+ Copy the provided `config.example.yaml` to `config.yaml`. Set the url fields under `layout_analyze_model` and `oxr_model` to the addresses of the two services you started above.
256
+
257
+ ```Bash
258
+ # pip install -c docker/requirements.allinone.txt .
259
+
260
+ oxr serve --config config.yaml \
261
+ --host 127.0.0.1 --port 8000 --workers 1
262
+ ```
263
+
264
+ Once all three services are running, use the demo and parsing API in the same way as with Docker. See Sections 1.3 and 1.4.
265
+
266
+ #### 1.3 Open the Visual Demo
267
+
268
+ Open http://localhost:8000/demo and upload a PDF or image. You can view the original file, Markdown preview, source, layout bounding boxes, and reading order, and download the Markdown.
269
+
270
+ The demo uses a temporary file cache that expires after 1 hour by default and is renewed during processing. Cached results become inaccessible once the cache expires or is cleared, so download your results promptly.
271
+
272
+ #### 1.4 Send a Parsing Request
273
+
274
+ OXR's API is compatible with the SoMark API. Although OXR does not support some specialized capabilities, SoMark's ecosystem fully supports deployed OXR services. Visit the [SoMark website](https://somark.ai) and refer to the [API documentation](https://docs.somark.ai/api-reference).
275
+
276
+ Call the parsing endpoint directly:
277
+
278
+ ```Bash
279
+ curl --fail-with-body http://localhost:8000/v1/parse/sync \
280
+ -F 'file=@document.pdf' \
281
+ -F 'api_key=dummy' \
282
+ -F 'output_formats=markdown' \
283
+ -F 'output_formats=json'
284
+ ```
285
+
286
+ SoMark provides a full set of CLI and SDK tools for document parsing in Python and JavaScript. OXR is compatible with their document parsing interfaces, so you can use them by changing the baseURL. For more information, see https://docs.somark.ai/en/cli-sdk.
287
+
288
+ ```Bash
289
+ pip install somark
290
+
291
+ somark --base-url http://localhost:8000/v1 --api-key dummy \
292
+ parse document.pdf --formats md,json --out ./parsed-output/
293
+ ```
294
+
295
+ ## Ecosystem
296
+
297
+ The SoMark team has developed a range of tools for document parsing. If you find them helpful, please give them a ⭐ Star.
298
+
299
+ | | Description | Project URL |
300
+ |-|-|-|
301
+ | SoMarkDown | The official Markdown parsing and rendering library for SoMark and OXR | https://github.com/SoMarkAI/SoMarkDown |
302
+ | SoMarkDownViewer | A live editing application for SoMarkDown | https://github.com/SoMarkAI/SoMarkDownViewer |
303
+ | SoPDF | A high-performance PDF processing library with a permissive open-source license | https://github.com/SoMarkAI/SoPDF |
304
+ | Docision | MultiModal Jev for Document. | https://github.com/SoMarkAI/docision |
305
+ | somark-py | The official Python SDK & CLI for SoMark and OXR | [https://github.com/SoMarkAI/somark-py](https://github.com/SoMarkAI/somark-py) |
306
+ | somark-js | The official JS / TS SDK & CLI for SoMark and OXR | https://github.com/SoMarkAI/somark-js |
307
+
308
+ ---
309
+
310
+ <div align="center">
311
+ <img src="https://raw.githubusercontent.com/SoMarkAI/image_bed/main/OXR_README/assets/OXR_Star_on_GitHub.gif" width="800" />
312
+ </div>
313
+
314
+ ## Citation
315
+
316
+ For now, please cite the OXR-1.0 open-source project. We will add a paper citation once the technical report is published.
317
+
318
+ ```bibtex
319
+ @misc{somark2026oxr,
320
+ author = {{SoMark}},
321
+ title = {{OXR-1.0}},
322
+ url = {https://github.com/SoMarkAI/OXR},
323
+ note = {GitHub repository}
324
+ }
325
+ ```
326
+
327
+ ## License
328
+
329
+ OXR is released under the [OXR Open Source License](./LICENSE), a custom license based on Apache 2.0 with additional terms.
330
+
331
+ For further services, contact the SoMark team at [somark.ai](http://somark.ai).
332
+
333
+ ## Community
334
+
335
+ Join the OXR community to discuss document parsing, share feedback, and keep up with the latest updates.
336
+
337
+ - [Discord](https://discord.gg/msCPj2AtNN) — Discuss OXR, exchange tips, and share what you're building.
338
+ - [X / Twitter](https://x.com/somarkai) — Keep up with OXR releases and news from SoMark.
339
+ - [LinkedIn](https://www.linkedin.com/company/somarkai/posts/?feedView=all) — Follow SoMark for company news and product updates.
340
+ - [GitHub Issues](https://github.com/SoMarkAI/OXR/issues) — Report bugs, suggest features, and track progress.
341
+ - **WeChat** — Scan the QR codes below to follow the SoMark official account or join the OXR group and exchange ideas with other users.
342
+
343
+ <div align="center">
344
+
345
+ | WeChat Official Account | WeChat Group |
346
+ | :---: | :---: |
347
+ | <img src="https://raw.githubusercontent.com/SoMarkAI/OXR/main/assets/wechat-account.jpg" alt="QR code for the SoMark WeChat official account" width="200" /> | <img src="https://raw.githubusercontent.com/SoMarkAI/OXR/main/assets/wechat-community.png" alt="QR code for the OXR WeChat group" width="200" /> |
348
+
349
+ </div>
350
+
added_tokens.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "</think>": 151668,
3
+ "</tool_call>": 151658,
4
+ "</tool_response>": 151666,
5
+ "<think>": 151667,
6
+ "<tool_call>": 151657,
7
+ "<tool_response>": 151665,
8
+ "<|box_end|>": 151649,
9
+ "<|box_start|>": 151648,
10
+ "<|endoftext|>": 151643,
11
+ "<|file_sep|>": 151664,
12
+ "<|fim_middle|>": 151660,
13
+ "<|fim_pad|>": 151662,
14
+ "<|fim_prefix|>": 151659,
15
+ "<|fim_suffix|>": 151661,
16
+ "<|im_end|>": 151645,
17
+ "<|im_start|>": 151644,
18
+ "<|image_pad|>": 151655,
19
+ "<|object_ref_end|>": 151647,
20
+ "<|object_ref_start|>": 151646,
21
+ "<|quad_end|>": 151651,
22
+ "<|quad_start|>": 151650,
23
+ "<|repo_name|>": 151663,
24
+ "<|video_pad|>": 151656,
25
+ "<|vision_end|>": 151653,
26
+ "<|vision_pad|>": 151654,
27
+ "<|vision_start|>": 151652
28
+ }
chat_template.jinja ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {%- for message in messages -%}
2
+ {{- '<|im_start|>' + message['role'] + '\n' -}}
3
+ {%- if message['content'] is string -%}
4
+ {{- message['content'] -}}
5
+ {%- else -%}
6
+ {%- for content in message['content'] -%}
7
+ {%- if content['type'] == 'image_url' or content['type'] == 'image' -%}
8
+ {{- '<image>' -}}
9
+ {%- elif content['type'] == 'text' -%}
10
+ {{- content['text'] -}}
11
+ {%- endif -%}
12
+ {%- endfor -%}
13
+ {%- endif -%}
14
+ {{- '<|im_end|>\n' -}}
15
+ {%- endfor -%}
16
+ {%- if add_generation_prompt -%}
17
+ {{- '<|im_start|>assistant\n' -}}
18
+ {%- endif -%}
config.json ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "OXR"
4
+ ],
5
+ "auto_map": {
6
+ "AutoConfig": "configuration_oxr.OXRConfig",
7
+ "AutoModelForCausalLM": "modeling_oxr.OXR"
8
+ },
9
+ "conversation_formatter_class": "Qwen3ConversationFormatter",
10
+ "dtype": "bfloat16",
11
+ "eos_token_id": 151643,
12
+ "hidden_size": 1024,
13
+ "keys_to_ignore_at_inference": [
14
+ "past_key_values"
15
+ ],
16
+ "llm_config": {
17
+ "_name_or_path": "Qwen/Qwen3-0.6B-Base",
18
+ "architectures": [
19
+ "Qwen3ForCausalLM"
20
+ ],
21
+ "attention_bias": false,
22
+ "attention_dropout": 0.0,
23
+ "bos_token_id": 151643,
24
+ "dtype": "bfloat16",
25
+ "eos_token_id": 151643,
26
+ "head_dim": 128,
27
+ "hidden_act": "silu",
28
+ "hidden_size": 1024,
29
+ "initializer_range": 0.02,
30
+ "intermediate_size": 3072,
31
+ "layer_types": [
32
+ "full_attention",
33
+ "full_attention",
34
+ "full_attention",
35
+ "full_attention",
36
+ "full_attention",
37
+ "full_attention",
38
+ "full_attention",
39
+ "full_attention",
40
+ "full_attention",
41
+ "full_attention",
42
+ "full_attention",
43
+ "full_attention",
44
+ "full_attention",
45
+ "full_attention",
46
+ "full_attention",
47
+ "full_attention",
48
+ "full_attention",
49
+ "full_attention",
50
+ "full_attention",
51
+ "full_attention",
52
+ "full_attention",
53
+ "full_attention",
54
+ "full_attention",
55
+ "full_attention",
56
+ "full_attention",
57
+ "full_attention",
58
+ "full_attention",
59
+ "full_attention"
60
+ ],
61
+ "max_position_embeddings": 32768,
62
+ "max_window_layers": 28,
63
+ "model_type": "qwen3",
64
+ "num_attention_heads": 16,
65
+ "num_hidden_layers": 28,
66
+ "num_key_value_heads": 8,
67
+ "pad_token_id": 151643,
68
+ "rms_norm_eps": 1e-06,
69
+ "rope_scaling": null,
70
+ "rope_theta": 1000000,
71
+ "sliding_window": null,
72
+ "tie_word_embeddings": true,
73
+ "use_cache": false,
74
+ "use_sliding_window": false,
75
+ "vocab_size": 151936
76
+ },
77
+ "max_position_embeddings": 40960,
78
+ "model_type": "oxr",
79
+ "num_attention_heads": 32,
80
+ "pad_token_id": 151643,
81
+ "transformers_version": "5.6.2",
82
+ "use_cache": false,
83
+ "visual_vocab_size": 65536,
84
+ "vit_config": {
85
+ "_attn_implementation_autoset": true,
86
+ "_name_or_path": "google/siglip2-so400m-patch16-512",
87
+ "attention_dropout": 0.0,
88
+ "dtype": "bfloat16",
89
+ "fullatt_block_indexes": null,
90
+ "hidden_act": "gelu_pytorch_tanh",
91
+ "hidden_size": 1152,
92
+ "hidden_stride": 2,
93
+ "image_size": 512,
94
+ "intermediate_size": 4304,
95
+ "layer_norm_eps": 1e-06,
96
+ "model_type": "siglip2_navit",
97
+ "num_attention_heads": 16,
98
+ "num_channels": 3,
99
+ "num_hidden_layers": 27,
100
+ "num_patches": -1,
101
+ "pad_token_id": 151643,
102
+ "patch_size": 16,
103
+ "preserve_original_pe": true,
104
+ "temporal_patch_size": 1,
105
+ "use_rope": true,
106
+ "window_size": 112
107
+ },
108
+ "vocab_size": 151936
109
+ }
configuration_oxr.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, Optional, List, Union
2
+
3
+ from transformers import AutoConfig, Qwen3Config
4
+ from transformers.configuration_utils import PretrainedConfig
5
+
6
+ __all__ = ["Siglip2NavitConfig", "OXRConfig"]
7
+
8
+
9
+ class Siglip2NavitConfig(PretrainedConfig):
10
+ """This is the configuration class to store the configuration of an [`AIMv2Model`].
11
+
12
+ Instantiating a configuration with the defaults will yield a similar configuration
13
+ to that of the [apple/aimv2-large-patch14-224](https://huggingface.co/apple/aimv2-large-patch14-224).
14
+
15
+ Args:
16
+ hidden_size: Dimension of the hidden representations.
17
+ intermediate_size: Dimension of the SwiGLU representations.
18
+ num_hidden_layers: Number of hidden layers in the Transformer.
19
+ num_attention_heads: Number of attention heads for each attention layer
20
+ in the Transformer.
21
+ num_channels: Number of input channels.
22
+ image_size: Image size.
23
+ patch_size: Patch size.
24
+ rms_norm_eps: Epsilon value used for the RMS normalization layer.
25
+ attention_dropout: Dropout ratio for attention probabilities.
26
+ projection_dropout: Dropout ratio for the projection layer after the attention.
27
+ qkv_bias: Whether to add a bias to the queries, keys and values.
28
+ use_bias: Whether to add a bias in the feed-forward and projection layers.
29
+ kwargs: Keyword arguments for the [`PretrainedConfig`].
30
+ """
31
+
32
+ model_type: str = "siglip2_navit"
33
+
34
+ def __init__(
35
+ self,
36
+ hidden_size: int = 1024,
37
+ intermediate_size: int = 4096,
38
+ num_hidden_layers: int = 24,
39
+ num_attention_heads: int = 16,
40
+ num_channels: int = 3,
41
+ num_patches: int = -1,
42
+ image_size: int = 512,
43
+ patch_size: int = 16,
44
+ hidden_act: str="gelu_pytorch_tanh",
45
+ layer_norm_eps: float = 1e-6,
46
+ attention_dropout: float = 0.0,
47
+ hidden_stride: int = 2,
48
+ window_size: int = 112,
49
+ fullatt_block_indexes: Optional[list] = None,
50
+ temporal_patch_size: int = 1,
51
+ preserve_original_pe: bool = True,
52
+ use_rope: bool = True,
53
+ **kwargs: Any,
54
+ ):
55
+ super().__init__(**kwargs)
56
+ self.hidden_size = hidden_size
57
+ self.intermediate_size = intermediate_size
58
+ self.num_hidden_layers = num_hidden_layers
59
+ self.num_attention_heads = num_attention_heads
60
+ self.num_channels = num_channels
61
+ self.num_patches = num_patches
62
+ self.patch_size = patch_size
63
+ self.image_size = image_size
64
+ self.hidden_act = hidden_act
65
+ self.attention_dropout = attention_dropout
66
+ self.layer_norm_eps = layer_norm_eps
67
+ self.hidden_stride = hidden_stride
68
+ self.window_size = window_size
69
+ self.fullatt_block_indexes = fullatt_block_indexes
70
+ self.temporal_patch_size = temporal_patch_size
71
+ self.preserve_original_pe = preserve_original_pe
72
+ self.use_rope = use_rope
73
+
74
+ class OXRConfig(PretrainedConfig):
75
+ model_type = "oxr"
76
+ sub_configs = dict(llm_config=Qwen3Config, vit_config=Siglip2NavitConfig)
77
+
78
+ def __init__(self,
79
+ llm_config: Optional[Union[Qwen3Config, dict]] = None,
80
+ vit_config: Optional[Union[Siglip2NavitConfig, dict]] = None,
81
+ visual_vocab_size=65536,
82
+ hidden_size=None,
83
+ **kwargs
84
+ ):
85
+ super().__init__(**kwargs)
86
+ if isinstance(llm_config, dict):
87
+ if "model_type" in llm_config:
88
+ config_kwargs = {k: v for k, v in llm_config.items() if k != "model_type"}
89
+ llm_config = AutoConfig.for_model(llm_config["model_type"], **config_kwargs)
90
+ else:
91
+ llm_config = Qwen3Config(**llm_config)
92
+ self.llm_config = llm_config
93
+ if isinstance(vit_config, dict):
94
+ vit_config = Siglip2NavitConfig(**vit_config)
95
+ self.vit_config = vit_config
96
+ self.visual_vocab_size = visual_vocab_size
97
+ self.hidden_size = hidden_size
98
+ if kwargs.get('attn_implementation'):
99
+ self.llm_config._attn_implementation = kwargs['attn_implementation']
100
+ self.vit_config._attn_implementation = kwargs['attn_implementation']
generation_config.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_from_model_config": true,
3
+ "eos_token_id": [
4
+ 151643,
5
+ 151645
6
+ ],
7
+ "transformers_version": "4.57.1"
8
+ }
layout_analysis/README.md ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Layout Analysis
2
+
3
+ D-FINE Stage-2 B2 layout model with 12 classes. Detects page regions;
4
+ text recognition and reading order are handled by OXR.
5
+
6
+ ## Files
7
+
8
+ - `last.pth`: inference-only model weights (no optimizer or training state).
9
+ - `metadata.json`: model identity, architecture, preprocessing, class IDs,
10
+ checkpoint size and SHA-256 (schema version 1).
11
+
12
+ `model_version` is a public release version (`major.minor.patch`), not a
13
+ training-run identifier.
14
+
15
+ ## Usage
16
+
17
+ Use the LA inference service in the OXR code repository at
18
+ `services/layout_analyze/`. Mount this directory into the service and set
19
+ `MODEL_PATH` to the mounted `last.pth` path.
20
+
21
+ The matching LA service reads `metadata.json` next to the checkpoint at startup
22
+ and validates its supported architecture, preprocessing, class IDs and weight
23
+ integrity. Both files must be mounted. `MODEL_METADATA_PATH` can override the
24
+ metadata path. Unsupported metadata or a mismatched checkpoint fails startup.
25
+ This is not a Transformers `AutoModel` configuration. Device, precision and
26
+ CUDA Graph settings are selected by the service, not by the model metadata.
27
+
28
+ Checkpoint verification:
29
+
30
+ ```bash
31
+ shasum -a 256 layout_analysis/last.pth
32
+ # dff78ccf18439955362ca44a69eb28fb0b40222a7d278860589682de0176dc17
33
+ ```
layout_analysis/last.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dff78ccf18439955362ca44a69eb28fb0b40222a7d278860589682de0176dc17
3
+ size 78791307
layout_analysis/metadata.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schema_version": 1,
3
+ "model_type": "dfine",
4
+ "model_version": "1.0.0",
5
+ "architecture": "stage2_b2",
6
+ "checkpoint": "last.pth",
7
+ "checkpoint_size_bytes": 78791307,
8
+ "checkpoint_sha256": "dff78ccf18439955362ca44a69eb28fb0b40222a7d278860589682de0176dc17",
9
+ "num_classes": 12,
10
+ "image_size": 960,
11
+ "color_order": "rgb",
12
+ "resize_backend": "opencv_lanczos4",
13
+ "postprocess": "model_letterbox",
14
+ "provides_reading_order": false,
15
+ "id2label": {
16
+ "0": "Caption",
17
+ "1": "Footnote",
18
+ "2": "Formula",
19
+ "3": "Text",
20
+ "4": "Page-footer",
21
+ "5": "Page-header",
22
+ "6": "Picture",
23
+ "7": "Title",
24
+ "8": "Table",
25
+ "9": "Code Block",
26
+ "10": "Stamp",
27
+ "11": "Chemical Structure"
28
+ }
29
+ }
merges.txt ADDED
The diff for this file is too large to render. See raw diff
 
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:795bb2f1faac579f58829913f2c606202531a96c1203e19ef7a70b103fb7ea51
3
+ size 3068857520
modeling_oxr.py ADDED
@@ -0,0 +1,1037 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ from typing import Dict, List, Optional, Tuple, Union
3
+
4
+ import PIL.Image
5
+ import numpy as np
6
+ import torch
7
+ from torch import Tensor, nn
8
+ from torch.nn import functional as F
9
+ from transformers import (
10
+ AutoConfig,
11
+ AutoImageProcessor,
12
+ AutoModel,
13
+ AutoModelForCausalLM,
14
+ AutoTokenizer,
15
+ __version__ as TRANSFORMERS_VERSION,
16
+ )
17
+ from transformers.activations import ACT2FN
18
+ from transformers.generation.utils import GenerateOutput
19
+ from transformers.modeling_outputs import BaseModelOutputWithNoAttention
20
+ from transformers.modeling_utils import PreTrainedModel
21
+ from transformers.utils import is_flash_attn_2_available
22
+
23
+ from .configuration_oxr import Siglip2NavitConfig, OXRConfig
24
+
25
+
26
+ if is_flash_attn_2_available() :
27
+ from flash_attn import flash_attn_varlen_func
28
+ from flash_attn.layers.rotary import apply_rotary_emb
29
+
30
+
31
+ IMAGE_PLACEHOLDER = "<image>"
32
+ IMAGE_PLACEHOLDER_ID = -200
33
+ VIDEO_PLACEHOLDER = "<video>"
34
+ VIDEO_PLACEHOLDER_ID = -201
35
+
36
+ VISUAL_ATOM_ID = -300
37
+ INDICATOR_IDS = [-301, -302, -303, -304]
38
+
39
+
40
+ # copied from qwen2.5-vl
41
+ class VisionRotaryEmbedding(nn.Module):
42
+ def __init__(self, dim: int, theta: float = 10000.0) -> None:
43
+ super().__init__()
44
+ self.dim = dim
45
+ self.theta = theta
46
+ inv_freq = 1.0 / (theta ** (torch.arange(0, dim, 2, dtype=torch.float) / dim))
47
+ self.register_buffer("inv_freq", inv_freq, persistent=False)
48
+
49
+ def forward(self, seqlen: int) -> torch.Tensor:
50
+ inv_freq = 1.0 / (
51
+ self.theta ** (torch.arange(0, self.dim, 2, device=self.inv_freq.device, dtype=torch.float) / self.dim)
52
+ )
53
+ seq = torch.arange(seqlen, device=inv_freq.device, dtype=inv_freq.dtype)
54
+ freqs = torch.outer(seq, inv_freq)
55
+ return freqs
56
+
57
+
58
+ class Siglip2VisionEmbeddings(nn.Module):
59
+ def __init__(self, config: Siglip2NavitConfig):
60
+ super().__init__()
61
+ self.config = config
62
+ self.embed_dim = config.hidden_size
63
+ self.patch_size = config.patch_size
64
+ self.image_size = config.image_size
65
+ self.num_patches = config.num_patches
66
+ self.preserve_original_pe = config.preserve_original_pe
67
+ self.hidden_stride = config.hidden_stride
68
+
69
+
70
+ # siglip2 naflex
71
+ if self.num_patches > 0:
72
+ self.patch_embedding = nn.Linear(
73
+ in_features=config.num_channels * self.patch_size * self.patch_size,
74
+ out_features=self.embed_dim,
75
+ )
76
+ if self.preserve_original_pe:
77
+ self.position_embedding_size = int(self.num_patches**0.5)
78
+ self.position_embedding = nn.Embedding(self.num_patches, self.embed_dim)
79
+
80
+ else:
81
+ self.patch_embedding = nn.Conv2d(
82
+ in_channels=config.num_channels,
83
+ out_channels=self.embed_dim,
84
+ kernel_size=self.patch_size,
85
+ stride=self.patch_size,
86
+ padding="valid",
87
+ )
88
+ if self.preserve_original_pe:
89
+ self.num_patches = (self.image_size // self.patch_size) ** 2
90
+ self.position_embedding_size = self.image_size // self.patch_size
91
+ self.position_embedding = nn.Embedding(self.num_patches, self.embed_dim)
92
+
93
+ @staticmethod
94
+ def resize_positional_embeddings(
95
+ positional_embeddings: torch.Tensor,
96
+ spatial_shapes: torch.LongTensor,
97
+ max_length: int,
98
+ ) -> torch.Tensor:
99
+ """
100
+ Resize positional embeddings to image-specific size and pad to a fixed size.
101
+
102
+ Args:
103
+ positional_embeddings (`torch.Tensor`):
104
+ Position embeddings of shape (height, width, embed_dim)
105
+ spatial_shapes (`torch.LongTensor`):
106
+ Spatial shapes of shape (batch_size, 2) to resize the positional embeddings to
107
+ max_length (`int`):
108
+ Maximum length of the positional embeddings to pad resized positional embeddings to
109
+
110
+ Returns:
111
+ `torch.Tensor`: Embeddings of shape (batch_size, max_length, embed_dim)
112
+ """
113
+ batch_size = spatial_shapes.shape[0]
114
+ embed_dim = positional_embeddings.shape[-1]
115
+ source_dtype = positional_embeddings.dtype
116
+
117
+ resulted_positional_embeddings = torch.empty(
118
+ (batch_size, max_length, embed_dim),
119
+ device=positional_embeddings.device,
120
+ dtype=source_dtype,
121
+ )
122
+
123
+ # (height, width, embed_dim) -> (1, embed_dim, height, width) for interpolation
124
+ positional_embeddings = positional_embeddings.permute(2, 0, 1).unsqueeze(0)
125
+
126
+ # Upcast to float32 on CPU because antialias is not supported for bfloat16/float16 on CPU
127
+ if positional_embeddings.device.type == "cpu":
128
+ positional_embeddings = positional_embeddings.to(torch.float32)
129
+
130
+ for i in range(batch_size):
131
+ # (1, dim, height, width) -> (1, dim, target_height, target_width)
132
+ height, width = spatial_shapes[i]
133
+ resized_embeddings = F.interpolate(
134
+ positional_embeddings,
135
+ size=(height, width),
136
+ mode="bilinear",
137
+ align_corners=False,
138
+ antialias=True,
139
+ )
140
+
141
+ # (1, dim, target_height, target_width) -> (target_height * target_width, dim)
142
+ resized_embeddings = resized_embeddings.reshape(embed_dim, height * width).transpose(0, 1)
143
+
144
+ # Cast to original dtype
145
+ resized_embeddings = resized_embeddings.to(source_dtype)
146
+
147
+ resulted_positional_embeddings[i, : height * width] = resized_embeddings
148
+ resulted_positional_embeddings[i, height * width :] = resized_embeddings[0]
149
+
150
+ return resulted_positional_embeddings
151
+
152
+ def forward(self, pixel_values: torch.FloatTensor,
153
+ grid_thws: Optional[torch.LongTensor] = None) -> torch.Tensor:
154
+ """
155
+ Args:
156
+ pixel_values (`torch.FloatTensor`):
157
+ Pixel values of shape (num_patches, num_channels * temporal_patch_size * patch_size * patch_size)
158
+ grid_thws: (`torch.LongTensor`):
159
+ grid shape (num_patches, 3)
160
+ """
161
+
162
+ # Apply patch embeddings to already patchified pixel values
163
+ target_dtype = self.patch_embedding.weight.dtype
164
+ if isinstance(self.patch_embedding, nn.Linear):
165
+ patch_embeds = self.patch_embedding(pixel_values.to(dtype=target_dtype))
166
+ elif isinstance(self.patch_embedding, nn.Conv2d):
167
+ pixel_values = pixel_values.view(-1, self.config.num_channels * self.config.temporal_patch_size, self.patch_size,
168
+ self.patch_size)
169
+ patch_embeds = self.patch_embedding(pixel_values.to(dtype=target_dtype))
170
+ patch_embeds = patch_embeds.reshape(-1, self.embed_dim)
171
+
172
+
173
+ if self.preserve_original_pe:
174
+ assert grid_thws is not None
175
+ pos_embed_new = torch.zeros_like(patch_embeds)
176
+ ori_h = ori_w = self.position_embedding_size
177
+ positional_embeddings = self.position_embedding.weight.reshape(
178
+ self.position_embedding_size, self.position_embedding_size, -1
179
+ ).unsqueeze(0).permute(0,3,1,2)
180
+ # pos_embed = self.pos_embed.reshape(1, ori_h, ori_w, -1).permute(0, 3, 1, 2)
181
+ cnt = 0
182
+ for t, h, w in grid_thws:
183
+ thw = t * h * w
184
+ pe = F.interpolate(positional_embeddings, size=(h, w), mode='bicubic', align_corners=False)
185
+ pe = pe.permute(0, 2, 3, 1).reshape(1, h * w, -1)
186
+ pe = pe[0].repeat(t, 1)
187
+ pe = pe.reshape(t, h // self.hidden_stride, self.hidden_stride, w // self.hidden_stride,
188
+ self.hidden_stride, -1)
189
+ pe = pe.permute(0, 1, 3, 2, 4, 5).reshape(thw, -1)
190
+ pos_embed_new[cnt:cnt + thw] = pe
191
+ cnt += thw
192
+ patch_embeds = patch_embeds + pos_embed_new
193
+
194
+ return patch_embeds
195
+
196
+
197
+ # copied from qwen2.5-vl
198
+ def apply_rotary_pos_emb_flashatt(
199
+ q: torch.Tensor, k: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor
200
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
201
+ cos = cos.chunk(2, dim=-1)[0].contiguous()
202
+ sin = sin.chunk(2, dim=-1)[0].contiguous()
203
+ q_embed = apply_rotary_emb(q.float(), cos.float(), sin.float()).type_as(q)
204
+ k_embed = apply_rotary_emb(k.float(), cos.float(), sin.float()).type_as(k)
205
+ return q_embed, k_embed
206
+
207
+
208
+ # Copied from transformers.models.llama.modeling_llama.rotate_half
209
+ def rotate_half(x):
210
+ """Rotates half the hidden dims of the input."""
211
+ x1 = x[..., : x.shape[-1] // 2]
212
+ x2 = x[..., x.shape[-1] // 2 :]
213
+ return torch.cat((-x2, x1), dim=-1)
214
+
215
+
216
+ def apply_rotary_pos_emb_vision(
217
+ q: torch.Tensor, k: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor
218
+ ) -> tuple[torch.Tensor, torch.Tensor]:
219
+ orig_q_dtype = q.dtype
220
+ orig_k_dtype = k.dtype
221
+ q, k = q.float(), k.float()
222
+ cos, sin = cos.unsqueeze(-2).float(), sin.unsqueeze(-2).float()
223
+ q_embed = (q * cos) + (rotate_half(q) * sin)
224
+ k_embed = (k * cos) + (rotate_half(k) * sin)
225
+ q_embed = q_embed.to(orig_q_dtype)
226
+ k_embed = k_embed.to(orig_k_dtype)
227
+ return q_embed, k_embed
228
+
229
+
230
+ class Siglip2Attention(nn.Module):
231
+ """Multi-headed attention from 'Attention Is All You Need' paper"""
232
+
233
+ def __init__(self, config):
234
+ super().__init__()
235
+ self.config = config
236
+ self.embed_dim = config.hidden_size
237
+ self.num_heads = config.num_attention_heads
238
+ self.head_dim = self.embed_dim // self.num_heads
239
+ if self.head_dim * self.num_heads != self.embed_dim:
240
+ raise ValueError(
241
+ f"embed_dim must be divisible by num_heads (got `embed_dim`: {self.embed_dim} and `num_heads`:"
242
+ f" {self.num_heads})."
243
+ )
244
+ self.scale = self.head_dim**-0.5
245
+ self.dropout = config.attention_dropout
246
+ self.is_causal = False
247
+
248
+ self.k_proj = nn.Linear(self.embed_dim, self.embed_dim)
249
+ self.v_proj = nn.Linear(self.embed_dim, self.embed_dim)
250
+ self.q_proj = nn.Linear(self.embed_dim, self.embed_dim)
251
+ self.out_proj = nn.Linear(self.embed_dim, self.embed_dim)
252
+
253
+ self.use_rope = config.use_rope
254
+
255
+ def forward(
256
+ self,
257
+ hidden_states: torch.Tensor,
258
+ cu_seqlens: torch.Tensor,
259
+ position_embeddings: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
260
+ ) -> tuple[torch.Tensor, Optional[torch.Tensor]]:
261
+ """Input shape: Batch x Time x Channel"""
262
+
263
+ seq_length, embed_dim = hidden_states.shape
264
+
265
+ queries = self.q_proj(hidden_states)
266
+ keys = self.k_proj(hidden_states)
267
+ values = self.v_proj(hidden_states)
268
+
269
+ queries = queries.view(seq_length, self.num_heads, self.head_dim)
270
+ keys = keys.view(seq_length, self.num_heads, self.head_dim)
271
+ values = values.view(seq_length, self.num_heads, self.head_dim)
272
+
273
+ if self.use_rope:
274
+ cos, sin = position_embeddings
275
+ if is_flash_attn_2_available() :
276
+ queries, keys = apply_rotary_pos_emb_flashatt(queries.unsqueeze(0), keys.unsqueeze(0), cos, sin)
277
+ else:
278
+ queries, keys = apply_rotary_pos_emb_vision(queries.unsqueeze(0), keys.unsqueeze(0), cos, sin)
279
+ queries = queries.squeeze(0)
280
+ keys = keys.squeeze(0)
281
+
282
+ max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max().item()
283
+ if is_flash_attn_2_available() :
284
+ attn_output = flash_attn_varlen_func(queries, keys, values, cu_seqlens, cu_seqlens, max_seqlen, max_seqlen).reshape(
285
+ seq_length, -1
286
+ )
287
+ else:
288
+ batch_size = cu_seqlens.shape[0] - 1
289
+ outputs = []
290
+ cu = cu_seqlens.tolist()
291
+ for i in range(batch_size):
292
+ start_idx = cu[i]
293
+ end_idx = cu[i + 1]
294
+ # Each sequence is processed independently.
295
+ q_i = queries[start_idx:end_idx].unsqueeze(0)
296
+ k_i = keys[start_idx:end_idx].unsqueeze(0)
297
+ v_i = values[start_idx:end_idx].unsqueeze(0)
298
+ # (1, seq_len, num_heads, head_dim) ->
299
+ # (1, num_heads, seq_len, head_dim)
300
+ q_i, k_i, v_i = [x.transpose(1, 2) for x in (q_i, k_i, v_i)]
301
+ output_i = F.scaled_dot_product_attention(q_i,
302
+ k_i,
303
+ v_i,
304
+ dropout_p=0.0)
305
+ # (1, num_heads, seq_len, head_dim) -> (seq_len, embed_dim)
306
+ output_i = output_i.transpose(1, 2).reshape(-1, self.embed_dim)
307
+ outputs.append(output_i)
308
+ attn_output = torch.cat(outputs, dim=0)
309
+
310
+ attn_output = self.out_proj(attn_output)
311
+ return attn_output
312
+
313
+ class Siglip2MLP(nn.Module):
314
+ def __init__(self, config):
315
+ super().__init__()
316
+ self.config = config
317
+ self.activation_fn = ACT2FN[config.hidden_act]
318
+ self.fc1 = nn.Linear(config.hidden_size, config.intermediate_size)
319
+ self.fc2 = nn.Linear(config.intermediate_size, config.hidden_size)
320
+
321
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
322
+ hidden_states = self.fc1(hidden_states)
323
+ hidden_states = self.activation_fn(hidden_states)
324
+ hidden_states = self.fc2(hidden_states)
325
+ return hidden_states
326
+
327
+
328
+ class Siglip2EncoderLayer(nn.Module):
329
+ def __init__(self, config: Siglip2NavitConfig):
330
+ super().__init__()
331
+ self.embed_dim = config.hidden_size
332
+ self.layer_norm1 = nn.LayerNorm(self.embed_dim, eps=config.layer_norm_eps)
333
+ self.self_attn = Siglip2Attention(config)
334
+ self.layer_norm2 = nn.LayerNorm(self.embed_dim, eps=config.layer_norm_eps)
335
+ self.mlp = Siglip2MLP(config)
336
+
337
+ def forward(
338
+ self,
339
+ hidden_states: torch.Tensor,
340
+ cu_seqlens: torch.Tensor,
341
+ position_embeddings: torch.Tensor
342
+ ) -> tuple[torch.FloatTensor]:
343
+ """
344
+ Args:
345
+ hidden_states (`torch.FloatTensor`):
346
+ Input to the layer of shape `(batch, seq_len, embed_dim)`.
347
+ attention_mask (`torch.FloatTensor`):
348
+ Attention mask of shape `(batch, 1, q_len, k_v_seq_len)` where padding elements are indicated by very large negative values.
349
+ output_attentions (`bool`, *optional*, defaults to `False`):
350
+ Whether or not to return the attentions tensors of all attention layers. See `attentions` under
351
+ returned tensors for more detail.
352
+ """
353
+ residual = hidden_states
354
+
355
+ hidden_states = self.layer_norm1(hidden_states)
356
+ hidden_states = self.self_attn(
357
+ hidden_states=hidden_states,
358
+ cu_seqlens=cu_seqlens,
359
+ position_embeddings=position_embeddings
360
+ )
361
+ hidden_states = residual + hidden_states
362
+
363
+ residual = hidden_states
364
+ hidden_states = self.layer_norm2(hidden_states)
365
+ hidden_states = self.mlp(hidden_states)
366
+ hidden_states = residual + hidden_states
367
+
368
+ return hidden_states
369
+
370
+ class Siglip2Encoder(nn.Module):
371
+ """
372
+ Transformer encoder consisting of `config.num_hidden_layers` self attention layers. Each layer is a
373
+ [`Siglip2EncoderLayer`].
374
+
375
+ Args:
376
+ config: Siglip2NavitConfig
377
+ """
378
+
379
+ def __init__(self, config: Siglip2NavitConfig):
380
+ super().__init__()
381
+ self.config = config
382
+ self.layers = nn.ModuleList([Siglip2EncoderLayer(config) for _ in range(config.num_hidden_layers)])
383
+ self.gradient_checkpointing = False
384
+
385
+ self.rotary_pos_emb = VisionRotaryEmbedding(config.hidden_size // config.num_attention_heads // 2)
386
+ self.patch_size = config.patch_size
387
+ self.hidden_stride = config.hidden_stride
388
+ self.window_size = config.window_size
389
+ self.spatial_merge_unit = config.hidden_stride * config.hidden_stride
390
+ self.fullatt_block_indexes = None if config.fullatt_block_indexes is None else [int(i) for i in config.fullatt_block_indexes.split('|')]
391
+
392
+
393
+ # copied from qwen2.5_vl
394
+ def rot_pos_emb(self, grid_thw):
395
+ pos_ids = []
396
+ for t, h, w in grid_thw:
397
+ hpos_ids = torch.arange(h).unsqueeze(1).expand(-1, w)
398
+ hpos_ids = hpos_ids.reshape(
399
+ h // self.hidden_stride,
400
+ self.hidden_stride,
401
+ w // self.hidden_stride,
402
+ self.hidden_stride,
403
+ )
404
+ hpos_ids = hpos_ids.permute(0, 2, 1, 3)
405
+ hpos_ids = hpos_ids.flatten()
406
+
407
+ wpos_ids = torch.arange(w).unsqueeze(0).expand(h, -1)
408
+ wpos_ids = wpos_ids.reshape(
409
+ h // self.hidden_stride,
410
+ self.hidden_stride,
411
+ w // self.hidden_stride,
412
+ self.hidden_stride,
413
+ )
414
+ wpos_ids = wpos_ids.permute(0, 2, 1, 3)
415
+ wpos_ids = wpos_ids.flatten()
416
+ pos_ids.append(torch.stack([hpos_ids, wpos_ids], dim=-1).repeat(t, 1))
417
+ pos_ids = torch.cat(pos_ids, dim=0)
418
+ max_grid_size = grid_thw[:, 1:].max()
419
+ rotary_pos_emb_full = self.rotary_pos_emb(max_grid_size)
420
+ rotary_pos_emb = rotary_pos_emb_full[pos_ids].flatten(1)
421
+ return rotary_pos_emb
422
+
423
+ def get_window_index(self, grid_thw):
424
+ window_index: list = []
425
+ cu_window_seqlens: list = [0]
426
+ window_index_id = 0
427
+ vit_merger_window_size = self.window_size // self.hidden_stride // self.patch_size # patch (after merge) number in each window
428
+
429
+ for grid_t, grid_h, grid_w in grid_thw:
430
+ llm_grid_h, llm_grid_w = (
431
+ grid_h // self.hidden_stride, # number of patch after merge
432
+ grid_w // self.hidden_stride,
433
+ )
434
+ index = torch.arange(grid_t * llm_grid_h * llm_grid_w).reshape(grid_t, llm_grid_h, llm_grid_w)
435
+ pad_h = vit_merger_window_size - llm_grid_h % vit_merger_window_size
436
+ pad_w = vit_merger_window_size - llm_grid_w % vit_merger_window_size
437
+ num_windows_h = (llm_grid_h + pad_h) // vit_merger_window_size
438
+ num_windows_w = (llm_grid_w + pad_w) // vit_merger_window_size
439
+ index_padded = F.pad(index, (0, pad_w, 0, pad_h), "constant", -100)
440
+ index_padded = index_padded.reshape(
441
+ grid_t,
442
+ num_windows_h,
443
+ vit_merger_window_size,
444
+ num_windows_w,
445
+ vit_merger_window_size,
446
+ )
447
+ index_padded = index_padded.permute(0, 1, 3, 2, 4).reshape(
448
+ grid_t,
449
+ num_windows_h * num_windows_w,
450
+ vit_merger_window_size,
451
+ vit_merger_window_size,
452
+ )
453
+ seqlens = (index_padded != -100).sum([2, 3]).reshape(-1)
454
+ index_padded = index_padded.reshape(-1)
455
+ index_new = index_padded[index_padded != -100]
456
+ window_index.append(index_new + window_index_id)
457
+ cu_seqlens_tmp = seqlens.cumsum(0) * self.spatial_merge_unit + cu_window_seqlens[-1]
458
+ cu_window_seqlens.extend(cu_seqlens_tmp.tolist())
459
+ window_index_id += (grid_t * llm_grid_h * llm_grid_w).item()
460
+ window_index = torch.cat(window_index, dim=0)
461
+
462
+ return window_index, cu_window_seqlens
463
+
464
+ # Ignore copy
465
+ def forward(
466
+ self,
467
+ inputs_embeds,
468
+ grid_thws: torch.Tensor,
469
+ output_hidden_states: bool = False,
470
+ ) -> Tuple[torch.Tensor, Optional[Tuple[torch.Tensor, ...]]]:
471
+ r"""
472
+ Args:
473
+ inputs_embeds (`torch.FloatTensor` of shape `(batch_size, sequence_length, hidden_size)`):
474
+ Optionally, instead of passing `input_ids` you can choose to directly pass an embedded representation.
475
+ This is useful if you want more control over how to convert `input_ids` indices into associated vectors
476
+ than the model's internal embedding lookup matrix.
477
+ attention_mask (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*):
478
+ Mask to avoid performing attention on padding token indices. Mask values selected in `[0, 1]`:
479
+
480
+ - 1 for tokens that are **not masked**,
481
+ - 0 for tokens that are **masked**.
482
+
483
+ [What are attention masks?](../glossary#attention-mask)
484
+ output_attentions (`bool`, *optional*):
485
+ Whether or not to return the attentions tensors of all attention layers. See `attentions` under
486
+ returned tensors for more detail.
487
+ output_hidden_states (`bool`, *optional*):
488
+ Whether or not to return the hidden states of all layers. See `hidden_states` under returned tensors
489
+ for more detail.
490
+ return_dict (`bool`, *optional*):
491
+ Whether or not to return a [`~utils.ModelOutput`] instead of a plain tuple.
492
+ """
493
+
494
+ rotary_pos_emb = self.rot_pos_emb(grid_thws)
495
+ window_index, cu_window_seqlens = self.get_window_index(grid_thws)
496
+ cu_window_seqlens = torch.tensor(
497
+ cu_window_seqlens,
498
+ device=inputs_embeds.device,
499
+ dtype=grid_thws.dtype if torch.jit.is_tracing() else torch.int32,
500
+ )
501
+ cu_window_seqlens = torch.unique_consecutive(cu_window_seqlens)
502
+
503
+ seq_len, _ = inputs_embeds.size()
504
+ inputs_embeds = inputs_embeds.reshape(seq_len // self.spatial_merge_unit, self.spatial_merge_unit, -1)
505
+ inputs_embeds = inputs_embeds[window_index, :, :]
506
+ inputs_embeds = inputs_embeds.reshape(seq_len, -1)
507
+ rotary_pos_emb = rotary_pos_emb.reshape(seq_len // self.spatial_merge_unit, self.spatial_merge_unit, -1)
508
+ rotary_pos_emb = rotary_pos_emb[window_index, :, :]
509
+ rotary_pos_emb = rotary_pos_emb.reshape(seq_len, -1)
510
+ emb = torch.cat((rotary_pos_emb, rotary_pos_emb), dim=-1)
511
+ position_embeddings = (emb.cos(), emb.sin())
512
+
513
+ cu_seqlens = torch.repeat_interleave(grid_thws[:, 1] * grid_thws[:, 2], grid_thws[:, 0]).cumsum(
514
+ dim=0,
515
+ # Select dtype based on the following factors:
516
+ # - FA2 requires that cu_seqlens_q must have dtype int32
517
+ # - torch.onnx.export requires that cu_seqlens_q must have same dtype as grid_thw
518
+ # See https://github.com/huggingface/transformers/pull/34852 for more information
519
+ dtype=grid_thws.dtype if torch.jit.is_tracing() else torch.int32,
520
+ )
521
+ cu_seqlens = F.pad(cu_seqlens, (1, 0), value=0)
522
+
523
+ reverse_indices = torch.argsort(window_index)
524
+ encoder_states = () if output_hidden_states else None
525
+
526
+ hidden_states = inputs_embeds
527
+ for index, block in enumerate(self.layers):
528
+ if self.fullatt_block_indexes is None or index in self.fullatt_block_indexes:
529
+ cu_seqlens_tmp = cu_seqlens
530
+ else:
531
+ cu_seqlens_tmp = cu_window_seqlens
532
+ if self.gradient_checkpointing and self.training:
533
+ hidden_states = self._gradient_checkpointing_func(block.__call__, hidden_states, cu_seqlens_tmp, position_embeddings)
534
+ else:
535
+ hidden_states = block(hidden_states, cu_seqlens_tmp, position_embeddings)
536
+ if output_hidden_states:
537
+ hidden_states_ = hidden_states.reshape(seq_len // self.spatial_merge_unit, self.spatial_merge_unit, -1)
538
+ encoder_states += (hidden_states_[reverse_indices, :].reshape(seq_len, -1),)
539
+ # tokens = self.post_trunk_norm(tokens)
540
+ hidden_states = hidden_states.reshape(seq_len // self.spatial_merge_unit, self.spatial_merge_unit, -1)
541
+ hidden_states = hidden_states[reverse_indices, :].reshape(seq_len, -1)
542
+
543
+ return hidden_states, encoder_states
544
+
545
+ class Siglip2VisionTransformer(nn.Module):
546
+ def __init__(self, config: Siglip2NavitConfig):
547
+ super().__init__()
548
+ self.config = config
549
+ embed_dim = config.hidden_size
550
+
551
+ self.embeddings = Siglip2VisionEmbeddings(config)
552
+ self.encoder = Siglip2Encoder(config)
553
+ self.post_layernorm = nn.LayerNorm(embed_dim, eps=config.layer_norm_eps)
554
+ self._use_flash_attention_2 = config._attn_implementation == "flash_attention_2"
555
+
556
+ def forward(
557
+ self,
558
+ pixel_values: torch.FloatTensor,
559
+ grid_thws: torch.LongTensor,
560
+ output_hidden_states: Optional[bool] = True,
561
+ return_dict: Optional[bool] = True,
562
+ ) -> Union[
563
+ Tuple[torch.Tensor],
564
+ Tuple[torch.Tensor, Tuple[torch.Tensor, ...]],
565
+ BaseModelOutputWithNoAttention,
566
+ ]:
567
+ r"""
568
+ spatial_shapes (`torch.LongTensor` of shape `(batch_size, 2)`):
569
+ Tensor containing the spatial dimensions (height, width) of the input images.
570
+ """
571
+ # output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
572
+ # output_hidden_states = (
573
+ # output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
574
+ # )
575
+
576
+ hidden_states = self.embeddings(pixel_values, grid_thws)
577
+
578
+ last_hidden_state, hidden_states = self.encoder(hidden_states, grid_thws, output_hidden_states)
579
+ last_hidden_state = self.post_layernorm(last_hidden_state)
580
+
581
+ if not return_dict:
582
+ output = (last_hidden_state,)
583
+ output += (hidden_states,) if output_hidden_states else ()
584
+ return output
585
+
586
+ return BaseModelOutputWithNoAttention(
587
+ last_hidden_state=last_hidden_state,
588
+ hidden_states=hidden_states
589
+ )
590
+
591
+ class Siglip2PreTrainedModel(PreTrainedModel):
592
+ config_class = Siglip2NavitConfig
593
+ base_model_prefix = "siglip2_navit"
594
+ supports_gradient_checkpointing = True
595
+
596
+ _no_split_modules = [
597
+ "Siglip2VisionEmbeddings",
598
+ "Siglip2EncoderLayer",
599
+ ]
600
+ _supports_flash_attn_2 = True
601
+ _supports_sdpa = False
602
+ _supports_flex_attn = False
603
+ _supports_attention_backend = True
604
+
605
+
606
+ class Siglip2NavitModel(Siglip2PreTrainedModel):
607
+ config_class = Siglip2NavitConfig
608
+ main_input_name = "pixel_values"
609
+
610
+ def __init__(self, config: Siglip2NavitConfig):
611
+ super().__init__(config)
612
+
613
+ self.vision_model = Siglip2VisionTransformer(config)
614
+
615
+ def get_input_embeddings(self) -> nn.Module:
616
+ return self.vision_model.embeddings.patch_embedding
617
+
618
+ def forward(
619
+ self,
620
+ pixel_values: torch.FloatTensor,
621
+ grid_thws: torch.LongTensor,
622
+ output_hidden_states: Optional[bool] = None,
623
+ return_dict: Optional[bool] = None,
624
+ ) -> Union[
625
+ Tuple[torch.Tensor],
626
+ Tuple[torch.Tensor, Tuple[torch.Tensor, ...]],
627
+ BaseModelOutputWithNoAttention,
628
+ ]:
629
+
630
+ if output_hidden_states is None:
631
+ output_hidden_states = self.config.output_hidden_states
632
+ if return_dict is None:
633
+ return_dict = self.config.use_return_dict
634
+
635
+ return self.vision_model(
636
+ pixel_values=pixel_values,
637
+ grid_thws=grid_thws,
638
+ output_hidden_states=output_hidden_states,
639
+ return_dict=return_dict,
640
+ )
641
+
642
+ class OXREmbedding(torch.nn.Embedding):
643
+ """
644
+ A visual embedding layer that can handle both discrete token IDs (long) and continuous
645
+ soft-token probabilities (float).
646
+ """
647
+
648
+ def forward(self, visual_tokens: Tensor) -> Tensor:
649
+ if visual_tokens.dtype in [torch.int8, torch.int16, torch.int32, torch.int64, torch.long]:
650
+ return super().forward(visual_tokens)
651
+ # Handle soft tokens (probabilities) by matrix multiplication with the embedding weight
652
+ return torch.matmul(visual_tokens, self.weight)
653
+
654
+
655
+ class OXRVisualTokenizer(torch.nn.Module):
656
+ """
657
+ Tokenizes images or videos into a sequence of continuous visual tokens.
658
+ """
659
+
660
+ def __init__(self, vit, visual_vocab_size, image_processor_name_or_path, *args, **kwargs):
661
+ super().__init__(*args, **kwargs)
662
+ self.vit = vit
663
+ image_processor_kwargs = {"do_center_crop": False}
664
+ if int(TRANSFORMERS_VERSION.split(".", 1)[0]) >= 5:
665
+ image_processor_kwargs["backend"] = "pil"
666
+ else:
667
+ image_processor_kwargs["use_fast"] = False
668
+ self.image_processor = AutoImageProcessor.from_pretrained(
669
+ image_processor_name_or_path,
670
+ **image_processor_kwargs,
671
+ )
672
+ head_dim = visual_vocab_size - len(INDICATOR_IDS)
673
+ self.head = torch.nn.Sequential(
674
+ torch.nn.Linear(self.vit.config.hidden_size * self.vit.config.hidden_stride ** 2, head_dim, bias=False),
675
+ torch.nn.LayerNorm(head_dim)
676
+ )
677
+
678
+ def _encode(self, pixel_values, grid_thws):
679
+ output = self.vit(pixel_values, grid_thws, output_hidden_states=True, return_dict=True)
680
+ features = output.hidden_states[-1]
681
+ seq_len, _ = features.shape
682
+ features = features.reshape(seq_len // (self.vit.config.hidden_stride ** 2), -1)
683
+ return features
684
+
685
+ # Adapted from qwen2_vl
686
+ @staticmethod
687
+ def smart_resize(
688
+ height: int, width: int, factor: int = 28, min_pixels: int = 448 * 448, max_pixels: int = 1344 * 1792
689
+ ):
690
+ """Rescales the image so that the following conditions are met:
691
+ 1. Both dimensions are divisible by 'factor'.
692
+ 2. The total number of pixels is within ['min_pixels', 'max_pixels'].
693
+ 3. The aspect ratio is maintained as closely as possible.
694
+ """
695
+ if height < factor or width < factor:
696
+ if height < width:
697
+ width = round(factor / height * width)
698
+ height = factor
699
+ else:
700
+ height = round(factor / width * height)
701
+ width = factor
702
+
703
+ elif max(height, width) / min(height, width) > 200:
704
+ if height > width:
705
+ height = 200 * width
706
+ else:
707
+ width = 200 * height
708
+
709
+ h_bar = round(height / factor) * factor
710
+ w_bar = round(width / factor) * factor
711
+ if h_bar * w_bar > max_pixels:
712
+ beta = math.sqrt((height * width) / max_pixels)
713
+ h_bar = math.floor(height / beta / factor) * factor
714
+ w_bar = math.floor(width / beta / factor) * factor
715
+ elif h_bar * w_bar < min_pixels:
716
+ beta = math.sqrt(min_pixels / (height * width))
717
+ h_bar = math.ceil(height * beta / factor) * factor
718
+ w_bar = math.ceil(width * beta / factor) * factor
719
+ return h_bar, w_bar
720
+
721
+ def preprocess(
722
+ self,
723
+ image: Optional[PIL.Image.Image] = None,
724
+ video: Optional[List[PIL.Image.Image]] = None,
725
+ min_pixels: Optional[int] = None,
726
+ max_pixels: Optional[int] = None
727
+ ):
728
+ patch_size = self.vit.config.patch_size
729
+ temporal_patch_size = self.vit.config.temporal_patch_size
730
+ hidden_stride = self.vit.config.hidden_stride
731
+ assert (image is None) ^ (video is None), "Invalid input: expect either image or video"
732
+ if image is not None:
733
+ images = [image]
734
+ else:
735
+ images = video
736
+ images = [image.convert("RGB") if image.mode != 'RGB' else image for image in images]
737
+ width, height = images[0].size
738
+ processed_images = []
739
+ for image in images:
740
+ resized_height, resized_width = self.smart_resize(
741
+ height,
742
+ width,
743
+ factor=patch_size * hidden_stride,
744
+ min_pixels=min_pixels,
745
+ max_pixels=max_pixels,
746
+ )
747
+ new_size = dict(height=resized_height, width=resized_width)
748
+ new_image = self.image_processor.preprocess(image, size=new_size, return_tensors="np")['pixel_values'][0]
749
+ processed_images.append(new_image)
750
+
751
+ patches = np.array(processed_images)
752
+ if patches.shape[0] % temporal_patch_size != 0:
753
+ repeats = np.repeat(patches[-1][np.newaxis], temporal_patch_size - 1, axis=0)
754
+ patches = np.concatenate([patches, repeats], axis=0)
755
+ channel = patches.shape[1]
756
+ grid_t = patches.shape[0] // temporal_patch_size
757
+ grid_h, grid_w = resized_height // patch_size, resized_width // patch_size
758
+ grid_thw = torch.tensor([[grid_t, grid_h, grid_w]])
759
+
760
+ patches = patches.reshape(
761
+ grid_t, temporal_patch_size, channel,
762
+ grid_h // hidden_stride, hidden_stride, patch_size,
763
+ grid_w // hidden_stride, hidden_stride, patch_size,
764
+ )
765
+ patches = patches.transpose(0, 3, 6, 4, 7, 2, 1, 5, 8)
766
+ flatten_patches = patches.reshape(
767
+ grid_t * grid_h * grid_w, channel * temporal_patch_size * patch_size * patch_size
768
+ )
769
+ flatten_patches = torch.tensor(flatten_patches)
770
+
771
+ return flatten_patches, grid_thw
772
+
773
+ def forward(
774
+ self, pixel_values, grid_thws
775
+ ) -> torch.Tensor: # [BatchSize, ImageShape] -> [BatchSize, #Token, VocabSize]
776
+ features = self._encode(pixel_values, grid_thws)
777
+ logits = self.head(features)
778
+ tokens = torch.softmax(logits, dim=-1, dtype=torch.float32).to(logits.dtype)
779
+
780
+ token_len, _ = tokens.shape
781
+ padding_tensor = torch.zeros(size=(token_len, len(INDICATOR_IDS)),
782
+ dtype=tokens.dtype,
783
+ device=tokens.device,
784
+ layout=tokens.layout,
785
+ requires_grad=False)
786
+ tokens = torch.cat((tokens, padding_tensor), dim=1)
787
+ return tokens
788
+
789
+
790
+ class OXRPreTrainedModel(PreTrainedModel):
791
+ config_class = OXRConfig
792
+ base_model_prefix = "oxr"
793
+
794
+
795
+ class OXR(OXRPreTrainedModel):
796
+ _supports_flash_attn_2 = True
797
+
798
+ def __init__(self, config: OXRConfig, *inputs, **kwargs):
799
+ super().__init__(config, *inputs, **kwargs)
800
+
801
+ self.llm = AutoModelForCausalLM.from_config(self.config.llm_config)
802
+ assert self.config.hidden_size == self.llm.config.hidden_size, "hidden size mismatch"
803
+ self.text_tokenizer = AutoTokenizer.from_pretrained(
804
+ self.config.name_or_path,
805
+ trust_remote_code=True,
806
+ )
807
+ self.visual_tokenizer = OXRVisualTokenizer(vit=AutoModel.from_config(self.config.vit_config),
808
+ visual_vocab_size=self.config.visual_vocab_size,
809
+ image_processor_name_or_path=self.config.name_or_path)
810
+
811
+ self.vte = OXREmbedding(self.config.visual_vocab_size, self.config.hidden_size,
812
+ device=self.visual_tokenizer.vit.device, dtype=self.visual_tokenizer.vit.dtype)
813
+ indicator_token_indices = torch.arange(
814
+ self.config.visual_vocab_size - len(INDICATOR_IDS),
815
+ self.config.visual_vocab_size,
816
+ dtype=torch.long
817
+ )
818
+ self.register_buffer("indicator_token_indices", indicator_token_indices, persistent=False)
819
+
820
+ def _merge_modules(modules_list: tuple):
821
+ merged_modules = []
822
+ for modules in modules_list:
823
+ merged_modules.extend(modules if modules else [])
824
+ return merged_modules
825
+
826
+ # Standard model configurations for parallelism and device placement
827
+ self._no_split_modules = _merge_modules(
828
+ (self.llm._no_split_modules, self.visual_tokenizer.vit._no_split_modules))
829
+ self._skip_keys_device_placement = self.llm._skip_keys_device_placement
830
+ self._keep_in_fp32_modules = _merge_modules(
831
+ (self.llm._keep_in_fp32_modules, self.visual_tokenizer.vit._keep_in_fp32_modules))
832
+ self.is_parallelizable = all((
833
+ getattr(self.llm, "is_parallelizable", False),
834
+ getattr(self.visual_tokenizer.vit, "is_parallelizable", False),
835
+ ))
836
+ self.supports_gradient_checkpointing = True
837
+ self.post_init()
838
+
839
+ def tie_weights(self, missing_keys: Optional[set[str]] = None, recompute_mapping: bool = True):
840
+ self.llm.tie_weights()
841
+ try:
842
+ return PreTrainedModel.tie_weights(
843
+ self,
844
+ missing_keys=missing_keys,
845
+ recompute_mapping=recompute_mapping,
846
+ )
847
+ except TypeError as exc:
848
+ if "unexpected keyword argument" not in str(exc):
849
+ raise
850
+ return PreTrainedModel.tie_weights(self)
851
+
852
+ def get_wte(self):
853
+ return self.llm.get_input_embeddings()
854
+
855
+ def forward(
856
+ self,
857
+ input_ids: torch.Tensor,
858
+ attention_mask: torch.Tensor,
859
+ pixel_values: Optional[torch.Tensor],
860
+ grid_thws: Optional[torch.Tensor],
861
+ labels: Optional[torch.Tensor] = None,
862
+ **kwargs
863
+ ):
864
+ inputs_embeds = self.merge_multimodal(
865
+ input_ids=input_ids,
866
+ pixel_values=pixel_values,
867
+ grid_thws=grid_thws,
868
+ )
869
+ return self.llm(inputs_embeds=inputs_embeds, attention_mask=attention_mask, labels=labels, **kwargs)
870
+
871
+ def merge_multimodal(
872
+ self,
873
+ input_ids: torch.Tensor,
874
+ pixel_values: Optional[torch.Tensor],
875
+ grid_thws: Optional[torch.Tensor],
876
+ ):
877
+ placeholder_token_mask = torch.lt(input_ids, 0)
878
+ multimodal_embeds = self.get_wte()(torch.masked_fill(input_ids, placeholder_token_mask, 0))
879
+
880
+ if pixel_values is not None:
881
+ visual_indicator_embeds = self.vte(self.indicator_token_indices).to(
882
+ dtype=multimodal_embeds.dtype, device=multimodal_embeds.device
883
+ )
884
+ visual_tokens = self.visual_tokenizer(pixel_values, grid_thws)
885
+ visual_embeds = self.vte(visual_tokens).to(dtype=multimodal_embeds.dtype, device=multimodal_embeds.device)
886
+
887
+ for i, indicator_id in enumerate(INDICATOR_IDS):
888
+ multimodal_embeds[input_ids == indicator_id] = visual_indicator_embeds[i]
889
+ multimodal_embeds[input_ids == VISUAL_ATOM_ID] = visual_embeds
890
+
891
+ return multimodal_embeds
892
+
893
+ def _merge_inputs(
894
+ self, raw_input_ids, placeholder_id, grid_thws, indicator_begin_id, indicator_end_id
895
+ ):
896
+ input_ids = []
897
+ prev_index = 0
898
+ placeholder_indexes = [i for i, v in enumerate(raw_input_ids) if v == placeholder_id]
899
+ for placeholder_index, grid_thw in zip(placeholder_indexes, grid_thws):
900
+ input_ids.extend(raw_input_ids[prev_index:placeholder_index])
901
+ num_image_atoms = grid_thw.prod().item()
902
+ num_image_atoms //= self.visual_tokenizer.vit.config.hidden_stride ** 2
903
+ num_image_atoms //= self.visual_tokenizer.vit.config.temporal_patch_size
904
+ input_ids.extend([indicator_begin_id] + [VISUAL_ATOM_ID] * num_image_atoms + [indicator_end_id])
905
+ prev_index = placeholder_index + 1
906
+ input_ids.extend(raw_input_ids[prev_index:])
907
+ return input_ids
908
+
909
+ def _tokenize_with_visual_placeholder(self, text):
910
+ placeholder = VIDEO_PLACEHOLDER if VIDEO_PLACEHOLDER in text else IMAGE_PLACEHOLDER
911
+ placeholder_id = VIDEO_PLACEHOLDER_ID if VIDEO_PLACEHOLDER in text else IMAGE_PLACEHOLDER_ID
912
+ chunks = [self.text_tokenizer(chunk, add_special_tokens=False).input_ids for chunk in text.split(placeholder)]
913
+ input_ids = chunks[0]
914
+ for chunk in chunks[1:]:
915
+ input_ids.append(placeholder_id)
916
+ input_ids.extend(chunk)
917
+ return input_ids
918
+
919
+ def preprocess_inputs(
920
+ self,
921
+ messages: List[Union[str, Dict]],
922
+ min_pixels=448 * 448,
923
+ max_pixels=1344 * 1792,
924
+ add_generation_prompt=True,
925
+ enable_thinking=False
926
+ ):
927
+ text = self.text_tokenizer.apply_chat_template(
928
+ messages,
929
+ tokenize=False,
930
+ add_generation_prompt=add_generation_prompt,
931
+ enable_thinking=enable_thinking
932
+ )
933
+ input_ids = self._tokenize_with_visual_placeholder(text)
934
+ images = []
935
+ videos = []
936
+ for message in messages:
937
+ content = message["content"]
938
+ if isinstance(content, list):
939
+ images.extend([item["image"] for item in content if item.get("image") is not None])
940
+ videos.extend([item["video"] for item in content if item.get("video") is not None])
941
+ if images and videos:
942
+ raise ValueError(
943
+ "Multiple visual input data types detected (both image and video provided). "
944
+ "This model supports only one type of visual input data at a time. "
945
+ "Please provide either image or video, but not both."
946
+ )
947
+
948
+ pixel_values, grid_thws = None, None
949
+ if images:
950
+ pixel_values, grid_thws = zip(
951
+ *(self.visual_tokenizer.preprocess(image=image, min_pixels=min_pixels, max_pixels=max_pixels)
952
+ for image in images)
953
+ )
954
+ input_ids = self._merge_inputs(
955
+ input_ids, IMAGE_PLACEHOLDER_ID, grid_thws, INDICATOR_IDS[0], INDICATOR_IDS[1]
956
+ )
957
+ pixel_values = torch.cat(pixel_values, dim=0)
958
+ grid_thws = torch.cat(grid_thws, dim=0)
959
+ elif videos:
960
+ assert len(videos) == 1, "only support single video"
961
+ pixel_values, grid_thws = self.visual_tokenizer.preprocess(
962
+ video=videos[0], min_pixels=min_pixels, max_pixels=max_pixels
963
+ )
964
+ input_ids = self._merge_inputs(
965
+ input_ids, VIDEO_PLACEHOLDER_ID, grid_thws, INDICATOR_IDS[2], INDICATOR_IDS[3]
966
+ )
967
+
968
+ input_ids = torch.tensor(input_ids, dtype=torch.long).unsqueeze(0)
969
+
970
+ return input_ids, pixel_values, grid_thws
971
+
972
+ def generate(
973
+ self,
974
+ inputs: Optional[torch.Tensor] = None,
975
+ **kwargs,
976
+ ) -> Union[GenerateOutput, torch.LongTensor]:
977
+ attention_mask = torch.ne(inputs, self.text_tokenizer.pad_token_id).to(device=inputs.device)
978
+ inputs_embeds = self.merge_multimodal(
979
+ input_ids=inputs,
980
+ pixel_values=kwargs.pop('pixel_values', None),
981
+ grid_thws=kwargs.pop('grid_thws', None)
982
+ )
983
+ enable_thinking = kwargs.pop('enable_thinking', False)
984
+ enable_thinking_budget = kwargs.pop('enable_thinking_budget', False)
985
+ thinking_budget = kwargs.pop('thinking_budget', 1024)
986
+
987
+ if enable_thinking and enable_thinking_budget:
988
+ actual_max_new_tokens = kwargs['max_new_tokens']
989
+ kwargs['max_new_tokens'] = thinking_budget
990
+ generated_ids = self.llm.generate(inputs=None, inputs_embeds=inputs_embeds, attention_mask=attention_mask, **kwargs)
991
+ output_ids = generated_ids
992
+ output_ids_list = generated_ids[0]
993
+
994
+ # check if the generation has already finished (151645 is <|im_end|>)
995
+ if 151645 not in output_ids_list:
996
+ # check if the thinking process has finished (151668 is </think>)
997
+ # and prepare the second model input
998
+ if 151668 not in output_ids_list:
999
+ early_stopping_text = "\n\nConsidering the limited time by the user, I have to give the solution based on the thinking directly now.\n</think>\n\n"
1000
+ early_stopping_ids = self.text_tokenizer(early_stopping_text, return_tensors="pt", return_attention_mask=False).input_ids.to(inputs.device)
1001
+ input_ids_appendent = torch.cat([output_ids, early_stopping_ids], dim=-1)
1002
+ kwargs['streamer'].put(early_stopping_ids) if 'streamer' in kwargs else None
1003
+ else:
1004
+ input_ids_appendent = output_ids
1005
+
1006
+
1007
+ # second generation
1008
+ new_inputs = torch.cat([inputs, input_ids_appendent], dim=-1)
1009
+ attention_mask = torch.ne(new_inputs, self.text_tokenizer.pad_token_id).to(device=inputs.device)
1010
+ inputs_embeds_appendent = self.merge_multimodal(
1011
+ input_ids=input_ids_appendent,
1012
+ pixel_values=None,
1013
+ grid_thws=None
1014
+ )
1015
+ new_inputs_embeds = torch.cat([inputs_embeds, inputs_embeds_appendent], dim=-2)
1016
+
1017
+ kwargs['max_new_tokens'] = inputs_embeds.size(-2) + actual_max_new_tokens - new_inputs_embeds.size(-2)
1018
+ generated_ids2 = self.llm.generate(inputs=None, inputs_embeds=new_inputs_embeds, attention_mask=attention_mask, **kwargs)
1019
+ kwargs['streamer'].manual_end() if 'streamer' in kwargs else None
1020
+ return torch.cat([input_ids_appendent, generated_ids2], dim=-1)
1021
+
1022
+ else:
1023
+ kwargs['streamer'].manual_end() if 'streamer' in kwargs else None
1024
+ return generated_ids
1025
+
1026
+ else:
1027
+ generated_ids = self.llm.generate(inputs=None, inputs_embeds=inputs_embeds, attention_mask=attention_mask, **kwargs)
1028
+ kwargs['streamer'].manual_end() if 'streamer' in kwargs else None
1029
+ return generated_ids
1030
+
1031
+
1032
+ AutoConfig.register('siglip2_navit', Siglip2NavitConfig)
1033
+ AutoModel.register(Siglip2NavitConfig, Siglip2NavitModel)
1034
+ try:
1035
+ AutoModelForCausalLM.register(OXRConfig, OXR)
1036
+ except ValueError:
1037
+ pass
plugins/vllm/README.md ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # OXR vLLM plugin
2
+
3
+ Registers the OXR architecture through `vllm.general_plugins`. Requires
4
+ Python 3.12+, vLLM 0.25.1 and Transformers 5.13.1. It does not modify the
5
+ vLLM installation. The supported Docker runtime uses PyTorch 2.11.0+cu130.
6
+
7
+ Install from the model repository root in a Linux CUDA environment:
8
+
9
+ ```bash
10
+ python -m pip install ./plugins/vllm
11
+ ```
12
+
13
+ Start with the model files and weights in the repository root:
14
+
15
+ ```bash
16
+ VLLM_PLUGINS=oxr vllm serve . \
17
+ --trust-remote-code \
18
+ --served-model-name oxr \
19
+ --chat-template ./chat_template.jinja \
20
+ --dtype bfloat16 \
21
+ --attention-config '{"backend":"TRITON_ATTN"}' \
22
+ --max-model-len 16384
23
+ ```
24
+
25
+ The LA model in `layout_analysis/` is served separately by the OXR layout
26
+ analysis service, not by this plugin.
27
+
28
+ To roll back to vLLM 0.19.1, use the previous model plugin together with the
29
+ matching previous OXR launcher and Docker image. Do not install this plugin
30
+ into the old runtime.
plugins/vllm/oxr_vllm/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Out-of-tree OXR model registration for vLLM."""
2
+
3
+
4
+ def register() -> None:
5
+ """Register lazily so plugin discovery does not initialize CUDA."""
6
+ from vllm.model_executor.models.registry import ModelRegistry
7
+
8
+ ModelRegistry.register_model("OXR", "oxr_vllm.oxr_model:OXR")
9
+
10
+
11
+ __all__ = ["register"]
plugins/vllm/oxr_vllm/oxr_model.py ADDED
@@ -0,0 +1,660 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """OXR model implementation for vLLM 0.25.1."""
2
+
3
+ from collections.abc import Iterable, Mapping
4
+ from functools import partial
5
+ from typing import Annotated, Literal
6
+
7
+ import torch
8
+ import torch.nn as nn
9
+ from transformers import BaseImageProcessor, BatchFeature, PretrainedConfig
10
+
11
+ from vllm.config import VllmConfig
12
+ from vllm.config.multimodal import BaseDummyOptions
13
+ from vllm.inputs import MultiModalDataDict
14
+ from vllm.model_executor.layers.linear import ReplicatedLinear
15
+ from vllm.model_executor.layers.quantization import QuantizationConfig
16
+ from vllm.model_executor.models.interfaces import (
17
+ MultiModalEmbeddings,
18
+ SupportsMultiModal,
19
+ SupportsPP,
20
+ )
21
+ from vllm.model_executor.models.siglip2navit import Siglip2NavitModel
22
+ from vllm.model_executor.models.utils import (
23
+ AutoWeightsLoader,
24
+ flatten_bn,
25
+ init_vllm_registered_model,
26
+ maybe_prefix,
27
+ )
28
+ from vllm.multimodal import MULTIMODAL_REGISTRY
29
+ from vllm.multimodal.inputs import MultiModalFieldConfig, MultiModalKwargsItems
30
+ from vllm.multimodal.parse import ImageSize, MultiModalDataItems
31
+ from vllm.multimodal.processing import (
32
+ BaseDummyInputsBuilder,
33
+ BaseMultiModalProcessor,
34
+ BaseProcessingInfo,
35
+ PromptReplacement,
36
+ )
37
+ from vllm.renderers import TokenizeParams
38
+ from vllm.sequence import IntermediateTensors
39
+ from vllm.utils.tensor_schema import TensorSchema, TensorShape
40
+
41
+ from .oxr_processor import OXRProcessor
42
+
43
+ IMAGE_TOKEN = "<image>"
44
+ VIDEO_TOKEN = "<video>"
45
+ IMAGE_PLACEHOLDER_ID = -200
46
+ VIDEO_PLACEHOLDER_ID = -201
47
+ VISUAL_ATOM_ID = -300
48
+ OXR_INDICATOR_IDS = [-301, -302, -303, -304]
49
+
50
+ IMAGE_PAD_TOKEN_MAP = {
51
+ "gemma2": "<unused0>",
52
+ "llama": "<|reserved_special_token_0|>",
53
+ "qwen2": "<|image_pad|>",
54
+ "qwen3": "<|image_pad|>",
55
+ }
56
+ IMAGE_PAD_TOKEN_ID_MAP = {
57
+ "gemma2": 7,
58
+ "llama": 128002,
59
+ "qwen2": 151655,
60
+ "qwen3": 151655,
61
+ }
62
+
63
+
64
+ def get_llm_config(config: PretrainedConfig) -> PretrainedConfig:
65
+ llm_config = getattr(config, "llm_config", None)
66
+ if llm_config is not None:
67
+ return llm_config
68
+
69
+ text_config = getattr(config, "text_config", None)
70
+ if text_config is not None:
71
+ return text_config
72
+
73
+ get_text_config = getattr(config, "get_text_config", None)
74
+ if get_text_config is not None:
75
+ candidate = get_text_config()
76
+ if candidate is not config:
77
+ return candidate
78
+
79
+ raise ValueError("OXR config does not expose an LLM sub-config")
80
+
81
+
82
+ def _oxr_field_config() -> Mapping[str, MultiModalFieldConfig]:
83
+ return dict(
84
+ pixel_values=MultiModalFieldConfig.batched("image"),
85
+ grids=MultiModalFieldConfig.batched("image"),
86
+ indicator_tokens=MultiModalFieldConfig.batched("image"),
87
+ video_pixel_values=MultiModalFieldConfig.batched("video"),
88
+ video_indicator_tokens=MultiModalFieldConfig.batched("video"),
89
+ video_grids=MultiModalFieldConfig.batched("video"),
90
+ )
91
+
92
+
93
+ class OXRImagePatchInputs(TensorSchema):
94
+ type: Literal["image_patches"]
95
+ flat_data: Annotated[torch.Tensor, TensorShape("bnp", "patch_size")]
96
+ indicator_tokens: Annotated[torch.Tensor, TensorShape("indicators")]
97
+ patches_per_item: Annotated[list[int], TensorShape("bn")]
98
+ grids: Annotated[torch.Tensor, TensorShape("bn", 3)]
99
+
100
+
101
+ class OXRVideoPatchInputs(TensorSchema):
102
+ type: Literal["video_patches"]
103
+ flat_data: Annotated[torch.Tensor, TensorShape("bnp", "patch_size")]
104
+ indicator_tokens: Annotated[torch.Tensor, TensorShape("indicators")]
105
+ patches_per_item: Annotated[list[int], TensorShape("bn")]
106
+ grids: Annotated[torch.Tensor, TensorShape("bn", 3)]
107
+
108
+
109
+ class OXREmbedding(torch.nn.Embedding):
110
+ def forward(self, visual_tokens: torch.Tensor) -> torch.Tensor:
111
+ if visual_tokens.dtype in (
112
+ torch.int8,
113
+ torch.int16,
114
+ torch.int32,
115
+ torch.int64,
116
+ torch.long,
117
+ ):
118
+ return super().forward(visual_tokens)
119
+ return torch.matmul(visual_tokens, self.weight)
120
+
121
+ @property
122
+ def device(self) -> torch.device:
123
+ return self.weight.device
124
+
125
+ @property
126
+ def dtype(self) -> torch.dtype:
127
+ return self.weight.dtype
128
+
129
+
130
+ class OXRVisualTokenizer(torch.nn.Module):
131
+ def __init__(
132
+ self,
133
+ config: PretrainedConfig,
134
+ visual_vocab_size: int,
135
+ quant_config: QuantizationConfig | None = None,
136
+ prefix: str = "",
137
+ ):
138
+ super().__init__()
139
+ self.config = config
140
+ self.vit = self._init_backbone(
141
+ config=config,
142
+ quant_config=quant_config,
143
+ prefix=f"{prefix}.vit",
144
+ )
145
+ head_dim = visual_vocab_size - len(OXR_INDICATOR_IDS)
146
+ self.head = torch.nn.Sequential(
147
+ ReplicatedLinear(
148
+ self.config.hidden_size * self.config.hidden_stride**2,
149
+ head_dim,
150
+ bias=False,
151
+ return_bias=False,
152
+ quant_config=quant_config,
153
+ prefix=f"{prefix}.head.0",
154
+ ),
155
+ torch.nn.LayerNorm(head_dim),
156
+ )
157
+
158
+ def _init_backbone(
159
+ self,
160
+ config: PretrainedConfig,
161
+ quant_config: QuantizationConfig | None = None,
162
+ prefix: str = "",
163
+ ):
164
+ if config.model_type == "siglip2_navit":
165
+ return Siglip2NavitModel(
166
+ config=config,
167
+ quant_config=quant_config,
168
+ prefix=prefix,
169
+ )
170
+ raise ValueError(f"Unsupported visual tokenizer model_type: {config.model_type}")
171
+
172
+ @property
173
+ def dtype(self) -> torch.dtype:
174
+ return next(self.head.parameters()).dtype
175
+
176
+ @property
177
+ def device(self) -> torch.device:
178
+ return next(self.head.parameters()).device
179
+
180
+ def tokenize(self, logits: torch.Tensor) -> torch.Tensor:
181
+ return torch.softmax(logits, dim=-1, dtype=torch.float32).to(logits.dtype)
182
+
183
+ def encode(
184
+ self,
185
+ pixel_values: torch.Tensor,
186
+ grid_thws: torch.Tensor,
187
+ ) -> torch.Tensor:
188
+ features = self.vit(pixel_values, grid_thws)
189
+ seq_len, _ = features.shape
190
+ return features.reshape(seq_len // (self.config.hidden_stride**2), -1)
191
+
192
+ def forward(
193
+ self,
194
+ pixel_values: torch.Tensor,
195
+ grid_thws: torch.Tensor,
196
+ ) -> torch.Tensor:
197
+ features = self.encode(pixel_values, grid_thws)
198
+ logits = self.head(features)
199
+ tokens = self.tokenize(logits)
200
+ return torch.nn.functional.pad(
201
+ tokens,
202
+ (0, len(OXR_INDICATOR_IDS)),
203
+ mode="constant",
204
+ value=0,
205
+ )
206
+
207
+
208
+ class OXRProcessingInfo(BaseProcessingInfo):
209
+ def get_hf_config(self):
210
+ return self.ctx.get_hf_config()
211
+
212
+ def get_hf_processor(self, **kwargs):
213
+ vit_config = self.get_hf_config().vit_config
214
+ return self.ctx.get_hf_processor(
215
+ OXRProcessor,
216
+ image_pad_token=self.get_image_pad_token(),
217
+ patch_size=vit_config.patch_size,
218
+ hidden_stride=vit_config.hidden_stride,
219
+ temporal_patch_size=vit_config.temporal_patch_size,
220
+ **kwargs,
221
+ )
222
+
223
+ def get_default_tok_params(self) -> TokenizeParams:
224
+ return super().get_default_tok_params().with_kwargs(add_special_tokens=False)
225
+
226
+ def get_image_pad_token(self) -> str:
227
+ text_model_type = get_llm_config(self.get_hf_config()).model_type
228
+ return IMAGE_PAD_TOKEN_MAP[text_model_type]
229
+
230
+ def get_image_processor(self) -> BaseImageProcessor:
231
+ return self.get_hf_processor().image_processor # type: ignore[return-value]
232
+
233
+ def get_supported_mm_limits(self) -> Mapping[str, int | None]:
234
+ return {"image": None, "video": 1}
235
+
236
+ def get_image_size_with_most_features(self) -> ImageSize:
237
+ return ImageSize(width=1792, height=1792)
238
+
239
+ def get_num_image_tokens(
240
+ self,
241
+ *,
242
+ image_width: int,
243
+ image_height: int,
244
+ num_frames: int = 1,
245
+ ) -> int:
246
+ vit_config = self.get_hf_config().vit_config
247
+ padded_num_frames = num_frames + (-num_frames % vit_config.temporal_patch_size)
248
+ grid_t = max(padded_num_frames // vit_config.temporal_patch_size, 1)
249
+ grid_h = image_height // vit_config.patch_size
250
+ grid_w = image_width // vit_config.patch_size
251
+ return grid_t * grid_h * grid_w
252
+
253
+ def get_max_image_tokens(self) -> int:
254
+ target_width, target_height = self.get_image_size_with_most_features()
255
+ return self.get_num_image_tokens(
256
+ image_width=target_width,
257
+ image_height=target_height,
258
+ )
259
+
260
+ def _get_max_video_frames(self, max_tokens: int) -> int:
261
+ target_width, target_height = self.get_image_size_with_most_features()
262
+ num_frames = 0
263
+ while True:
264
+ next_num_frames = num_frames + 1
265
+ next_max_tokens = self.get_num_video_tokens(
266
+ image_width=target_width,
267
+ image_height=target_height,
268
+ num_frames=next_num_frames,
269
+ )
270
+ if next_max_tokens > max_tokens:
271
+ break
272
+ num_frames = next_num_frames
273
+ return num_frames
274
+
275
+ def get_num_frames_with_most_features(
276
+ self,
277
+ seq_len: int,
278
+ mm_counts: Mapping[str, int],
279
+ ) -> int:
280
+ max_images = mm_counts.get("image", 0)
281
+ max_videos = mm_counts.get("video", 0)
282
+ max_image_tokens = self.get_max_image_tokens() * max_images
283
+ max_total_frames = self._get_max_video_frames(seq_len - max_image_tokens)
284
+ max_frames_per_video = max_total_frames // max(max_videos, 1)
285
+ return max(max_frames_per_video, 1)
286
+
287
+ def get_num_video_tokens(
288
+ self,
289
+ *,
290
+ image_width: int,
291
+ image_height: int,
292
+ num_frames: int,
293
+ ) -> int:
294
+ return self.get_num_image_tokens(
295
+ image_width=image_width,
296
+ image_height=image_height,
297
+ num_frames=num_frames,
298
+ )
299
+
300
+ def get_max_video_tokens(
301
+ self,
302
+ seq_len: int,
303
+ mm_counts: Mapping[str, int],
304
+ ) -> int:
305
+ target_width, target_height = self.get_image_size_with_most_features()
306
+ return self.get_num_video_tokens(
307
+ image_width=target_width,
308
+ image_height=target_height,
309
+ num_frames=self.get_num_frames_with_most_features(seq_len, mm_counts),
310
+ )
311
+
312
+ def get_mm_max_tokens_per_item(
313
+ self,
314
+ seq_len: int,
315
+ mm_counts: Mapping[str, int],
316
+ ) -> Mapping[str, int]:
317
+ # Return a conservative upper bound so vLLM does not profile the
318
+ # visual tower with dozens of maximum-sized dummy images/videos.
319
+ return {
320
+ "image": self.get_max_image_tokens() + 2,
321
+ "video": self.get_max_video_tokens(seq_len, mm_counts) + 2,
322
+ }
323
+
324
+
325
+ class OXRDummyInputsBuilder(BaseDummyInputsBuilder[OXRProcessingInfo]):
326
+ def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
327
+ return IMAGE_TOKEN * mm_counts.get("image", 0) + VIDEO_TOKEN * mm_counts.get(
328
+ "video", 0
329
+ )
330
+
331
+ def get_dummy_mm_data(
332
+ self,
333
+ seq_len: int,
334
+ mm_counts: Mapping[str, int],
335
+ mm_options: Mapping[str, BaseDummyOptions],
336
+ ) -> MultiModalDataDict:
337
+ num_images = mm_counts.get("image", 0)
338
+ num_videos = mm_counts.get("video", 0)
339
+ target_width, target_height = self.info.get_image_size_with_most_features()
340
+ target_num_frames = self.info.get_num_frames_with_most_features(
341
+ seq_len, mm_counts
342
+ )
343
+
344
+ return {
345
+ "image": self._get_dummy_images(
346
+ width=target_width,
347
+ height=target_height,
348
+ num_images=num_images,
349
+ overrides=mm_options.get("image"),
350
+ ),
351
+ "video": self._get_dummy_videos(
352
+ width=target_width,
353
+ height=target_height,
354
+ num_frames=target_num_frames,
355
+ num_videos=num_videos,
356
+ overrides=mm_options.get("video"),
357
+ ),
358
+ }
359
+
360
+
361
+ class OXRMultiModalProcessor(BaseMultiModalProcessor[OXRProcessingInfo]):
362
+ def visual_indicators_to_visual_tokens(
363
+ self,
364
+ visual_indicators: list[int],
365
+ ) -> list[int]:
366
+ vocab_size = self.info.get_hf_config().visual_vocab_size
367
+ indicator_by_id = {token_id: idx for idx, token_id in enumerate(OXR_INDICATOR_IDS)}
368
+ return [
369
+ vocab_size - len(OXR_INDICATOR_IDS) + indicator_by_id[token_id]
370
+ for token_id in visual_indicators
371
+ if token_id in indicator_by_id
372
+ ]
373
+
374
+ def _call_hf_processor(
375
+ self,
376
+ prompt: str,
377
+ mm_data: Mapping[str, object],
378
+ mm_kwargs: Mapping[str, object],
379
+ tok_kwargs: Mapping[str, object],
380
+ ) -> BatchFeature:
381
+ if not mm_data:
382
+ tokenizer = self.info.get_tokenizer()
383
+ prompt_ids = tokenizer.encode(prompt, add_special_tokens=False)
384
+ return BatchFeature(dict(input_ids=[prompt_ids]), tensor_type="pt")
385
+
386
+ processed_outputs = super()._call_hf_processor(
387
+ prompt=prompt,
388
+ mm_data=mm_data,
389
+ mm_kwargs=mm_kwargs,
390
+ tok_kwargs=tok_kwargs,
391
+ )
392
+ hf_processor = self.info.get_hf_processor()
393
+
394
+ if "videos" in mm_data:
395
+ visual_indicators = [
396
+ hf_processor.construct_visual_indicators((1, 1, 1), True)
397
+ for _ in processed_outputs["video_grids"]
398
+ ]
399
+ processed_outputs["video_indicator_tokens"] = torch.tensor(
400
+ [
401
+ self.visual_indicators_to_visual_tokens(indicators)
402
+ for indicators in visual_indicators
403
+ ],
404
+ dtype=torch.long,
405
+ )
406
+
407
+ if "images" in mm_data:
408
+ visual_indicators = [
409
+ hf_processor.construct_visual_indicators((1, 1, 1), False)
410
+ for _ in processed_outputs["grids"]
411
+ ]
412
+ processed_outputs["indicator_tokens"] = torch.tensor(
413
+ [
414
+ self.visual_indicators_to_visual_tokens(indicators)
415
+ for indicators in visual_indicators
416
+ ],
417
+ dtype=torch.long,
418
+ )
419
+
420
+ return processed_outputs
421
+
422
+ def _apply_hf_processor_tokens_only(self, prompt_tokens: list[int]) -> list[int]:
423
+ return prompt_tokens
424
+
425
+ def _get_mm_fields_config(
426
+ self,
427
+ hf_inputs: BatchFeature,
428
+ hf_processor_mm_kwargs: Mapping[str, object],
429
+ ) -> Mapping[str, MultiModalFieldConfig]:
430
+ return _oxr_field_config()
431
+
432
+ def _get_prompt_updates(
433
+ self,
434
+ mm_items: MultiModalDataItems,
435
+ hf_processor_mm_kwargs: Mapping[str, object],
436
+ out_mm_kwargs: MultiModalKwargsItems,
437
+ ) -> list[PromptReplacement]:
438
+ def get_replacement(item_idx: int, modality: str) -> list[int]:
439
+ if modality == "image":
440
+ grid = out_mm_kwargs["image"][item_idx]["grids"].data
441
+ elif modality == "video":
442
+ grid = out_mm_kwargs["video"][item_idx]["video_grids"].data
443
+ else:
444
+ raise ValueError(f"Unsupported modality: {modality}")
445
+
446
+ hf_processor = self.info.get_hf_processor()
447
+ return hf_processor.construct_visual_placeholders(
448
+ grid[0],
449
+ is_video=(modality == "video"),
450
+ )
451
+
452
+ return [
453
+ PromptReplacement(
454
+ modality=modality,
455
+ target=IMAGE_TOKEN if modality == "image" else VIDEO_TOKEN,
456
+ replacement=partial(get_replacement, modality=modality),
457
+ )
458
+ for modality in ("image", "video")
459
+ ]
460
+
461
+
462
+ @MULTIMODAL_REGISTRY.register_processor(
463
+ OXRMultiModalProcessor,
464
+ info=OXRProcessingInfo,
465
+ dummy_inputs=OXRDummyInputsBuilder,
466
+ )
467
+ class OXR(nn.Module, SupportsMultiModal, SupportsPP):
468
+ @classmethod
469
+ def get_placeholder_str(cls, modality: str, i: int) -> str | None:
470
+ if modality.startswith("image"):
471
+ return IMAGE_TOKEN
472
+ if modality.startswith("video"):
473
+ return VIDEO_TOKEN
474
+ raise ValueError("Only image or video modality is supported")
475
+
476
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
477
+ super().__init__()
478
+ config = vllm_config.model_config.hf_config
479
+ quant_config = vllm_config.quant_config
480
+
481
+ self.config: PretrainedConfig = config
482
+
483
+ with self._mark_language_model(vllm_config):
484
+ self.llm = init_vllm_registered_model(
485
+ vllm_config=vllm_config.with_hf_config(get_llm_config(config)),
486
+ prefix=maybe_prefix(prefix, "llm"),
487
+ )
488
+
489
+ with self._mark_tower_model(vllm_config, {"image", "video"}):
490
+ self.visual_tokenizer = OXRVisualTokenizer(
491
+ config=config.vit_config,
492
+ visual_vocab_size=config.visual_vocab_size,
493
+ quant_config=quant_config,
494
+ prefix=f"{prefix}.visual_tokenizer",
495
+ )
496
+ self.vte = OXREmbedding(config.visual_vocab_size, config.hidden_size)
497
+
498
+ text_model_type = get_llm_config(config).model_type
499
+ self.image_pad_token_id = IMAGE_PAD_TOKEN_ID_MAP[text_model_type]
500
+
501
+ self.make_empty_intermediate_tensors = (
502
+ self.get_language_model().make_empty_intermediate_tensors
503
+ )
504
+
505
+ def _parse_and_validate_image_input(
506
+ self,
507
+ **kwargs: object,
508
+ ) -> OXRImagePatchInputs | None:
509
+ pixel_values = kwargs.pop("pixel_values", None)
510
+ indicator_tokens = kwargs.pop("indicator_tokens", None)
511
+ grids = kwargs.pop("grids", None)
512
+ if pixel_values is None and indicator_tokens is None:
513
+ return None
514
+
515
+ if pixel_values is not None and indicator_tokens is not None:
516
+ if not isinstance(pixel_values, (torch.Tensor, list)):
517
+ raise ValueError(f"Incorrect type of pixel values: {type(pixel_values)}")
518
+ if not isinstance(indicator_tokens, (torch.Tensor, list)):
519
+ raise ValueError(
520
+ f"Incorrect type of indicator_tokens: {type(indicator_tokens)}"
521
+ )
522
+ if grids is None:
523
+ raise ValueError("Missing grids for image input")
524
+
525
+ return OXRImagePatchInputs(
526
+ type="image_patches",
527
+ flat_data=flatten_bn(pixel_values, concat=True),
528
+ patches_per_item=[
529
+ x.shape[0] // (self.config.vit_config.hidden_stride**2)
530
+ for x in pixel_values
531
+ ],
532
+ indicator_tokens=flatten_bn(indicator_tokens, concat=True),
533
+ grids=flatten_bn(grids, concat=True),
534
+ )
535
+
536
+ raise AssertionError("This line should be unreachable.")
537
+
538
+ def _parse_and_validate_video_input(
539
+ self,
540
+ **kwargs: object,
541
+ ) -> OXRVideoPatchInputs | None:
542
+ pixel_values = kwargs.pop("video_pixel_values", None)
543
+ indicator_tokens = kwargs.pop("video_indicator_tokens", None)
544
+ grids = kwargs.pop("video_grids", None)
545
+ if pixel_values is None and indicator_tokens is None:
546
+ return None
547
+
548
+ if pixel_values is not None and indicator_tokens is not None:
549
+ if not isinstance(pixel_values, (torch.Tensor, list)):
550
+ raise ValueError(f"Incorrect type of pixel values: {type(pixel_values)}")
551
+ if not isinstance(indicator_tokens, (torch.Tensor, list)):
552
+ raise ValueError(
553
+ f"Incorrect type of indicator_tokens: {type(indicator_tokens)}"
554
+ )
555
+ if grids is None:
556
+ raise ValueError("Missing grids for video input")
557
+
558
+ return OXRVideoPatchInputs(
559
+ type="video_patches",
560
+ flat_data=flatten_bn(pixel_values, concat=True),
561
+ patches_per_item=[
562
+ x.shape[0] // (self.config.vit_config.hidden_stride**2)
563
+ for x in pixel_values
564
+ ],
565
+ indicator_tokens=flatten_bn(indicator_tokens, concat=True),
566
+ grids=flatten_bn(grids, concat=True),
567
+ )
568
+
569
+ raise AssertionError("This line should be unreachable.")
570
+
571
+ def _process_visual_input(
572
+ self,
573
+ visual_input: OXRImagePatchInputs | OXRVideoPatchInputs,
574
+ ) -> MultiModalEmbeddings:
575
+ image_patches_flat = visual_input["flat_data"]
576
+ patches_per_item = visual_input["patches_per_item"]
577
+ indicator_tokens = visual_input["indicator_tokens"]
578
+ grid_thws = visual_input["grids"]
579
+
580
+ visual_tokens = self.visual_tokenizer(
581
+ image_patches_flat.to(self.visual_tokenizer.dtype),
582
+ grid_thws,
583
+ )
584
+ visual_embeds = self.vte(visual_tokens)
585
+ indicator_embeds = self.vte(indicator_tokens)
586
+
587
+ visual_embeds_per_item = visual_embeds.split(patches_per_item, dim=0)
588
+ indicator_embeds_per_item = indicator_embeds.split(
589
+ [2] * len(patches_per_item)
590
+ )
591
+
592
+ return tuple(
593
+ torch.cat([indicator[:1], visual, indicator[1:]], dim=0)
594
+ for indicator, visual in zip(
595
+ indicator_embeds_per_item,
596
+ visual_embeds_per_item,
597
+ )
598
+ )
599
+
600
+ def _parse_and_validate_multimodal_inputs(self, **kwargs: object) -> dict:
601
+ modalities = {}
602
+ for input_key in kwargs:
603
+ if (
604
+ input_key in ("pixel_values", "indicator_tokens", "grids")
605
+ and "images" not in modalities
606
+ ):
607
+ modalities["images"] = self._parse_and_validate_image_input(**kwargs)
608
+ if (
609
+ input_key
610
+ in ("video_pixel_values", "video_indicator_tokens", "video_grids")
611
+ and "videos" not in modalities
612
+ ):
613
+ modalities["videos"] = self._parse_and_validate_video_input(**kwargs)
614
+ return modalities
615
+
616
+ def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
617
+ modalities = self._parse_and_validate_multimodal_inputs(**kwargs)
618
+ if not modalities:
619
+ return []
620
+
621
+ multimodal_embeddings: tuple[torch.Tensor, ...] = ()
622
+ for modality in modalities:
623
+ if modality == "images":
624
+ image_input = modalities["images"]
625
+ multimodal_embeddings += tuple(self._process_visual_input(image_input))
626
+ if modality == "videos":
627
+ video_input = modalities["videos"]
628
+ multimodal_embeddings += tuple(self._process_visual_input(video_input))
629
+ return multimodal_embeddings
630
+
631
+ def forward(
632
+ self,
633
+ input_ids: torch.Tensor | None,
634
+ positions: torch.Tensor,
635
+ intermediate_tensors: IntermediateTensors | None = None,
636
+ inputs_embeds: torch.Tensor | None = None,
637
+ **kwargs: object,
638
+ ) -> torch.Tensor | IntermediateTensors:
639
+ if intermediate_tensors is not None:
640
+ inputs_embeds = None
641
+
642
+ return self.llm(
643
+ input_ids=input_ids,
644
+ positions=positions,
645
+ intermediate_tensors=intermediate_tensors,
646
+ inputs_embeds=inputs_embeds,
647
+ )
648
+
649
+ def compute_logits(
650
+ self,
651
+ hidden_states: torch.Tensor,
652
+ ) -> torch.Tensor | None:
653
+ return self.llm.compute_logits(hidden_states)
654
+
655
+ def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
656
+ loader = AutoWeightsLoader(self)
657
+ return loader.load_weights(weights)
658
+
659
+ def get_language_model(self) -> torch.nn.Module:
660
+ return self.llm
plugins/vllm/oxr_vllm/oxr_processor.py ADDED
@@ -0,0 +1,384 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """OXR processor for the vLLM 0.25.1 plugin."""
2
+
3
+ import math
4
+ from functools import cached_property
5
+ from typing import Optional, Union
6
+
7
+ import numpy as np
8
+ import PIL.Image
9
+ import torch
10
+ from transformers import AutoProcessor, BatchFeature
11
+ from transformers.image_utils import ImageInput
12
+ from transformers.processing_utils import ProcessingKwargs, ProcessorMixin, Unpack
13
+ from transformers.tokenization_utils_base import PreTokenizedInput, TextInput
14
+
15
+ IMAGE_TOKEN = "<image>"
16
+ VIDEO_TOKEN = "<video>"
17
+ MIN_PIXELS = 200704
18
+ MAX_PIXELS = 921600
19
+
20
+
21
+ class OXRProcessorKwargs(ProcessingKwargs, total=False): # type: ignore[call-arg]
22
+ _defaults = {
23
+ "text_kwargs": {
24
+ "padding": False,
25
+ },
26
+ "images_kwargs": {
27
+ "do_convert_rgb": True,
28
+ },
29
+ "videos_kwargs": {
30
+ "do_convert_rgb": True,
31
+ },
32
+ }
33
+
34
+
35
+ class OXRProcessor(ProcessorMixin):
36
+ attributes = ["image_processor", "tokenizer"]
37
+ valid_kwargs = ["chat_template", "image_pad_token"]
38
+
39
+ image_processor_class = "AutoImageProcessor"
40
+ tokenizer_class = "AutoTokenizer"
41
+
42
+ def __init__(
43
+ self,
44
+ image_processor=None,
45
+ tokenizer=None,
46
+ chat_template=None,
47
+ image_pad_token=None,
48
+ patch_size=16,
49
+ hidden_stride=2,
50
+ temporal_patch_size=1,
51
+ **kwargs,
52
+ ):
53
+ self.image_token = IMAGE_TOKEN
54
+ self.video_token = VIDEO_TOKEN
55
+ self.image_pad_token = image_pad_token or "<|image_pad|>"
56
+ self.patch_size = patch_size
57
+ self.hidden_stride = hidden_stride
58
+ self.temporal_patch_size = temporal_patch_size
59
+ super().__init__(image_processor, tokenizer, chat_template=chat_template)
60
+
61
+ @cached_property
62
+ def extra_special_tokens(self):
63
+ image_pad_token_id = self.tokenizer.get_vocab()[self.image_pad_token]
64
+ return {
65
+ "image_token": -200,
66
+ "video_token": -201,
67
+ "visual_atom": -300,
68
+ "image_start": -301,
69
+ "image_end": -302,
70
+ "video_start": -303,
71
+ "video_end": -304,
72
+ "image_pad": image_pad_token_id,
73
+ }
74
+
75
+ def __call__(
76
+ self,
77
+ images: ImageInput = None,
78
+ videos: Union[np.ndarray, list[ImageInput]] = None,
79
+ text: Union[
80
+ TextInput,
81
+ PreTokenizedInput,
82
+ list[TextInput],
83
+ list[PreTokenizedInput],
84
+ ] = None,
85
+ **kwargs: Unpack[OXRProcessorKwargs],
86
+ ) -> BatchFeature:
87
+ output_kwargs = self._merge_kwargs(
88
+ OXRProcessorKwargs,
89
+ tokenizer_init_kwargs=self.tokenizer.init_kwargs,
90
+ **kwargs,
91
+ )
92
+ visual_features = {}
93
+ output = BatchFeature()
94
+
95
+ if images is not None:
96
+ processed_images = []
97
+ image_placeholders_list = []
98
+ grids = []
99
+ for image in images if isinstance(images, list) else [images]:
100
+ pixel_values, image_placeholders, grid = self.preprocess_multidata(
101
+ images=image,
102
+ **output_kwargs["images_kwargs"],
103
+ )
104
+ processed_images.append(pixel_values)
105
+ image_placeholders_list.append(image_placeholders)
106
+ grids.append(grid)
107
+
108
+ if processed_images:
109
+ visual_features["image_placeholders"] = image_placeholders_list
110
+ output["pixel_values"] = processed_images
111
+ output["grids"] = grids
112
+
113
+ if videos is not None:
114
+ processed_videos = []
115
+ video_placeholders_list = []
116
+ grids = []
117
+ for video in videos if isinstance(videos, list) else [videos]:
118
+ pixel_values, video_placeholders, grid = self.preprocess_multidata(
119
+ video=video,
120
+ **output_kwargs["videos_kwargs"],
121
+ )
122
+ processed_videos.append(pixel_values)
123
+ video_placeholders_list.append(video_placeholders)
124
+ grids.append(grid)
125
+
126
+ if processed_videos:
127
+ visual_features["video_placeholders"] = video_placeholders_list
128
+ output["video_pixel_values"] = processed_videos
129
+ output["video_grids"] = grids
130
+
131
+ if text is None:
132
+ return BatchFeature(data=visual_features)
133
+
134
+ if not isinstance(text, list):
135
+ text = [text]
136
+ tokenized_batched_text = self._tokenize_with_visual_symbol(text)
137
+ image_token_id = self.get_token_value("image_token")
138
+ video_token_id = self.get_token_value("video_token")
139
+ replaced_ids_list = []
140
+ image_idx = 0
141
+ video_idx = 0
142
+ for ids_tensor in tokenized_batched_text:
143
+ has_image_tokens = (
144
+ image_token_id in ids_tensor
145
+ and "image_placeholders" in visual_features
146
+ and image_idx < len(visual_features["image_placeholders"])
147
+ )
148
+ has_video_tokens = (
149
+ video_token_id in ids_tensor
150
+ and "video_placeholders" in visual_features
151
+ and video_idx < len(visual_features["video_placeholders"])
152
+ )
153
+ if has_image_tokens or has_video_tokens:
154
+ new_ids = []
155
+ for token_id in ids_tensor.tolist():
156
+ if token_id == image_token_id:
157
+ new_ids.extend(visual_features["image_placeholders"][image_idx])
158
+ image_idx += 1
159
+ elif token_id == video_token_id:
160
+ new_ids.extend(visual_features["video_placeholders"][video_idx])
161
+ video_idx += 1
162
+ else:
163
+ new_ids.append(token_id)
164
+ ids_tensor = torch.tensor(new_ids, dtype=torch.long)
165
+ replaced_ids_list.append(ids_tensor)
166
+
167
+ if replaced_ids_list:
168
+ output["input_ids"] = torch.stack(replaced_ids_list)
169
+ else:
170
+ output["input_ids"] = torch.tensor([], dtype=torch.long)
171
+ return output
172
+
173
+ def _tokenize_with_visual_symbol(self, text_list: list[str]) -> torch.LongTensor:
174
+ batch_token_ids = []
175
+ for text in text_list:
176
+ token_ids = []
177
+ video_token_id = self.get_token_value("video_token")
178
+ image_token_id = self.get_token_value("image_token")
179
+ video_split_texts = text.split(self.video_token)
180
+
181
+ for j, video_segment in enumerate(video_split_texts):
182
+ image_split_texts = video_segment.split(self.image_token)
183
+ text_chunks = [
184
+ self.tokenizer(chunk, add_special_tokens=False).input_ids
185
+ for chunk in image_split_texts
186
+ ]
187
+ segment_tokens = []
188
+ for i, chunk in enumerate(text_chunks):
189
+ segment_tokens.extend(chunk)
190
+ if i < len(text_chunks) - 1:
191
+ segment_tokens.append(image_token_id)
192
+ token_ids.extend(segment_tokens)
193
+ if j < len(video_split_texts) - 1:
194
+ token_ids.append(video_token_id)
195
+ batch_token_ids.append(token_ids)
196
+ return torch.tensor(batch_token_ids, dtype=torch.long)
197
+
198
+ def smart_resize(
199
+ self,
200
+ height: int,
201
+ width: int,
202
+ factor: int = 32,
203
+ min_pixels: int = MIN_PIXELS,
204
+ max_pixels: int = MAX_PIXELS,
205
+ ):
206
+ if height < factor or width < factor:
207
+ if height < width:
208
+ width = round(factor / height * width)
209
+ height = factor
210
+ else:
211
+ height = round(factor / width * height)
212
+ width = factor
213
+ elif max(height, width) / min(height, width) > 200:
214
+ if height > width:
215
+ height = 200 * width
216
+ else:
217
+ width = 200 * height
218
+
219
+ h_bar = round(height / factor) * factor
220
+ w_bar = round(width / factor) * factor
221
+ if h_bar * w_bar > max_pixels:
222
+ beta = math.sqrt((height * width) / max_pixels)
223
+ h_bar = math.floor(height / beta / factor) * factor
224
+ w_bar = math.floor(width / beta / factor) * factor
225
+ elif h_bar * w_bar < min_pixels:
226
+ beta = math.sqrt(min_pixels / (height * width))
227
+ h_bar = math.ceil(height * beta / factor) * factor
228
+ w_bar = math.ceil(width * beta / factor) * factor
229
+ return h_bar, w_bar
230
+
231
+ def get_token_value(self, token_name: str):
232
+ return self.extra_special_tokens[token_name]
233
+
234
+ def construct_visual_indicators(self, grid, is_video: bool = False):
235
+ if is_video:
236
+ start_token = self.get_token_value("video_start")
237
+ end_token = self.get_token_value("video_end")
238
+ else:
239
+ start_token = self.get_token_value("image_start")
240
+ end_token = self.get_token_value("image_end")
241
+
242
+ visual_indicators = [start_token, self.get_token_value("visual_atom")]
243
+ if grid[0] * grid[1] > 1:
244
+ for _ in range(grid[0]):
245
+ for _ in range(grid[1]):
246
+ visual_indicators.append(self.get_token_value("visual_atom"))
247
+ visual_indicators.append(end_token)
248
+ return visual_indicators
249
+
250
+ def construct_visual_placeholders(self, grid, is_video: bool = False):
251
+ visual_placeholders = self.construct_visual_indicators((1, 1), is_video)
252
+ visual_atom_token_id = self.get_token_value("visual_atom")
253
+ image_padding_token_id = self.get_token_value("image_pad")
254
+
255
+ num_image_atoms = grid[0] * grid[1] * grid[2]
256
+ num_image_atoms //= self.hidden_stride**2
257
+ num_image_atoms //= self.temporal_patch_size
258
+
259
+ vision_start_token_id = self.tokenizer.get_vocab().get(
260
+ "<|vision_start|>",
261
+ image_padding_token_id,
262
+ )
263
+ vision_end_token_id = self.tokenizer.get_vocab().get(
264
+ "<|vision_end|>",
265
+ image_padding_token_id,
266
+ )
267
+
268
+ padded_placeholder_tokens = []
269
+ for token in visual_placeholders:
270
+ if token == visual_atom_token_id:
271
+ padded_placeholder_tokens.extend(
272
+ [image_padding_token_id] * num_image_atoms
273
+ )
274
+ elif token in (
275
+ self.get_token_value("image_start"),
276
+ self.get_token_value("video_start"),
277
+ ):
278
+ padded_placeholder_tokens.append(vision_start_token_id)
279
+ elif token in (
280
+ self.get_token_value("image_end"),
281
+ self.get_token_value("video_end"),
282
+ ):
283
+ padded_placeholder_tokens.append(vision_end_token_id)
284
+ else:
285
+ padded_placeholder_tokens.append(image_padding_token_id)
286
+ return padded_placeholder_tokens
287
+
288
+ def preprocess_multidata(
289
+ self,
290
+ images: Optional[Union[PIL.Image.Image, list[PIL.Image.Image]]] = None,
291
+ video: Optional[Union[list[PIL.Image.Image], np.ndarray]] = None,
292
+ do_convert_rgb: Optional[bool] = True,
293
+ convert_to_rgb: Optional[bool] = None,
294
+ min_pixels: int = MIN_PIXELS,
295
+ max_pixels: int = MAX_PIXELS,
296
+ return_tensors: Optional[str] = "pt",
297
+ ):
298
+ if convert_to_rgb is not None:
299
+ do_convert_rgb = convert_to_rgb
300
+
301
+ is_video = False
302
+ if images is not None:
303
+ if not isinstance(images, list):
304
+ images = [images]
305
+ elif video is not None:
306
+ is_video = True
307
+ if isinstance(video, np.ndarray):
308
+ images = [
309
+ PIL.Image.fromarray(video[i].astype(np.uint8))
310
+ for i in range(video.shape[0])
311
+ ]
312
+ elif isinstance(video, list):
313
+ images = video
314
+ else:
315
+ raise ValueError("Expected either images or video")
316
+
317
+ min_pixels = min(
318
+ max_pixels if max_pixels is not None else MAX_PIXELS,
319
+ min_pixels if min_pixels is not None else MIN_PIXELS,
320
+ )
321
+ images = [
322
+ image.convert("RGB") if do_convert_rgb and image.mode != "RGB" else image
323
+ for image in images
324
+ ]
325
+
326
+ width, height = images[0].size
327
+ resized_height, resized_width = height, width
328
+ processed_images = []
329
+ for image in images:
330
+ resized_height, resized_width = self.smart_resize(
331
+ height,
332
+ width,
333
+ factor=self.patch_size * self.hidden_stride,
334
+ min_pixels=min_pixels,
335
+ max_pixels=max_pixels,
336
+ )
337
+ image_pt = self.image_processor.preprocess(
338
+ image,
339
+ size=dict(height=resized_height, width=resized_width),
340
+ return_tensors="np",
341
+ )["pixel_values"][0]
342
+ processed_images.append(image_pt)
343
+
344
+ patches = np.array(processed_images)
345
+ frame_remainder = patches.shape[0] % self.temporal_patch_size
346
+ if frame_remainder != 0:
347
+ num_to_pad = self.temporal_patch_size - frame_remainder
348
+ repeats = np.repeat(patches[-1][np.newaxis], num_to_pad, axis=0)
349
+ patches = np.concatenate([patches, repeats], axis=0)
350
+
351
+ channel = patches.shape[1]
352
+ grid_t = patches.shape[0] // self.temporal_patch_size
353
+ grid_h = resized_height // self.patch_size
354
+ grid_w = resized_width // self.patch_size
355
+
356
+ patches = patches.reshape(
357
+ grid_t,
358
+ self.temporal_patch_size,
359
+ channel,
360
+ grid_h // self.hidden_stride,
361
+ self.hidden_stride,
362
+ self.patch_size,
363
+ grid_w // self.hidden_stride,
364
+ self.hidden_stride,
365
+ self.patch_size,
366
+ )
367
+ patches = patches.transpose(0, 3, 6, 4, 7, 2, 1, 5, 8)
368
+ flatten_patches = patches.reshape(
369
+ grid_t * grid_h * grid_w,
370
+ channel * self.temporal_patch_size * self.patch_size * self.patch_size,
371
+ )
372
+
373
+ visual_placeholders = self.construct_visual_placeholders(
374
+ [grid_t, grid_h, grid_w],
375
+ is_video,
376
+ )
377
+ return (
378
+ torch.tensor(flatten_patches),
379
+ visual_placeholders,
380
+ torch.tensor([[grid_t, grid_h, grid_w]]),
381
+ )
382
+
383
+
384
+ AutoProcessor.register("OXRProcessor", OXRProcessor)
plugins/vllm/pyproject.toml ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "oxr-vllm-plugin"
7
+ version = "0.2.0"
8
+ description = "Out-of-tree OXR model plugin for vLLM 0.25.1"
9
+ requires-python = ">=3.12"
10
+ dependencies = ["vllm==0.25.1", "transformers==5.13.1"]
11
+
12
+ [project.entry-points."vllm.general_plugins"]
13
+ oxr = "oxr_vllm:register"
14
+
15
+ [tool.setuptools.packages.find]
16
+ include = ["oxr_vllm", "oxr_vllm.*"]
preprocessor_config.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "do_convert_rgb": null,
3
+ "do_normalize": true,
4
+ "do_rescale": true,
5
+ "do_resize": true,
6
+ "image_mean": [
7
+ 0.5,
8
+ 0.5,
9
+ 0.5
10
+ ],
11
+ "image_processor_type": "SiglipImageProcessor",
12
+ "image_std": [
13
+ 0.5,
14
+ 0.5,
15
+ 0.5
16
+ ],
17
+ "processor_class": "SiglipProcessor",
18
+ "resample": 2,
19
+ "rescale_factor": 0.00392156862745098,
20
+ "size": {
21
+ "height": 512,
22
+ "width": 512
23
+ }
24
+ }
special_tokens_map.json ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "additional_special_tokens": [
3
+ "<|im_start|>",
4
+ "<|im_end|>",
5
+ "<|object_ref_start|>",
6
+ "<|object_ref_end|>",
7
+ "<|box_start|>",
8
+ "<|box_end|>",
9
+ "<|quad_start|>",
10
+ "<|quad_end|>",
11
+ "<|vision_start|>",
12
+ "<|vision_end|>",
13
+ "<|vision_pad|>",
14
+ "<|image_pad|>",
15
+ "<|video_pad|>"
16
+ ],
17
+ "eos_token": {
18
+ "content": "<|endoftext|>",
19
+ "lstrip": false,
20
+ "normalized": false,
21
+ "rstrip": false,
22
+ "single_word": false
23
+ },
24
+ "pad_token": {
25
+ "content": "<|endoftext|>",
26
+ "lstrip": false,
27
+ "normalized": false,
28
+ "rstrip": false,
29
+ "single_word": false
30
+ }
31
+ }
tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:aeb13307a71acd8fe81861d94ad54ab689df773318809eed3cbe794b4492dae4
3
+ size 11422654
tokenizer_config.json ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_bos_token": false,
3
+ "add_prefix_space": false,
4
+ "added_tokens_decoder": {
5
+ "151643": {
6
+ "content": "<|endoftext|>",
7
+ "lstrip": false,
8
+ "normalized": false,
9
+ "rstrip": false,
10
+ "single_word": false,
11
+ "special": true
12
+ },
13
+ "151644": {
14
+ "content": "<|im_start|>",
15
+ "lstrip": false,
16
+ "normalized": false,
17
+ "rstrip": false,
18
+ "single_word": false,
19
+ "special": true
20
+ },
21
+ "151645": {
22
+ "content": "<|im_end|>",
23
+ "lstrip": false,
24
+ "normalized": false,
25
+ "rstrip": false,
26
+ "single_word": false,
27
+ "special": true
28
+ },
29
+ "151646": {
30
+ "content": "<|object_ref_start|>",
31
+ "lstrip": false,
32
+ "normalized": false,
33
+ "rstrip": false,
34
+ "single_word": false,
35
+ "special": true
36
+ },
37
+ "151647": {
38
+ "content": "<|object_ref_end|>",
39
+ "lstrip": false,
40
+ "normalized": false,
41
+ "rstrip": false,
42
+ "single_word": false,
43
+ "special": true
44
+ },
45
+ "151648": {
46
+ "content": "<|box_start|>",
47
+ "lstrip": false,
48
+ "normalized": false,
49
+ "rstrip": false,
50
+ "single_word": false,
51
+ "special": true
52
+ },
53
+ "151649": {
54
+ "content": "<|box_end|>",
55
+ "lstrip": false,
56
+ "normalized": false,
57
+ "rstrip": false,
58
+ "single_word": false,
59
+ "special": true
60
+ },
61
+ "151650": {
62
+ "content": "<|quad_start|>",
63
+ "lstrip": false,
64
+ "normalized": false,
65
+ "rstrip": false,
66
+ "single_word": false,
67
+ "special": true
68
+ },
69
+ "151651": {
70
+ "content": "<|quad_end|>",
71
+ "lstrip": false,
72
+ "normalized": false,
73
+ "rstrip": false,
74
+ "single_word": false,
75
+ "special": true
76
+ },
77
+ "151652": {
78
+ "content": "<|vision_start|>",
79
+ "lstrip": false,
80
+ "normalized": false,
81
+ "rstrip": false,
82
+ "single_word": false,
83
+ "special": true
84
+ },
85
+ "151653": {
86
+ "content": "<|vision_end|>",
87
+ "lstrip": false,
88
+ "normalized": false,
89
+ "rstrip": false,
90
+ "single_word": false,
91
+ "special": true
92
+ },
93
+ "151654": {
94
+ "content": "<|vision_pad|>",
95
+ "lstrip": false,
96
+ "normalized": false,
97
+ "rstrip": false,
98
+ "single_word": false,
99
+ "special": true
100
+ },
101
+ "151655": {
102
+ "content": "<|image_pad|>",
103
+ "lstrip": false,
104
+ "normalized": false,
105
+ "rstrip": false,
106
+ "single_word": false,
107
+ "special": true
108
+ },
109
+ "151656": {
110
+ "content": "<|video_pad|>",
111
+ "lstrip": false,
112
+ "normalized": false,
113
+ "rstrip": false,
114
+ "single_word": false,
115
+ "special": true
116
+ },
117
+ "151657": {
118
+ "content": "<tool_call>",
119
+ "lstrip": false,
120
+ "normalized": false,
121
+ "rstrip": false,
122
+ "single_word": false,
123
+ "special": false
124
+ },
125
+ "151658": {
126
+ "content": "</tool_call>",
127
+ "lstrip": false,
128
+ "normalized": false,
129
+ "rstrip": false,
130
+ "single_word": false,
131
+ "special": false
132
+ },
133
+ "151659": {
134
+ "content": "<|fim_prefix|>",
135
+ "lstrip": false,
136
+ "normalized": false,
137
+ "rstrip": false,
138
+ "single_word": false,
139
+ "special": false
140
+ },
141
+ "151660": {
142
+ "content": "<|fim_middle|>",
143
+ "lstrip": false,
144
+ "normalized": false,
145
+ "rstrip": false,
146
+ "single_word": false,
147
+ "special": false
148
+ },
149
+ "151661": {
150
+ "content": "<|fim_suffix|>",
151
+ "lstrip": false,
152
+ "normalized": false,
153
+ "rstrip": false,
154
+ "single_word": false,
155
+ "special": false
156
+ },
157
+ "151662": {
158
+ "content": "<|fim_pad|>",
159
+ "lstrip": false,
160
+ "normalized": false,
161
+ "rstrip": false,
162
+ "single_word": false,
163
+ "special": false
164
+ },
165
+ "151663": {
166
+ "content": "<|repo_name|>",
167
+ "lstrip": false,
168
+ "normalized": false,
169
+ "rstrip": false,
170
+ "single_word": false,
171
+ "special": false
172
+ },
173
+ "151664": {
174
+ "content": "<|file_sep|>",
175
+ "lstrip": false,
176
+ "normalized": false,
177
+ "rstrip": false,
178
+ "single_word": false,
179
+ "special": false
180
+ },
181
+ "151665": {
182
+ "content": "<tool_response>",
183
+ "lstrip": false,
184
+ "normalized": false,
185
+ "rstrip": false,
186
+ "single_word": false,
187
+ "special": false
188
+ },
189
+ "151666": {
190
+ "content": "</tool_response>",
191
+ "lstrip": false,
192
+ "normalized": false,
193
+ "rstrip": false,
194
+ "single_word": false,
195
+ "special": false
196
+ },
197
+ "151667": {
198
+ "content": "<think>",
199
+ "lstrip": false,
200
+ "normalized": false,
201
+ "rstrip": false,
202
+ "single_word": false,
203
+ "special": false
204
+ },
205
+ "151668": {
206
+ "content": "</think>",
207
+ "lstrip": false,
208
+ "normalized": false,
209
+ "rstrip": false,
210
+ "single_word": false,
211
+ "special": false
212
+ }
213
+ },
214
+ "additional_special_tokens": [
215
+ "<|im_start|>",
216
+ "<|im_end|>",
217
+ "<|object_ref_start|>",
218
+ "<|object_ref_end|>",
219
+ "<|box_start|>",
220
+ "<|box_end|>",
221
+ "<|quad_start|>",
222
+ "<|quad_end|>",
223
+ "<|vision_start|>",
224
+ "<|vision_end|>",
225
+ "<|vision_pad|>",
226
+ "<|image_pad|>",
227
+ "<|video_pad|>"
228
+ ],
229
+ "bos_token": null,
230
+ "clean_up_tokenization_spaces": false,
231
+ "eos_token": "<|endoftext|>",
232
+ "errors": "replace",
233
+ "extra_special_tokens": {},
234
+ "model_max_length": 131072,
235
+ "pad_token": "<|endoftext|>",
236
+ "processor_class": "SiglipProcessor",
237
+ "split_special_tokens": false,
238
+ "tokenizer_class": "Qwen2Tokenizer",
239
+ "unk_token": null
240
+ }
vocab.json ADDED
The diff for this file is too large to render. See raw diff