On-device inference · research prototype
A generation that runs out of memory mid-answer does not have to die, and does not have to start over. Molt moves it onto a smaller model between two tokens and carries the KV cache across — the animal walks out of the shell.
Measured on qwen2.5-1.5b-ladder, cpu / float32, 5 prompts per condition, identical pressure trace for every condition.
On a phone or a laptop the model shares memory with everything else. When the camera app launches, the OS wants memory back now. An inference process conventionally has three choices, and all three are bad:
Molt adds a fourth: change the model between two tokens, and take the attention state with you.
Every conventional elastic-serving stack picks a model per request. That is useless here, and the reason is structural rather than an implementation detail: a long answer takes tens of seconds, an app launch takes one, so the spike lands inside the answer and the next request boundary is far away in the future. Between the two, the process is holding a footprint the OS has already decided it cannot have.
D-reqbound) was reclaimed 5 times out of 5 prompts — the same score as never adapting at all. Allowed to switch mid-stream, the identical code was reclaimed 0 times.Three rungs of one model family, sharing a tokenizer. Two of the three transplant routes are structurally different problems, which is the point of this particular ladder:
The pressure trace drops the budget to 2,002 MiB — well below what tier0 needs (5,889 MiB) — and it does so in two steps, so every rung of the ladder is exercised in a single run. The spike is timed to land mid-answer on purpose.
| condition | kills | worst ITL | mean ITL | switches | switch cost | handoff JSD | judge agree | accuracy | peak MiB |
|---|---|---|---|---|---|---|---|---|---|
| A · Static-large | 5 | 246 | — | 0.0 | 0 | — | — | 0.00 | 5,915 |
| B · Static-small | 0 | 80 | 58 | 0.0 | 0 | — | 0.750 | 0.67 | 1,896 |
| C · Restart-on-pressure | 0 | 4,528 | 655 | 2.2 | 3,615 | 0.0386 | 0.975 | 1.00 | 5,915 |
| D · Molt | 0 | 4,586 | 606 | 2.2 | 900 | 0.2199 | 0.817 | 1.00 | 7,802 |
| D⁻ · Molt, no calibration | 0 | 2,561 | 567 | 2.2 | 755 | 0.2313 | 0.792 | 1.00 | 5,918 |
| D⁻ · Molt, no learned projection | 0 | 3,664 | 571 | 2.2 | 22 | 0.1917 | 0.717 | 1.00 | 7,799 |
| D⁻ · Molt, no top-k recompute | 0 | 3,708 | 664 | 2.2 | 29 | 0.2243 | 0.817 | 1.00 | 7,799 |
| D⁻ · Molt, request-boundary only | 5 | 210 | — | 0.0 | 0 | — | — | 0.00 | 5,918 |
Latencies in milliseconds. Handoff JSD is the divergence between what the outgoing model would have said at the switch position and what the incoming model actually said — lower is a smoother seam. A killed run scores 0 on accuracy rather than dropping out of the average.
Condition C re-reads the prompt on the new model, so its handoff divergence is the irreducible floor: two different models simply disagree by that much about the next token. Molt does not reach that floor.
So the honest summary is a purchase, not a free lunch: a 4.0× cheaper switch costs 5.7× more distribution discontinuity at the seam than a full re-prefill would. Most of that gap is the value projection, which is the weakest map in the system.
Carrying the cache versus re-reading the prompt, both rungs kept warm so the comparison is about the KV work rather than about model loading, which both strategies pay identically.
| route | tokens carried | transplant | re-prefill | speed-up | FLOPs avoided |
|---|---|---|---|---|---|
| tier0->tier2 | 128 | 54 ms | 262 ms | 4.85× | 74% |
| tier0->tier2 | 256 | 93 ms | 415 ms | 4.46× | 74% |
| tier0->tier2 | 512 | 180 ms | 793 ms | 4.41× | 74% |
| tier0->tier2 | 1,024 | 399 ms | 1,746 ms | 4.38× | 74% |
| tier0->tier1 | 128 | 345 ms | 1,696 ms | 4.91× | 79% |
| tier0->tier1 | 256 | 421 ms | 2,069 ms | 4.91× | 79% |
| tier0->tier1 | 512 | 596 ms | 3,013 ms | 5.06× | 79% |
| tier0->tier1 | 1,024 | 985 ms | 4,830 ms | 4.90× | 79% |
Each mechanism removed on its own, same trace, same prompts. Reported as measured, including where a mechanism did not pay for itself — an ablation table that only ever confirms the design is not an ablation table.
| arm | what is removed | judge agree | judge ppl | handoff JSD | switch cost | peak MiB | kills |
|---|---|---|---|---|---|---|---|
| D · Molt | — (full system) | 0.817 | 3.09 | 0.2199 | 900 ms | 7,802 | 0 |
| D⁻ · Molt, no calibration | logit blending (core #3) | 0.792 | 3.69 | 0.2313 | 755 ms | 5,918 | 0 |
| D⁻ · Molt, no learned projection | learned projection + RoPE sandwich (core #1i, and with it the top-k recompute, which needs a learned map) | 0.717 | 24.14 | 0.1917 | 22 ms | 7,799 | 0 |
| D⁻ · Molt, no top-k recompute | top-k native recompute (core #1iii) | 0.817 | 3.21 | 0.2243 | 29 ms | 7,799 | 0 |
| D⁻ · Molt, request-boundary only | the ability to switch mid-stream (core #2) | — | — | — | 0 ms | 5,918 | 5 |
HuggingFace caches keys after RoPE. Two rungs with different head_dim rotate by different angles, so no position-independent matrix can map one cache onto the other — the required map would depend on each token's absolute position. Molt therefore sandwiches the learned matrix between an un-rotation at the source's angles and a re-rotation at the destination's. That one detail is what makes a cross-size transplant well-posed at all.
Three mechanisms, fitted offline by closed-form ridge regression on a few thousand tokens of generic text:
Held-out fit residuals (relative RMS, on windows the map never saw):
| route | map | K | V | hidden |
|---|---|---|---|---|
| tier0->tier1 | diag | 0.013 | 0.028 | 0.006 |
| tier0->tier2 | dense | 0.185 | 0.424 | 0.077 |
| tier1->tier0 | diag | 0.013 | 0.028 | 0.006 |
| tier1->tier2 | dense | 0.185 | 0.424 | 0.077 |
| tier2->tier0 | dense | 0.295 | 0.604 | 0.054 |
| tier2->tier1 | dense | 0.295 | 0.604 | 0.054 |
The value map is the weak one, and most of the residual post-migration quality gap lives there.
scripts/diagnose_projection.py) shows why — the naive map's cache has a reconstruction error of 1.31 (worse than predicting zeros) and induces attention 11.4× flatter than the destination model's own, against 1.5× for the fitted map. That flatness is exactly the blur that flatters a bounded divergence.A foreground conversation and two background batch jobs share one moving budget. The scheduler has four levers, applied synchronously, before anyone takes a step, in order of increasing harm: defer admission, demote a rung, park the weights while keeping the cache, and finally shed the oldest context. Terminating is not on the list.
Max overshoot is max over time of (usage − budget), sampled after enforcement at every scheduling round. Negative means the budget was never exceeded.
The prototype ships a streaming server whose elasticity is visible on the wire: every token event names the rung that produced it, and a switch arrives as its own event rather than as a surprise. Below is a real session — not a mock-up — with the budget cut to 2651 MiB eight tokens into the answer.
[start on tier0 · 17 prompt tokens · TTFT 3122 ms]
An operating system reclaims memory from background
[budget cut to 2651 MiB]
processes
⇆ tier0 → tier1 (molt, 260 ms, 79% FLOPs saved, pressure 0.81)
to free up resources and improve system performance. When a process is
no longer actively using memory, the operating system can reclaim that
memory for other processes or applications
[done: 40 tokens · rungs used tier0, tier1 · 1 migration costing 260 ms · killed=False]
The sentence “…reclaims memory from background processes to free up resources…” is written by two different models. The word before the switch and the word after it are separated by a 260 ms migration and nothing else — no re-prefill, no restart, no dropped connection.
Stated plainly, because a prototype that hides its edges is worth less than one that marks them.
ln 2 on real text), judge perplexity alone (rewards degenerate repetition), and a single-needle recall task (every condition scored 1.00, including the ones that were killed). The replacements are what the table reports.Molt · elastic on-device inference · research prototype
Every number on this page is read directly from the benchmark's own output; the page is generated, not transcribed.
Ladder qwen2.5-1.5b-ladder · cpu · trace spike_mid_answer