---
license: mit
pipeline_tag: image-feature-extraction
library_name: onnxruntime
base_model:
- moonshotai/MoonViT-SO-400M
tags:
- onnx
- vision-encoder
---
# MoonViT-SO-400M — ONNX
An ONNX export of **[moonshotai/MoonViT-SO-400M](https://huggingface.co/moonshotai/MoonViT-SO-400M)**,
the native-resolution vision encoder from Kimi-VL (initialized from and continually pre-trained on
SigLIP-SO-400M). Built with the standalone `MoonVit.py` script (`build` / `eval` / `inference`).
**Parity vs the original PyTorch model: cosine = 1.000000** (see `eval` below).
## What's in this folder
| File | Description |
|---|---|
| `moonvit.onnx` | The exported graph. Legacy export = single file (~1.7 GB); dynamo export = graph + `moonvit.onnx.data` (external weights). |
| `manifest.json` | Export metadata — grid, I/O shapes, normalization, which exporter was used. |
| `README.md` | This file. |
## Important: fixed-grid export
MoonViT packs patches NaViT-style and its internals branch on `grid_hws.tolist()` (per-image
Python loops for the interpolated position embedding, the 2D RoPE, and the 2×2 patch merger), so
the graph is **data-dependent on the image grid**. This export **bakes one fixed grid as a
constant**. The default is **28×28 patches = 392×392 px**; check `manifest.json` for the exact grid
this file was built with. For other input resolutions, export another variant
(`--grid-h/--grid-w`).
- **Input** `pixel_values`: `float32 [L, 3, 14, 14]` — `L = grid_h × grid_w` packed 14×14 patches,
raster order, normalized with mean/std = 0.5.
- **Output** `image_features`: `float32 [L/4, 4, 1152]` — merged tokens after the 2×2 patch merger
(e.g. 28×28 → `[196, 4, 1152]`).
## Usage (onnxruntime)
```python
import json, numpy as np, onnxruntime as ort
from PIL import Image
d = "." # this folder
man = json.load(open(f"{d}/manifest.json"))
gh, gw, P = man["grid_h"], man["grid_w"], man["patch_size"]
# preprocess an image to the export grid (raster-order packed patches, mean/std 0.5)
img = Image.open("figures/demo.png").convert("RGB").resize((gw * P, gh * P), Image.BICUBIC)
x = (np.asarray(img, np.float32) / 255.0 - 0.5) / 0.5 # [H,W,3]
x = x.transpose(2, 0, 1).reshape(3, gh, P, gw, P).transpose(1, 3, 0, 2, 4)
pixel_values = np.ascontiguousarray(x.reshape(gh * gw, 3, P, P))
sess = ort.InferenceSession(f"{d}/{man['file']}", providers=["CPUExecutionProvider"])
feats = sess.run(None, {"pixel_values": pixel_values})[0] # [gh*gw/4, 4, 1152]
print(feats.shape)
```
Or via the script: `uv run MoonVit.py inference --onnx-model-path --image img.png`.
## How it was built (`MoonVit.py`)
```bash
uv run MoonVit.py build --output MoonVitOnnx # legacy exporter (default)
uv run MoonVit.py build --output MoonVitOnnx --dynamo # new torch.onnx dynamo exporter
uv run MoonVit.py eval --onnx-model-path MoonVitOnnx # original PyTorch vs ONNX
uv run MoonVit.py inference --onnx-model-path MoonVitOnnx --image img.png
```
Both exporters are supported and produce **identical accuracy (cos 1.000000)**:
| Exporter | Graph | Weights | Notes |
|---|---|---|---|
| `legacy` (default) | ~5,050 nodes | inline (~1.7 GB single file) | unrolls the data-dependent loops against the baked grid |
| `dynamo` (`--dynamo`) | ~1,794 nodes | external `.onnx.data` | cleaner/smaller graph; requires `onnxscript` |
Export notes (handled automatically by the script; both verified bit-faithful before export):
1. **Complex RoPE → real cos/sin.** ONNX has no complex dtype; the 2D-RoPE `freqs_cis` is
precomputed for the baked grid and applied as real-valued rotation.
2. **`PytorchGELUTanh` shim** — the upstream remote code imports it from `transformers.activations`
(removed in newer transformers); it's exactly `nn.GELU(approximate="tanh")`.
3. **Dynamo only:** full-attention SDPA (single baked image ⇒ the block-diagonal mask is all-True)
and concrete-int patch-merger / pos-emb interpolation, to avoid unbacked-symint ops.
4. **Legacy only:** int32→int64 normalization of `Slice`/`Gather` index tensors.
## Eval result
```
grid=28x28 original PyTorch vs ONNX
sample 0: cos=1.000000 max|Δ|=9.995e-02
sample 1: cos=1.000000 max|Δ|=6.195e-03
sample 2: cos=1.000000 max|Δ|=2.052e-03
=== PASS (worst cosine 1.000000, tol 0.999) ===
```
(`max|Δ|` is on large-magnitude features, feature std ≈ 4.2 — cosine 1.0 is the real signal.)
## Original model reference (PyTorch)
```python
from PIL import Image
from transformers import AutoModel, AutoImageProcessor
model_path = "moonshotai/MoonViT-SO-400M"
model = AutoModel.from_pretrained(model_path, torch_dtype="auto", device_map="auto",
trust_remote_code=True)
processor = AutoImageProcessor.from_pretrained(model_path, trust_remote_code=True)
image = Image.open("./figures/demo.png")
proc = processor(image, return_tensors="pt").to(dtype=model.dtype, device=model.device)
image_features: list = model(proc.pixel_values, proc.image_grid_hws)
print(image_features[0].dtype, image_features[0].shape) # e.g. bf16, [N, 4, 1152]
```
See the [Kimi-VL Technical Report](https://huggingface.co/papers/2504.07491) for training details.