trainstream 0.1.1__py3-none-any.whl

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.
@@ -0,0 +1,62 @@
1
+ """TrainStream: PCIe offloading engine for PyTorch training.
2
+
3
+ import trainstream
4
+ model, optimizer = trainstream.wrap(model, optimizer)
5
+
6
+ Optimizer state, gradients and (a profiled fraction of) activations are moved
7
+ to pinned host RAM over dedicated CUDA copy streams, so models that would not
8
+ fit in VRAM with a stock optimizer can still be trained on a single GPU.
9
+ """
10
+
11
+ import sys
12
+
13
+ try:
14
+ import torch # noqa: F401 (loads libtorch / c10 before the extension)
15
+ except ImportError as _exc: # pragma: no cover - depends on the environment
16
+ raise ImportError(
17
+ "TrainStream needs PyTorch with CUDA. Install it first, e.g.\n"
18
+ " pip install torch --index-url https://download.pytorch.org/whl/cu130") from _exc
19
+
20
+ try:
21
+ from . import _C # prebuilt (build.bat / setup.py build_ext / --no-build-isolation install)
22
+ except ImportError:
23
+ from . import _build
24
+
25
+ _C = _build.load() # compiled on first import, then loaded from the cache
26
+ sys.modules[__name__ + "._C"] = _C
27
+
28
+ from .hooks import ActivationOffloader, GradientOffloader, ParamReadyHooks, find_blocks
29
+ from .optimizer import OffloadAdamW
30
+ from .profiler import HardwareProfile, profile_hardware
31
+ from .wrapper import TrainStreamSession, memory_report, unwrap, wrap
32
+
33
+ __version__ = "0.1.1"
34
+
35
+ __all__ = [
36
+ "wrap",
37
+ "unwrap",
38
+ "memory_report",
39
+ "OffloadAdamW",
40
+ "ActivationOffloader",
41
+ "GradientOffloader",
42
+ "ParamReadyHooks",
43
+ "TrainStreamSession",
44
+ "HardwareProfile",
45
+ "profile_hardware",
46
+ "find_blocks",
47
+ "pinned_stats",
48
+ "transfer_stats",
49
+ ]
50
+
51
+
52
+ def pinned_stats():
53
+ """Pin-logging allocator counters plus live bytes per allocation tag."""
54
+ stats = dict(_C.pinned_stats())
55
+ stats["by_tag"] = dict(_C.pinned_tag_bytes())
56
+ return stats
57
+
58
+
59
+ def transfer_stats(device=None):
60
+ """Cumulative bytes / copies issued on TrainStream's D2H and H2D streams."""
61
+ idx = torch.cuda.current_device() if device is None else (torch.device(device).index or 0)
62
+ return dict(_C.transfer_stats(idx))
trainstream/_build.py ADDED
@@ -0,0 +1,201 @@
1
+ """Compile-on-first-import for trainstream._C.
2
+
3
+ `pip install trainstream` installs a pure-Python wheel that carries the C++/CUDA
4
+ sources in trainstream/csrc. The first `import trainstream` compiles them
5
+ against the PyTorch that is installed (a few minutes) and caches the result;
6
+ later imports load the cached module directly, without a compiler.
7
+
8
+ The cache lives in %LOCALAPPDATA%\\trainstream (Windows) or
9
+ ~/.cache/trainstream (Linux); TRAINSTREAM_CACHE_DIR overrides it. Builds are
10
+ keyed by the sources, Python, PyTorch, CUDA and GPU architectures, so upgrading
11
+ any of them triggers a fresh build instead of loading a stale one.
12
+ """
13
+
14
+ import glob
15
+ import hashlib
16
+ import importlib.machinery
17
+ import importlib.util
18
+ import os
19
+ import re
20
+ import shutil
21
+ import subprocess
22
+ import sys
23
+
24
+ import torch
25
+
26
+ NAME = "trainstream_C"
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"]
29
+ HEADERS = ["kernels.h", "offload_adamw.h", "trainstream.h"]
30
+
31
+ if sys.platform == "win32":
32
+ CXX_FLAGS = ["/O2", "/EHsc", "/W3", "/wd4244", "/wd4267", "/wd4251", "/wd4275", "/wd4996"]
33
+ NVCC_FLAGS = ["-O3", "--use_fast_math", "-lineinfo", "--expt-relaxed-constexpr", "-Xcompiler", "/wd4819"]
34
+ LIB_EXT = ".pyd"
35
+ else:
36
+ CXX_FLAGS = ["-O3", "-Wall", "-Wno-unused-function", "-fvisibility=hidden"]
37
+ NVCC_FLAGS = ["-O3", "--use_fast_math", "-lineinfo", "--expt-relaxed-constexpr"]
38
+ LIB_EXT = ".so"
39
+
40
+
41
+ class BuildError(ImportError):
42
+ pass
43
+
44
+
45
+ def _cache_root():
46
+ if os.environ.get("TRAINSTREAM_CACHE_DIR"):
47
+ return os.environ["TRAINSTREAM_CACHE_DIR"]
48
+ if sys.platform == "win32":
49
+ base = os.environ.get("LOCALAPPDATA") or os.path.expanduser("~\\AppData\\Local")
50
+ return os.path.join(base, "trainstream", "build")
51
+ base = os.environ.get("XDG_CACHE_HOME") or os.path.expanduser("~/.cache")
52
+ return os.path.join(base, "trainstream")
53
+
54
+
55
+ def _arch_list():
56
+ if os.environ.get("TORCH_CUDA_ARCH_LIST"):
57
+ return os.environ["TORCH_CUDA_ARCH_LIST"]
58
+ caps = sorted({torch.cuda.get_device_capability(i) for i in range(torch.cuda.device_count())})
59
+ return ";".join(f"{major}.{minor}" for major, minor in caps)
60
+
61
+
62
+ def _build_key(arch):
63
+ h = hashlib.sha256()
64
+ for f in SOURCES + HEADERS:
65
+ with open(os.path.join(CSRC, f), "rb") as fh:
66
+ h.update(f.encode() + b"\0" + fh.read())
67
+ h.update(repr((sys.version_info[:2], torch.__version__, torch.version.cuda, arch,
68
+ CXX_FLAGS, NVCC_FLAGS)).encode())
69
+ tag = f"py{sys.version_info[0]}{sys.version_info[1]}-torch{torch.__version__}"
70
+ return re.sub(r"[^A-Za-z0-9_.-]", "_", tag) + "-" + h.hexdigest()[:12]
71
+
72
+
73
+ def _import_library(path):
74
+ loader = importlib.machinery.ExtensionFileLoader(NAME, path)
75
+ spec = importlib.util.spec_from_file_location(NAME, path, loader=loader)
76
+ module = importlib.util.module_from_spec(spec)
77
+ loader.exec_module(module)
78
+ return module
79
+
80
+
81
+ def _version_key(path):
82
+ return tuple(int(p) for p in re.findall(r"\d+", os.path.basename(path)))
83
+
84
+
85
+ def _setup_cuda_home():
86
+ if os.environ.get("CUDA_HOME") or os.environ.get("CUDA_PATH") or shutil.which("nvcc"):
87
+ return
88
+ if sys.platform == "win32":
89
+ pattern = os.path.join(os.environ.get("ProgramFiles", "C:\\Program Files"),
90
+ "NVIDIA GPU Computing Toolkit", "CUDA", "v*")
91
+ else:
92
+ pattern = "/usr/local/cuda*"
93
+ found = [d for d in glob.glob(pattern) if os.path.isfile(os.path.join(
94
+ d, "bin", "nvcc.exe" if sys.platform == "win32" else "nvcc"))]
95
+ if not found:
96
+ return
97
+ major = (torch.version.cuda or "").split(".")[0]
98
+ same_major = [d for d in found if _version_key(d)[:1] == (int(major),)] if major.isdigit() else []
99
+ os.environ["CUDA_HOME"] = max(same_major or found, key=_version_key)
100
+
101
+
102
+ def _setup_msvc():
103
+ """Import the x64 MSVC environment (vcvars64.bat) when cl.exe is not on PATH."""
104
+ if shutil.which("cl"):
105
+ return
106
+ vswhere = os.path.join(os.environ.get("ProgramFiles(x86)", "C:\\Program Files (x86)"),
107
+ "Microsoft Visual Studio", "Installer", "vswhere.exe")
108
+ if not os.path.isfile(vswhere):
109
+ return
110
+ try:
111
+ out = subprocess.check_output(
112
+ [vswhere, "-latest", "-products", "*", "-requires",
113
+ "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", "-property", "installationPath"],
114
+ text=True, stderr=subprocess.DEVNULL)
115
+ except (OSError, subprocess.CalledProcessError):
116
+ return
117
+ roots = [line.strip() for line in out.splitlines() if line.strip()]
118
+ if not roots:
119
+ return
120
+ vcvars = os.path.join(roots[0], "VC", "Auxiliary", "Build", "vcvars64.bat")
121
+ if not os.path.isfile(vcvars):
122
+ return
123
+ try:
124
+ env = subprocess.check_output(f'cmd /s /c ""{vcvars}" >nul && set"',
125
+ text=True, stderr=subprocess.DEVNULL)
126
+ except (OSError, subprocess.CalledProcessError):
127
+ return
128
+ for line in env.splitlines():
129
+ key, sep, value = line.partition("=")
130
+ if sep and key:
131
+ os.environ[key] = value
132
+ os.environ.setdefault("DISTUTILS_USE_SDK", "1")
133
+
134
+
135
+ def _setup_ninja():
136
+ if shutil.which("ninja"):
137
+ return
138
+ try:
139
+ import ninja
140
+ except ImportError:
141
+ return
142
+ os.environ["PATH"] = ninja.BIN_DIR + os.pathsep + os.environ.get("PATH", "")
143
+
144
+
145
+ def _hint():
146
+ if sys.platform == "win32":
147
+ return ("TrainStream needs the CUDA toolkit (same major version as PyTorch's CUDA, "
148
+ "https://developer.nvidia.com/cuda-downloads) and Visual Studio 2022+ Build Tools "
149
+ "with the 'Desktop development with C++' workload.")
150
+ return ("TrainStream needs the CUDA toolkit (same major version as PyTorch's CUDA) "
151
+ "and g++ 11+ (e.g. apt install build-essential).")
152
+
153
+
154
+ def load():
155
+ """Return the compiled extension, building it first if no cached build matches."""
156
+ if torch.version.cuda is None:
157
+ raise BuildError(
158
+ f"TrainStream needs a CUDA build of PyTorch, but torch {torch.__version__} is CPU-only. "
159
+ "Install one from https://pytorch.org/get-started/locally/ , e.g.\n"
160
+ " pip install torch --index-url https://download.pytorch.org/whl/cu130")
161
+ if not torch.cuda.is_available() and not os.environ.get("TORCH_CUDA_ARCH_LIST"):
162
+ raise BuildError("TrainStream needs an NVIDIA GPU, but torch.cuda.is_available() is False "
163
+ "(set TORCH_CUDA_ARCH_LIST to compile without one).")
164
+
165
+ arch = _arch_list()
166
+ build_dir = os.path.join(_cache_root(), _build_key(arch))
167
+ library = os.path.join(build_dir, NAME + LIB_EXT)
168
+ if os.path.isfile(library):
169
+ return _import_library(library)
170
+
171
+ os.makedirs(build_dir, exist_ok=True)
172
+ _setup_ninja()
173
+ _setup_cuda_home()
174
+ if sys.platform == "win32":
175
+ _setup_msvc()
176
+ os.environ["TORCH_CUDA_ARCH_LIST"] = arch
177
+
178
+ from torch.utils import cpp_extension
179
+ if os.environ.get("CUDA_HOME"):
180
+ cpp_extension.CUDA_HOME = os.environ["CUDA_HOME"]
181
+ if cpp_extension.CUDA_HOME is None:
182
+ raise BuildError("TrainStream could not find the CUDA toolkit (nvcc). " + _hint()
183
+ + " Set CUDA_HOME if it is installed somewhere unusual.")
184
+
185
+ print(f"TrainStream: compiling its CUDA extension for torch {torch.__version__} "
186
+ f"(first import only, takes a few minutes; cached in {build_dir})", file=sys.stderr, flush=True)
187
+ try:
188
+ module = cpp_extension.load(
189
+ name=NAME,
190
+ sources=[os.path.join(CSRC, f) for f in SOURCES],
191
+ extra_include_paths=[CSRC],
192
+ extra_cflags=CXX_FLAGS,
193
+ extra_cuda_cflags=NVCC_FLAGS,
194
+ build_directory=build_dir,
195
+ verbose=bool(os.environ.get("TRAINSTREAM_VERBOSE_BUILD")),
196
+ )
197
+ except Exception as exc:
198
+ raise BuildError(f"TrainStream failed to compile its CUDA extension. {_hint()}\n"
199
+ f"Set TRAINSTREAM_VERBOSE_BUILD=1 to see the full compiler output.\n{exc}") from exc
200
+ print("TrainStream: build finished.", file=sys.stderr, flush=True)
201
+ return module
@@ -0,0 +1,139 @@
1
+ // TrainStream — Python bindings (module: trainstream._C).
2
+ //
3
+ // Every call that can block on the GPU or on worker threads releases the GIL,
4
+ // so autograd's backward thread and the training thread never deadlock on it.
5
+
6
+ #include <torch/extension.h>
7
+
8
+ #include <cstdio>
9
+ #include <exception>
10
+
11
+ #include "offload_adamw.h"
12
+ #include "trainstream.h"
13
+
14
+ namespace py = pybind11;
15
+ using namespace trainstream;
16
+
17
+ namespace {
18
+ using nogil = py::call_guard<py::gil_scoped_release>;
19
+
20
+ // An exception escaping a destructor (e.g. a CUDA error surfacing inside the
21
+ // caching allocator's free path) calls std::terminate, and the default abort
22
+ // handler prints only a stack. Print the message first, then chain.
23
+ std::terminate_handler g_prev_terminate = nullptr;
24
+ void print_then_terminate() {
25
+ if (auto ex = std::current_exception()) {
26
+ try {
27
+ std::rethrow_exception(ex);
28
+ } catch (const std::exception& e) {
29
+ std::fprintf(stderr, "[trainstream] terminate: %s\n", e.what());
30
+ } catch (...) {
31
+ std::fprintf(stderr, "[trainstream] terminate: unknown exception\n");
32
+ }
33
+ std::fflush(stderr);
34
+ }
35
+ if (g_prev_terminate) g_prev_terminate();
36
+ std::abort();
37
+ }
38
+
39
+ std::map<std::string, std::string> build_info() {
40
+ int runtime_version = 0, driver_version = 0;
41
+ cudaRuntimeGetVersion(&runtime_version);
42
+ cudaDriverGetVersion(&driver_version);
43
+ return {{"cuda_runtime", std::to_string(runtime_version)},
44
+ {"cuda_driver", std::to_string(driver_version)},
45
+ {"compiled_with_cuda", std::to_string(CUDART_VERSION)},
46
+ #ifdef _MSVC_LANG
47
+ {"cxx_standard", std::to_string(_MSVC_LANG)}}; // MSVC pins __cplusplus to 199711L
48
+ #else
49
+ {"cxx_standard", std::to_string(__cplusplus)}};
50
+ #endif
51
+ }
52
+ } // namespace
53
+
54
+ PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
55
+ m.doc() = "TrainStream native runtime: pinned memory, DMA streams, offloaded AdamW";
56
+ g_prev_terminate = std::set_terminate(print_then_terminate);
57
+
58
+ m.def("build_info", &build_info);
59
+ m.def("now_ms", &now_ms);
60
+ m.def("init", [](int device) { runtime(device); }, py::arg("device") = -1);
61
+ m.def("is_dense", [](const at::Tensor& t) { return t.is_non_overlapping_and_dense(); },
62
+ "True if the tensor occupies exactly numel*itemsize contiguous bytes (any dimension order).");
63
+
64
+ // ---- events -------------------------------------------------------------
65
+ py::class_<Event, std::shared_ptr<Event>>(m, "Event")
66
+ .def(py::init([](int device, bool timing) { return make_event(resolve_device(device), timing); }),
67
+ py::arg("device") = -1, py::arg("timing") = false)
68
+ .def("record", [](Event& e) { e.record(compute_stream(e.device())); },
69
+ "Record on the current PyTorch compute stream.")
70
+ .def("query", &Event::query)
71
+ .def("synchronize", &Event::synchronize, nogil())
72
+ .def("wait", [](Event& e) { e.block(compute_stream(e.device())); },
73
+ "Make the current compute stream wait for this event (GPU-side, non-blocking for the host).")
74
+ .def("elapsed_ms", &Event::elapsed_ms, nogil())
75
+ .def_property_readonly("recorded", &Event::recorded)
76
+ .def_property_readonly("device", &Event::device);
77
+ m.def("record_event", &record_on_current, py::arg("device") = -1, py::arg("timing") = false);
78
+
79
+ // ---- pinned memory --------------------------------------------------------
80
+ m.def("pinned_empty", &pinned_empty, py::arg("sizes"), py::arg("dtype"), py::arg("tag") = "user",
81
+ py::arg("mapped") = false, py::arg("cached") = true);
82
+ m.def("pinned_stats", &pinned_stats);
83
+ m.def("pinned_tag_bytes", &pinned_tag_bytes);
84
+ m.def("pinned_log", &pinned_log, py::arg("limit") = 256);
85
+ m.def("pinned_empty_cache", &pinned_empty_cache, nogil());
86
+ m.def("pinned_set_cache_limit", &pinned_set_cache_limit);
87
+
88
+ // ---- transfers ------------------------------------------------------------
89
+ m.def("d2h_async", &d2h_async, py::arg("dst"), py::arg("src"));
90
+ m.def("h2d_async", &h2d_async, py::arg("dst"), py::arg("src"), py::arg("after") = EventPtr());
91
+ m.def("h2d_alloc_async", &h2d_alloc_async, py::arg("src"), py::arg("after") = EventPtr(),
92
+ py::arg("device") = -1, py::arg("sizes") = std::vector<int64_t>{},
93
+ py::arg("strides") = std::vector<int64_t>{});
94
+ m.def("transfer_stats", &transfer_stats, py::arg("device") = -1);
95
+ m.def("synchronize_streams", [](int device) {
96
+ Runtime& rt = runtime(device);
97
+ TS_CUDA_CHECK(cudaStreamSynchronize(rt.d2h));
98
+ TS_CUDA_CHECK(cudaStreamSynchronize(rt.h2d));
99
+ TS_CUDA_CHECK(cudaStreamSynchronize(rt.upd));
100
+ }, py::arg("device") = -1, nogil());
101
+
102
+ // ---- benchmarks -----------------------------------------------------------
103
+ m.def("benchmark_pcie", &benchmark_pcie, py::arg("device") = -1, py::arg("bytes") = 256LL << 20,
104
+ py::arg("iters") = 10, nogil());
105
+ m.def("benchmark_matmul", &benchmark_matmul, py::arg("device") = -1, py::arg("n") = 4096,
106
+ py::arg("dtype") = at::kBFloat16, py::arg("iters") = 20, nogil());
107
+ m.def("benchmark_cpu_adamw", &benchmark_cpu_adamw, py::arg("numel") = 1 << 24, py::arg("threads") = 0,
108
+ nogil());
109
+ m.def("benchmark_zero_copy_adamw", &benchmark_zero_copy_adamw, py::arg("device") = -1,
110
+ py::arg("numel") = 1 << 24, nogil());
111
+
112
+ // ---- optimizer engine -----------------------------------------------------
113
+ py::class_<OffloadAdamW>(m, "OffloadAdamWEngine")
114
+ .def(py::init<int, const std::string&, int, int64_t>(), py::arg("device"), py::arg("mode") = "cpu",
115
+ py::arg("threads") = 0, py::arg("chunk_elems") = 1 << 20)
116
+ .def("add_param", &OffloadAdamW::add_param, py::arg("param"), py::arg("pin_moments") = true)
117
+ .def("offload_grad", &OffloadAdamW::offload_grad, py::arg("id"), py::arg("grad"),
118
+ py::arg("apply_update") = false, py::arg("lr") = 0.0, py::arg("beta1") = 0.9, py::arg("beta2") = 0.999,
119
+ py::arg("eps") = 1e-8, py::arg("weight_decay") = 0.0, nogil())
120
+ .def("step", &OffloadAdamW::step, py::arg("ids"), py::arg("lrs"), py::arg("beta1s"), py::arg("beta2s"),
121
+ py::arg("epss"), py::arg("wds"), py::arg("grads"), py::arg("max_grad_norm") = 0.0,
122
+ py::arg("compute_norm") = false, nogil())
123
+ .def("wait_ready", &OffloadAdamW::wait_ready, nogil())
124
+ .def("synchronize", &OffloadAdamW::synchronize, nogil())
125
+ .def("last_grad_norm", &OffloadAdamW::last_grad_norm, nogil())
126
+ .def("state", &OffloadAdamW::state, nogil())
127
+ .def("load_state",
128
+ [](OffloadAdamW& e, int64_t id, const at::Tensor& m, const at::Tensor& v,
129
+ const std::optional<at::Tensor>& master, int64_t step) {
130
+ e.load_state(id, m, v, master.value_or(at::Tensor()), step);
131
+ },
132
+ py::arg("id"), py::arg("exp_avg"), py::arg("exp_avg_sq"), py::arg("master") = py::none(),
133
+ py::arg("step") = 0, nogil())
134
+ .def("get_step", &OffloadAdamW::get_step)
135
+ .def("stats", &OffloadAdamW::stats)
136
+ .def_property_readonly("mode", &OffloadAdamW::mode)
137
+ .def_property_readonly("num_params", &OffloadAdamW::num_params)
138
+ .def_property_readonly("threads", &OffloadAdamW::threads);
139
+ }
@@ -0,0 +1,49 @@
1
+ // TrainStream — device kernel launch interface.
2
+ //
3
+ // This header is deliberately free of PyTorch includes so that nvcc only has to
4
+ // parse CUDA runtime headers when compiling offload_adamw_kernel.cu. The C++
5
+ // translation units translate at::ScalarType into kernels::DType before calling.
6
+ #pragma once
7
+
8
+ #include <cstdint>
9
+ #include <cuda_runtime.h>
10
+
11
+ namespace trainstream {
12
+ namespace kernels {
13
+
14
+ enum class DType : int { kFloat32 = 0, kFloat16 = 1, kBFloat16 = 2 };
15
+
16
+ // Hyper-parameters for one AdamW step on one tensor. Bias corrections are folded
17
+ // on the host so the kernel does no pow() per element:
18
+ // step_size = lr / (1 - beta1^t)
19
+ // bias_correction2_sqrt = sqrt(1 - beta2^t)
20
+ struct AdamWParams {
21
+ float lr;
22
+ float beta1;
23
+ float beta2;
24
+ float eps;
25
+ float weight_decay;
26
+ float step_size;
27
+ float bias_correction2_sqrt;
28
+ float max_grad_norm; // only read when a grad_sq_norm pointer is supplied
29
+ };
30
+
31
+ // Fused AdamW where the optimizer state lives in *mapped pinned host memory*.
32
+ // `exp_avg`, `exp_avg_sq` and (optionally) `master` are device-visible aliases of
33
+ // cudaHostAlloc(cudaHostAllocMapped) buffers, so every state read/write is a PCIe
34
+ // transaction and the moments never occupy VRAM. `param` and `grad` live in VRAM.
35
+ // If `master` is non-null the fp32 master copy is updated and `param` receives a
36
+ // rounded copy (mixed-precision training with fp16/bf16 weights).
37
+ // If `grad_sq_norm` is non-null it points at a device float holding sum(g^2) over
38
+ // all gradients; the kernel derives the global-norm clip coefficient from it.
39
+ cudaError_t adamw_zero_copy(DType dtype, void* param, const void* grad, float* exp_avg,
40
+ float* exp_avg_sq, float* master, int64_t n,
41
+ const AdamWParams& hp, const float* grad_sq_norm, int sm_count,
42
+ cudaStream_t stream);
43
+
44
+ // out[0] += sum(grad[i]^2). Used for global gradient-norm clipping on the GPU path.
45
+ cudaError_t grad_sq_norm_accumulate(DType dtype, const void* grad, int64_t n, float* out,
46
+ int sm_count, cudaStream_t stream);
47
+
48
+ } // namespace kernels
49
+ } // namespace trainstream