streamft 0.2.0__tar.gz → 0.2.1__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/streamft.egg-info → streamft-0.2.1}/PKG-INFO +39 -17
- {streamft-0.2.0 → streamft-0.2.1}/README.md +38 -16
- {streamft-0.2.0 → streamft-0.2.1}/pyproject.toml +1 -1
- {streamft-0.2.0 → streamft-0.2.1}/streamft/stream_quant_lora.py +22 -10
- {streamft-0.2.0 → streamft-0.2.1/streamft.egg-info}/PKG-INFO +39 -17
- {streamft-0.2.0 → streamft-0.2.1}/tests/test_streamft_smoke.py +50 -0
- {streamft-0.2.0 → streamft-0.2.1}/LICENSE +0 -0
- {streamft-0.2.0 → streamft-0.2.1}/setup.cfg +0 -0
- {streamft-0.2.0 → streamft-0.2.1}/streamft/__init__.py +0 -0
- {streamft-0.2.0 → streamft-0.2.1}/streamft/__main__.py +0 -0
- {streamft-0.2.0 → streamft-0.2.1}/streamft/autotune.py +0 -0
- {streamft-0.2.0 → streamft-0.2.1}/streamft/cli.py +0 -0
- {streamft-0.2.0 → streamft-0.2.1}/streamft/data.py +0 -0
- {streamft-0.2.0 → streamft-0.2.1}/streamft/disk_quant_store.py +0 -0
- {streamft-0.2.0 → streamft-0.2.1}/streamft/doctor.py +0 -0
- {streamft-0.2.0 → streamft-0.2.1}/streamft/fit.py +0 -0
- {streamft-0.2.0 → streamft-0.2.1}/streamft/lora.py +0 -0
- {streamft-0.2.0 → streamft-0.2.1}/streamft/metrics.py +0 -0
- {streamft-0.2.0 → streamft-0.2.1}/streamft/stream_train.py +0 -0
- {streamft-0.2.0 → streamft-0.2.1}/streamft.egg-info/SOURCES.txt +0 -0
- {streamft-0.2.0 → streamft-0.2.1}/streamft.egg-info/dependency_links.txt +0 -0
- {streamft-0.2.0 → streamft-0.2.1}/streamft.egg-info/entry_points.txt +0 -0
- {streamft-0.2.0 → streamft-0.2.1}/streamft.egg-info/requires.txt +0 -0
- {streamft-0.2.0 → streamft-0.2.1}/streamft.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: streamft
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Fine-tune models that do not fit: stream NF4 weights from disk, one transformer block at a time.
|
|
5
5
|
Author-email: Gokhan Cava <byjameson@users.noreply.github.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -30,6 +30,8 @@ Dynamic: license-file
|
|
|
30
30
|
|
|
31
31
|
# stream-finetune — fine-tune a 32B model on a 6 GB laptop GPU
|
|
32
32
|
|
|
33
|
+
[](https://pypi.org/project/streamft/) [](https://huggingface.co/spaces/byjameson/will-it-fit)
|
|
34
|
+
|
|
33
35
|
A small training engine that fine-tunes models that **do not fit in VRAM**. The NF4
|
|
34
36
|
weights are mmap'd from disk, one transformer block is materialised on the GPU at a
|
|
35
37
|
time, and the backward recomputes through it. The base stays frozen; a LoRA adapter is
|
|
@@ -61,7 +63,7 @@ and the correction of an earlier wrong claim ("8.6× / 100 tok/s") are in
|
|
|
61
63
|
## Quick start
|
|
62
64
|
|
|
63
65
|
```powershell
|
|
64
|
-
pip install
|
|
66
|
+
pip install streamft
|
|
65
67
|
|
|
66
68
|
# 0) will it fit? memory arithmetic + the measured reference, no weights downloaded
|
|
67
69
|
streamft fit --model Qwen/Qwen2.5-14B-Instruct --gpu 6 --seq 512
|
|
@@ -142,30 +144,33 @@ Raw output: `results/gradcheck.json` (git-ignored; the numbers above are copied
|
|
|
142
144
|
|
|
143
145
|
## Compared with PEFT (matched runs)
|
|
144
146
|
|
|
145
|
-
Same data, same LoRA (r=8,
|
|
147
|
+
Same data, same LoRA (r=8, alpha=16, q/k/v/o), same AdamW lr 1e-4, batch 1, same prompt
|
|
146
148
|
masking, same token-weighted masked eval on 8 held-out rows. PEFT uses
|
|
147
149
|
`prepare_model_for_kbit_training` with gradient checkpointing (the standard QLoRA setup).
|
|
150
|
+
Step times move by up to ~30% between runs on this laptop; quality is the stable number.
|
|
148
151
|
|
|
149
152
|
**3B (fits entirely):**
|
|
150
153
|
|
|
151
154
|
| | streamft (36 blocks resident) | PEFT / transformers |
|
|
152
155
|
|---|---:|---:|
|
|
153
|
-
| step time, median of 30 @ seq 512 | 0.
|
|
156
|
+
| step time, median of 30 @ seq 512 | 0.94–1.24 s | **0.58–0.88 s** |
|
|
154
157
|
| peak VRAM | **3.85 GB** | 4.90 GB |
|
|
155
158
|
| eval before | 1.42087 | 1.42087 |
|
|
156
|
-
| eval after | **1.
|
|
159
|
+
| eval after | **1.30720** (−0.1137) | 1.33002 (−0.0909) |
|
|
157
160
|
|
|
158
|
-
**7B
|
|
161
|
+
**7B / 14B / 32B (do not fit comfortably), after the masked-gradient fix:**
|
|
159
162
|
|
|
160
|
-
| model | step time, median @ seq 512 | peak VRAM | eval (start → after) |
|
|
163
|
+
| model | step time, median @ seq 512 | peak VRAM | eval (start → after, 30 steps) |
|
|
161
164
|
|---|---:|---:|---:|
|
|
162
|
-
| 7B, streamft (28 blocks) | **1.
|
|
165
|
+
| 7B, streamft (28 blocks) | **1.55 s** | **5.51 GB** | 2.1928 → **1.1055** |
|
|
163
166
|
| 7B, PEFT | 8.96 s | 10.05 GB | 2.1914 → 1.1491 |
|
|
164
|
-
| 14B, streamft (6 blocks) | **
|
|
165
|
-
| 14B, PEFT | 18.56 s | 16.48 GB |
|
|
167
|
+
| 14B, streamft (6 blocks) | **5.86 s** | **3.86 GB** | 2.3748 → **0.9742** |
|
|
168
|
+
| 14B, PEFT | 18.56 s | 16.48 GB | 3 steps only |
|
|
169
|
+
| 32B, streamft (2 blocks, seq 1024) | **12.48 s** | **3.90 GB** | 1.8755 → **1.0008** |
|
|
170
|
+
| 32B, PEFT | fails before step 1 | — | — |
|
|
166
171
|
|
|
167
172
|
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.
|
|
173
|
+
longer fit, so it spills (10.05 GB reserved) and pays ~5.8× the step time. Note that
|
|
169
174
|
streamft's 7B peak (5.51 GB) is *higher* than its 14B peak (3.86 GB) because the 7B run
|
|
170
175
|
kept all 28 blocks resident while the 14B run kept 6 — the window is a choice, and the
|
|
171
176
|
14B number is the one to copy for a 6 GB card.
|
|
@@ -174,17 +179,34 @@ kept all 28 blocks resident while the 14B run kept 6 — the window is a choice,
|
|
|
174
179
|
into shared memory (19.45 GB reserved on a 6 GiB card), and `prepare_model_for_kbit_training`
|
|
175
180
|
then dies allocating the fp32 norm copy: *CUDA out of memory. Tried to allocate 2.90 GiB;
|
|
176
181
|
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
|
|
182
|
+
**12.5 s/step and 3.90 GB** (64/64 layers).
|
|
178
183
|
|
|
179
184
|
The 3B row is the honest answer to "is this faster?": **no** — on a model that fits, PEFT
|
|
180
|
-
is
|
|
181
|
-
OOM on Windows, it **spills into shared system memory**
|
|
182
|
-
|
|
183
|
-
|
|
185
|
+
is faster and uses 1.05 GB more. The 14B/32B rows are the design point: PEFT does not
|
|
186
|
+
OOM on Windows, it **spills into shared system memory** and pays 2.8–5.8× the step time.
|
|
187
|
+
|
|
188
|
+
### Correction: masked gradient (25 Oct, after an external review)
|
|
189
|
+
|
|
190
|
+
`chunked_lm_loss_and_backward_masked` divided every chunk by **that chunk's** valid-token
|
|
191
|
+
count. The reported loss was still the correct mean, so the curves looked right, but the
|
|
192
|
+
accumulated gradient over-weighted chunks with few unmasked tokens — i.e. the prompt-heavy
|
|
193
|
+
start of an SFT row. Measured on a toy model with an uneven mask: **112–427% relative
|
|
194
|
+
gradient error** at seq 512/1024 with 60% of the row masked (chunk 128), while balanced
|
|
195
|
+
masks were unaffected.
|
|
196
|
+
|
|
197
|
+
The fix divides every chunk by the **global** valid count (computed once, no-grad), so the
|
|
198
|
+
gradient is exactly d(mean CE over valid tokens)/dh. Two tests pin it:
|
|
199
|
+
`test_masked_chunked_gradient_matches_full_with_uneven_masks` and
|
|
200
|
+
`test_masked_chunked_handles_fully_masked_chunks`.
|
|
201
|
+
|
|
202
|
+
All SFT runs above were re-run with the fix. Quality held: 3B −0.114 (was −0.119),
|
|
203
|
+
7B −1.087 (was −1.083), 32B −0.875 (was −0.880); 14B is now 30 steps (−1.40) instead of
|
|
204
|
+
10. The numbers in the tables are the post-fix ones.
|
|
184
205
|
|
|
185
206
|
Reproduce: `python scripts/compare_peft.py --model checkpoints/qwen3b_bnb4bit --steps 30`
|
|
186
|
-
and `... --model checkpoints/qwen14b_nf4 --steps
|
|
207
|
+
and `... --model checkpoints/qwen14b_nf4 --steps 30`.
|
|
187
208
|
|
|
209
|
+
## What this does not do
|
|
188
210
|
## What this does not do
|
|
189
211
|
|
|
190
212
|
- **2k context does not fit** on 6 GB. It is ~0.6 GB short: with `--activation-cache cpu`
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# stream-finetune — fine-tune a 32B model on a 6 GB laptop GPU
|
|
2
2
|
|
|
3
|
+
[](https://pypi.org/project/streamft/) [](https://huggingface.co/spaces/byjameson/will-it-fit)
|
|
4
|
+
|
|
3
5
|
A small training engine that fine-tunes models that **do not fit in VRAM**. The NF4
|
|
4
6
|
weights are mmap'd from disk, one transformer block is materialised on the GPU at a
|
|
5
7
|
time, and the backward recomputes through it. The base stays frozen; a LoRA adapter is
|
|
@@ -31,7 +33,7 @@ and the correction of an earlier wrong claim ("8.6× / 100 tok/s") are in
|
|
|
31
33
|
## Quick start
|
|
32
34
|
|
|
33
35
|
```powershell
|
|
34
|
-
pip install
|
|
36
|
+
pip install streamft
|
|
35
37
|
|
|
36
38
|
# 0) will it fit? memory arithmetic + the measured reference, no weights downloaded
|
|
37
39
|
streamft fit --model Qwen/Qwen2.5-14B-Instruct --gpu 6 --seq 512
|
|
@@ -112,30 +114,33 @@ Raw output: `results/gradcheck.json` (git-ignored; the numbers above are copied
|
|
|
112
114
|
|
|
113
115
|
## Compared with PEFT (matched runs)
|
|
114
116
|
|
|
115
|
-
Same data, same LoRA (r=8,
|
|
117
|
+
Same data, same LoRA (r=8, alpha=16, q/k/v/o), same AdamW lr 1e-4, batch 1, same prompt
|
|
116
118
|
masking, same token-weighted masked eval on 8 held-out rows. PEFT uses
|
|
117
119
|
`prepare_model_for_kbit_training` with gradient checkpointing (the standard QLoRA setup).
|
|
120
|
+
Step times move by up to ~30% between runs on this laptop; quality is the stable number.
|
|
118
121
|
|
|
119
122
|
**3B (fits entirely):**
|
|
120
123
|
|
|
121
124
|
| | streamft (36 blocks resident) | PEFT / transformers |
|
|
122
125
|
|---|---:|---:|
|
|
123
|
-
| step time, median of 30 @ seq 512 | 0.
|
|
126
|
+
| step time, median of 30 @ seq 512 | 0.94–1.24 s | **0.58–0.88 s** |
|
|
124
127
|
| peak VRAM | **3.85 GB** | 4.90 GB |
|
|
125
128
|
| eval before | 1.42087 | 1.42087 |
|
|
126
|
-
| eval after | **1.
|
|
129
|
+
| eval after | **1.30720** (−0.1137) | 1.33002 (−0.0909) |
|
|
127
130
|
|
|
128
|
-
**7B
|
|
131
|
+
**7B / 14B / 32B (do not fit comfortably), after the masked-gradient fix:**
|
|
129
132
|
|
|
130
|
-
| model | step time, median @ seq 512 | peak VRAM | eval (start → after) |
|
|
133
|
+
| model | step time, median @ seq 512 | peak VRAM | eval (start → after, 30 steps) |
|
|
131
134
|
|---|---:|---:|---:|
|
|
132
|
-
| 7B, streamft (28 blocks) | **1.
|
|
135
|
+
| 7B, streamft (28 blocks) | **1.55 s** | **5.51 GB** | 2.1928 → **1.1055** |
|
|
133
136
|
| 7B, PEFT | 8.96 s | 10.05 GB | 2.1914 → 1.1491 |
|
|
134
|
-
| 14B, streamft (6 blocks) | **
|
|
135
|
-
| 14B, PEFT | 18.56 s | 16.48 GB |
|
|
137
|
+
| 14B, streamft (6 blocks) | **5.86 s** | **3.86 GB** | 2.3748 → **0.9742** |
|
|
138
|
+
| 14B, PEFT | 18.56 s | 16.48 GB | 3 steps only |
|
|
139
|
+
| 32B, streamft (2 blocks, seq 1024) | **12.48 s** | **3.90 GB** | 1.8755 → **1.0008** |
|
|
140
|
+
| 32B, PEFT | fails before step 1 | — | — |
|
|
136
141
|
|
|
137
142
|
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.
|
|
143
|
+
longer fit, so it spills (10.05 GB reserved) and pays ~5.8× the step time. Note that
|
|
139
144
|
streamft's 7B peak (5.51 GB) is *higher* than its 14B peak (3.86 GB) because the 7B run
|
|
140
145
|
kept all 28 blocks resident while the 14B run kept 6 — the window is a choice, and the
|
|
141
146
|
14B number is the one to copy for a 6 GB card.
|
|
@@ -144,17 +149,34 @@ kept all 28 blocks resident while the 14B run kept 6 — the window is a choice,
|
|
|
144
149
|
into shared memory (19.45 GB reserved on a 6 GiB card), and `prepare_model_for_kbit_training`
|
|
145
150
|
then dies allocating the fp32 norm copy: *CUDA out of memory. Tried to allocate 2.90 GiB;
|
|
146
151
|
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
|
|
152
|
+
**12.5 s/step and 3.90 GB** (64/64 layers).
|
|
148
153
|
|
|
149
154
|
The 3B row is the honest answer to "is this faster?": **no** — on a model that fits, PEFT
|
|
150
|
-
is
|
|
151
|
-
OOM on Windows, it **spills into shared system memory**
|
|
152
|
-
|
|
153
|
-
|
|
155
|
+
is faster and uses 1.05 GB more. The 14B/32B rows are the design point: PEFT does not
|
|
156
|
+
OOM on Windows, it **spills into shared system memory** and pays 2.8–5.8× the step time.
|
|
157
|
+
|
|
158
|
+
### Correction: masked gradient (25 Oct, after an external review)
|
|
159
|
+
|
|
160
|
+
`chunked_lm_loss_and_backward_masked` divided every chunk by **that chunk's** valid-token
|
|
161
|
+
count. The reported loss was still the correct mean, so the curves looked right, but the
|
|
162
|
+
accumulated gradient over-weighted chunks with few unmasked tokens — i.e. the prompt-heavy
|
|
163
|
+
start of an SFT row. Measured on a toy model with an uneven mask: **112–427% relative
|
|
164
|
+
gradient error** at seq 512/1024 with 60% of the row masked (chunk 128), while balanced
|
|
165
|
+
masks were unaffected.
|
|
166
|
+
|
|
167
|
+
The fix divides every chunk by the **global** valid count (computed once, no-grad), so the
|
|
168
|
+
gradient is exactly d(mean CE over valid tokens)/dh. Two tests pin it:
|
|
169
|
+
`test_masked_chunked_gradient_matches_full_with_uneven_masks` and
|
|
170
|
+
`test_masked_chunked_handles_fully_masked_chunks`.
|
|
171
|
+
|
|
172
|
+
All SFT runs above were re-run with the fix. Quality held: 3B −0.114 (was −0.119),
|
|
173
|
+
7B −1.087 (was −1.083), 32B −0.875 (was −0.880); 14B is now 30 steps (−1.40) instead of
|
|
174
|
+
10. The numbers in the tables are the post-fix ones.
|
|
154
175
|
|
|
155
176
|
Reproduce: `python scripts/compare_peft.py --model checkpoints/qwen3b_bnb4bit --steps 30`
|
|
156
|
-
and `... --model checkpoints/qwen14b_nf4 --steps
|
|
177
|
+
and `... --model checkpoints/qwen14b_nf4 --steps 30`.
|
|
157
178
|
|
|
179
|
+
## What this does not do
|
|
158
180
|
## What this does not do
|
|
159
181
|
|
|
160
182
|
- **2k context does not fit** on 6 GB. It is ~0.6 GB short: with `--activation-cache cpu`
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "streamft"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "Fine-tune models that do not fit: stream NF4 weights from disk, one transformer block at a time."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -22,27 +22,39 @@ from .stream_train import _layer_out, chunked_lm_loss_and_backward
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def chunked_lm_loss_and_backward_masked(head, h, ys, chunk=128, ignore_index=-100):
|
|
25
|
-
"""Chunked CE with an ignore_index (for SFT prompt masking).
|
|
25
|
+
"""Chunked CE with an ignore_index (for SFT prompt masking).
|
|
26
|
+
|
|
27
|
+
Every chunk contributes `loss_sum / valid_total` where valid_total is the number of
|
|
28
|
+
unmasked targets in the **whole sequence**, so the accumulated gradient is exactly
|
|
29
|
+
d(mean CE over valid tokens)/dh.
|
|
30
|
+
|
|
31
|
+
FIX (25.10.2026, reported by an external reviewer): the first version divided each
|
|
32
|
+
chunk by that chunk's own valid count. Losses were unaffected (the returned value
|
|
33
|
+
summed to the right mean) but the gradient over-weighted chunks with few valid
|
|
34
|
+
tokens, i.e. the prompt-heavy start of an SFT row. tests/test_streamft_smoke.py
|
|
35
|
+
pins the corrected behaviour with an uneven mask.
|
|
36
|
+
"""
|
|
26
37
|
B, T, _ = h.shape
|
|
38
|
+
tgt_all = ys.to(h.device)
|
|
39
|
+
valid_total = int((tgt_all != ignore_index).sum())
|
|
40
|
+
if valid_total == 0:
|
|
41
|
+
zero = torch.zeros((), device=h.device)
|
|
42
|
+
return zero, zero
|
|
27
43
|
total = torch.zeros((), device=h.device, dtype=torch.float32)
|
|
28
|
-
valid_total = 0
|
|
29
44
|
for a in range(0, T, chunk):
|
|
30
45
|
b = min(a + chunk, T)
|
|
31
|
-
|
|
32
|
-
tgt
|
|
33
|
-
valid = int((tgt != ignore_index).sum())
|
|
34
|
-
if valid == 0:
|
|
46
|
+
tgt = tgt_all[:, a:b].reshape(-1)
|
|
47
|
+
if int((tgt != ignore_index).sum()) == 0:
|
|
35
48
|
continue
|
|
49
|
+
logits = head(h[:, a:b])
|
|
36
50
|
loss_sum = torch.nn.functional.cross_entropy(
|
|
37
51
|
logits.reshape(-1, logits.shape[-1]).float(), tgt,
|
|
38
52
|
reduction="sum", ignore_index=ignore_index,
|
|
39
53
|
)
|
|
40
|
-
(loss_sum /
|
|
54
|
+
(loss_sum / valid_total).backward()
|
|
41
55
|
with torch.no_grad():
|
|
42
56
|
total = total + loss_sum.detach()
|
|
43
|
-
|
|
44
|
-
denom = max(valid_total, 1)
|
|
45
|
-
return total / denom, total / denom
|
|
57
|
+
return total / valid_total, total / valid_total
|
|
46
58
|
|
|
47
59
|
|
|
48
60
|
def base_model_of(model):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: streamft
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Fine-tune models that do not fit: stream NF4 weights from disk, one transformer block at a time.
|
|
5
5
|
Author-email: Gokhan Cava <byjameson@users.noreply.github.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -30,6 +30,8 @@ Dynamic: license-file
|
|
|
30
30
|
|
|
31
31
|
# stream-finetune — fine-tune a 32B model on a 6 GB laptop GPU
|
|
32
32
|
|
|
33
|
+
[](https://pypi.org/project/streamft/) [](https://huggingface.co/spaces/byjameson/will-it-fit)
|
|
34
|
+
|
|
33
35
|
A small training engine that fine-tunes models that **do not fit in VRAM**. The NF4
|
|
34
36
|
weights are mmap'd from disk, one transformer block is materialised on the GPU at a
|
|
35
37
|
time, and the backward recomputes through it. The base stays frozen; a LoRA adapter is
|
|
@@ -61,7 +63,7 @@ and the correction of an earlier wrong claim ("8.6× / 100 tok/s") are in
|
|
|
61
63
|
## Quick start
|
|
62
64
|
|
|
63
65
|
```powershell
|
|
64
|
-
pip install
|
|
66
|
+
pip install streamft
|
|
65
67
|
|
|
66
68
|
# 0) will it fit? memory arithmetic + the measured reference, no weights downloaded
|
|
67
69
|
streamft fit --model Qwen/Qwen2.5-14B-Instruct --gpu 6 --seq 512
|
|
@@ -142,30 +144,33 @@ Raw output: `results/gradcheck.json` (git-ignored; the numbers above are copied
|
|
|
142
144
|
|
|
143
145
|
## Compared with PEFT (matched runs)
|
|
144
146
|
|
|
145
|
-
Same data, same LoRA (r=8,
|
|
147
|
+
Same data, same LoRA (r=8, alpha=16, q/k/v/o), same AdamW lr 1e-4, batch 1, same prompt
|
|
146
148
|
masking, same token-weighted masked eval on 8 held-out rows. PEFT uses
|
|
147
149
|
`prepare_model_for_kbit_training` with gradient checkpointing (the standard QLoRA setup).
|
|
150
|
+
Step times move by up to ~30% between runs on this laptop; quality is the stable number.
|
|
148
151
|
|
|
149
152
|
**3B (fits entirely):**
|
|
150
153
|
|
|
151
154
|
| | streamft (36 blocks resident) | PEFT / transformers |
|
|
152
155
|
|---|---:|---:|
|
|
153
|
-
| step time, median of 30 @ seq 512 | 0.
|
|
156
|
+
| step time, median of 30 @ seq 512 | 0.94–1.24 s | **0.58–0.88 s** |
|
|
154
157
|
| peak VRAM | **3.85 GB** | 4.90 GB |
|
|
155
158
|
| eval before | 1.42087 | 1.42087 |
|
|
156
|
-
| eval after | **1.
|
|
159
|
+
| eval after | **1.30720** (−0.1137) | 1.33002 (−0.0909) |
|
|
157
160
|
|
|
158
|
-
**7B
|
|
161
|
+
**7B / 14B / 32B (do not fit comfortably), after the masked-gradient fix:**
|
|
159
162
|
|
|
160
|
-
| model | step time, median @ seq 512 | peak VRAM | eval (start → after) |
|
|
163
|
+
| model | step time, median @ seq 512 | peak VRAM | eval (start → after, 30 steps) |
|
|
161
164
|
|---|---:|---:|---:|
|
|
162
|
-
| 7B, streamft (28 blocks) | **1.
|
|
165
|
+
| 7B, streamft (28 blocks) | **1.55 s** | **5.51 GB** | 2.1928 → **1.1055** |
|
|
163
166
|
| 7B, PEFT | 8.96 s | 10.05 GB | 2.1914 → 1.1491 |
|
|
164
|
-
| 14B, streamft (6 blocks) | **
|
|
165
|
-
| 14B, PEFT | 18.56 s | 16.48 GB |
|
|
167
|
+
| 14B, streamft (6 blocks) | **5.86 s** | **3.86 GB** | 2.3748 → **0.9742** |
|
|
168
|
+
| 14B, PEFT | 18.56 s | 16.48 GB | 3 steps only |
|
|
169
|
+
| 32B, streamft (2 blocks, seq 1024) | **12.48 s** | **3.90 GB** | 1.8755 → **1.0008** |
|
|
170
|
+
| 32B, PEFT | fails before step 1 | — | — |
|
|
166
171
|
|
|
167
172
|
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.
|
|
173
|
+
longer fit, so it spills (10.05 GB reserved) and pays ~5.8× the step time. Note that
|
|
169
174
|
streamft's 7B peak (5.51 GB) is *higher* than its 14B peak (3.86 GB) because the 7B run
|
|
170
175
|
kept all 28 blocks resident while the 14B run kept 6 — the window is a choice, and the
|
|
171
176
|
14B number is the one to copy for a 6 GB card.
|
|
@@ -174,17 +179,34 @@ kept all 28 blocks resident while the 14B run kept 6 — the window is a choice,
|
|
|
174
179
|
into shared memory (19.45 GB reserved on a 6 GiB card), and `prepare_model_for_kbit_training`
|
|
175
180
|
then dies allocating the fp32 norm copy: *CUDA out of memory. Tried to allocate 2.90 GiB;
|
|
176
181
|
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
|
|
182
|
+
**12.5 s/step and 3.90 GB** (64/64 layers).
|
|
178
183
|
|
|
179
184
|
The 3B row is the honest answer to "is this faster?": **no** — on a model that fits, PEFT
|
|
180
|
-
is
|
|
181
|
-
OOM on Windows, it **spills into shared system memory**
|
|
182
|
-
|
|
183
|
-
|
|
185
|
+
is faster and uses 1.05 GB more. The 14B/32B rows are the design point: PEFT does not
|
|
186
|
+
OOM on Windows, it **spills into shared system memory** and pays 2.8–5.8× the step time.
|
|
187
|
+
|
|
188
|
+
### Correction: masked gradient (25 Oct, after an external review)
|
|
189
|
+
|
|
190
|
+
`chunked_lm_loss_and_backward_masked` divided every chunk by **that chunk's** valid-token
|
|
191
|
+
count. The reported loss was still the correct mean, so the curves looked right, but the
|
|
192
|
+
accumulated gradient over-weighted chunks with few unmasked tokens — i.e. the prompt-heavy
|
|
193
|
+
start of an SFT row. Measured on a toy model with an uneven mask: **112–427% relative
|
|
194
|
+
gradient error** at seq 512/1024 with 60% of the row masked (chunk 128), while balanced
|
|
195
|
+
masks were unaffected.
|
|
196
|
+
|
|
197
|
+
The fix divides every chunk by the **global** valid count (computed once, no-grad), so the
|
|
198
|
+
gradient is exactly d(mean CE over valid tokens)/dh. Two tests pin it:
|
|
199
|
+
`test_masked_chunked_gradient_matches_full_with_uneven_masks` and
|
|
200
|
+
`test_masked_chunked_handles_fully_masked_chunks`.
|
|
201
|
+
|
|
202
|
+
All SFT runs above were re-run with the fix. Quality held: 3B −0.114 (was −0.119),
|
|
203
|
+
7B −1.087 (was −1.083), 32B −0.875 (was −0.880); 14B is now 30 steps (−1.40) instead of
|
|
204
|
+
10. The numbers in the tables are the post-fix ones.
|
|
184
205
|
|
|
185
206
|
Reproduce: `python scripts/compare_peft.py --model checkpoints/qwen3b_bnb4bit --steps 30`
|
|
186
|
-
and `... --model checkpoints/qwen14b_nf4 --steps
|
|
207
|
+
and `... --model checkpoints/qwen14b_nf4 --steps 30`.
|
|
187
208
|
|
|
209
|
+
## What this does not do
|
|
188
210
|
## What this does not do
|
|
189
211
|
|
|
190
212
|
- **2k context does not fit** on 6 GB. It is ~0.6 GB short: with `--activation-cache cpu`
|
|
@@ -112,3 +112,53 @@ def test_chunked_ce_gradient_matches_full_mean():
|
|
|
112
112
|
|
|
113
113
|
def test_cli_is_importable():
|
|
114
114
|
assert callable(cli.main)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ------------------------------------------- masked (SFT) chunked loss
|
|
118
|
+
|
|
119
|
+
def test_masked_chunked_gradient_matches_full_with_uneven_masks():
|
|
120
|
+
"""The 25.10 fix: chunks with different valid counts must not be re-weighted.
|
|
121
|
+
|
|
122
|
+
With chunk=3 and a prompt mask over the first 4 positions, chunk 0 has zero valid
|
|
123
|
+
targets, chunk 1 has two, later chunks have three. The old implementation divided
|
|
124
|
+
each chunk by its own count, which inflated the sparse chunks' gradient.
|
|
125
|
+
"""
|
|
126
|
+
from streamft.stream_quant_lora import chunked_lm_loss_and_backward_masked
|
|
127
|
+
|
|
128
|
+
torch.manual_seed(0)
|
|
129
|
+
d, T, V = 12, 10, 7
|
|
130
|
+
head = nn.Linear(d, V, bias=False)
|
|
131
|
+
ys = torch.randint(0, V, (1, T))
|
|
132
|
+
ys[0, :4] = -100 # prompt
|
|
133
|
+
ys[0, 7] = -100 # a masked middle position
|
|
134
|
+
|
|
135
|
+
h1 = torch.randn(1, T, d, requires_grad=True)
|
|
136
|
+
full = F.cross_entropy(head(h1).reshape(-1, V), ys.reshape(-1),
|
|
137
|
+
ignore_index=-100, reduction="mean")
|
|
138
|
+
full.backward()
|
|
139
|
+
|
|
140
|
+
h2 = torch.randn(1, T, d, requires_grad=True)
|
|
141
|
+
with torch.no_grad():
|
|
142
|
+
h2.copy_(h1.detach())
|
|
143
|
+
_, mean = chunked_lm_loss_and_backward_masked(head, h2, ys, chunk=3)
|
|
144
|
+
|
|
145
|
+
assert math.isclose(float(mean.detach()), float(full.detach()), rel_tol=1e-6)
|
|
146
|
+
assert torch.allclose(h1.grad, h2.grad, atol=1e-6)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_masked_chunked_handles_fully_masked_chunks():
|
|
150
|
+
"""A chunk with no valid targets must contribute nothing at all."""
|
|
151
|
+
from streamft.stream_quant_lora import chunked_lm_loss_and_backward_masked
|
|
152
|
+
|
|
153
|
+
torch.manual_seed(0)
|
|
154
|
+
d, T, V = 8, 6, 5
|
|
155
|
+
head = nn.Linear(d, V, bias=False)
|
|
156
|
+
ys = torch.randint(0, V, (1, T))
|
|
157
|
+
ys[0, :3] = -100
|
|
158
|
+
h = torch.randn(1, T, d, requires_grad=True)
|
|
159
|
+
_, mean = chunked_lm_loss_and_backward_masked(head, h, ys, chunk=2)
|
|
160
|
+
logits = head(h).reshape(-1, V)
|
|
161
|
+
tgt = ys.reshape(-1)
|
|
162
|
+
mask = tgt != -100
|
|
163
|
+
ref = F.cross_entropy(logits[mask], tgt[mask], reduction="mean")
|
|
164
|
+
assert math.isclose(float(mean.detach()), float(ref.detach()), rel_tol=1e-6)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|