Upload folder using huggingface_hub
Browse files- .DS_Store +0 -0
- .gitattributes +3 -0
- README.md +88 -0
- Transformer.onnx +3 -0
- Transformer.onnx.data +3 -0
- Vision.onnx +3 -0
- Vision.onnx.data +3 -0
- export_script/a_export_to_onnx.py +727 -0
- export_script/b_export_onnx_vision.py +212 -0
- export_script/qwen3_vl_embedding.py +393 -0
- export_script/requirements.txt +6 -0
- export_script/run_all.sh +10 -0
- rotary_params.npz +3 -0
- text_prompt_APOv2.1_BF16/fire_pred_prompts/fire/fire.pt +3 -0
- text_prompt_APOv2.1_BF16/fire_pred_prompts/normal/normal.pt +3 -0
- text_prompt_APOv2.1_BF16/smoke_pred_prompts/normal/normal.pt +3 -0
- text_prompt_APOv2.1_BF16/smoke_pred_prompts/smoke/smoke.pt +3 -0
- tokenizer/added_tokens.json +28 -0
- tokenizer/chat_template.jinja +125 -0
- tokenizer/merges.txt +0 -0
- tokenizer/preprocessor_config.json +39 -0
- tokenizer/special_tokens_map.json +31 -0
- tokenizer/tokenizer.json +3 -0
- tokenizer/tokenizer_config.json +240 -0
- tokenizer/video_preprocessor_config.json +41 -0
- tokenizer/vocab.json +0 -0
.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
|
|
|