TL;DR
Liquid AI's LFM3-Omni-2B takes text, images, video and audio in, and answers in text or speech. Its GGUF release is not one file but two: a language model file and a projector file called mmproj that carries the vision encoder, the audio encoder and the speech-output head. At Q4_K_M the pair is 1,391,216,800 bytes (1.296 GiB), which is 31.5% of the BF16 pair. The "4-bit" language model file averages 5.05 bits per weight, because the token embedding and 12 of the 24 feed-forward down projections are kept at Q6_K.
We read the tensor headers straight from the Hugging Face repository and did the arithmetic for weights, KV cache, images and video. Only 6 of the 24 layers grow a KV cache with context, so a full 128,000-token window costs about 1.47 GiB at fp16, not the nearly 6 GiB a plain 24-layer transformer would need. We did not run the model on a phone or laptop, so everything below is file and header math plus clearly marked estimates.
What is actually in the LFM3-Omni-2B GGUF release?
The repository LiquidAI/LFM3-Omni-2B-GGUF was created on October 27, 2026 and, when we read it on November 9, showed 299,362 downloads and 153 likes. It holds five quantizations of the language model and five matching projector files. Sizes below come from the Hugging Face API with blobs=true.
| Quantization | Language model (bytes) | mmproj (bytes) | Pair (bytes) | Pair (GiB) |
|---|---|---|---|---|
| BF16 (F16 is identical) | 3,466,628,608 | 949,841,248 | 4,416,469,856 | 4.113 |
| Q8_0 | 1,846,240,768 | 506,842,528 | 2,353,083,296 | 2.191 |
| Q4_K_M | 1,099,630,080 | 291,586,720 | 1,391,216,800 | 1.296 |
| Q4_0 | 1,049,617,920 | 270,576,544 | 1,320,194,464 | 1.230 |
Two things stand out. First, the projector is a fifth of the download at every level (20.96% at Q4_K_M), so ignoring it when you size an app undercounts by roughly a quarter of the language model file. Second, the choice of projector quantization moves the total more than most people expect: BF16 against Q4_K_M differs by 658,254,528 bytes. The README's llama-tts example pairs the Q4_K_M language model with the Q8_0 projector, which gives 1,606,472,608 bytes, 215 MB more than an all-Q4_K_M pair. The README also says -hf downloads "the vision and audio projector" without saying which quantization it picks, so check what actually lands on disk.
Why is a "4-bit" file 5.05 bits per weight?
The header lists every tensor with its type and shape, so you can sum bytes per type without downloading the weights. For the Q4_K_M language model file the tensor bytes are 1,091,380,480 across 1,728,801,600 parameters. The file is 8,249,600 bytes larger, which is tokenizer and metadata.
| Type | Parameters | Bytes | Bits per weight |
|---|---|---|---|
| Q4_K | 1,272,283,136 | 715,659,264 | 4.500 |
| Q6_K | 456,130,560 | 374,169,600 | 6.562 |
| F32 | 387,904 | 1,551,616 | 32.000 |
Q6_K covers 26.4% of the parameters but 34.3% of the bytes. Most of that is the token embedding, a 128,000 by 2,048 matrix of 262,144,000 weights that takes 215,040,000 bytes, 19.6% of the file. The rest is the ffn_down tensor in 12 of the 24 layers (layers 0, 1, 2, 5, 8, 11, 14, 17, 20, 21, 22 and 23) plus the attn_v tensor in the 5 attention layers it applies to. This is the usual K-quant "mixed" recipe: spend extra bits where rounding error does the most damage, and accept that the file is bigger than the name suggests. Across the pair, Q4_K_M is also 71,022,336 bytes larger than Q4_0.
The projector follows the same pattern at 4.92 bits per weight: 474,504,706 parameters, 391,037,952 of them in Q4_K and 81,494,016 in Q6_K, with a small F16 block.
What does the second file give you?
The projector's own metadata tells you. It declares a vision encoder (14 blocks, 1,024 wide, 16 pixel patches), an audio encoder (17 blocks, 768 wide, 128 mel bins) and a generation head for audio output (3 blocks, 1,024 wide). Vision, audio in and speech out all live there, which is why text-only chat should work with the language model file alone while anything multimodal needs -mm or -hf. Test that on your build before you rely on it.
Liquid's card says the omni model replaces a separate speech recognizer, vision-language model, language model and speech synthesizer. In deployment terms that means one runtime and one language model to serve, but still two files and 475M extra parameters to hold in memory.
How much memory does the context window cost?
The lfm3.attention.head_count_kv array in the header has a non-zero entry for only 11 of the 24 layers. Thirteen layers are short convolutions with a fixed small state. Of the 11 attention layers, 5 use a sliding window of 256 tokens and 6 attend to the full context. Each attention layer stores 4 KV heads of dimension 128, so one token costs 2 x 4 x 128 x 2 bytes = 2,048 bytes per full-attention layer at fp16, and 12,288 bytes across the 6 layers that grow.
| Context | KV cache, fp16 | Weights + KV (Q4_K_M pair) |
|---|---|---|
| 8,192 | 98.5 MiB | 1.392 GiB |
| 32,768 | 386.5 MiB | 1.673 GiB |
| 128,000 | 1,502.5 MiB | 2.763 GiB |
The sliding-window layers add a fixed 2.5 MiB and are included in those figures. If all 24 layers were full attention with the same 4 KV heads of 128, one token would cost 49,152 bytes and 128,000 tokens would need 5.86 GiB. That comparison is hypothetical, but it shows what the hybrid layout buys: about 4x less cache. A quantized KV cache can roughly halve the remainder. A q8_0 cache stores 8.5 bits per element against 16, which puts the 128,000-token cache near 798 MiB. llama.cpp exposes --cache-type-k and --cache-type-v for this, and a quantized V cache needs flash attention enabled, so confirm your backend supports it.
These totals count weights and KV only. Compute buffers, the encoders' activations, the audio codec path and the runtime are not in them, and we did not measure those.
What does an image or a video do to the budget?
The projector header sets image_min_pixels to 65,536 and image_max_pixels to 2,097,152. Each token covers a 32 by 32 pixel block (16 pixel patches merged 2 by 2), so a picture costs between 64 and 2,048 tokens. That matches the model card's "up to 2,048 tokens per image", which is a useful consistency check between the card and the file. At 12,288 bytes of KV per token, the largest image adds 24 MiB of cache and the smallest 0.75 MiB. Images are cheap in KV terms. The encoder pass is the part to profile.
Video is where context fills up. The card says video is sampled at 2 fps with 4 to 384 frames, and the vision config sets temporal_patch_size to 2, so we estimate that each pair of frames costs about one image's worth of tokens. That is our assumption, not a published figure, so read the token count from your runtime's log before trusting it. Under it, a 384-frame clip (192 seconds) costs:
| Tokens per frame pair | Total tokens | KV, fp16 | Fits 128K context |
|---|---|---|---|
| 64 | 12,288 | 146.5 MiB | yes |
| 256 | 49,152 | 578.5 MiB | yes |
| 1,024 | 196,608 | 2,306.5 MiB | no |
A 512 by 512 frame is 256 tokens, so a clip of that size fits with room to spare. A frame near 1,024 by 1,024 produces 1,024 tokens per pair and overflows the window before the soundtrack is counted. The card notes that llama.cpp always uses 2 fps and 4 to 384 frames for this model and ignores --video-fps, so on that runtime you control cost through input resolution, not frame rate. Prefill time for tens of thousands of tokens is a separate limit on a phone, and we have no measurement for it.
Can you check this yourself?
The first script reads a GGUF header with an HTTP Range request and sums bytes per quantization type. It ran against the Q4_K_M files and gave the numbers above.
# Sum tensor bytes per quant type from a GGUF header, fetched with an HTTP Range request. import struct, sys, urllib.request, collections SIZES = {0: ("F32", 1, 4), 1: ("F16", 1, 2), 8: ("Q8_0", 32, 34), 12: ("Q4_K", 256, 144), 14: ("Q6_K", 256, 210), 30: ("BF16", 1, 2)} # extend for other quant types SCALAR = {0: "B", 1: "b", 2: "H", 3: "h", 4: "I", 5: "i", 6: "f", 7: "?", 10: "Q", 11: "q", 12: "d"} def header(url, n=24_000_000): req = urllib.request.Request(url, headers={"Range": f"bytes=0-{n-1}"}) return urllib.request.urlopen(req).read() def tensors(b): p = 4 def u(f): nonlocal p v = struct.unpack_from("<" + f, b, p)[0]; p += struct.calcsize("<" + f); return v def s(): nonlocal p n = u("Q"); v = b[p:p+n]; p += n; return v def skip(t): nonlocal p if t == 8: s() elif t == 9: et, n = u("I"), u("Q") for _ in range(n): skip(et) else: p += struct.calcsize("<" + SCALAR[t]) assert b[:4] == b"GGUF"; u("I"); nt, nk = u("Q"), u("Q") for _ in range(nk): s(); skip(u("I")) for _ in range(nt): name = s().decode(); dims = [u("Q") for _ in range(u("I"))]; ty = u("I"); u("Q") yield name, dims, ty if __name__ == "__main__": url = sys.argv[1] agg = collections.defaultdict(lambda: [0, 0]) for name, dims, ty in tensors(header(url)): label, block, size = SIZES[ty] n = 1 for d in dims: n *= d agg[label][0] += n; agg[label][1] += n // block * size total = sum(v[1] for v in agg.values()); params = sum(v[0] for v in agg.values()) for k, (n, by) in sorted(agg.items(), key=lambda kv: -kv[1][1]): print(f"{k:5s} params={n:>13,} bytes={by:>13,} bits/weight={by*8/n:.3f}") print(f"total bytes={total:,} params={params:,} average bits/weight={total*8/params:.3f}") Run it with python gguf_bytes.py https://huggingface.co/LiquidAI/LFM3-Omni-2B-GGUF/resolve/main/LFM3-Omni-2B-Q4_K_M.gguf. For other quantization types, add their block sizes to SIZES.
The second script derives the KV numbers from the metadata arrays instead of from a formula you have to trust.
# KV and context budget for LFM3-Omni-2B from its GGUF metadata (values read from the header). head_count_kv = [0,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0] # lfm3.attention.head_count_kv swa_pattern = [False,False,False,False,True,False,False,False,True,False,False,False,True,False,False,False, True,False,False,False,True,False,False,False] # lfm3.attention.sliding_window_pattern head_dim, window, bytes_per_elem = 128, 256, 2 # key_length, sliding_window, fp16 per_tok = sum(2 * h * head_dim * bytes_per_elem for h, swa in zip(head_count_kv, swa_pattern) if h and not swa) # full-attention layers only swa_fixed = sum(2 * h * head_dim * bytes_per_elem * window for h, swa in zip(head_count_kv, swa_pattern) if h and swa) # sliding layers, capped print(f"KV per token: {per_tok:,} B sliding-window layers, fixed: {swa_fixed/2**20:.1f} MiB") def kv_mib(ctx): return (per_tok * ctx + swa_fixed) / 2**20 def video_tokens(frames=384, tokens_per_frame_pair=256): # assumption: temporal_patch_size 2 return (frames // 2) * tokens_per_frame_pair for ctx in (8192, 32768, 128000): print(f"ctx {ctx:>7,}: KV {kv_mib(ctx):8.1f} MiB") for tpp in (64, 256, 1024): t = video_tokens(tokens_per_frame_pair=tpp) print(f"384-frame video at {tpp:>4} tokens/pair: {t:>8,} tokens, KV {kv_mib(t):8.1f} MiB, fits 128K: {t <= 128000}") What did we not measure?
We did not run the model, so there are no tokens-per-second, time-to-first-token or peak resident memory figures here. The GGUF README publishes no per-quantization quality scores that we could find, so we cannot tell you what Q4_K_M costs in word error rate against BF16. The benchmark table on the base model card is Liquid's own: it lists Daily-Omni at 70.09 against 58.81 for Qwen2.5-Omni 3B and 50.63 for Gemma 4 E2B, and English ASR WER at 6.36 against 6.30 and 5.14 for Qwen2.5-Omni 3B and Phi-4 multimodal. We did not rerun any of it. The same table lists those competitors at 6B and 5.6B total parameters against 2.2B here, so the byte math starts from a smaller base, but we did not read their GGUF headers.
What should an edge team do next?
Treat the pair as the unit you ship and test. Pick the projector quantization on purpose, because it is a 658 MB decision. Run your own ASR word error rate on the Q4_K_M and Q8_0 pairs with audio from your users, since the quality loss is unpublished. Cap context and input resolution for video features instead of relying on the 128,000-token ceiling. And profile peak memory during the image and audio encode steps on the target device, because the table above deliberately leaves them out.
FAQ
How much RAM does LFM3-Omni-2B need at Q4_K_M?
Weights plus an 8,192-token fp16 KV cache come to 1.392 GiB by our arithmetic. Compute buffers and encoder activations come on top, and we did not measure them, so leave headroom beyond that figure.
Do I need the mmproj file?
For images, audio input and speech output, yes: the file's metadata declares the vision encoder, the audio encoder and the audio generation head. For plain text chat the language model file should be enough, but verify it on your runtime.
Is Q4_K_M really 4-bit?
The language model file averages 5.05 bits per weight and the projector 4.92. Most weights are Q4_K at 4.5 bits, but the token embedding and some FFN and attention tensors are Q6_K at 6.56 bits.
Why is the KV cache so small for a 128K window?
Only 6 of the 24 layers attend to the full context. Thirteen are short convolutions and 5 use a 256-token sliding window, so one token costs 12,288 bytes of cache instead of 49,152.
Which runtimes support it?
The GGUF README names llama.cpp b11960 (v0.8.0) or newer, including a realtime voice endpoint, LM Studio with image input, and Ollama v0.40.12 or newer on macOS for image and audio input.
Are these our own measurements?
Partly. File sizes and tensor bytes were read from the Hugging Face API and the GGUF headers on November 9, 2026. The KV and video figures are our arithmetic on those headers, and the tokens-per-frame-pair figure is an assumption. Benchmark scores are Liquid's.
Further reading
- LFM3-1.7B On-Device: The 256K KV Cache Math, Which GGUF to Ship, and the Safety Test to Run First
- Kimi K3 in 8 GB of RAM: Why 28x More Memory Buys Only 1.7x Speed, and the Byte Math That Predicts It
Sources: Hugging Face repositories LiquidAI/LFM3-Omni-2B-GGUF and LiquidAI/LFM3-Omni-2B, and Liquid AI's LFM3 announcement. File sizes, tensor types and metadata were read on November 9, 2026.