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.
Files changed (44) hide show
  1. {trainstream-0.3.0/trainstream.egg-info → trainstream-0.4.0}/PKG-INFO +29 -8
  2. {trainstream-0.3.0 → trainstream-0.4.0}/README.md +27 -6
  3. {trainstream-0.3.0 → trainstream-0.4.0}/setup.py +4 -2
  4. {trainstream-0.3.0 → trainstream-0.4.0}/tests/test_huggingface.py +96 -0
  5. trainstream-0.4.0/tests/test_quant.py +279 -0
  6. {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/__init__.py +5 -2
  7. {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/_build.py +2 -1
  8. {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/csrc/bindings.cpp +2 -0
  9. {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/csrc/kernels.h +7 -0
  10. trainstream-0.4.0/trainstream/csrc/quant.cpp +71 -0
  11. trainstream-0.4.0/trainstream/csrc/quant_kernel.cu +124 -0
  12. {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/csrc/trainstream.h +8 -0
  13. {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/integrations/huggingface.py +118 -0
  14. {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/paging.py +103 -7
  15. {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/planner.py +5 -3
  16. trainstream-0.4.0/trainstream/quant.py +214 -0
  17. {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/wrapper.py +114 -26
  18. {trainstream-0.3.0 → trainstream-0.4.0/trainstream.egg-info}/PKG-INFO +29 -8
  19. {trainstream-0.3.0 → trainstream-0.4.0}/trainstream.egg-info/SOURCES.txt +4 -0
  20. {trainstream-0.3.0 → trainstream-0.4.0}/LICENSE +0 -0
  21. {trainstream-0.3.0 → trainstream-0.4.0}/MANIFEST.in +0 -0
  22. {trainstream-0.3.0 → trainstream-0.4.0}/NOTICE +0 -0
  23. {trainstream-0.3.0 → trainstream-0.4.0}/build.bat +0 -0
  24. {trainstream-0.3.0 → trainstream-0.4.0}/pyproject.toml +0 -0
  25. {trainstream-0.3.0 → trainstream-0.4.0}/setup.cfg +0 -0
  26. {trainstream-0.3.0 → trainstream-0.4.0}/tests/test_distributed.py +0 -0
  27. {trainstream-0.3.0 → trainstream-0.4.0}/tests/test_features.py +0 -0
  28. {trainstream-0.3.0 → trainstream-0.4.0}/tests/test_offload.py +0 -0
  29. {trainstream-0.3.0 → trainstream-0.4.0}/tests/test_optimizer.py +0 -0
  30. {trainstream-0.3.0 → trainstream-0.4.0}/tests/test_paging.py +0 -0
  31. {trainstream-0.3.0 → trainstream-0.4.0}/tests/test_planner.py +0 -0
  32. {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/csrc/memory_pinner.cpp +0 -0
  33. {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/csrc/offload_adamw.cpp +0 -0
  34. {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/csrc/offload_adamw.h +0 -0
  35. {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/csrc/offload_adamw_kernel.cu +0 -0
  36. {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/hooks.py +0 -0
  37. {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/integrations/__init__.py +0 -0
  38. {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/losses.py +0 -0
  39. {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/optimizer.py +0 -0
  40. {trainstream-0.3.0 → trainstream-0.4.0}/trainstream/profiler.py +0 -0
  41. {trainstream-0.3.0 → trainstream-0.4.0}/trainstream.egg-info/dependency_links.txt +0 -0
  42. {trainstream-0.3.0 → trainstream-0.4.0}/trainstream.egg-info/not-zip-safe +0 -0
  43. {trainstream-0.3.0 → trainstream-0.4.0}/trainstream.egg-info/requires.txt +0 -0
  44. {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.3.0
4
- Summary: Zero-dependency PCIe offloading engine for PyTorch training (optimizer, gradients, activations).
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 89 tests pass on both, RTX 5090 Laptop GPU. Native Linux and other GPUs are untested.
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
- `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.
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
- - `docs/index.html` — full documentation
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 (LoRA base), plus activations; measured on 64 GB: a 3.6B full fine-tune and LoRA on a 14.8B base
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 89 tests pass on both, RTX 5090 Laptop GPU. Native Linux and other GPUs are untested.
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
- `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.
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
- - `docs/index.html` — full documentation
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 (LoRA base), plus activations; measured on 64 GB: a 3.6B full fine-tune and LoRA on a 14.8B base
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.3.0",
71
+ version="0.4.0",
70
72
  author="John Anish G",
71
73
  license="Apache-2.0",
72
- description="Zero-dependency PCIe offloading engine for PyTorch training (optimizer, gradients, activations).",
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 are moved to host RAM over
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.3.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);