NagaYu's picture
Model card: macOS 27.2 re-measurement, NagaYu links, hf download, Space not provided
9831eca verified
|
Raw History Blame Contribute Delete
13 kB
---
license: apache-2.0
base_model: HuggingFaceTB/SmolVLM-256M-Instruct
library_name: coreml
pipeline_tag: image-text-to-text
language:
- en
tags:
- coreml
- core-ml
- apple-silicon
- on-device
- vlm
- smolvlm
- image-to-text
---
# SmolVLM-256M β€” Core ML
Core ML (`.mlpackage`) builds of [HuggingFaceTB/SmolVLM-256M-Instruct](https://huggingface.co/HuggingFaceTB/SmolVLM-256M-Instruct),
converted for on-device image captioning and VQA on Apple silicon.
> **Two of the four compute-unit settings load successfully and return garbage.** They do not
> crash, so nothing tells you they are wrong. Use `.all` or `.cpuAndGPU`, and compile the
> package before loading it. Details below.
Captions an image in **~2 s of model time on macOS 27.2** (~1.3 s on macOS 26.6.2), entirely on device. The companion Swift CLI
that drives these packages has **no Python and no third-party runtime dependencies**, and
its preprocessing is bit-exact with the HuggingFace reference β€” a libjpeg-compatible JPEG
decoder and a Pillow-exact LANCZOS resampler, both reimplemented in Swift and re-verified
against golden data on every run.
```
image ──► vision (fp16) ──► prefill (K/V as plain outputs)
β”‚
text ──► BPE tokenizer ─────────┴─► decode (S=1, MLState) ──► tokens
```
## ⚠️ Compute units: pick `.all` or `.cpuAndGPU`, and compile first
Measured on macOS 26.5 / coremltools 9.0, loading the **precompiled `.mlmodelc`** from a
Swift app:
| setting | loads | correct | notes |
|---|---|---|---|
| `.all` | yes | **yes** | fastest measured β€” vision ~87 ms, decode ~44 tok/s |
| `.cpuAndGPU` | yes | **yes** | vision ~167 ms, decode ~43 tok/s |
| `.cpuAndNeuralEngine` | yes | **no** | emits `<end_of_utterance>` from the first token |
| `.cpuOnly` | yes | **no** | caption collapses to `This Is That Is.` |
Correctness needs the GPU in the mix. The two GPU-less settings **do not crash** β€” they
silently return wrong output, which is the harder failure to notice.
Separately, loading the `.mlpackage` **from Python** with `compute_units=ALL`
(`ct.models.MLModel(...)`) still crashes with SIGSEGV on this environment β€” re-confirmed on
macOS 27.2 with the real `smolvlm_decode_hybrid.mlpackage`, where it dies during the load. Compile to
`.mlmodelc` first, or stay on `.cpuAndGPU` in Python. On macOS 27.2 the CLI's `auto` probe still
selects `.all`; `.cpuAndNeuralEngine` and `.cpuOnly` were not re-tested there.
Because "it loaded" is not evidence of correctness, the companion CLI defaults to
`--compute-units auto`: it probes each candidate in a **subprocess** (these failures are
process-killing signals, not catchable errors) and accepts one only if its output token
matches `.cpuAndGPU`. The result is cached per OS build, costing about 0.7 s once.
## Companion repos
- **Verify with the dataset β€” [NagaYu/smolvlm-coreml-golden](https://huggingface.co/datasets/NagaYu/smolvlm-coreml-golden)** (~43 MB). It holds the golden token ids, pixel tensors, raw PIL decode and test image that `smolvlm --check --root .` compares against on every run. Download it next to these packages if you want the bit-exactness claims on this card proven on your machine rather than taken on faith.
- **Compare against the Space β€” `smolvlm-coreml-oracle` (currently not provided).** The Space is not published at the moment, so there is no link; its source is in [`space/`](https://github.com/NagaYu/smolvlm-coreml/tree/main/space) of the companion repo. It runs the original SmolVLM-256M in **PyTorch on CPU**; Hugging Face Spaces are Linux, so Core ML cannot execute there and this Space is deliberately *not* a demo of these packages. It is the OS-independent reference oracle β€” it emits the reference artifacts you diff your on-device Core ML run against, and its output does not move when your Mac's OS moves.
> **Arrived here after an Apple OS update?** Read [docs/os-upgrade.md](https://github.com/NagaYu/smolvlm-coreml/blob/main/docs/os-upgrade.md) first β€” these packages still load (built with `minimum_deployment_target = iOS18`, specificationVersion 9 = iOS18/macOS15), but the goldens and the compute-unit verdicts are stamped to macOS 26.5 / coremltools 9.0 (the timings below were re-measured on macOS 27.2), and that guide is the checklist for re-proving them.
## Files
| File | Role |
|---|---|
| `smolvlm_vision.mlpackage` | Vision encoder + connector (fp16) |
| `smolvlm_prefill_kv.mlpackage` | Batch prefill, P=128, K/V returned as plain outputs (fp16) |
| `smolvlm_decode_hybrid.mlpackage` | S=1 decode, state only for generated tokens (fp16) |
| `smolvlm_prefill_kv_1024.mlpackage` | Prefill for the 13-tile hi-res split path, P=1024 |
| `smolvlm_decode_hybrid_1024.mlpackage` | Decode for the split path |
| `*_int8.mlpackage` | INT8 per-channel **asymmetric** variants of the prefill/decode packages β€” roughly half the size, see the caveat below |
There is deliberately **no INT8 vision package**: quantizing the vision tower drops cosine
similarity against the torch reference from 1.00000 to 0.99407, which is enough to change the
caption wording (still accurate, but no longer matching the HuggingFace output). Vision stays fp16.
All packages were converted with **coremltools 9.0**. Other versions may lower the graph
differently; regenerate with `convert_all.py` if you need a different toolchain.
## Usage
The Swift CLI, the conversion scripts, and the bit-exactness verification all live in the
companion repository: **[NagaYu/smolvlm-coreml](https://github.com/NagaYu/smolvlm-coreml)**.
The CLI resolves both the `.mlpackage` files **and** `exports/` from a single `--root`
directory, so download the packages and runtime assets **into the cloned repo**. This repo
mirrors the clone's layout, so `--local-dir .` puts every file where `--root .` looks for it:
```bash
git clone https://github.com/NagaYu/smolvlm-coreml && cd smolvlm-coreml
# download only the minimal fp16 set + runtime assets, into the repo root
hf download NagaYu/SmolVLM-256M-Instruct-CoreML --local-dir . \
--include "smolvlm_vision.mlpackage/*" \
--include "smolvlm_prefill_kv.mlpackage/*" \
--include "smolvlm_decode_hybrid.mlpackage/*" \
--include "exports/embeddings.bin" --include "exports/tok/*" --include "exports/specials.json"
cd SmolVLMSwift && swift build -c release && cd ..
./SmolVLMSwift/.build/release/smolvlm --image test_image.jpg --root .
./SmolVLMSwift/.build/release/smolvlm --image test_image.jpg --question "How many cats?" --root .
./SmolVLMSwift/.build/release/smolvlm --image test_image.jpg --split --root . # 13-tile hi-res
./SmolVLMSwift/.build/release/smolvlm --check --root . # re-prove bit-exactness
```
Add `--include "*_1024*"` for the hi-res split path, or `--include "*_int8*"` for the
INT8 variants. The non-split goldens for `--check` are committed in the GitHub repo; the
split-path pixel golden is in the dataset repo. You can also regenerate every package
yourself with `python convert_all.py && python export_artifacts.py` instead of downloading them.
## Measured performance
Apple M2 MacBook Pro, macOS 27.2 (build 26B123), fp16, warm process, median of 10 runs after
2 warmups. The CLI default (`--compute-units auto`) still selects `.all` here; `.cpuAndGPU`
is shown as the conservative fallback. The `--split` column was not re-measured on 27.2 and
is the earlier macOS 26.6.2 measurement:
| Stage | `.all` (auto-selected) | `.cpuAndGPU` | `--split`, 13 tiles (P=1024, `.cpuAndGPU`) β€” 26.6.2 |
|---|---|---|---|
| Model load (warm) | 0.25 s | 0.2 s | 0.3 s |
| Vision | ~90 ms (1 tile) | ~168 ms | ~1.3 s (13 tiles) |
| Prefill + first logits | ~950 ms (81 tok) | ~960 ms | ~300 ms (876 tok) |
| Decode | ~41 tok/s | ~41 tok/s | ~34 tok/s |
| 40-token caption, model time | ~2.0 s | ~2.1 s | β€” |
On macOS 26.6.2 the same packages measured vision ~87 ms / ~167 ms, prefill + first logits
~200 ms / ~180 ms and decode ~43 / ~40 tok/s (`.all` / `.cpuAndGPU`), for a ~1.3 s caption.
The 27.2 prefill slowdown reproduces with both the original compile and a fresh 27.2 compile,
so it comes from the OS runtime, not the packages. Decode rate varies by a few tok/s between
sessions. The first run in a fresh process also pays a cold-start penalty on vision and
prefill; the numbers above are warm.
## Why the architecture looks like this
The obvious design β€” a multifunction package where `prefill` and `decode` share `MLState`,
as the WWDC-era examples suggest β€” does not survive on this runtime. It crashes in the
decode loop under every compute-unit combination tested, including `CPU_ONLY Γ— CPU_ONLY`;
in the run that was instrumented step-by-step, the first cross-function predict succeeded
before the loop died. The same goes for `write_state` (appears to succeed, round-trips
cleanly through `read_state`, then dies dozens of predicts later) and for passing one
model's state into another model's `predict` (immediate crash).
So state never crosses a boundary here. **Prefill is stateless** and returns all 30 layers'
K/V as ordinary outputs (`[1, 90, P, 64]`); **decode takes them back as ordinary inputs**
and keeps `MLState` only for the tokens it generates itself. That design ported to Swift
without a single crash.
The full catalog of seven reproducible crashes, with the reasoning and repro steps:
**[the field report](https://github.com/NagaYu/smolvlm-coreml/blob/main/docs/field-report.md)**.
## Verified parity
The companion CLI's `--check` mode re-proves the tokenizer, JPEG decode and resize rows on
every run (re-confirmed on macOS 27.2 / Swift 6.4: 81/81, 876/876, 0/255, maxdiff 0.000000).
It does not run Core ML; the normalization, prefill and caption rows were measured against
the Python pipeline separately.
| Component | Match level |
|---|---|
| Tokenizer | Exact β€” 81/81 tokens non-split, 876/876 split |
| JPEG decode | Bit-exact vs libjpeg, maxdiff 0/255 |
| Resize | Bit-exact vs Pillow LANCZOS (down to its fixed-point arithmetic) |
| Normalization | Bit-exact, all 256 values identical in fp16 |
| Prefill embeddings / K/V | Bitwise identical to the Python Core ML pipeline |
| Caption output | Character-identical to the Python Core ML pipeline, deterministic across runs |
The non-split caption also matches HuggingFace `transformers` greedy output token-for-token.
## Limitations β€” please read
- **Environment-stamped.** The packages were built and the goldens exported on macOS 26.5 /
coremltools 9.0 / Apple silicon; the timings, the `--check` results and the Python `ALL`
crash were re-measured on macOS 27.2 (Apple M2, Swift 6.4) without rebuilding anything. A
later OS may fix the crashes described here β€” or quietly change the numerics or the
speed. All three have been observed.
- **Greedy output drifts across OS sessions.** With bit-identical inputs and artifacts,
greedy decode produced different captions across OS sessions days apart, while remaining
fully deterministic within a session. Likely last-ulp changes in GPU kernels between
system updates β€” probably not a bug, but it means **never use a saved caption string as a
regression baseline across OS updates.** Re-derive goldens against the current OS.
- **INT8 costs ~12 s of GPU shader compilation per process.** Core ML does not cache it
across launches, so you trade a cold-start tax on every run for roughly half the size.
Worth it for a long-lived process, not for a CLI β€” which is why fp16 is the default.
- **Split-path captions can differ from a torch-fp32-vision HuggingFace run in late
tokens.** This is inherent to running the vision tower in Core ML fp16, not a porting
bug: the *Python* Core ML pipeline diverges from torch-fp32 in exactly the same place.
- **This is a reference implementation for one model, not a general library.** Dimensions
(576 hidden, 30 layers) are hardcoded where it keeps the code readable.
- The bundled JPEG decoder covers baseline 4:4:4 and grayscale β€” what libjpeg parity was
verified against. Other JPEGs fall back to CoreGraphics (Β±4/255 decode variance).
## Licenses & attribution
- **Weights**: derivative works of [HuggingFaceTB/SmolVLM-256M-Instruct](https://huggingface.co/HuggingFaceTB/SmolVLM-256M-Instruct),
Apache 2.0. All credit for the model itself goes to the SmolVLM authors at HuggingFaceTB.
- **Conversion scripts and Swift CLI**: MIT, in the [companion repository](https://github.com/NagaYu/smolvlm-coreml).
## Citation
If the crash catalog or the stateless-prefill design is useful in your own work, a link to
the [field report](https://github.com/NagaYu/smolvlm-coreml/blob/main/docs/field-report.md)
or the [repository](https://github.com/NagaYu/smolvlm-coreml) is appreciated. Reports of
these crashes reproducing β€” or *not* reproducing β€” on other OS versions are especially
welcome; the more independent reproductions exist, the faster they get looked at.