trainstream 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. {trainstream-0.1.0 → trainstream-0.2.0}/MANIFEST.in +1 -3
  2. trainstream-0.2.0/PKG-INFO +157 -0
  3. trainstream-0.2.0/README.md +117 -0
  4. {trainstream-0.1.0 → trainstream-0.2.0}/setup.py +20 -27
  5. trainstream-0.2.0/tests/test_distributed.py +82 -0
  6. trainstream-0.2.0/tests/test_features.py +279 -0
  7. {trainstream-0.1.0 → trainstream-0.2.0}/tests/test_huggingface.py +60 -7
  8. {trainstream-0.1.0 → trainstream-0.2.0}/tests/test_offload.py +18 -3
  9. {trainstream-0.1.0 → trainstream-0.2.0}/tests/test_optimizer.py +1 -1
  10. trainstream-0.2.0/tests/test_paging.py +259 -0
  11. {trainstream-0.1.0 → trainstream-0.2.0}/trainstream/__init__.py +23 -10
  12. trainstream-0.2.0/trainstream/_build.py +201 -0
  13. {trainstream-0.1.0 → trainstream-0.2.0/trainstream}/csrc/bindings.cpp +21 -5
  14. {trainstream-0.1.0 → trainstream-0.2.0/trainstream}/csrc/kernels.h +6 -3
  15. {trainstream-0.1.0 → trainstream-0.2.0/trainstream}/csrc/offload_adamw.cpp +250 -55
  16. {trainstream-0.1.0 → trainstream-0.2.0/trainstream}/csrc/offload_adamw.h +55 -7
  17. {trainstream-0.1.0 → trainstream-0.2.0/trainstream}/csrc/offload_adamw_kernel.cu +54 -17
  18. {trainstream-0.1.0 → trainstream-0.2.0}/trainstream/hooks.py +8 -6
  19. {trainstream-0.1.0 → trainstream-0.2.0}/trainstream/integrations/huggingface.py +85 -1
  20. trainstream-0.2.0/trainstream/losses.py +103 -0
  21. trainstream-0.2.0/trainstream/optimizer.py +674 -0
  22. trainstream-0.2.0/trainstream/paging.py +262 -0
  23. trainstream-0.2.0/trainstream/wrapper.py +486 -0
  24. trainstream-0.2.0/trainstream.egg-info/PKG-INFO +157 -0
  25. {trainstream-0.1.0 → trainstream-0.2.0}/trainstream.egg-info/SOURCES.txt +13 -7
  26. {trainstream-0.1.0 → trainstream-0.2.0}/trainstream.egg-info/requires.txt +1 -0
  27. trainstream-0.1.0/PKG-INFO +0 -119
  28. trainstream-0.1.0/README.md +0 -81
  29. trainstream-0.1.0/trainstream/optimizer.py +0 -333
  30. trainstream-0.1.0/trainstream/wrapper.py +0 -191
  31. trainstream-0.1.0/trainstream.egg-info/PKG-INFO +0 -119
  32. {trainstream-0.1.0 → trainstream-0.2.0}/LICENSE +0 -0
  33. {trainstream-0.1.0 → trainstream-0.2.0}/NOTICE +0 -0
  34. {trainstream-0.1.0 → trainstream-0.2.0}/build.bat +0 -0
  35. {trainstream-0.1.0 → trainstream-0.2.0}/pyproject.toml +0 -0
  36. {trainstream-0.1.0 → trainstream-0.2.0}/setup.cfg +0 -0
  37. {trainstream-0.1.0 → trainstream-0.2.0/trainstream}/csrc/memory_pinner.cpp +0 -0
  38. {trainstream-0.1.0 → trainstream-0.2.0/trainstream}/csrc/trainstream.h +0 -0
  39. {trainstream-0.1.0 → trainstream-0.2.0}/trainstream/integrations/__init__.py +0 -0
  40. {trainstream-0.1.0 → trainstream-0.2.0}/trainstream/profiler.py +0 -0
  41. {trainstream-0.1.0 → trainstream-0.2.0}/trainstream.egg-info/dependency_links.txt +0 -0
  42. {trainstream-0.1.0 → trainstream-0.2.0}/trainstream.egg-info/not-zip-safe +0 -0
  43. {trainstream-0.1.0 → trainstream-0.2.0}/trainstream.egg-info/top_level.txt +0 -0
@@ -1,8 +1,6 @@
1
1
  include README.md LICENSE NOTICE pyproject.toml setup.py build.bat
2
- recursive-include csrc *.h *.cpp *.cu
2
+ recursive-include trainstream/csrc *.h *.cpp *.cu
3
3
  recursive-include trainstream *.py
4
- prune dashboard
5
- prune research
6
4
  prune runs
7
5
  prune build
8
6
  global-exclude *.py[cod] __pycache__ *.pyd *.so
@@ -0,0 +1,157 @@
1
+ Metadata-Version: 2.4
2
+ Name: trainstream
3
+ Version: 0.2.0
4
+ Summary: Zero-dependency PCIe offloading engine for PyTorch training (optimizer, gradients, activations).
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 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.
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 keeps only the weights in VRAM: gradients stream to pinned host memory as autograd produces them, AdamW runs on host threads while the backward pass continues, and a profiled share of activations is paged out and prefetched back block by block. Updates match `torch.optim.AdamW` (max parameter difference ≈3·10⁻⁷ after 20 steps); activation paging is bit-exact.
51
+
52
+ ## Results (RTX 5090 Laptop GPU, 24 GB)
53
+
54
+ | Workload | Stock PyTorch AdamW | TrainStream |
55
+ |---|---|---|
56
+ | 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) |
57
+ | 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 |
58
+ | 1.8B GPT, weights paged to RAM (0.2) | — | 2.4 GiB peak, 2.1 s/step; 1.84 GiB inside a 4 GB VRAM cap with bf16 moments |
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
+ | 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
+ | LoRA r=16 on the Qwen2.5-14B architecture (random weights; 29.5 GB) | does not fit | 5.6 GB peak, 2.48 s/step, 36 GB of host RAM |
62
+ | 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
+
64
+ 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).
65
+
66
+ ## Requirements
67
+
68
+ - An NVIDIA GPU and a CUDA build of PyTorch
69
+ - 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 78 tests pass on both, RTX 5090 Laptop GPU. Native Linux and other GPUs are untested.
71
+
72
+ ## Install
73
+
74
+ ```bash
75
+ pip install torch --index-url https://download.pytorch.org/whl/cu130 # if you don't have CUDA PyTorch yet
76
+ pip install trainstream
77
+ ```
78
+
79
+ The first `import trainstream` compiles the CUDA extension against your PyTorch (a few minutes, once) and caches it; every later import is instant. On Windows it finds Visual Studio and the CUDA toolkit itself, so any terminal works. The cache is keyed by PyTorch/CUDA/Python version and GPU, so upgrading PyTorch triggers one rebuild.
80
+
81
+ | Variable | Effect |
82
+ |---|---|
83
+ | `TRAINSTREAM_CACHE_DIR` | where builds are cached (default `%LOCALAPPDATA%\trainstream\build`, Linux `~/.cache/trainstream`) |
84
+ | `TRAINSTREAM_VERBOSE_BUILD=1` | show compiler output |
85
+ | `TORCH_CUDA_ARCH_LIST` | GPU architectures to compile for (default: the GPUs present) |
86
+ | `CUDA_HOME` | CUDA toolkit to use, if it isn't found automatically |
87
+
88
+ To compile ahead of time instead: `pip install trainstream --no-binary trainstream --no-build-isolation`. From a checkout: `build.bat` (Windows) or `python setup.py build_ext --inplace`.
89
+
90
+ ## Usage
91
+
92
+ ```python
93
+ import torch, trainstream
94
+
95
+ model = MyModel().cuda()
96
+ optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)
97
+ model, optimizer = trainstream.wrap(model, optimizer) # max_grad_norm=..., accumulation_steps=... if you use them
98
+
99
+ for x, y in loader:
100
+ with torch.autocast("cuda", dtype=torch.bfloat16):
101
+ loss = model(x.cuda(), y.cuda())
102
+ loss.backward()
103
+ optimizer.step()
104
+ optimizer.zero_grad()
105
+ ```
106
+
107
+ Models bigger than the GPU: build on the CPU and let TrainStream page the transformer-block weights:
108
+
109
+ ```python
110
+ model = MyGPT(cfg) # stays on the CPU
111
+ optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)
112
+ model, optimizer = trainstream.wrap(model, optimizer, device="cuda",
113
+ param_offload=True, moment_dtype="bfloat16")
114
+ ```
115
+
116
+ `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 a 14.8B-parameter base through `TrainStreamTrainer`: pass the model on the CPU and its layers stay in host RAM.
117
+
118
+ | `wrap()` option | What it does |
119
+ |---|---|
120
+ | `offload="auto"` | switches to stock fused AdamW after 2 steps if the model fits (`True` always offloads) |
121
+ | `param_offload="auto"` | pages block weights to host RAM when they'd take more than half the GPU; `True`, a fraction, or block indices |
122
+ | `activation_offload="auto"` | pages a profiled share of activations (`True`, a fraction, `False`) |
123
+ | `moment_dtype="bfloat16"` | bf16 optimizer moments with stochastic rounding: half the host memory for them |
124
+ | `distributed` | data parallel when a `torch.distributed` group with more than 1 rank exists; gradients averaged on the host over gloo |
125
+ | `max_grad_norm`, `accumulation_steps` | clipping and gradient accumulation, done inside the optimizer |
126
+
127
+ Also: `trainstream.chunked_cross_entropy(hidden, weight, targets)` computes the loss without materializing the logits. `.to()`, `.cuda()` and `.half()` work after `wrap()`, and reading a weight anywhere waits for its in-flight update.
128
+
129
+ Hugging Face (`pip install transformers accelerate`):
130
+
131
+ ```python
132
+ from trainstream.integrations.huggingface import TrainStreamTrainer
133
+ trainer = TrainStreamTrainer(model=model, args=TrainingArguments(..., bf16=True), train_dataset=ds)
134
+ trainer.train()
135
+ ```
136
+
137
+ `TrainStreamTrainer` computes the causal-LM loss in chunks, so the full logits tensor never exists. Pass extra `wrap()` options as `trainstream={...}`. Use `bf16=True` or fp32; fp16 with loss scaling is not supported.
138
+
139
+ ## Tools in this repository
140
+
141
+ - `examples/verify_ram_offload.py` — proves where training memory lives (VRAM vs system RAM) with a PASS/FAIL verdict
142
+ - `examples/hf_finetune.py` — fine-tune a Hugging Face model, optionally side by side with the stock Trainer
143
+ - `examples/train_bigger_than_vram.py` — weight paging on a model built on the CPU, under a VRAM cap
144
+ - `examples/lora_large_model.py` — LoRA on 7B/14B-size bases larger than the GPU, optionally against the stock Trainer
145
+ - `docs/index.html` — full documentation
146
+
147
+ ## Limitations
148
+
149
+ - Host RAM is the ceiling: 8–12 bytes per trained parameter and 2–4 per frozen one (LoRA base), plus activations; measured on 64 GB: a 3.6B full fine-tune and LoRA on a 14.8B base
150
+ - Weight paging covers the repeated block stack and needs `optimizer_mode="cpu"`; each paged weight crosses PCIe twice per step
151
+ - Data parallel only (no tensor/pipeline parallelism); tested with 2 processes on 1 GPU, not across several GPUs or machines
152
+ - `TrainStreamTrainer` is single-process; fp16 + GradScaler and AMSGrad aren't supported; `torch.compile` hasn't been tested
153
+ - Host AdamW speed is bounded by system RAM bandwidth
154
+
155
+ ## License
156
+
157
+ Apache License 2.0 — see [LICENSE](LICENSE) and [NOTICE](NOTICE). Copyright 2026 John Anish G.
@@ -0,0 +1,117 @@
1
+ # TrainStream
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.
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 keeps only the weights in VRAM: gradients stream to pinned host memory as autograd produces them, AdamW runs on host threads while the backward pass continues, and a profiled share of activations is paged out and prefetched back block by block. Updates match `torch.optim.AdamW` (max parameter difference ≈3·10⁻⁷ after 20 steps); activation paging is bit-exact.
11
+
12
+ ## Results (RTX 5090 Laptop GPU, 24 GB)
13
+
14
+ | Workload | Stock PyTorch AdamW | TrainStream |
15
+ |---|---|---|
16
+ | 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) |
17
+ | 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 |
18
+ | 1.8B GPT, weights paged to RAM (0.2) | — | 2.4 GiB peak, 2.1 s/step; 1.84 GiB inside a 4 GB VRAM cap with bf16 moments |
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
+ | 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
+ | LoRA r=16 on the Qwen2.5-14B architecture (random weights; 29.5 GB) | does not fit | 5.6 GB peak, 2.48 s/step, 36 GB of host RAM |
22
+ | 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
+
24
+ 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).
25
+
26
+ ## Requirements
27
+
28
+ - An NVIDIA GPU and a CUDA build of PyTorch
29
+ - 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 78 tests pass on both, RTX 5090 Laptop GPU. Native Linux and other GPUs are untested.
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install torch --index-url https://download.pytorch.org/whl/cu130 # if you don't have CUDA PyTorch yet
36
+ pip install trainstream
37
+ ```
38
+
39
+ The first `import trainstream` compiles the CUDA extension against your PyTorch (a few minutes, once) and caches it; every later import is instant. On Windows it finds Visual Studio and the CUDA toolkit itself, so any terminal works. The cache is keyed by PyTorch/CUDA/Python version and GPU, so upgrading PyTorch triggers one rebuild.
40
+
41
+ | Variable | Effect |
42
+ |---|---|
43
+ | `TRAINSTREAM_CACHE_DIR` | where builds are cached (default `%LOCALAPPDATA%\trainstream\build`, Linux `~/.cache/trainstream`) |
44
+ | `TRAINSTREAM_VERBOSE_BUILD=1` | show compiler output |
45
+ | `TORCH_CUDA_ARCH_LIST` | GPU architectures to compile for (default: the GPUs present) |
46
+ | `CUDA_HOME` | CUDA toolkit to use, if it isn't found automatically |
47
+
48
+ To compile ahead of time instead: `pip install trainstream --no-binary trainstream --no-build-isolation`. From a checkout: `build.bat` (Windows) or `python setup.py build_ext --inplace`.
49
+
50
+ ## Usage
51
+
52
+ ```python
53
+ import torch, trainstream
54
+
55
+ model = MyModel().cuda()
56
+ optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)
57
+ model, optimizer = trainstream.wrap(model, optimizer) # max_grad_norm=..., accumulation_steps=... if you use them
58
+
59
+ for x, y in loader:
60
+ with torch.autocast("cuda", dtype=torch.bfloat16):
61
+ loss = model(x.cuda(), y.cuda())
62
+ loss.backward()
63
+ optimizer.step()
64
+ optimizer.zero_grad()
65
+ ```
66
+
67
+ Models bigger than the GPU: build on the CPU and let TrainStream page the transformer-block weights:
68
+
69
+ ```python
70
+ model = MyGPT(cfg) # stays on the CPU
71
+ optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)
72
+ model, optimizer = trainstream.wrap(model, optimizer, device="cuda",
73
+ param_offload=True, moment_dtype="bfloat16")
74
+ ```
75
+
76
+ `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 a 14.8B-parameter base through `TrainStreamTrainer`: pass the model on the CPU and its layers stay in host RAM.
77
+
78
+ | `wrap()` option | What it does |
79
+ |---|---|
80
+ | `offload="auto"` | switches to stock fused AdamW after 2 steps if the model fits (`True` always offloads) |
81
+ | `param_offload="auto"` | pages block weights to host RAM when they'd take more than half the GPU; `True`, a fraction, or block indices |
82
+ | `activation_offload="auto"` | pages a profiled share of activations (`True`, a fraction, `False`) |
83
+ | `moment_dtype="bfloat16"` | bf16 optimizer moments with stochastic rounding: half the host memory for them |
84
+ | `distributed` | data parallel when a `torch.distributed` group with more than 1 rank exists; gradients averaged on the host over gloo |
85
+ | `max_grad_norm`, `accumulation_steps` | clipping and gradient accumulation, done inside the optimizer |
86
+
87
+ Also: `trainstream.chunked_cross_entropy(hidden, weight, targets)` computes the loss without materializing the logits. `.to()`, `.cuda()` and `.half()` work after `wrap()`, and reading a weight anywhere waits for its in-flight update.
88
+
89
+ Hugging Face (`pip install transformers accelerate`):
90
+
91
+ ```python
92
+ from trainstream.integrations.huggingface import TrainStreamTrainer
93
+ trainer = TrainStreamTrainer(model=model, args=TrainingArguments(..., bf16=True), train_dataset=ds)
94
+ trainer.train()
95
+ ```
96
+
97
+ `TrainStreamTrainer` computes the causal-LM loss in chunks, so the full logits tensor never exists. Pass extra `wrap()` options as `trainstream={...}`. Use `bf16=True` or fp32; fp16 with loss scaling is not supported.
98
+
99
+ ## Tools in this repository
100
+
101
+ - `examples/verify_ram_offload.py` — proves where training memory lives (VRAM vs system RAM) with a PASS/FAIL verdict
102
+ - `examples/hf_finetune.py` — fine-tune a Hugging Face model, optionally side by side with the stock Trainer
103
+ - `examples/train_bigger_than_vram.py` — weight paging on a model built on the CPU, under a VRAM cap
104
+ - `examples/lora_large_model.py` — LoRA on 7B/14B-size bases larger than the GPU, optionally against the stock Trainer
105
+ - `docs/index.html` — full documentation
106
+
107
+ ## Limitations
108
+
109
+ - Host RAM is the ceiling: 8–12 bytes per trained parameter and 2–4 per frozen one (LoRA base), plus activations; measured on 64 GB: a 3.6B full fine-tune and LoRA on a 14.8B base
110
+ - Weight paging covers the repeated block stack and needs `optimizer_mode="cpu"`; each paged weight crosses PCIe twice per step
111
+ - Data parallel only (no tensor/pipeline parallelism); tested with 2 processes on 1 GPU, not across several GPUs or machines
112
+ - `TrainStreamTrainer` is single-process; fp16 + GradScaler and AMSGrad aren't supported; `torch.compile` hasn't been tested
113
+ - Host AdamW speed is bounded by system RAM bandwidth
114
+
115
+ ## License
116
+
117
+ Apache License 2.0 — see [LICENSE](LICENSE) and [NOTICE](NOTICE). Copyright 2026 John Anish G.
@@ -1,12 +1,13 @@
1
1
  """Build script for TrainStream.
2
2
 
3
- pip install trainstream --no-build-isolation # needs PyTorch (CUDA) + CUDA toolkit + C++ compiler
4
- python setup.py build_ext --inplace # from a checkout (Windows: build.bat)
3
+ pip install trainstream # pure-Python wheel; _C compiles on first import
4
+ python setup.py build_ext --inplace # from a checkout (Windows: build.bat)
5
+ pip install . --no-build-isolation # compile _C ahead of time into the install
5
6
 
6
- TrainStream is a PyTorch C++/CUDA extension, so it is compiled against the
7
- PyTorch already installed on your machine. Install PyTorch first and pass
8
- --no-build-isolation so pip builds against it (an isolated build environment
9
- would not contain your CUDA-enabled PyTorch).
7
+ The C++/CUDA sources ship inside the package (trainstream/csrc). Without
8
+ PyTorch at build time (pip's default isolated build, CI) this produces a
9
+ pure-Python wheel and trainstream/_build.py compiles the extension against the
10
+ user's PyTorch on first import. With PyTorch available it compiles _C now.
10
11
 
11
12
  Target architectures come from TORCH_CUDA_ARCH_LIST when set; otherwise the
12
13
  compute capability of every visible GPU is used (e.g. "12.0" for Blackwell).
@@ -19,23 +20,12 @@ from setuptools import find_packages, setup
19
20
 
20
21
  HERE = os.path.dirname(os.path.abspath(__file__))
21
22
 
22
- # Building the compiled module needs torch; packaging the sources (sdist,
23
- # metadata) does not, so CI machines without a GPU can still publish them.
24
- COMPILE_COMMANDS = {"build_ext", "bdist_wheel", "build", "install", "develop", "editable_wheel", "bdist_egg"}
25
- compiling = any(arg in COMPILE_COMMANDS for arg in sys.argv[1:])
26
-
27
23
  try:
28
24
  import torch
29
25
  from torch.utils.cpp_extension import BuildExtension, CUDAExtension
30
- except ImportError as exc:
31
- if compiling:
32
- raise SystemExit(
33
- "TrainStream compiles against your installed PyTorch.\n"
34
- " 1. Install a CUDA build of PyTorch, e.g.\n"
35
- " pip install torch --index-url https://download.pytorch.org/whl/cu130\n"
36
- " 2. Install TrainStream without build isolation:\n"
37
- " pip install trainstream --no-build-isolation\n"
38
- f"(import error: {exc})")
26
+ except ImportError:
27
+ torch = None
28
+ if os.environ.get("TRAINSTREAM_PURE_WHEEL"): # force the compile-on-import wheel
39
29
  torch = None
40
30
 
41
31
 
@@ -61,12 +51,12 @@ if torch is not None:
61
51
  ext_modules = [CUDAExtension(
62
52
  name="trainstream._C",
63
53
  sources=[
64
- "csrc/bindings.cpp",
65
- "csrc/memory_pinner.cpp",
66
- "csrc/offload_adamw.cpp",
67
- "csrc/offload_adamw_kernel.cu",
54
+ "trainstream/csrc/bindings.cpp",
55
+ "trainstream/csrc/memory_pinner.cpp",
56
+ "trainstream/csrc/offload_adamw.cpp",
57
+ "trainstream/csrc/offload_adamw_kernel.cu",
68
58
  ],
69
- include_dirs=[os.path.join(HERE, "csrc")],
59
+ include_dirs=[os.path.join(HERE, "trainstream", "csrc")],
70
60
  extra_compile_args={"cxx": cxx_flags, "nvcc": nvcc_flags},
71
61
  )]
72
62
  cmdclass = {"build_ext": BuildExtension.with_options(use_ninja=True)}
@@ -76,7 +66,7 @@ with open(os.path.join(HERE, "README.md"), encoding="utf-8") as fh:
76
66
 
77
67
  setup(
78
68
  name="trainstream",
79
- version="0.1.0",
69
+ version="0.2.0",
80
70
  author="John Anish G",
81
71
  license="Apache-2.0",
82
72
  description="Zero-dependency PCIe offloading engine for PyTorch training (optimizer, gradients, activations).",
@@ -86,10 +76,13 @@ setup(
86
76
  project_urls={"Source": "https://github.com/johnanishg/TrainStream",
87
77
  "Issues": "https://github.com/johnanishg/TrainStream/issues"},
88
78
  packages=find_packages(include=["trainstream", "trainstream.*"]),
79
+ package_data={"trainstream": ["csrc/*.h", "csrc/*.cpp", "csrc/*.cu"]},
89
80
  ext_modules=ext_modules,
90
81
  cmdclass=cmdclass,
91
82
  python_requires=">=3.9",
92
- install_requires=[], # torch is a build+runtime prerequisite, not a declared dependency
83
+ # torch is not declared: users must pick the CUDA build that matches their driver.
84
+ # ninja drives the compile-on-first-import build.
85
+ install_requires=["ninja"],
93
86
  extras_require={"hf": ["transformers", "accelerate", "peft"]},
94
87
  classifiers=[
95
88
  "Development Status :: 3 - Alpha",
@@ -0,0 +1,82 @@
1
+ """Data-parallel training: N processes, gradients averaged on the host.
2
+
3
+ Two ranks share one GPU here (gloo needs no second card). Each rank trains on
4
+ its own half of every batch; the result must equal one process training on the
5
+ whole batch with torch.optim.AdamW.
6
+ """
7
+
8
+ import copy
9
+ import os
10
+ import pathlib
11
+ import tempfile
12
+
13
+ import pytest
14
+ import torch
15
+ import torch.distributed as dist
16
+ import torch.multiprocessing as mp
17
+ from torch import nn
18
+ from torch.nn import functional as F
19
+
20
+ pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required")
21
+
22
+ HP = dict(lr=2e-3, betas=(0.9, 0.95), weight_decay=0.05)
23
+ WORLD = 2
24
+ STEPS = 6
25
+
26
+
27
+ def make_model(seed):
28
+ torch.manual_seed(seed)
29
+ return nn.Sequential(nn.Linear(32, 128), nn.GELU(), nn.Linear(128, 128), nn.GELU(), nn.Linear(128, 8))
30
+
31
+
32
+ def full_batches(accum):
33
+ g = torch.Generator().manual_seed(7)
34
+ return [[(torch.randn(16, 32, generator=g), torch.randint(0, 8, (16,), generator=g)) for _ in range(accum)]
35
+ for _ in range(STEPS)]
36
+
37
+
38
+ def _worker(rank, store, out, clip, accum, param_offload):
39
+ import trainstream
40
+ dist.init_process_group("gloo", init_method=pathlib.Path(store).as_uri(), rank=rank, world_size=WORLD)
41
+ model = make_model(seed=100 + rank).cuda() # different init per rank: wrap() broadcasts rank 0's
42
+ model, opt = trainstream.wrap(model, torch.optim.AdamW(model.parameters(), **HP), offload=True,
43
+ activation_offload=False, param_offload=param_offload, max_grad_norm=clip,
44
+ accumulation_steps=accum, blocks=[model[0], model[2]] if param_offload else None)
45
+ assert opt._dp is not None # detected from the initialized process group
46
+ for micro in full_batches(accum):
47
+ for x, y in micro:
48
+ xs, ys = x.chunk(WORLD)[rank].cuda(), y.chunk(WORLD)[rank].cuda()
49
+ (F.cross_entropy(model(xs), ys) / accum).backward()
50
+ opt.step()
51
+ opt.zero_grad()
52
+ opt.synchronize()
53
+ sd = {k: v.detach().cpu() for k, v in model.state_dict().items()}
54
+ torch.save(sd, os.path.join(out, f"rank{rank}.pt"))
55
+ trainstream.unwrap(model)
56
+ dist.destroy_process_group()
57
+
58
+
59
+ def reference(clip, accum):
60
+ model = make_model(seed=100).cuda()
61
+ opt = torch.optim.AdamW(model.parameters(), foreach=False, **HP)
62
+ for micro in full_batches(accum):
63
+ for x, y in micro:
64
+ (F.cross_entropy(model(x.cuda()), y.cuda()) / accum).backward()
65
+ if clip:
66
+ torch.nn.utils.clip_grad_norm_(model.parameters(), clip)
67
+ opt.step()
68
+ opt.zero_grad()
69
+ return {k: v.detach().cpu() for k, v in model.state_dict().items()}
70
+
71
+
72
+ @pytest.mark.parametrize("clip,accum,param_offload", [(None, 1, False), (0.5, 1, False), (None, 2, False),
73
+ (None, 1, True)])
74
+ def test_data_parallel_matches_single_process(clip, accum, param_offload):
75
+ tmp = tempfile.mkdtemp()
76
+ store = os.path.join(tmp, "store")
77
+ mp.spawn(_worker, args=(store, tmp, clip, accum, param_offload), nprocs=WORLD, join=True)
78
+ ranks = [torch.load(os.path.join(tmp, f"rank{r}.pt")) for r in range(WORLD)]
79
+ ref = reference(clip, accum)
80
+ for k in ref:
81
+ assert torch.equal(ranks[0][k], ranks[1][k]), k # replicas stay identical
82
+ assert (ranks[0][k] - ref[k]).abs().max().item() < 1e-4, k