One Omni Model, Two GGUF Files: What LFM3-Omni-2B Really Costs in RAM, from the Tensor Headers Up
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("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
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to