June 2026

By Davide Ciffa

Luce Spark: a 35B MoE on a 16 GB GPU, without the offload tax

A 33-35B mixture-of-experts model fires only a handful of its experts per token, but to keep it on the GPU you still pay for all of them. Luce Spark pins the experts your traffic actually uses, offloads the rest to CPU, and decodes the whole token in one fused graph so offload stops costing speed. Qwen3.6 35B-A3B runs in 13.3 GiB (down from ~20.5) and Laguna XS.2 in 14.6 GiB (down from 18.8), both on a 16 GB card that could not load them before, and decode holds ~100 tok/s, near the ~119 all-GPU ceiling. It tunes itself from live traffic. One flag, no calibration step.

Luce Spark: serving a 33-35B MoE from a fraction of the experts, on consumer memory

TL;DR

The problem: a sparse model with a dense memory bill

Qwen3.6 35B-A3B and Laguna XS.2 are both A3B models: 35B and 33B total parameters, but only ~3B active per token. The router picks roughly 8 of 256 experts at each layer and ignores the rest. The compute bill is small. The memory bill is not: to keep the model on the GPU you hold every expert in VRAM, because any of them might be next.

On a 24 GB card that fits, barely. The experts alone are 18.2 GiB on Qwen and 16.6 GiB on Laguna; add the non-expert weights and a KV cache and you are at 18-21 GiB before context. On a 16 GB card it does not fit at all. You are paying full price for parameters that, for any given request, are mostly idle.

Standard expert offloading puts the cold experts in system RAM and computes them on the CPU. That frees VRAM but it is slow if you offload the wrong ones: pick the resident set badly and you hit the CPU tier on a third of every token's routing. The resident set is the whole game.

How Spark works

Spark is built on the hot/cold MoE offload engine that already ships in lucebox-hub. It adds the two pieces that make offload actually fast: knowing which experts to keep, and a cheap way to fix that decision while serving.

  router picks 8 experts
     hot   (calibrated, pinned on GPU) ───────────► GPU
     warm  (in the cache ring)         ───────────► GPU
     cold  miss ─ swap into a spare slot (LRU) ───► GPU
                  (rare after warmup, bounded VRAM)

The cache ring is a small over-allocation of the hot expert stack, so a swap is "copy three weight tensors into a spare slot and update one routing entry". The existing GPU FFN serves it with no special path. It is the same mechanism for both backends: laguna and qwen partition hot from cold on the host, so the swap is picked up by the lookup they already do.

Memory: a 33-35B MoE under 16 GiB

Peak VRAM measured on an RTX 3090, ctx 4096. "All-GPU" holds every expert resident; "Spark" pins ~60% of expert weight and swaps the rest through the cache.

ModelAll-GPU VRAMSpark VRAMSavedFits 16 GB?
Laguna XS.2 (33B-A3B)18.8 GiB14.6 GiB4.2 GiByes
Qwen3.6 35B-A3B~20.5 GiB13.3 GiB~7 GiByes

The footprint is set by two numbers you control: the share of experts pinned hot and the number of cache slots. Both are capped, so the total never drifts above the budget. Trade cache slots against context length to keep headroom under whatever card you are targeting.

Speed: offload, minus the tax

Offloading normally costs throughput. Two things claw it back: calibrating which experts stay resident, and decoding the token in one fused graph instead of 40 per-layer ones. Same model, same 60% residency, same 16 GB card, generating the same answer:

Side-by-side terminal race: naive expert offload decodes Laguna XS.2 at 66 tok/s while Luce Spark decodes the same output at 100 tok/s on the same RTX 3090 at 60% GPU residency.
Naive offload vs Luce Spark, both at 60% GPU residency. Same model, same card, same output. Spark finishes first: 66 to 100 tok/s, 1.5x the decode.

Laguna decode, 60% of expert weight resident:

Laguna XS.2, 60% residentDecode tok/s% of all-GPU
Naive offload (uniform split)6655%
Spark, calibrated placement8168%
Spark, calibrated + cache + single-graph~100~85%
All-GPU (needs 24 GB)119100%

Calibration recovers most of the gap (66 to 81). The rest was never about where the experts live, it was per-layer submission overhead: the offloaded path was building 40 separate GPU graphs per token. Folding the routed FFN into the attention graph and running the whole token as one fused graph removes that. The proof it is faithful: at full residency the fused decode is bit-identical to all-GPU (128/128 tokens, verified by spark/bench.py) and runs at the same ~119 tok/s. At 60% residency it holds ~100 tok/s, about 85% of the all-GPU ceiling and 1.5x a naive offload.

Why offload still trails all-GPU a little. At 60% residency the working set genuinely exceeds the resident experts, so a few percent of routings each token are re-fetched from CPU. The single fused graph plus async, pinned-memory copies hide that I/O under compute, which is the ~85% you see. Closing the last ~15% means either more resident VRAM or predicting the next experts before they are needed; token-level prediction caps around 53% recall, so that part is honest open work, not a free lunch.

Both backends, measured. The detail above is Laguna. Qwen3.6 35B-A3B offloads even better: its expert swap hides the cold fetches without a fused graph, so it keeps 92% of all-GPU at its 13.3 GiB operating point. Each model at its Spark operating point, same RTX 3090:

Model (Spark)All-GPUSparkSpeed kept
Laguna XS.2 (33B-A3B)119 tok/s100 tok/s85%
Qwen3.6 35B-A3B108 tok/s100 tok/s92%

One self-tuning command

There is no pipeline to run in production. The server tunes itself from its own traffic:

# laguna or qwen35moe, same flag
dflash_server models/Qwen3.6-35B-A3B-Q4_K_M.gguf --spark

# optional: cache slots per layer (default 32)
dflash_server models/laguna-xs2-Q4_K_M.gguf --spark --spark-slots 48

--spark enables the bounded cache, loads a learned placement profile from <model>.gguf.spark.csv if it exists, and keeps writing it after every request from live routing. First boot starts uniform and warms the cache within a session; each restart loads a better profile and starts warmer.

[spark] autotune ON (qwen35moe): cache_slots=16, profile=...spark.csv (loaded)
[qwen35moe] hybrid storage ready: total_hot=6053 total_cold=4187
   source=hotness:.../Qwen3.6-35B-A3B-UD-Q4_K_M.gguf.spark.csv

The placement gets better the more the model serves, with no operator step. If you want a warm start on day one, the optional offline tooling in optimizations/spark/ bootstraps a profile from a corpus you provide, for example your own agent session logs, but it is not required.

Bottom line

A mixture-of-experts model is sparse in compute and, with Spark, sparse in memory too. Serving only the experts traffic actually touches puts a 33-35B MoE on a 16 GB GPU: Qwen3.6 35B-A3B in 13.3 GiB, Laguna XS.2 in 14.6 GiB, both decoding around 100 tok/s, near what the same model gets with every expert resident on a 24 GB card. It is one flag, it works for both backends, and it tunes itself the longer it runs. The class of model that used to demand a 24 GB card now runs on consumer 16 GB silicon, and on a local-inference PC that is the difference between "fits" and "does not".


Source: Luce Spark on github.com/Luce-Org/lucebox-hub (tooling and docs in optimizations/spark/, engine in server/src/common/moe_hybrid_*). Numbers measured on an RTX 3090 24 GB, Qwen3.6 35B-A3B and Laguna XS.2 at Q4_K_M, ctx 4096. Built on the merged hot/cold MoE offload engine.

Related

Run a 35B MoE on 16 GB

Open-source. One flag. Self-tuning. RTX 3090 / 16 GB class hardware.

GitHub Laguna post Compare hardware Discord