On-device inference · research prototype

Molt

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.

5kills · always-large
0kills · Molt
4.0×cheaper switch than restart
5.1×transplant vs re-prefill
-166MiB over budget · 3 tenants

Measured on qwen2.5-1.5b-ladder, cpu / float32, 5 prompts per condition, identical pressure trace for every condition.

press play
 
3x
tier0 tier1 tier2 the bar is memory: fill = in use, red line = the budget
One paragraph, three models. A replay of a recorded session — every token, timestamp and migration cost comes from the raw event stream. The budget is cut mid-sentence; the background colour behind each token is the rung that produced it. Nothing restarts.
BUDGET EXCEEDED10 ms100 ms1 stier0→tier1tier1→tier2tier2→tier0generated token index →D · MoltC · restart
The whole argument in one plot. Per-token latency through the same pressure event. The shaded band is where the memory budget is exceeded. Both arms switch models at the same moment; the restart baseline pays a full re-prefill of the prompt each time, Molt carries the cache across. Log scale.
tier0 · Qwen2.5-1.5B fp32tier1 · Qwen2.5-1.5B int8tier2 · Qwen2.5-0.5B fp32one cell = one generated token, coloured by the model that produced it

The three bad options

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.

Why per-request model selection cannot fix this

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.

Measured, not asserted: the same Molt machinery restricted to switching only at request boundaries (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.

The ladder

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:

tier0
Qwen2.5-1.5B fp32
the rung you want to be on
5,889 MiB
tier1
Qwen2.5-1.5B int8
same weights, lower precision — a diagonal scale re-alignment
2,143 MiB
tier2
Qwen2.5-0.5B fp32
different depth and head_dim — a learned linear projection
1,885 MiB

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.

Results

conditionkillsworst ITLmean ITLswitchesswitch costhandoff JSDjudge agreeaccuracypeak MiB
A · Static-large52460.000.005,915
B · Static-small080580.000.7500.671,896
C · Restart-on-pressure04,5286552.23,6150.03860.9751.005,915
D · Molt04,5866062.29000.21990.8171.007,802
D⁻ · Molt, no calibration02,5615672.27550.23130.7921.005,918
D⁻ · Molt, no learned projection03,6645712.2220.19170.7171.007,799
D⁻ · Molt, no top-k recompute03,7086642.2290.22430.8171.007,799
D⁻ · Molt, request-boundary only52100.000.005,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.

The trade-off, stated plainly

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.

0.039handoff JSD · re-prefill floor
0.220handoff JSD · transplant
4.0×faster switch

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.

And one number that does not move much. The worst single token is 4,528 ms for restart against 4,586 ms for Molt — nearly the same, because in a cold-start run that token is dominated by loading the destination model from disk, which both strategies pay identically. What Molt removes is the re-prefill on top of it: 3,615 ms → 900 ms. The sweep below isolates that component with both rungs already warm.

What a switch costs

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.

routetokens carriedtransplantre-prefillspeed-upFLOPs avoided
tier0->tier212854 ms262 ms4.85×74%
tier0->tier225693 ms415 ms4.46×74%
tier0->tier2512180 ms793 ms4.41×74%
tier0->tier21,024399 ms1,746 ms4.38×74%
tier0->tier1128345 ms1,696 ms4.91×79%
tier0->tier1256421 ms2,069 ms4.91×79%
tier0->tier1512596 ms3,013 ms5.06×79%
tier0->tier11,024985 ms4,830 ms4.90×79%

Ablations

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.

Read the two quality columns together. Handoff JSD measures the seam, at one position. It is bounded, and it rewards blurring: a map that degrades the cache into a flat distribution can score a lower one-shot divergence while the text falls apart a few tokens later. Judge agreement catches that. Where the two disagree in the table below, agreement is the one to trust.
armwhat is removedjudge agreejudge pplhandoff JSDswitch costpeak MiBkills
D · Molt— (full system)0.8173.090.2199900 ms7,8020
D⁻ · Molt, no calibrationlogit blending (core #3)0.7923.690.2313755 ms5,9180
D⁻ · Molt, no learned projectionlearned projection + RoPE sandwich (core #1i, and with it the top-k recompute, which needs a learned map)0.71724.140.191722 ms7,7990
D⁻ · Molt, no top-k recomputetop-k native recompute (core #1iii)0.8173.210.224329 ms7,7990
D⁻ · Molt, request-boundary onlythe ability to switch mid-stream (core #2)0 ms5,9185

How the cache crosses

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:

  1. A learned linear projection per destination layer, inside the RoPE sandwich, plus a depth remap.
  2. A diagonal scale re-alignment when the rungs share weights and differ only in precision.
  3. Selective top-k recompute: the destination's last few layers are recomputed natively from a projected boundary hidden state, rather than projected. Verified bit-exact against a full forward pass.

Held-out fit residuals (relative RMS, on windows the map never saw):

routemapKVhidden
tier0->tier1diag0.0130.0280.006
tier0->tier2dense0.1850.4240.077
tier1->tier0diag0.0130.0280.006
tier1->tier2dense0.1850.4240.077
tier2->tier0dense0.2950.6040.054
tier2->tier1dense0.2950.6040.054

The value map is the weak one, and most of the residual post-migration quality gap lives there.

Worth one measurement, because the ablation table above looks contradictory at first: against a naive truncated map, the fitted projection has a higher handoff JSD but far better text. Running both through the same prefix (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.

Three tenants, one budget, nobody killed

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.

0forced terminations
-166MiB max overshoot
6demotions
3parks

Max overshoot is max over time of (usage − budget), sampled after enforcement at every scheduling round. Negative means the budget was never exceeded.

It runs as a service

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.

What this does not show

Stated plainly, because a prototype that hides its edges is worth less than one that marks them.


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