Custom converter: open-weight Hugging Face LLMs/VLMs -> on-device LiteRT-LM (.litertlm), with one-command reproduction
19
stars
168
commits
Python
primary language
Sep 9, 2026
updated
Convert open-weight Hugging Face models to .litertlm bundles for the LiteRT-LM
runtime (CPU/GPU on iOS, Android, desktop). Two things live here:
python scripts/convert.py <org>/<model> — one command from Hub
id to a gated bundle. It covers finetunes of Qwen3.5, LFM2.5, MiniCPM5, granite-4.0-h,
Falcon-H1, Nemotron-H/Nemotron-3-Nano, and every dense architecture the stock exporter
handles — about 2,670 tagged derivatives on the Hub as of 2026-08-26. LoRA/PEFT repos
merge automatically. Every bundle is gated before it is called done; broken models are
refused with a machine-readable reason.pip install litert-torch ai-edge-quantizer "transformers==5.14.*" huggingface_hub litert-lm
export PY=python # scripts default to ~/venvs/ltconv040dev/bin/python; override with PY
One family needs a different stack: Qwen3.5 exports only on litert-torch main
(pip install 'litert-torch @ git+https://github.com/google-ai-edge/litert-torch.git' in a
fresh venv — the released 0.9.4 ships the exportables but its output is degenerate).
convert.py refuses with the exact install command when the installed toolchain can't
convert the model honestly, so you can start without reading further.
python scripts/convert.py <org>/<model> # -> out/<model>/ bundle + convert_report.json
python scripts/convert.py <org>/<model> --int4 # proven int4 recipe (blockwise-32 OCTAV)
python scripts/convert.py <org>/<model> --gate-script my_gate.py # task-specific models
One run does five things:
auto_map for a model_type transformers
now registers natively is not remote-code: the pinned library implementation loads and
the repo's Python is never imported, so it converts (measured on Nemotron-3-Nano-4B).add_bos_token: False (or bos == eos) and the template
never renders a leading BOS; the ExecutorMetadata section is retrofitted where the
exporter omits it.verify_quality.py (8 questions, bar 6/8, think-aware budget) — or your
--gate-script for models the generic gate cannot certify (an Arabic diacritizer answers
no trivia). Exit 0 = converted and gated, 1 = converted but gate failed, 2 = refused.Routing is automatic, by config.json model_type. What the converter accepts, with the
Hub derivative counts behind the coverage claim (recounted 2026-08-26 via base_model
tags over the bases named in each row; mirrors included):
| base family | bases | Hub finetunes + adapters | toolchain | path |
|---|---|---|---|---|
| any dense arch the stock exporter handles | llama 3.x, qwen 2/2.5/3, smollm3, olmo2, phi, ministral, … | open-ended | default stack | stock export |
| MiniCPM5 | 1B | 53 + 54 | default stack | stock export (plain llama rail) |
| granite-4.1 (dense) | 3b | 20 + 15 | default stack | stock export; spurious-BOS guard fires (bos == eos) |
| Hy-MT2 (hunyuan_v1_dense) | 1.8B | 10 + 1 | default stack | stock export after a bitwise-equal rope bake; duplicate-BOS guard fires |
| Qwen3.5 | 0.8B / 2B / 4B | 1,214 + 928 | litert-torch main | stock export; CPU gate |
| LFM2.5 | 350M / 1.2B / 2.6B | 210 + 94 | released 0.9.3/0.9.4 | stock export + ExecutorMetadata retrofit |
| granite-4.0-h | 350m / 1b | 24 + 4 | pinned checkout | family recipe (HYBRID_RECIPE) |
| Falcon-H1 | 0.5B / 1.5B / 1.5B-Deep / 3B | 14 + 2 | pinned checkout | family recipe (HYBRID_RECIPE) |
| Zamba2 | 1.2B / 2.7B | 3 + 0; the only real one is pre-port-serialized | pinned checkout | routed; pre-port checkpoints are refused with re-serialization instructions |
| Nemotron-H | Nemotron-H-4B / Nemotron-3-Nano-4B | 19 + 14 | pinned checkout | family recipe (HYBRID_RECIPE); measured on Nemotron-3-Nano-4B (shipped) |
For HYBRID_RECIPE families the one-time checkout setup command is printed on refusal.
The measurements behind each row — template byte-equality, greedy A/B against the HF
reference, and three gate refusals of genuinely defective derivatives — are in
REPRODUCE.md.
iPhone. A converted bundle loads in an existing iOS app through swift-litert-lm: recipe (one SwiftPM dependency, LiteRTChat(huggingFaceRepo:fileName:) or LiteRTChat(modelFileURL:), stop and release, a verify command with its expected output).
VLM derivatives. bash scripts/ship_qwen2vl_derivative.sh <org>/<model> builds the full
bundle. Qwen2-VL derivatives train the vision tower too (measured: 339/391 vision tensors
differ on the top derivative), so vision re-exports from the derivative's own weights. The
script ends with a tokenizer-parity gate (scripts/gate_specials.py: every added-token special,
Latin-1, emoji — the engine's encode against the upstream tokenizer.json). The 2026-08 failure
on a format-exact derivative, first blamed on the runtime, was the bundle's own SentencePiece
section; the gate is what catches it, and REPRODUCE.md carries the correction.
bash scripts/reproduce_llm.sh --list # 21 LLM keys
bash scripts/reproduce_llm.sh olmo2-1b # -> out/olmo2-1b/model.litertlm
bash scripts/reproduce_vlm.sh --list # 13 VLMs
bash scripts/reproduce_vlm.sh ovis2.5-2b # -> out/*-bundle/Ovis2.5-2B.litertlm
The 2026-08 verification sweep executed every then-current LLM recipe end-to-end and gated
it: 16/18 reproduced and passed (the two exceptions are documented — one source repo
went gated, one thinking model the strict gate over-flags). For one model the reproduced
weights are bit-identical to the published artifact. Per-model recipes, caveats, and
device measurements: REPRODUCE.md; per-model cards: cards/.
What the lists contain:
reproduce_llm.sh (21): llama32-3b, qwen3-1.7b, qwen3-4b-thinking,
qwen25-3b, ministral3-3b (+-reasoning), olmo2-1b/7b, smollm3-3b, twil-lm3,
phi4-mini-reasoning, r1-distill-qwen-1.5b/7b, nanbeige4.1-3b, nanbeige4.2-3b,
polaris-4b, vibethinker-3b, jan-nano, fastcontext-4b, falcon3-3b, s1-mini.reproduce_vlm.sh (13): granite-docling-258m, internvl3-1b,
internvl3.5-1b/2b/4b, llava-onevision-0.5b, mage-vl, north-micro-vision,
ovis2.5-2b, paddleocr-vl-1.6, qwen2-vl-2b, smolvlm2-500m, smolvlm2-2.2b..tflite encoders — plus
Qwen3-TTS-12Hz-0.6B (speech) and Bonsai-Image-ternary-4B (FLUX.2-klein image generation),
which run as LiteRT graphs under host loops rather than .litertlm bundles.EXTERNALIZE_EMBEDDER=1 CACHE=4096 $PY scripts/export_simple_template.py <hf_id> out/<name> templates/<t>.jinja BOCTAV4
(pick a template from templates/; FORCE_SPM=1 for thinking models with added tokens).
Then add a case to scripts/reproduce_llm.sh to keep it reproducible.scripts/ship_*.sh with its
convert_*_vision.py / prep_*_decoder.py, adjust dims and the image token.
ovis_work/ shows how to make a dynamic-resolution (NaViT) vision tower export-able.granite_work/,
falcon_h1_work/, zamba2_work/, and nemotron_h_work/ are the working examples — each
is one litert-torch patch plus one driver script.Defined in scripts/export_simple_template.py:
| recipe | what | when |
|---|---|---|
BOCTAV4 | blockwise-32 int4 + OCTAV optimal clipping + int8 embedding | best quality (Mac/Android) |
BOCTAV4_128 | blockwise-128 variant | 4B models / iOS (~2 GiB section limit) |
BMIX4[_128] | blockwise int4 min-max + int8 embedding | GPTQ ingest, or when OCTAV isn't needed |
OCTAV is data-free (no calibration set). EXTERNALIZE_EMBEDDER=1 splits the embedding so 3B+
models load under the iOS section limit; reasoning models use a thinking template and
CACHE=4096.
A .litertlm bundle carries the conversation contract in its header, but nothing
machine-readable says which of a repo's files fits which device, backend, and RAM budget, at
what measured speed. manifest/ defines a repo-level litertlm_manifest.json for that
deployment layer: manifest/SCHEMA.md (spec),
manifest/make_manifest.py (generator),
manifest/examples/ (finished manifests for two of the published repos — 60 repos ship one as of 2026-09-05), and readers/ (dependency-free TypeScript and Dart reference readers).
The schema is registered in SchemaStore, so editors that use its catalog (VS Code, JetBrains, and others) validate and autocomplete any file named litertlm_manifest.json with no setup.
The same manifest drives Google Play delivery: tools/play_ai_pack/ turns
it into a Play for On-device AI pack — one device group per manifest recommendation, the
device-targeting XML, and a host app that fetches the pack and runs the file on the backend the
manifest names.
| path | what |
|---|---|
scripts/convert.py | the finetune converter (entry gate → export → guards → exit gate) |
scripts/export_simple_template.py | the LLM engine (template + quant recipe + env knobs) |
scripts/reproduce_llm.sh · scripts/reproduce_vlm.sh | one-command reproductions |
scripts/ship_*.sh, convert_*_vision.py, prep_*_decoder*.py, build_*_bundle.py | the VLM pipeline |
granite_work/ falcon_h1_work/ zamba2_work/ nemotron_h_work/ | pinned hybrid-family recipes |
qwen35_work/ lfm_work/ minicpm_work/ … | per-family recipes and gates |
templates/, recipes/ | chat templates + quant recipe JSONs |
cards/ | model cards for the converted bundles |
| REPRODUCE.md | the full measurement record behind every claim above |
Code: Apache-2.0. Converted model bundles inherit their base model's license.
This is an independent open-source project, not affiliated with or endorsed by Google.
LiteRT and LiteRT-LM are Google projects; .litertlm names their runtime's bundle format.
168 commits
Python
85.5%
Shell
4.6%
Swift
3.5%
Kotlin
2.1%
Objective-C++
1.6%
Custom converter: open-weight Hugging Face LLMs/VLMs -> on-device LiteRT-LM (.litertlm), with one-command reproduction
19
stars
168
commits
Python
primary language
Sep 9, 2026
updated
Convert open-weight Hugging Face models to .litertlm bundles for the LiteRT-LM
runtime (CPU/GPU on iOS, Android, desktop). Two things live here:
python scripts/convert.py <org>/<model> — one command from Hub
id to a gated bundle. It covers finetunes of Qwen3.5, LFM2.5, MiniCPM5, granite-4.0-h,
Falcon-H1, Nemotron-H/Nemotron-3-Nano, and every dense architecture the stock exporter
handles — about 2,670 tagged derivatives on the Hub as of 2026-08-26. LoRA/PEFT repos
merge automatically. Every bundle is gated before it is called done; broken models are
refused with a machine-readable reason.pip install litert-torch ai-edge-quantizer "transformers==5.14.*" huggingface_hub litert-lm
export PY=python # scripts default to ~/venvs/ltconv040dev/bin/python; override with PY
One family needs a different stack: Qwen3.5 exports only on litert-torch main
(pip install 'litert-torch @ git+https://github.com/google-ai-edge/litert-torch.git' in a
fresh venv — the released 0.9.4 ships the exportables but its output is degenerate).
convert.py refuses with the exact install command when the installed toolchain can't
convert the model honestly, so you can start without reading further.
python scripts/convert.py <org>/<model> # -> out/<model>/ bundle + convert_report.json
python scripts/convert.py <org>/<model> --int4 # proven int4 recipe (blockwise-32 OCTAV)
python scripts/convert.py <org>/<model> --gate-script my_gate.py # task-specific models
One run does five things:
auto_map for a model_type transformers
now registers natively is not remote-code: the pinned library implementation loads and
the repo's Python is never imported, so it converts (measured on Nemotron-3-Nano-4B).add_bos_token: False (or bos == eos) and the template
never renders a leading BOS; the ExecutorMetadata section is retrofitted where the
exporter omits it.verify_quality.py (8 questions, bar 6/8, think-aware budget) — or your
--gate-script for models the generic gate cannot certify (an Arabic diacritizer answers
no trivia). Exit 0 = converted and gated, 1 = converted but gate failed, 2 = refused.Routing is automatic, by config.json model_type. What the converter accepts, with the
Hub derivative counts behind the coverage claim (recounted 2026-08-26 via base_model
tags over the bases named in each row; mirrors included):
| base family | bases | Hub finetunes + adapters | toolchain | path |
|---|---|---|---|---|
| any dense arch the stock exporter handles | llama 3.x, qwen 2/2.5/3, smollm3, olmo2, phi, ministral, … | open-ended | default stack | stock export |
| MiniCPM5 | 1B | 53 + 54 | default stack | stock export (plain llama rail) |
| granite-4.1 (dense) | 3b | 20 + 15 | default stack | stock export; spurious-BOS guard fires (bos == eos) |
| Hy-MT2 (hunyuan_v1_dense) | 1.8B | 10 + 1 | default stack | stock export after a bitwise-equal rope bake; duplicate-BOS guard fires |
| Qwen3.5 | 0.8B / 2B / 4B | 1,214 + 928 | litert-torch main | stock export; CPU gate |
| LFM2.5 | 350M / 1.2B / 2.6B | 210 + 94 | released 0.9.3/0.9.4 | stock export + ExecutorMetadata retrofit |
| granite-4.0-h | 350m / 1b | 24 + 4 | pinned checkout | family recipe (HYBRID_RECIPE) |
| Falcon-H1 | 0.5B / 1.5B / 1.5B-Deep / 3B | 14 + 2 | pinned checkout | family recipe (HYBRID_RECIPE) |
| Zamba2 | 1.2B / 2.7B | 3 + 0; the only real one is pre-port-serialized | pinned checkout | routed; pre-port checkpoints are refused with re-serialization instructions |
| Nemotron-H | Nemotron-H-4B / Nemotron-3-Nano-4B | 19 + 14 | pinned checkout | family recipe (HYBRID_RECIPE); measured on Nemotron-3-Nano-4B (shipped) |
For HYBRID_RECIPE families the one-time checkout setup command is printed on refusal.
The measurements behind each row — template byte-equality, greedy A/B against the HF
reference, and three gate refusals of genuinely defective derivatives — are in
REPRODUCE.md.
iPhone. A converted bundle loads in an existing iOS app through swift-litert-lm: recipe (one SwiftPM dependency, LiteRTChat(huggingFaceRepo:fileName:) or LiteRTChat(modelFileURL:), stop and release, a verify command with its expected output).
VLM derivatives. bash scripts/ship_qwen2vl_derivative.sh <org>/<model> builds the full
bundle. Qwen2-VL derivatives train the vision tower too (measured: 339/391 vision tensors
differ on the top derivative), so vision re-exports from the derivative's own weights. The
script ends with a tokenizer-parity gate (scripts/gate_specials.py: every added-token special,
Latin-1, emoji — the engine's encode against the upstream tokenizer.json). The 2026-08 failure
on a format-exact derivative, first blamed on the runtime, was the bundle's own SentencePiece
section; the gate is what catches it, and REPRODUCE.md carries the correction.
bash scripts/reproduce_llm.sh --list # 21 LLM keys
bash scripts/reproduce_llm.sh olmo2-1b # -> out/olmo2-1b/model.litertlm
bash scripts/reproduce_vlm.sh --list # 13 VLMs
bash scripts/reproduce_vlm.sh ovis2.5-2b # -> out/*-bundle/Ovis2.5-2B.litertlm
The 2026-08 verification sweep executed every then-current LLM recipe end-to-end and gated
it: 16/18 reproduced and passed (the two exceptions are documented — one source repo
went gated, one thinking model the strict gate over-flags). For one model the reproduced
weights are bit-identical to the published artifact. Per-model recipes, caveats, and
device measurements: REPRODUCE.md; per-model cards: cards/.
What the lists contain:
reproduce_llm.sh (21): llama32-3b, qwen3-1.7b, qwen3-4b-thinking,
qwen25-3b, ministral3-3b (+-reasoning), olmo2-1b/7b, smollm3-3b, twil-lm3,
phi4-mini-reasoning, r1-distill-qwen-1.5b/7b, nanbeige4.1-3b, nanbeige4.2-3b,
polaris-4b, vibethinker-3b, jan-nano, fastcontext-4b, falcon3-3b, s1-mini.reproduce_vlm.sh (13): granite-docling-258m, internvl3-1b,
internvl3.5-1b/2b/4b, llava-onevision-0.5b, mage-vl, north-micro-vision,
ovis2.5-2b, paddleocr-vl-1.6, qwen2-vl-2b, smolvlm2-500m, smolvlm2-2.2b..tflite encoders — plus
Qwen3-TTS-12Hz-0.6B (speech) and Bonsai-Image-ternary-4B (FLUX.2-klein image generation),
which run as LiteRT graphs under host loops rather than .litertlm bundles.EXTERNALIZE_EMBEDDER=1 CACHE=4096 $PY scripts/export_simple_template.py <hf_id> out/<name> templates/<t>.jinja BOCTAV4
(pick a template from templates/; FORCE_SPM=1 for thinking models with added tokens).
Then add a case to scripts/reproduce_llm.sh to keep it reproducible.scripts/ship_*.sh with its
convert_*_vision.py / prep_*_decoder.py, adjust dims and the image token.
ovis_work/ shows how to make a dynamic-resolution (NaViT) vision tower export-able.granite_work/,
falcon_h1_work/, zamba2_work/, and nemotron_h_work/ are the working examples — each
is one litert-torch patch plus one driver script.Defined in scripts/export_simple_template.py:
| recipe | what | when |
|---|---|---|
BOCTAV4 | blockwise-32 int4 + OCTAV optimal clipping + int8 embedding | best quality (Mac/Android) |
BOCTAV4_128 | blockwise-128 variant | 4B models / iOS (~2 GiB section limit) |
BMIX4[_128] | blockwise int4 min-max + int8 embedding | GPTQ ingest, or when OCTAV isn't needed |
OCTAV is data-free (no calibration set). EXTERNALIZE_EMBEDDER=1 splits the embedding so 3B+
models load under the iOS section limit; reasoning models use a thinking template and
CACHE=4096.
A .litertlm bundle carries the conversation contract in its header, but nothing
machine-readable says which of a repo's files fits which device, backend, and RAM budget, at
what measured speed. manifest/ defines a repo-level litertlm_manifest.json for that
deployment layer: manifest/SCHEMA.md (spec),
manifest/make_manifest.py (generator),
manifest/examples/ (finished manifests for two of the published repos — 60 repos ship one as of 2026-09-05), and readers/ (dependency-free TypeScript and Dart reference readers).
The schema is registered in SchemaStore, so editors that use its catalog (VS Code, JetBrains, and others) validate and autocomplete any file named litertlm_manifest.json with no setup.
The same manifest drives Google Play delivery: tools/play_ai_pack/ turns
it into a Play for On-device AI pack — one device group per manifest recommendation, the
device-targeting XML, and a host app that fetches the pack and runs the file on the backend the
manifest names.
| path | what |
|---|---|
scripts/convert.py | the finetune converter (entry gate → export → guards → exit gate) |
scripts/export_simple_template.py | the LLM engine (template + quant recipe + env knobs) |
scripts/reproduce_llm.sh · scripts/reproduce_vlm.sh | one-command reproductions |
scripts/ship_*.sh, convert_*_vision.py, prep_*_decoder*.py, build_*_bundle.py | the VLM pipeline |
granite_work/ falcon_h1_work/ zamba2_work/ nemotron_h_work/ | pinned hybrid-family recipes |
qwen35_work/ lfm_work/ minicpm_work/ … | per-family recipes and gates |
templates/, recipes/ | chat templates + quant recipe JSONs |
cards/ | model cards for the converted bundles |
| REPRODUCE.md | the full measurement record behind every claim above |
Code: Apache-2.0. Converted model bundles inherit their base model's license.
This is an independent open-source project, not affiliated with or endorsed by Google.
LiteRT and LiteRT-LM are Google projects; .litertlm names their runtime's bundle format.
168 commits
Python
85.5%
Shell
4.6%
Swift
3.5%
Kotlin
2.1%
Objective-C++
1.6%