mlboydaisuke commited on
Commit
c5ef87d
·
verified ·
1 Parent(s): 4924cff

Upload README.md with huggingface_hub

Browse files
Files changed (1) hide show
  1. README.md +109 -0
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))