howtospark
Recipes
?

1 × Spark256K ctx38.77 tok/s@ 2KvLLMthinking onvanilla

InternScience's agentic-RL Agents-A1 (35B MoE, ~3B active) in the vendor's FP8 on one DGX Spark, serving the full 262,144-token context at 38.8 tok/s single-stream.

spark .1 · single nodepeer
60 GiB free
Activations + graphs10.9* GiB
Expert planes30.2 GiBFP8
114 usable
54* / 114 GiB
47% 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

Agents-A1 35B-A3B — FP8
2K8K32K128K
decode tok/s39.038.035.127.2
ttft357ms1.27s5.91s37.85s
prefill tok/s5.6k6.4k5.5k3.5k
power32W36W38W42W

includes 1 community run · @kvncrw

median per context · o256

Contributors

Overview

Agents-A1 is a 35B mixture-of-experts model with ~3B active parameters, hybrid attention (30 gated-delta linear-attention layers to 10 full-attention layers) and a 262,144-token window. The vendor ships an FP8 export alongside the BF16 release, and that export is the whole reason this configuration is easy: 37.7 GB in one safetensors file means a single Spark holds the weights with two thirds of its unified memory to spare, and the hybrid layer plan means the full native context costs 8 GiB of KV rather than a fight. So there is no memory puzzle to solve here, and the recipe deliberately does not invent one. The flags below are the plain ones: no tensor parallelism, no MoE backend override, no --enforce-eager, no --quantization, and specifically no --language-model-only. What the configuration is actually shaped by is the checkpoint's quantization scope. The vendor quantized the routed experts and the full-attention projections to FP8 and left the linear-attention path, the routers, the embedding table and lm_head in BF16 — which is a reasonable cut, but it means the per-token read is not dominated by the thing the file is dominated by. Summing the header, a decode step reads about 4.49 GB: 1.01 GB of routed experts (8 of 256), 2.02 GB of linear-attention projections and 1.02 GB of unquantized lm_head. The experts are 86% of the file and 22% of the reads. That ratio is what sets the measured 38.8 tok/s, and it is also why a 4-bit build of the same architecture is much faster than this one rather than slightly faster. The other thing this recipe settles is the speculative-decoding question, because the obvious answer is wrong. The vendor publishes a dense 4.5B sibling on the identical 248,320-token vocabulary, which looks like a ready-made draft. It is not: quantized to NVFP4 that sibling still reads 3.52 GB per token against the target's 4.49 GB, so a drafted token costs 78% of a verified one. Even at 100% acceptance and k=1 the arithmetic ceiling is about +12%, and at any realistic acceptance rate it is a loss. Sparsity inverts the usual intuition — an 8x smaller model is not an 8x cheaper draft when the big one only reads 3% of its experts. This configuration therefore ships with no draft, and the section below records both the arithmetic and the load failure we hit trying it anyway.

  • 1 × DGX Spark (GB10, sm_121) — no tensor parallelism, no second node
  • Measured 38.77 tok/s single-stream decode at a 2,048-token prompt, warm, greedy, on stock vLLM 0.26.0
  • Full 262,144-token context served, bracketed with real requests: a 253,840-token prompt answers in 105.3 s, and anything past the window is refused with HTTP 400 rather than crashing
  • Weights 35.31 GiB; whole serving working set ~54 of 114 usable GiB
  • 8 GiB of KV buys 412,980 tokens — 1.58x concurrency at the full window, because only 10 of 40 layers cache
  • No flags beyond the defaults: vLLM auto-selects the TRITON FP8 MoE backend and FLASH_ATTN on sm_121, and both work
  • A decode step reads 4.49 GB, of which the 256 routed experts are only 1.01 GB — measured from the safetensors header
  • No speculative draft: the config advertises mtp_num_hidden_layers: 1 but the checkpoint ships zero MTP tensors, and the vendor's dense 4.5B sibling is too expensive per token to pay as an external draft
  • Weight load 282 s from one 37.7 GB safetensors file; full startup about 7 minutes

Software requirements

  • 1 × DGX Spark (GB10, sm_121), NVIDIA driver ≥ 580 (CUDA-13 capable)
  • vLLM 0.26.0 with torch 2.11.0+cu130 (verified 2026-07-28). Stock, unpatched
  • The `hf` CLI to fetch the checkpoint (~37.7 GB)
  • ~45 GB of free disk on the serving node

Quick start

  1. 1

    Free the node first

    Anything still holding unified memory — a previous vLLM, a leftover Ray cluster — comes out of the same pool this serve needs.

    pgrep -af 'vllm serve|ray::' || echo 'node is clear'
    ray stop --force 2>/dev/null || true
    free -gbash
  2. 2

    Download the checkpoint

    One safetensors file of 37,676,005,968 bytes holding 62,546 tensors, and no model.safetensors.index.json. That matters for pre-flight: the usual checks that read the weight index do not apply, so read the file's own header instead — the first 8 bytes are a little-endian u64 header length and the next that-many bytes are the tensor map, both fetchable with HTTP range requests before you download anything.

    hf download InternScience/Agents-A1-FP8 \
      --local-dir ~/models/hf/Agents-A1-FP8
    
    # verify before serving
    stat -c %s ~/models/hf/Agents-A1-FP8/model.safetensors   # expect 37676005968
    find ~/models/hf/Agents-A1-FP8 -name '*.incomplete' | wc -l   # expect 0bash

    Ours pulled at ~60-70 MB/s, about ten minutes. A partial file fails several minutes into the load, so check the byte count first.

  3. 3

    Serve it

    Note what is not here: no --tensor-parallel-size, no --moe-backend, no --enforce-eager, no --quantization, no --speculative-config, and in particular no --language-model-only. This checkpoint ships its vision tower, so the tower must be constructed for those weights to have somewhere to land.

    PATH="$HOME/venvs/vllm-026/bin:$HOME/.local/bin:/usr/local/cuda/bin:$PATH" \
    TORCHINDUCTOR_COMPILE_THREADS=2 MAX_JOBS=4 \
    vllm serve ~/models/hf/Agents-A1-FP8 \
      --served-model-name Agents-A1-FP8 \
      --max-model-len 262144 \
      --kv-cache-memory-bytes 8589934592 \
      --gpu-memory-utilization 0.90 \
      --max-num-seqs 4 \
      --max-num-batched-tokens 8192 \
      --port 8000bash

    MAX_JOBS=4 bounds ninja's parallel nvcc if a kernel JIT fires inside the engine's first forward. Keep ninja (~/.local/bin) and nvcc (/usr/local/cuda/bin) on PATH.

  4. 4

    Confirm what it actually chose

    Four lines tell you the run is healthy. If the MoE backend line says EMULATION, or the KV pool is far off 412,980 tokens, read the troubleshooting section before benchmarking anything.

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

    Expect: TRITON, FLASH_ATTN, 35.31 GiB / 282 s, 412,980 tokens, 1.58x.

  5. 5

    Warm it up before you trust any number

    Expert planes fault in from NVMe on first touch. Send a few hundred tokens of real traffic after every restart, then measure. The tell that you are still cold is a 512-token prompt benchmarking slower than a 2,048-token one; warm, ours run 39.04 and 38.77 tok/s respectively.

    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":"Agents-A1-FP8",
             "messages":[{"role":"user","content":"Write an LRU cache in Python."}],
             "max_tokens":256,"temperature":0}' > /dev/null
    donebash
  6. 6

    Check the memory you actually used

    Read this with the API up and idle, never during weight loading — mid-load the KV pool does not exist yet and the figure flatters you by exactly the amount you are about to allocate.

    free -g
    # expect ~58 GiB used of 121 — weights 35.3 + KV 8 + overhead ~10.9 + OS ~4bash

Key vLLM parameters

ParameterValuePurpose
--language-model-onlyNOT set (important)Do not pass it. config.json carries a vision_config and this checkpoint genuinely ships the tower — 0.89 GB of model.visual.* tensors are in the file — so the tower has to be constructed for those weights to load. On a vLLM whose qwen3_5.py carries a tower-skipping guard, passing the flag produces 'ValueError: There is no module or parameter named visual'. The reliable check is the checkpoint's own tensor names, not the config and not the architecture name, both of which look identical on releases that need the opposite answer.
--kv-cache-memory-bytes8589934592 (8 GiB)Pins the KV pool deterministically instead of nudging the utilization fraction, which is unreliable on GB10 because vLLM's utilization math counts system-wide usage. 8 GiB buys 412,980 tokens = 1.58x concurrency at the full 262,144 context, with margin for the few-percent pool variation between boots. It also skips vLLM 0.26.0's memory-profiling assertion, which is worth having on unified memory where free memory can drift upward mid-profile.
--max-model-len262144The model's full native window, and the pool is divided down to 1.58x rather than left idle at a higher multiple. On this architecture full_attention_interval is 4, so only 10 of 40 layers hold KV at about 20 KiB/token — context is cheap here and there is no reason to inherit a smaller value from another config.
--max-num-seqs4Left at 4 rather than dropped to 1-2. There is no speculative draft in this configuration, so nothing derives a CUDA-graph capture size from it and it is a pure scheduler knob — but the KV it could claim is headroom we are not using anyway, so lowering it buys nothing.
--max-num-batched-tokens8192Sets the prefill chunk. 8192 held through a real 253,840-token prompt without incident, so there was no reason to cut it; on this hardware a smaller chunk is the first thing to try if a long prefill dies, before touching --max-model-len.
--moe-backendNOT setvLLM auto-selects TRITON for FP8 MoE on sm_121 out of a menu that also lists FLASHINFER_TRTLLM, FLASHINFER_CUTLASS, DEEPGEMM, VLLM_CUTLASS and MARLIN, and it serves correctly. Do not hardcode a backend from another recipe — the working set on sm_121 differs per checkpoint format and per engine version, and an explicit choice that is unsupported is a hard startup failure rather than a fallback.
--speculative-configNOT set (measured decision)There is no draft worth running. The checkpoint declares mtp_num_hidden_layers: 1 but ships zero mtp.* tensors, so there is no in-checkpoint MTP head. The vendor's dense 4.5B sibling on the identical vocabulary looks like an external draft, but at NVFP4 it reads 3.52 GB per token against this target's 4.49 GB — 78% of a full verify — so even at perfect acceptance and k=1 the ceiling is about +12%, and at any realistic acceptance rate it loses. See troubleshooting for the load failure we hit confirming it.

API usage

Chat

curl -s http://127.0.0.1:8000/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"Agents-A1-FP8",
       "messages":[{"role":"user","content":"Refactor this function to be tail-recursive."}],
       "max_tokens":512,"temperature":0}'bash

Measure decode correctly

curl -s -N http://127.0.0.1:8000/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"Agents-A1-FP8",
       "messages":[{"role":"user","content":"Write a red-black tree in Python."}],
       "max_tokens":256,"temperature":0,
       "stream":true,"stream_options":{"include_usage":true}}' \
  | tail -2bash

Troubleshooting

The dense 4.5B sibling refuses to load as an external draft: AssertionError in load_column_parallel_weight
Do not run it. The failure is fixable in principle (serve a draft whose files match one format), but it is not worth fixing here — see the next entry for why the pairing cannot pay even if it loads.
You want speculative decoding on this model and there is an obvious candidate draft
Screen a draft by bytes-read-per-token, not by parameter count, before spending a download on it. At k=1 the best case is (1 + 0.78) reads for 2 tokens, an 11% ceiling at 100% acceptance; realistic acceptance makes it a loss, and larger k makes it worse. A useful draft on this hardware wants to be well under ~15% of the target's per-token read — which for a sparse MoE target usually means an MTP head or a purpose-built EAGLE/DFlash draft, not a small dense sibling.
A prompt longer than the window is rejected with HTTP 400
Nothing to fix — but note the shape of the curve before promising this context to anyone. Time-to-first-token runs 196 ms at 511 tokens, 403 ms at 2,042, 1.35 s at 8,208, 43.1 s at 144,950 and 105.3 s at 253,840, while decode stays between 37.7 and 39.0 tok/s across the short end. The long-context story here is a prefill cost, not a decode cost.
Decode is 38.8 tok/s and you expected more from a 3B-active MoE
Nothing is misconfigured. If you want this architecture faster on a Spark, the lever is a quant that also compresses the linear-attention projections and lm_head, not a different MoE backend or a bigger KV pool. Diff a candidate quant's ignore list before assuming lower bits mean faster decode.
vLLM picks the TRITON FP8 MoE backend and you want to force something else
Leave it on auto. MARLIN was measured against the auto-selected TRITON on the same node in the same session, warm, and the two are not separable: 39.08 vs 38.77 tok/s decode and 4,945 vs 5,071 tok/s prefill at a 2,042-token prompt, both differences well inside the boot-to-boot spread on this hardware. Memory is identical too (35.25 vs 35.31 GiB loaded, the same 412,980-token pool). There is no backend win to chase here — the ceiling is the checkpoint's quantization scope, not the kernel.

Revision history

What has changed on this page since it was published, and what it measured. Newest first.

  1. re-measured

    Tested the auto-selected TRITON FP8 MoE backend against every alternative that will start on sm_121; kept auto.

    No change to the served config or to any published number. MARLIN measured 39.08 tok/s decode / 4,945 tok/s prefill at a 2,042-token prompt against TRITON's 38.77 / 5,071 on the same node in the same session, warm — not separable. VLLM_CUTLASS and FLASHINFER_TRTLLM have no sm_121 kernel; DEEPGEMM and FLASHINFER_CUTLASS reject this checkpoint's per-channel static weight scales.

apache-2.0
Memory budget

One node, comfortably. vLLM logs 'Model loading took 35.31 GiB memory' against a 37,676,005,968-byte single-file checkpoint, and steady-state `free -g` with the API up and idle reads 58 GiB used of 121 — subtract the ~4 GiB idle OS baseline and the serving working set is ~54 of the 114 usable GiB, so more than half the machine is still free. The weight split is measured per tensor from the safetensors header rather than guessed: routed experts plus the shared expert 32.41 GB, the gated-delta linear-attention projections 2.02 GB, lm_head 1.02 GB, embed_tokens 1.02 GB (a 248,320-token vocabulary at hidden_size 2048), a vision tower this recipe never uses 0.89 GB, and full-attention q/k/v/o only 0.27 GB. KV is the interesting part and it is cheap: full_attention_interval is 4, so only 10 of the 40 layers hold a real cache, each with 2 KV heads at head_dim 256 — about 20 KiB/token. An 8 GiB pin yields 412,980 tokens, 1.58x concurrency at the model's full 262,144-token window. Only the overhead segment is inferred, back-computed from the steady-state total.