jordaniel29 commited on
Commit
df65075
·
verified ·
1 Parent(s): 20ea3e4

Upload folder using huggingface_hub

Browse files
.DS_Store ADDED
Binary file (10.2 kB). View file
 
.gitattributes CHANGED
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ Transformer.onnx.data filter=lfs diff=lfs merge=lfs -text
37
+ Vision.onnx.data filter=lfs diff=lfs merge=lfs -text
38
+ tokenizer/tokenizer.json filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ base_model: Qwen/Qwen3-VL-Embedding-2B
4
+ pipeline_tag: feature-extraction
5
+ tags:
6
+ - onnx
7
+ - qwen3
8
+ - qwen3-vl
9
+ - embedding
10
+ - multimodal
11
+ - vision-language
12
+ ---
13
+
14
+ # Qwen3-VL-Embedding-2B — ONNX
15
+
16
+ ONNX export of [Qwen/Qwen3-VL-Embedding-2B](https://huggingface.co/Qwen/Qwen3-VL-Embedding-2B), split into a separate vision encoder and transformer decoder so each component can be consumed independently by ONNX Runtime or built into a TensorRT engine.
17
+
18
+ The image resolution and temporal size are **baked into the ONNX graph** (see [Configuration](#configuration)). Only `seq_len` on the transformer is dynamic.
19
+
20
+ ## Repository contents
21
+
22
+ | File / Dir | Purpose |
23
+ |------------|---------|
24
+ | `Vision.onnx` (+ `.onnx.data`) | Vision encoder. Fixed input resolution and temporal size baked in. |
25
+ | `Transformer.onnx` (+ `.onnx.data`) | Transformer decoder layers, embedding mode (no KV cache), dynamic `seq_len`. |
26
+ | `rotary_params.npz` | mRoPE parameters (`inv_freq`, `mrope_section`), token embedding weights, and image/grid config (`image_height/width`, `height_factor/width_factor`, `patch_size`, `merge_size`, `hidden_size`, `head_dim`, …). Required alongside the ONNX files at inference time. |
27
+ | `tokenizer/` | HF `Qwen3VLProcessor` / tokenizer files for text + image preprocessing. |
28
+ | `export_script/` | Scripts used to produce the ONNX files (see [Reproducing the export](#reproducing-the-export)). |
29
+ | `text_prompt_APOv2.1_BF16/` | Sample text prompts (fire / smoke detection) used for downstream evaluation — not required for loading the model. |
30
+
31
+ ## Configuration
32
+
33
+ The export was produced with:
34
+
35
+ | Variable | Value | Notes |
36
+ |----------|-------|-------|
37
+ | `IMG_SIZE` | `(768, 768)` | Fixed input resolution — must be a multiple of `patch_size × merge_size`. |
38
+ | `TEMPORAL_SIZE` | `1` | Frames per clip. |
39
+
40
+ Changing either of these requires re-running the full export pipeline.
41
+
42
+ ## Usage
43
+
44
+ Load the ONNX files with ONNX Runtime and apply the mRoPE / token-embedding parameters from `rotary_params.npz` around the transformer. High-level flow:
45
+
46
+ 1. Preprocess inputs with the tokenizer / processor in `tokenizer/`.
47
+ 2. Run image pixels through `Vision.onnx` to obtain visual tokens.
48
+ 3. Look up text token embeddings using the weights saved in `rotary_params.npz`.
49
+ 4. Concatenate visual + text embeddings into the transformer input sequence.
50
+ 5. Run the sequence through `Transformer.onnx` with mRoPE parameters from `rotary_params.npz`.
51
+ 6. The final pooled hidden state is the multimodal embedding.
52
+
53
+ ### TensorRT
54
+
55
+ These ONNX files are designed to be converted to TensorRT engines. Notes for the TRT build:
56
+
57
+ - FP16 is safe globally, except for normalization-sensitive layers — force those to FP32 to avoid overflow in the RMSNorm `x * rsqrt(square(x).sum())` pattern.
58
+ - Engines are **not portable** across GPU architectures or TRT versions and must be built on the target machine.
59
+
60
+ ## Reproducing the export
61
+
62
+ Scripts in `export_script/` regenerate the ONNX assets from the HF PyTorch checkpoint:
63
+
64
+ ```bash
65
+ # Install dependencies
66
+ pip install -r export_script/requirements.txt
67
+
68
+ # Full pipeline
69
+ bash export_script/run_all.sh
70
+
71
+ # Or run the stages individually
72
+ python export_script/a_export_to_onnx.py
73
+ python export_script/b_export_onnx_vision.py
74
+ ```
75
+
76
+ - **`a_export_to_onnx.py`** — Exports `Transformer.onnx` and saves `rotary_params.npz`. Also produces an initial `Vision.onnx` via a manual path (norm fusion, GELU replacement), which gets overwritten in step b.
77
+ - **`b_export_onnx_vision.py`** — Re-exports `Vision.onnx` by wrapping the HF `Qwen3VLVisionModel` directly. Traces the exact PyTorch code path, so numerics match HF.
78
+ - **`qwen3_vl_embedding.py`** — Model wrapper used by the export scripts.
79
+
80
+ The TensorRT build step is not included in this repo; it depends on your local GPU / TRT installation.
81
+
82
+ ## Known limitations
83
+
84
+ The export pipeline currently only produces reliable engines for `TEMPORAL_SIZE ≤ 2`. Beyond that, the Torch vs TRT cosine similarity drops below 0.99 and parity can no longer be guaranteed. If you need longer temporal contexts, the export path will need further investigation (likely around the temporal patching / rotary handling).
85
+
86
+ ## License
87
+
88
+ Apache-2.0, inherited from the base model. See [Qwen/Qwen3-VL-Embedding-2B](https://huggingface.co/Qwen/Qwen3-VL-Embedding-2B) for upstream terms.
Transformer.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e2bad7dc0bf0539edcb93cf2ebe224129309b6095e217356d91df82051fbeaa9
3
+ size 260402
Transformer.onnx.data ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:08c8f43cd7efdea87386b879317f1ae5ff83fd32b8b28438d46c44c3e358a19b
3
+ size 5637496832
Vision.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d0c503e408a564218370c3ff09c157d2f80928c4fa26ab39ce3c51e99386ada2
3
+ size 731088
Vision.onnx.data ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f276a72b814c0c5de573f7ed3a4e5196ea714dfa4cac2d2aa55cc4ec566137f7
3
+ size 1627828224
export_script/a_export_to_onnx.py ADDED
@@ -0,0 +1,727 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Export Qwen3-VL-Embedding model to ONNX files for inference.
3
+
4
+ Produces 2 ONNX files:
5
+ 1. Vision.onnx — Vision encoder (fixed image resolution)
6
+ 2. Transformer.onnx — Transformer decoder layers (embedding mode, no KV cache)
7
+
8
+ Also saves rotary_params.npz, which contains:
9
+ • inv_freq + mrope_section — for Python-side mRoPE computation
10
+ • embed_weight — token-embedding lookup table (replaces Embed.onnx)
11
+ • image dimensions / config values
12
+
13
+ Usage:
14
+ python 1_export_to_onnx.py
15
+ python 1_export_to_onnx.py --model_path /path/to/model --output_dir /path/to/output
16
+ """
17
+
18
+ import math
19
+ import os
20
+ import gc
21
+ import glob
22
+ import argparse
23
+ import torch
24
+ import onnx
25
+ import numpy as np
26
+ from transformers.models.qwen3_vl.modeling_qwen3_vl import (
27
+ Qwen3VLVisionModel,
28
+ )
29
+ from qwen3_vl_embedding import Qwen3VLForEmbedding, MAX_TOTAL_PIXELS
30
+
31
+
32
+ def consolidate_external_data(onnx_path: str):
33
+ """
34
+ Re-save an ONNX model so all external weights live in one .onnx.data file
35
+ instead of hundreds of per-tensor files.
36
+ """
37
+ out_dir = os.path.dirname(onnx_path)
38
+ base = os.path.basename(onnx_path)
39
+ data_rel = base + ".data" # sibling file: Transformer.onnx.data
40
+ model = onnx.load(onnx_path, load_external_data=True)
41
+
42
+ # Delete ONNX per-tensor external files (e.g.
43
+ # `model.model.language_model.layers.0.self_attn.qk_norm_weight`). They can
44
+ # be named with dots, so we use an allow-list of known suffixes we want to
45
+ # keep. Subdirectories (e.g. `tokenizer/`) are skipped via the isfile check.
46
+ PRESERVE_SUFFIXES = (".onnx", ".onnx.data", ".npz", ".bin")
47
+ for fname in list(os.listdir(out_dir)):
48
+ fpath = os.path.join(out_dir, fname)
49
+ if not os.path.isfile(fpath):
50
+ continue
51
+ if any(fname.endswith(sfx) for sfx in PRESERVE_SUFFIXES):
52
+ continue
53
+ try:
54
+ os.remove(fpath)
55
+ except OSError:
56
+ pass
57
+
58
+ # Remove the target .data file if it already exists (stale)
59
+ target_data = os.path.join(out_dir, data_rel)
60
+ if os.path.exists(target_data):
61
+ os.remove(target_data)
62
+
63
+ onnx.save_model(
64
+ model, onnx_path,
65
+ save_as_external_data=True,
66
+ all_tensors_to_one_file=True,
67
+ location=data_rel,
68
+ size_threshold=1024,
69
+ convert_attribute=False,
70
+ )
71
+ del model
72
+ gc.collect()
73
+
74
+
75
+ # ══════════════════════════════════════════════════════════════════════════════
76
+ # Configuration — reads from the module's config.py
77
+ # ══════════════════════════════════════════════════════════════════════════════
78
+ IMG_SIZE = (768,768) # (h,w)
79
+ QWEN3VLE_MODEL_HF_PATH = os.getenv(
80
+ "MODEL_QWEN3VLE_MODEL_HF_PATH", "/home/jordan/Research/Product-AI-mono/assets/model/Qwen3-VL-Embedding-2B"
81
+ )
82
+ QWEN3VLE_TRT_DIR_PATH = os.getenv(
83
+ "MODEL_QWEN3VLE_TRT_DIR_PATH", "assets/model/Qwen3-VL-Embedding-2B-onnx"
84
+ )
85
+ TEMPORAL_SIZE=1
86
+
87
+ DEFAULT_MODEL_PATH = QWEN3VLE_MODEL_HF_PATH
88
+ DEFAULT_OUTPUT_DIR = QWEN3VLE_TRT_DIR_PATH
89
+
90
+ # Vision — fixed image resolution at export time.
91
+ # Must be multiples of (patch_size * merge_size), validated after model loading.
92
+ IMAGE_HEIGHT = IMG_SIZE[0]
93
+ IMAGE_WIDTH = IMG_SIZE[1]
94
+
95
+ MAX_SEQ_LEN = MAX_TOTAL_PIXELS # 8192
96
+ OPSET = 17
97
+
98
+
99
+ # ══════════════════════════════════════════════════════════════════════════════
100
+ # Token Embedding Module
101
+ # ══════════════════════════════════════════════════════════════════════════════
102
+ class LLM_EMBED(torch.nn.Module):
103
+ """Extract the token-embedding layer and run it in float32."""
104
+
105
+ def __init__(self, model):
106
+ super().__init__()
107
+ self.embed_tokens = model.model.language_model.embed_tokens.float()
108
+
109
+ def forward(self, input_ids):
110
+ return self.embed_tokens(input_ids)
111
+
112
+
113
+ # ══════════════════════════════════════════════════════════════════════════════
114
+ # Vision Encoder Module
115
+ # ══════════════════════════════════════════════════════════════════════════════
116
+ class LLM_VISION_EMBED(torch.nn.Module):
117
+ """
118
+ ONNX-exportable vision encoder with baked-in positional / rotary
119
+ embeddings for a fixed image resolution.
120
+
121
+ Takes the HF processor's pixel_values directly (already CLIP-normalised
122
+ and patched) so the patch ordering, normalisation, and Conv3d behaviour
123
+ are identical to the PyTorch model.
124
+
125
+ Input : float32 pixel_values [total_patches, flatten_dim]
126
+ Output: (deepstack_feature_0, …, vision_hidden_states)
127
+ """
128
+
129
+ def __init__(self, model, height_factor, width_factor, temporal_patches=1):
130
+ super().__init__()
131
+ visual = model.model.visual
132
+ vision_config = model.config.vision_config
133
+
134
+ self.num_heads = vision_config.num_heads
135
+ self.head_dim = vision_config.hidden_size // self.num_heads
136
+ self.head_dim_half = self.head_dim // 2
137
+ self.embed_dim = visual.patch_embed.embed_dim
138
+ self.merge_size = visual.spatial_merge_size
139
+ self.t_patches = temporal_patches
140
+
141
+ ms = self.merge_size
142
+ self.grid_h = height_factor * ms
143
+ self.grid_w = width_factor * ms
144
+
145
+ # ── pre-compute positional / rotary embeddings for the fixed grid ─
146
+ grid_thw = torch.tensor(
147
+ [[self.t_patches, self.grid_h, self.grid_w]], dtype=torch.int32
148
+ )
149
+
150
+ pos_embeds = Qwen3VLVisionModel.fast_pos_embed_interpolate(
151
+ visual, grid_thw
152
+ ).unsqueeze(0) # [1, N, embed_dim]
153
+ self.register_buffer("pos_embeds", pos_embeds)
154
+
155
+ rot_pos_emb = (
156
+ Qwen3VLVisionModel.rot_pos_emb(visual, grid_thw)
157
+ .float()
158
+ .unsqueeze(0).unsqueeze(0).unsqueeze(0) # [1, 1, 1, N, dim/2]
159
+ )
160
+ cos = rot_pos_emb.cos()
161
+ sin = rot_pos_emb.sin()
162
+ self.register_buffer("rotary_cos", torch.cat([cos, cos], dim=-1))
163
+ self.register_buffer("rotary_sin", torch.cat([-sin, sin], dim=-1))
164
+
165
+ # ── ONNX-friendly GELU ──────────────────────────────────────────
166
+ self._replace_gelu(visual)
167
+
168
+ # ── fuse layer-norms into the subsequent linear layers ──────────
169
+ scaling = self.head_dim ** -0.25
170
+ for blk in visual.blocks:
171
+ blk.attn.qkv.weight.data[: -self.embed_dim] *= scaling
172
+ blk.attn.qkv.bias.data[: -self.embed_dim] *= scaling
173
+ self._fuse_norm(blk.norm1, blk.attn.qkv)
174
+ self._fuse_norm(blk.norm2, blk.mlp.linear_fc1)
175
+
176
+ for ds_layer in visual.deepstack_merger_list:
177
+ self._fuse_norm(ds_layer.norm, ds_layer.linear_fc1)
178
+
179
+ self._fuse_norm(visual.merger.norm, visual.merger.linear_fc1)
180
+
181
+ self.visual = visual
182
+
183
+ # ── helpers ──────────────────────────────────────────────────────────
184
+ @staticmethod
185
+ def _fuse_norm(norm, linear):
186
+ norm_bias = norm.bias.data
187
+ norm_weight = norm.weight.data
188
+ if linear.weight.shape[1] != norm_bias.shape[0]:
189
+ repeat_factor = linear.weight.shape[1] // norm_bias.shape[0]
190
+ norm_bias = norm_bias.repeat(repeat_factor)
191
+ norm_weight = norm_weight.repeat(repeat_factor)
192
+ linear.bias.data.add_(torch.matmul(linear.weight.data, norm_bias))
193
+ linear.weight.data.mul_(norm_weight.unsqueeze(0))
194
+ norm.elementwise_affine = False
195
+ norm.weight = None
196
+ norm.bias = None
197
+
198
+ @staticmethod
199
+ def _replace_gelu(module):
200
+ for name, child in module.named_children():
201
+ if isinstance(child, torch.nn.GELU):
202
+ setattr(module, name, torch.nn.GELU(approximate="tanh"))
203
+ else:
204
+ LLM_VISION_EMBED._replace_gelu(child)
205
+
206
+ def _rotate_half(self, x, batch_size):
207
+ x = x.view(2, batch_size, self.num_heads, -1, 2, self.head_dim_half)
208
+ x = x.flip(-2)
209
+ return x.view(2, batch_size, self.num_heads, -1, self.head_dim)
210
+
211
+ def _spatial_merge(self, x, target_hidden_size):
212
+ """Explicit spatial reshape: group merge_size×merge_size patches per frame."""
213
+ ms = self.merge_size
214
+ x = x.view(1, self.t_patches, self.grid_h, self.grid_w, -1)
215
+ x = x.view(1, self.t_patches, self.grid_h // ms, ms, self.grid_w // ms, ms, -1)
216
+ x = x.permute(0, 1, 2, 4, 3, 5, 6)
217
+ return x.reshape(1, -1, target_hidden_size)
218
+
219
+ # ── forward ──────────────────────────────────────────────────────────
220
+ def forward(self, pixel_values):
221
+ """
222
+ pixel_values : float32 [total_patches, flatten_dim]
223
+ Already CLIP-normalised, from the HF processor.
224
+ """
225
+ batch_size = 1
226
+
227
+ # patch embedding (original HF module)
228
+ hidden = self.visual.patch_embed(pixel_values) # [N, embed_dim]
229
+ hidden = hidden.unsqueeze(0) # [1, N, embed_dim]
230
+ hidden = hidden + self.pos_embeds
231
+
232
+ # transformer blocks
233
+ deepstack_features = []
234
+ ds_indices = self.visual.deepstack_visual_indexes
235
+ ds_modules = self.visual.deepstack_merger_list
236
+
237
+ for layer_num, blk in enumerate(self.visual.blocks):
238
+ # --- self-attention (manual, ONNX-safe) ---
239
+ h_norm = blk.norm1(hidden)
240
+ qkv = blk.attn.qkv(h_norm)
241
+ qkv = qkv.reshape(batch_size, -1, 3, self.num_heads, self.head_dim)
242
+ qkv = qkv.permute(2, 0, 3, 1, 4) # [3, B, heads, seq, dim]
243
+ qk, v = qkv.split([2, 1], dim=0)
244
+
245
+ qk_rot = (
246
+ qk * self.rotary_cos
247
+ + self._rotate_half(qk, batch_size) * self.rotary_sin
248
+ )
249
+ q_rot, k_rot = qk_rot.split([1, 1], dim=0)
250
+ attn = torch.matmul(q_rot, k_rot.transpose(-1, -2))
251
+ attn = torch.softmax(attn, dim=-1)
252
+ attn = torch.matmul(attn, v)
253
+ attn = attn.transpose(2, 3).reshape(
254
+ batch_size, -1, blk.attn.proj.in_features
255
+ )
256
+ hidden = hidden + blk.attn.proj(attn)
257
+
258
+ # --- feed-forward ---
259
+ mlp_out = blk.mlp.linear_fc1(blk.norm2(hidden))
260
+ mlp_out = blk.mlp.act_fn(mlp_out)
261
+ mlp_out = blk.mlp.linear_fc2(mlp_out)
262
+ hidden = hidden + mlp_out
263
+
264
+ # --- deepstack (explicit spatial merge) ---
265
+ if layer_num in ds_indices:
266
+ idx = ds_indices.index(layer_num)
267
+ ds_layer = ds_modules[idx]
268
+ x_ds = self._spatial_merge(hidden, ds_layer.hidden_size)
269
+ x_ds = ds_layer.norm(x_ds)
270
+ x_ds = ds_layer.linear_fc1(x_ds)
271
+ x_ds = ds_layer.act_fn(x_ds)
272
+ x_ds = ds_layer.linear_fc2(x_ds)
273
+ deepstack_features.append(x_ds)
274
+
275
+ # merger (explicit spatial merge)
276
+ hidden = self.visual.merger.norm(hidden)
277
+ hidden = self._spatial_merge(hidden, self.visual.merger.hidden_size)
278
+ hidden = self.visual.merger.linear_fc1(hidden)
279
+ hidden = self.visual.merger.act_fn(hidden)
280
+ hidden = self.visual.merger.linear_fc2(hidden)
281
+
282
+ return *deepstack_features, hidden
283
+
284
+
285
+ # ══════════════════════════════════════════════════════════════════════════════
286
+ # Transformer Module (embedding mode — no KV cache, no lm_head)
287
+ # ══════════════════════════════════════════════════════════════════════════════
288
+ class LLM_MAIN_EMBED(torch.nn.Module):
289
+ """
290
+ All decoder layers in a single ONNX graph.
291
+
292
+ Differences from the generation-oriented LLM_MAIN:
293
+ • No KV-cache inputs / outputs (single forward pass)
294
+ • No lm_head projection (we need hidden states, not logits)
295
+ • Final RMSNorm is kept (not fused into lm_head)
296
+ • Outputs the full sequence (not just last token)
297
+
298
+ Input layout (via *all_inputs):
299
+ hidden_states, ds_feat_0 … ds_feat_N, rotary_cos, rotary_sin, attn_mask
300
+
301
+ Output:
302
+ last_hidden_state [batch, seq_len, hidden_size]
303
+ """
304
+
305
+ def __init__(
306
+ self,
307
+ model,
308
+ num_heads,
309
+ num_key_value_heads,
310
+ head_dim,
311
+ num_layers,
312
+ hidden_size,
313
+ deepstack_features_len,
314
+ ):
315
+ super().__init__()
316
+ self.model = model
317
+
318
+ self.head_dim = head_dim
319
+ self.head_dim_half = head_dim // 2
320
+ self.num_heads = num_heads
321
+ self.num_key_value_heads = num_key_value_heads
322
+ self.num_key_value_groups = num_heads // num_key_value_heads
323
+ self.qk_heads = num_heads + num_key_value_heads
324
+ self.num_layers = num_layers
325
+ self.deepstack_features_len = deepstack_features_len
326
+
327
+ # 3 trailing args after deepstack features: cos, sin, mask
328
+ self._ds_offset = 3 + deepstack_features_len
329
+
330
+ # ── save the final RMSNorm (before weight fusion touches layers) ─
331
+ lang = model.model.language_model
332
+ self.register_buffer(
333
+ "final_norm_weight", lang.norm.weight.data.clone()
334
+ )
335
+ self.final_norm_eps = float(lang.norm.variance_epsilon)
336
+
337
+ # ── ONNX-friendly GELU ──────────────────────────────────────────
338
+ self._replace_gelu(model)
339
+
340
+ # ── fuse weights (same optimisations as reference) ──────────────
341
+ self._fuse_weights(hidden_size)
342
+
343
+ # ═══════════��══════════════════════════════════════════════════════════
344
+ # Weight fusion (runs once at init)
345
+ # ══════════════════════════════════════════════════════════════════════
346
+ def _fuse_weights(self, hidden_size):
347
+ scale_factor = self.head_dim ** -0.25
348
+ norm_factor = hidden_size ** 0.5
349
+ norm_factor_qk = self.head_dim ** 0.5
350
+
351
+ with torch.no_grad():
352
+ for layer in self.model.model.language_model.layers:
353
+ self._fuse_qkv(layer, scale_factor, norm_factor, norm_factor_qk)
354
+ self._fuse_gate_up(layer, norm_factor)
355
+ # NOTE: we do NOT fuse the final norm into lm_head (no lm_head).
356
+
357
+ def _fuse_qkv(self, layer, scale_factor, norm_factor, norm_factor_qk):
358
+ attn = layer.self_attn
359
+ q, k, v = attn.q_proj, attn.k_proj, attn.v_proj
360
+
361
+ in_f = int(q.in_features)
362
+ out_f = int(q.out_features + k.out_features + v.out_features)
363
+ has_bias = any(p.bias is not None for p in (q, k, v))
364
+
365
+ qkv = torch.nn.Linear(in_f, out_f, bias=has_bias)
366
+ qkv.weight.copy_(torch.cat([q.weight, k.weight, v.weight], dim=0))
367
+ if has_bias:
368
+ def _b(p):
369
+ return p.bias if p.bias is not None else torch.zeros(
370
+ p.out_features, dtype=qkv.weight.dtype
371
+ )
372
+ qkv.bias.copy_(torch.cat([_b(q), _b(k), _b(v)], dim=0))
373
+
374
+ attn.q_out_features = int(q.out_features)
375
+ attn.k_out_features = int(k.out_features)
376
+ attn.v_out_features = int(v.out_features)
377
+ del attn.q_proj, attn.k_proj, attn.v_proj
378
+
379
+ # fuse QK norms + attention scaling
380
+ combined_scale = scale_factor * norm_factor_qk
381
+ attn.q_norm.weight.mul_(combined_scale)
382
+ attn.k_norm.weight.mul_(combined_scale)
383
+ q_norm_rep = attn.q_norm.weight.repeat(self.num_heads)
384
+ k_norm_rep = attn.k_norm.weight.repeat(self.num_key_value_heads)
385
+ attn.qk_norm_weight = torch.nn.Parameter(
386
+ torch.cat([q_norm_rep, k_norm_rep], dim=0).view(
387
+ 1, 1, 1, -1, self.head_dim
388
+ )
389
+ )
390
+ del attn.q_norm, attn.k_norm
391
+
392
+ # absorb input LayerNorm into QKV
393
+ input_norm_w = layer.input_layernorm.weight.unsqueeze(0) * norm_factor
394
+ qkv.weight.mul_(input_norm_w)
395
+ attn.qkv = qkv
396
+ del layer.input_layernorm
397
+
398
+ def _fuse_gate_up(self, layer, norm_factor):
399
+ post_norm_w = layer.post_attention_layernorm.weight.unsqueeze(0) * norm_factor
400
+ gate, up = layer.mlp.gate_proj, layer.mlp.up_proj
401
+ gate_up = torch.nn.Linear(
402
+ gate.in_features, gate.out_features + up.out_features, bias=False
403
+ )
404
+ gate_up.weight.copy_(
405
+ torch.cat([gate.weight * post_norm_w, up.weight * post_norm_w], dim=0)
406
+ )
407
+ layer.mlp.gate_up_proj = gate_up
408
+ del layer.mlp.gate_proj, layer.mlp.up_proj, layer.post_attention_layernorm
409
+
410
+ # ══════════════════════════════════════════════════════════════════════
411
+ # Utility
412
+ # ══════════════════════════════════════════════════════════════════════
413
+ @staticmethod
414
+ def _replace_gelu(module):
415
+ for name, child in module.named_children():
416
+ if isinstance(child, torch.nn.GELU):
417
+ setattr(module, name, torch.nn.GELU(approximate="tanh"))
418
+ else:
419
+ LLM_MAIN_EMBED._replace_gelu(child)
420
+
421
+ def _rms_norm(self, x):
422
+ """Sum-based RMS norm (weight is absorbed into the subsequent linear)."""
423
+ return x * torch.rsqrt(x.square().sum(-1, keepdim=True))
424
+
425
+ def _rotate_half(self, x, batch_size):
426
+ x = x.view(batch_size, -1, 1, self.qk_heads, 2, self.head_dim_half)
427
+ x = x.flip(-2)
428
+ return x.view(batch_size, -1, 1, self.qk_heads, self.head_dim)
429
+
430
+ # ══════════════════════════════════════════════════════════════════════
431
+ # Forward
432
+ # ══════════════════════════════════════════════════════════════════════
433
+ def forward(self, *all_inputs):
434
+ """
435
+ all_inputs layout:
436
+ hidden_states [batch, seq, hidden]
437
+ deepstack_feat_0 … [1, seq, hidden] (×deepstack_features_len)
438
+ rotary_cos [1, seq, 1, 1, head_dim]
439
+ rotary_sin [1, seq, 1, 1, head_dim]
440
+ attention_mask [1, 1, 1, seq, seq]
441
+ """
442
+ hidden_states = all_inputs[0]
443
+ rotary_cos = all_inputs[-3]
444
+ rotary_sin = all_inputs[-2]
445
+ attention_mask = all_inputs[-1]
446
+ batch_size = hidden_states.shape[0]
447
+
448
+ for i, layer in enumerate(self.model.model.language_model.layers):
449
+ # ── self-attention ───────────────────────────────────────
450
+ residual = hidden_states
451
+ hidden_states = self._rms_norm(hidden_states)
452
+
453
+ qkv = layer.self_attn.qkv(hidden_states)
454
+ qkv = qkv.reshape(
455
+ batch_size, -1, 1,
456
+ self.qk_heads + self.num_key_value_heads,
457
+ self.head_dim,
458
+ )
459
+ qk, v = torch.split(
460
+ qkv, [self.qk_heads, self.num_key_value_heads], dim=-2
461
+ )
462
+
463
+ qk = self._rms_norm(qk) * layer.self_attn.qk_norm_weight
464
+ qk_rot = (
465
+ qk * rotary_cos
466
+ + self._rotate_half(qk, batch_size) * rotary_sin
467
+ )
468
+
469
+ q, k = torch.split(
470
+ qk_rot, [self.num_heads, self.num_key_value_heads], dim=-2
471
+ )
472
+ q = q.reshape(
473
+ batch_size, -1,
474
+ self.num_key_value_heads,
475
+ self.num_key_value_groups,
476
+ self.head_dim,
477
+ )
478
+ q = q.permute(0, 2, 3, 1, 4) # [B, kv_h, g, seq, dim]
479
+ k = k.permute(0, 3, 2, 4, 1) # [B, kv_h, 1, dim, seq]
480
+ v = v.transpose(1, 3) # [B, kv_h, 1, seq, dim]
481
+
482
+ # no KV cache — direct attention over the full sequence
483
+ attn = torch.matmul(q, k) + attention_mask
484
+ attn = torch.softmax(attn, dim=-1)
485
+ attn = torch.matmul(attn, v)
486
+
487
+ attn = attn.permute(0, 3, 1, 2, 4).reshape(
488
+ batch_size, -1, layer.self_attn.o_proj.in_features
489
+ )
490
+ hidden_states = residual + layer.self_attn.o_proj(attn)
491
+
492
+ # ── feed-forward ─────────────────────────────────────────
493
+ residual = hidden_states
494
+ hidden_states = self._rms_norm(hidden_states)
495
+
496
+ gate_up = layer.mlp.gate_up_proj(hidden_states)
497
+ gate, up = torch.split(
498
+ gate_up,
499
+ [layer.mlp.down_proj.in_features, layer.mlp.down_proj.in_features],
500
+ dim=-1,
501
+ )
502
+ hidden_states = residual + layer.mlp.down_proj(
503
+ layer.mlp.act_fn(gate) * up
504
+ )
505
+
506
+ # ── deepstack feature injection ──────────────────────────
507
+ if i < self.deepstack_features_len:
508
+ hidden_states = all_inputs[1 + i] + hidden_states
509
+
510
+ # ── final RMSNorm (standard, not fused) ─────────────────────────
511
+ variance = hidden_states.pow(2).mean(-1, keepdim=True)
512
+ hidden_states = hidden_states * torch.rsqrt(
513
+ variance + self.final_norm_eps
514
+ )
515
+ hidden_states = hidden_states * self.final_norm_weight
516
+
517
+ return hidden_states
518
+
519
+
520
+ # ══════════════════════════════════════════════════════════════════════════════
521
+ # Export
522
+ # ══════════════════════════════════════════════════════════════════════════════
523
+ def main(model_path, output_dir):
524
+ os.makedirs(output_dir, exist_ok=True)
525
+
526
+ onnx_vision = os.path.join(output_dir, "Vision.onnx")
527
+ onnx_transformer = os.path.join(output_dir, "Transformer.onnx")
528
+ rotary_params = os.path.join(output_dir, "rotary_params.npz")
529
+
530
+ # ── Save processor files (tokenizer + image processor config) ───────
531
+ # Stored in a `tokenizer/` subdirectory so the output layout stays tidy
532
+ # and the directory remains self-contained — no HF repo pull at inference.
533
+ print("Saving processor files …")
534
+ from transformers.models.qwen3_vl.processing_qwen3_vl import Qwen3VLProcessor
535
+ tokenizer_dir = os.path.join(output_dir, "tokenizer")
536
+ os.makedirs(tokenizer_dir, exist_ok=True)
537
+ Qwen3VLProcessor.from_pretrained(model_path).save_pretrained(tokenizer_dir)
538
+ print(f" Processor saved to {tokenizer_dir}")
539
+
540
+ print("Loading model …")
541
+ model = Qwen3VLForEmbedding.from_pretrained(
542
+ model_path, torch_dtype=torch.float32, device_map="cpu", low_cpu_mem_usage=True
543
+ ).eval()
544
+
545
+ # ── extract config ──��───────────────────────────────────────────────
546
+ text_cfg = model.config.text_config
547
+ num_heads = text_cfg.num_attention_heads
548
+ num_key_value_heads = text_cfg.num_key_value_heads
549
+ head_dim = text_cfg.head_dim
550
+ num_layers = text_cfg.num_hidden_layers
551
+ hidden_size = text_cfg.hidden_size
552
+ deepstack_features_len = len(model.model.visual.deepstack_visual_indexes)
553
+
554
+ patch_size = model.model.visual.patch_size
555
+ merge_size = model.model.visual.spatial_merge_size
556
+ temporal_patch_size = model.model.visual.patch_embed.temporal_patch_size
557
+ unit = patch_size * merge_size
558
+
559
+ # Compute or validate IMAGE_HEIGHT / IMAGE_WIDTH
560
+ if IMAGE_HEIGHT is None:
561
+ IMAGE_HEIGHT_val = 16 * unit # sensible default
562
+ else:
563
+ IMAGE_HEIGHT_val = IMAGE_HEIGHT
564
+ if IMAGE_WIDTH is None:
565
+ IMAGE_WIDTH_val = 16 * unit
566
+ else:
567
+ IMAGE_WIDTH_val = IMAGE_WIDTH
568
+
569
+ assert IMAGE_HEIGHT_val % unit == 0, (
570
+ f"IMAGE_HEIGHT ({IMAGE_HEIGHT_val}) must be a multiple of "
571
+ f"patch_size * merge_size = {unit}"
572
+ )
573
+ assert IMAGE_WIDTH_val % unit == 0, (
574
+ f"IMAGE_WIDTH ({IMAGE_WIDTH_val}) must be a multiple of "
575
+ f"patch_size * merge_size = {unit}"
576
+ )
577
+
578
+ HEIGHT_FACTOR = IMAGE_HEIGHT_val // unit
579
+ WIDTH_FACTOR = IMAGE_WIDTH_val // unit
580
+
581
+ # Number of temporal patches after Conv3d with stride=temporal_patch_size.
582
+ # The HF processor pads frames to multiples of temporal_patch_size.
583
+ temporal_patches = math.ceil(TEMPORAL_SIZE / temporal_patch_size)
584
+ vision_embed_size = temporal_patches * HEIGHT_FACTOR * WIDTH_FACTOR
585
+
586
+ print(f" text : layers={num_layers} heads={num_heads} kv_heads={num_key_value_heads} "
587
+ f"head_dim={head_dim} hidden={hidden_size}")
588
+ print(f" vision: deepstack={deepstack_features_len} patch={patch_size} merge={merge_size}")
589
+ print(f" TEMPORAL_SIZE={TEMPORAL_SIZE} temporal_patch_size={temporal_patch_size}"
590
+ f" → temporal_patches={temporal_patches}")
591
+ print(f" image size: {IMAGE_HEIGHT_val}x{IMAGE_WIDTH_val} → {vision_embed_size} vision tokens")
592
+
593
+ with torch.inference_mode():
594
+
595
+ # ══════════════════════════════════════════════════════════════════
596
+ # 1. Save rotary parameters + token embedding weight (to skip Embed.onnx)
597
+ # ══════════════════════════════════════════════════════════════════
598
+ rotary_emb = model.model.language_model.rotary_emb
599
+ inv_freq = rotary_emb.inv_freq.cpu().numpy()
600
+ mrope_section = np.array(rotary_emb.mrope_section, dtype=np.int64)
601
+ embed_weight = (
602
+ model.model.language_model.embed_tokens.weight
603
+ .detach().cpu().float().numpy()
604
+ )
605
+ np.savez(
606
+ rotary_params,
607
+ inv_freq=inv_freq,
608
+ mrope_section=mrope_section,
609
+ embed_weight=embed_weight,
610
+ image_height=IMAGE_HEIGHT_val,
611
+ image_width=IMAGE_WIDTH_val,
612
+ height_factor=HEIGHT_FACTOR,
613
+ width_factor=WIDTH_FACTOR,
614
+ patch_size=patch_size,
615
+ merge_size=merge_size,
616
+ hidden_size=hidden_size,
617
+ head_dim=head_dim,
618
+ max_seq_len=MAX_SEQ_LEN,
619
+ temporal_patch_size=temporal_patch_size,
620
+ temporal_patches=temporal_patches,
621
+ temporal_size=TEMPORAL_SIZE,
622
+ )
623
+ print(f"Saved rotary params → {rotary_params}")
624
+
625
+ # ══════════════════════════════════════════════════════════════════
626
+ # 2. Export Vision
627
+ # ══════════════════════════════════════════════════════════════════
628
+ print("Exporting Vision …")
629
+ total_patches = temporal_patches * HEIGHT_FACTOR * merge_size * WIDTH_FACTOR * merge_size
630
+ flatten_dim = 3 * temporal_patch_size * patch_size * patch_size
631
+ dummy_pixels = torch.randn(total_patches, flatten_dim, dtype=torch.float32)
632
+
633
+ vision_output_names = []
634
+ vision_dynamic_axes = {}
635
+ for i in range(deepstack_features_len):
636
+ name = f"deepstack_feature_{i}"
637
+ vision_output_names.append(name)
638
+ vision_output_names.append("vision_hidden_states")
639
+
640
+ torch.onnx.export(
641
+ LLM_VISION_EMBED(model, HEIGHT_FACTOR, WIDTH_FACTOR, temporal_patches),
642
+ (dummy_pixels,),
643
+ onnx_vision,
644
+ input_names=["pixel_values"],
645
+ output_names=vision_output_names,
646
+ dynamic_axes=vision_dynamic_axes if vision_dynamic_axes else None,
647
+ opset_version=OPSET,
648
+ dynamo=False,
649
+ )
650
+ del dummy_pixels
651
+ gc.collect()
652
+ print(f" → {onnx_vision}")
653
+ print(" Consolidating external weights …")
654
+ consolidate_external_data(onnx_vision)
655
+
656
+ # ══════════════════════════════════════════════════════════════════
657
+ # 3. Export Transformer
658
+ # ══════════════════════════════════════════════════════════════════
659
+ print("Exporting Transformer …")
660
+
661
+ # dummy sequence length for tracing
662
+ dummy_seq = 10 + vision_embed_size
663
+ dummy_hidden = torch.ones((1, dummy_seq, hidden_size), dtype=torch.float32)
664
+ dummy_ds = torch.ones((1, dummy_seq, hidden_size), dtype=torch.float32)
665
+ dummy_cos = torch.zeros((1, dummy_seq, 1, 1, head_dim), dtype=torch.float32)
666
+ dummy_sin = torch.zeros((1, dummy_seq, 1, 1, head_dim), dtype=torch.float32)
667
+ dummy_mask = torch.zeros((1, 1, 1, dummy_seq, dummy_seq), dtype=torch.float32)
668
+
669
+ all_inputs = [dummy_hidden]
670
+ input_names = ["hidden_states"]
671
+ dynamic_axes = {
672
+ "hidden_states": {0: "batch", 1: "seq_len"},
673
+ "last_hidden_state": {0: "batch", 1: "seq_len"},
674
+ "rotary_cos": {1: "seq_len"},
675
+ "rotary_sin": {1: "seq_len"},
676
+ "attention_mask": {3: "seq_len", 4: "seq_len"},
677
+ }
678
+
679
+ for i in range(deepstack_features_len):
680
+ name = f"deepstack_features_{i}"
681
+ input_names.append(name)
682
+ all_inputs.append(dummy_ds)
683
+ dynamic_axes[name] = {1: "seq_len"}
684
+
685
+ all_inputs.extend([dummy_cos, dummy_sin, dummy_mask])
686
+ input_names.extend(["rotary_cos", "rotary_sin", "attention_mask"])
687
+
688
+ model_main = LLM_MAIN_EMBED(
689
+ model, num_heads, num_key_value_heads, head_dim,
690
+ num_layers, hidden_size, deepstack_features_len,
691
+ )
692
+ del model
693
+ gc.collect()
694
+
695
+ torch.onnx.export(
696
+ model_main,
697
+ tuple(all_inputs),
698
+ onnx_transformer,
699
+ input_names=input_names,
700
+ output_names=["last_hidden_state"],
701
+ dynamic_axes=dynamic_axes,
702
+ opset_version=OPSET,
703
+ dynamo=False,
704
+ )
705
+ del model_main, all_inputs
706
+ gc.collect()
707
+ print(f" → {onnx_transformer}")
708
+ print(" Consolidating external weights …")
709
+ consolidate_external_data(onnx_transformer)
710
+
711
+ print("Export complete.")
712
+
713
+ if __name__ == "__main__":
714
+ parser = argparse.ArgumentParser(
715
+ description="Export Qwen3-VL-Embedding model to ONNX files"
716
+ )
717
+ parser.add_argument(
718
+ "--model_path", type=str, default=DEFAULT_MODEL_PATH,
719
+ help=f"Path to the HF model (default: {DEFAULT_MODEL_PATH})",
720
+ )
721
+ parser.add_argument(
722
+ "--output_dir", type=str, default=DEFAULT_OUTPUT_DIR,
723
+ help=f"Directory to save ONNX files (default: {DEFAULT_OUTPUT_DIR})",
724
+ )
725
+ args = parser.parse_args()
726
+
727
+ main(args.model_path, args.output_dir)
export_script/b_export_onnx_vision.py ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Direct HF export of the Qwen3-VL vision encoder to ONNX.
3
+
4
+ Instead of manually reimplementing the vision encoder (with norm fusion,
5
+ GELU replacement, etc.), this script wraps the HF model's own
6
+ Qwen3VLVisionModel and exports it via torch.onnx.export.
7
+
8
+ This produces a Vision.onnx that is numerically identical to PyTorch
9
+ because it traces through the exact same code path.
10
+
11
+ Usage:
12
+ python 2_export_onnx_vision.py
13
+ python 2_export_onnx_vision.py --model_path /path/to/model --output_dir /path/to/output
14
+
15
+ This ONLY re-exports Vision.onnx. The Embed.onnx, Transformer.onnx,
16
+ and rotary_params.npz from export_embedding_onnx.py are reused as-is.
17
+ """
18
+
19
+ import math
20
+ import os
21
+ import gc
22
+ import argparse
23
+ import torch
24
+ import onnx
25
+ import numpy as np
26
+ from qwen3_vl_embedding import Qwen3VLForEmbedding
27
+
28
+ def consolidate_external_data(onnx_path: str):
29
+ """
30
+ Re-save an ONNX model so all external weights live in one .onnx.data file
31
+ instead of hundreds of per-tensor files.
32
+ """
33
+ out_dir = os.path.dirname(onnx_path)
34
+ base = os.path.basename(onnx_path)
35
+ data_rel = base + ".data" # sibling file: Transformer.onnx.data
36
+ model = onnx.load(onnx_path, load_external_data=True)
37
+
38
+ # Delete ONNX per-tensor external files (e.g.
39
+ # `model.model.language_model.layers.0.self_attn.qk_norm_weight`). They can
40
+ # be named with dots, so we use an allow-list of known suffixes we want to
41
+ # keep. Subdirectories (e.g. `tokenizer/`) are skipped via the isfile check.
42
+ PRESERVE_SUFFIXES = (".onnx", ".onnx.data", ".npz", ".bin")
43
+ for fname in list(os.listdir(out_dir)):
44
+ fpath = os.path.join(out_dir, fname)
45
+ if not os.path.isfile(fpath):
46
+ continue
47
+ if any(fname.endswith(sfx) for sfx in PRESERVE_SUFFIXES):
48
+ continue
49
+ try:
50
+ os.remove(fpath)
51
+ except OSError:
52
+ pass
53
+
54
+ # Remove the target .data file if it already exists (stale)
55
+ target_data = os.path.join(out_dir, data_rel)
56
+ if os.path.exists(target_data):
57
+ os.remove(target_data)
58
+
59
+ onnx.save_model(
60
+ model, onnx_path,
61
+ save_as_external_data=True,
62
+ all_tensors_to_one_file=True,
63
+ location=data_rel,
64
+ size_threshold=1024,
65
+ convert_attribute=False,
66
+ )
67
+ del model
68
+ gc.collect()
69
+
70
+ # ══════════════════════════════════════════════════════════════════════════════
71
+ # Configuration (must match export_embedding_onnx.py)
72
+ # ══════════════════════════════════════════════════════════════════════════════
73
+ QWEN3VLE_MODEL_HF_PATH = os.getenv(
74
+ "MODEL_QWEN3VLE_MODEL_HF_PATH", "/home/jordan/Research/Product-AI-mono/assets/model/Qwen3-VL-Embedding-2B"
75
+ )
76
+ QWEN3VLE_TRT_DIR_PATH = os.getenv(
77
+ "MODEL_QWEN3VLE_TRT_DIR_PATH", "assets/model/Qwen3-VL-Embedding-2B-onnx"
78
+ )
79
+ TEMPORAL_SIZE=1
80
+
81
+ DEFAULT_MODEL_PATH = QWEN3VLE_MODEL_HF_PATH
82
+ DEFAULT_OUTPUT_DIR = QWEN3VLE_TRT_DIR_PATH
83
+ OPSET = 17
84
+
85
+
86
+ # ══════════════════════════════════════════════════════════════════════════════
87
+ # Vision Wrapper — thin shell around the HF model
88
+ # ══════════════════════════════════════════════════════════════════════════════
89
+ class VisionEncoderWrapper(torch.nn.Module):
90
+ """
91
+ Wraps Qwen3VLVisionModel for ONNX export.
92
+
93
+ The HF vision model's forward() takes (hidden_states, grid_thw) and
94
+ returns (merged_features, deepstack_features_list).
95
+
96
+ For ONNX we bake in a fixed grid_thw and return flat outputs.
97
+ """
98
+
99
+ def __init__(self, visual, grid_thw: torch.Tensor):
100
+ super().__init__()
101
+ self.visual = visual
102
+ # Bake in the fixed grid as a buffer so it's part of the ONNX graph
103
+ self.register_buffer("grid_thw", grid_thw)
104
+
105
+ def forward(self, pixel_values):
106
+ """
107
+ pixel_values : float32 [total_patches, flatten_dim]
108
+ From the HF processor (already CLIP-normalised).
109
+
110
+ Returns: (deepstack_feature_0, …, vision_hidden_states)
111
+ """
112
+ hidden_states, deepstack_features = self.visual(
113
+ pixel_values, grid_thw=self.grid_thw
114
+ )
115
+ # Return deepstack features first, then merged vision features
116
+ return *deepstack_features, hidden_states
117
+
118
+
119
+ # ══════════════════════════════════════════════════════════��═══════════════════
120
+ # Export
121
+ # ══════════════════════════════════════════════════════════════════════════════
122
+ def main(model_path, output_dir):
123
+ os.makedirs(output_dir, exist_ok=True)
124
+ onnx_vision = os.path.join(output_dir, "Vision.onnx")
125
+
126
+ # ── Load rotary_params to get the grid config ────────────────────────
127
+ rp = np.load(os.path.join(output_dir, "rotary_params.npz"))
128
+ height_factor = int(rp["height_factor"])
129
+ width_factor = int(rp["width_factor"])
130
+ patch_size = int(rp["patch_size"])
131
+ merge_size = int(rp["merge_size"])
132
+
133
+ grid_h = height_factor * merge_size
134
+ grid_w = width_factor * merge_size
135
+ image_height = int(rp["image_height"])
136
+ image_width = int(rp["image_width"])
137
+
138
+ temporal_patch_size_npz = int(rp["temporal_patch_size"]) if "temporal_patch_size" in rp else 2
139
+ temporal_patches = math.ceil(TEMPORAL_SIZE / temporal_patch_size_npz)
140
+ total_patches = temporal_patches * grid_h * grid_w
141
+
142
+ print(f"Grid: {temporal_patches}x{grid_h}x{grid_w} = {total_patches} patches")
143
+ print(f"Image: {image_height}x{image_width} TEMPORAL_SIZE={TEMPORAL_SIZE}")
144
+
145
+ # ── Load model ──────────────────────────────────────────────────────
146
+ print("Loading model …")
147
+ model = Qwen3VLForEmbedding.from_pretrained(
148
+ model_path,
149
+ torch_dtype=torch.float32,
150
+ device_map="cpu",
151
+ low_cpu_mem_usage=True,
152
+ _attn_implementation="eager",
153
+ ).eval()
154
+
155
+ visual = model.model.visual
156
+ temporal_patch_size = visual.patch_embed.temporal_patch_size
157
+ flatten_dim = 3 * temporal_patch_size * patch_size * patch_size
158
+
159
+ deepstack_features_len = len(visual.deepstack_visual_indexes)
160
+ print(f" deepstack features: {deepstack_features_len}")
161
+ print(f" temporal_patch_size: {temporal_patch_size}")
162
+ print(f" flatten_dim: {flatten_dim}")
163
+
164
+ # ── Build wrapper ───────────────────────────────────────────────────
165
+ grid_thw = torch.tensor([[temporal_patches, grid_h, grid_w]], dtype=torch.int64)
166
+ wrapper = VisionEncoderWrapper(visual, grid_thw)
167
+
168
+ # ── Build dummy input ───────────────────────────────────────────────
169
+ dummy_pixels = torch.randn(total_patches, flatten_dim, dtype=torch.float32)
170
+
171
+ # ── Output names ────────────────────────────────────────────────────
172
+ output_names = []
173
+ for i in range(deepstack_features_len):
174
+ output_names.append(f"deepstack_feature_{i}")
175
+ output_names.append("vision_hidden_states")
176
+
177
+ # ── Export ──────────────────────────────────────────────────────────
178
+ print("Exporting Vision (direct HF) …")
179
+ with torch.inference_mode():
180
+ torch.onnx.export(
181
+ wrapper,
182
+ (dummy_pixels,),
183
+ onnx_vision,
184
+ input_names=["pixel_values"],
185
+ output_names=output_names,
186
+ opset_version=OPSET,
187
+ dynamo=False,
188
+ )
189
+
190
+ del wrapper, model
191
+ gc.collect()
192
+ print(f" → {onnx_vision}")
193
+ print("Consolidating external weights …")
194
+ consolidate_external_data(onnx_vision)
195
+ print("Done.")
196
+
197
+
198
+ if __name__ == "__main__":
199
+ parser = argparse.ArgumentParser(
200
+ description="Export Qwen3-VL vision encoder to ONNX (direct HF)"
201
+ )
202
+ parser.add_argument(
203
+ "--model_path", type=str, default=DEFAULT_MODEL_PATH,
204
+ help=f"Path to the HF model (default: {DEFAULT_MODEL_PATH})",
205
+ )
206
+ parser.add_argument(
207
+ "--output_dir", type=str, default=DEFAULT_OUTPUT_DIR,
208
+ help=f"Directory to save ONNX files (default: {DEFAULT_OUTPUT_DIR})",
209
+ )
210
+ args = parser.parse_args()
211
+
212
+ main(args.model_path, args.output_dir)
export_script/qwen3_vl_embedding.py ADDED
@@ -0,0 +1,393 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import torch
3
+ import torch.nn.functional as F
4
+ import unicodedata
5
+ import numpy as np
6
+ import logging
7
+
8
+ from PIL import Image
9
+ from urllib.parse import urlparse
10
+ from dataclasses import dataclass
11
+ from typing import Optional, List, Union, Dict, Any
12
+ from transformers.models.qwen3_vl.modeling_qwen3_vl import Qwen3VLPreTrainedModel, Qwen3VLModel, Qwen3VLConfig
13
+ from transformers.models.qwen3_vl.processing_qwen3_vl import Qwen3VLProcessor
14
+ from transformers.modeling_outputs import ModelOutput
15
+ from transformers.processing_utils import Unpack
16
+ from transformers.utils import TransformersKwargs
17
+ from transformers.cache_utils import Cache
18
+ from transformers.utils.generic import check_model_inputs
19
+ from qwen_vl_utils.vision_process import process_vision_info
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # Constants for configuration
24
+ MAX_LENGTH = 8192
25
+ IMAGE_BASE_FACTOR = 16
26
+ IMAGE_FACTOR = IMAGE_BASE_FACTOR * 2
27
+ MIN_PIXELS = 4 * IMAGE_FACTOR * IMAGE_FACTOR
28
+ MAX_PIXELS = 1800 * IMAGE_FACTOR * IMAGE_FACTOR
29
+ FPS = 1
30
+ MAX_FRAMES = 64
31
+ FRAME_MAX_PIXELS = 768 * IMAGE_FACTOR * IMAGE_FACTOR
32
+ MAX_TOTAL_PIXELS = 10 * FRAME_MAX_PIXELS
33
+ PAD_TOKEN = "<|endoftext|>"
34
+
35
+ # Define output structure for embeddings
36
+ @dataclass
37
+ class Qwen3VLForEmbeddingOutput(ModelOutput):
38
+ last_hidden_state: Optional[torch.FloatTensor] = None
39
+ attention_mask: Optional[torch.Tensor] = None
40
+
41
+ # Define model class to compute embeddings
42
+ class Qwen3VLForEmbedding(Qwen3VLPreTrainedModel):
43
+ _checkpoint_conversion_mapping = {}
44
+ accepts_loss_kwargs = False
45
+ config: Qwen3VLConfig
46
+
47
+ def __init__(self, config):
48
+ super().__init__(config)
49
+ self.model = Qwen3VLModel(config)
50
+ self.post_init()
51
+
52
+ def get_input_embeddings(self):
53
+ return self.model.get_input_embeddings()
54
+
55
+ def set_input_embeddings(self, value):
56
+ self.model.set_input_embeddings(value)
57
+
58
+ def set_decoder(self, decoder):
59
+ self.model.set_decoder(decoder)
60
+
61
+ def get_decoder(self):
62
+ return self.model.get_decoder()
63
+
64
+ # Extract video features from model
65
+ def get_video_features(self, pixel_values_videos: torch.FloatTensor,
66
+ video_grid_thw: Optional[torch.LongTensor] = None):
67
+ return self.model.get_video_features(pixel_values_videos, video_grid_thw)
68
+
69
+ # Extract image features from model
70
+ def get_image_features(self, pixel_values: torch.FloatTensor,
71
+ image_grid_thw: Optional[torch.LongTensor] = None):
72
+ return self.model.get_image_features(pixel_values, image_grid_thw)
73
+
74
+ # Make modules accessible through properties
75
+ @property
76
+ def language_model(self):
77
+ return self.model.language_model
78
+
79
+ @property
80
+ def visual(self):
81
+ return self.model.visual
82
+
83
+ # Forward pass through model with input parameters
84
+ # @check_model_inputs
85
+ def forward(self,
86
+ input_ids: torch.LongTensor = None,
87
+ attention_mask: Optional[torch.Tensor] = None,
88
+ position_ids: Optional[torch.LongTensor] = None,
89
+ past_key_values: Optional[Cache] = None,
90
+ inputs_embeds: Optional[torch.FloatTensor] = None,
91
+ pixel_values: Optional[torch.Tensor] = None,
92
+ pixel_values_videos: Optional[torch.FloatTensor] = None,
93
+ image_grid_thw: Optional[torch.LongTensor] = None,
94
+ video_grid_thw: Optional[torch.LongTensor] = None,
95
+ cache_position: Optional[torch.LongTensor] = None,
96
+ logits_to_keep: Union[int, torch.Tensor] = 0,
97
+ **kwargs: Unpack[TransformersKwargs],
98
+ ) -> Union[tuple, Qwen3VLForEmbeddingOutput]:
99
+ # Pass inputs through the model
100
+ outputs = self.model(
101
+ input_ids=input_ids,
102
+ pixel_values=pixel_values,
103
+ pixel_values_videos=pixel_values_videos,
104
+ image_grid_thw=image_grid_thw,
105
+ video_grid_thw=video_grid_thw,
106
+ position_ids=position_ids,
107
+ attention_mask=attention_mask,
108
+ past_key_values=past_key_values,
109
+ inputs_embeds=inputs_embeds,
110
+ cache_position=cache_position,
111
+ **kwargs,
112
+ )
113
+ # Return the model output
114
+ return Qwen3VLForEmbeddingOutput(
115
+ last_hidden_state=outputs.last_hidden_state,
116
+ attention_mask=attention_mask,
117
+ )
118
+
119
+ def sample_frames(frames: List[Union[str, Image.Image]], max_segments: int) -> List[Union[str, Image.Image]]:
120
+ duration = len(frames)
121
+ if duration <= max_segments:
122
+ return frames
123
+
124
+ frame_id_array = np.linspace(0, duration - 1, max_segments, dtype=int)
125
+ frame_id_list = frame_id_array.tolist()
126
+ sampled_frames = [ frames[frame_idx] for frame_idx in frame_id_list ]
127
+ return sampled_frames
128
+
129
+ def is_image_path(path: str) -> bool:
130
+ image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.tiff', '.svg'}
131
+
132
+ if path.startswith(('http://', 'https://')):
133
+ # Parse URL to remove query parameters
134
+ parsed_url = urlparse(path)
135
+ clean_path = parsed_url.path
136
+ else:
137
+ clean_path = path
138
+
139
+ # Check file extension
140
+ _, ext = os.path.splitext(clean_path.lower())
141
+ return ext in image_extensions
142
+
143
+ def is_video_input(video) -> bool:
144
+ if isinstance(video, str):
145
+ return True
146
+
147
+ if isinstance(video, list) and len(video) > 0:
148
+ # Check first element to determine the type
149
+ first_elem = video[0]
150
+
151
+ if isinstance(first_elem, Image.Image):
152
+ return True
153
+
154
+ if isinstance(first_elem, str):
155
+ return is_image_path(first_elem)
156
+
157
+ return False
158
+
159
+ # Define embedder class for processing inputs and generating embeddings
160
+ class Qwen3VLEmbedder():
161
+ def __init__(
162
+ self,
163
+ model_name_or_path: str,
164
+ max_length: int = MAX_LENGTH,
165
+ min_pixels: int = MIN_PIXELS,
166
+ max_pixels: int = MAX_PIXELS,
167
+ total_pixels: int = MAX_TOTAL_PIXELS,
168
+ fps: float = FPS,
169
+ max_frames: int = MAX_FRAMES,
170
+ default_instruction: str = "Represent the user's input.",
171
+ **kwargs
172
+ ):
173
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
174
+
175
+ self.max_length = max_length
176
+ self.min_pixels = min_pixels
177
+ self.max_pixels = max_pixels
178
+ self.total_pixels = total_pixels
179
+ self.fps = fps
180
+ self.max_frames = max_frames
181
+
182
+ self.default_instruction = default_instruction
183
+
184
+ self.model = Qwen3VLForEmbedding.from_pretrained(
185
+ model_name_or_path, trust_remote_code=True, **kwargs
186
+ ).to(device)
187
+ self.processor = Qwen3VLProcessor.from_pretrained(
188
+ model_name_or_path, padding_side='right'
189
+ )
190
+ self.model.eval()
191
+
192
+ @torch.no_grad()
193
+ def forward(self, inputs: Dict[str, Any]) -> Dict[str, torch.Tensor]:
194
+ outputs = self.model(**inputs)
195
+ return {
196
+ 'last_hidden_state': outputs.last_hidden_state,
197
+ 'attention_mask': inputs.get('attention_mask')
198
+ }
199
+
200
+ # Truncate token sequence to a specified max length
201
+ def _truncate_tokens(self, token_ids: List[int], max_length: int) -> List[int]:
202
+ if len(token_ids) <= max_length:
203
+ return token_ids
204
+
205
+ special_token_ids = set(self.processor.tokenizer.all_special_ids)
206
+ num_special = sum(1 for token_idx in token_ids if token_idx in special_token_ids)
207
+ num_non_special_to_keep = max_length - num_special
208
+
209
+ final_token_ids = []
210
+ non_special_kept_count = 0
211
+ # Ensure retention of special tokens while truncating the rest
212
+ for token_idx in token_ids:
213
+ if token_idx in special_token_ids:
214
+ final_token_ids.append(token_idx)
215
+ elif non_special_kept_count < num_non_special_to_keep:
216
+ final_token_ids.append(token_idx)
217
+ non_special_kept_count += 1
218
+ return final_token_ids
219
+
220
+ def format_model_input(
221
+ self,
222
+ text: Optional[Union[List[str], str]] = None,
223
+ image: Optional[Union[List[Union[str, Image.Image]], str, Image.Image]] = None,
224
+ video: Optional[Union[List[Union[str, List[Union[str, Image.Image]]]], str, List[Union[str, Image.Image]]]] = None,
225
+ instruction: Optional[str] = None,
226
+ fps: Optional[float] = None,
227
+ max_frames: Optional[int] = None
228
+ ) -> List[Dict]:
229
+
230
+ # Ensure instruction ends with punctuation
231
+ if instruction:
232
+ instruction = instruction.strip()
233
+ if instruction and not unicodedata.category(instruction[-1]).startswith('P'):
234
+ instruction = instruction + '.'
235
+
236
+ # Initialize conversation with system prompts
237
+ content = []
238
+ conversation = [
239
+ {"role": "system", "content": [{"type": "text", "text": instruction or self.default_instruction}]},
240
+ {"role": "user", "content": content}
241
+ ]
242
+
243
+ # Normalize text input to list
244
+ if text is None:
245
+ texts = []
246
+ elif isinstance(text, str):
247
+ texts = [text]
248
+ else:
249
+ texts = text
250
+
251
+ # Normalize image input to list
252
+ if image is None:
253
+ images = []
254
+ elif not isinstance(image, list):
255
+ images = [image]
256
+ else:
257
+ images = image
258
+
259
+ # Normalize video input to list
260
+ if video is None:
261
+ videos = []
262
+ elif is_video_input(video):
263
+ videos = [video]
264
+ else:
265
+ # Assume it's a list of videos
266
+ videos = video
267
+
268
+ # Add text, image, or video content to conversation
269
+ if not texts and not images and not videos:
270
+ content.append({'type': 'text', 'text': "NULL"})
271
+ return conversation
272
+
273
+ # Process each video
274
+ for vid in videos:
275
+ video_content = None
276
+ video_kwargs = {'total_pixels': self.total_pixels}
277
+
278
+ if isinstance(vid, list):
279
+ # Video as frame sequence
280
+ video_content = vid
281
+ if self.max_frames is not None:
282
+ video_content = sample_frames(video_content, self.max_frames)
283
+ video_content = [
284
+ ('file://' + ele if isinstance(ele, str) else ele)
285
+ for ele in video_content
286
+ ]
287
+ elif isinstance(vid, str):
288
+ # Video as file path
289
+ video_content = vid if vid.startswith(('http://', 'https://')) else 'file://' + vid
290
+ video_kwargs = {'fps': fps or self.fps, 'max_frames': max_frames or self.max_frames}
291
+ else:
292
+ raise TypeError(f"Unrecognized video type: {type(vid)}")
293
+
294
+ # Add video input to content
295
+ if video_content:
296
+ content.append({
297
+ 'type': 'video',
298
+ 'video': video_content,
299
+ **video_kwargs
300
+ })
301
+
302
+ # Process each image
303
+ for img in images:
304
+ image_content = None
305
+
306
+ if isinstance(img, Image.Image):
307
+ image_content = img
308
+ elif isinstance(img, str):
309
+ image_content = img if img.startswith(('http://', 'https://')) else 'file://' + img
310
+ else:
311
+ raise TypeError(f"Unrecognized image type: {type(img)}")
312
+
313
+ # Add image input to content
314
+ if image_content:
315
+ content.append({
316
+ 'type': 'image',
317
+ 'image': image_content,
318
+ "min_pixels": self.min_pixels,
319
+ "max_pixels": self.max_pixels
320
+ })
321
+
322
+ # Process each text
323
+ for txt in texts:
324
+ content.append({'type': 'text', 'text': txt})
325
+
326
+ return conversation
327
+
328
+ # Preprocess input conversations for model consumption
329
+ def _preprocess_inputs(self, conversations: List[List[Dict]]) -> Dict[str, torch.Tensor]:
330
+ text = self.processor.apply_chat_template(
331
+ conversations, add_generation_prompt=True, tokenize=False
332
+ )
333
+
334
+ try:
335
+ images, video_inputs, video_kwargs = process_vision_info(
336
+ conversations, image_patch_size=16,
337
+ return_video_metadata=True, return_video_kwargs=True
338
+ )
339
+ except Exception as e:
340
+ logger.error(f"Error in processing vision info: {e}")
341
+ images = None
342
+ video_inputs = None
343
+ video_kwargs = {'do_sample_frames': False}
344
+ text = self.processor.apply_chat_template(
345
+ [{'role': 'user', 'content': [{'type': 'text', 'text': 'NULL'}]}],
346
+ add_generation_prompt=True, tokenize=False
347
+ )
348
+
349
+ if video_inputs is not None:
350
+ videos, video_metadata = zip(*video_inputs)
351
+ videos = list(videos)
352
+ video_metadata = list(video_metadata)
353
+ else:
354
+ videos, video_metadata = None, None
355
+
356
+ inputs = self.processor(
357
+ text=text, images=images, videos=videos, video_metadata=video_metadata, truncation=True,
358
+ max_length=self.max_length, padding=True, do_resize=False, return_tensors='pt',
359
+ **video_kwargs
360
+ )
361
+ return inputs
362
+
363
+ # Pool the last hidden state by attention mask for embeddings
364
+ @staticmethod
365
+ def _pooling_last(hidden_state: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor:
366
+ flipped_tensor = attention_mask.flip(dims=[1])
367
+ last_one_positions = flipped_tensor.argmax(dim=1)
368
+ col = attention_mask.shape[1] - last_one_positions - 1
369
+ row = torch.arange(hidden_state.shape[0], device=hidden_state.device)
370
+ return hidden_state[row, col]
371
+
372
+ # Process inputs to generate normalized embeddings
373
+ def process(self, inputs: List[Dict[str, Any]], normalize: bool = True) -> tuple:
374
+ conversations = [self.format_model_input(
375
+ text=ele.get('text'),
376
+ image=ele.get('image'),
377
+ video=ele.get('video'),
378
+ instruction=ele.get('instruction'),
379
+ fps=ele.get('fps'),
380
+ max_frames=ele.get('max_frames')
381
+ ) for ele in inputs]
382
+
383
+ processed_inputs = self._preprocess_inputs(conversations)
384
+ processed_inputs = {k: v.to(self.model.device) for k, v in processed_inputs.items()}
385
+
386
+ outputs = self.forward(processed_inputs)
387
+ embeddings = self._pooling_last(outputs['last_hidden_state'], outputs['attention_mask'])
388
+
389
+ # Normalize the embeddings if specified
390
+ if normalize:
391
+ embeddings = F.normalize(embeddings, p=2, dim=-1)
392
+
393
+ return embeddings
export_script/requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ torch>=2.4
2
+ onnx>=1.16
3
+ numpy>=1.26
4
+ transformers>=4.57
5
+ Pillow>=10.0
6
+ qwen-vl-utils>=0.0.11
export_script/run_all.sh ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ echo "Installing Dependencies"
2
+ pip install -r requirements.txt
3
+
4
+ echo "First Stage - a_export_to_onnx"
5
+ python export_script/a_export_to_onnx.py
6
+
7
+ echo "Second Stage - b_export_onnx_vision"
8
+ python export_script/b_export_onnx_vision
9
+
10
+ echo "Finished!"
rotary_params.npz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:54f94155bdc24961992f8098b355f1edd606aad4d533bd66a8aae3de3122a19d
3
+ size 1244663948
text_prompt_APOv2.1_BF16/fire_pred_prompts/fire/fire.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4b72141c31896aa7490c9fee6ed34eab0def685cd655011857967e2cf22dec70
3
+ size 9684
text_prompt_APOv2.1_BF16/fire_pred_prompts/normal/normal.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3437e019f4161d510c6ae29482056b5ffe830c7c31cc9a23e7ec53184fcd9429
3
+ size 9698
text_prompt_APOv2.1_BF16/smoke_pred_prompts/normal/normal.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8f52892acef497521ce6e647ddd8666c3627b80f4cd1f1796657c5f0c5d3cc1e
3
+ size 9698
text_prompt_APOv2.1_BF16/smoke_pred_prompts/smoke/smoke.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:696ca3f417de363201f5ee9f0332105e6e1e92b58694abdfea95f6f919ee382f
3
+ size 9691
tokenizer/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
+ }
tokenizer/chat_template.jinja ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {%- set default_system_message = 'Represent the user\'s input.' -%}
2
+ {%- if tools %}
3
+ {{- '<|im_start|>system\n' }}
4
+ {%- if messages[0].role == 'system' %}
5
+ {%- if messages[0].content is string %}
6
+ {{- messages[0].content }}
7
+ {%- else %}
8
+ {%- for content in messages[0].content %}
9
+ {%- if 'text' in content %}
10
+ {{- content.text }}
11
+ {%- endif %}
12
+ {%- endfor %}
13
+ {%- endif %}
14
+ {{- '\n\n' }}
15
+ {%- else %}
16
+ {{- default_system_message + '\n\n' }}
17
+ {%- endif %}
18
+ {{- "# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>" }}
19
+ {%- for tool in tools %}
20
+ {{- "\n" }}
21
+ {{- tool | tojson }}
22
+ {%- endfor %}
23
+ {{- "\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call><|im_end|>\n" }}
24
+ {%- else %}
25
+ {%- if messages[0].role == 'system' %}
26
+ {{- '<|im_start|>system\n' }}
27
+ {%- if messages[0].content is string %}
28
+ {{- messages[0].content }}
29
+ {%- else %}
30
+ {%- for content in messages[0].content %}
31
+ {%- if 'text' in content %}
32
+ {{- content.text }}
33
+ {%- endif %}
34
+ {%- endfor %}
35
+ {%- endif %}
36
+ {{- '<|im_end|>\n' }}
37
+ {%- else %}
38
+ {{- '<|im_start|>system\n' + default_system_message + '<|im_end|>\n' }}
39
+ {%- endif %}
40
+ {%- endif %}
41
+ {%- set image_count = namespace(value=0) %}
42
+ {%- set video_count = namespace(value=0) %}
43
+ {%- for message in messages %}
44
+ {%- if message.role == "user" %}
45
+ {{- '<|im_start|>' + message.role + '\n' }}
46
+ {%- if message.content is string %}
47
+ {{- message.content }}
48
+ {%- else %}
49
+ {%- for content in message.content %}
50
+ {%- if content.type == 'image' or 'image' in content or 'image_url' in content %}
51
+ {%- set image_count.value = image_count.value + 1 %}
52
+ {%- if add_vision_id %}Picture {{ image_count.value }}: {% endif -%}
53
+ <|vision_start|><|image_pad|><|vision_end|>
54
+ {%- elif content.type == 'video' or 'video' in content %}
55
+ {%- set video_count.value = video_count.value + 1 %}
56
+ {%- if add_vision_id %}Video {{ video_count.value }}: {% endif -%}
57
+ <|vision_start|><|video_pad|><|vision_end|>
58
+ {%- elif 'text' in content %}
59
+ {{- content.text }}
60
+ {%- endif %}
61
+ {%- endfor %}
62
+ {%- endif %}
63
+ {{- '<|im_end|>\n' }}
64
+ {%- elif message.role == "assistant" %}
65
+ {{- '<|im_start|>' + message.role + '\n' }}
66
+ {%- if message.content is string %}
67
+ {{- message.content }}
68
+ {%- else %}
69
+ {%- for content_item in message.content %}
70
+ {%- if 'text' in content_item %}
71
+ {{- content_item.text }}
72
+ {%- endif %}
73
+ {%- endfor %}
74
+ {%- endif %}
75
+ {%- if message.tool_calls %}
76
+ {%- for tool_call in message.tool_calls %}
77
+ {%- if (loop.first and message.content) or (not loop.first) %}
78
+ {{- '\n' }}
79
+ {%- endif %}
80
+ {%- if tool_call.function %}
81
+ {%- set tool_call = tool_call.function %}
82
+ {%- endif %}
83
+ {{- '<tool_call>\n{"name": "' }}
84
+ {{- tool_call.name }}
85
+ {{- '", "arguments": ' }}
86
+ {%- if tool_call.arguments is string %}
87
+ {{- tool_call.arguments }}
88
+ {%- else %}
89
+ {{- tool_call.arguments | tojson }}
90
+ {%- endif %}
91
+ {{- '}\n</tool_call>' }}
92
+ {%- endfor %}
93
+ {%- endif %}
94
+ {{- '<|im_end|>\n' }}
95
+ {%- elif message.role == "tool" %}
96
+ {%- if loop.first or (messages[loop.index0 - 1].role != "tool") %}
97
+ {{- '<|im_start|>user' }}
98
+ {%- endif %}
99
+ {{- '\n<tool_response>\n' }}
100
+ {%- if message.content is string %}
101
+ {{- message.content }}
102
+ {%- else %}
103
+ {%- for content in message.content %}
104
+ {%- if content.type == 'image' or 'image' in content or 'image_url' in content %}
105
+ {%- set image_count.value = image_count.value + 1 %}
106
+ {%- if add_vision_id %}Picture {{ image_count.value }}: {% endif -%}
107
+ <|vision_start|><|image_pad|><|vision_end|>
108
+ {%- elif content.type == 'video' or 'video' in content %}
109
+ {%- set video_count.value = video_count.value + 1 %}
110
+ {%- if add_vision_id %}Video {{ video_count.value }}: {% endif -%}
111
+ <|vision_start|><|video_pad|><|vision_end|>
112
+ {%- elif 'text' in content %}
113
+ {{- content.text }}
114
+ {%- endif %}
115
+ {%- endfor %}
116
+ {%- endif %}
117
+ {{- '\n</tool_response>' }}
118
+ {%- if loop.last or (messages[loop.index0 + 1].role != "tool") %}
119
+ {{- '<|im_end|>\n' }}
120
+ {%- endif %}
121
+ {%- endif %}
122
+ {%- endfor %}
123
+ {%- if add_generation_prompt %}
124
+ {{- '<|im_start|>assistant\n' }}
125
+ {%- endif %}
tokenizer/merges.txt ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer/preprocessor_config.json ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "crop_size": null,
3
+ "data_format": "channels_first",
4
+ "default_to_square": true,
5
+ "device": null,
6
+ "disable_grouping": null,
7
+ "do_center_crop": null,
8
+ "do_convert_rgb": true,
9
+ "do_normalize": true,
10
+ "do_pad": null,
11
+ "do_rescale": true,
12
+ "do_resize": true,
13
+ "image_mean": [
14
+ 0.5,
15
+ 0.5,
16
+ 0.5
17
+ ],
18
+ "image_processor_type": "Qwen2VLImageProcessorFast",
19
+ "image_std": [
20
+ 0.5,
21
+ 0.5,
22
+ 0.5
23
+ ],
24
+ "input_data_format": null,
25
+ "max_pixels": 1310720,
26
+ "merge_size": 2,
27
+ "min_pixels": 4096,
28
+ "pad_size": null,
29
+ "patch_size": 16,
30
+ "processor_class": "Qwen3VLProcessor",
31
+ "resample": 3,
32
+ "rescale_factor": 0.00392156862745098,
33
+ "return_tensors": null,
34
+ "size": {
35
+ "longest_edge": 1310720,
36
+ "shortest_edge": 4096
37
+ },
38
+ "temporal_patch_size": 2
39
+ }
tokenizer/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": "<|im_end|>",
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/tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:def76fb086971c7867b829c23a26261e38d9d74e02139253b38aeb9df8b4b50a
3
+ size 11423705
tokenizer/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": "<|im_end|>",
232
+ "errors": "replace",
233
+ "extra_special_tokens": {},
234
+ "model_max_length": 262144,
235
+ "pad_token": "<|endoftext|>",
236
+ "processor_class": "Qwen3VLProcessor",
237
+ "split_special_tokens": false,
238
+ "tokenizer_class": "Qwen2Tokenizer",
239
+ "unk_token": null
240
+ }
tokenizer/video_preprocessor_config.json ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "crop_size": null,
3
+ "data_format": "channels_first",
4
+ "default_to_square": true,
5
+ "device": null,
6
+ "do_center_crop": null,
7
+ "do_convert_rgb": true,
8
+ "do_normalize": true,
9
+ "do_rescale": true,
10
+ "do_resize": true,
11
+ "do_sample_frames": true,
12
+ "fps": 2,
13
+ "image_mean": [
14
+ 0.5,
15
+ 0.5,
16
+ 0.5
17
+ ],
18
+ "image_std": [
19
+ 0.5,
20
+ 0.5,
21
+ 0.5
22
+ ],
23
+ "input_data_format": null,
24
+ "max_frames": 768,
25
+ "merge_size": 2,
26
+ "min_frames": 4,
27
+ "num_frames": null,
28
+ "pad_size": null,
29
+ "patch_size": 16,
30
+ "processor_class": "Qwen3VLProcessor",
31
+ "resample": 3,
32
+ "rescale_factor": 0.00392156862745098,
33
+ "return_metadata": false,
34
+ "size": {
35
+ "longest_edge": 25165824,
36
+ "shortest_edge": 4096
37
+ },
38
+ "temporal_patch_size": 2,
39
+ "video_metadata": null,
40
+ "video_processor_type": "Qwen3VLVideoProcessor"
41
+ }
tokenizer/vocab.json ADDED
The diff for this file is too large to render. See raw diff