Contents TandemLLM Docs
v0.9.2
Star58

Part III, How the engine works

18LFM2.5-24B-A2B

A decode token of LiquidAI’s LFM2.5-24B-A2B reads 4.91 GB of its BF16 weights and 1.31 GB of our 4.0-bit EXL3 pack (arithmetic, from the headers), because only 4 of its 64 experts run in each of 38 layers. On one DGX Spark the engine decodes it at 188.2 tok/s from the 2.5 pack, 153.5 tok/s from the default 3.5 pack and 44.8 tok/s in BF16 (measured, 2026-10-14). It is the third model TandemLLM serves and the first that is not a Qwen; its support ships in release 0.9.0. This chapter says what is different about it and what is measured; Chapter 16 describes the adapter path it came in through, and docs/lfm2.5-24b-a2b.md is the same material for the repository.

18.1 The model

Table 18.1 LFM2.5-24B-A2B beside Flash-Next.
partLFM2.5-24B-A2BFlash-Next
layers40: 30 short convolutions, 10 attention48: 36 Gated DeltaNet, 12 attention
MLP64 experts a layer, 4 run a token, no shared expert; layers 0 and 1 dense512 experts, 10 routed and 1 shared
residual streamone of 2,0484 streams of 2,560
attentionfull, 32 query and 8 KV heads of 64sparse past 2,051 tokens
state of a sequence240 KiB of convolution inputs plus 20,480 bytes a token110.3 MiB plus 29,952 bytes a token
context131,072 tokens262,144
draft layernoneone MTP layer
weights servedBF16, or our EXL3 packs at 2.5 to 6.0 bits a weightturboderp’s EXL3 packs

A short-convolution layer projects the normed input to three vectors B, C and x of 2,048, takes B times x, runs a depthwise convolution of 3 taps over that product along the sequence, multiplies by C and projects back. Its state is the last two products, so a sequence keeps 30 layers times 2 times 2,048 values. The router takes the sigmoid of each expert’s logit, adds a per-expert bias to choose the 4, and renormalises the 4 chosen sigmoids. engine/models/lfm2_moe/config.py reads the configuration and stops on a field it does not know.

The tokenizer is LiquidAI’s own (128,000 ids). The lookup corpus and the suffix store of the Qwen models hold other token ids, so the server refuses them by fingerprint and the LFM2.5 profiles keep a store of their own.

18.2 Profiles

Table 18.2 The LFM2.5 profiles, measured 2026-10-14: the download, the weights on the device at load, the decode step from its graph (tok/s), chat through the server plain / with the lookup (tok/s), and against BF16 on oasst1 chat the KL divergence and the top-1 agreement (exllamav3’s model_diff).
profilediskweightsstepchatKLtop-1
lfm25-exl3-2.58.20 GB7.64 GiB188.2185.4 / 175.60.31378.9 %
lfm25-exl3-3.09.68 GB9.4 GiB175.0171.3 / 168.50.19583.6 %
lfm25-exl3-3.511.16 GB10.56 GiB153.5150.9 / 143.70.13686.7 %
lfm25-exl3-4.012.64 GB12.06 GiB145.9143.7 / 139.40.09888.5 %
lfm25-exl3-4.514.12 GB13.31 GiB128.8128.2 / 124.70.06391.2 %
lfm25-exl3-6.018.63 GB17.37 GiB106.9105.2 / 103.50.03993.4 %
lfm25-bf1647.9 GB44.95 GiB44.844.5 / 43.30100 %

Every profile fits the Spark preset at the model’s whole context: its KV is 2.5 GiB at 131,072 tokens. The quality numbers are exllamav3’s model_diff against the BF16 checkpoint on 20 rows of 2,048 tokens of oasst1 chat; this engine’s own comparison (tools/lfm_quality.py, 100 rows) agrees within 0.02. They are high next to exllamav3 conversions of other models at the same rates: the same recipe on the 8B sibling at 4.10 bits came within 0.01 of a reference conversion, so the 24B is harder at the same rate, for reasons not tested yet. The default is lfm25-exl3-3.5: the fastest profile in chat as it serves (the lookup on) whose chat top-1 agreement is at least 85 % (86.7 % at 143.7 tok/s; 3.0 is faster at 83.6 %).

Chat decode speed against chat top-1 agreement with BF16, every size (measured, 2026-10-14; one request, 512 greedy tokens; the data of Table <a class=18.2). The default, 3.5, is the fastest at 85 % or more.">
Figure 18.1 Chat decode speed against chat top-1 agreement with BF16, every size (measured, 2026-10-14; one request, 512 greedy tokens; the data of Table 18.2). The default, 3.5, is the fastest at 85 % or more.
Chat KL divergence and top-1 agreement against BF16 by size (exllamav3’s model_diff on oasst1 chat).
Figure 18.2 Chat KL divergence and top-1 agreement against BF16 by size (exllamav3’s model_diff on oasst1 chat).

Below 4 bits a weight, exllamav3 1.6.0 gives some tensors half-integer rates (1.5, 2.5 or 3.5 bits; a tile is 16K+816K + 8 int16 wide). In such a tile an odd weight takes one bit more than an even one, so weight ii’s 16-bit window ends at bit (i+1)Ka+⌊(i+1)/2⌋(i+1)K_a + \lfloor (i+1)/2 \rfloor of the tile’s ring, KaK_a the whole part of the rate. The engine’s kernels decode these tiles as exllamav3 does, in the decode step, a verify block and the prefill.

18.3 One row path and exact rollback

The decode step and every row of a verify block run the same kernels a row at a time (engine/models/lfm2_moe/kernels.py): the norms, the short convolution over the row’s own window, q and k with their rotary, attention over the row’s visible keys in fixed chunks of 512 keys, and the router. The EXL3 projections run on tools/fn_kernels.dense and the experts on its MoE bank kernels, which Flash-Next uses too; the BF16 projections run on a 16-row tile GEMM. So a row of a verify block has the bits of the decode step at that position. The load-time self-check proves it on the device: a recorded decode of 24 tokens verified as one block, every row bit for bit, before speculation turns on.

A verify block writes nothing into the convolution state. The commit takes the new state from the last two inputs along the accepted path, which are copies of the block’s own rows, and moves the path’s KV rows down. A rollback is exact by construction.

Table 18.3 The correctness ladder. measured, 2026-10-14.
checkresult
reference forward against transformers, a tiny random model in fp32within 2.1e-5 over a prefill and 3 decode steps
BF16 engine against transformers in BF16, 8 chat prompts, 1,024 positionstop token 98.05 %; 19 of the 20 differences near ties; KL 0.0064
4.0 engine against exllamav3’s own forward, the same tokenstop token 97.85 %; all 22 differences near ties
trellis decode against exllamav3’s LinearEXL3bit for bit

18.4 Speed

The decode step from its CUDA graph, one greedy request: 188.2 tok/s on 2.5, 145.9 on 4.0 (191 GB/s of the bytes a token reads), 44.8 in BF16 (220 GB/s) (measured). Each pack has its own dense splits (ops/lfm25-dense-splits.json; 4.0: 136.1 tok/s without them). Eager launches cost 5 %. An 8,192-token prompt prefills at 7,360 tok/s on 4.0.

Each verify row adds about 3 ms, because a block reads the union of its rows’ experts (Table 18.4). A block pays when it is accepted for about 1.4 tokens a row.

Table 18.4 A chain block of nn rows on 4.0 from its graph, the median of 20 replays at 1k tokens of context, and the experts its rows read over the 38 MoE layers (of 2,432). measured, 2026-10-14.
rows1234681216
ms7.4210.6213.7916.8621.4027.4736.1745.88
experts152259333391491563664735

The model has no draft layer. The server drafts with the lookup alone, through the shared tree drafter with no head (NoHead), cut by StairCut with each profile’s price file (ops/prices/lfm2.5-24b-a2b/). A round with no lookup match is the plain step and is priced as one; each round’s own match decides through the cut’s calibrated acceptance; the lookup’s confidence carries its match length. On 4.0 with the store off, edit and copy asks run at 291.1 and 326.4 tok/s against 141.3 and 141.9 plain; prose, chat and code run within 5 % of plain; every text equals the plain one (measured). The 20 requests of tools/fn_server_check.py give the same answers with and without the lookup, largest logprob difference 0.0.

Chat plain against edit and copy asks with the lookup, every size (measured, 2026-10-14).
Figure 18.3 Chat plain against edit and copy asks with the lookup, every size (measured, 2026-10-14).

18.5 Serving

The chat template is the checkpoint’s own and opens a reasoning block at the end of every prompt; a client that turns thinking off gets the block closed right after the generation prompt. Tool calls are a Python list between <|tool_call_start|> and <|tool_call_end|>; server/toolcall.py rewrites each block into the parser’s JSON form as the text arrives, with values read as Python literals and never run, so a client gets OpenAI tool_calls. Liquid’s recommended sampling is the default: temperature 0.1, top_k 50, repetition penalty 1.05. The Setup Guide turns the conductor on, with the model fit below.

18.5.1 The conductor

The conductor, Laya and the narrator are on by default. The model’s adapter declares how the router fits it (Capabilities.conductor, QWEN38\_CONDUCTOR\_FIT to switch it off): Laya alone answers the class question, since the model’s own one-word answers with thinking off are no class (0 to 2 of 8 right); a turn nobody classified thinks at the profile’s default level with nothing added; a class sets the level and the budget with only its core skill in the note. The router’s notes sit at the tail of the user’s message, and this model reads them as part of the request: the same note texts cost 2.7 times the raw thinking there and 1.38 times as the system message. On 2.5, four short chat asks thought 2,234 reasoning tokens against 2,155 raw and 9,098 under the router as written; seven Chat-tab tasks run twice succeeded 14 of 14 with 9,850 reasoning tokens, against 12 of 14 and 68,170. Raw runs: models/lfm2.5-24b-a2b/measurements/conductor-0.9.0.json.

Caches: with the prefix resumed at 4,096 and 12,288 tokens, warm answers equal cold ones with equal logprobs (tools/fn_cache_check.py). The session spill drains 13,650 tokens in 309 ms and reads them back in 163 ms; the disk tier restores 12,288 tokens in 0.96 s against 2.6 s cold; both give the cold answer (measured). Parallel: three slots over one copy of the weights; a slot’s plain step joins the round as one row, from a graph when every slot takes one. On 4.0, 1, 2 and 3 requests at once decode 139.9, 169.1 and 177.8 tok/s in total, every answer equal to its request alone (measured).

18.6 What is not measured

  • The cause of the 24B’s higher KL than its 8B sibling at the same rate.
  • Any task benchmark, and the benchmark row of Chapter 15.
  • The KV pool on this model: off in its profiles (QSE\_KV\_POOL=off), so each slot reserves its window at load.

Not supported yet: a draft head (none is published for this model) and the shadow reader, which reads Qwen’s token ids and stays off.