Upload README.md with huggingface_hub
Browse files
README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
tags:
|
| 4 |
+
- executorch
|
| 5 |
+
- xnnpack
|
| 6 |
+
- pte
|
| 7 |
+
- on-device
|
| 8 |
+
- image-text-to-text
|
| 9 |
+
base_model:
|
| 10 |
+
- HuggingFaceTB/SmolVLM2-256M-Video-Instruct
|
| 11 |
+
---
|
| 12 |
+
# SmolVLM2-256M — ExecuTorch
|
| 13 |
+
|
| 14 |
+
`smolvlm2_256m_video_instruct_xnnpack_8da8w.pte` (348 MB)
|
| 15 |
+
|
| 16 |
+
- **Source**: HuggingFaceTB/SmolVLM2-256M-Video-Instruct — a SigLIP vision tower (768
|
| 17 |
+
wide, 12 layers) and a Llama decoder (576 wide, 30 layers)
|
| 18 |
+
- **License**: Apache-2.0
|
| 19 |
+
- **Input**: a 512×512 picture as `[1, 3, 512, 512]`, and token ids for the words around it
|
| 20 |
+
- **Output**: logits over the 49,280-token vocabulary
|
| 21 |
+
|
| 22 |
+
One file, three entry points — the shape ExecuTorch's multimodal runner asks for:
|
| 23 |
+
|
| 24 |
+
| method | in | out |
|
| 25 |
+
|---|---|---|
|
| 26 |
+
| `vision_encoder` | `[1, 3, 512, 512]` | `[1, 64, 576]` rows in the decoder's embedding space |
|
| 27 |
+
| `token_embeddings` | token ids | embeddings |
|
| 28 |
+
| `text_model` | embeddings, positions | logits, and its own cache |
|
| 29 |
+
|
| 30 |
+
A decoder that only takes token ids cannot be told about a picture. Splitting a
|
| 31 |
+
vision-language model into a vision `.pte` and a text `.pte` runs aground there;
|
| 32 |
+
`MultimodalPrefiller::load` asks one module for `token_embeddings` and `text_model` and uses
|
| 33 |
+
`vision_encoder` if it finds it.
|
| 34 |
+
|
| 35 |
+
`vision_encoder` here takes a picture rather than a patch sequence, which is what that
|
| 36 |
+
runner hands it — so this file can be driven by the runner as well as method by method.
|
| 37 |
+
|
| 38 |
+
## Verification (Mac arm64, 2026-08-21)
|
| 39 |
+
|
| 40 |
+
| check | result |
|
| 41 |
+
|---|---|
|
| 42 |
+
| vision half vs the untouched tower, 24 photographs | worst **corr 0.99871** |
|
| 43 |
+
| first-step logits vs eager | **corr 0.99197**, same top-1 |
|
| 44 |
+
| greedy tokens vs eager, 12 steps | **1/12** |
|
| 45 |
+
|
| 46 |
+
The wrappers themselves are exact: run in eager without quantization they agree with the
|
| 47 |
+
model at corr 1.000000, so what these numbers measure is the weights, not the wiring.
|
| 48 |
+
|
| 49 |
+
## Why the decoder is int8 and not int4
|
| 50 |
+
|
| 51 |
+
The vision tower is int8 per output channel and the decoder is int8 over groups of 32.
|
| 52 |
+
int4 on a decoder this size does not hold:
|
| 53 |
+
|
| 54 |
+
| | int4 decoder | int8 decoder |
|
| 55 |
+
|---|---|---|
|
| 56 |
+
| size | 289 MB | 348 MB |
|
| 57 |
+
| first-step logits vs eager | corr 0.83175 | corr 0.99197 |
|
| 58 |
+
| greedy tokens vs eager | 0/12 | 1/12 |
|
| 59 |
+
|
| 60 |
+
At int4 it does not even pick the same first word, which is why only the int8 build is here. 59 MB is not worth that.
|
| 61 |
+
|
| 62 |
+
## Ask for one tile
|
| 63 |
+
|
| 64 |
+
The processor cuts a picture into tiles by aspect ratio and by size, and a 512×512 photograph
|
| 65 |
+
becomes **seventeen** of them — 1088 image tokens, against the 64 this graph produces and the
|
| 66 |
+
512 of context it was built with. `do_image_splitting = False` on the processor (or its image
|
| 67 |
+
processor) gives the one tile that matches.
|
| 68 |
+
|
| 69 |
+
```bash
|
| 70 |
+
VL_CKPT=HuggingFaceTB/SmolVLM2-256M-Video-Instruct \
|
| 71 |
+
VL_PTE=smolvlm2_256m_video_instruct_xnnpack_8da8w.pte \
|
| 72 |
+
python convert/run_vl.py <image> "What is in this picture?"
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
The driver builds the prompt with the model's own processor and writes the picture's rows
|
| 76 |
+
wherever the processor put an image token, which is what the model does internally.
|
| 77 |
+
|
| 78 |
+
## It invents text on signs
|
| 79 |
+
|
| 80 |
+
Asked about a London street, this model answers with a shop name that is not there. That is
|
| 81 |
+
the model and not the conversion: eager, unquantized, invents a different one from the same
|
| 82 |
+
photograph. If reading signs matters, LFM2.5-VL reads them
|
| 83 |
+
([450M](https://huggingface.co/mlboydaisuke/LFM2.5-VL-450M-ExecuTorch)).
|
| 84 |
+
|
| 85 |
+
## Conversion
|
| 86 |
+
|
| 87 |
+
`convert/export_vl_bundle.py`. Two things needed re-authoring:
|
| 88 |
+
|
| 89 |
+
- **The position embeddings are chosen with `torch.bucketize`**, so that a picture filling
|
| 90 |
+
part of the grid still lands on the right ones. There is no `bucketize` kernel in the
|
| 91 |
+
runtime, and a full square grid has one answer anyway: patch *i* takes position *i*. The
|
| 92 |
+
export checks that against the model's own code rather than assuming it.
|
| 93 |
+
- **The cache has to live inside `text_model`.** ExecuTorch copies a mutable buffer into
|
| 94 |
+
each method that names it (`Program::load_mutable_subsegment_into` writes into the
|
| 95 |
+
method's own memory), so a prefill method and a decode method would each get their own and
|
| 96 |
+
neither would see the other's writes. One graph serves both. `StaticCache` does not
|
| 97 |
+
survive lowering, and neither does a cache held by any object outside the module tree —
|
| 98 |
+
its tensors get lifted a second time as constants, and `run_decompositions` then returns a
|
| 99 |
+
function where a GraphModule was expected.
|
| 100 |
+
|
| 101 |
+
## The number that decides whether it runs on a phone
|
| 102 |
+
|
| 103 |
+
`CONTEXT`, the upper bound on the dynamic sequence dimension. The memory planner sizes its
|
| 104 |
+
arena for the bound, not for what a picture costs: at 4096 that arena runs to gigabytes and
|
| 105 |
+
iOS kills the process with signal 9 before the first method has finished loading. One
|
| 106 |
+
picture is 64 rows here, so 512 leaves room for a long question and a long answer.
|
| 107 |
+
|
| 108 |
+
(conversion scripts: [executorch-models](https://github.com/john-rocky/executorch-models) ·
|
| 109 |
+
iOS sample: [executorch-samples](https://github.com/john-rocky/executorch-samples))
|