File size: 7,518 Bytes
9c20c13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fcdc94e
 
 
9c20c13
fcdc94e
 
 
 
 
 
 
f6648f9
fcdc94e
 
 
 
 
 
 
 
 
f6648f9
 
9c20c13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14a8606
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
---
license: other
license_name: lfm1.0
license_link: LICENSE
base_model: LiquidAI/LFM2.5-VL-450M
tags:
  - coreai
  - aimodel
  - apple-silicon
  - on-device
  - lfm2
  - vision-language
  - siglip2
pipeline_tag: image-text-to-text
---

# LFM2.5-VL-450M β€” Apple Core AI (`.aimodel`)

**LiquidAI's LFM2.5-VL-450M converted to Apple's Core AI** (the Core ML successor announced at
WWDC26), ready to run on iOS 27 / macOS 27. Image + text β†’ text in **658 MB** β€” small enough to
sit inside an app rather than be one.

Two bundles, run in sequence: a **SigLIP2-NaFlex vision tower + projector** (`patches [1024,768]
β†’ image_embeds [256,1024]`) and the **LFM2 conv+attention hybrid decoder** β€” the same decoder as
[LFM2.5-1.2B](https://huggingface.co/mlboydaisuke/LFM2.5-1.2B-CoreAI), reached in this checkpoint
by a `model.language_model.` key prefix β€” with the image tokens spliced in through a static
`image_embeds` input. Hidden 1024, 16 layers = 10 short-conv + 6 GQA attention, vocab 65 536,
tied head. No recurrent scan, so decode is loop-free and rides Apple's `coreai-pipelined` GPU
engine with no custom kernels.

Not a thinking model (unlike the 2.6B): the generation prompt does not open `<think>`.

> Requires the iOS 27 / macOS 27 beta (Core AI ships with the OS). Conversion code, gates and
> knowledge base: **[coreai-model-zoo](https://github.com/john-rocky/coreai-model-zoo)**.

## Bundles

| path | size | measured (M4 Max) | numerics |
|---|---:|---|---|
| `gpu-pipelined/lfm2_5_vl_450m_vision_fp16` | 181 MB | **18.0 ms**/image | `image_embeds` cos **0.999996** vs fp32 HF |
| `gpu-pipelined/lfm2_5_vl_450m_decode_int8lin` | 477 MB | β€” | 7/9 suite cases token-exact (fp16 baseline: 8/9) |
| `gpu-pipelined/lfm2_5_vl_450m_decode_int8lin_textcore` | 477 MB | **609.2 prompt / 387.2 decode tok/s** | oracle gate **PASS 16/16** |

M4 Max, macOS 27.0 (26A5378n), Xcode 27.0 (27A5218g), `coreai-torch 0.4.1`,
`llm-benchmark -p 128 -g 256 -n 3`, `COREAI_CHUNK_THRESHOLD=1`.

The Mac tok/s row is the **text core** β€” the same decoder weights exported with no image input β€”
because `llm-runner` has no way to bind the VLM bundle's `image_embeds` buffer. The text core is
also a usable 350M LFM2 text model on its own.

### iPhone 17 Pro (AOT h18p, `ios-h18p/`, settled)

| bundle | prefill | decode | numerics |
|---|---:|---:|---|
| **`decode_int8lin`, image bound** | **123.2** | **112.0** | nat 16/16 + image oracle 24/24 |
| `decode_int8lin_textcore` | 122.1 | 110.6 | nat 16/16 + oracle 16/16 |
| `decode_int8lin`, g=1024 | 122.4 | 108.6 | no collapse |
| **`vision_fp16`** | β€” | **33.6 ms**/image | cos 0.999995 vs the same tower on Mac |

Binding the 256Γ—1024 fp16 image buffer costs nothing per step β€” the VLM bundle and the text core
measure the same speed within noise. Engine ready in 0.5 s warm.

On device the image path describes the same picture as fp32 but is not token-identical: it drops
one adjective at a near-tie (*"two tabby cats … stretched out on its side"* β†’ *"two cats … lying
on its side"*), with the tokens between the two forks identical. That is the fp16 near-tie class,
not an image-path error.

The tower's **first** encode pays ~860 ms of on-device compile; warm it with a dummy encode at
load and the user's first photo gets the 33.6 ms number instead.

## What it is good at, and what it is not

A 450M VLM answers scene-level questions β€” what is in the picture, where it is, which colours
dominate β€” and misses fine-grained geometry. That is the checkpoint, not the conversion: the
same weights on other on-device runtimes show the same split. Treat it as a caption / triage
model that fits beside an application.

The bundle bakes **one 512Γ—512 patch grid** (32Γ—32 patches β†’ 2Γ— unshuffle β†’ 256 tokens, which is
exactly the checkpoint's own `max_image_tokens`). The upstream model is NaFlex β€” it picks a grid
per image and keeps the aspect ratio β€” so a non-square image is stretched here. That is the
price of a fixed graph, and it is the one thing to weigh before choosing this over the source
model.

`int4` is **not published**: 0 of 9 gate cases token-exact, and the failure mode is fluent drift
rather than obvious breakage β€” a kitchen becomes "a traditional *Italian* kitchen" where fp32
says "historical or rustic". Read generations, not loss curves, before trusting int4 on a model
this small.

## Run it

```bash
git clone https://github.com/apple/coreai-models   # + the zoo's engine patches, see below
swift build -c release --product llm-runner

# the text core (no image), to check the decoder end of the pair
COREAI_CHUNK_THRESHOLD=1 .build/release/llm-runner \
  --model gpu-pipelined/lfm2_5_vl_450m_decode_int8lin_textcore \
  --prompt "The alphabet begins A, B, C," \
  --max-tokens 64 --sampling-strategy greedy \
  --inference-engine-variant coreai-pipelined --warmup off
```

`--warmup off` matters: default warmup submits a synthetic 256-token prefill and these bundles
are static-S=1. The engine patches (`coreai-pipelined-extra-states` for the conv state,
`coreai-pipelined-static-inputs` for `image_embeds`) are in the zoo under `apps/`.

For the image path, the host does three things: resize to 512Γ—512 with an **antialiased**
bilinear filter (PIL/torchvision `antialias=True`, *not* a 2Γ—2 GPU bilinear tap), normalize
`(x/255 βˆ’ 0.5)/0.5`, and patchify into 16Γ—16 patches with the **channel as the fastest axis**
(`[y][x][c]`). Then run the vision bundle, bind its output as `image_embeds`, and rewrite the
prompt's `<image>` ids (id 396) to `V + slot`. The reference implementation is
[`_smoke/lfm25vl_preprocess.py`](https://github.com/john-rocky/coreai-model-zoo/blob/main/_smoke/lfm25vl_preprocess.py).

## Converting this family yourself

The trap that costs a day: **build the oracle on transformers β‰₯ 5**. transformers 4.57.6 applies
the projector's LayerNorm unconditionally, while this config sets `projector_use_layernorm:
false` and ships no such weights β€” and `nn.LayerNorm`'s default init (weight 1, bias 0) means no
warning and no visible garbage, just a quietly different reference that would certify a wrong
port as PASS.

Two more, both readable straight off the weight shapes: `patch_embedding.weight` is `[768, 768]`
β€” a **Linear over pre-flattened patches**, not a Conv2d over an image β€” and
`position_embedding.weight` is `[256, 768]`, a 16Γ—16 grid that is **bilinearly resized (with
antialias) to the actual patch grid**. A port written from a MiniCPM-V or Qwen-VL SigLIP recipe
gets both wrong and still produces fluent text.

Everything is in
[`conversion/export_lfm25vl_pipelined.py`](https://github.com/john-rocky/coreai-model-zoo/blob/main/conversion/export_lfm25vl_pipelined.py)
and [`knowledge/lfm2.5-vl-port.md`](https://github.com/john-rocky/coreai-model-zoo/blob/main/knowledge/lfm2.5-vl-port.md).

## License

LFM Open License v1.0, carried from
[`LiquidAI/LFM2.5-VL-450M`](https://huggingface.co/LiquidAI/LFM2.5-VL-450M) (revision
`fc6221ca597f3315e4f82fc2df606783267b34ba`). Not affiliated with Apple or LiquidAI.

<!-- funnel:v1 -->

---

**More models in this format:** [Core AI Model Zoo](https://huggingface.co/collections/mlboydaisuke/core-ai-model-zoo-6a7ff330f753e8dcae04671a) β€” 75 models, each with the recipe that produced it.

**Want a different model on-device?** [Open a request](https://github.com/john-rocky/on-device-requests) β€” free, open weights only; the export and its measured numbers get published publicly.

<!-- /funnel:v1 -->