Streaming a Mixture-of-Experts base (v0.77.0)

Layer streaming keeps the frozen base off the card and copies it in one decoder layer at a time. Layer streaming is Soup's implementation of layer-to-layer streaming of a frozen base (L2L, Pudipeddi et al. 2020). v0.77.0 takes it to Mixture-of-Experts bases of the DeepSeek-V3 family: their MLA attention, their routed experts beside a shared expert, their FP8 block-quantised checkpoints, and a second way to stream the experts, one at a time. It is still BETA and opt-in (training.stream_layers: true).

Read the scope first. Every measurement on this page comes from one RTX 5070 Laptop 8 GB, from gates that ran on CPU only, or from a model built in a real model's shape with random weights. Nothing has been trained on Kimi K2. No DeepSeek-V3-family checkpoint has a resident reference at real size, and nothing here is comparable with the 4 GB laptop rows on the other streaming pages: upstream's benchmarks index calls the RTX 5070 box a new baseline, not comparable to the 4 GB laptop's records.

Which bases stream

BaseHow it streamsEvidence
model_type: deepseek_v3 (DeepSeek-V3, Moonlight-16B-A3B and other bases on that decoder)Natively, as the eleventh architecture on the allowlistAdmitted on streamed-versus-resident parity, forward and backward, on a tiny model built from a config, on CPU in float32; no real DeepSeek-V3 checkpoint was compared against a resident run, and there is no NF4 parity check for it
Kimi-K2-Base and Kimi-K2-Instruct as published (model_type: kimi_k2)Their config.json is read as plain data into transformers' own DeepseekV3Config behind a field-by-field check, and the base then streams as deepseek_v3Verified on CPU with a tiny K2-shaped checkpoint only; nothing has been trained on K2
Kimi-K2-Thinking, Kimi-K2-Instruct-0905Refused by that check, which names the fields that differ (the first ten, then how many more)None: no supported route yet
Kimi K2.5 and K2.6 (model_type: kimi_k25, a vision-language wrapper)Not admittedNone

The sharder stacks per-expert keys into the fused expert tensors, drops the multi-token-prediction layers stored past num_hidden_layers, and keeps the router's e_score_correction_bias in float32.

target_modules: auto adapts the MLA projections: q_a_proj and q_b_proj, or a single q_proj when the config sets q_lora_rank: null (Moonlight-16B-A3B does), plus kv_a_proj_with_mqa, kv_b_proj and o_proj. Before v0.77.0 the deepseek_v3 entry named only the low-rank query pair, so on a q_lora_rank: null base "the query path trained none, without a warning" (#2071); see If you trained before v0.77.0.

Model code is never the repository's. Under stream_layers the config, the meta skeleton used by the setup probes and the streamed model are transformers' own classes. --trust-remote-code reaches the tokenizer only (#1933), which is where Moonlight-16B-A3B and Kimi K2 need it. A base that has only repository model code cannot be streamed.

Resident Kimi K2 is refused at load on the transformers backend (#2130). A config naming moonshotai/Kimi-K2-Base or moonshotai/Kimi-K2-Instruct without stream_layers stops with Resident Kimi K2 is unsupported on Transformers 5.x. Use the guarded training.stream_layers: true route (#2107) ...; Kimi-K2-Thinking and Kimi-K2-Instruct-0905 get ... No supported route yet. The two resident recipes on those bases, kimi-k2-sft and kimi-k2-thinking-grpo, are marked unavailable: soup recipes show still prints them, and soup recipes use refuses with exit 3 and writes no file.

Two ways to stream the experts

training.stream_expert_layoutWhat the layer buffers holdRouted experts under quantization: 4bit
fused (the default)the whole MoE layer, routed experts included, so the largest layer bounds the cardstay at the stream dtype
per_expertthe dense part only (attention, norms, router, shared expert); each routed expert is read from its own file when the router picks it, and again in the backwardNF4, one pair of matrices per expert

per_expert makes the VRAM bound independent of the expert count. It buys capacity, not speed: each routed expert is read from the cache volume about three times per step (forward, checkpoint recompute, backward), one at a time. On one RTX 5070 Laptop 8 GB, the same Moonlight-16B LoRA config peaked at 1.0 GB of GPU memory against 3.1 GB fused (NF4 experts against bf16, different cache tiers), at about 2.7x the step time (one run each; the record's controlled pair reads 1.9 GB against 3.9 GB at about 2x). Because only per_expert quantises the routed experts, the two layouts train against numerically different bases under 4bit.

Keep the cache on NVMe. Whatever the tier, per_expert reads the routed experts from the cache volume (the RAM tier holds only the dense part), so Soup prints a yellow warning when that volume is not NVMe: training.stream_expert_layout='per_expert' reads every routed expert from the cache volume about three times per step, and that volume reads as ..., not NVMe: expect steps bound by that disk. It is a warning, not a refusal (#2170).

Limits:

  • DeepSeek-V3 family only, a Kimi K2 base that passes the check included. Any other base is refused at trainer setup, before a weight is resolved, with training.stream_expert_layout='per_expert' is gated on resident parity per architecture; .... soup train --dry-run stops before that check, so a dry run passes such a config.
  • No adapters on the routed experts. moe_lora: true beside per_expert is refused at config load: training.stream_expert_layout='per_expert' streams the routed experts one at a time and cannot train adapters on them; unset training.moe_lora (adapt attention or the shared expert instead) or use stream_expert_layout='fused'.
  • Going back to an older Soup. A Soup older than v0.77.0 accepts a per-expert cache as reusable and then fails ("shard is missing decoder weights"). Delete the cache directory, or re-shard with fused, before you downgrade.
  • What the pre-flights charge. The VRAM pre-flight counts the dense-only layer buffers plus one routed expert in flight, printed as an experts line. The disk pre-flight sizes the cache in the layout you chose, from the source safetensors headers, so a fused cache is charged at the stream dtype it is really written at.

A config that loads on the v0.77.0 binary (checked with soup doctor --config):

yaml
base: moonshotai/Moonlight-16B-A3B
task: sft
backend: transformers

data:
  train: ./data.jsonl
  format: alpaca
  max_length: 512

training:
  epochs: 1
  batch_size: 1
  quantization: 4bit
  stream_layers: true
  stream_source: auto
  stream_expert_layout: per_expert
  lora:
    r: 8
    target_modules: [q_proj, kv_a_proj_with_mqa, kv_b_proj, o_proj]

output: ./output

Run it with soup train --config soup.yaml --trust-remote-code, because Moonlight-16B-A3B's tokenizer is repository code. Loading is not the same as fitting: the tier, the NVMe warning and the VRAM and disk pre-flights are decided at trainer setup, on your box. Drop the stream_expert_layout line for the default fused layout.

Adapters on the experts (moe_lora) under streaming

With the default fused layout, moe_lora: true adapts the fused expert parameters as on a resident run. Since v0.77.0 a streamed run adapts the union of your list targets (an explicit list, or what auto resolves to on DeepSeek-V3) and the MoE targets, in the same order as a resident run of the same config, and the VRAM pre-flight charges that union and one adapter per routed expert (#2072). A string target, such as a regex, keeps its meaning and gains the MoE targets on top (#2095). A non-default training.moe_aux_loss_coeff is refused with stream_layers, because the streamed setup never applies the MoE auxiliary loss (#1749).

FP8 block-quantised checkpoints

DeepSeek-V3 is published as an FP8 block-quantised checkpoint, and Kimi K2 uses the same format: float8_e4m3fn weights, each with a float32 weight_scale_inv per 128 x 128 block, declared in config.json. Since v0.77.0 layer streaming shards such a checkpoint by dequantising it as it writes the layer cache (#2061). There is no config key: the format is detected from the checkpoint.

  • Each FP8 weight is multiplied by its block scales in float32, the last block cropped at a ragged edge, cast to the stream dtype, and then stored by the same rules as a weight from a bf16 checkpoint (NF4 under quantization: 4bit wherever the layout quantises it). Routed experts are dequantised before they are stacked or written per expert. The scales are never stored.
  • The work goes one band of rows at a time, so a tensor costs its output plus a small band.
  • The disk pre-flight charges each FP8 weight at the rate its dequantised tensor is stored at and nothing for the scales, and so does the RAM check before sharding under stream_source: ram.
  • For deepseek_v3, config.json is part of the cache's identity, so a cache sharded from the bf16 checkpoint is never reused for its FP8 copy at the same path, nor the reverse.
  • After training, merging or serving the adapter loads the base through transformers' own FP8 path, which is a different numerical path from the dequantised weights the adapter was trained on.

On a tiny DeepSeek-V3, the model streamed from its FP8 copy matches a resident model holding the same dequantised weights bit for bit (a CPU gate). On a GPU, the loss check of an FP8 source against the bf16 source failed (see the table below), so nothing here says an FP8 source trains like its bf16 source.

Refused, with a message naming the supported format: every other FP8 spelling (per-tensor scales, e5m2, compressed-tensors, ue8m0 scales, static activation scales, a missing or misshapen scale, an FP8 tensor without a scale, a scale without its tensor) and an FP8 base that is not deepseek_v3. Damaged values (a NaN after dequantisation, a negative or non-finite scale, a float16 overflow under a float16 stream) are refused naming the tensor, when that tensor is read: before the cache index is written, though files of earlier layers may already be on disk. Any other quantised base (compressed-tensors INT4 such as Kimi-K2-Thinking's, GPTQ, AWQ, bitsandbytes, an integer or packed tensor, or a tensor named like a quantiser's companion such as qweight or SCB) is refused by name before a byte of the cache is written, and a cache an older Soup sharded from such a source is not reused (#2103). Point base at a bf16 copy, or for DeepSeek-V3 at its block-FP8 release.

Kimi K2

The check admits a K2 config.json only where transformers' DeepseekV3Config computes what K2's own modelling code computes, and refuses before the tokenizer or any weight is loaded, naming the fields that differ and their values, the first ten and then how many more (#2107). Kimi-K2-Base and -Instruct as published pass it. Parity with K2's own code was not run: Soup never executes it, the guard was derived by reading it, and known differences remain, in upstream's words "the MoE combine's rounding and eager versus sdpa attention".

  • Tokenizer. It exists only as repository code, so a K2 run needs --trust-remote-code (Soup prints its REMOTE CODE WARNING panel) and pip install tiktoken, which Soup does not install. When the tokenizer fails to load on a K2 base, Soup says so.
  • A brief CPU allocation at full size (#2154). On a full-size K2, setup briefly allocates about 22.5 GB of CPU commit per MoE layer before moving it to the meta device. Resident memory barely moves, but a machine without that commit headroom fails with a raw DefaultCPUAllocator error. On a full-size DeepSeek-V3 the setup briefly allocates about 15 GB the same way.
  • Disk. Over the published Base index (no weights downloaded), the disk pre-flight sizes a cache under quantization: 4bit at 533.2 GB with per_expert and 2.04 TB with fused, because the fused routed experts stay bf16. That is the pre-flight's arithmetic, not a cache anyone wrote.

What was measured

Every record is linked at the v0.77.0 tag. All of them ran on development code before the tag, and none of them names a person.

RecordMachineWhat it showsIts own limits
Expert streaming gateRTX 5070 Laptop 8 GB and CPUPASS. On three real OLMoE-1B-7B layers, the streamed experts block equals transformers' resident experts block, torch.equal in 27 of 27 checks (output, input gradient, routing-weight gradient) in CPU float32, CUDA bf16, and CUDA bf16 with NF4 experts against the dequantised control. Each routed-to expert was read exactly twice, once in the forward and once in the backward (one block, no checkpoint recompute)One MoE block on random hidden states, not a whole model; no DeepSeek-V3 weights; no timing
Moonlight-16B on an 8 GB cardRTX 5070 Laptop 8 GBBoth arms COMPLETE. In soup train, the same LoRA config peaked at 1.0 GB of GPU memory with per_expert against 3.1 GB fused (NF4 experts against bf16, different cache tiers), at about 2.7x the step time, one run each; the record's controlled pair reads 1.9 GB against 3.9 GB at about 2xThe fused store was partly in the page cache; the two arms train numerically different bases; no quality or convergence claim
FP8 shard gateCPU onlyG1, G2, G3 pass. The dequantisation equals an independent per-block reference; a tiny DeepSeek-V3 streamed from its FP8 copy matches a resident model holding the same dequantised weights bit for bit (logits, loss, LoRA gradients, both layouts). On Moonlight-16B-A3B converted to the format by the project, the 27-layer NF4 cache (30.60 GB) came within 0.0002% of the pre-flight's estimateNo GPU; a conversion of a real model, not an official FP8 release
FP8 source, GPU loss checkRTX 5070 Laptop 8 GBFAIL on two of its pre-registered limits: the per-step loss on a short row and the step time. Both arms completedSpeed parity and per-step loss parity are not shown. The record says its rule commit was never pushed, so "written before any run" cannot be checked from public history, and its analysis of the cause comes after the verdict
Kimi K2 admission gateCPU onlyA tiny K2-like checkpoint (the published config at tiny dimensions) streams bit for bit like a resident DeepseekV3ForCausalLM on the same weights (logits, loss, LoRA gradients; fused and per_expert; from bf16 and from its FP8 block copy). The field-by-field config check fails as registered on one of 43 fields: transformers adds rope_theta and rope_type to rope_scaling, as it does for any DeepSeek-V3 config. The disk pre-flight sizes the published Base index exactlyNo GPU, no NF4 model parity, no K2 weights, nothing trained on K2
A step through a K2-shaped modelRTX 5070 Laptop 8 GBIn a measurement harness (not soup train) on one RTX 5070 Laptop, a LoRA step through a model built in Kimi K2's shape with random weights (shape only; nothing was trained on K2) peaked at 4,723 MiB of VRAM and took 14.5-18.5 minutesThe record calls it "a capacity demonstration, not a training regime"; random weights, no quality, convergence or real checkpoint

Two more records are about that shape and claim nothing: a sequence-scaling probe on the same store ended with no verdict, and a note on whether a 24 to 32 GB card could hold one K2 layer is arithmetic only. Neither is quoted here. The measurement records page indexes all eight.

What was not measured

  • Any training run on Kimi K2, or any GPU run of a real K2 config or K2 weights (the K2-shaped GPU step above used random weights in a measurement harness).
  • A resident reference for a real DeepSeek-V3-family checkpoint: Moonlight-16B-A3B does not fit the 8 GB card resident.
  • A preference loss (dpo, orpo, simpo, kto) on a DeepSeek-V3-family base. All five streaming tasks are admitted on it, but its parity gate is a causal-LM loss and its LoRA gradients, and no preference-loss parity was measured.
  • A throughput claim for the family: the records time random-token-id harness steps and 9-row soup train runs on one laptop, and no tokens-per-second figure from them is quoted here. Nothing was measured on a 4 GB card.

If you trained before v0.77.0

Three fixes in this release change what an MoE LoRA run trains. In upstream's words:

  • target_modules: auto on a DeepSeek-V3 base whose config sets q_lora_rank: null, such as Moonlight-16B-A3B: "the key/value and output projections took the adapter and the query path trained none, without a warning" (#2071). Verified in the code at v0.76.0 and v0.76.1; a base with an integer q_lora_rank is unaffected.
  • moe_lora: true under stream_layers: the setup replaced your resolved targets with the MoE list, so "on Qwen3.5-MoE the linear-attention in_proj_qkv / out_proj were dropped; and an explicit lora.target_modules list was dropped entirely" (#2072).
  • moe_lora: true with a string target_modules (a regex, all-linear, or auto on qwen4_exp), resident or streamed: the string was replaced by the MoE list, and a regex "was dropped outright" (#2095).

The last two were verified in the code at v0.76.0 and v0.76.1. If those modules mattered to you, re-train. The full list of v0.77.0 re-run items is on the v0.77.0 release page.

Other streaming keys in v0.77.0

Three opt-in keys arrived beside this one and work on dense bases too: stream_recompute, stream_loss_chunk and stream_nf4_scale_cache. They are described, with their limits and what was measured, in Scaling a streaming run.

See also

Soup is free and Apache-2.0. If it saved you a training run, starring the repo costs nothing and helps most. You can also fund the GPU time behind the work a 4 GB laptop cannot reach.