Repository navigation
Releases: DenisovAV/litetune
Release list
litetune 0.6.0
The runtime and conversion pins are those of 0.5.1: LiteRT-LM 0.18.0.
Nothing here changes a bundle an earlier release made for a model it already took. No convert again is needed.
Six more models, three of them new families (#88)
litetune now takes Qwen3-1.7B and Qwen2.5-1.5B-Instruct, one size up in the two Qwen families, and three families it did not take before: IBM Granite 4.1, MiniCPM5 and Qwen 3.5. Each was tuned, converted and verified on banking77, with the recipe, rows and scorer of the earlier banking77 runs. On every verify the runtime and the float reference rendered the same token ids on all 600 prompts. Every bundle held a LiteRT-LM conversation. The numbers are in MEASUREMENTS.md.
| model | float | dynamic_wi8_afp32 |
weight_only_wi8_afp32 |
|---|---|---|---|
| Qwen3-1.7B | 0.7400 | 0.7383 | 0.7383 |
| Qwen2.5-1.5B-Instruct | 0.7683 | 0.7717 | 0.7733 |
| granite-4.1-3b | 0.7800 | 0.7883 | 0.7783 |
| MiniCPM5-1B | 0.7167 | 0.7067 | 0.7167 |
| Qwen3.5-0.8B | 0.7800 | 0.7767 | 0.7917 |
| Qwen3.5-2B | 0.8000 | 0.7833 | 0.7967 |
The paired difference from the float reference is resolved in two cases, and they go in opposite directions:
- Qwen3.5-0.8B's
weight_onlyfile scored 0.0117 above its reference. - Qwen3.5-2B's
dynamicfile scored 0.0167 below its own.
Every other difference is within its interval.
What each new rule does:
- Granite 4.1 composes the checkpoint's own chat template behind the text-parts block, as Qwen 2.5 does.
- MiniCPM5 and Qwen 3.5 pack LiteRT-LM v0.18.0's canonical template with litetune's edits.
convertuses it only after its single-turn renderings match the checkpoint's own. - Qwen 3.5: a LiteRT-LM Engine that serves one prompt after another carries Qwen 3.5's recurrent state into the next prompt (LiteRT-LM#3165). On 50 held-out prompts it answered 22 of them otherwise than a process per prompt did; with an Engine per prompt it answered none otherwise.
verifytherefore builds an Engine for every Qwen 3.5 prompt. This was measured onverify's own driver, not in an app. - Qwen 3.5 LoRA: 18 of Qwen 3.5's 24 layers are linear attention, whose projections are not among the seven
tunetargets. LoRA reaches every layer's MLP and six layers' attention.
A template that prints its own start token (#88)
LiteRT-LM hands a chat template an empty bos_token and puts the bundle's start token in front of what the template renders. convert used to compare a template's single turns without that token, so MiniCPM5, whose template prints <s>, could not be checked and was refused. The check now predicts the start token the exporter writes and renders with it in front. After export it reads that token back from the bundle.
Decisions for every family (#88)
0.5.0 trained and measured decisions on google/gemma-3-270m-it only. Now every family litetune has a rule for takes a decision. One or two models of each were trained on the emotion task (six options), verified with a float twin and bundled (MEASUREMENTS.md).
- Passed verify: gemma-3-270m, functiongemma-270m, gemma-4-E2B, Qwen3 0.6B and 1.7B, Qwen2.5 0.5B and 1.5B, MiniCPM5-1B, granite-4.1-3b and Qwen3.5-0.8B.
- On the 300 rows the runtime scored, its top option was the reference's on 0.9900 to 1.0000 of them.
- Every float twin but Qwen 3.5's was within 3.80e-04 nats of the reference.
- gemma-3-1b-it was refused by the abstain rule, not by the runtime. The reference was wrong on 0.0629 of the held-out rows it answered, against the 0.05 its threshold was fitted to keep.
- Qwen 3.5's float twin is held to 5e-02 nats, not 1e-3. Its bundle was 2.65e-02 from a float64 reference on the twin's rows, where PyTorch in float32 was within 2.7e-05: the bundle's own float32 arithmetic. The manifest says so. Each option is scored on an Engine of its own, and a decision took a median 39,766 ms on four CPU threads.
What changed for a decision:
-
--float-twinis required for every family but Gemma 3 text. The float32 export checks the runtime's scoring method on the checkpoint.prepare,tuneandverifyname the models of the family that were measured, and the task. -
The
litetune.decisionsection records how the runtime scored the options:rewind: one session, rewound after each option;fresh_engine: an Engine per option, for Qwen 3.5. This costs an Engine load per option.
A decider is to score the same way.
-
Control text is refused in a decision's states and options: the control text of any supported family, and every special token of the tokenizer when
prepareis given one. A split that 0.5.x accepted can now be refused if one of its options holds text such as<s>or<think>. -
Gemma 4's exported bundle does not apply the logit soft-cap its forward applies. The reference follows the bundle. Greedy text is the same either way; sampled text is not.
litetune 0.5.1
The runtime and conversion pins are those of 0.4.3.
A bundle no longer denies the measurements it carries (#86)
litetune bundle writes a manifest.json of its own. The command never passes it a run manifest, so the file lists no stages and its status reads inconclusive. Its limitation was worded as if it were about the whole bundle:
no run manifest was supplied to the bundle stage, so this file records only what the bundle stage itself observed: which files were packaged, not what produced them or what was measured
That sentence was also in report.json, beside the measurements --verify-manifest had put there. It is now said of manifest.json alone, and points at report.json for the measurements.
The limitation every bundle carries first said the bundle "was assembled from what the run recorded". bundle also runs with no verify manifest and no training record, the contract given as flags, and then there is no run record. It now says the bundle was assembled from the files and records the stage was given.
Who is affected: every bundle an earlier litetune built carries both old sentences. They are wrong about the bundle, not about the model: the measurements in its report.json are the ones verify recorded. Nothing needs to be built again.
noul measured on two tasks (#85)
prepare and verify tell a noul question how far option scoring has measured its type. They said "on one task only"; they now say "on two". score is still measured on one.
Both runs are on google/gemma-3-270m-it. The candidate ran on LiteRT-LM 0.18.0's CPU backend, on four threads of an x86_64 Linux machine. The float reference ran in PyTorch on a GPU. The details are in MEASUREMENTS.md.
- SST-2, which is SST-5 without its neutral rows, as "Is the text positive?". verify passed it:
- the runtime's top option was the reference's on 0.9767 of 300 rows;
- the reference's accuracy on 1,821 held-out rows was 0.9055.
- Irony, whose eight-bit bundle verify refused in 0.5.0, trained for two epochs, at 0.9367 against the 0.95 gate. Trained again for four and for six epochs, with every other flag the same, it passed:
- the runtime agreed with the reference on 0.9667 and 0.9500 of the same 300 rows, the second exactly on the floor;
- the reference's accuracy on 784 held-out rows was 0.6671 and 0.7117, against 0.6901 at two epochs. Neither difference is resolved: −0.0230 ± 0.0511 and +0.0217 ± 0.0365, paired on the same rows.
MEASUREMENTS.md also records that 0.5.0 converted, verified and bundled SST-5's checkpoint again, and that every score verify took equalled the earlier run's bit for bit.
litetune 0.5.0
The runtime and conversion pins are those of 0.4.3.
Decisions: a model that chooses among options (#82, #83)
A decision model answers a question about a text with one of a fixed list of options: which emotion, which support issue, yes or no. litetune now trains one, measures it the way flutter_edge_ai's InferenceModel.createDecider is to use it, and writes the question, the options and the calibration into the .litertlm itself, as a litetune.decision section. Nothing ships beside the file.
createDecider scores every option's log-probability through the runtime instead of letting the model generate. In flutter_edge_ai it is a proposal at the time of this release; until it ships, the call throws DeciderUnsupportedException.
The stages are the usual five, and the README walks through them:
-
prepare --questionsrenders every row into the promptcreateDecidersends and sets a calibration split aside. The prompt is pinned by golden strings intests/fixtures/decision-contract-v1.json, which flutter_edge_ai's renderer is to test against too. -
tune --decisionchecks every row against the decision and records it beside the checkpoint. -
convertadds--prefill_lengths=8,128to a checkpoint that records a decision, unless the caller passes a setting of their own. Scoring re-prefills one token per decision, which otherwise ran padded to 128 tokens. -
verifyfits a temperature on the float checkpoint's scores and, forchoice, the threshold below which an answer abstains. It then scores held-out rows through the runtime and compares. It refuses:- a runtime that scored another prompt than the reference;
- answers that agree with the reference's on fewer than 95 % of rows;
- a float twin more than 1e-3 nats off;
- a threshold that answers no in-scope row.
A self-check finds out whether the runtime drops a prompt token after a prefill, as LiteRT-LM 0.18.0 does, and scores accordingly.
-
bundlewrites the section only when verify passed or was inconclusive. With any other status it bundles the file without one, and takes out a section an earlier bundle left. It exits with verify's status: 1 for a failed gate, 4 for a verify that could not check.
Measured on google/gemma-3-270m-it. The candidate ran on LiteRT-LM 0.18.0's CPU backend, on four threads of an x86_64 Linux machine. The float reference ran in PyTorch on a GPU. The datasets, commands and the rest are in MEASUREMENTS.md.
- Emotion, six options,
choice: verify passed.- The reference's accuracy on 2,000 held-out rows was 0.9335.
- On 300 rows spread through the split, the runtime's top option was the reference's on every row. Its answer, abstentions included, matched on 0.9933.
- A decision took 370.1 ms, median.
- Irony as
noul: verify refused the eight-bit bundle. Its answers agreed with the reference's on 0.9367 of 300 rows, below the 0.95 gate. - SST-5's five levels as
score: verify passed at 0.9567.
What is not measured: any model but google/gemma-3-270m-it, whose family alone is accepted, and a phone. The README lists the rest.
It needs training on the task: the untuned model, and one trained on other tasks, chose far worse. A decision costs one scoring pass per option, so its cost grows with the option count.
A base-model record that cannot be read is reported (#80)
convert used to take a litetune.json beside the checkpoint for no record when it was a directory, a dangling link, a FIFO or a device. It then guessed the base model from the path, and the report said nothing. It now reports such a record as a limitation. It reads the record with the same reader that verify and the chat-template choice already used.
Two edge cases of that reader changed, both for Qwen3 only, the one family LiteRT-LM's own template is offered to:
- A record linked through a file is now unreadable rather than absent, so the checkpoint keeps its own chat template.
- A record nested too deeply for Python's JSON parser no longer stops
convert.
litetune 0.4.3
Convert a Qwen3 bundle again if an application holds a conversation with it. Every Qwen3 bundle an earlier litetune converted without a template of your own carries Qwen's own chat template, and LiteRT-LM's conversation cannot render that template past the first message: the first message is answered, and the second fails with INTERNAL: The new rendered template string does not start with the previous rendered template string. A bundle used for single prompts, one conversation per prompt, is unaffected, and so are its published figures. No other family is affected.
Qwen3 bundles hold a conversation (#78)
LiteRT-LM renders the whole history on every message and requires the new rendering to start with the previous one (Conversation::GetSingleTurnTextFromFullHistory; google-ai-edge/LiteRT-LM#3443). Qwen3's template renders the last assistant turn with an empty <think> pair while it is the last message, and without it once the next one arrives.
convert now hands a Qwen3 checkpoint LiteRT-LM v0.18.0's own models/qwen3/chat_template.jinja, vendored with its provenance, when it renders single turns exactly as the checkpoint's own template does. That covers the base model as published, and a checkpoint tune trained on rendered prompts without declarations. Otherwise the checkpoint's own template is kept, as before, and the report says the bundle answers one message.
Measured on Linux x86_64 on litert-lm 0.18.0's CPU backend, dynamic_wi8_afp32:
- The tuned Qwen3 checkpoint. Converted by 0.4.2, it failed the second message in both conversations tried. Converted by this release, it answered three in a row.
Qwen/Qwen3-0.6Bas published. Converted before this release, it failed the second message. Converted by this release, it answered "Paris", then "Berlin".verifyon the tuned bundle is unchanged: 0.6983 for candidate and reference, cost 0.0000 (10), with the reference's token ids for all 600 prompts.
Every bundle's template is checked (#78)
convert checks two things for every family:
- that the bundle's chat template renders single turns as the checkpoint was trained;
- that it holds a LiteRT-LM conversation past its first message, over plain replies, replies with reasoning, and two and three turns, with
enable_thinkingundefined, true and false, with and without a tool.
A bundle that breaks the second is still exported, and its report says it answers one message.
The rendering follows LiteRT-LM v0.18.0's MiniJinja. A template using something that rendering does not reproduce is reported as not checked rather than passed. For a checkpoint template the single-turn check needs, that means nothing is exported.
Qwen2.5's, Gemma 3's, Gemma 4's and FunctionGemma's templates hold.
Windows stops a stage and everything it started (#77)
Each stage now runs in a job object that owns the child before it starts. A timeout, Ctrl-C, Ctrl-Break or a forced kill of litetune ends the stage and everything it spawned: pip's compilers, torch workers, the interpreter behind a venv's python.exe. Stopping gives the stage a Ctrl-Break and a grace before it is killed, and Ctrl-Break to litetune exits 4.
Measured on the windows-latest CI image on CPython 3.10, 3.12 and 3.13 with real process trees, each against a negative control.
Also on Windows:
- a stage that closes its pipes and then hangs keeps its output;
- on Python 3.10, a usage error with stderr closed exits 4 rather than 120.
litetune 0.4.2
Nothing in this release asks you to convert or verify again: every published figure stands.
Refused before pip, from what PyPI publishes (#74)
litetune now carries, for every pinned package a stage installs, the wheel files PyPI lists for that exact release, and checks them against this interpreter's compatible tags (as pip computes them, through packaging, which litetune now depends on) before building a stage environment. Where a pinned package has no wheel for this platform and Python, the stage is refused at once, naming the package, instead of failing inside pip after downloading torch:
converton Windows:litert-converter==0.4.0publishes wheels for macOS on Apple Silicon and Linux x86_64 only, and no source distribution. Use WSL2 or Linux.verify,tuneandpreparekeep working on 64-bit Windows.- A 32-bit or native ARM64 Python on Windows, Intel macOS, macOS before 12, and a glibc older than a pinned wheel's floor.
- Where only this Python version is missing a wheel, the refusal names the Python versions for which every pin has one on this platform.
Installing from another index or a wheelhouse that has the wheel, set LITETUNE_ALLOW_MISSING_WHEELS=1: the gap becomes a note and pip is asked.
Python 3.13 (#74)
tune runs on Python 3.13 on Linux x86_64. The training environment pins sentencepiece 0.2.1 there, the first release with Python 3.13 wheels, and 0.2.0 on Python 3.10 – 3.12 as before, so those environments are unchanged and are not rebuilt. On Linux x86_64 with Python 3.13 the previous pins did not install at all. Measured on 2026-10-12: one short LoRA run of Qwen/Qwen2.5-0.5B-Instruct gave the same loss and a byte-identical adapter with 0.4.1 on Python 3.12, with this release on Python 3.12, and with this release on Python 3.13. torch 2.5.1 publishes Python 3.13 wheels for Linux x86_64 only, so elsewhere Python 3.13 is refused before pip.
Windows (#74)
- Manifests and declarations written by Windows PowerShell 5.1's
>(UTF-16) are read; in prerendered mode, where declarations ship as given,bundlerefuses one that is not UTF-8 without a byte-order mark, by name. - Console output and
--helpno longer raise on characters the console's code page cannot encode. converttouches the input checkpoint only once its environment stands.- The provisioning lock tells a lock another process holds from one the filesystem will not take.
pip installruns in your working directory, so relative pip settings keep their meaning, but a module in that directory is not imported in pip's place; index URLs and pip's output are reported with credentials replaced by***.- The test suite now runs on Windows in CI, including an end-to-end check of
convert's refusal.
The float reference decodes greedily (#73)
verify's float reference called transformers' generate greedily, and transformers merged the checkpoint's own generation_config into that call, so settings that change a greedy pick applied. Qwen2.5-0.5B-Instruct's sets repetition_penalty 1.1; the candidate, decoded by LiteRT-LM, gets no penalty. The reference now keeps only the checkpoint's token ids and cache settings, and verify names any setting it left out. Among the measured checkpoints only Qwen2.5 set one -- Gemma 3 270M and 1B, Qwen3 0.6B and FunctionGemma set only sampling, cache and end-of-sequence fields; Gemma 4 E2B's was not checked. Re-measured on litert-lm 0.18.0, the Qwen2.5 dynamic_wi8_afp32 figures are unchanged: candidate 0.7700, reference 0.7700, cost 0.0000 on 8 discordant rows.
litetune 0.4.1
Only one kind of bundle needs converting again: a Qwen3 0.6B or Qwen2.5 0.5B Instruct bundle litetune 0.4.0 converted from a checkpoint it did not recognise -- another organisation's copy (for example unsloth/Qwen3-0.6B) or a fine-tune under its own name. 0.4.0 gave such a bundle no block that reads LiteRT-LM 0.18.0's list of parts, so on 0.18.0 it loses the user's text (Qwen3) or fails every send (Qwen2.5); verify refuses it. A bundle 0.4.0 converted from Qwen/Qwen3-0.6B, Qwen/Qwen3-0.6B-Base or Qwen/Qwen2.5-0.5B-Instruct at their current revisions, or from what tune made from them, is unaffected.
The checkpoint's own template (#71)
For the Qwen3 0.6B and Qwen2.5 0.5B Instruct rules, convert now puts the text-parts block in front of the chat template the converted checkpoint's own tokenizer carries, read the way the exporter reads it, instead of a copy of one revision's template packaged with litetune. It composes only for a template the block is for -- one user message given as parts must render as the template renders it given as a string -- and reads the template back from every bundle it writes; a bundle that does not carry exactly the composed text fails its recipe and is moved aside. The rules claim these checkpoints under other organisations and names again, but not quantized repacks the exporter cannot load.
For the two tuned banking77 checkpoints the published Qwen sections use, the composed bundles carry the same template as 0.4.0's but for Jinja comments, and verified on litert-lm 0.18.0 they score exactly what 0.4.0's did; MEASUREMENTS.md has the figures in the 0.18.0 section. Qwen3-0.6B-Base, to which 0.4.0 gave Qwen3-0.6B's template, renders one user turn and a two-turn text conversation to the same prompt with either, rendered by transformers' Jinja2; the README has the comparison.
Exporter flags (#71)
Flags passed to the exporter with --flag= are read once, by the rules the exporter's command-line parser uses: - and _ are one, --no<name> is False, values are the literals it makes of them, and of a repeated flag the last wins. The exporter then receives exactly what convert checked and reports. Forms the parser would read differently, and the three flags convert sets itself, are refused with what to pass instead. Gemma 4 refuses any model-type override, which skips its metadata builder, and an opt-out of --externalize_embedder, without which it does not export.
A bundle whose header cannot be read, or whose sections run past the end of the file, now fails its recipe for every family.
Python from uv (#70)
Stage environments link the interpreter instead of copying it. Under a Python that loads libpython relative to itself -- such as the CPython 3.10.19 and 3.11.14 uv 0.10.6 installs on macOS -- a copied interpreter could not start, so no stage environment could be provisioned. A failure while building one now says why, and an environment whose interpreter was removed says so and is rebuilt. A CI job now runs the environment tests on macOS under such a Python.
litetune 0.4.0
Two kinds of bundle need converting again with this release.
- A Qwen3 0.6B or Qwen2.5 0.5B Instruct bundle for an app on LiteRT-LM 0.18.0, which flutter_edge_ai_litertlm 1.10.0 ships. From 0.18.0 the runtime hands a bundle's template every message's content as a list of parts, and both checkpoints' chat templates read it as a string: on 0.18.0 a Qwen3 bundle rendered the user's turn empty with no error, and every send to a Qwen2.5 bundle failed. litetune up to 0.3.0 packed the checkpoint's template unchanged; 0.4.0 packs it behind a block that turns a list of text parts back into the string. A bundle converted with a template of your own is not affected by litetune's rules.
verifyrefuses an old bundle at its rendering check and names this as a possible cause (#67). - Any bundle
convertwrote to a relative--output-diron Python 3.10 or 3.11. Below Python 3.12 the GPU-activation repack resolved its data paths twice and failed, so the bundle kept no GPU activation key;convert's report named the key as not written. On a GPU backend such a bundle computes activations in F16, measured on a Galaxy S24 as<pad>floods and wrong tool names on litert-lm 0.16.1 and as replies with no text and no call on 0.18.0 (#67).
LiteRT-LM 0.18.0
verify runs the candidate on litert-lm==0.18.0, and convert packs with litert-lm-builder==0.18.0; the exporter and quantizer pins are unchanged. Every published number re-measured on 0.18.0 -- 22 verified bundles: gemma-3-270m and gemma-3-1b at eight and four bits, the tuned Gemma 4 E2B at eight, FunctionGemma's tool path in both grammar modes, and both Qwen families converted again -- equals its 0.17.1 number to four places, and the four-bit recipes refused on 0.17.1 are refused again with the same counts. MEASUREMENTS.md has the table, The 0.17.1 re-measurements again on litert-lm 0.18.0 (#67).
On a Galaxy S24 GPU with litertlm-android 0.18.0, FunctionGemma still needs the GPU activation key convert writes: without it 8 of 20 replies came back with no text and no call (#67).
A checkpoint litetune has no rules for is now told, at convert, that 0.18.0 hands its template a list of parts, and that verify's rendering check is what shows whether its template reads one (#67).
litetune towers
A new command rewrites a Gemma 4 .litertlm's tower sections (#68):
# A text-only bundle: drop the vision and audio towers.
litetune towers --model gemma-4-E2B-it.litertlm --drop vision --drop audio \
--output gemma-4-E2B-it-text.litertlm
# Towers from one bundle into another.
litetune towers --model gemma-4-E2B-it-npu.litertlm --add vision --add audio \
--from gemma-4-E2B-it.litertlm --metadata-from-donor \
--output gemma-4-E2B-it-npu-towers.litertlmEvery section is copied byte for byte and read back before the result is linked into place. Both inputs are treated as untrusted: their headers, section types and item keys are checked before litert-lm-builder unpacks them, and what the command parses is bounded. --add refuses a donor whose adapters write a different embedding width than the bundle's text model, metadata that is not Gemma 4's, a token string or id the two tokenizers disagree on, and media settings the added towers were not built for.
Measured on Google's gemma-4-E2B-it.litertlm: dropping the six tower sections removes 332,387,932 of its 2,588,147,712 bytes; on litert-lm 0.18.0's CPU backend the result answered eight greedy prompts exactly as the full bundle did, at a peak RSS within 1 MB, and refused image and audio input. Adding those towers to a text-only Gemma 4 E2B build for SM8850 gave a bundle that, on one Galaxy S26 through LiteRT-LM 0.18.0's C API, answered an image and an audio turn with its prefill_decode graph on the NPU, and gave the same eight text answers as the build alone. That is a smoke test on one phone, not a quality measurement; MEASUREMENTS.md has both sections.
Also
litetune 0.3.0
If a float-reference run on litetune 0.1.5 through 0.2.1 timed out after placing the model, its manifest could record the reference backend as unknown. Generated answers and scores are unaffected; re-run only if you rely on the hardware provenance of that timed-out run. The timeout path now retains the device and environment that the child had already reported (#63).
Apple GPU support
tune and the Hugging Face float-reference side of verify can now run through PyTorch's Metal Performance Shaders (mps) backend on Apple silicon. Candidate generation in verify remains a separate LiteRT-LM backend.
Device selection is CUDA, then MPS, then CPU, using the PyTorch installation inside the stage environment. Set LITETUNE_DEVICE=cpu to force CPU execution.
Before an MPS run, litetune checks macOS memory pressure and computes a conservative unified-memory budget. With litetune-computed allocator settings, it refuses to start when pressure is already elevated or the computed budget is below 1 GiB. MPS allocator settings supplied by the operator are validated and preserved; an operator-supplied high watermark takes responsibility for that limit instead of applying the computed-budget floor. The resulting policy, host facts, allocator environment and sampled memory maxima are recorded in the run metadata.
MPS training defaults to float32 and one example per forward pass. Supervised-token-weighted gradient accumulation preserves --batch-size as the optimizer batch, while --micro-batch-size lets the operator choose another forward-pass size. CPU and CUDA keep their previous full-batch default.
Unsupported MPS operations and MPS out-of-memory errors are reported as device limitations with LITETUNE_DEVICE=cpu as the fallback, rather than as failed training recipes.
The guarded MPS validation run recorded on #63 trained Qwen2.5-0.5B-Instruct for 2,400 examples and 300 optimizer steps in 343.0 seconds. A separate CPU baseline with the same example and step counts took 1,424.4 seconds. During the MPS run, memory pressure stayed normal, swap grew by 0 bytes, and the largest sampled MPS driver allocation was 6,146,768,896 bytes.
litetune 0.2.1
Nothing in this release makes an earlier result wrong, and the package is unchanged apart from its version. It finishes re-measuring on litert-lm 0.17.1 — the runtime 0.2.0 pins — what could be re-measured, and publishes it beside the 0.16.1 numbers in MEASUREMENTS.md.
Each pair of runs differs in runtime, transport and possibly CPU, so no difference between them is attributed to the runtime; a cost that resolved in one run and not the other is two runs' verdicts, not a measured change.
Four bits, on the four banking77 checkpoints (#60)
The same sixteen bundles. Every block-wise cost resolves again:
dynamic_wi4b32_afp32, 0.16.1 |
0.17.1 | dynamic_wi4b32_emb8_afp32, 0.16.1 |
0.17.1 | |
|---|---|---|---|---|
| Qwen3-0.6B | +0.0350 | +0.0283 | +0.0550 | +0.0500 |
| Qwen2.5-0.5B | +0.0767 | +0.0767 | +0.0917 | +0.0833 |
| gemma-3-1b | +0.0883 | +0.0833 | +0.0883 | +0.0783 |
| gemma-3-270m | +0.3483 | +0.3650 | +0.3200 | +0.4067 |
Every channelwise recipe that refused on 0.16.1 refuses again but one: weight_only_wi4_afp32 on Qwen2.5-0.5B finished all 600 generations and costs +0.2500 against a float score of 0.7700. That is not the runtime finishing what 0.16.1 could not: on 0.16.1 one prompt exceeded litetune's 300 s per-prompt limit, and since 0.1.9 the transport no longer cuts a single prompt at 300 s: the split gets 300 s per prompt as one budget. On gemma-3-270m the two block-wise recipes reversed order between the runs, and which costs it less is not established by either.
The tuned Gemma 4 E2B (#61)
The same two eight-bit bundles, against the same seven-projection checkpoint:
| 0.16.1 | 0.17.1 | |
|---|---|---|
dynamic_wi8_afp32 |
+0.0217 ±0.0142 (resolved, 19 discordant) | +0.0083 ±0.0108 (unresolved, 11) |
weight_only_wi8_afp32 |
+0.0183 ±0.0108 (resolved, 11) | +0.0050 ±0.0057 (unresolved, 3) |
Not re-measured
The headline FunctionGemma runs, the conversions of Gemma 4's base weights, the runs on untuned bases and the phone runs remain 0.16.1's, or the device's, and say so.
The site's Qwen2.5-0.5B card no longer calls its channelwise four-bit scores usable: they cost +0.2683 and +0.2500 against 0.7700.
litetune 0.2.0
If you ship a model on litert-lm 0.17.1, re-run verify with 0.2.0. flutter_gemma_litertlm 1.8.0 ships that runtime. Every litetune up to 0.1.9 measured the converted model on litert-lm 0.16.1, and 0.17.1 does not give 0.16.1's answers, so a 0.1.x number describes your bundle on a runtime your app no longer runs. The number was not wrong; it is a number about something else.
Why a runtime bump is a new basis for every number
Measured on Linux x86-64, CPU backend: after each of twelve prompts on three bundles, one fixed sentence was scored with run_text_scoring, which no sampler or detokenizer touches.
| Per-token scores identical, of 36 prompt-bundle scorings | same runtime | 0.16.1 against 0.17.1 |
|---|---|---|
| same CPU model, another run or machine | 36 | 1 |
| Intel Xeon @ 2.20GHz against AMD EPYC 7B12 | 0 | 0 |
One runtime on one CPU model reproduced every score to the bit — on 0.17.1 on the Intel model with 1, 4, 8, 16 or 32 threads alike. Across runtimes, or across CPU models, it did not. litetune's own driver outputs agree: 152 of 152 identical when 0.16.1 ran twice on one machine, 47 of 152 between the runtimes. So a litert-lm number belongs to a runtime version and a CPU, and the model name alone does not identify a CPU: two machines reporting Intel(R) Xeon(R) CPU @ 2.20GHz differed in AVX-512.
What changes
- The runtime.
verifyruns the candidate onlitert-lm==0.17.1.convertpinslitert-torch-nightly==0.10.0.dev20260926withlitert-lm-builder==0.17.1, and now also the quantizer, LiteRT and converter packages it requires, which up to 0.1.9 floated to whatever was newest the day of the install. - Every number says where it came from. A
verifymanifest records the litert-lm version its candidate ran on and the host's CPU model — on the default CPU backend, the CPU that computed the answers — with its architecture, core count and the instruction-set features XNNPACK chooses kernels by where the driver could read them; its limitations say so when the version or the CPU model could not be read, when the version differs from the pin, or when cores of more than one kind were open to the candidate.bundlecarries the runtime and CPU the manifest recorded, with the CPU's features where it has them, intoestablished_against, rather than the pin of whichever litetune packed it. - No unconstrained output under a grammar-on label. Where a bundle has no SentencePiece tokenizer, 0.16.1 refused a grammar-on conversation and 0.17.1 logs a warning and decodes without the grammar. The tool path now reads the bundle's tokenizer sections first and refuses that mode itself.
- Large integers. 0.17.1 returns a tool-call integer that fits in 64 bits as an integer;
tuneaccepts such a call. Scores compare numbers by value, so no score moves.
Re-measured on 0.17.1
Old numbers stay, labelled 0.16.1; the 0.17.1 ones go beside them in MEASUREMENTS.md. The runs differ in runtime, transport and possibly CPU — the 0.16.1 runs did not record theirs — so a difference is not attributed to the runtime.
FunctionGemma through the tool path, the same checkpoint and split: 0.9109 ±0.0221 with the grammar off and on, against 0.9125 ±0.0219 on 0.16.1; conversion cost +0.0141 ±0.0092 on 9 discordant rows, still resolved, against +0.0125 ±0.0087 on 8. A bundle re-converted with the new toolchain scored the same.
The four banking77 eight-bit pairs, the same bundles by SHA-256. Cost of conversion, with discordant rows:
dynamic_wi8_afp32, 0.16.1 |
0.17.1 | weight_only_wi8_afp32, 0.16.1 |
0.17.1 | |
|---|---|---|---|---|
| gemma-3-270m | +0.0000 (34) | −0.0067 (30) | +0.0033 (16) | −0.0033 (12) |
| Qwen3-0.6B | +0.0067 (20) | +0.0000 (10) | +0.0167 (12, resolved) | +0.0050 (5) |
| gemma-3-1b | +0.0083 (23) | +0.0100 (12) | +0.0167 (14, resolved) | +0.0000 (4) |
| Qwen2.5-0.5B | +0.0100 (16) | +0.0000 (8) | +0.0100 (10) | −0.0017 (3) |
Two costs resolved on 0.16.1 and none does on 0.17.1. That is two runs' verdicts, not a measured change in a cost, and neither run is withdrawn: litetune computes no interval between two runs.
Not yet re-measured: the four-bit recipes, Gemma 4's conversion costs, the headline FunctionGemma text-path runs and the phone runs. The README and MEASUREMENTS.md say so where those numbers appear.