howtospark
Recipes
?

1 × Spark125K ctx· model max57.5 tok/s@ 2KvLLMthinking on

Liquid AI's LFM2.5 8B-A1B (8.4B MoE, ~1B active) unquantized in BF16 on one DGX Spark, serving the full 128,000-token context at 57.5 tok/s single-stream.

spark .2 · single nodepeer
89 GiB free
Expert planes14.44 GiBBF16
114 usable
25* / 114 GiB
22% of usable
Expert planesDense weightsKV cacheActivations + graphsOS reserve

* segment sizes marked with an asterisk are estimates pending a measured run

Eval scores

(compare all)

Bench card

LFM2.5 8B-A1B (Liquid AI) — BF16
2K8K32K
decode tok/s53.853.049.7
ttft178ms547ms2.41s
prefill tok/s11.2k14.9k13.6k
power30W34W42W

includes 1 community run · @kvncrw

median per context · o256

Contributors

Overview

LFM2.5 8B-A1B is deliberately the small end of what a DGX Spark can run, and it is worth a page for two reasons that have nothing to do with size. The first is the sequence mixer: 18 of its 24 layers are short convolutions (conv_L_cache 3) and only 6 are full attention, so the KV cache costs 12.29 KiB per token instead of the 40-80 KiB a same-size dense transformer would want. The model's entire 128,000-token native window fits in a 3 GiB pool with headroom to spare — there is no context tuning to do here, only a decision about how much of the pool to hand back. The second is what that buys you in practice: a 126,026-token prompt answers in 13.5 seconds on one node, and decode only falls from 57.5 tok/s at a 2,048-token prompt to 44.9 at 98,528 — a 22% drop across a 48x range of prompt length. The honest counterweight is that 57.5 tok/s is not the runaway number the parameter count suggests. Decode on a Spark is bandwidth-bound, and summing this checkpoint's per-token reads gives 3.37 GB: 1,938 MB of routed experts (top-4 of 32), 604 MB of short-conv projections, 524 MB of tied lm_head, and 302 MB of everything else. Against the Spark's 273 GB/s that is an 81 tok/s ceiling, and the measured 57.5 sits at 71% of it — a healthy, well-configured fraction, not a misconfiguration. An 8B MoE is small in memory, not proportionally small in bytes read per token. Reasoning is always on: the chat template exposes no thinking toggle, so every response opens with a <think> block that counts against your output budget.

  • One DGX Spark, no tensor parallelism, no quantization step — the vendor BF16 checkpoint served as-is
  • 57.5 tok/s single-stream decode at a 2,048-token prompt (vLLM 0.26.0, warm, greedy)
  • The full 128,000-token native context, verified with a 126,026-token request that answered in 13.5 s
  • 24.7 GiB of serving working set on a 114 GiB node — 16.9 GB of weights plus a 3 GiB KV pin
  • Only 6 of 24 layers are full attention, so KV costs 12.29 KiB/token and 128K context is effectively free
  • Prefill peaks at 14,600 tok/s; TTFT is 192 ms at 2K, 2.4 s at 32K, 9.6 s at 98K
  • No speculative decoding — the checkpoint ships no MTP head and no EAGLE/DFlash draft exists for it

Software requirements

  • NVIDIA driver 580.159.03 (CUDA 13 capable) — the stock DGX Spark image
  • vLLM 0.26.0 with torch 2.11.0+cu130. Lfm2MoeForCausalLM is registered natively; no patch, no --trust-remote-code, no out-of-tree plugin
  • vLLM 0.24.0 also registers the architecture if that is what you have installed; the numbers on this page were measured on 0.26.0
  • Last verified 2026-07-27 on vLLM 0.26.0
  • ~17 GB of free disk for the checkpoint

Quick start

  1. 1

    Free the node first

    Everything below assumes one DGX Spark with nothing else resident. Unified memory means a leftover server does not merely slow you down, it changes what fits.

    pkill -f "[v]llm serve"
    ray stop --force 2>/dev/null || true
    free -gbash

    Use the [v] character class. A plain `pkill -f "vllm serve"` run over SSH matches its own remote command and kills your session.

  2. 2

    Download the checkpoint

    16.9 GB across 5 safetensors shards, plus the tokenizer and chat template. Nothing else is needed — there is no draft model and no separate quantized artifact.

    hf download LiquidAI/LFM2.5-8B-A1B --local-dir ~/models/hf/LFM2.5-8B-A1B
    
    # confirm the tree is complete before serving
    find ~/models/hf/LFM2.5-8B-A1B -name '*.incomplete' | wc -l   # must be 0
    du -sh ~/models/hf/LFM2.5-8B-A1B                              # ~16 GBbash
  3. 3

    Serve it

    One node, no parallelism. The two flags that matter are the context length and the pinned KV pool; everything else is stock.

    export PATH="$HOME/venvs/vllm-026/bin:$HOME/.local/bin:/usr/local/cuda/bin:$PATH"
    export VLLM_CACHE_ROOT=$HOME/.cache/vllm-lfm25
    export MAX_JOBS=4 TORCHINDUCTOR_COMPILE_THREADS=2
    
    vllm serve ~/models/hf/LFM2.5-8B-A1B \
      --served-model-name LiquidAI/LFM2.5-8B-A1B \
      --max-model-len 128000 \
      --kv-cache-memory-bytes 3221225472 \
      --gpu-memory-utilization 0.90 \
      --max-num-seqs 4 \
      --max-num-batched-tokens 8192 \
      --port 8000bash

    Run it detached if you are driving the box over SSH: `setsid nohup bash serve.sh > /tmp/lfm-serve.log 2>&1 < /dev/null &`. A foreground serve dies with the connection.

  4. 4

    Confirm what the engine chose

    Two lines in the log are worth reading before you trust any number: the MoE backend it auto-selected, and the KV pool it actually built.

    grep -E 'MoE backend|attention backend|Model loading took|KV cache size|Maximum concurrency' /tmp/lfm-serve.logbash

    Expected on a GB10 Spark: `Using FlashInfer CUTLASS Unquantized MoE backend`, `Using FLASH_ATTN attention backend`, `Model loading took 15.8 GiB`, `GPU KV cache size: 262,045 tokens`, `Maximum concurrency for 128,000 tokens per request: 2.05x`. Do not pass --moe-backend; the auto-selection is correct here.

  5. 5

    Warm it before you measure anything

    The first requests after a boot pay for kernel JIT and page faults on the expert planes. Send a few hundred tokens of real traffic first — a cold pass can misreport decode by 5-10% and will make the shortest prompt look slowest.

    for i in 1 2 3; do
      curl -s http://127.0.0.1:8000/v1/chat/completions \
        -H 'Content-Type: application/json' \
        -d "{\"model\":\"LiquidAI/LFM2.5-8B-A1B\",\"messages\":[{\"role\":\"user\",\"content\":\"Count to 40 in words, one per line. Run $i\"}],\"max_tokens\":600,\"temperature\":0}" \
        -o /dev/null -w '%{time_total}s\n'
    donebash

    The tell that you are warm: decode should fall monotonically as the prompt grows. If a 512-token prompt benchmarks slower than a 2,048-token one, the first scenario paid the JIT and the whole pass is suspect.

Key vLLM parameters

ParameterValuePurpose
--max-model-len128000The model's full native window (max_position_embeddings). There is no reason to serve less: the entire context costs 1.5 GiB of KV, so a smaller value would hand back memory the node does not need and cap requests for nothing.
--kv-cache-memory-bytes3221225472 (3 GiB)Pins the KV pool at an exact size instead of letting vLLM derive it from a utilization fraction. On unified memory the fraction is not a reliable lever, and on vLLM 0.26.0 pinning also skips a memory-profiling assertion that can abort startup with no traceback when the OS reclaims page cache mid-profile. 3 GiB yields 262,045 tokens = 2.05x concurrency at the full window.
--gpu-memory-utilization0.90Largely inert once KV is pinned — it bounds the allocator rather than sizing the pool. Left at 0.90 because nothing on this config is close to binding.
--max-num-seqs4Left at a small-but-not-minimal value. There is no speculative draft here, so nothing derives the CUDA-graph capture size from it and it is not a decode knob on this config — but it costs no KV you are using, so there is nothing to gain by dropping it to 1.
--max-num-batched-tokens8192Bounds the chunked-prefill chunk. A 126,026-token prompt prefills fine at this value; it is also the first knob to cut, not --max-model-len, if a long prefill ever kills the engine on a different build.
--moe-backendnot set (deliberately)vLLM auto-selects `FlashInfer CUTLASS Unquantized MoE` on GB10 for this BF16 checkpoint and it works. Forcing a backend here is how you end up on a slower or unsupported kernel; read what the log chose instead.
--speculative-confignot set (nothing to point it at)The checkpoint ships no MTP head and no published EAGLE/DFlash draft targets this model, so the biggest decode lever on a Spark is simply unavailable. At 8.4B this model is a more plausible draft than a draft target.

API usage

Chat completion

The reply opens with a <think> block. There is no way to turn it off from the API — the chat template has no enable_thinking or reasoning_effort variable — so budget max_tokens for the reasoning as well as the answer.

curl -s http://127.0.0.1:8000/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "LiquidAI/LFM2.5-8B-A1B",
    "messages": [{"role": "user", "content": "Write a Python function that merges two sorted lists."}],
    "max_tokens": 512,
    "temperature": 0
  }'bash

Measure decode correctly

Give every request a unique prefix if you repeat it — vLLM caches prompt prefixes, and a cached prefill reports a fake TTFT and a fake prefill rate.

# Take tok/s from the server's own usage accounting, not by counting SSE frames.
curl -s http://127.0.0.1:8000/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"LiquidAI/LFM2.5-8B-A1B","messages":[{"role":"user","content":"Explain how a B-tree insert works, step by step."}],"max_tokens":512,"temperature":0}' \
  -w '\ntotal=%{time_total}s\n' \
  | python3 -c 'import sys,json; d=json.loads(sys.stdin.read().split(chr(10))[0]); print(d["usage"])'bash

Troubleshooting

Decode is ~57 tok/s and you expected far more from an 8B model.
That is the correct number for this checkpoint on this hardware, not a misconfiguration. Decode on a Spark is bandwidth-bound, so what matters is bytes read per token, not parameter count. Summing this checkpoint's safetensors headers gives 3.37 GB read per token: 1,938 MB of routed experts (top-4 of 32 across 22 layers), 604 MB of short-conv projections, 524 MB of tied lm_head, 176 MB of dense FFN and 126 MB of attention. Against 273 GB/s of LPDDR that is an 81 tok/s ceiling and the measured 57.5 is 71% of it. Two of those terms surprise people: the short-conv layers are cheap in KV but not in bytes, and tie_word_embeddings makes the 524 MB embedding matrix a per-token read as lm_head.
A request with a ~133,000-token prompt returns HTTP 400.
Expected — that is above --max-model-len 128000, which is the model's native maximum, and vLLM refuses it cleanly rather than crashing. The largest prompt verified on this config is 126,026 tokens, which answered in 13.5 s. If you need more you would need RoPE scaling, which this recipe does not use and has not measured.
The engine dies right after `torch.compile took NN s` with no traceback and no OOM in the kernel log.
On vLLM 0.26.0 this is usually the memory-profiling assertion, not a crashed kernel: profiling requires free memory only to decrease, which is false on unified memory where the reading drifts upward when the OS reclaims page cache. Passing --kv-cache-memory-bytes (as this recipe does) early-returns before that assert and logs 'skipped memory profiling'. Also do not poll the box with `free`, `du` or `ps` while it is profiling — that churn can cause it.
Startup seems to hang for several minutes between weight loading and the KV cache line.
Normal on a first boot. Our cold boot took 94 s to load weights, then ~6 minutes before `GPU KV cache size` appeared, then 55 s of graph capture — flashinfer has no prebuilt sm_121a cubins and compiles them on first use. Confirm it is working rather than wedged with `pgrep -af 'ninja|cicc|nvcc'`. It is cached under ~/.cache/flashinfer afterwards; keep VLLM_CACHE_ROOT stable across boots so you only pay it once.
You want to quantize it to get more speed.
Ruled out, and worth explaining. Only MLX and GGUF community quants exist — there is no NVFP4 or FP8 build of this checkpoint. More importantly the payoff is capped by shape: 15.5 of the 16.9 GB is routed experts, but top-4-of-32 routing means one token reads only 4/32 of them, so a 4-bit expert-only cut would free ~11 GiB of memory on a node that is already using 24.7 of 114, while cutting only ~970 MB from a 3.37 GB per-token read. The memory win and the speed win are different numbers here and neither is the constraint.
Speculative decoding does not start.
There is nothing to point it at. The checkpoint contains no MTP or next-token-prediction layers and no EAGLE/DFlash draft has been published for LFM2.5. Weightless prompt-lookup (--speculative-config '{"method":"ngram",...}') is the only option that needs no draft, and it is sharply workload-dependent: it only pays on traffic where the output repeats the prompt, such as apply-this-diff editing. It was not measured on this config.
lfm1.0
Memory budget

Nothing about this config is memory-constrained, and saying so plainly is the point of the page: the whole serving working set is 24.7 of the 114 usable GiB on one Spark, with the model's entire native 128,000-token context already served. The weights are 16,935,715,072 bytes of BF16 safetensors (vLLM logs 'Model loading took 15.8 GiB memory'), and summing the safetensors headers by prefix splits that exactly: 15,502 MB is the 32 routed experts across the 22 MoE layers, and the remaining 1,434 MB is everything else — the 18 short-conv blocks (604 MB), the tied embedding/lm_head matrix (524 MB), the two dense FFN layers (176 MB) and the six attention layers (126 MB). KV is the part that almost disappears. Only 6 of the 24 layers are full attention, at 8 KV heads and head_dim 64, so a token costs 12,288 bytes of KV plus a little short-conv state: pinning the pool at exactly 3 GiB with --kv-cache-memory-bytes yielded 262,045 tokens, which vLLM reports as 2.05x concurrency at the full 128,000-token window. Steady-state `free -m` with the API up reads 29,449 MB used; the same box measured 4,193 MB immediately after teardown, so the serving working set is 25,256 MB = 24.67 GiB. The weight segments and the KV pin are measured; the 5.9 GiB overhead segment is back-computed from those measured totals (graph capture alone accounted for 0.19 GiB per the engine log).