trainstream 0.3.0__tar.gz → 0.4.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.
Files changed (48) hide show
  1. trainstream-0.4.1/PKG-INFO +325 -0
  2. trainstream-0.4.1/README.md +285 -0
  3. trainstream-0.4.1/pyproject.toml +10 -0
  4. {trainstream-0.3.0 → trainstream-0.4.1}/setup.py +5 -2
  5. {trainstream-0.3.0 → trainstream-0.4.1}/tests/test_huggingface.py +96 -0
  6. trainstream-0.4.1/tests/test_quant.py +279 -0
  7. {trainstream-0.3.0 → trainstream-0.4.1}/trainstream/__init__.py +7 -3
  8. {trainstream-0.3.0 → trainstream-0.4.1}/trainstream/_build.py +9 -5
  9. {trainstream-0.3.0 → trainstream-0.4.1}/trainstream/csrc/bindings.cpp +2 -0
  10. {trainstream-0.3.0 → trainstream-0.4.1}/trainstream/csrc/kernels.h +7 -0
  11. trainstream-0.4.1/trainstream/csrc/quant.cpp +71 -0
  12. trainstream-0.4.1/trainstream/csrc/quant_kernel.cu +124 -0
  13. {trainstream-0.3.0 → trainstream-0.4.1}/trainstream/csrc/trainstream.h +8 -0
  14. {trainstream-0.3.0 → trainstream-0.4.1}/trainstream/integrations/huggingface.py +119 -1
  15. {trainstream-0.3.0 → trainstream-0.4.1}/trainstream/paging.py +103 -7
  16. {trainstream-0.3.0 → trainstream-0.4.1}/trainstream/planner.py +5 -3
  17. trainstream-0.4.1/trainstream/quant.py +214 -0
  18. {trainstream-0.3.0 → trainstream-0.4.1}/trainstream/wrapper.py +114 -26
  19. trainstream-0.4.1/trainstream.egg-info/PKG-INFO +325 -0
  20. {trainstream-0.3.0 → trainstream-0.4.1}/trainstream.egg-info/SOURCES.txt +4 -0
  21. trainstream-0.3.0/PKG-INFO +0 -158
  22. trainstream-0.3.0/README.md +0 -118
  23. trainstream-0.3.0/pyproject.toml +0 -6
  24. trainstream-0.3.0/trainstream.egg-info/PKG-INFO +0 -158
  25. {trainstream-0.3.0 → trainstream-0.4.1}/LICENSE +0 -0
  26. {trainstream-0.3.0 → trainstream-0.4.1}/MANIFEST.in +0 -0
  27. {trainstream-0.3.0 → trainstream-0.4.1}/NOTICE +0 -0
  28. {trainstream-0.3.0 → trainstream-0.4.1}/build.bat +0 -0
  29. {trainstream-0.3.0 → trainstream-0.4.1}/setup.cfg +0 -0
  30. {trainstream-0.3.0 → trainstream-0.4.1}/tests/test_distributed.py +0 -0
  31. {trainstream-0.3.0 → trainstream-0.4.1}/tests/test_features.py +0 -0
  32. {trainstream-0.3.0 → trainstream-0.4.1}/tests/test_offload.py +0 -0
  33. {trainstream-0.3.0 → trainstream-0.4.1}/tests/test_optimizer.py +0 -0
  34. {trainstream-0.3.0 → trainstream-0.4.1}/tests/test_paging.py +0 -0
  35. {trainstream-0.3.0 → trainstream-0.4.1}/tests/test_planner.py +0 -0
  36. {trainstream-0.3.0 → trainstream-0.4.1}/trainstream/csrc/memory_pinner.cpp +0 -0
  37. {trainstream-0.3.0 → trainstream-0.4.1}/trainstream/csrc/offload_adamw.cpp +0 -0
  38. {trainstream-0.3.0 → trainstream-0.4.1}/trainstream/csrc/offload_adamw.h +0 -0
  39. {trainstream-0.3.0 → trainstream-0.4.1}/trainstream/csrc/offload_adamw_kernel.cu +0 -0
  40. {trainstream-0.3.0 → trainstream-0.4.1}/trainstream/hooks.py +0 -0
  41. {trainstream-0.3.0 → trainstream-0.4.1}/trainstream/integrations/__init__.py +0 -0
  42. {trainstream-0.3.0 → trainstream-0.4.1}/trainstream/losses.py +0 -0
  43. {trainstream-0.3.0 → trainstream-0.4.1}/trainstream/optimizer.py +0 -0
  44. {trainstream-0.3.0 → trainstream-0.4.1}/trainstream/profiler.py +0 -0
  45. {trainstream-0.3.0 → trainstream-0.4.1}/trainstream.egg-info/dependency_links.txt +0 -0
  46. {trainstream-0.3.0 → trainstream-0.4.1}/trainstream.egg-info/not-zip-safe +0 -0
  47. {trainstream-0.3.0 → trainstream-0.4.1}/trainstream.egg-info/requires.txt +0 -0
  48. {trainstream-0.3.0 → trainstream-0.4.1}/trainstream.egg-info/top_level.txt +0 -0
@@ -0,0 +1,325 @@
1
+ Metadata-Version: 2.4
2
+ Name: trainstream
3
+ Version: 0.4.1
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
+ Home-page: https://github.com/johnanishg/TrainStream
6
+ Author: John Anish G
7
+ License: Apache-2.0
8
+ Project-URL: Source, https://github.com/johnanishg/TrainStream
9
+ Project-URL: Issues, https://github.com/johnanishg/TrainStream/issues
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: C++
15
+ Classifier: Environment :: GPU :: NVIDIA CUDA
16
+ Classifier: Operating System :: Microsoft :: Windows
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ License-File: NOTICE
23
+ Requires-Dist: ninja
24
+ Provides-Extra: hf
25
+ Requires-Dist: transformers; extra == "hf"
26
+ Requires-Dist: accelerate; extra == "hf"
27
+ Requires-Dist: peft; extra == "hf"
28
+ Dynamic: author
29
+ Dynamic: classifier
30
+ Dynamic: description
31
+ Dynamic: description-content-type
32
+ Dynamic: home-page
33
+ Dynamic: license
34
+ Dynamic: license-file
35
+ Dynamic: project-url
36
+ Dynamic: provides-extra
37
+ Dynamic: requires-dist
38
+ Dynamic: requires-python
39
+ Dynamic: summary
40
+
41
+ # TrainStream
42
+
43
+ Train and fine-tune models larger than your GPU's memory. TrainStream moves optimizer state, gradients, activations and, when needed, the weights themselves into system RAM, and streams them back over PCIe exactly when the GPU needs them. 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
+
45
+ ```python
46
+ import trainstream
47
+ model, optimizer = trainstream.wrap(model, optimizer)
48
+ ```
49
+
50
+ TrainStream is a C++/CUDA extension for PyTorch. It uses only the C++ standard library, the CUDA runtime and PyTorch's ATen: no DeepSpeed, no bitsandbytes. Updates match `torch.optim.AdamW` (max parameter difference ≈3·10⁻⁷ after 20 steps), and activation and weight paging are bit-exact.
51
+
52
+ ## What's new in 0.4
53
+
54
+ - **0.4.1:** install instructions for uv (`uv pip install`, `uv add` / `uv sync`) next to pip, checked on Windows and Linux; clearer messages when the installed torch is the CPU-only build.
55
+
56
+ In 0.4.0:
57
+
58
+ - **Built-in weight quantization.** `wrap(..., quantize="auto" | "none" | "int8" | "nf4")` stores frozen weights quantized and expands them with TrainStream's own CUDA kernel just before each layer runs. `"auto"` (the default) quantizes only when a run can't fit otherwise; `prefer="speed"` also lets it choose NF4 when that keeps every layer in VRAM.
59
+ - **`load_model()`** reads a Hugging Face checkpoint one tensor at a time and quantizes while reading, so a base larger than your RAM never exists in full precision.
60
+ - **`trainstream.quantize_model()`** quantizes a model you've already loaded, in place.
61
+ - Real Qwen2.5-14B LoRA on a 24 GB laptop GPU: **1.37 → 0.87 s/step**. A 32B-shaped base (65.5 GB in bf16, more than the machine's 64 GB of RAM) trains at 2.53 s/step.
62
+
63
+ ## Results (RTX 5090 Laptop GPU 24 GB, 64 GB RAM)
64
+
65
+ | Workload | Stock PyTorch | TrainStream |
66
+ |---|---|---|
67
+ | 1.8B-parameter GPT, fp32, batch 8×512 | 34.1 GiB peak: exceeds VRAM, driver spills to RAM, 13.7 s/step | 20.6 GiB, 1.1 s/step (13.5 GiB with all activations paged) |
68
+ | Qwen2.5-1.5B full fine-tune (Hugging Face Trainer) | 35.9 GiB peak (spill), 111.7 s / 10 steps | 18.7 GiB, 28.5 s / 10 steps, same losses |
69
+ | 1.8B GPT, weights paged to RAM | — | 2.4 GiB peak, 2.1 s/step; 1.84 GiB inside a 4 GB VRAM cap with bf16 moments |
70
+ | 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 |
71
+ | LoRA r=16, Qwen2.5-7B architecture | 20.3 GB peak, 0.27 s/step | 4.6 GB peak, 1.25 s/step (every layer paged) |
72
+ | LoRA r=16, real Qwen2.5-14B, bf16 base | does not fit (29.5 GB of weights) | 1.37 s/step: the memory planner keeps 28 of 48 layers in VRAM and streams 20 from RAM |
73
+ | LoRA r=16, real Qwen2.5-14B, NF4 base (`quantize="auto", prefer="speed"`) | does not fit | **0.87 s/step**: all 48 layers in VRAM as 4-bit, 18.3 GB peak, 11.3 GB process RAM |
74
+ | LoRA r=16, Qwen2.5-32B architecture, NF4 base (`load_model(quantize="nf4")`) | does not fit (65.5 GB in bf16, more than the RAM) | 2.53 s/step, 21.3 GB peak, 31.4 GB process RAM |
75
+ | 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 |
76
+
77
+ Offloading costs throughput when a model already fits, which is why `offload="auto"` measures two steps and switches to plain fused GPU AdamW if offloading isn't needed. TrainStream's value is training what doesn't fit, or freeing VRAM for larger batches. All numbers come from one machine. The 7B and 32B rows use the exact Qwen2.5 shapes with random weights; the 14B rows use the real checkpoint.
78
+
79
+ ## Requirements
80
+
81
+ - An NVIDIA GPU and a CUDA build of PyTorch
82
+ - 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)
83
+ - Optional, for the Hugging Face integration: `transformers`, `accelerate`, and `peft` for LoRA (all three come with `trainstream[hf]`)
84
+
85
+ Tested on an RTX 5090 Laptop GPU with Windows 11 (PyTorch 2.14.1, CUDA 13.4, MSVC, Python 3.14), where all 123 tests pass, and Ubuntu 24.04 under WSL2 (PyTorch 2.14.1, CUDA 13.3, GCC 13.3, Python 3.12), where 122 pass and 1 is skipped (the bitsandbytes comparison, without bitsandbytes installed). Installs were checked with pip and with uv 0.13 (`uv pip install`, `uv sync` in a project, and building from the sdist) on both. Native Linux and other GPUs are untested.
86
+
87
+ ## Install
88
+
89
+ TrainStream installs with **pip** or **uv**. It is a small pure-Python wheel and doesn't pull in PyTorch, because you choose the CUDA build of PyTorch that matches your driver. Install CUDA PyTorch first (or in the same command), then TrainStream.
90
+
91
+ **pip**
92
+
93
+ ```bash
94
+ pip install torch --index-url https://download.pytorch.org/whl/cu130 # if you don't have CUDA PyTorch yet
95
+ pip install trainstream # core library
96
+ pip install "trainstream[hf]" # + transformers, accelerate, peft
97
+ ```
98
+
99
+ **uv** (`uv pip`)
100
+
101
+ ```bash
102
+ uv pip install torch trainstream --torch-backend=auto # picks the CUDA build for your driver
103
+ uv pip install torch "trainstream[hf]" --torch-backend=auto # + transformers, accelerate, peft
104
+ ```
105
+
106
+ **uv project** (`uv add` / `uv sync`). Point `torch` at the PyTorch index in your `pyproject.toml`:
107
+
108
+ ```toml
109
+ [project]
110
+ dependencies = ["torch", "trainstream"] # or "trainstream[hf]"
111
+
112
+ [tool.uv.sources]
113
+ torch = { index = "pytorch-cu130" }
114
+
115
+ [[tool.uv.index]]
116
+ name = "pytorch-cu130"
117
+ url = "https://download.pytorch.org/whl/cu130"
118
+ explicit = true
119
+ ```
120
+
121
+ Then `uv sync` and `uv run python train.py`.
122
+
123
+ > **CPU-only torch trap.** On Windows, `torch` from PyPI is the CPU build. If something else that depends on torch (for example `transformers` or `accelerate`) is installed without the CUDA index, the installer may fetch it, and `import trainstream` then stops with a message saying so. Fix: `pip install torch --index-url https://download.pytorch.org/whl/cu130 --force-reinstall`, or `uv pip install torch --torch-backend=auto --reinstall-package torch`.
124
+
125
+ The first `import trainstream` compiles the CUDA extension against your PyTorch (16–19 s on Windows and 18 s on Linux, measured) and caches it; later imports take about a second. On Windows it finds Visual Studio and the CUDA toolkit itself, so any terminal works. The cache is keyed by PyTorch, CUDA and Python version and GPU, so upgrading PyTorch triggers one rebuild.
126
+
127
+ Upgrade: `pip install -U --no-cache-dir trainstream` or `uv pip install -U trainstream` (`uv lock --upgrade-package trainstream` in a project).
128
+
129
+ To compile at install time instead of on first import: `pip install trainstream --no-binary trainstream --no-build-isolation` (or the same flags with `uv pip install`). From a checkout: `build.bat` (Windows) or `python setup.py build_ext --inplace`.
130
+
131
+ ## Quick start
132
+
133
+ ```python
134
+ import torch, trainstream
135
+
136
+ model = MyModel().cuda() # 1. build the model
137
+ optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4) # 2. a normal optimizer
138
+ model, optimizer = trainstream.wrap(model, optimizer) # 3. wrap once, before the first forward
139
+
140
+ for x, y in loader: # 4. an ordinary training loop
141
+ with torch.autocast("cuda", dtype=torch.bfloat16):
142
+ loss = model(x.cuda(), y.cuda())
143
+ loss.backward() # gradients stream to host RAM during backward
144
+ optimizer.step() # host AdamW; returns once the update is queued
145
+ optimizer.zero_grad()
146
+ ```
147
+
148
+ ### A model larger than the GPU
149
+
150
+ Leave it on the CPU and pass `device`. The memory planner measures the machine's VRAM and RAM, keeps as many layers resident as fit, and streams the rest:
151
+
152
+ ```python
153
+ model = MyGPT(cfg) # stays on the CPU
154
+ optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)
155
+ model, optimizer = trainstream.wrap(model, optimizer, device="cuda")
156
+ ```
157
+
158
+ ### Hugging Face Trainer
159
+
160
+ Swap one class name (`pip install transformers accelerate`):
161
+
162
+ ```python
163
+ from transformers import AutoModelForCausalLM, TrainingArguments
164
+ from trainstream.integrations.huggingface import TrainStreamTrainer
165
+
166
+ model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-1.5B", dtype=torch.float32)
167
+ args = TrainingArguments(output_dir="out", bf16=True, per_device_train_batch_size=4,
168
+ gradient_accumulation_steps=2, learning_rate=2e-5, max_grad_norm=1.0)
169
+ trainer = TrainStreamTrainer(model=model, args=args, train_dataset=ds,
170
+ trainstream={"moment_dtype": "auto"}) # optional: any wrap() option
171
+ trainer.train()
172
+ ```
173
+
174
+ `TrainStreamTrainer` computes the causal-LM loss in chunks, so the full logits tensor never exists. Hyper-parameters, the LR schedule, accumulation, clipping, checkpoints and `resume_from_checkpoint` carry over. Use `bf16=True` or fp32; fp16 with loss scaling isn't supported. For your own loop: `model, optimizer = wrap_hf(model, optimizer)`.
175
+
176
+ ### LoRA on a quantized base (QLoRA-style)
177
+
178
+ ```python
179
+ import peft
180
+ from trainstream.integrations.huggingface import TrainStreamTrainer, load_model
181
+
182
+ model = load_model("Qwen/Qwen2.5-14B", quantize="nf4") # streams the checkpoint, quantizes while reading
183
+ model = peft.get_peft_model(model, peft.LoraConfig(
184
+ r=16, lora_alpha=32, lora_dropout=0.0, task_type="CAUSAL_LM",
185
+ target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"]))
186
+ trainer = TrainStreamTrainer(model=model, args=TrainingArguments(output_dir="out", bf16=True), train_dataset=ds)
187
+ trainer.train()
188
+ ```
189
+
190
+ Or let `wrap()` decide: `trainstream.wrap(model, optimizer, device="cuda", quantize="auto", prefer="speed")`. Or quantize beforehand: `trainstream.quantize_model(model, "int8")`. Only frozen weights are quantized; freeze the base (or add adapters) first.
191
+
192
+ ### Data parallel
193
+
194
+ ```python
195
+ # torchrun --nproc_per_node=2 train.py
196
+ import os, torch.distributed as dist
197
+ dist.init_process_group("nccl")
198
+ torch.cuda.set_device(int(os.environ["LOCAL_RANK"]))
199
+ model, optimizer = trainstream.wrap(MyModel().cuda(), torch.optim.AdamW(...)) # detected automatically
200
+ ```
201
+
202
+ ### Inspecting what it decided
203
+
204
+ ```python
205
+ r = trainstream.memory_report(model)
206
+ r["events"] # plain-text log: quantization, memory plan, resident mode, .to()
207
+ r["params"]["paged_blocks"] # layers whose weights stream from host RAM
208
+ r["params"]["quantize"] # None, "int8" or "nf4"
209
+ r["plan"]["history"] # every planner move, with the measured peaks
210
+ ```
211
+
212
+ ### Rules of thumb
213
+
214
+ - Order: build the model → freeze / add adapters → (quantize) → create the optimizer → `wrap()` → train.
215
+ - Use `max_grad_norm=` instead of `torch.nn.utils.clip_grad_norm_`; gradients have already left the GPU.
216
+ - Declare gradient accumulation with `accumulation_steps=`.
217
+ - `model.state_dict()` and `optimizer.state_dict()` work while wrapped. `trainstream.unwrap(model)` loads every weight back into VRAM and removes the hooks.
218
+
219
+ ## Options
220
+
221
+ ### `trainstream.wrap(model, optimizer=None, **options) → (model, optimizer)`
222
+
223
+ | Option | Accepted values | Default | What it does |
224
+ |---|---|---|---|
225
+ | `optimizer` | `torch.optim.AdamW`, `torch.optim.Adam`, `trainstream.OffloadAdamW`, `None` | `None` | converted, with hyper-parameters and state migrated; `None` creates one with `lr` |
226
+ | `optimizer_mode` | `"cpu"`, `"gpu"`, `"auto"` | `"cpu"` | host AdamW, a zero-copy CUDA kernel, or chosen from the hardware profile |
227
+ | `offload` | `"auto"`, `True` | `"auto"` | `"auto"` switches to fused torch AdamW after 2 steps if the model fits |
228
+ | `activation_offload` | `"auto"`, `True`, `False`, fraction 0.0–1.0 | `"auto"` | how many blocks' activations are paged to RAM |
229
+ | `param_offload` | `"auto"`, `True`, `False`, fraction 0.0–1.0, list of block indices | `"auto"` | `"auto"`: the memory planner decides which layers stay in VRAM |
230
+ | `quantize` | `"auto"`, `"none"`, `None`, `False`, `"int8"`, `"nf4"` | `"auto"` | frozen weights only; `"auto"` quantizes only when the run can't fit otherwise (int8 before NF4) |
231
+ | `quant_block_size` | even integer ≥ 2 | `64` | values per fp32 scale |
232
+ | `prefer` | `"exact"`, `"speed"` | `"exact"` | `"speed"` lets `quantize="auto"` choose NF4 when it keeps every layer in VRAM |
233
+ | `moment_dtype` | `"auto"`, `"float32"`, `"bfloat16"`, `"bf16"`, `torch.bfloat16` | `"auto"` | fp32 unless that wouldn't fit in RAM; bf16 uses stochastic rounding |
234
+ | `device` | `None`, `"cuda"`, `"cuda:N"`, `torch.device` | `None` | target GPU when the model is on the CPU |
235
+ | `blocks` | `None`, list of layer modules | `None` | the repeated layer stack; auto-detected |
236
+ | `distributed` | `None`, `True`, `False` | `None` | data parallel when a process group with > 1 rank exists |
237
+ | `guard_params` | `True`, `False` | `True` | reads of a weight wait for its in-flight update |
238
+ | `max_grad_norm` | `None`, float | `None` | global-norm clipping inside the optimizer |
239
+ | `accumulation_steps` | integer ≥ 1 | `1` | backward passes per `step()` |
240
+ | `vram_headroom` | float 0.0–1.0 | `0.10` | share of usable VRAM the planner leaves free |
241
+ | `check_memory` | `True`, `False` | `True` | raise `MemoryError` at `wrap()` when the run can't fit |
242
+ | `max_inflight_bytes` | `None`, bytes | `None` (1 GiB or 10% of VRAM) | VRAM held by transfers still crossing PCIe |
243
+ | `max_activation_slowdown` | float | `0.15` | how much slower `"auto"` activation paging may make a step |
244
+ | `prefetch_depth` | integer ≥ 1 | `1` | activation blocks fetched ahead in backward |
245
+ | `min_offload_bytes` | bytes | `1048576` | smaller activations stay on the GPU |
246
+ | `threads` | `None`, integer | `None` (all cores) | host AdamW threads |
247
+ | `profile` | `True`, `False` | `True` | run (or load cached) hardware benchmarks |
248
+ | `lr` | float | `1e-3` | used only when `optimizer` is `None` |
249
+ | `verbose` | `True`, `False` | `False` | print decisions as they happen |
250
+
251
+ Anything else raises `ValueError` (for example `quantize="fp4"`).
252
+
253
+ ### Quantization formats
254
+
255
+ | `quantize=` | Bytes per weight | Mean relative error | Use it when |
256
+ |---|---|---|---|
257
+ | `"none"` | 2 (bf16) | 0 (bit-exact) | it fits |
258
+ | `"int8"` | 1.06 | 0.6 % | bf16 doesn't fit in RAM |
259
+ | `"nf4"` | 0.56 | 9 % | int8 doesn't fit either, or with `prefer="speed"` |
260
+ | `"auto"` | — | — | decided from the measured RAM and VRAM |
261
+
262
+ NF4 is bit-identical to bitsandbytes' NF4 (same codes, packing and scales); TrainStream doesn't depend on bitsandbytes. A quantized run trains on the dequantized weights, as QLoRA does, so it isn't bit-exact with the bf16 base.
263
+
264
+ ### `load_model(name_or_path, ...)` (in `trainstream.integrations.huggingface`)
265
+
266
+ | Argument | Accepted values | Default |
267
+ |---|---|---|
268
+ | `name_or_path` | Hub id or local folder with `.safetensors` files | — |
269
+ | `quantize` | `"auto"`, `"none"`, `None`, `False`, `"int8"`, `"nf4"` | `"auto"` (quantize only if bf16 wouldn't fit in `ram_fraction` of free RAM) |
270
+ | `dtype` | `torch.bfloat16`, `torch.float16`, `torch.float32` | `torch.bfloat16` |
271
+ | `quant_block_size` | even integer ≥ 2 | `64` |
272
+ | `random_weights` | `True`, `False` | `False` (build the shapes without reading a checkpoint) |
273
+ | `ram_fraction` | float 0.0–1.0 | `0.85` |
274
+ | `trust_remote_code` | `True`, `False` | `False` |
275
+ | `**config_overrides` | any config field, e.g. `num_hidden_layers=4` | — |
276
+
277
+ ### Other functions
278
+
279
+ | Function | Arguments |
280
+ |---|---|
281
+ | `trainstream.quantize_model(model, quantize="nf4", *, blocks=None, block_size=64, pin_budget=None)` | `quantize`: `"int8"`, `"nf4"`; returns `{"tensors", "bytes_before", "bytes_after"}` |
282
+ | `TrainStreamTrainer(..., trainstream={...})` | any `wrap()` option, plus `"chunked_loss"` (`True`/`False`, default `True`) and `"loss_chunk_size"` (default `1024`) |
283
+ | `trainstream.OffloadAdamW(params, lr=1e-3, betas=(0.9, 0.999), eps=1e-8, weight_decay=1e-2, *, mode="cpu", max_grad_norm=None, accumulation_steps=1, moment_dtype="float32", ...)` | `mode`: `"cpu"`, `"gpu"`; `moment_dtype`: `"float32"`, `"bfloat16"` |
284
+ | `trainstream.chunked_cross_entropy(hidden, weight, targets, bias=None, *, chunk_size=1024, ignore_index=-100, reduction="mean", num_items=None)` | `reduction`: `"mean"`, `"sum"`; `causal_lm_loss` takes the same and shifts labels |
285
+ | `trainstream.profile_hardware(device=None, quick=True, force=False)` | returns a `HardwareProfile` |
286
+ | `trainstream.memory_report(model)`, `trainstream.unwrap(model)` | a wrapped model |
287
+
288
+ ### Environment variables
289
+
290
+ | Variable | Effect |
291
+ |---|---|
292
+ | `TRAINSTREAM_CACHE_DIR` | where builds are cached (default `%LOCALAPPDATA%\trainstream\build`, Linux `~/.cache/trainstream`) |
293
+ | `TRAINSTREAM_VERBOSE_BUILD=1` | show compiler output |
294
+ | `TORCH_CUDA_ARCH_LIST` | GPU architectures to compile for (default: the GPUs present) |
295
+ | `CUDA_HOME` / `CUDA_PATH` | CUDA toolkit to use, if it isn't found automatically |
296
+ | `LOCAL_WORLD_SIZE` / `SLURM_NTASKS_PER_NODE` | processes sharing the machine; the planner splits RAM between them |
297
+
298
+ ## How it works
299
+
300
+ 1. **Measure.** At `wrap()`: free VRAM, available RAM, the page-lock budget, PCIe bandwidth, and the size of every layer.
301
+ 2. **Plan.** Which layers stay in VRAM and which stream from RAM, the optimizer moment precision, whether to quantize the frozen weights, and how many activations to page. Runs that can't fit are refused with an explanation.
302
+ 3. **Stream.** Gradients go to pinned host memory as autograd produces them; AdamW runs on host threads while backward continues; weights and activations come back on dedicated CUDA streams ordered by events, overlapping compute.
303
+ 4. **Adapt.** The first steps' measured peaks move layers in or out of VRAM, then the plan settles; a watchdog reacts if a later step grows. If the model fits anyway, it switches to stock fused AdamW.
304
+
305
+ ## Examples
306
+
307
+ | Script | What it does | Main flags |
308
+ |---|---|---|
309
+ | `examples/lora_large_model.py` | LoRA on bases larger than the GPU, bf16 or quantized | `--size 1.5b\|7b\|14b\|32b`, `--model HUB_ID`, `--quantize auto\|none\|int8\|nf4`, `--prefer exact\|speed`, `--stock`, `--steps`, `--batch`, `--seq`, `--rank` |
310
+ | `examples/hf_finetune.py` | fine-tune a Hugging Face model, optionally beside the stock Trainer | `--model`, `--lora`, `--compare`, `--activation-offload` |
311
+ | `examples/train_bigger_than_vram.py` | 1.8B model (7.3 GB of fp32 weights) inside a 4 GB VRAM cap | `--vram-gb`, `--moments fp32\|bf16`, `--layers`, `--d-model` |
312
+ | `examples/verify_ram_offload.py` | proves where training memory lives, with a PASS/FAIL verdict | `--gpt`, `--hf`, `--vram-cap-gb`, `--engine both\|baseline\|trainstream` |
313
+
314
+ ## Limitations
315
+
316
+ - Host RAM is the ceiling: 8–12 bytes per trained parameter and 2–4 per frozen one (1.06 with int8, 0.56 with NF4), plus activations. Measured on 64 GB: a 3.6B full fine-tune, and LoRA on a 32.8B NF4 base.
317
+ - Quantization covers frozen block weights only (not embeddings, the output layer or trained weights) and isn't bit-exact. `load_model` reads `.safetensors` causal-LM checkpoints only.
318
+ - Weight paging covers the repeated block stack and needs `optimizer_mode="cpu"`; each paged weight crosses PCIe twice per step.
319
+ - Data parallel only (no tensor or pipeline parallelism); tested with 2 processes on 1 GPU, not across several GPUs or machines.
320
+ - `TrainStreamTrainer` is single-process. fp16 with a GradScaler and AMSGrad aren't supported; `torch.compile` hasn't been tested.
321
+ - Host AdamW speed is bounded by system RAM bandwidth.
322
+
323
+ ## License
324
+
325
+ Apache License 2.0 — see [LICENSE](LICENSE) and [NOTICE](NOTICE). Copyright 2026 John Anish G.
@@ -0,0 +1,285 @@
1
+ # TrainStream
2
+
3
+ Train and fine-tune models larger than your GPU's memory. TrainStream moves optimizer state, gradients, activations and, when needed, the weights themselves into system RAM, and streams them back over PCIe exactly when the GPU needs them. 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
+
5
+ ```python
6
+ import trainstream
7
+ model, optimizer = trainstream.wrap(model, optimizer)
8
+ ```
9
+
10
+ TrainStream is a C++/CUDA extension for PyTorch. It uses only the C++ standard library, the CUDA runtime and PyTorch's ATen: no DeepSpeed, no bitsandbytes. Updates match `torch.optim.AdamW` (max parameter difference ≈3·10⁻⁷ after 20 steps), and activation and weight paging are bit-exact.
11
+
12
+ ## What's new in 0.4
13
+
14
+ - **0.4.1:** install instructions for uv (`uv pip install`, `uv add` / `uv sync`) next to pip, checked on Windows and Linux; clearer messages when the installed torch is the CPU-only build.
15
+
16
+ In 0.4.0:
17
+
18
+ - **Built-in weight quantization.** `wrap(..., quantize="auto" | "none" | "int8" | "nf4")` stores frozen weights quantized and expands them with TrainStream's own CUDA kernel just before each layer runs. `"auto"` (the default) quantizes only when a run can't fit otherwise; `prefer="speed"` also lets it choose NF4 when that keeps every layer in VRAM.
19
+ - **`load_model()`** reads a Hugging Face checkpoint one tensor at a time and quantizes while reading, so a base larger than your RAM never exists in full precision.
20
+ - **`trainstream.quantize_model()`** quantizes a model you've already loaded, in place.
21
+ - Real Qwen2.5-14B LoRA on a 24 GB laptop GPU: **1.37 → 0.87 s/step**. A 32B-shaped base (65.5 GB in bf16, more than the machine's 64 GB of RAM) trains at 2.53 s/step.
22
+
23
+ ## Results (RTX 5090 Laptop GPU 24 GB, 64 GB RAM)
24
+
25
+ | Workload | Stock PyTorch | TrainStream |
26
+ |---|---|---|
27
+ | 1.8B-parameter GPT, fp32, batch 8×512 | 34.1 GiB peak: exceeds VRAM, driver spills to RAM, 13.7 s/step | 20.6 GiB, 1.1 s/step (13.5 GiB with all activations paged) |
28
+ | Qwen2.5-1.5B full fine-tune (Hugging Face Trainer) | 35.9 GiB peak (spill), 111.7 s / 10 steps | 18.7 GiB, 28.5 s / 10 steps, same losses |
29
+ | 1.8B GPT, weights paged to RAM | — | 2.4 GiB peak, 2.1 s/step; 1.84 GiB inside a 4 GB VRAM cap with bf16 moments |
30
+ | 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 |
31
+ | LoRA r=16, Qwen2.5-7B architecture | 20.3 GB peak, 0.27 s/step | 4.6 GB peak, 1.25 s/step (every layer paged) |
32
+ | LoRA r=16, real Qwen2.5-14B, bf16 base | does not fit (29.5 GB of weights) | 1.37 s/step: the memory planner keeps 28 of 48 layers in VRAM and streams 20 from RAM |
33
+ | LoRA r=16, real Qwen2.5-14B, NF4 base (`quantize="auto", prefer="speed"`) | does not fit | **0.87 s/step**: all 48 layers in VRAM as 4-bit, 18.3 GB peak, 11.3 GB process RAM |
34
+ | LoRA r=16, Qwen2.5-32B architecture, NF4 base (`load_model(quantize="nf4")`) | does not fit (65.5 GB in bf16, more than the RAM) | 2.53 s/step, 21.3 GB peak, 31.4 GB process RAM |
35
+ | 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 |
36
+
37
+ Offloading costs throughput when a model already fits, which is why `offload="auto"` measures two steps and switches to plain fused GPU AdamW if offloading isn't needed. TrainStream's value is training what doesn't fit, or freeing VRAM for larger batches. All numbers come from one machine. The 7B and 32B rows use the exact Qwen2.5 shapes with random weights; the 14B rows use the real checkpoint.
38
+
39
+ ## Requirements
40
+
41
+ - An NVIDIA GPU and a CUDA build of PyTorch
42
+ - 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)
43
+ - Optional, for the Hugging Face integration: `transformers`, `accelerate`, and `peft` for LoRA (all three come with `trainstream[hf]`)
44
+
45
+ Tested on an RTX 5090 Laptop GPU with Windows 11 (PyTorch 2.14.1, CUDA 13.4, MSVC, Python 3.14), where all 123 tests pass, and Ubuntu 24.04 under WSL2 (PyTorch 2.14.1, CUDA 13.3, GCC 13.3, Python 3.12), where 122 pass and 1 is skipped (the bitsandbytes comparison, without bitsandbytes installed). Installs were checked with pip and with uv 0.13 (`uv pip install`, `uv sync` in a project, and building from the sdist) on both. Native Linux and other GPUs are untested.
46
+
47
+ ## Install
48
+
49
+ TrainStream installs with **pip** or **uv**. It is a small pure-Python wheel and doesn't pull in PyTorch, because you choose the CUDA build of PyTorch that matches your driver. Install CUDA PyTorch first (or in the same command), then TrainStream.
50
+
51
+ **pip**
52
+
53
+ ```bash
54
+ pip install torch --index-url https://download.pytorch.org/whl/cu130 # if you don't have CUDA PyTorch yet
55
+ pip install trainstream # core library
56
+ pip install "trainstream[hf]" # + transformers, accelerate, peft
57
+ ```
58
+
59
+ **uv** (`uv pip`)
60
+
61
+ ```bash
62
+ uv pip install torch trainstream --torch-backend=auto # picks the CUDA build for your driver
63
+ uv pip install torch "trainstream[hf]" --torch-backend=auto # + transformers, accelerate, peft
64
+ ```
65
+
66
+ **uv project** (`uv add` / `uv sync`). Point `torch` at the PyTorch index in your `pyproject.toml`:
67
+
68
+ ```toml
69
+ [project]
70
+ dependencies = ["torch", "trainstream"] # or "trainstream[hf]"
71
+
72
+ [tool.uv.sources]
73
+ torch = { index = "pytorch-cu130" }
74
+
75
+ [[tool.uv.index]]
76
+ name = "pytorch-cu130"
77
+ url = "https://download.pytorch.org/whl/cu130"
78
+ explicit = true
79
+ ```
80
+
81
+ Then `uv sync` and `uv run python train.py`.
82
+
83
+ > **CPU-only torch trap.** On Windows, `torch` from PyPI is the CPU build. If something else that depends on torch (for example `transformers` or `accelerate`) is installed without the CUDA index, the installer may fetch it, and `import trainstream` then stops with a message saying so. Fix: `pip install torch --index-url https://download.pytorch.org/whl/cu130 --force-reinstall`, or `uv pip install torch --torch-backend=auto --reinstall-package torch`.
84
+
85
+ The first `import trainstream` compiles the CUDA extension against your PyTorch (16–19 s on Windows and 18 s on Linux, measured) and caches it; later imports take about a second. On Windows it finds Visual Studio and the CUDA toolkit itself, so any terminal works. The cache is keyed by PyTorch, CUDA and Python version and GPU, so upgrading PyTorch triggers one rebuild.
86
+
87
+ Upgrade: `pip install -U --no-cache-dir trainstream` or `uv pip install -U trainstream` (`uv lock --upgrade-package trainstream` in a project).
88
+
89
+ To compile at install time instead of on first import: `pip install trainstream --no-binary trainstream --no-build-isolation` (or the same flags with `uv pip install`). From a checkout: `build.bat` (Windows) or `python setup.py build_ext --inplace`.
90
+
91
+ ## Quick start
92
+
93
+ ```python
94
+ import torch, trainstream
95
+
96
+ model = MyModel().cuda() # 1. build the model
97
+ optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4) # 2. a normal optimizer
98
+ model, optimizer = trainstream.wrap(model, optimizer) # 3. wrap once, before the first forward
99
+
100
+ for x, y in loader: # 4. an ordinary training loop
101
+ with torch.autocast("cuda", dtype=torch.bfloat16):
102
+ loss = model(x.cuda(), y.cuda())
103
+ loss.backward() # gradients stream to host RAM during backward
104
+ optimizer.step() # host AdamW; returns once the update is queued
105
+ optimizer.zero_grad()
106
+ ```
107
+
108
+ ### A model larger than the GPU
109
+
110
+ Leave it on the CPU and pass `device`. The memory planner measures the machine's VRAM and RAM, keeps as many layers resident as fit, and streams the rest:
111
+
112
+ ```python
113
+ model = MyGPT(cfg) # stays on the CPU
114
+ optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)
115
+ model, optimizer = trainstream.wrap(model, optimizer, device="cuda")
116
+ ```
117
+
118
+ ### Hugging Face Trainer
119
+
120
+ Swap one class name (`pip install transformers accelerate`):
121
+
122
+ ```python
123
+ from transformers import AutoModelForCausalLM, TrainingArguments
124
+ from trainstream.integrations.huggingface import TrainStreamTrainer
125
+
126
+ model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-1.5B", dtype=torch.float32)
127
+ args = TrainingArguments(output_dir="out", bf16=True, per_device_train_batch_size=4,
128
+ gradient_accumulation_steps=2, learning_rate=2e-5, max_grad_norm=1.0)
129
+ trainer = TrainStreamTrainer(model=model, args=args, train_dataset=ds,
130
+ trainstream={"moment_dtype": "auto"}) # optional: any wrap() option
131
+ trainer.train()
132
+ ```
133
+
134
+ `TrainStreamTrainer` computes the causal-LM loss in chunks, so the full logits tensor never exists. Hyper-parameters, the LR schedule, accumulation, clipping, checkpoints and `resume_from_checkpoint` carry over. Use `bf16=True` or fp32; fp16 with loss scaling isn't supported. For your own loop: `model, optimizer = wrap_hf(model, optimizer)`.
135
+
136
+ ### LoRA on a quantized base (QLoRA-style)
137
+
138
+ ```python
139
+ import peft
140
+ from trainstream.integrations.huggingface import TrainStreamTrainer, load_model
141
+
142
+ model = load_model("Qwen/Qwen2.5-14B", quantize="nf4") # streams the checkpoint, quantizes while reading
143
+ model = peft.get_peft_model(model, peft.LoraConfig(
144
+ r=16, lora_alpha=32, lora_dropout=0.0, task_type="CAUSAL_LM",
145
+ target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"]))
146
+ trainer = TrainStreamTrainer(model=model, args=TrainingArguments(output_dir="out", bf16=True), train_dataset=ds)
147
+ trainer.train()
148
+ ```
149
+
150
+ Or let `wrap()` decide: `trainstream.wrap(model, optimizer, device="cuda", quantize="auto", prefer="speed")`. Or quantize beforehand: `trainstream.quantize_model(model, "int8")`. Only frozen weights are quantized; freeze the base (or add adapters) first.
151
+
152
+ ### Data parallel
153
+
154
+ ```python
155
+ # torchrun --nproc_per_node=2 train.py
156
+ import os, torch.distributed as dist
157
+ dist.init_process_group("nccl")
158
+ torch.cuda.set_device(int(os.environ["LOCAL_RANK"]))
159
+ model, optimizer = trainstream.wrap(MyModel().cuda(), torch.optim.AdamW(...)) # detected automatically
160
+ ```
161
+
162
+ ### Inspecting what it decided
163
+
164
+ ```python
165
+ r = trainstream.memory_report(model)
166
+ r["events"] # plain-text log: quantization, memory plan, resident mode, .to()
167
+ r["params"]["paged_blocks"] # layers whose weights stream from host RAM
168
+ r["params"]["quantize"] # None, "int8" or "nf4"
169
+ r["plan"]["history"] # every planner move, with the measured peaks
170
+ ```
171
+
172
+ ### Rules of thumb
173
+
174
+ - Order: build the model → freeze / add adapters → (quantize) → create the optimizer → `wrap()` → train.
175
+ - Use `max_grad_norm=` instead of `torch.nn.utils.clip_grad_norm_`; gradients have already left the GPU.
176
+ - Declare gradient accumulation with `accumulation_steps=`.
177
+ - `model.state_dict()` and `optimizer.state_dict()` work while wrapped. `trainstream.unwrap(model)` loads every weight back into VRAM and removes the hooks.
178
+
179
+ ## Options
180
+
181
+ ### `trainstream.wrap(model, optimizer=None, **options) → (model, optimizer)`
182
+
183
+ | Option | Accepted values | Default | What it does |
184
+ |---|---|---|---|
185
+ | `optimizer` | `torch.optim.AdamW`, `torch.optim.Adam`, `trainstream.OffloadAdamW`, `None` | `None` | converted, with hyper-parameters and state migrated; `None` creates one with `lr` |
186
+ | `optimizer_mode` | `"cpu"`, `"gpu"`, `"auto"` | `"cpu"` | host AdamW, a zero-copy CUDA kernel, or chosen from the hardware profile |
187
+ | `offload` | `"auto"`, `True` | `"auto"` | `"auto"` switches to fused torch AdamW after 2 steps if the model fits |
188
+ | `activation_offload` | `"auto"`, `True`, `False`, fraction 0.0–1.0 | `"auto"` | how many blocks' activations are paged to RAM |
189
+ | `param_offload` | `"auto"`, `True`, `False`, fraction 0.0–1.0, list of block indices | `"auto"` | `"auto"`: the memory planner decides which layers stay in VRAM |
190
+ | `quantize` | `"auto"`, `"none"`, `None`, `False`, `"int8"`, `"nf4"` | `"auto"` | frozen weights only; `"auto"` quantizes only when the run can't fit otherwise (int8 before NF4) |
191
+ | `quant_block_size` | even integer ≥ 2 | `64` | values per fp32 scale |
192
+ | `prefer` | `"exact"`, `"speed"` | `"exact"` | `"speed"` lets `quantize="auto"` choose NF4 when it keeps every layer in VRAM |
193
+ | `moment_dtype` | `"auto"`, `"float32"`, `"bfloat16"`, `"bf16"`, `torch.bfloat16` | `"auto"` | fp32 unless that wouldn't fit in RAM; bf16 uses stochastic rounding |
194
+ | `device` | `None`, `"cuda"`, `"cuda:N"`, `torch.device` | `None` | target GPU when the model is on the CPU |
195
+ | `blocks` | `None`, list of layer modules | `None` | the repeated layer stack; auto-detected |
196
+ | `distributed` | `None`, `True`, `False` | `None` | data parallel when a process group with > 1 rank exists |
197
+ | `guard_params` | `True`, `False` | `True` | reads of a weight wait for its in-flight update |
198
+ | `max_grad_norm` | `None`, float | `None` | global-norm clipping inside the optimizer |
199
+ | `accumulation_steps` | integer ≥ 1 | `1` | backward passes per `step()` |
200
+ | `vram_headroom` | float 0.0–1.0 | `0.10` | share of usable VRAM the planner leaves free |
201
+ | `check_memory` | `True`, `False` | `True` | raise `MemoryError` at `wrap()` when the run can't fit |
202
+ | `max_inflight_bytes` | `None`, bytes | `None` (1 GiB or 10% of VRAM) | VRAM held by transfers still crossing PCIe |
203
+ | `max_activation_slowdown` | float | `0.15` | how much slower `"auto"` activation paging may make a step |
204
+ | `prefetch_depth` | integer ≥ 1 | `1` | activation blocks fetched ahead in backward |
205
+ | `min_offload_bytes` | bytes | `1048576` | smaller activations stay on the GPU |
206
+ | `threads` | `None`, integer | `None` (all cores) | host AdamW threads |
207
+ | `profile` | `True`, `False` | `True` | run (or load cached) hardware benchmarks |
208
+ | `lr` | float | `1e-3` | used only when `optimizer` is `None` |
209
+ | `verbose` | `True`, `False` | `False` | print decisions as they happen |
210
+
211
+ Anything else raises `ValueError` (for example `quantize="fp4"`).
212
+
213
+ ### Quantization formats
214
+
215
+ | `quantize=` | Bytes per weight | Mean relative error | Use it when |
216
+ |---|---|---|---|
217
+ | `"none"` | 2 (bf16) | 0 (bit-exact) | it fits |
218
+ | `"int8"` | 1.06 | 0.6 % | bf16 doesn't fit in RAM |
219
+ | `"nf4"` | 0.56 | 9 % | int8 doesn't fit either, or with `prefer="speed"` |
220
+ | `"auto"` | — | — | decided from the measured RAM and VRAM |
221
+
222
+ NF4 is bit-identical to bitsandbytes' NF4 (same codes, packing and scales); TrainStream doesn't depend on bitsandbytes. A quantized run trains on the dequantized weights, as QLoRA does, so it isn't bit-exact with the bf16 base.
223
+
224
+ ### `load_model(name_or_path, ...)` (in `trainstream.integrations.huggingface`)
225
+
226
+ | Argument | Accepted values | Default |
227
+ |---|---|---|
228
+ | `name_or_path` | Hub id or local folder with `.safetensors` files | — |
229
+ | `quantize` | `"auto"`, `"none"`, `None`, `False`, `"int8"`, `"nf4"` | `"auto"` (quantize only if bf16 wouldn't fit in `ram_fraction` of free RAM) |
230
+ | `dtype` | `torch.bfloat16`, `torch.float16`, `torch.float32` | `torch.bfloat16` |
231
+ | `quant_block_size` | even integer ≥ 2 | `64` |
232
+ | `random_weights` | `True`, `False` | `False` (build the shapes without reading a checkpoint) |
233
+ | `ram_fraction` | float 0.0–1.0 | `0.85` |
234
+ | `trust_remote_code` | `True`, `False` | `False` |
235
+ | `**config_overrides` | any config field, e.g. `num_hidden_layers=4` | — |
236
+
237
+ ### Other functions
238
+
239
+ | Function | Arguments |
240
+ |---|---|
241
+ | `trainstream.quantize_model(model, quantize="nf4", *, blocks=None, block_size=64, pin_budget=None)` | `quantize`: `"int8"`, `"nf4"`; returns `{"tensors", "bytes_before", "bytes_after"}` |
242
+ | `TrainStreamTrainer(..., trainstream={...})` | any `wrap()` option, plus `"chunked_loss"` (`True`/`False`, default `True`) and `"loss_chunk_size"` (default `1024`) |
243
+ | `trainstream.OffloadAdamW(params, lr=1e-3, betas=(0.9, 0.999), eps=1e-8, weight_decay=1e-2, *, mode="cpu", max_grad_norm=None, accumulation_steps=1, moment_dtype="float32", ...)` | `mode`: `"cpu"`, `"gpu"`; `moment_dtype`: `"float32"`, `"bfloat16"` |
244
+ | `trainstream.chunked_cross_entropy(hidden, weight, targets, bias=None, *, chunk_size=1024, ignore_index=-100, reduction="mean", num_items=None)` | `reduction`: `"mean"`, `"sum"`; `causal_lm_loss` takes the same and shifts labels |
245
+ | `trainstream.profile_hardware(device=None, quick=True, force=False)` | returns a `HardwareProfile` |
246
+ | `trainstream.memory_report(model)`, `trainstream.unwrap(model)` | a wrapped model |
247
+
248
+ ### Environment variables
249
+
250
+ | Variable | Effect |
251
+ |---|---|
252
+ | `TRAINSTREAM_CACHE_DIR` | where builds are cached (default `%LOCALAPPDATA%\trainstream\build`, Linux `~/.cache/trainstream`) |
253
+ | `TRAINSTREAM_VERBOSE_BUILD=1` | show compiler output |
254
+ | `TORCH_CUDA_ARCH_LIST` | GPU architectures to compile for (default: the GPUs present) |
255
+ | `CUDA_HOME` / `CUDA_PATH` | CUDA toolkit to use, if it isn't found automatically |
256
+ | `LOCAL_WORLD_SIZE` / `SLURM_NTASKS_PER_NODE` | processes sharing the machine; the planner splits RAM between them |
257
+
258
+ ## How it works
259
+
260
+ 1. **Measure.** At `wrap()`: free VRAM, available RAM, the page-lock budget, PCIe bandwidth, and the size of every layer.
261
+ 2. **Plan.** Which layers stay in VRAM and which stream from RAM, the optimizer moment precision, whether to quantize the frozen weights, and how many activations to page. Runs that can't fit are refused with an explanation.
262
+ 3. **Stream.** Gradients go to pinned host memory as autograd produces them; AdamW runs on host threads while backward continues; weights and activations come back on dedicated CUDA streams ordered by events, overlapping compute.
263
+ 4. **Adapt.** The first steps' measured peaks move layers in or out of VRAM, then the plan settles; a watchdog reacts if a later step grows. If the model fits anyway, it switches to stock fused AdamW.
264
+
265
+ ## Examples
266
+
267
+ | Script | What it does | Main flags |
268
+ |---|---|---|
269
+ | `examples/lora_large_model.py` | LoRA on bases larger than the GPU, bf16 or quantized | `--size 1.5b\|7b\|14b\|32b`, `--model HUB_ID`, `--quantize auto\|none\|int8\|nf4`, `--prefer exact\|speed`, `--stock`, `--steps`, `--batch`, `--seq`, `--rank` |
270
+ | `examples/hf_finetune.py` | fine-tune a Hugging Face model, optionally beside the stock Trainer | `--model`, `--lora`, `--compare`, `--activation-offload` |
271
+ | `examples/train_bigger_than_vram.py` | 1.8B model (7.3 GB of fp32 weights) inside a 4 GB VRAM cap | `--vram-gb`, `--moments fp32\|bf16`, `--layers`, `--d-model` |
272
+ | `examples/verify_ram_offload.py` | proves where training memory lives, with a PASS/FAIL verdict | `--gpt`, `--hf`, `--vram-cap-gb`, `--engine both\|baseline\|trainstream` |
273
+
274
+ ## Limitations
275
+
276
+ - Host RAM is the ceiling: 8–12 bytes per trained parameter and 2–4 per frozen one (1.06 with int8, 0.56 with NF4), plus activations. Measured on 64 GB: a 3.6B full fine-tune, and LoRA on a 32.8B NF4 base.
277
+ - Quantization covers frozen block weights only (not embeddings, the output layer or trained weights) and isn't bit-exact. `load_model` reads `.safetensors` causal-LM checkpoints only.
278
+ - Weight paging covers the repeated block stack and needs `optimizer_mode="cpu"`; each paged weight crosses PCIe twice per step.
279
+ - Data parallel only (no tensor or pipeline parallelism); tested with 2 processes on 1 GPU, not across several GPUs or machines.
280
+ - `TrainStreamTrainer` is single-process. fp16 with a GradScaler and AMSGrad aren't supported; `torch.compile` hasn't been tested.
281
+ - Host AdamW speed is bounded by system RAM bandwidth.
282
+
283
+ ## License
284
+
285
+ Apache License 2.0 — see [LICENSE](LICENSE) and [NOTICE](NOTICE). Copyright 2026 John Anish G.
@@ -0,0 +1,10 @@
1
+ [build-system]
2
+ # PyTorch is deliberately not a build requirement. Built in an isolated
3
+ # environment (pip, uv, `python -m build`, `uv build`), this produces a
4
+ # pure-Python wheel, and trainstream/_build.py compiles the extension on first
5
+ # import against the user's own CUDA build of PyTorch. To compile at install
6
+ # time against the torch already installed instead:
7
+ # pip install trainstream --no-binary trainstream --no-build-isolation
8
+ # uv pip install trainstream --no-binary trainstream --no-build-isolation
9
+ requires = ["setuptools>=64", "wheel"]
10
+ build-backend = "setuptools.build_meta"