The layer streaming paper
The mechanism behind training an 8B model on a 4 GB laptop GPU is written up as a preprint, together with the correctness protocol that verifies a streamed run is bit-exact against a resident one. This page is the short version, plus everything you need to cite it.
Exact Layer Streaming: LoRA Fine-Tuning of an 8B Model on a 4 GB Laptop GPU. Makazhan, A. (2026). Zenodo. 10.5281/zenodo.21771064. Preprint, CC-BY-4.0.
That DOI is a concept DOI: it always resolves to the newest version, so a revision never strands a citation. Quote it rather than a version-specific one.
What it reports
Two results, both on one machine: an RTX 3050 Laptop with 4 GB of VRAM, 16.9 GB of host RAM, running Windows 11.
- Llama-3.1-8B-Instruct in NF4 at 119.6 tok/s, 3.32 GB peak, 100% SM occupancy.
- Qwen2.5-3B with an un-quantized bf16 base at 143 tok/s in 2.15 GB, a configuration that raises CUDA out of memory when the same model is trained resident on the same card.
Both are configurations whose weights alone exceed the card. Measured overhead against resident training is 1.43x, at 0.5B, the only size on that machine that admits a valid resident baseline at all.
The paper is deliberate about what that does not beat. Layer streaming on a small card is well established for inference, and AirLLM's own documentation says layer sharding is not compatible with gradient propagation. The published fine-tuning result for this hardware class is 1.3B, reached by LSP-Offload (arXiv:2406.10181) by compressing the optimisation into a learned sparse subspace rather than by streaming the base. The systems that do stream weights during training are evaluated on an H200 with 1.5 TB of host memory, an RTX 4090 with 256 GB, or a cluster. The unserved combination, and the one the paper addresses, is streamed training on a small card.
The result it defends hardest is not the throughput
In a streamed trainer the natural failure is silent. If the autograd graph is severed at the base weights, the upper layers still train, the loss still descends, and the run looks healthy. A throughput number taken before correctness is established measures nothing.
So the central check is bit-exactness against a resident reference of the same numerics: resident NF4 for a streamed NF4 run, never resident bf16, because quantisation error is wide enough to hide a real defect inside it. Against a matched reference the expected answer is 0.0, so anything else is a bug by construction.
That discipline paid for itself. It caught a PEFT dispatch path that selected a different LoRA implementation on a meta-device skeleton and produced a 0.94 maximum logit divergence with byte-identical weights and byte-identical adapters: no exception, no warning, and a loss curve that looked entirely healthy. Nothing short of bit-exactness would have surfaced it.
The protocol itself, on SmolLM2-135M in bf16 with LoRA r=16 on q_proj and v_proj:
| Check | Threshold | Measured |
|---|---|---|
| Streamed vs resident logits, max abs diff | < 1e-3 | 0.0, bit-exact |
| LoRA gradient at layer 0 | non-zero | 9.50e-01, 30 of 30 layers non-zero |
| 100-step loss curve vs resident | within noise | max relative difference 0.0 |
| Same seed twice | identical | 0.0 |
| With pinned-host boundary offload | < 2% relative | 4.83e-3 |
| Two buffers vs three | identical | 0.0 |
Bit-exactness was then re-verified across nine model_type families in both bf16 and NF4, 14 of 14 comparisons at a maximum absolute logit difference of 0.000e+00. Two rows carry more weight than the others. phi3 fuses Q, K and V into a single qkv_proj, so the projection names a Llama-shaped implementation would look for do not exist at all, and it is bit-exact anyway, which is the strongest evidence that layer detection walks the real module tree rather than a naming assumption. And gemma3 is deliberately refused while gemma3_text is accepted, with the accept and the refuse asserted together so the refusal cannot decay into a spelling accident.
The protocol runs on CPU and is part of the project's test suite rather than a one-off experiment, so a regression fails CI instead of reaching a user.
Three findings that have nothing to do with streaming
These are the parts most likely to be useful even if you never enable layer streaming.
- The cross-entropy logits term costs 14 bytes per element, not 6. A first-principles budget counts the bf16 logits plus an fp32 upcast. The measured figure is 14, because the loss holds the bf16 logits, the fp32 upcast, an fp32 log-softmax and an fp32 gradient live at the same time. That is a 2.33x under-prediction on a term that dominates: at batch 8 the logits tensor was over a hundred times the size of the entire layer buffer pool. Anyone budgeting VRAM for a large-vocabulary model from first principles will make the same mistake.
- Windows and WDDM do not raise out of memory, they spill. One row of the fitting grid allocated 9.27 GB on a 4.29 GB card and completed without an exception. The methodological consequence is blunt: on this platform, "it did not crash" is not evidence that a configuration fits. It also cost a discarded baseline, a resident run whose reported peak turned out to be the driver paging into host memory.
- Gradient accumulation is per-token I/O-neutral. The received wisdom, the paper's own earlier draft included, is that accumulation multiplies I/O linearly because every micro-batch re-reads the model. True per optimizer step, and misleading per token, which is the unit that sets wall-clock time: layer reads per 1000 tokens held constant at 175.78 across accumulation 1, 2 and 4. The real cost is opportunity cost, measured at 2.52x at an equal effective batch of 4, which is why the tool prints "raise batch size until the pre-flight refuses, then accumulate" rather than the standard low-VRAM advice of keeping batch at 1.
A fourth, smaller one, aimed at anyone quoting a fraction of peak on a consumer GPU: a ceiling is only comparable to a throughput measured in the same session. Repeating one GEMM six times inside a single session was stable to under 1%, while the same card varied by about 13% between sessions at the same reported clock. Any fraction-of-ceiling figure has to state the SM clock it was taken at.
What it does not claim
The limitations section is long on purpose, and it is the reason to trust the rest.
- 8B is the largest size measured. No 14B or larger claim is supported by any number in the paper.
- The 3B bf16 figure is a lower bound. That machine could not page-lock a 5.55 GB store, so the run fell back to a pageable one, which makes the host-to-device copy synchronous. The occupancy drop from 96.8% to 79.3% is that cost, measured rather than estimated.
- Windows and WDDM throughout. The numbers are systematically pessimistic against Linux, and there is no Linux measurement.
- One card, and architecture coverage verified at small scale. The nine-family gate used tiny from-config checkpoints rather than downloaded ones, deliberately, because the risk being tested is a naming property and not a size property. The throughput results are Qwen2.5 and Llama only.
- Bit-exactness is verified at 135M, not at 8B. At 8B it is impossible by construction: it needs a resident reference, which cannot exist for a model that does not fit. But a valid resident baseline does exist at 0.5B and was not used for parity, and the paper names that as the most useful missing experiment in it.
- n = 1. Every throughput row is a single run of 50 measured steps after 10 warm-up, on a working laptop that was not otherwise idle, and no variance is reported. The one exception is the accumulation comparison, repeated in an interleaved order precisely because a monotonic clock drift would otherwise have manufactured the result.
- The RAM-versus-disk performance gap is unmeasured and no figure is claimed for it. Correctness of the disk tier is established; relative speed is not.
The records behind it
Every number comes from measurement logs published verbatim in the benchmarks directory. They are the working gate records kept while each item was built, not a summary assembled afterwards, so they carry the failures, the assumptions that turned out wrong and the numbers that were measured and then discarded, in the order those things happened. Read a file from its top rather than lifting a passage out of the middle: a passage may be one the same page later corrects.
Reproducing the headline row
The implementation shipped in Soup v0.72.3 under Apache-2.0. Two keys below are load-bearing, and omitting them is a silent divergence rather than an error: gradient_accumulation_steps (the shipped default is 4, and at batch 1 that measures roughly 2.5x lower throughput) and, for the un-quantized run, quantization (the shipped default is 4bit).
base: meta-llama/Llama-3.1-8B-Instruct
task: sft
data:
train: data.jsonl
max_length: 512
training:
stream_layers: true
stream_source: ram
stream_buffers: 2
quantization: 4bit
batch_size: 1
gradient_accumulation_steps: 1 # load-bearing: the shipped default is 4
optimizer: paged_adamw_8bit
lora:
r: 16
target_modules: [q_proj, v_proj]Omitting the lora block is not equivalent either: it defaults to r=64 with automatic target selection rather than the r=16 on q_proj and v_proj that produced these numbers. The run also needs free host RAM of at least the store size divided by 0.7, about 5.1 GB for the 8B NF4 store, and the pre-flight refuses below that rather than thrashing.
Cite it
@misc{makazhan2026exact,
title = {Exact Layer Streaming: LoRA Fine-Tuning of an 8B Model on a 4 GB Laptop GPU},
author = {Makazhan, Alpamys},
year = {2026},
publisher = {Zenodo},
doi = {10.5281/zenodo.21771064},
url = {https://doi.org/10.5281/zenodo.21771064}
}See also
- Layer streaming — the mechanism as it ships today, the measured numbers, and the full refusal table.
- Scaling a streaming run — architectures, batch versus gradient accumulation, the VRAM pre-flight, resume and the disk tier.
- Preference losses over streaming — DPO, ORPO, SimPO and KTO against a streamed base, added after the paper was written.
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.