Android cache format¶
Where an installed model lives on device, and the rules the SDK follows when writing to it. The package schema is in MODEL_FORMAT.md; the Hub side is in HUB_PACKAGE_FORMAT.md.
Layout¶
The cache root defaults to context.filesDir. One directory per model, named by
sanitize_repo_id:
<cacheDir>/
├── org__Tiny-Model/
│ ├── mobiletransformers_manifest.json
│ ├── checksums.json # the installed variant's digests
│ ├── tokenizer/ # flattened from shared/tokenizer (+ chat_template.jinja)
│ ├── inference/
│ │ ├── model.onnx, model.onnx_data
│ │ ├── generation_config.json
│ │ ├── genai_config.json # present iff the GenAI engine is supported
│ │ ├── weight_handoff_map.json
│ │ ├── <tensor>.bin, <tensor>.bin.sha256
│ │ └── merger_*.onnx
│ ├── train/ # present iff the training feature was installed
│ │ ├── training_model.onnx, eval_model.onnx, optimizer_model.onnx
│ │ ├── training_config.json, trainable_parameters.json
│ │ ├── checkpoint/
│ │ └── training_state.json # written by the trainer, not the installer
│ └── embedding/ # present iff the RAG feature was installed
└── .staging/, .download/ # transient; removed on success
The variant is flattened away. On the Hub a package holds several variants; on device exactly one is
installed, so variants/<id>/inference/ becomes inference/. The installed variant id is recoverable
from the manifest that ships alongside it.
This is the layout LLMRepository already probed before packages existed, which is why installation is
a pure file operation and the runtime needed no changes to consume Hub models.
Install is crash-safe¶
ModelPackageInstaller (Kotlin) and hub/pull.py::install_package (Python) follow the same sequence:
- Build the complete new tree in a staging directory (
.staging/<sanitized>/.partial/<sanitized>). - Rename any existing install aside to
.retired-<sanitized>-<n>. - Rename the staged tree into place.
- Delete the retired tree.
If step 3 fails, the retired tree is renamed back — a failed update is a no-op, not data loss. The
ordering matters because a model directory can hold locally trained state (train/checkpoint,
training_state.json) that exists nowhere else: deleting the live tree before the replacement is in
place would destroy it if the process died in between.
Staging directories are always fully removed on success, so .staging//.download//.retired-* are
never part of a healthy cache.
Reading the cache¶
CacheIndex.list(cacheDir) enumerates installed models with their base model id, size and whether a
manifest is present — the backing for a package-management UI. It reads only the manifest and the
directory tree; it never loads a model.
Writes after install¶
Only two things modify a model directory after installation:
| Writer | Writes | When |
|---|---|---|
ORTTrainerNative |
train/checkpoint, train/training_state.json, train/training_logs.json |
during/after training |
weight_merger.cpp |
inference/<tensor>.bin + .bin.sha256 |
on merge |
The merger overwrites tensors in place in inference/, atomically (write to a temp file, fsync,
rename) with a refreshed checksum sidecar. There is no separate merged/ directory — the inference
graph references those exact filenames via weight_handoff_map.json, so a merge is complete the moment
the renames land. See MODEL_FORMAT.md for which
checksum wins afterwards.
Failure modes¶
All fail closed:
| Situation | Behaviour |
|---|---|
No weight_handoff_map.json |
nothing was merged — load the base weights (not an error) |
Map present, a .bin missing or checksum mismatched |
MissingArtifactException naming the tensor |
| Merged tensor's dtype/shape/size disagrees with the map | native session creation fails; no fallback to base weights |
| Partial merge | the merge reports failure; the package is not presented as trained |
| Requested variant incompatible with the device | NoCompatibleVariantException before downloading |