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 +21 -0
- streamft-0.2.0/PKG-INFO +239 -0
- streamft-0.2.0/README.md +209 -0
- streamft-0.2.0/pyproject.toml +43 -0
- streamft-0.2.0/setup.cfg +4 -0
- streamft-0.2.0/streamft/__init__.py +32 -0
- streamft-0.2.0/streamft/__main__.py +59 -0
- streamft-0.2.0/streamft/autotune.py +236 -0
- streamft-0.2.0/streamft/cli.py +450 -0
- streamft-0.2.0/streamft/data.py +81 -0
- streamft-0.2.0/streamft/disk_quant_store.py +353 -0
- streamft-0.2.0/streamft/doctor.py +34 -0
- streamft-0.2.0/streamft/fit.py +128 -0
- streamft-0.2.0/streamft/lora.py +108 -0
- streamft-0.2.0/streamft/metrics.py +110 -0
- streamft-0.2.0/streamft/stream_quant_lora.py +216 -0
- streamft-0.2.0/streamft/stream_train.py +230 -0
- streamft-0.2.0/streamft.egg-info/PKG-INFO +239 -0
- streamft-0.2.0/streamft.egg-info/SOURCES.txt +22 -0
- streamft-0.2.0/streamft.egg-info/dependency_links.txt +1 -0
- streamft-0.2.0/streamft.egg-info/entry_points.txt +2 -0
- streamft-0.2.0/streamft.egg-info/requires.txt +14 -0
- streamft-0.2.0/streamft.egg-info/top_level.txt +1 -0
- streamft-0.2.0/tests/test_streamft_smoke.py +114 -0
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.
|
streamft-0.2.0/PKG-INFO
ADDED
|
@@ -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).
|
streamft-0.2.0/README.md
ADDED
|
@@ -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"]
|
streamft-0.2.0/setup.cfg
ADDED
|
@@ -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())
|