streamft 0.2.0__py3-none-any.whl

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.
@@ -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,18 @@
1
+ streamft/__init__.py,sha256=Qy4EBcSygbPSecAGW5vA_bdTu1lg6olpLNApdQ895Ko,1188
2
+ streamft/__main__.py,sha256=3hi3eVzHgmmqyNPgW9MVW78pugkujP5VHaDZyVDvmZs,1565
3
+ streamft/autotune.py,sha256=a4wMCr96o2UFAVYJpG7ZFdh0qa0p1q7hZa8C8eRG00c,9325
4
+ streamft/cli.py,sha256=YcBGDEDvAqQU4rBiNyjwS6kT_pJ_b6KFOAxUnKhAnTc,19816
5
+ streamft/data.py,sha256=lgbOgoQWkMNsxoG_VU-JgGHLbLgEutQ_1BQm7O4PnZk,2858
6
+ streamft/disk_quant_store.py,sha256=ZynGt5CHk8UVaCXHY0kqsKHi6Aj9G9raXw-SjYP9YAw,14353
7
+ streamft/doctor.py,sha256=8s5ubDODCScCDy9u14uE39v4A30O-laAj0we9leZlQE,1247
8
+ streamft/fit.py,sha256=GjxrTXwO_c4DCIvozmavuv4lYFyolE4bM35eQsi4uxI,5700
9
+ streamft/lora.py,sha256=qqwKfjpwGc66srjvJrsHF6pgyowCW_wVVRyWhi_NuVY,4378
10
+ streamft/metrics.py,sha256=68E6aQNCQqulEUPRTCaRNc11iW7dOxbl8uxO9cTRr0o,4220
11
+ streamft/stream_quant_lora.py,sha256=7Th3l7WmukKiNAb42c7QtvfD1QWN7b_BG01Ks41YDOI,9979
12
+ streamft/stream_train.py,sha256=E_HNl5FW7QYl_w4gqXY-6yRjRukUBVU0f8Lx5qe8ppo,9897
13
+ streamft-0.2.0.dist-info/licenses/LICENSE,sha256=-F5MsJDp8WbiSjRe2Rw6VH-rwYYKugBXisdHKU7ueE8,1074
14
+ streamft-0.2.0.dist-info/METADATA,sha256=mRR5ToRZYJVcpML0wM1GcjnfRjatfJsQHqVbPFZuxyI,12057
15
+ streamft-0.2.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
16
+ streamft-0.2.0.dist-info/entry_points.txt,sha256=U3czt3f8-nxScIZUsvkBvCjju172Hdaz8rPgLVw4nmU,52
17
+ streamft-0.2.0.dist-info/top_level.txt,sha256=8mZnSJ3vtGbxSF-ZX80u8q85sGhLdKzQF3kchAL8YT0,9
18
+ streamft-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ streamft = streamft.__main__:main
@@ -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 @@
1
+ streamft