File size: 8,798 Bytes
1c87874
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
---
license: other
license_name: lfm1.0
license_link: LICENSE
base_model: LiquidAI/LFM2.5-230M
tags:
  - coreai
  - aimodel
  - apple-silicon
  - on-device
  - neural-engine
  - lfm2
  - hybrid
  - tool-calling
  - function-calling
pipeline_tag: text-generation
---

# Uncle Rudy β€” LFM2.5-230M tool-caller β€” Apple Core AI (`.aimodel`)

A **LoRA fine-tune of [LiquidAI/LFM2.5-230M](https://huggingface.co/LiquidAI/LFM2.5-230M)** that turns
a single spoken/typed utterance into a **JSON tool call** for a to-do app β€” converted to Apple's
**Core AI** `.aimodel` format and running on the **Neural Engine** (iOS 27 / macOS 27).

> [!IMPORTANT]
> This is a **static-shape / Neural Engine** bundle (4 entrypoints: `load_embeddings`,
> `gather_embeddings`, `extend_*`, `prompt_opt_*`) β†’ Apple's `EngineFactory` selects the
> **`static-shape`** engine. It is **not** a `gpu-pipelined` decode bundle, so it will **not**
> load on the `coreai-pipelined` GPU path that most published Core AI chat bundles use.
> It also needs a small runtime patch β€” see [Runtime requirements](#runtime-requirements).

## What it does

Give it one utterance; it emits one tool call.

```
"Remind me to buy milk"                  β†’ {"name":"create_todo","arguments":{"title":"Buy milk","due":null}}
"Remind me to call the dentist tomorrow" β†’ {"name":"create_todo","arguments":{"title":"Call the dentist","due":"tomorrow"}}
"Delete the dentist task"                β†’ {"name":"delete_todo","arguments":{"target":"Call the dentist"}}
```

Tool surface: `create_todo` (`title`, `due`) Β· `set_status` (`target`, `status`) Β·
`update_todo` (`target`, `title`, `due`) Β· `delete_todo` (`target`).

Apply the bundle's own chat template and feed the raw utterance as the user turn β€” the fine-tune
emits the assistant tool call directly. **No system prompt / tool-schema preamble is needed**
(training used `mask_prompt`, so loss was computed only on the tool-call tokens). Decode **greedily**.

## Bundle

```
ane-static/uncle_rudy_lfm2_230m_toolcaller_int4pal/
β”œβ”€β”€ uncle_rudy_lfm2_230m_toolcaller_int4pal.aimodel/   # main.mlirb Β· main.hash Β· metadata.json
β”œβ”€β”€ tokenizer/                                          # tokenizer.json Β· tokenizer_config.json
β”‚                                                       # chat_template.jinja Β· generation_config.json
└── metadata.json                                       # LanguageBundle manifest
```

| | |
|---|---|
| Base | LiquidAI/LFM2.5-230M (14 layers = 8 short-conv + 6 GQA attention, hidden 1024, 16/8 heads, vocab 65536, tied embeddings) |
| Fine-tune | LoRA r=16, scale 16, 400 iters, `mask_prompt: true` (loss on tool call only), fused |
| Compression | `4bit_weight_palettized_group32` (embedding table int8, per Core AI's iOS path) |
| Size | **144 MB** `.aimodel` (~148 MB with tokenizer) |
| Max context | **512** (prompt + generation) β€” deliberately small; see [Context length](#context-length) |
| Engine | `static-shape` (Neural Engine) |

## Runtime requirements

1. **iOS 27 / macOS 27** β€” Core AI ships with the OS. **Device-only on iOS**: `CoreAI.framework` is
   in the iPhoneOS SDK but **not** the iOS Simulator SDK, so this cannot run in the Simulator.
2. **Toolchain β‰₯ beta 3 era.** This bundle was exported with **`coreai-torch 0.4.1` / `coreai-core 1.0.0b2`**
   so it carries the *versioned-IR* location format the beta-3 on-device compiler requires. (Bundles
   exported with the June-era `coreai-torch 0.4.0` / `coreai-core 1.0.0b1` fail on beta 3 with
   `expected AICode versioned location … Failed to convert to versioned IR … cannot unwrap empty odiec_module_t`.)
3. **A conv-cache extra-state patch on the static-shape engine.** LFM2 is a conv+attention hybrid: its
   short-conv layers carry a rolling **conv cache** *in addition to* the KV cache, so the model exports
   **three** states (`key_cache`, `value_cache`, `conv_cache`). Apple's `StaticShapeEngine` hardcodes two.
   The engine must allocate the extra state, **zero-fill it on reset** (unlike KV it is read directly, not
   mask-gated), and bind it by name each step.

> Note: the conv state cannot be partially prefix-rewound (it only holds the last `L-1` columns), so a
> conv model should reprocess each request from scratch rather than reuse a partial KV prefix.

## Use it

The bundle is a standard `LanguageBundle` (`.aimodel` + `tokenizer/` + `metadata.json`) β€” point the
runtime at the **bundle directory**:

```swift
import CoreAILanguageModels

let bundle    = try LanguageBundle(at: bundleDir)          // dir containing the .aimodel
let engine    = try await CoreAIRunner(from: bundle).makeInferenceEngine()  // β†’ static-shape / ANE
let tokenizer = try await bundle.loadTokenizer()

let generator = try await TextGeneratorBuilder()
    .withInferenceEngine(engine)
    .withTokenizer(tokenizer)
    .withSampling(configuration: .greedy)
    .build()

// `.prompt` applies the bundle's chat template; the fine-tune emits the tool call.
let json = try await generator.generate(input: .prompt("Remind me to buy milk"), maxTokens: 60)
```

Catalog entry, for apps that pull Core AI bundles from HF by tree path:

```swift
ModelSpec(
    bundleName:   "uncle_rudy_lfm2_230m_toolcaller_int4pal",
    hfRemotePath: "ane-static/uncle_rudy_lfm2_230m_toolcaller_int4pal",
    repoURL:      "https://huggingface.co/sabeshbesh/uncle-rudy-lfm2-230m-CoreAI",
    label:        "Uncle Rudy 230M",
    approxSizeGB: 0.15,
    warmupToken:  1,
    maxContext:   512)
```

⚠️ A catalog/`ModelSpec` alone is not sufficient: an app wired to the **`coreai-pipelined`** engine
must route this bundle to the **static-shape** engine and carry the conv-cache patch above.

## First load is slow β€” by design

The **first** load on a given device compiles the model's Neural Engine graphs **on-device**, inside
Apple's `AIModel(contentsOf:options:)`. This cannot be shipped precompiled β€” the compiled program is
specific to that device + OS. The OS caches the result, so every later load is near-instant.

| | |
|---|---|
| Cold (first) load, Apple Silicon ANE | **~54 s** |
| Warm load (cached) | **~0.02–0.1 s** |

### Context length

Exported at **512** context on purpose. The iOS static export fans out one specialized ANE graph per
`(context_bucket Γ— query_length)` per function β€” at 2048 that is **24** graphs and a **~151 s** cold
compile; at 512 it is **12** graphs and **~54 s**. A tool-caller only ever sees a short utterance plus
a ≀60-token call, so 512 is ample. Re-export at a larger `--max-context-length` if you need more, and
pay the longer one-time compile.

## Measured

Greedy, `llm-runner`, **macOS / Apple Silicon Neural Engine** (this bundle):

| | |
|---|---|
| Prefill | ~80–330 tok/s |
| Decode | ~73–83 tok/s |

> iPhone on-device throughput is **not yet published** β€” these are Mac-ANE numbers for the same bundle.
> Treat them as indicative, not as iPhone figures.

**Correctness.** The re-authored BC1S / Neural-Engine model was gated against the fp32 Hugging Face
reference: **100% next-token top-1 match (12/12 positions)**, logits PSNR ~52 dB, on a truncated model
covering both layer types (conv + attention). The lower-than-macOS PSNR (~70 dB on the GPU/dynamic
path) is expected: the iOS path int8-quantizes the embedding table and computes attention per-head,
which reassociates fp16 arithmetic β€” every argmax still matches.

## Conversion notes

Converted from the fused Hugging Face checkpoint via a custom Core AI recipe on top of
[apple/coreai-models](https://github.com/apple/coreai-models). Two things were needed beyond the
stock pipeline:

- **MLX β†’ PyTorch conv-weight transpose.** `mlx_lm fuse` writes depthwise Conv1d weights in MLX axis
  order `(out, kernel, in)` = `[1024, 3, 1]`; PyTorch's `modeling_lfm2` wants `(out, in, kernel)` =
  `[1024, 1, 3]`. Only the 8 conv layers are affected (Linear/embedding/norm layouts are identical),
  and the fix is an axis swap β€” verified bit-exact against the base weights.
- **Re-authoring for the Neural Engine.** BC1S `(B, C, 1, S)` layout, projections as 1Γ—1 `Conv2d`,
  per-head attention (no fused SDPA on ANE), transposed causal mask using `-40000` rather than `-inf`,
  and the short conv as a depthwise `Conv2d(D, D, (1, L), groups=D)` over the sequence axis, with the
  conv cache threaded as a third functional state.

## License

Weights derive from [LiquidAI/LFM2.5-230M](https://huggingface.co/LiquidAI/LFM2.5-230M) and are
redistributed under the **LFM Open License v1.0** ([LICENSE](LICENSE)) β€” Apache-style grants, but
**commercial use is licensed only for entities under US$10M annual revenue** (qualified non-profits
exempt for non-commercial/research use). Review the LICENSE before any commercial deployment.