streamft 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
streamft-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LCGA contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,239 @@
1
+ Metadata-Version: 2.4
2
+ Name: streamft
3
+ Version: 0.2.0
4
+ Summary: Fine-tune models that do not fit: stream NF4 weights from disk, one transformer block at a time.
5
+ Author-email: Gokhan Cava <byjameson@users.noreply.github.com>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/byjameson/stream-finetune
8
+ Project-URL: Issues, https://github.com/byjameson/stream-finetune/issues
9
+ Keywords: fine-tuning,qlora,nf4,low-vram,lora,llm
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: License :: OSI Approved :: Apache Software License
12
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: torch
17
+ Requires-Dist: transformers>=4.40
18
+ Requires-Dist: safetensors
19
+ Requires-Dist: tokenizers
20
+ Requires-Dist: accelerate
21
+ Requires-Dist: datasets
22
+ Requires-Dist: huggingface-hub
23
+ Requires-Dist: numpy
24
+ Requires-Dist: psutil
25
+ Requires-Dist: bitsandbytes
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8.0; extra == "dev"
28
+ Requires-Dist: peft>=0.21; extra == "dev"
29
+ Dynamic: license-file
30
+
31
+ # stream-finetune — fine-tune a 32B model on a 6 GB laptop GPU
32
+
33
+ A small training engine that fine-tunes models that **do not fit in VRAM**. The NF4
34
+ weights are mmap'd from disk, one transformer block is materialised on the GPU at a
35
+ time, and the backward recomputes through it. The base stays frozen; a LoRA adapter is
36
+ trained.
37
+
38
+ **Try it without installing anything:** [Will it fit?](https://huggingface.co/spaces/byjameson/will-it-fit)
39
+ — type a model id and a VRAM size; it says whether the whole model fits and shows the
40
+ measured speed where a measurement exists (and says so where it doesn't).
41
+
42
+ Measured on one machine — **RTX 3050 Laptop, 6 GB VRAM, 34 GB RAM, NVMe, Windows 11
43
+ (WDDM), AC power**:
44
+
45
+ | seq (LM path) | step | tokens/s | peak VRAM |
46
+ |---:|---:|---:|---:|
47
+ | 128 | 10.70 s | 12.0 | 3.18 GB |
48
+ | 512 | 17.95 s | 28.5 | 3.82 GB |
49
+ | **1024** | **27.91 s** | **36.7** | **4.61 GB** |
50
+ | 2048 | 64.53 s | 31.7 | 6.60–7.93 GB (spills) |
51
+
52
+ The 32B weights are **19.2 GB**. They never fully live in VRAM. At 1024 tokens/step the
53
+ throughput is **3.1×** the 128-token rate, because the per-step I/O is amortised over
54
+ more tokens. 50 steps of Dolly-15k SFT moved eval loss **1.8755 → 0.9661**.
55
+
56
+ **Start with [`GUIDE_6GB_32B.md`](GUIDE_6GB_32B.md)** — the practical write-up: presets,
57
+ the six traps, and which numbers are real. The measurement protocol, the discarded rows
58
+ and the correction of an earlier wrong claim ("8.6× / 100 tok/s") are in
59
+ [`LONGCTX_FINDINGS.md`](LONGCTX_FINDINGS.md).
60
+
61
+ ## Quick start
62
+
63
+ ```powershell
64
+ pip install -e . # or: pip install streamft (once it is on PyPI)
65
+
66
+ # 0) will it fit? memory arithmetic + the measured reference, no weights downloaded
67
+ streamft fit --model Qwen/Qwen2.5-14B-Instruct --gpu 6 --seq 512
68
+ streamft doctor # torch / CUDA / bitsandbytes / free VRAM
69
+
70
+ # 1) token shards for the LM path (SFT on Dolly needs none of this)
71
+ streamft prepare --model models/Qwen2.5-0.5B --save-quant checkpoints/qwen0.5b_nf4
72
+ python scripts/prep_data.py --model models/Qwen2.5-0.5B
73
+
74
+ # 1) turn a local bf16 model into a pre-quantized NF4 checkpoint (once)
75
+ python -m streamft.cli --prepare --model models/Qwen2.5-32B-Instruct `
76
+ --save-quant checkpoints/qwen32b_nf4
77
+
78
+ # 2) fine-tune it, streaming from disk
79
+ python tools/finetune.py --model checkpoints/qwen32b_nf4 --mode lm --seq 1024 --steps 50
80
+ python tools/finetune.py --model checkpoints/qwen14b_nf4 --data dolly --seq 512 --steps 240 `
81
+ --save checkpoints/my_adapter
82
+
83
+ # show the exact engine command without running it
84
+ python tools/finetune.py ... --print-only
85
+
86
+ # CPU smoke tests (5 cases, no GPU, no downloads)
87
+ pip install -r requirements-dev.txt; python -m pytest tests -q
88
+ ```
89
+
90
+ `tools/finetune.py` encodes the VRAM presets we measured, because the *combination* of
91
+ flags decides whether a run fits — not any single flag:
92
+
93
+ | model | resident blocks | shared tensors | hard cap |
94
+ |---|---:|---|---:|
95
+ | 32B, ≤2k seq | 2 | `as-stored` | 5.3 GB |
96
+ | 32B, >2k seq | 1 | `as-stored` | 5.3 GB |
97
+ | 14B | 6 | `as-stored` | 5.2 GB |
98
+ | 7B / 3B / 1.5B | autotune | — | — |
99
+
100
+ ## How it works
101
+
102
+ ```
103
+ disk (NF4 safetensors)
104
+ └─ mmap ──► one decoder block at a time ──► GPU ──► forward ──► drop
105
+ │
106
+ recompute in backward
107
+ ```
108
+
109
+ - `streamft/disk_quant_store.py` — mmap'd checkpoint, block loader, LRU layer cache
110
+ (GPU or host RAM for the recompute inputs).
111
+ - `streamft/stream_quant_lora.py` — the streaming trainer: base frozen, LoRA trainable,
112
+ loss chunked over the sequence so activations stay small.
113
+ - `streamft/stream_train.py` — per-layer forward hooks, chunked loss/backward,
114
+ streaming eval CE.
115
+ - `streamft/autotune.py` — chooses the resident-block window from measured VRAM/RAM.
116
+ - `streamft/data.py`, `streamft/metrics.py`, `streamft/lora.py` — samplers, append-only
117
+ JSONL log, minimal LoRA.
118
+ - `streamft/cli.py` — the engine CLI (`--prepare`, SFT/LM modes, `--eval_seq`,
119
+ `--activation-cache cpu`, `--head nf4`).
120
+
121
+ The training tag in the evidence log is still `exp54_disk_qlora`, so rows written by
122
+ this engine and by the original experiment are directly comparable.
123
+
124
+ ## Verification
125
+
126
+ `python scripts/verify_gradients.py --model-dir checkpoints/qwen3b_bnb4bit --seq 128`
127
+
128
+ - **One resident block vs all 36 resident** (identical adapter weights, Qwen2.5-3B NF4,
129
+ seq 128): the loss is identical to 10 decimals and the adapter gradients are
130
+ **bit-identical** — 288 tensors, max relative error `0.0`, global relative error `0.0`.
131
+ This is the streaming/recompute claim: dropping blocks and recomputing them in the
132
+ backward changes nothing.
133
+ - **Manual block backward vs PyTorch autograd** through the same forward: global relative
134
+ error **1.33%** (cosine `0.9998`). The same 1.33% appears with *all* blocks resident,
135
+ so it is bf16 dequantisation precision, not the streaming path.
136
+ - Finite differences cannot check this on a 4-bit base: the LoRA delta is added to a bf16
137
+ tensor, so a one-entry perturbation is below one bf16 ULP (measured for the same entry:
138
+ `eps=2e-3` → `-0.12`, `eps=2e-2` → `+0.02`). The chunked-loss gradient itself is
139
+ checked exactly in `tests/test_streamft_smoke.py`.
140
+
141
+ Raw output: `results/gradcheck.json` (git-ignored; the numbers above are copied from it).
142
+
143
+ ## Compared with PEFT (matched runs)
144
+
145
+ Same data, same LoRA (r=8, α=16, q/k/v/o), same AdamW lr 1e-4, batch 1, same prompt
146
+ masking, same token-weighted masked eval on 8 held-out rows. PEFT uses
147
+ `prepare_model_for_kbit_training` with gradient checkpointing (the standard QLoRA setup).
148
+
149
+ **3B (fits entirely):**
150
+
151
+ | | streamft (36 blocks resident) | PEFT / transformers |
152
+ |---|---:|---:|
153
+ | step time, median of 30 @ seq 512 | 0.938 s | **0.579 s** |
154
+ | peak VRAM | **3.85 GB** | 4.90 GB |
155
+ | eval before | 1.42087 | 1.42087 |
156
+ | eval after | **1.30154** (−0.1193) | 1.32722 (−0.0936) |
157
+
158
+ **7B and 14B (do not fit comfortably):**
159
+
160
+ | model | step time, median @ seq 512 | peak VRAM | eval (start → after) |
161
+ |---|---:|---:|---:|
162
+ | 7B, streamft (28 blocks) | **1.70 s** | **5.51 GB** | 2.1928 → 1.1094 |
163
+ | 7B, PEFT | 8.96 s | 10.05 GB | 2.1914 → 1.1491 |
164
+ | 14B, streamft (6 blocks) | **6.73 s** | **3.86 GB** | 2.3748 → 1.5312 (10 steps) |
165
+ | 14B, PEFT | 18.56 s | 16.48 GB | 2.3748 (3 steps only) |
166
+
167
+ At 7B the crossover has already happened: PEFT's 4.4 GB of weights plus activations no
168
+ longer fit, so it spills (10.05 GB reserved) and pays 5.3× the step time. Note that
169
+ streamft's 7B peak (5.51 GB) is *higher* than its 14B peak (3.86 GB) because the 7B run
170
+ kept all 28 blocks resident while the 14B run kept 6 — the window is a choice, and the
171
+ 14B number is the one to copy for a 6 GB card.
172
+
173
+ **32B: PEFT does not reach the first step.** The 19.2 GB of weights load only by spilling
174
+ into shared memory (19.45 GB reserved on a 6 GiB card), and `prepare_model_for_kbit_training`
175
+ then dies allocating the fp32 norm copy: *CUDA out of memory. Tried to allocate 2.90 GiB;
176
+ 18.00 GiB is already allocated.* The streaming engine runs the same checkpoint at
177
+ **12.5 s/step and 3.90 GB** (64/64 layers, held-out 1.8755 to 0.9959).
178
+
179
+ The 3B row is the honest answer to "is this faster?": **no** — on a model that fits, PEFT
180
+ is 1.6× faster and uses 1.05 GB more. The 14B row is the design point: PEFT does not
181
+ OOM on Windows, it **spills into shared system memory** (16.48 GB reserved on a 6.4 GB
182
+ card) and pays 2.8× the step time; streamft stays at 3.86 GB. Over 30 steps and one seed
183
+ the 3B quality difference is not meaningful.
184
+
185
+ Reproduce: `python scripts/compare_peft.py --model checkpoints/qwen3b_bnb4bit --steps 30`
186
+ and `... --model checkpoints/qwen14b_nf4 --steps 3`.
187
+
188
+ ## What this does not do
189
+
190
+ - **2k context does not fit** on 6 GB. It is ~0.6 GB short: with `--activation-cache cpu`
191
+ the 2k peak is 6.60 GB, and what remains is the bf16 `lm_head` (1.56 GB). The 4-bit
192
+ `--head nf4` path is **broken in the bitsandbytes version here**.
193
+ - **Longer context is not a quality lever by itself.** At a matched token budget
194
+ (122,880 tokens), training at seq 512 gave a 2k-context CE of 1.2833; training at
195
+ seq 2048 gave 1.3033. Data volume is the lever.
196
+ - **SFT `--seq` is a truncation cap**, not a padding target: one dataset row per step.
197
+ Dolly rows are 43–1964 tokens, median 134. Never compute tokens/s from `--seq` on
198
+ that path.
199
+ - Speed numbers are medians of warm runs, AC power only. Battery/Modern-Standby runs are
200
+ invalid and were discarded.
201
+ - Single GPU, Windows/WDDM. Linux should work (no WDDM shared-memory cliff) but is not
202
+ measured here.
203
+
204
+ ## Repository layout
205
+
206
+ ```
207
+ streamft/ the engine (package)
208
+ tools/finetune.py one-command wrapper with the measured presets
209
+ scripts/ longctx_sweep.py — the LM-path measurement sweep
210
+ GUIDE_6GB_32B.md practical guide
211
+ LONGCTX_FINDINGS.md protocol, curve, the 2k wall, the E4 result, corrections
212
+ LONGCTX_PLAN.md the measurement plan
213
+ ARTICLE.md the DEV Community post source
214
+ results/ the engine appends results.jsonl here (git-ignored)
215
+ ```
216
+
217
+ ## The research line
218
+
219
+ This repository grew out of an attempt to replace dense attention with a **learned
220
+ block-sparse router**. That line is mostly a negative result and now lives on its own, as
221
+ an archive: **[byjameson/lcga-local](https://github.com/byjameson/lcga-local)** —
222
+ [`LCGA_FINDINGS.md`](https://github.com/byjameson/lcga-local/blob/main/LCGA_FINDINGS.md),
223
+ all experiments, every raw JSON, and the three measurement errors that were corrected.
224
+ The short version: on plain language modelling the learned router ties a budget-matched
225
+ static window; on retrieval data it reaches dense exact match at a 25% block budget.
226
+
227
+ History: this repository was previously named `lcga-local`; GitHub redirects the old
228
+ URLs. The engine lived at `experiments/exp54_disk_qlora.py` + `lcsa/` until the split.
229
+
230
+ ## Articles
231
+
232
+ - [Fine-tuning a 32B model on a 6 GB laptop GPU — and the three measurements that nearly fooled me](https://dev.to/mycute_97b39c4e34027570f9/fine-tuning-a-32b-model-on-a-6-gb-laptop-gpu-and-the-three-measurements-that-nearly-fooled-me-2ojm)
233
+ - [Running a 4B model in 6 GB and training 3B parameters on a laptop](https://dev.to/mycute_97b39c4e34027570f9/running-a-4b-model-in-6-gb-and-training-3b-parameters-on-a-laptop-4al)
234
+ - [The same 32B post on Medium](https://medium.com/@myredditacc9393/fine-tuning-a-32b-model-on-a-6-gb-laptop-gpu-e749df447229)
235
+ - [Will it fit? A free planner for fine-tuning models that do not fit](https://dev.to/mycute_97b39c4e34027570f9/will-it-fit-a-free-planner-for-fine-tuning-models-that-do-not-fit-on-your-gpu-o50)
236
+
237
+ ## License
238
+
239
+ Apache-2.0 — see [`LICENSE`](LICENSE).
@@ -0,0 +1,209 @@
1
+ # stream-finetune — fine-tune a 32B model on a 6 GB laptop GPU
2
+
3
+ A small training engine that fine-tunes models that **do not fit in VRAM**. The NF4
4
+ weights are mmap'd from disk, one transformer block is materialised on the GPU at a
5
+ time, and the backward recomputes through it. The base stays frozen; a LoRA adapter is
6
+ trained.
7
+
8
+ **Try it without installing anything:** [Will it fit?](https://huggingface.co/spaces/byjameson/will-it-fit)
9
+ — type a model id and a VRAM size; it says whether the whole model fits and shows the
10
+ measured speed where a measurement exists (and says so where it doesn't).
11
+
12
+ Measured on one machine — **RTX 3050 Laptop, 6 GB VRAM, 34 GB RAM, NVMe, Windows 11
13
+ (WDDM), AC power**:
14
+
15
+ | seq (LM path) | step | tokens/s | peak VRAM |
16
+ |---:|---:|---:|---:|
17
+ | 128 | 10.70 s | 12.0 | 3.18 GB |
18
+ | 512 | 17.95 s | 28.5 | 3.82 GB |
19
+ | **1024** | **27.91 s** | **36.7** | **4.61 GB** |
20
+ | 2048 | 64.53 s | 31.7 | 6.60–7.93 GB (spills) |
21
+
22
+ The 32B weights are **19.2 GB**. They never fully live in VRAM. At 1024 tokens/step the
23
+ throughput is **3.1×** the 128-token rate, because the per-step I/O is amortised over
24
+ more tokens. 50 steps of Dolly-15k SFT moved eval loss **1.8755 → 0.9661**.
25
+
26
+ **Start with [`GUIDE_6GB_32B.md`](GUIDE_6GB_32B.md)** — the practical write-up: presets,
27
+ the six traps, and which numbers are real. The measurement protocol, the discarded rows
28
+ and the correction of an earlier wrong claim ("8.6× / 100 tok/s") are in
29
+ [`LONGCTX_FINDINGS.md`](LONGCTX_FINDINGS.md).
30
+
31
+ ## Quick start
32
+
33
+ ```powershell
34
+ pip install -e . # or: pip install streamft (once it is on PyPI)
35
+
36
+ # 0) will it fit? memory arithmetic + the measured reference, no weights downloaded
37
+ streamft fit --model Qwen/Qwen2.5-14B-Instruct --gpu 6 --seq 512
38
+ streamft doctor # torch / CUDA / bitsandbytes / free VRAM
39
+
40
+ # 1) token shards for the LM path (SFT on Dolly needs none of this)
41
+ streamft prepare --model models/Qwen2.5-0.5B --save-quant checkpoints/qwen0.5b_nf4
42
+ python scripts/prep_data.py --model models/Qwen2.5-0.5B
43
+
44
+ # 1) turn a local bf16 model into a pre-quantized NF4 checkpoint (once)
45
+ python -m streamft.cli --prepare --model models/Qwen2.5-32B-Instruct `
46
+ --save-quant checkpoints/qwen32b_nf4
47
+
48
+ # 2) fine-tune it, streaming from disk
49
+ python tools/finetune.py --model checkpoints/qwen32b_nf4 --mode lm --seq 1024 --steps 50
50
+ python tools/finetune.py --model checkpoints/qwen14b_nf4 --data dolly --seq 512 --steps 240 `
51
+ --save checkpoints/my_adapter
52
+
53
+ # show the exact engine command without running it
54
+ python tools/finetune.py ... --print-only
55
+
56
+ # CPU smoke tests (5 cases, no GPU, no downloads)
57
+ pip install -r requirements-dev.txt; python -m pytest tests -q
58
+ ```
59
+
60
+ `tools/finetune.py` encodes the VRAM presets we measured, because the *combination* of
61
+ flags decides whether a run fits — not any single flag:
62
+
63
+ | model | resident blocks | shared tensors | hard cap |
64
+ |---|---:|---|---:|
65
+ | 32B, ≤2k seq | 2 | `as-stored` | 5.3 GB |
66
+ | 32B, >2k seq | 1 | `as-stored` | 5.3 GB |
67
+ | 14B | 6 | `as-stored` | 5.2 GB |
68
+ | 7B / 3B / 1.5B | autotune | — | — |
69
+
70
+ ## How it works
71
+
72
+ ```
73
+ disk (NF4 safetensors)
74
+ └─ mmap ──► one decoder block at a time ──► GPU ──► forward ──► drop
75
+ │
76
+ recompute in backward
77
+ ```
78
+
79
+ - `streamft/disk_quant_store.py` — mmap'd checkpoint, block loader, LRU layer cache
80
+ (GPU or host RAM for the recompute inputs).
81
+ - `streamft/stream_quant_lora.py` — the streaming trainer: base frozen, LoRA trainable,
82
+ loss chunked over the sequence so activations stay small.
83
+ - `streamft/stream_train.py` — per-layer forward hooks, chunked loss/backward,
84
+ streaming eval CE.
85
+ - `streamft/autotune.py` — chooses the resident-block window from measured VRAM/RAM.
86
+ - `streamft/data.py`, `streamft/metrics.py`, `streamft/lora.py` — samplers, append-only
87
+ JSONL log, minimal LoRA.
88
+ - `streamft/cli.py` — the engine CLI (`--prepare`, SFT/LM modes, `--eval_seq`,
89
+ `--activation-cache cpu`, `--head nf4`).
90
+
91
+ The training tag in the evidence log is still `exp54_disk_qlora`, so rows written by
92
+ this engine and by the original experiment are directly comparable.
93
+
94
+ ## Verification
95
+
96
+ `python scripts/verify_gradients.py --model-dir checkpoints/qwen3b_bnb4bit --seq 128`
97
+
98
+ - **One resident block vs all 36 resident** (identical adapter weights, Qwen2.5-3B NF4,
99
+ seq 128): the loss is identical to 10 decimals and the adapter gradients are
100
+ **bit-identical** — 288 tensors, max relative error `0.0`, global relative error `0.0`.
101
+ This is the streaming/recompute claim: dropping blocks and recomputing them in the
102
+ backward changes nothing.
103
+ - **Manual block backward vs PyTorch autograd** through the same forward: global relative
104
+ error **1.33%** (cosine `0.9998`). The same 1.33% appears with *all* blocks resident,
105
+ so it is bf16 dequantisation precision, not the streaming path.
106
+ - Finite differences cannot check this on a 4-bit base: the LoRA delta is added to a bf16
107
+ tensor, so a one-entry perturbation is below one bf16 ULP (measured for the same entry:
108
+ `eps=2e-3` → `-0.12`, `eps=2e-2` → `+0.02`). The chunked-loss gradient itself is
109
+ checked exactly in `tests/test_streamft_smoke.py`.
110
+
111
+ Raw output: `results/gradcheck.json` (git-ignored; the numbers above are copied from it).
112
+
113
+ ## Compared with PEFT (matched runs)
114
+
115
+ Same data, same LoRA (r=8, α=16, q/k/v/o), same AdamW lr 1e-4, batch 1, same prompt
116
+ masking, same token-weighted masked eval on 8 held-out rows. PEFT uses
117
+ `prepare_model_for_kbit_training` with gradient checkpointing (the standard QLoRA setup).
118
+
119
+ **3B (fits entirely):**
120
+
121
+ | | streamft (36 blocks resident) | PEFT / transformers |
122
+ |---|---:|---:|
123
+ | step time, median of 30 @ seq 512 | 0.938 s | **0.579 s** |
124
+ | peak VRAM | **3.85 GB** | 4.90 GB |
125
+ | eval before | 1.42087 | 1.42087 |
126
+ | eval after | **1.30154** (−0.1193) | 1.32722 (−0.0936) |
127
+
128
+ **7B and 14B (do not fit comfortably):**
129
+
130
+ | model | step time, median @ seq 512 | peak VRAM | eval (start → after) |
131
+ |---|---:|---:|---:|
132
+ | 7B, streamft (28 blocks) | **1.70 s** | **5.51 GB** | 2.1928 → 1.1094 |
133
+ | 7B, PEFT | 8.96 s | 10.05 GB | 2.1914 → 1.1491 |
134
+ | 14B, streamft (6 blocks) | **6.73 s** | **3.86 GB** | 2.3748 → 1.5312 (10 steps) |
135
+ | 14B, PEFT | 18.56 s | 16.48 GB | 2.3748 (3 steps only) |
136
+
137
+ At 7B the crossover has already happened: PEFT's 4.4 GB of weights plus activations no
138
+ longer fit, so it spills (10.05 GB reserved) and pays 5.3× the step time. Note that
139
+ streamft's 7B peak (5.51 GB) is *higher* than its 14B peak (3.86 GB) because the 7B run
140
+ kept all 28 blocks resident while the 14B run kept 6 — the window is a choice, and the
141
+ 14B number is the one to copy for a 6 GB card.
142
+
143
+ **32B: PEFT does not reach the first step.** The 19.2 GB of weights load only by spilling
144
+ into shared memory (19.45 GB reserved on a 6 GiB card), and `prepare_model_for_kbit_training`
145
+ then dies allocating the fp32 norm copy: *CUDA out of memory. Tried to allocate 2.90 GiB;
146
+ 18.00 GiB is already allocated.* The streaming engine runs the same checkpoint at
147
+ **12.5 s/step and 3.90 GB** (64/64 layers, held-out 1.8755 to 0.9959).
148
+
149
+ The 3B row is the honest answer to "is this faster?": **no** — on a model that fits, PEFT
150
+ is 1.6× faster and uses 1.05 GB more. The 14B row is the design point: PEFT does not
151
+ OOM on Windows, it **spills into shared system memory** (16.48 GB reserved on a 6.4 GB
152
+ card) and pays 2.8× the step time; streamft stays at 3.86 GB. Over 30 steps and one seed
153
+ the 3B quality difference is not meaningful.
154
+
155
+ Reproduce: `python scripts/compare_peft.py --model checkpoints/qwen3b_bnb4bit --steps 30`
156
+ and `... --model checkpoints/qwen14b_nf4 --steps 3`.
157
+
158
+ ## What this does not do
159
+
160
+ - **2k context does not fit** on 6 GB. It is ~0.6 GB short: with `--activation-cache cpu`
161
+ the 2k peak is 6.60 GB, and what remains is the bf16 `lm_head` (1.56 GB). The 4-bit
162
+ `--head nf4` path is **broken in the bitsandbytes version here**.
163
+ - **Longer context is not a quality lever by itself.** At a matched token budget
164
+ (122,880 tokens), training at seq 512 gave a 2k-context CE of 1.2833; training at
165
+ seq 2048 gave 1.3033. Data volume is the lever.
166
+ - **SFT `--seq` is a truncation cap**, not a padding target: one dataset row per step.
167
+ Dolly rows are 43–1964 tokens, median 134. Never compute tokens/s from `--seq` on
168
+ that path.
169
+ - Speed numbers are medians of warm runs, AC power only. Battery/Modern-Standby runs are
170
+ invalid and were discarded.
171
+ - Single GPU, Windows/WDDM. Linux should work (no WDDM shared-memory cliff) but is not
172
+ measured here.
173
+
174
+ ## Repository layout
175
+
176
+ ```
177
+ streamft/ the engine (package)
178
+ tools/finetune.py one-command wrapper with the measured presets
179
+ scripts/ longctx_sweep.py — the LM-path measurement sweep
180
+ GUIDE_6GB_32B.md practical guide
181
+ LONGCTX_FINDINGS.md protocol, curve, the 2k wall, the E4 result, corrections
182
+ LONGCTX_PLAN.md the measurement plan
183
+ ARTICLE.md the DEV Community post source
184
+ results/ the engine appends results.jsonl here (git-ignored)
185
+ ```
186
+
187
+ ## The research line
188
+
189
+ This repository grew out of an attempt to replace dense attention with a **learned
190
+ block-sparse router**. That line is mostly a negative result and now lives on its own, as
191
+ an archive: **[byjameson/lcga-local](https://github.com/byjameson/lcga-local)** —
192
+ [`LCGA_FINDINGS.md`](https://github.com/byjameson/lcga-local/blob/main/LCGA_FINDINGS.md),
193
+ all experiments, every raw JSON, and the three measurement errors that were corrected.
194
+ The short version: on plain language modelling the learned router ties a budget-matched
195
+ static window; on retrieval data it reaches dense exact match at a 25% block budget.
196
+
197
+ History: this repository was previously named `lcga-local`; GitHub redirects the old
198
+ URLs. The engine lived at `experiments/exp54_disk_qlora.py` + `lcsa/` until the split.
199
+
200
+ ## Articles
201
+
202
+ - [Fine-tuning a 32B model on a 6 GB laptop GPU — and the three measurements that nearly fooled me](https://dev.to/mycute_97b39c4e34027570f9/fine-tuning-a-32b-model-on-a-6-gb-laptop-gpu-and-the-three-measurements-that-nearly-fooled-me-2ojm)
203
+ - [Running a 4B model in 6 GB and training 3B parameters on a laptop](https://dev.to/mycute_97b39c4e34027570f9/running-a-4b-model-in-6-gb-and-training-3b-parameters-on-a-laptop-4al)
204
+ - [The same 32B post on Medium](https://medium.com/@myredditacc9393/fine-tuning-a-32b-model-on-a-6-gb-laptop-gpu-e749df447229)
205
+ - [Will it fit? A free planner for fine-tuning models that do not fit](https://dev.to/mycute_97b39c4e34027570f9/will-it-fit-a-free-planner-for-fine-tuning-models-that-do-not-fit-on-your-gpu-o50)
206
+
207
+ ## License
208
+
209
+ Apache-2.0 — see [`LICENSE`](LICENSE).
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "streamft"
7
+ version = "0.2.0"
8
+ description = "Fine-tune models that do not fit: stream NF4 weights from disk, one transformer block at a time."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "Apache-2.0" }
12
+ authors = [{ name = "Gokhan Cava", email = "byjameson@users.noreply.github.com" }]
13
+ keywords = ["fine-tuning", "qlora", "nf4", "low-vram", "lora", "llm"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: Apache Software License",
17
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
18
+ ]
19
+ dependencies = [
20
+ "torch",
21
+ "transformers>=4.40",
22
+ "safetensors",
23
+ "tokenizers",
24
+ "accelerate",
25
+ "datasets",
26
+ "huggingface-hub",
27
+ "numpy",
28
+ "psutil",
29
+ "bitsandbytes",
30
+ ]
31
+
32
+ [project.optional-dependencies]
33
+ dev = ["pytest>=8.0", "peft>=0.21"]
34
+
35
+ [project.scripts]
36
+ streamft = "streamft.__main__:main"
37
+
38
+ [project.urls]
39
+ Homepage = "https://github.com/byjameson/stream-finetune"
40
+ Issues = "https://github.com/byjameson/stream-finetune/issues"
41
+
42
+ [tool.setuptools]
43
+ packages = ["streamft"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,32 @@
1
+ """streamft — fine-tune models that do not fit, one block at a time.
2
+
3
+ The engine memory-maps a pre-quantized NF4 checkpoint, materialises one transformer
4
+ block on the GPU at a time (with recompute in the backward) and keeps a LoRA adapter
5
+ trainable. Measured on a 6 GB RTX 3050 Laptop: a 32B model at 1024 tokens/step,
6
+ 36.7 tok/s, 4.61 GB peak.
7
+
8
+ Modules:
9
+
10
+ cli command line entry point (``python -m streamft.cli --help``)
11
+ disk_quant_store mmap'd NF4 checkpoint, block loader, layer cache
12
+ stream_quant_lora streaming trainer (base frozen, LoRA trainable)
13
+ stream_train chunked loss/backward, per-layer forward hooks, streaming eval
14
+ autotune chooses the resident-block window from VRAM/RAM
15
+ data token shards and the train/eval samplers
16
+ metrics append-only JSONL evidence log
17
+ lora minimal LoRA layers and adapter state dicts
18
+
19
+ The training tag written to the evidence log stays ``exp54_disk_qlora`` so that old
20
+ and new rows are directly comparable.
21
+ """
22
+
23
+ __all__ = [
24
+ "autotune",
25
+ "cli",
26
+ "data",
27
+ "disk_quant_store",
28
+ "lora",
29
+ "metrics",
30
+ "stream_quant_lora",
31
+ "stream_train",
32
+ ]
@@ -0,0 +1,59 @@
1
+ """Console entry point: `streamft <command>`.
2
+
3
+ streamft fit will this model fit? (memory arithmetic + measured speed when known)
4
+ streamft prepare turn a bf16 model into a pre-quantized NF4 checkpoint
5
+ streamft train the streaming trainer (all exp54 flags)
6
+ streamft doctor environment report (torch, CUDA, bitsandbytes, VRAM)
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import sys
11
+
12
+ USAGE = """streamft - fine-tune models that do not fit
13
+
14
+ streamft fit --model <dir|hf-id> [--gpu 6] [--seq 512]
15
+ streamft prepare --model <dir> --save-quant checkpoints/<name>
16
+ streamft train --model-dir checkpoints/<name> --sft --steps 30 [--seq 1024]
17
+ streamft doctor
18
+
19
+ Docs: https://github.com/byjameson/stream-finetune
20
+ """
21
+
22
+
23
+ def main() -> int:
24
+ argv = sys.argv[1:]
25
+ if not argv or argv[0] in ("-h", "--help", "help"):
26
+ print(USAGE)
27
+ return 0
28
+
29
+ cmd, rest = argv[0], argv[1:]
30
+
31
+ if cmd == "fit":
32
+ from streamft.fit import main as fit_main
33
+
34
+ return fit_main(rest)
35
+
36
+ if cmd == "doctor":
37
+ from streamft.doctor import main as doctor_main
38
+
39
+ return doctor_main(rest)
40
+
41
+ if cmd == "prepare":
42
+ from streamft import cli
43
+
44
+ sys.argv = ["streamft", "--prepare"] + rest
45
+ return cli.main()
46
+
47
+ if cmd == "train":
48
+ from streamft import cli
49
+
50
+ sys.argv = ["streamft"] + rest
51
+ return cli.main()
52
+
53
+ print(f"unknown command: {cmd}\n")
54
+ print(USAGE)
55
+ return 2
56
+
57
+
58
+ if __name__ == "__main__":
59
+ raise SystemExit(main())