On-device ML · evaluation · fine-tuning

A 0.6B model on a phone reached 80% of Claude Sonnet. Here is how I measured it.

Heed turns a messy spoken note into a clean, sorted task list, entirely on the phone. This is how I built the model, and, more importantly, how I convinced myself the numbers are real.

80%of Claude Sonnet's quality on a sealed test
+20 ptsahead of GPT-5.4 mini, 95% CI +17 to +23
5.1 smedian per note on a mid-range phone CPU
0network calls: the app has no internet permission

The problem

People think out loud. A note sounds like "umm okay so after church I need to call Kwabena's mum about his boots, before Tuesday's training… oh and Kojo said my phone will be ready Monday at four, so I have to go pick that up… and yeah, buy like twenty cones, the old ones are all ruined". What the user wants back is three items: a call, a pickup on Monday at four, and something to buy. Nothing invented and no detail lost.

The hard part was the constraint, not the task. Heed is private by design: the app has no internet permission, so a cloud model is not an option. The model had to fit on an ordinary Android phone, run on the CPU in a few seconds, use about a gigabyte of memory, and read the output of a speech model that writes everything in lowercase with no punctuation. In practice that means a model under one billion parameters.

So the real question was: how close can a model that small get to a frontier model on this one narrow job? Answering it honestly turned out to be most of the work.

The result

On a sealed test set of 617 notes, 126 of them real notes from me and five friends, the fine-tuned Qwen3-0.6B running as an int8 file on a OnePlus phone scored 75.6 judged task F1. Claude Sonnet 5.5 scored 94.8 on the same notes. That makes Heed 80% of Sonnet, and 84% on the real notes alone.

Bar chart of judged task F1: Sonnet 94.8, Flash 91.8, Haiku 85.6, Heed 75.6, GPT-5.4 mini 55.5
Sealed test, run once. Solid bars: all 617 notes. Light bars: the 126 real notes.
Gaps to Heed with 95% confidence intervals
Every gap with its 95% paired-bootstrap interval (4,000 resamples over notes). Haiku and Flash are clearly better. GPT-5.4 mini is clearly worse.

I want to be precise about what this does and does not say. Heed beats GPT-5.4 mini by 20 points, and the interval is nowhere near zero. Claude Haiku 5.5 and Gemini 3.5 Flash are still better, by 10 and 16 points. On the 126 real notes alone, which are shorter and easier, GPT-5.4 mini catches up and the two are level. A 0.6B model running offline is not a frontier model. It gets surprisingly close on one narrow job, at no cost per note and with no data leaving the phone.

How it was measured

This is the section I would read first if I were reviewing this work. A number like "80% of Sonnet" is only as good as the test behind it.

1. A sealed test, opened once

Early on I set aside 617 notes: 126 real ones, plus synthetic and long notes in fixed shares. They were never used for training, never used to pick an epoch or a seed, and never even copied to the machine that trained the models. The test was run exactly once, on the final model, after I had written down in advance how each result would be worded. Everything I tuned, I tuned on a separate validation set of 451 notes. That set flatters the chosen model by about a point, which is why the headline uses the test.

2. Why not word overlap

My first metric compared the model's tasks with an answer key using word overlap. It is cheap and repeatable, but it has two flaws that matter here. It counts a different but correct wording as an error: "Phone Kwabena's mum about the boots" against "Call Kwabena's mother about his boots". And because the key was written by Sonnet, it rewards models that write like Sonnet. Comparing frontier models that phrase things their own way would be unfair. Word overlap is fine for comparing two versions of my own model during development. It is not fine for a headline.

3. The metric: judged task F1

For the comparison, a judge reads the note and a list of tasks, and labels every task:

It also lists what the speaker clearly meant that no task covers: the missed tasks. Then:

precision = good / (good + fixable + wrong)
recall    = good / (good + fixable + missed)
F1        = 2 · precision · recall / (precision + recall)

A fixable task counts against both precision and recall. That is strict on purpose. A task that needs an edit still costs the user an edit, so it should not count as a success. A small example: "buy milk and eggs, call the dentist friday, and i wonder if we should repaint the fence". If the model lists "Buy milk", "Buy eggs", "Call the dentist" (Friday lost) and the fence as an idea, it scores 3/4 for both precision and recall, so F1 is 75. Keep "Friday" and it is 100.

4. An LLM judge, and how I tested it before trusting it

Judging about 7,000 tasks by hand was not realistic, so the judge is an LLM: Gemini 3.1 Pro. I chose a different model family from the one that wrote the labels, so it would not favour Claude-style lists. It sees only the note and an anonymous list. It never sees the answer key, never knows which system wrote the list, and the calls run in a shuffled order.

An LLM judge is a measuring instrument, so I tested it like one before using it. Each check had a pass mark written down before the test:

Judge checks: planted errors 96%, repeat agreement 96%, agreement with GPT-5.4 kappa 0.63, agreement with a human kappa 0.84
I planted errors in correct lists (a dropped task, an invented task, a changed detail, a merge, a wrong bucket) and checked that the judge found them. I re-ran calls to check stability, and compared it with a second LLM and with my own ratings.

The human check is the one I care about most. I rated 50 items blind, without seeing the judge's verdicts, and the judge agreed with me at Cohen's κ 0.84 (92% raw agreement). Earlier in the project I checked the simpler scorer the same way: 351 task pairs, κ 0.95 between my links and the automatic ones. I also measured anchoring: showing the judge the answer key moved its "good" rate by only 1.7 points.

A second judge from another family. Any claim that names a competitor also had to hold under a second judge, GPT-5.4, on 100 notes. The order came out the same: Sonnet, Flash, Haiku, Heed, GPT-5.4 mini. Two details make this check stronger. GPT-5.4 rated its smaller sibling about 12 points higher than Gemini did, which is a possible family bias that works against my claim. Heed still came out ahead, +10.3 [+3.3, +17.2]. And under that judge Heed reached 86% of Sonnet, so 80% is the more conservative of the two numbers.

5. A fair fight

The cloud models got the full 4,105-character labelling rulebook in their prompt; Heed got none, because it learned the rules from data. They ran as served, at temperature 0 with default reasoning settings. All answers went through the same parser. GPT-5.4 mini might do better with more reasoning effort, and I say so wherever the number appears.

What the errors look like

Verdicts: 70% good, 16% fixable, 5% wrong, 8% missed
More than half of Heed's errors are near misses: the task is real, but a detail is lost or the wording needs a fix.

On real notes, Heed gets the whole list perfect on 52% of notes, against 83% for Sonnet. It stays quiet correctly on 39 of 42 notes with nothing to do. The weakest bucket is Watch: 15% of Watch items land in the wrong bucket, partly because "watch" is also a verb.

The data

I had a few hundred real notes and needed thousands, so most of the training data is synthetic. Every note was labelled the same way: Claude Sonnet 5.5 at temperature 0, under one frozen labelling prompt, so the model learns one consistent style.

Training data composition
8,610 training notes. Real notes from me and five friends are a small share, but most of the validation and test numbers I trust come from real notes.

Two choices mattered more than the rest:

Before and after for four changes
The four changes that moved the numbers most. Each panel uses the metric of its own stage, so compare before and after within a panel, not across panels.

Choosing the model

I fine-tuned six small models on the same data with the same recipe and counted the corrections a user would have to make per note. Bigger was better, smoothly. A learning curve on the same data showed quality still improving at 100% of the notes. Data, not size, was the limit, which is why most of the later effort went into data.

Corrections per note by model size
Same data, same recipe, six base models. Lower is better.
Learning curve
Quality at 25%, 50% and 100% of the training notes. The curve has not flattened.

Qwen3.5-0.8B was slightly better than Qwen3-0.6B. I still shipped the 0.6B. Google's official LiteRT-LM converter supported Qwen3 but not Qwen3.5's hybrid attention layers. The 0.8B ran slowly on a phone CPU, its file was 27% larger, and the quality gap was small. On a phone, the model you can ship beats the model that scores best.

I also checked whether a small model could do the job with good prompting and no training. It could not. Prompted Qwen3-0.6B needed 3.6 corrections per note and got 3% of notes fully right. Untrained small models were worse than a TF-IDF baseline at picking the bucket. Fine-tuning was not optional.

Training

SettingValue
BaseQwen3-0.6B-Base, full fine-tune (596M parameters)
ObjectiveCross-entropy on the answer tokens only; the prompt is masked
OptimizerAdamW, lr 1e-5, 5% warm-up, cosine decay, gradient clipping 1.0
Batch16 rows per step (8 × 2 accumulation), max length 1,024
Epochs3, chosen by task score on validation
Precisionfp32 master weights, bf16 autocast, gradient checkpointing
HardwareOne rented RTX 4090, 33 minutes, peak 17 GB, about $2 of GPU per experiment
OutputCompact JSON, {"tasks":[{"t":"…","c":"…"}]}, converted to an int8 LiteRT-LM file (614 MB)

Full fine-tuning or LoRA

I used both, for different jobs. The early bucket and reminder classifiers were trained with LoRA (rank 16, α 32, on every linear layer), because they were trained on a laptop GPU with 4 GB of memory, where memory was the binding constraint. For the task writer I used full fine-tuning. A 0.6B model trains fully in 17 GB on a single rented RTX 4090, for about $2, so LoRA's main advantage, memory, was not needed. For scale, rank-16 LoRA on every linear layer of Qwen3-0.6B would train about 10 million parameters, under 2% of the model.

To be clear about the evidence: I did not run a head-to-head comparison of LoRA and full fine-tuning for the writer, so I make no claim about which gives better quality here. That comparison would be the next obvious ablation.

Choosing the epoch: loss is the wrong signal

In every run, validation loss was lowest after the first or second epoch and then rose, while the task score kept improving.

Validation loss rising while task score improves
One 8-epoch run, scored after every epoch. Early stopping on loss would have picked epoch 2 and thrown away about 3 points of task score.

Rising loss here means the model gets more confident on the tokens it gets wrong, not that its answers get worse. What I care about is the answers, so epochs are chosen by task score. The same run also showed that training longer did not help: eight epochs plateaued at the level of four.

Training curves of the app model
The training run of the app model: training loss, validation loss per epoch, and the learning-rate schedule.

Seeds and noise

The same recipe on two seeds gave results 1.5 to 3 points apart. So a single-run difference under about 4 points is not a result, and every recipe decision used paired comparisons and at least two seeds. It sounds obvious, but it killed several ideas that looked good on one run.

The most important decisions

DecisionWhy
Fine-tune instead of promptingPrompted small models were worse than TF-IDF at picking buckets
Only models that run on the phoneThe privacy promise. Bigger models were never candidates
Qwen3-0.6B over Qwen3.5-0.8BOfficial converter support, phone speed and file size, at a small quality cost
One consistent set of labelsMixed labels taught the model to hedge ("Maybe call…"). Relabelling everything with one prompt raised real-note scores from 61 to 78
Drop reminders and separate time fieldsOne model writing task, bucket, time and reminder worked, but reminders cost 8 to 10 points on real notes and depend on the person. The time now stays inside the task text, and reminders are set by hand
Train on speech-style text tooThe phone's speech model writes no punctuation. That gap went from 5.7 points to 0.2
An LLM judge, validated against a humanWord overlap punishes correct rewording and rewards the key's style
Choose epochs by task score, not lossLoss and quality move in opposite directions after epoch 2

What did not work

On the phone

Quality and speed by note length
Left: quality by note length on the sealed test. Right: median time on a OnePlus phone with a MediaTek Dimensity 8300, CPU only.

The model runs through LiteRT-LM on the CPU with greedy decoding. The median is 5.1 seconds per note (p95 12.7 s). Time is almost entirely decoding: about 1.35 s plus 17.6 ms per character of answer. The whole app uses about 1.2 GB with the model loaded, and only one model (speech, tasks or search) is in memory at a time. Since the runtime has no grammar-constrained decoding, a small guard stops the answer when the JSON closes or a task starts repeating. 614 of 617 answers on the test were valid JSON.

Limitations

All the models, including the experiments that failed, are on Hugging Face with their training curves, settings and scores. The synthetic notes, the labels, every system's answers on the public test notes and the judge's verdicts are in the dataset, with a small script that recomputes the scores. The full experiment-by-experiment record is in the model card.