trainstream 0.3.0__tar.gz → 0.4.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.
- {trainstream-0.3.0/trainstream.egg-info → trainstream-0.4.0}/PKG-INFO +29 -8
- {trainstream-0.3.0 → trainstream-0.4.0}/README.md +27 -6
- {trainstream-0.3.0 → trainstream-0.4.0}/setup.py +4 -2
- {trainstream-0.3.0 → trainstream-0.4.0}/tests/test_huggingface.py +96 -0
- trainstream-0.4.0/tests/test_quant.py +279 -0
- {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/__init__.py +5 -2
- {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/_build.py +2 -1
- {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/csrc/bindings.cpp +2 -0
- {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/csrc/kernels.h +7 -0
- trainstream-0.4.0/trainstream/csrc/quant.cpp +71 -0
- trainstream-0.4.0/trainstream/csrc/quant_kernel.cu +124 -0
- {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/csrc/trainstream.h +8 -0
- {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/integrations/huggingface.py +118 -0
- {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/paging.py +103 -7
- {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/planner.py +5 -3
- trainstream-0.4.0/trainstream/quant.py +214 -0
- {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/wrapper.py +114 -26
- {trainstream-0.3.0 → trainstream-0.4.0/trainstream.egg-info}/PKG-INFO +29 -8
- {trainstream-0.3.0 → trainstream-0.4.0}/trainstream.egg-info/SOURCES.txt +4 -0
- {trainstream-0.3.0 → trainstream-0.4.0}/LICENSE +0 -0
- {trainstream-0.3.0 → trainstream-0.4.0}/MANIFEST.in +0 -0
- {trainstream-0.3.0 → trainstream-0.4.0}/NOTICE +0 -0
- {trainstream-0.3.0 → trainstream-0.4.0}/build.bat +0 -0
- {trainstream-0.3.0 → trainstream-0.4.0}/pyproject.toml +0 -0
- {trainstream-0.3.0 → trainstream-0.4.0}/setup.cfg +0 -0
- {trainstream-0.3.0 → trainstream-0.4.0}/tests/test_distributed.py +0 -0
- {trainstream-0.3.0 → trainstream-0.4.0}/tests/test_features.py +0 -0
- {trainstream-0.3.0 → trainstream-0.4.0}/tests/test_offload.py +0 -0
- {trainstream-0.3.0 → trainstream-0.4.0}/tests/test_optimizer.py +0 -0
- {trainstream-0.3.0 → trainstream-0.4.0}/tests/test_paging.py +0 -0
- {trainstream-0.3.0 → trainstream-0.4.0}/tests/test_planner.py +0 -0
- {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/csrc/memory_pinner.cpp +0 -0
- {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/csrc/offload_adamw.cpp +0 -0
- {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/csrc/offload_adamw.h +0 -0
- {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/csrc/offload_adamw_kernel.cu +0 -0
- {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/hooks.py +0 -0
- {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/integrations/__init__.py +0 -0
- {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/losses.py +0 -0
- {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/optimizer.py +0 -0
- {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/profiler.py +0 -0
- {trainstream-0.3.0 → trainstream-0.4.0}/trainstream.egg-info/dependency_links.txt +0 -0
- {trainstream-0.3.0 → trainstream-0.4.0}/trainstream.egg-info/not-zip-safe +0 -0
- {trainstream-0.3.0 → trainstream-0.4.0}/trainstream.egg-info/requires.txt +0 -0
- {trainstream-0.3.0 → trainstream-0.4.0}/trainstream.egg-info/top_level.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: trainstream
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Zero-dependency
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Zero-dependency PyTorch training offload: optimizer, gradients, activations and weights to host RAM, with a memory planner and built-in int8/NF4 quantization.
|
|
5
5
|
Home-page: https://github.com/johnanishg/TrainStream
|
|
6
6
|
Author: John Anish G
|
|
7
7
|
License: Apache-2.0
|
|
@@ -40,7 +40,7 @@ Dynamic: summary
|
|
|
40
40
|
|
|
41
41
|
# TrainStream
|
|
42
42
|
|
|
43
|
-
Train and fine-tune models larger than your GPU's memory by moving optimizer state, gradients, activations and, when needed, the weights themselves to system RAM over PCIe. It takes one line of code and has no third-party dependencies.
|
|
43
|
+
Train and fine-tune models larger than your GPU's memory by moving optimizer state, gradients, activations and, when needed, the weights themselves to system RAM over PCIe. Frozen weights (a LoRA base) can also be stored as int8 or 4-bit NF4. It takes one line of code and has no third-party dependencies.
|
|
44
44
|
|
|
45
45
|
```python
|
|
46
46
|
import trainstream
|
|
@@ -59,6 +59,8 @@ TrainStream is a C++/CUDA extension for PyTorch. It keeps only the weights in VR
|
|
|
59
59
|
| 3.6B GPT full fine-tune (all parameters trained) | needs ~58 GB of VRAM | 3.1 GiB peak, 3.15 s/step, 30 GiB of host RAM |
|
|
60
60
|
| LoRA r=16 on the Qwen2.5-7B architecture (random weights) | 20.3 GB peak, 0.27 s/step | 4.6 GB peak, 1.25 s/step |
|
|
61
61
|
| LoRA r=16 on Qwen2.5-14B (29.5 GB of weights) | does not fit | 1.37 s/step: the memory planner keeps 28 of 48 layers in VRAM (19.9 GiB peak) and streams 20 from RAM |
|
|
62
|
+
| same, NF4 base (`quantize="auto", prefer="speed"`, 0.4) | does not fit | 0.87 s/step: all 48 layers in VRAM as 4-bit (7.43 GB), 18.3 GB peak, 11.3 GB of process RAM |
|
|
63
|
+
| LoRA r=16 on the Qwen2.5-32B architecture (65.5 GB in bf16, more than the 64 GB of RAM) | does not fit | 2.53 s/step with `load_model(quantize="nf4")`: 21.3 GB peak, 31.4 GB of process RAM |
|
|
62
64
|
| 85M GPT (fits either way) | 2.55 GiB, 43.7 ms/step | `offload="auto"` switches to stock fused AdamW: 2.56 GiB, 43.8 ms/step |
|
|
63
65
|
|
|
64
66
|
Offloading costs throughput when a model already fits, which is why `offload="auto"` measures two steps and then switches to plain fused GPU AdamW if offloading isn't needed. TrainStream's value is training what does not fit, or freeing VRAM for larger batches. All numbers come from a single machine (RTX 5090 Laptop, Windows 11).
|
|
@@ -67,7 +69,7 @@ Offloading costs throughput when a model already fits, which is why `offload="au
|
|
|
67
69
|
|
|
68
70
|
- An NVIDIA GPU and a CUDA build of PyTorch
|
|
69
71
|
- The CUDA toolkit matching PyTorch's CUDA major version, and a C++ compiler (Visual Studio 2022+ Build Tools with "Desktop development with C++" on Windows, GCC 11+ on Linux)
|
|
70
|
-
- Tested on Windows 11 (PyTorch 2.14.1, CUDA 13.4, MSVC, Python 3.14) and Ubuntu 24.04 under WSL2 (PyTorch 2.14.1+cu130, CUDA 13.3, GCC 13.3, Python 3.12): all
|
|
72
|
+
- Tested on Windows 11 (PyTorch 2.14.1, CUDA 13.4, MSVC, Python 3.14) and Ubuntu 24.04 under WSL2 (PyTorch 2.14.1+cu130, CUDA 13.3, GCC 13.3, Python 3.12): all 123 tests pass on Windows; on Linux 122 pass and 1 (the bitsandbytes comparison) is skipped without bitsandbytes installed, RTX 5090 Laptop GPU. Native Linux and other GPUs are untested.
|
|
71
73
|
|
|
72
74
|
## Install
|
|
73
75
|
|
|
@@ -113,13 +115,30 @@ model, optimizer = trainstream.wrap(model, optimizer, device="cuda",
|
|
|
113
115
|
param_offload=True, moment_dtype="bfloat16")
|
|
114
116
|
```
|
|
115
117
|
|
|
116
|
-
|
|
118
|
+
LoRA on a quantized base (QLoRA-style), with no extra package:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
import peft
|
|
122
|
+
from trainstream.integrations.huggingface import TrainStreamTrainer, load_model
|
|
123
|
+
|
|
124
|
+
model = load_model("Qwen/Qwen2.5-14B", quantize="nf4") # "auto" | "none" | "int8" | "nf4"; quantized while read
|
|
125
|
+
model = peft.get_peft_model(model, peft.LoraConfig(r=16, lora_alpha=32, task_type="CAUSAL_LM",
|
|
126
|
+
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"]))
|
|
127
|
+
trainer = TrainStreamTrainer(model=model, args=TrainingArguments(..., bf16=True), train_dataset=ds)
|
|
128
|
+
trainer.train()
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Or quantize inside `wrap()`: `trainstream.wrap(model, opt, device="cuda", quantize="auto", prefer="speed")`, or beforehand with `trainstream.quantize_model(model, "int8")`. Only frozen weights are quantized. A quantized run trains on the dequantized weights (as QLoRA does), so it isn't bit-exact with the bf16 base.
|
|
132
|
+
|
|
133
|
+
`examples/train_bigger_than_vram.py` trains a 1.8B model (7.3 GB of fp32 weights) inside a 4 GB VRAM cap. `examples/lora_large_model.py` LoRA-fine-tunes 7B–32B bases through `TrainStreamTrainer` (`--quantize auto|none|int8|nf4`, `--prefer exact|speed`).
|
|
117
134
|
|
|
118
135
|
| `wrap()` option | What it does |
|
|
119
136
|
|---|---|
|
|
120
137
|
| `offload="auto"` | switches to stock fused AdamW after 2 steps if the model fits (`True` always offloads) |
|
|
121
138
|
| `param_offload="auto"` | memory planner: measures the VRAM and RAM of the machine it runs on, keeps as many transformer blocks resident as fit, pages the rest, and refines the split from the first steps' measured peaks; `True`, a fraction, or block indices fix it |
|
|
122
139
|
| `activation_offload="auto"` | pages a profiled share of activations (`True`, a fraction, `False`) |
|
|
140
|
+
| `quantize="auto"` | frozen weights only: `"auto"` quantizes when the run can't fit otherwise (int8 before NF4); `"none"`, `"int8"`, `"nf4"` fix it; `quant_block_size=64` |
|
|
141
|
+
| `prefer="exact"` | `"speed"` lets `quantize="auto"` pick NF4 when it keeps every layer in VRAM |
|
|
123
142
|
| `moment_dtype="auto"` | fp32 optimizer moments, or bf16 (stochastic rounding, half the memory) when fp32 wouldn't fit in RAM |
|
|
124
143
|
| `check_memory=True` | refuse at `wrap()` with a clear explanation when the run can't fit in RAM/VRAM |
|
|
125
144
|
| `distributed` | data parallel when a `torch.distributed` group with more than 1 rank exists; gradients averaged on the host over gloo |
|
|
@@ -142,12 +161,14 @@ trainer.train()
|
|
|
142
161
|
- `examples/verify_ram_offload.py` — proves where training memory lives (VRAM vs system RAM) with a PASS/FAIL verdict
|
|
143
162
|
- `examples/hf_finetune.py` — fine-tune a Hugging Face model, optionally side by side with the stock Trainer
|
|
144
163
|
- `examples/train_bigger_than_vram.py` — weight paging on a model built on the CPU, under a VRAM cap
|
|
145
|
-
- `examples/lora_large_model.py` — LoRA on 7B/14B-size bases larger than the GPU, optionally against the stock Trainer
|
|
146
|
-
- `
|
|
164
|
+
- `examples/lora_large_model.py` — LoRA on 7B/14B/32B-size bases larger than the GPU, bf16 or quantized, optionally against the stock Trainer
|
|
165
|
+
- `TrainStreamDocs.html` — full documentation, including every option and the values it accepts (open it in a browser)
|
|
166
|
+
- `TrainStreamReport.html` — what TrainStream does, how it compares with other libraries, and every measured result
|
|
147
167
|
|
|
148
168
|
## Limitations
|
|
149
169
|
|
|
150
|
-
- Host RAM is the ceiling: 8–12 bytes per trained parameter and 2–4 per frozen one (
|
|
170
|
+
- Host RAM is the ceiling: 8–12 bytes per trained parameter and 2–4 per frozen one (1.06 int8, 0.56 NF4), plus activations; measured on 64 GB: a 3.6B full fine-tune and LoRA on a 32.8B NF4 base
|
|
171
|
+
- Quantization covers frozen block weights only (not embeddings, the output layer or trained weights) and is not bit-exact; `load_model` reads `.safetensors` causal-LM checkpoints
|
|
151
172
|
- Weight paging covers the repeated block stack and needs `optimizer_mode="cpu"`; each paged weight crosses PCIe twice per step
|
|
152
173
|
- Data parallel only (no tensor/pipeline parallelism); tested with 2 processes on 1 GPU, not across several GPUs or machines
|
|
153
174
|
- `TrainStreamTrainer` is single-process; fp16 + GradScaler and AMSGrad aren't supported; `torch.compile` hasn't been tested
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# TrainStream
|
|
2
2
|
|
|
3
|
-
Train and fine-tune models larger than your GPU's memory by moving optimizer state, gradients, activations and, when needed, the weights themselves to system RAM over PCIe. It takes one line of code and has no third-party dependencies.
|
|
3
|
+
Train and fine-tune models larger than your GPU's memory by moving optimizer state, gradients, activations and, when needed, the weights themselves to system RAM over PCIe. Frozen weights (a LoRA base) can also be stored as int8 or 4-bit NF4. It takes one line of code and has no third-party dependencies.
|
|
4
4
|
|
|
5
5
|
```python
|
|
6
6
|
import trainstream
|
|
@@ -19,6 +19,8 @@ TrainStream is a C++/CUDA extension for PyTorch. It keeps only the weights in VR
|
|
|
19
19
|
| 3.6B GPT full fine-tune (all parameters trained) | needs ~58 GB of VRAM | 3.1 GiB peak, 3.15 s/step, 30 GiB of host RAM |
|
|
20
20
|
| LoRA r=16 on the Qwen2.5-7B architecture (random weights) | 20.3 GB peak, 0.27 s/step | 4.6 GB peak, 1.25 s/step |
|
|
21
21
|
| LoRA r=16 on Qwen2.5-14B (29.5 GB of weights) | does not fit | 1.37 s/step: the memory planner keeps 28 of 48 layers in VRAM (19.9 GiB peak) and streams 20 from RAM |
|
|
22
|
+
| same, NF4 base (`quantize="auto", prefer="speed"`, 0.4) | does not fit | 0.87 s/step: all 48 layers in VRAM as 4-bit (7.43 GB), 18.3 GB peak, 11.3 GB of process RAM |
|
|
23
|
+
| LoRA r=16 on the Qwen2.5-32B architecture (65.5 GB in bf16, more than the 64 GB of RAM) | does not fit | 2.53 s/step with `load_model(quantize="nf4")`: 21.3 GB peak, 31.4 GB of process RAM |
|
|
22
24
|
| 85M GPT (fits either way) | 2.55 GiB, 43.7 ms/step | `offload="auto"` switches to stock fused AdamW: 2.56 GiB, 43.8 ms/step |
|
|
23
25
|
|
|
24
26
|
Offloading costs throughput when a model already fits, which is why `offload="auto"` measures two steps and then switches to plain fused GPU AdamW if offloading isn't needed. TrainStream's value is training what does not fit, or freeing VRAM for larger batches. All numbers come from a single machine (RTX 5090 Laptop, Windows 11).
|
|
@@ -27,7 +29,7 @@ Offloading costs throughput when a model already fits, which is why `offload="au
|
|
|
27
29
|
|
|
28
30
|
- An NVIDIA GPU and a CUDA build of PyTorch
|
|
29
31
|
- The CUDA toolkit matching PyTorch's CUDA major version, and a C++ compiler (Visual Studio 2022+ Build Tools with "Desktop development with C++" on Windows, GCC 11+ on Linux)
|
|
30
|
-
- Tested on Windows 11 (PyTorch 2.14.1, CUDA 13.4, MSVC, Python 3.14) and Ubuntu 24.04 under WSL2 (PyTorch 2.14.1+cu130, CUDA 13.3, GCC 13.3, Python 3.12): all
|
|
32
|
+
- Tested on Windows 11 (PyTorch 2.14.1, CUDA 13.4, MSVC, Python 3.14) and Ubuntu 24.04 under WSL2 (PyTorch 2.14.1+cu130, CUDA 13.3, GCC 13.3, Python 3.12): all 123 tests pass on Windows; on Linux 122 pass and 1 (the bitsandbytes comparison) is skipped without bitsandbytes installed, RTX 5090 Laptop GPU. Native Linux and other GPUs are untested.
|
|
31
33
|
|
|
32
34
|
## Install
|
|
33
35
|
|
|
@@ -73,13 +75,30 @@ model, optimizer = trainstream.wrap(model, optimizer, device="cuda",
|
|
|
73
75
|
param_offload=True, moment_dtype="bfloat16")
|
|
74
76
|
```
|
|
75
77
|
|
|
76
|
-
|
|
78
|
+
LoRA on a quantized base (QLoRA-style), with no extra package:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
import peft
|
|
82
|
+
from trainstream.integrations.huggingface import TrainStreamTrainer, load_model
|
|
83
|
+
|
|
84
|
+
model = load_model("Qwen/Qwen2.5-14B", quantize="nf4") # "auto" | "none" | "int8" | "nf4"; quantized while read
|
|
85
|
+
model = peft.get_peft_model(model, peft.LoraConfig(r=16, lora_alpha=32, task_type="CAUSAL_LM",
|
|
86
|
+
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"]))
|
|
87
|
+
trainer = TrainStreamTrainer(model=model, args=TrainingArguments(..., bf16=True), train_dataset=ds)
|
|
88
|
+
trainer.train()
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Or quantize inside `wrap()`: `trainstream.wrap(model, opt, device="cuda", quantize="auto", prefer="speed")`, or beforehand with `trainstream.quantize_model(model, "int8")`. Only frozen weights are quantized. A quantized run trains on the dequantized weights (as QLoRA does), so it isn't bit-exact with the bf16 base.
|
|
92
|
+
|
|
93
|
+
`examples/train_bigger_than_vram.py` trains a 1.8B model (7.3 GB of fp32 weights) inside a 4 GB VRAM cap. `examples/lora_large_model.py` LoRA-fine-tunes 7B–32B bases through `TrainStreamTrainer` (`--quantize auto|none|int8|nf4`, `--prefer exact|speed`).
|
|
77
94
|
|
|
78
95
|
| `wrap()` option | What it does |
|
|
79
96
|
|---|---|
|
|
80
97
|
| `offload="auto"` | switches to stock fused AdamW after 2 steps if the model fits (`True` always offloads) |
|
|
81
98
|
| `param_offload="auto"` | memory planner: measures the VRAM and RAM of the machine it runs on, keeps as many transformer blocks resident as fit, pages the rest, and refines the split from the first steps' measured peaks; `True`, a fraction, or block indices fix it |
|
|
82
99
|
| `activation_offload="auto"` | pages a profiled share of activations (`True`, a fraction, `False`) |
|
|
100
|
+
| `quantize="auto"` | frozen weights only: `"auto"` quantizes when the run can't fit otherwise (int8 before NF4); `"none"`, `"int8"`, `"nf4"` fix it; `quant_block_size=64` |
|
|
101
|
+
| `prefer="exact"` | `"speed"` lets `quantize="auto"` pick NF4 when it keeps every layer in VRAM |
|
|
83
102
|
| `moment_dtype="auto"` | fp32 optimizer moments, or bf16 (stochastic rounding, half the memory) when fp32 wouldn't fit in RAM |
|
|
84
103
|
| `check_memory=True` | refuse at `wrap()` with a clear explanation when the run can't fit in RAM/VRAM |
|
|
85
104
|
| `distributed` | data parallel when a `torch.distributed` group with more than 1 rank exists; gradients averaged on the host over gloo |
|
|
@@ -102,12 +121,14 @@ trainer.train()
|
|
|
102
121
|
- `examples/verify_ram_offload.py` — proves where training memory lives (VRAM vs system RAM) with a PASS/FAIL verdict
|
|
103
122
|
- `examples/hf_finetune.py` — fine-tune a Hugging Face model, optionally side by side with the stock Trainer
|
|
104
123
|
- `examples/train_bigger_than_vram.py` — weight paging on a model built on the CPU, under a VRAM cap
|
|
105
|
-
- `examples/lora_large_model.py` — LoRA on 7B/14B-size bases larger than the GPU, optionally against the stock Trainer
|
|
106
|
-
- `
|
|
124
|
+
- `examples/lora_large_model.py` — LoRA on 7B/14B/32B-size bases larger than the GPU, bf16 or quantized, optionally against the stock Trainer
|
|
125
|
+
- `TrainStreamDocs.html` — full documentation, including every option and the values it accepts (open it in a browser)
|
|
126
|
+
- `TrainStreamReport.html` — what TrainStream does, how it compares with other libraries, and every measured result
|
|
107
127
|
|
|
108
128
|
## Limitations
|
|
109
129
|
|
|
110
|
-
- Host RAM is the ceiling: 8–12 bytes per trained parameter and 2–4 per frozen one (
|
|
130
|
+
- Host RAM is the ceiling: 8–12 bytes per trained parameter and 2–4 per frozen one (1.06 int8, 0.56 NF4), plus activations; measured on 64 GB: a 3.6B full fine-tune and LoRA on a 32.8B NF4 base
|
|
131
|
+
- Quantization covers frozen block weights only (not embeddings, the output layer or trained weights) and is not bit-exact; `load_model` reads `.safetensors` causal-LM checkpoints
|
|
111
132
|
- Weight paging covers the repeated block stack and needs `optimizer_mode="cpu"`; each paged weight crosses PCIe twice per step
|
|
112
133
|
- Data parallel only (no tensor/pipeline parallelism); tested with 2 processes on 1 GPU, not across several GPUs or machines
|
|
113
134
|
- `TrainStreamTrainer` is single-process; fp16 + GradScaler and AMSGrad aren't supported; `torch.compile` hasn't been tested
|
|
@@ -55,6 +55,8 @@ if torch is not None:
|
|
|
55
55
|
"trainstream/csrc/memory_pinner.cpp",
|
|
56
56
|
"trainstream/csrc/offload_adamw.cpp",
|
|
57
57
|
"trainstream/csrc/offload_adamw_kernel.cu",
|
|
58
|
+
"trainstream/csrc/quant.cpp",
|
|
59
|
+
"trainstream/csrc/quant_kernel.cu",
|
|
58
60
|
],
|
|
59
61
|
include_dirs=[os.path.join(HERE, "trainstream", "csrc")],
|
|
60
62
|
extra_compile_args={"cxx": cxx_flags, "nvcc": nvcc_flags},
|
|
@@ -66,10 +68,10 @@ with open(os.path.join(HERE, "README.md"), encoding="utf-8") as fh:
|
|
|
66
68
|
|
|
67
69
|
setup(
|
|
68
70
|
name="trainstream",
|
|
69
|
-
version="0.
|
|
71
|
+
version="0.4.0",
|
|
70
72
|
author="John Anish G",
|
|
71
73
|
license="Apache-2.0",
|
|
72
|
-
description="Zero-dependency
|
|
74
|
+
description="Zero-dependency PyTorch training offload: optimizer, gradients, activations and weights to host RAM, with a memory planner and built-in int8/NF4 quantization.",
|
|
73
75
|
long_description=long_description,
|
|
74
76
|
long_description_content_type="text/markdown",
|
|
75
77
|
url="https://github.com/johnanishg/TrainStream",
|
|
@@ -227,3 +227,99 @@ def test_trainer_takes_a_cpu_model_and_pages_its_layers(tmp_path):
|
|
|
227
227
|
assert session.pager is not None and len(session.pager.paged) == 4
|
|
228
228
|
assert session.pager.frozen_pinned_bytes + session.pager.frozen_pageable_bytes > 0
|
|
229
229
|
assert losses(ts) == pytest.approx(losses(ref), abs=2e-4)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def saved_llama(tmp_path, tie=False, dtype=torch.float32):
|
|
233
|
+
torch.manual_seed(0)
|
|
234
|
+
cfg = LlamaConfig(vocab_size=VOCAB, hidden_size=128, intermediate_size=256, num_hidden_layers=4,
|
|
235
|
+
num_attention_heads=4, num_key_value_heads=2, max_position_embeddings=128,
|
|
236
|
+
tie_word_embeddings=tie)
|
|
237
|
+
LlamaForCausalLM(cfg).to(dtype).save_pretrained(str(tmp_path / "ckpt"))
|
|
238
|
+
return str(tmp_path / "ckpt")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@pytest.mark.parametrize("tie", [False, True])
|
|
242
|
+
def test_load_model_equals_from_pretrained(tmp_path, tie):
|
|
243
|
+
from trainstream.integrations.huggingface import load_model
|
|
244
|
+
d = saved_llama(tmp_path, tie=tie, dtype=torch.bfloat16)
|
|
245
|
+
ref = LlamaForCausalLM.from_pretrained(d, dtype=torch.bfloat16)
|
|
246
|
+
model = load_model(d, quantize="none")
|
|
247
|
+
assert model._ts_load_info["quantize"] is None
|
|
248
|
+
assert (model.lm_head.weight is model.model.embed_tokens.weight) == tie
|
|
249
|
+
ids = torch.randint(0, VOCAB, (2, 16))
|
|
250
|
+
with torch.no_grad():
|
|
251
|
+
assert torch.equal(ref(ids).logits, model(ids).logits)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@pytest.mark.parametrize("kind", ["int8", "nf4"])
|
|
255
|
+
def test_load_model_quantizes_while_reading(tmp_path, kind):
|
|
256
|
+
from trainstream import quant as Q
|
|
257
|
+
from trainstream.integrations.huggingface import load_model
|
|
258
|
+
d = saved_llama(tmp_path, dtype=torch.bfloat16)
|
|
259
|
+
ref = LlamaForCausalLM.from_pretrained(d, dtype=torch.bfloat16)
|
|
260
|
+
model = load_model(d, quantize=kind)
|
|
261
|
+
assert model._ts_load_info["quantized_tensors"] == 4 * 7 # q k v o gate up down per layer
|
|
262
|
+
for name, p in model.named_parameters():
|
|
263
|
+
qw = Q.quantized_of(p)
|
|
264
|
+
if qw is None:
|
|
265
|
+
assert torch.equal(p, ref.get_parameter(name)), name
|
|
266
|
+
continue
|
|
267
|
+
assert p.untyped_storage().nbytes() == 0 and not p.requires_grad and qw.kind == kind
|
|
268
|
+
q, a = Q.quantize_tensor(ref.get_parameter(name), kind, 64)
|
|
269
|
+
want = Q.dequantize_reference(q, a, kind, 64, p.numel(), torch.bfloat16).view(p.shape).cpu()
|
|
270
|
+
assert torch.equal(qw.dequantize().cpu(), want), name
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def test_load_model_auto_picks_the_most_precise_format_that_fits(tmp_path, monkeypatch):
|
|
274
|
+
import trainstream.planner as planner
|
|
275
|
+
from trainstream.integrations.huggingface import load_model
|
|
276
|
+
d = saved_llama(tmp_path, dtype=torch.bfloat16)
|
|
277
|
+
numel = sum(p.numel() for p in LlamaForCausalLM.from_pretrained(d).parameters())
|
|
278
|
+
monkeypatch.setattr(planner, "local_processes", lambda: 1)
|
|
279
|
+
for ram_factor, want in ((1.1, None), (0.7, "int8"), (0.45, "nf4")): # RAM as a multiple of the bf16 bytes
|
|
280
|
+
monkeypatch.setattr(planner, "available_ram_bytes", lambda f=ram_factor: int(f * 2 * numel))
|
|
281
|
+
model = load_model(d, ram_fraction=1.0)
|
|
282
|
+
assert model._ts_load_info["quantize"] == want, ram_factor
|
|
283
|
+
with pytest.raises(ValueError):
|
|
284
|
+
load_model(d, quantize="fp4")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def test_load_model_random_weights_and_overrides(tmp_path):
|
|
288
|
+
from trainstream.integrations.huggingface import load_model
|
|
289
|
+
d = saved_llama(tmp_path)
|
|
290
|
+
model = load_model(d, random_weights=True, quantize="nf4", num_hidden_layers=2)
|
|
291
|
+
assert len(model.model.layers) == 2 and model._ts_load_info["quantized_tensors"] == 2 * 7
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@pytest.mark.parametrize("kind", ["int8", "nf4"])
|
|
295
|
+
def test_trainer_qlora_on_loaded_model_matches_dequantized_reference(tmp_path, kind):
|
|
296
|
+
"""load_model(quantize=...) + LoRA + TrainStreamTrainer equals the stock
|
|
297
|
+
Trainer on a model holding the dequantized base weights."""
|
|
298
|
+
peft = pytest.importorskip("peft")
|
|
299
|
+
from trainstream import quant as Q
|
|
300
|
+
from trainstream.integrations.huggingface import load_model
|
|
301
|
+
d = saved_llama(tmp_path)
|
|
302
|
+
model = load_model(d, quantize=kind, dtype=torch.float32)
|
|
303
|
+
ref_base = LlamaForCausalLM.from_pretrained(d, dtype=torch.float32)
|
|
304
|
+
for name, p in model.named_parameters():
|
|
305
|
+
qw = Q.quantized_of(p)
|
|
306
|
+
if qw is not None:
|
|
307
|
+
ref_base.get_parameter(name).data.copy_(qw.dequantize().cpu())
|
|
308
|
+
lora = dict(r=8, lora_alpha=16, target_modules=["q_proj", "v_proj"], lora_dropout=0.0, task_type="CAUSAL_LM")
|
|
309
|
+
torch.manual_seed(1)
|
|
310
|
+
ref_model = peft.get_peft_model(ref_base.cuda(), peft.LoraConfig(**lora))
|
|
311
|
+
model = peft.get_peft_model(model, peft.LoraConfig(**lora))
|
|
312
|
+
ref_lora = dict(ref_model.named_parameters())
|
|
313
|
+
with torch.no_grad():
|
|
314
|
+
for n, p in model.named_parameters():
|
|
315
|
+
if p.requires_grad:
|
|
316
|
+
p.copy_(ref_lora[n].cpu())
|
|
317
|
+
ds = TokenDataset()
|
|
318
|
+
ref = Trainer(model=ref_model, args=args(tmp_path / "ref", max_steps=6), train_dataset=ds)
|
|
319
|
+
ref.train()
|
|
320
|
+
ts = TrainStreamTrainer(model=model, args=args(tmp_path / "ts", max_steps=6), train_dataset=ds,
|
|
321
|
+
trainstream={"offload": True, "activation_offload": False})
|
|
322
|
+
ts.train()
|
|
323
|
+
session = trainstream.wrapper.unwrap_session(model)
|
|
324
|
+
assert session.pager is not None and session.pager.quantized_bytes > 0
|
|
325
|
+
assert losses(ts) == pytest.approx(losses(ref), abs=2e-4)
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Quantized frozen weights (int8 / NF4): kernels, format, training, planner, decisions."""
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
import torch
|
|
7
|
+
from torch import nn
|
|
8
|
+
|
|
9
|
+
import trainstream
|
|
10
|
+
from trainstream import _C
|
|
11
|
+
from trainstream import quant as Q
|
|
12
|
+
from trainstream.planner import Budget
|
|
13
|
+
|
|
14
|
+
pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required")
|
|
15
|
+
|
|
16
|
+
DEV = "cuda"
|
|
17
|
+
HP = dict(lr=2e-3, betas=(0.9, 0.95), weight_decay=0.05)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# --------------------------------------------------------------------------- kernels and format
|
|
21
|
+
@pytest.mark.parametrize("kind", Q.KINDS)
|
|
22
|
+
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16, torch.float32])
|
|
23
|
+
@pytest.mark.parametrize("where", ["cuda", "host"])
|
|
24
|
+
def test_kernel_matches_reference(kind, dtype, where):
|
|
25
|
+
torch.manual_seed(0)
|
|
26
|
+
n = 64 * 513 + 6 # a partial last block
|
|
27
|
+
w = torch.randn(n, device=DEV) * 0.02
|
|
28
|
+
q, a = Q.quantize_tensor(w, kind, 64)
|
|
29
|
+
ref = Q.dequantize_reference(q, a, kind, 64, n, dtype)
|
|
30
|
+
if where == "host":
|
|
31
|
+
q, a = q.cpu().pin_memory(), a.cpu().pin_memory()
|
|
32
|
+
out = torch.empty(n, device=DEV, dtype=dtype)
|
|
33
|
+
ev, keep = _C.dequantize_async(out, q, a, kind, 64, _C.record_event(0))
|
|
34
|
+
ev.wait()
|
|
35
|
+
torch.cuda.synchronize()
|
|
36
|
+
assert torch.equal(out, ref)
|
|
37
|
+
err = ((ref.float() - w).abs().mean() / w.abs().mean()).item()
|
|
38
|
+
assert err < (0.12 if kind == "nf4" else 0.01)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_nf4_is_bit_identical_to_bitsandbytes():
|
|
42
|
+
bf = pytest.importorskip("bitsandbytes.functional")
|
|
43
|
+
torch.manual_seed(0)
|
|
44
|
+
w = (torch.randn(1024, 512, device=DEV) * 0.02).to(torch.bfloat16)
|
|
45
|
+
qb, st = bf.quantize_4bit(w, blocksize=64, quant_type="nf4", compress_statistics=False)
|
|
46
|
+
q, a = Q.quantize_tensor(w, "nf4", 64)
|
|
47
|
+
assert torch.equal(qb.view(-1), q) # same packed bytes
|
|
48
|
+
assert torch.equal(st.absmax.float(), a)
|
|
49
|
+
assert torch.equal(bf.dequantize_4bit(qb, st, quant_type="nf4"),
|
|
50
|
+
Q.dequantize_reference(q, a, "nf4", 64, w.numel(), torch.bfloat16).view_as(w))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# --------------------------------------------------------------------------- training
|
|
54
|
+
class Block(nn.Module):
|
|
55
|
+
"""A frozen 'base' MLP plus a small trainable adapter (LoRA-like)."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, d=256, r=8):
|
|
58
|
+
super().__init__()
|
|
59
|
+
self.ln = nn.LayerNorm(d)
|
|
60
|
+
self.fc1, self.fc2 = nn.Linear(d, 4 * d), nn.Linear(4 * d, d)
|
|
61
|
+
self.down, self.up = nn.Linear(d, r, bias=False), nn.Linear(r, d, bias=False)
|
|
62
|
+
nn.init.zeros_(self.up.weight)
|
|
63
|
+
|
|
64
|
+
def forward(self, x):
|
|
65
|
+
h = self.ln(x)
|
|
66
|
+
return x + self.fc2(torch.nn.functional.gelu(self.fc1(h))) + self.up(self.down(h))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Stack(nn.Module):
|
|
70
|
+
def __init__(self, d=256, n=6, vocab=128):
|
|
71
|
+
super().__init__()
|
|
72
|
+
self.emb = nn.Embedding(vocab, d)
|
|
73
|
+
self.blocks = nn.ModuleList(Block(d) for _ in range(n))
|
|
74
|
+
self.head = nn.Linear(d, vocab)
|
|
75
|
+
|
|
76
|
+
def forward(self, idx):
|
|
77
|
+
x = self.emb(idx)
|
|
78
|
+
for b in self.blocks:
|
|
79
|
+
x = b(x)
|
|
80
|
+
return self.head(x)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def make(seed=0):
|
|
84
|
+
torch.manual_seed(seed)
|
|
85
|
+
m = Stack().to(DEV)
|
|
86
|
+
for b in m.blocks: # the base is frozen, the adapters and the rest train
|
|
87
|
+
for lin in (b.fc1, b.fc2):
|
|
88
|
+
lin.weight.requires_grad_(False)
|
|
89
|
+
return m
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def tokens(n, seed=3, batch=8, seq=32, vocab=128):
|
|
93
|
+
g = torch.Generator().manual_seed(seed)
|
|
94
|
+
return [torch.randint(0, vocab, (batch, seq + 1), generator=g).to(DEV) for _ in range(n)]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def train(model, opt, data, between=None):
|
|
98
|
+
out = []
|
|
99
|
+
for k, t in enumerate(data):
|
|
100
|
+
logits = model(t[:, :-1])
|
|
101
|
+
loss = nn.functional.cross_entropy(logits.float().reshape(-1, logits.size(-1)), t[:, 1:].reshape(-1))
|
|
102
|
+
loss.backward()
|
|
103
|
+
opt.step()
|
|
104
|
+
opt.zero_grad()
|
|
105
|
+
out.append(loss.item())
|
|
106
|
+
if between:
|
|
107
|
+
between(k)
|
|
108
|
+
return out
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def dequantized_reference(model, kind):
|
|
112
|
+
"""A copy whose frozen weights hold exactly the values quantized training uses."""
|
|
113
|
+
ref = copy.deepcopy(model)
|
|
114
|
+
with torch.no_grad():
|
|
115
|
+
for b in ref.blocks:
|
|
116
|
+
for lin in (b.fc1, b.fc2):
|
|
117
|
+
q, a = Q.quantize_tensor(lin.weight, kind, 64)
|
|
118
|
+
lin.weight.copy_(Q.dequantize_reference(q, a, kind, 64, lin.weight.numel(), lin.weight.dtype)
|
|
119
|
+
.view_as(lin.weight))
|
|
120
|
+
return ref
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def trainable(m):
|
|
124
|
+
return [p for p in m.parameters() if p.requires_grad]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@pytest.mark.parametrize("kind", Q.KINDS)
|
|
128
|
+
@pytest.mark.parametrize("placement", ["near", "far"])
|
|
129
|
+
def test_quantized_training_equals_training_on_the_dequantized_weights(kind, placement):
|
|
130
|
+
base = make()
|
|
131
|
+
ref = dequantized_reference(base, kind)
|
|
132
|
+
opt_ref = torch.optim.AdamW(trainable(ref), foreach=False, **HP)
|
|
133
|
+
mine = copy.deepcopy(base)
|
|
134
|
+
mine, opt = trainstream.wrap(mine, torch.optim.AdamW(trainable(mine), **HP), offload=True, quantize=kind,
|
|
135
|
+
param_offload=(placement == "far"), activation_offload=False)
|
|
136
|
+
try:
|
|
137
|
+
s = trainstream.wrapper.unwrap_session(mine)
|
|
138
|
+
assert s.config["quantize"] == kind
|
|
139
|
+
assert len(s.pager.quant_blocks) == 6
|
|
140
|
+
assert (len(s.pager.qnear) == 6) == (placement == "near")
|
|
141
|
+
for b in mine.blocks: # no full-precision copy of the frozen base stays in VRAM between uses
|
|
142
|
+
assert Q.quantized_of(b.fc1.weight) is not None
|
|
143
|
+
assert all(Q.quantized_of(p) is None for p in trainable(b)) # trained weights never quantized
|
|
144
|
+
data = tokens(6)
|
|
145
|
+
assert train(mine, opt, data) == pytest.approx(train(ref, opt_ref, data), abs=1e-4)
|
|
146
|
+
rep = trainstream.memory_report(mine)
|
|
147
|
+
assert rep["params"]["quantize"] == kind and rep["params"]["quantized_bytes"] > 0
|
|
148
|
+
assert any(f"to {kind}" in e for e in rep["events"])
|
|
149
|
+
finally:
|
|
150
|
+
trainstream.unwrap(mine)
|
|
151
|
+
# unwrap leaves the dequantized values in place
|
|
152
|
+
assert torch.equal(mine.blocks[0].fc1.weight, ref.blocks[0].fc1.weight)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_moving_quantized_blocks_near_and_far_does_not_change_results():
|
|
156
|
+
base = make()
|
|
157
|
+
a, b = copy.deepcopy(base), copy.deepcopy(base)
|
|
158
|
+
a, opt_a = trainstream.wrap(a, torch.optim.AdamW(trainable(a), **HP), offload=True, quantize="nf4",
|
|
159
|
+
param_offload=False, activation_offload=False)
|
|
160
|
+
b, opt_b = trainstream.wrap(b, torch.optim.AdamW(trainable(b), **HP), offload=True, quantize="nf4",
|
|
161
|
+
param_offload=False, activation_offload=False)
|
|
162
|
+
pager = trainstream.wrapper.unwrap_session(b).pager
|
|
163
|
+
moves = {0: ([], [0, 2, 4]), 2: ([0], [1]), 4: ([1, 2, 4], [])} # after step k: (near, far)
|
|
164
|
+
|
|
165
|
+
def between(k):
|
|
166
|
+
if k in moves:
|
|
167
|
+
near, far = moves[k]
|
|
168
|
+
for i in far:
|
|
169
|
+
pager.page(i)
|
|
170
|
+
for i in near:
|
|
171
|
+
pager.unpage(i)
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
data = tokens(7)
|
|
175
|
+
assert train(a, opt_a, data) == train(b, opt_b, data, between)
|
|
176
|
+
# far {0,2,4} after step 0; {1,2,4} after step 2; all near again after step 4
|
|
177
|
+
assert pager.remote == set() and pager.qnear == set(range(6))
|
|
178
|
+
assert pager.stats["pages"] >= 4 and pager.stats["unpages"] >= 4
|
|
179
|
+
finally:
|
|
180
|
+
trainstream.unwrap(a)
|
|
181
|
+
trainstream.unwrap(b)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_state_dict_serves_the_dequantized_values():
|
|
185
|
+
base = make()
|
|
186
|
+
ref = dequantized_reference(base, "nf4")
|
|
187
|
+
mine, opt = trainstream.wrap(copy.deepcopy(base), torch.optim.AdamW(trainable(base), **HP), offload=True,
|
|
188
|
+
quantize="nf4", activation_offload=False)
|
|
189
|
+
try:
|
|
190
|
+
sd = mine.state_dict()
|
|
191
|
+
assert torch.equal(sd["blocks.2.fc1.weight"].to(DEV), ref.blocks[2].fc1.weight)
|
|
192
|
+
finally:
|
|
193
|
+
trainstream.unwrap(mine)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# --------------------------------------------------------------------------- decisions and errors
|
|
197
|
+
def fake_budget(vram_gib, ram_gib):
|
|
198
|
+
G = 1 << 30
|
|
199
|
+
return Budget(vram_total=int(vram_gib * G), vram_usable=int(vram_gib * G), ram_total=int(64 * G),
|
|
200
|
+
ram_available=int(ram_gib * G), pin_budget=int(25 * G))
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def frozen_bytes(m):
|
|
204
|
+
return sum(p.numel() * p.element_size() for p in m.parameters() if not p.requires_grad)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# frozen weights here are fp32: int8 keeps ~27% of their bytes, nf4 ~14%
|
|
208
|
+
@pytest.mark.parametrize("ram_factor,expect", [(1.5, None), (0.5, "int8"), (0.2, "nf4")])
|
|
209
|
+
def test_auto_quantizes_only_when_bf16_cannot_fit(monkeypatch, ram_factor, expect):
|
|
210
|
+
m = make()
|
|
211
|
+
t = sum(p.numel() for p in trainable(m))
|
|
212
|
+
room = 12 * t + frozen_bytes(m) * ram_factor # master + bf16 moments + staging for the trained part
|
|
213
|
+
monkeypatch.setattr(trainstream.wrapper, "measure_budget", lambda dev: fake_budget(20, room / 2**30 / 0.9))
|
|
214
|
+
m, opt = trainstream.wrap(m, torch.optim.AdamW(trainable(m), **HP), offload=True, activation_offload=False)
|
|
215
|
+
try:
|
|
216
|
+
assert trainstream.wrapper.unwrap_session(m).config["quantize"] == expect
|
|
217
|
+
finally:
|
|
218
|
+
trainstream.unwrap(m)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_auto_refuses_when_even_nf4_cannot_fit(monkeypatch):
|
|
222
|
+
m = make()
|
|
223
|
+
monkeypatch.setattr(trainstream.wrapper, "measure_budget", lambda dev: fake_budget(20, 0.001))
|
|
224
|
+
with pytest.raises(MemoryError, match="host RAM"):
|
|
225
|
+
trainstream.wrap(m, torch.optim.AdamW(trainable(m), **HP), offload=True, activation_offload=False)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_prefer_speed_picks_nf4_when_it_fits_in_vram_and_bf16_would_stream(monkeypatch):
|
|
229
|
+
m = make()
|
|
230
|
+
blocks = list(m.blocks)
|
|
231
|
+
bf16 = sum(p.numel() * p.element_size() for b in blocks for p in b.parameters())
|
|
232
|
+
nf4 = sum(Q.packed_bytes(p.numel(), "nf4") if Q.eligible(p) else p.numel() * p.element_size()
|
|
233
|
+
for b in blocks for p in b.parameters())
|
|
234
|
+
outside = sum(p.numel() * p.element_size() for p in m.parameters()) - bf16
|
|
235
|
+
# With a small budget V the planner keeps 0.6 V - outside - 96 MiB for block weights
|
|
236
|
+
# (10% headroom, 10% in-flight cap, 20% activation reserve, workspace): put that
|
|
237
|
+
# between the 4-bit and the bf16 size of the blocks.
|
|
238
|
+
v = (outside + (96 << 20) + (nf4 + bf16) / 2) / 0.6
|
|
239
|
+
monkeypatch.setattr(trainstream.wrapper, "measure_budget", lambda dev: fake_budget(v / 2**30, 32))
|
|
240
|
+
m, opt = trainstream.wrap(m, torch.optim.AdamW(trainable(m), **HP), offload=True, activation_offload=False,
|
|
241
|
+
prefer="speed", check_memory=False)
|
|
242
|
+
try:
|
|
243
|
+
assert trainstream.wrapper.unwrap_session(m).config["quantize"] == "nf4"
|
|
244
|
+
finally:
|
|
245
|
+
trainstream.unwrap(m)
|
|
246
|
+
m2 = make()
|
|
247
|
+
m2, opt2 = trainstream.wrap(m2, torch.optim.AdamW(trainable(m2), **HP), offload=True, activation_offload=False,
|
|
248
|
+
check_memory=False) # prefer="exact": stream bf16 instead
|
|
249
|
+
try:
|
|
250
|
+
s2 = trainstream.wrapper.unwrap_session(m2)
|
|
251
|
+
assert s2.config["quantize"] is None and s2.pager is not None and s2.pager.remote
|
|
252
|
+
finally:
|
|
253
|
+
trainstream.unwrap(m2)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def test_invalid_quantization_requests():
|
|
257
|
+
full = Stack().to(DEV) # everything trainable: nothing frozen to quantize
|
|
258
|
+
with pytest.raises(ValueError, match="frozen"):
|
|
259
|
+
trainstream.wrap(full, quantize="nf4")
|
|
260
|
+
with pytest.raises(ValueError, match="quantize must be"):
|
|
261
|
+
trainstream.wrap(make(), quantize="fp3")
|
|
262
|
+
with pytest.raises(ValueError, match="prefer must be"):
|
|
263
|
+
trainstream.wrap(make(), prefer="fast")
|
|
264
|
+
pre = make()
|
|
265
|
+
Q.quantize_model(pre, "int8")
|
|
266
|
+
with pytest.raises(ValueError, match="already holds quantized"):
|
|
267
|
+
trainstream.wrap(pre, torch.optim.AdamW(trainable(pre)), quantize="none")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def test_quantize_model_then_wrap_uses_the_given_kind():
|
|
271
|
+
m = make()
|
|
272
|
+
info = Q.quantize_model(m, "int8")
|
|
273
|
+
assert info["tensors"] == 12 and info["bytes_after"] < info["bytes_before"] / 3
|
|
274
|
+
m, opt = trainstream.wrap(m, torch.optim.AdamW(trainable(m), **HP), offload=True, activation_offload=False)
|
|
275
|
+
try:
|
|
276
|
+
assert trainstream.wrapper.unwrap_session(m).config["quantize"] == "int8"
|
|
277
|
+
train(m, opt, tokens(2))
|
|
278
|
+
finally:
|
|
279
|
+
trainstream.unwrap(m)
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
model, optimizer = trainstream.wrap(model, optimizer)
|
|
5
5
|
|
|
6
6
|
Optimizer state, gradients, a profiled fraction of activations and (when
|
|
7
|
-
needed) the transformer-block weights themselves
|
|
7
|
+
needed) the transformer-block weights themselves (frozen ones optionally
|
|
8
|
+
quantized to int8 / NF4) are moved to host RAM over
|
|
8
9
|
dedicated CUDA copy streams, so models that would not fit in VRAM with a stock
|
|
9
10
|
optimizer can still be trained on a single GPU — or on several, data parallel.
|
|
10
11
|
"""
|
|
@@ -31,9 +32,10 @@ from .losses import causal_lm_loss, chunked_cross_entropy
|
|
|
31
32
|
from .optimizer import OffloadAdamW
|
|
32
33
|
from .paging import ParamPager
|
|
33
34
|
from .profiler import HardwareProfile, profile_hardware
|
|
35
|
+
from .quant import quantize_model
|
|
34
36
|
from .wrapper import TrainStreamSession, memory_report, unwrap, wrap
|
|
35
37
|
|
|
36
|
-
__version__ = "0.
|
|
38
|
+
__version__ = "0.4.0"
|
|
37
39
|
|
|
38
40
|
__all__ = [
|
|
39
41
|
"wrap",
|
|
@@ -49,6 +51,7 @@ __all__ = [
|
|
|
49
51
|
"TrainStreamSession",
|
|
50
52
|
"HardwareProfile",
|
|
51
53
|
"profile_hardware",
|
|
54
|
+
"quantize_model",
|
|
52
55
|
"find_blocks",
|
|
53
56
|
"pinned_stats",
|
|
54
57
|
"transfer_stats",
|
|
@@ -25,7 +25,8 @@ import torch
|
|
|
25
25
|
|
|
26
26
|
NAME = "trainstream_C"
|
|
27
27
|
CSRC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "csrc")
|
|
28
|
-
SOURCES = ["bindings.cpp", "memory_pinner.cpp", "offload_adamw.cpp", "offload_adamw_kernel.cu"
|
|
28
|
+
SOURCES = ["bindings.cpp", "memory_pinner.cpp", "offload_adamw.cpp", "offload_adamw_kernel.cu",
|
|
29
|
+
"quant.cpp", "quant_kernel.cu"]
|
|
29
30
|
HEADERS = ["kernels.h", "offload_adamw.h", "trainstream.h"]
|
|
30
31
|
|
|
31
32
|
if sys.platform == "win32":
|
|
@@ -92,6 +92,8 @@ PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
|
|
|
92
92
|
py::arg("device") = -1, py::arg("sizes") = std::vector<int64_t>{},
|
|
93
93
|
py::arg("strides") = std::vector<int64_t>{});
|
|
94
94
|
m.def("transfer_stats", &transfer_stats, py::arg("device") = -1);
|
|
95
|
+
m.def("dequantize_async", &dequantize_async, py::arg("dst"), py::arg("q"), py::arg("absmax"), py::arg("kind"),
|
|
96
|
+
py::arg("block"), py::arg("after") = EventPtr());
|
|
95
97
|
m.def("synchronize_streams", [](int device) {
|
|
96
98
|
Runtime& rt = runtime(device);
|
|
97
99
|
TS_CUDA_CHECK(cudaStreamSynchronize(rt.d2h));
|
|
@@ -44,6 +44,13 @@ cudaError_t adamw_zero_copy(DType dtype, void* param, const void* grad, void* ex
|
|
|
44
44
|
const AdamWParams& hp, const float* grad_sq_norm, int sm_count,
|
|
45
45
|
cudaStream_t stream, bool bf16_moments = false, uint32_t seed = 0);
|
|
46
46
|
|
|
47
|
+
// Blockwise-quantized frozen weights (see quant_kernel.cu).
|
|
48
|
+
enum class QKind : int { kNF4 = 0, kInt8 = 1 };
|
|
49
|
+
|
|
50
|
+
// out[0..n) = dequantized values; one fp32 absmax per `block` values (block even).
|
|
51
|
+
cudaError_t dequantize(QKind kind, DType out_dtype, const void* q, const float* absmax, void* out, int64_t n,
|
|
52
|
+
int block, int sm_count, cudaStream_t stream);
|
|
53
|
+
|
|
47
54
|
// out[0] += sum(grad[i]^2). Used for global gradient-norm clipping on the GPU path.
|
|
48
55
|
cudaError_t grad_sq_norm_accumulate(DType dtype, const void* grad, int64_t n, float* out,
|
|
49
56
|
int sm_count, cudaStream_t stream);
|