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,712 @@
1
+ // TrainStream — offloaded AdamW engine (host worker pool, gradient pipeline,
2
+ // CPU AdamW, zero-copy GPU dispatch) and optimizer micro-benchmarks.
3
+
4
+ #include "offload_adamw.h"
5
+
6
+ #include "kernels.h"
7
+
8
+ #include <c10/util/BFloat16.h>
9
+ #include <c10/util/Half.h>
10
+
11
+ #include <algorithm>
12
+ #include <chrono>
13
+ #include <cmath>
14
+
15
+ namespace trainstream {
16
+
17
+ namespace {
18
+
19
+ kernels::DType to_kernel_dtype(at::ScalarType t) {
20
+ switch (t) {
21
+ case at::kFloat:
22
+ return kernels::DType::kFloat32;
23
+ case at::kHalf:
24
+ return kernels::DType::kFloat16;
25
+ case at::kBFloat16:
26
+ return kernels::DType::kBFloat16;
27
+ default:
28
+ TORCH_CHECK(false, "TrainStream supports float32/float16/bfloat16 parameters, got ", t);
29
+ }
30
+ }
31
+
32
+ struct HostHp {
33
+ float lr, beta1, beta2, eps, weight_decay, step_size, bc2_sqrt, clip;
34
+ };
35
+
36
+ HostHp make_hp(double lr, double b1, double b2, double eps, double wd, int64_t step, double clip) {
37
+ const double bc1 = 1.0 - std::pow(b1, static_cast<double>(step));
38
+ const double bc2 = 1.0 - std::pow(b2, static_cast<double>(step));
39
+ return {static_cast<float>(lr), static_cast<float>(b1),
40
+ static_cast<float>(b2), static_cast<float>(eps),
41
+ static_cast<float>(wd), static_cast<float>(lr / bc1),
42
+ static_cast<float>(std::sqrt(bc2)), static_cast<float>(clip)};
43
+ }
44
+
45
+ // Host AdamW over [b, e). Same arithmetic order as torch.optim.AdamW
46
+ // (single-tensor path): decoupled decay, lerp for m, addcmul for v,
47
+ // denom = sqrt(v)/sqrt(bc2) + eps, p -= (lr/bc1) * m/denom.
48
+ // G is the gradient element type (fp32 accumulator or the raw DMA staging
49
+ // buffer); with kStage the rounded weights are also written for the upload.
50
+ // The loop is branch-free so the compiler can vectorise it.
51
+ template <typename G, typename P, bool kStage>
52
+ void adamw_host(float* __restrict p, float* __restrict m, float* __restrict v,
53
+ const G* __restrict g, P* __restrict stage, int64_t b, int64_t e,
54
+ const HostHp& hp) {
55
+ const float decay = 1.0f - hp.lr * hp.weight_decay;
56
+ const float omb1 = 1.0f - hp.beta1;
57
+ const float omb2 = 1.0f - hp.beta2;
58
+ for (int64_t i = b; i < e; ++i) {
59
+ const float gi = static_cast<float>(g[i]) * hp.clip;
60
+ const float mi = m[i] + omb1 * (gi - m[i]);
61
+ const float vi = hp.beta2 * v[i] + omb2 * gi * gi;
62
+ const float denom = std::sqrt(vi) / hp.bc2_sqrt + hp.eps;
63
+ const float pi = p[i] * decay - hp.step_size * (mi / denom);
64
+ m[i] = mi;
65
+ v[i] = vi;
66
+ p[i] = pi;
67
+ if constexpr (kStage) stage[i] = static_cast<P>(pi);
68
+ }
69
+ }
70
+
71
+ template <typename G>
72
+ void adamw_dispatch_param(float* p, float* m, float* v, const G* g, at::ScalarType pdtype,
73
+ const at::Tensor& param_stage, int64_t b, int64_t e, const HostHp& hp) {
74
+ switch (pdtype) {
75
+ case at::kFloat:
76
+ adamw_host<G, float, false>(p, m, v, g, nullptr, b, e, hp);
77
+ break;
78
+ case at::kHalf:
79
+ adamw_host<G, at::Half, true>(p, m, v, g, param_stage.data_ptr<at::Half>(), b, e, hp);
80
+ break;
81
+ case at::kBFloat16:
82
+ adamw_host<G, at::BFloat16, true>(p, m, v, g, param_stage.data_ptr<at::BFloat16>(), b, e, hp);
83
+ break;
84
+ default:
85
+ TORCH_CHECK(false, "unsupported parameter dtype");
86
+ }
87
+ }
88
+
89
+ void adamw_dispatch(float* p, float* m, float* v, const at::Tensor& grad, at::ScalarType pdtype,
90
+ const at::Tensor& param_stage, int64_t b, int64_t e, const HostHp& hp) {
91
+ switch (grad.scalar_type()) {
92
+ case at::kFloat:
93
+ adamw_dispatch_param(p, m, v, grad.data_ptr<float>(), pdtype, param_stage, b, e, hp);
94
+ break;
95
+ case at::kHalf:
96
+ adamw_dispatch_param(p, m, v, grad.data_ptr<at::Half>(), pdtype, param_stage, b, e, hp);
97
+ break;
98
+ case at::kBFloat16:
99
+ adamw_dispatch_param(p, m, v, grad.data_ptr<at::BFloat16>(), pdtype, param_stage, b, e, hp);
100
+ break;
101
+ default:
102
+ TORCH_CHECK(false, "unsupported gradient dtype");
103
+ }
104
+ }
105
+
106
+ template <typename T>
107
+ void accumulate_host(float* __restrict acc, const T* __restrict g, int64_t b, int64_t e, bool fresh) {
108
+ if (fresh) {
109
+ for (int64_t i = b; i < e; ++i) acc[i] = static_cast<float>(g[i]);
110
+ } else {
111
+ for (int64_t i = b; i < e; ++i) acc[i] += static_cast<float>(g[i]);
112
+ }
113
+ }
114
+
115
+ int64_t elapsed_us(std::chrono::steady_clock::time_point t0) {
116
+ return std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - t0)
117
+ .count();
118
+ }
119
+
120
+ } // namespace
121
+
122
+ // ===========================================================================
123
+ // WorkerPool
124
+ // ===========================================================================
125
+ WorkerPool::WorkerPool(int threads, int device) {
126
+ threads = std::max(1, threads);
127
+ for (int t = 0; t < threads; ++t) {
128
+ auto w = std::make_unique<Worker>();
129
+ Worker* wp = w.get();
130
+ w->thread = std::thread([wp, device] {
131
+ cudaSetDevice(device); // CUDA's current device is per host thread
132
+ for (;;) {
133
+ std::function<void()> job;
134
+ {
135
+ std::unique_lock<std::mutex> lock(wp->mu);
136
+ wp->cv.wait(lock, [wp] { return wp->stop || !wp->queue.empty(); });
137
+ if (wp->queue.empty()) return; // stop requested and drained
138
+ job = std::move(wp->queue.front());
139
+ wp->queue.pop_front();
140
+ }
141
+ job();
142
+ }
143
+ });
144
+ workers_.push_back(std::move(w));
145
+ }
146
+ }
147
+
148
+ WorkerPool::~WorkerPool() {
149
+ for (auto& w : workers_) {
150
+ {
151
+ std::lock_guard<std::mutex> lock(w->mu);
152
+ w->stop = true;
153
+ }
154
+ w->cv.notify_all();
155
+ }
156
+ for (auto& w : workers_)
157
+ if (w->thread.joinable()) w->thread.join();
158
+ }
159
+
160
+ void WorkerPool::submit(size_t worker, std::function<void()> fn) {
161
+ Worker& w = *workers_[worker % workers_.size()];
162
+ {
163
+ std::lock_guard<std::mutex> lock(w.mu);
164
+ w.queue.push_back(std::move(fn));
165
+ }
166
+ w.cv.notify_one();
167
+ }
168
+
169
+ // ===========================================================================
170
+ // OffloadAdamW
171
+ // ===========================================================================
172
+ OffloadAdamW::OffloadAdamW(int device, const std::string& mode, int threads, int64_t chunk_elems)
173
+ : device_(resolve_device(device)), chunk_(std::max<int64_t>(chunk_elems, 4096)) {
174
+ TORCH_CHECK(mode == "cpu" || mode == "gpu", "OffloadAdamW mode must be 'cpu' or 'gpu', got '", mode, "'");
175
+ gpu_mode_ = mode == "gpu";
176
+ runtime(device_); // create streams eagerly
177
+ if (threads <= 0) threads = std::max(1u, std::thread::hardware_concurrency() / 2);
178
+ if (!gpu_mode_) pool_ = std::make_unique<WorkerPool>(threads, device_);
179
+ if (gpu_mode_) {
180
+ c10::cuda::CUDAGuard guard(static_cast<c10::DeviceIndex>(device_));
181
+ TS_CUDA_CHECK(cudaMalloc(&norm_dev_, sizeof(float)));
182
+ norm_host_ = pinned_empty({1}, at::kFloat, "adamw.norm", false, false);
183
+ }
184
+ }
185
+
186
+ OffloadAdamW::~OffloadAdamW() {
187
+ try {
188
+ synchronize();
189
+ } catch (...) {
190
+ }
191
+ pool_.reset(); // joins workers before slots (and their buffers) die
192
+ if (norm_dev_ != nullptr) cudaFree(norm_dev_);
193
+ }
194
+
195
+ OffloadAdamW::Slot& OffloadAdamW::slot(int64_t id) {
196
+ TORCH_CHECK(id >= 0 && id < static_cast<int64_t>(slots_.size()), "OffloadAdamW: bad param id ", id);
197
+ return *slots_[static_cast<size_t>(id)];
198
+ }
199
+
200
+ size_t OffloadAdamW::worker_for(int64_t id, int64_t chunk) const {
201
+ // Deterministic (param, chunk) -> worker mapping gives per-chunk FIFO
202
+ // ordering (accumulate -> norm -> update) without any extra locking.
203
+ return static_cast<size_t>(static_cast<uint64_t>(id) * 2654435761ull + static_cast<uint64_t>(chunk));
204
+ }
205
+
206
+ int64_t OffloadAdamW::add_param(const at::Tensor& param, bool pin_moments) {
207
+ TORCH_CHECK(param.is_cuda(), "OffloadAdamW: parameters must live on a CUDA device");
208
+ TORCH_CHECK(param.get_device() == device_, "OffloadAdamW: parameter on cuda:", param.get_device(),
209
+ " but engine bound to cuda:", device_);
210
+ TORCH_CHECK(param.is_contiguous(), "OffloadAdamW: parameters must be contiguous");
211
+ to_kernel_dtype(param.scalar_type());
212
+
213
+ auto s = std::make_unique<Slot>();
214
+ s->id = static_cast<int64_t>(slots_.size());
215
+ s->param = param.detach().view({-1});
216
+ s->numel = s->param.numel();
217
+ s->pdtype = param.scalar_type();
218
+ s->nchunks = std::max<int64_t>(1, (s->numel + chunk_ - 1) / chunk_);
219
+ const std::vector<int64_t> shape{s->numel};
220
+ const bool mapped = gpu_mode_;
221
+
222
+ if (!gpu_mode_ || s->pdtype != at::kFloat) {
223
+ s->master = pinned_empty(shape, at::kFloat, "adamw.master_fp32", mapped, false);
224
+ s->master.copy_(s->param); // device -> host with dtype conversion
225
+ }
226
+ // Moments: the zero-copy kernel needs them mapped+pinned. In cpu mode only
227
+ // host threads touch them, so page-locking is optional (pin_moments) — it
228
+ // keeps the total page-locked footprint within what the OS will tolerate.
229
+ if (gpu_mode_ || pin_moments) {
230
+ s->exp_avg = pinned_empty(shape, at::kFloat, "adamw.exp_avg", mapped, false);
231
+ s->exp_avg_sq = pinned_empty(shape, at::kFloat, "adamw.exp_avg_sq", mapped, false);
232
+ s->exp_avg.zero_();
233
+ s->exp_avg_sq.zero_();
234
+ pinned_state_bytes_ += s->exp_avg.nbytes() * 2;
235
+ } else {
236
+ s->exp_avg = at::zeros(shape, at::TensorOptions().dtype(at::kFloat));
237
+ s->exp_avg_sq = at::zeros(shape, at::TensorOptions().dtype(at::kFloat));
238
+ pageable_state_bytes_ += s->exp_avg.nbytes() * 2;
239
+ }
240
+ int64_t bytes = s->exp_avg.nbytes() * 2 + (s->master.defined() ? s->master.nbytes() : 0);
241
+ if (s->master.defined()) pinned_state_bytes_ += s->master.nbytes();
242
+
243
+ if (gpu_mode_) {
244
+ s->d_m = static_cast<float*>(pinned_device_pointer(s->exp_avg));
245
+ s->d_v = static_cast<float*>(pinned_device_pointer(s->exp_avg_sq));
246
+ if (s->master.defined()) s->d_master = static_cast<float*>(pinned_device_pointer(s->master));
247
+ } else {
248
+ // grad_acc (fp32, host-only) is created lazily: the eager single-micro-batch
249
+ // path reads the DMA staging buffer directly and never needs it.
250
+ s->grad_stage = pinned_empty(shape, s->pdtype, "grad.staging", false, false);
251
+ bytes += s->grad_stage.nbytes();
252
+ pinned_state_bytes_ += s->grad_stage.nbytes();
253
+ if (s->pdtype != at::kFloat) {
254
+ s->param_stage = pinned_empty(shape, s->pdtype, "param.staging", false, false);
255
+ bytes += s->param_stage.nbytes();
256
+ pinned_state_bytes_ += s->param_stage.nbytes();
257
+ }
258
+ }
259
+ host_state_bytes_ += bytes;
260
+ slots_.push_back(std::move(s));
261
+ return slots_.back()->id;
262
+ }
263
+
264
+ void OffloadAdamW::record_error(const std::string& msg) {
265
+ std::lock_guard<std::mutex> lock(err_mu_);
266
+ if (error_.empty()) error_ = msg;
267
+ }
268
+
269
+ void OffloadAdamW::check_error() {
270
+ std::lock_guard<std::mutex> lock(err_mu_);
271
+ if (!error_.empty()) {
272
+ const std::string msg = error_;
273
+ error_.clear();
274
+ TORCH_CHECK(false, "TrainStream optimizer worker failed: ", msg);
275
+ }
276
+ }
277
+
278
+ void OffloadAdamW::run_batch(Slot& s, std::shared_ptr<ChunkFn> body,
279
+ std::shared_ptr<std::function<void()>> finalize) {
280
+ {
281
+ std::lock_guard<std::mutex> lock(s.mu);
282
+ ++s.inflight;
283
+ }
284
+ auto remaining = std::make_shared<std::atomic<int64_t>>(s.nchunks);
285
+ Slot* sp = &s;
286
+ for (int64_t c = 0; c < s.nchunks; ++c) {
287
+ const int64_t b = c * chunk_;
288
+ const int64_t e = std::min(s.numel, b + chunk_);
289
+ pool_->submit(worker_for(s.id, c), [this, sp, body, finalize, remaining, c, b, e] {
290
+ try {
291
+ (*body)(c, b, e);
292
+ } catch (const std::exception& ex) {
293
+ record_error(ex.what());
294
+ }
295
+ if (remaining->fetch_sub(1) == 1) {
296
+ if (finalize) {
297
+ try {
298
+ (*finalize)();
299
+ } catch (const std::exception& ex) {
300
+ record_error(ex.what());
301
+ }
302
+ }
303
+ {
304
+ std::lock_guard<std::mutex> lock(sp->mu);
305
+ --sp->inflight;
306
+ }
307
+ sp->cv.notify_all();
308
+ }
309
+ });
310
+ }
311
+ }
312
+
313
+ void OffloadAdamW::wait_idle(Slot& s) {
314
+ std::unique_lock<std::mutex> lock(s.mu);
315
+ s.cv.wait(lock, [&s] { return s.inflight == 0; });
316
+ }
317
+
318
+ void OffloadAdamW::offload_grad(int64_t id, const at::Tensor& grad, bool apply_update, double lr,
319
+ double beta1, double beta2, double eps, double weight_decay) {
320
+ TORCH_CHECK(!gpu_mode_, "offload_grad is only used in 'cpu' mode");
321
+ check_error();
322
+ Slot& s = slot(id);
323
+ TORCH_CHECK(grad.is_cuda() && grad.numel() == s.numel, "offload_grad: gradient shape mismatch for param ", id);
324
+
325
+ // The staging buffer may still be read by the previous accumulate batch.
326
+ const auto t0 = std::chrono::steady_clock::now();
327
+ wait_idle(s);
328
+ grad_stall_us_ += elapsed_us(t0);
329
+
330
+ at::Tensor g = grad.contiguous().view({-1});
331
+ if (g.scalar_type() != s.grad_stage.scalar_type())
332
+ s.grad_stage = pinned_empty({s.numel}, g.scalar_type(), "grad.staging", false, false);
333
+ s.d2h_done = d2h_async(s.grad_stage, g);
334
+ s.grad_keep = g; // released by the worker once the DMA has landed
335
+ grad_bytes_ += static_cast<int64_t>(g.nbytes());
336
+
337
+ const bool fresh = s.acc_fresh;
338
+ EventPtr landed = s.d2h_done;
339
+ if (apply_update && fresh) {
340
+ // Single micro-batch: AdamW reads the DMA staging buffer directly, skipping
341
+ // the fp32 accumulator round trip (~8 fewer bytes of host traffic/element).
342
+ steps_eager_ += 1;
343
+ s.has_grad = false; // cleared synchronously: step() must not update twice
344
+ s.acc_fresh = false;
345
+ // Everything that reads this weight in backward is already queued on the
346
+ // compute stream (the post-accumulate hook fires after its last use).
347
+ enqueue_update(s, lr, beta1, beta2, eps, weight_decay, 1.0, record_on_current(device_), landed);
348
+ return;
349
+ }
350
+ s.acc_fresh = false;
351
+ s.has_grad = true;
352
+ if (!s.grad_acc.defined()) {
353
+ s.grad_acc = at::empty({s.numel}, at::TensorOptions().dtype(at::kFloat)); // pageable, host-only
354
+ pageable_state_bytes_ += s.grad_acc.nbytes();
355
+ host_state_bytes_ += s.grad_acc.nbytes();
356
+ }
357
+ Slot* sp = &s;
358
+ auto body = std::make_shared<ChunkFn>([this, sp, landed, fresh](int64_t, int64_t b, int64_t e) {
359
+ landed->synchronize(); // DMA complete -> staging bytes are valid
360
+ const auto t = std::chrono::steady_clock::now();
361
+ float* acc = sp->grad_acc.data_ptr<float>();
362
+ switch (sp->grad_stage.scalar_type()) {
363
+ case at::kFloat:
364
+ accumulate_host(acc, sp->grad_stage.data_ptr<float>(), b, e, fresh);
365
+ break;
366
+ case at::kHalf:
367
+ accumulate_host(acc, sp->grad_stage.data_ptr<at::Half>(), b, e, fresh);
368
+ break;
369
+ case at::kBFloat16:
370
+ accumulate_host(acc, sp->grad_stage.data_ptr<at::BFloat16>(), b, e, fresh);
371
+ break;
372
+ default:
373
+ TORCH_CHECK(false, "unsupported gradient dtype");
374
+ }
375
+ accumulate_us_ += elapsed_us(t);
376
+ });
377
+ // Every chunk synchronised on `landed`, so the VRAM gradient is free to go.
378
+ auto release = std::make_shared<std::function<void()>>([sp] { sp->grad_keep.reset(); });
379
+ run_batch(s, body, release);
380
+ if (apply_update) {
381
+ steps_eager_ += 1;
382
+ s.has_grad = false; // cleared synchronously: step() must not update twice
383
+ enqueue_update(s, lr, beta1, beta2, eps, weight_decay, 1.0, record_on_current(device_), nullptr);
384
+ }
385
+ }
386
+
387
+ void OffloadAdamW::enqueue_update(Slot& s, double lr, double beta1, double beta2, double eps, double wd,
388
+ double clip, EventPtr after, EventPtr from_stage) {
389
+ Slot* sp = &s;
390
+ s.step += 1;
391
+ const HostHp hp = make_hp(lr, beta1, beta2, eps, wd, s.step, clip);
392
+ const EventPtr prev_ready = s.ready;
393
+
394
+ auto body = std::make_shared<ChunkFn>([this, sp, hp, prev_ready, from_stage](int64_t, int64_t b, int64_t e) {
395
+ // The previous H2D may still be reading master/param_stage.
396
+ if (prev_ready) prev_ready->synchronize();
397
+ if (from_stage) from_stage->synchronize(); // gradient DMA landed in staging
398
+ const auto t = std::chrono::steady_clock::now();
399
+ adamw_dispatch(sp->master.data_ptr<float>(), sp->exp_avg.data_ptr<float>(), sp->exp_avg_sq.data_ptr<float>(),
400
+ from_stage ? sp->grad_stage : sp->grad_acc, sp->pdtype, sp->param_stage, b, e, hp);
401
+ update_us_ += elapsed_us(t);
402
+ });
403
+
404
+ auto finalize = std::make_shared<std::function<void()>>([this, sp, after] {
405
+ sp->grad_keep.reset(); // fused path: staging consumed, DMA long complete
406
+ const at::Tensor& src = sp->param_stage.defined() ? sp->param_stage : sp->master;
407
+ EventPtr done = h2d_async(sp->param, src, after);
408
+ param_bytes_ += static_cast<int64_t>(src.nbytes());
409
+ {
410
+ std::lock_guard<std::mutex> lock(sp->mu);
411
+ sp->ready = std::move(done);
412
+ sp->has_grad = false;
413
+ sp->acc_fresh = true;
414
+ }
415
+ last_finalize_ms_ = now_ms();
416
+ });
417
+ run_batch(s, body, finalize);
418
+ }
419
+
420
+ double OffloadAdamW::step(const std::vector<int64_t>& ids, const std::vector<double>& lrs,
421
+ const std::vector<double>& beta1s, const std::vector<double>& beta2s,
422
+ const std::vector<double>& epss, const std::vector<double>& wds,
423
+ const std::vector<at::Tensor>& grads, double max_grad_norm,
424
+ bool compute_norm) {
425
+ const size_t n = ids.size();
426
+ TORCH_CHECK(lrs.size() == n && beta1s.size() == n && beta2s.size() == n && epss.size() == n &&
427
+ wds.size() == n,
428
+ "OffloadAdamW.step: hyper-parameter vectors must match ids");
429
+ check_error();
430
+ steps_ += 1;
431
+ if (gpu_mode_) {
432
+ TORCH_CHECK(grads.size() == n, "OffloadAdamW.step(gpu): one gradient (or None) per id required");
433
+ step_gpu(ids, lrs, beta1s, beta2s, epss, wds, grads, max_grad_norm, compute_norm);
434
+ return -1.0;
435
+ }
436
+ return step_cpu(ids, lrs, beta1s, beta2s, epss, wds, max_grad_norm, compute_norm);
437
+ }
438
+
439
+ double OffloadAdamW::step_cpu(const std::vector<int64_t>& ids, const std::vector<double>& lrs,
440
+ const std::vector<double>& beta1s, const std::vector<double>& beta2s,
441
+ const std::vector<double>& epss, const std::vector<double>& wds,
442
+ double max_grad_norm, bool compute_norm) {
443
+ // Everything enqueued on the compute stream so far (the whole backward) must
444
+ // finish before new weights may overwrite the parameters on the GPU.
445
+ EventPtr step_ev = record_on_current(device_);
446
+ step_start_ms_ = now_ms();
447
+
448
+ std::vector<size_t> active;
449
+ for (size_t k = 0; k < ids.size(); ++k)
450
+ if (slot(ids[k]).has_grad) active.push_back(k);
451
+
452
+ double clip = 1.0;
453
+ double norm = -1.0;
454
+ if (max_grad_norm > 0.0 || compute_norm) {
455
+ // Global norm needs every gradient: run a reduction batch per slot (FIFO
456
+ // after its accumulations), then barrier.
457
+ std::vector<int64_t> offsets;
458
+ int64_t total = 0;
459
+ for (size_t k : active) {
460
+ offsets.push_back(total);
461
+ total += slot(ids[k]).nchunks;
462
+ }
463
+ auto partials = std::make_shared<std::vector<double>>(static_cast<size_t>(total), 0.0);
464
+ for (size_t a = 0; a < active.size(); ++a) {
465
+ Slot& s = slot(ids[active[a]]);
466
+ Slot* sp = &s;
467
+ const int64_t off = offsets[a];
468
+ auto body = std::make_shared<ChunkFn>([sp, partials, off](int64_t c, int64_t b, int64_t e) {
469
+ const float* g = sp->grad_acc.data_ptr<float>();
470
+ double acc = 0.0;
471
+ for (int64_t i = b; i < e; ++i) acc += static_cast<double>(g[i]) * g[i];
472
+ (*partials)[static_cast<size_t>(off + c)] = acc;
473
+ });
474
+ run_batch(s, body, nullptr);
475
+ }
476
+ for (size_t k : active) wait_idle(slot(ids[k]));
477
+ check_error();
478
+ double sq = 0.0;
479
+ for (double p : *partials) sq += p;
480
+ norm = std::sqrt(sq);
481
+ last_norm_ = norm;
482
+ if (max_grad_norm > 0.0) clip = std::min(1.0, max_grad_norm / (norm + 1e-6));
483
+ }
484
+
485
+ for (size_t k : active)
486
+ enqueue_update(slot(ids[k]), lrs[k], beta1s[k], beta2s[k], epss[k], wds[k], clip, step_ev, nullptr);
487
+ return norm;
488
+ }
489
+
490
+ void OffloadAdamW::step_gpu(const std::vector<int64_t>& ids, const std::vector<double>& lrs,
491
+ const std::vector<double>& beta1s, const std::vector<double>& beta2s,
492
+ const std::vector<double>& epss, const std::vector<double>& wds,
493
+ const std::vector<at::Tensor>& grads, double max_grad_norm,
494
+ bool compute_norm) {
495
+ Runtime& rt = runtime(device_);
496
+ c10::cuda::CUDAGuard guard(static_cast<c10::DeviceIndex>(device_));
497
+
498
+ // Normalise gradients first (any copy kernels land on the compute stream
499
+ // before step_ev is recorded, so the update stream sees finished data).
500
+ std::vector<at::Tensor> g(ids.size());
501
+ for (size_t k = 0; k < ids.size(); ++k) {
502
+ if (!grads[k].defined()) continue;
503
+ Slot& s = slot(ids[k]);
504
+ TORCH_CHECK(grads[k].numel() == s.numel, "step(gpu): gradient shape mismatch for param ", ids[k]);
505
+ g[k] = grads[k].to(s.pdtype).contiguous().view({-1});
506
+ }
507
+ const auto t0 = std::chrono::steady_clock::now();
508
+ EventPtr step_ev = record_on_current(device_);
509
+ step_ev->block(rt.upd);
510
+
511
+ const bool use_norm = max_grad_norm > 0.0 || compute_norm;
512
+ if (use_norm) {
513
+ TS_CUDA_CHECK(cudaMemsetAsync(norm_dev_, 0, sizeof(float), rt.upd));
514
+ for (size_t k = 0; k < ids.size(); ++k) {
515
+ if (!g[k].defined()) continue;
516
+ TS_CUDA_CHECK(kernels::grad_sq_norm_accumulate(to_kernel_dtype(g[k].scalar_type()), g[k].data_ptr(),
517
+ g[k].numel(), norm_dev_, rt.sm_count, rt.upd));
518
+ }
519
+ }
520
+ for (size_t k = 0; k < ids.size(); ++k) {
521
+ if (!g[k].defined()) continue;
522
+ Slot& s = slot(ids[k]);
523
+ s.step += 1;
524
+ const HostHp h = make_hp(lrs[k], beta1s[k], beta2s[k], epss[k], wds[k], s.step, 1.0);
525
+ kernels::AdamWParams hp{h.lr, h.beta1, h.beta2, h.eps, h.weight_decay, h.step_size, h.bc2_sqrt,
526
+ static_cast<float>(max_grad_norm)};
527
+ TS_CUDA_CHECK(kernels::adamw_zero_copy(to_kernel_dtype(s.pdtype), s.param.data_ptr(), g[k].data_ptr(),
528
+ s.d_m, s.d_v, s.d_master, s.numel, hp,
529
+ max_grad_norm > 0.0 ? norm_dev_ : nullptr, rt.sm_count, rt.upd));
530
+ // Keep the gradient alive instead of recordStream (whose free-path event
531
+ // creation can abort on a full device). It is replaced next step, after
532
+ // the next forward has waited on this kernel's `ready` event.
533
+ {
534
+ std::lock_guard<std::mutex> lock(s.mu);
535
+ s.grad_keep = g[k];
536
+ }
537
+ auto ev = make_event(device_);
538
+ ev->record(rt.upd);
539
+ std::lock_guard<std::mutex> lock(s.mu);
540
+ s.ready = std::move(ev);
541
+ }
542
+ if (use_norm) {
543
+ TS_CUDA_CHECK(cudaMemcpyAsync(norm_host_.data_ptr(), norm_dev_, sizeof(float), cudaMemcpyDeviceToHost, rt.upd));
544
+ norm_ev_ = make_event(device_);
545
+ norm_ev_->record(rt.upd);
546
+ }
547
+ update_us_ += elapsed_us(t0); // host launch overhead only; device time is async
548
+ }
549
+
550
+ void OffloadAdamW::wait_ready(const std::vector<int64_t>& ids) {
551
+ check_error();
552
+ const cudaStream_t s = compute_stream(device_);
553
+ const auto t0 = std::chrono::steady_clock::now();
554
+ for (int64_t id : ids) {
555
+ Slot& sl = slot(id);
556
+ EventPtr ready;
557
+ {
558
+ std::unique_lock<std::mutex> lock(sl.mu);
559
+ sl.cv.wait(lock, [&sl] { return sl.inflight == 0; });
560
+ ready = sl.ready;
561
+ }
562
+ if (ready) ready->block(s);
563
+ if (gpu_mode_) {
564
+ // The compute stream now waits on the update kernel, so any reuse of the
565
+ // gradient's block (freed into the compute pool) is ordered after it.
566
+ std::lock_guard<std::mutex> lock(sl.mu);
567
+ sl.grad_keep.reset();
568
+ }
569
+ }
570
+ ready_stall_us_ += elapsed_us(t0);
571
+ }
572
+
573
+ void OffloadAdamW::synchronize() {
574
+ for (auto& sp : slots_) {
575
+ wait_idle(*sp);
576
+ EventPtr ready;
577
+ {
578
+ std::lock_guard<std::mutex> lock(sp->mu);
579
+ ready = sp->ready;
580
+ }
581
+ if (ready) ready->synchronize();
582
+ }
583
+ if (gpu_mode_) {
584
+ TS_CUDA_CHECK(cudaStreamSynchronize(runtime(device_).upd));
585
+ for (auto& sp : slots_) {
586
+ std::lock_guard<std::mutex> lock(sp->mu);
587
+ sp->grad_keep.reset(); // kernels are done: gradients can go
588
+ }
589
+ }
590
+ check_error();
591
+ }
592
+
593
+ double OffloadAdamW::last_grad_norm() {
594
+ if (gpu_mode_ && norm_ev_) {
595
+ norm_ev_->synchronize();
596
+ last_norm_ = std::sqrt(static_cast<double>(norm_host_.data_ptr<float>()[0]));
597
+ norm_ev_.reset();
598
+ }
599
+ return last_norm_;
600
+ }
601
+
602
+ std::tuple<at::Tensor, at::Tensor, at::Tensor> OffloadAdamW::state(int64_t id) {
603
+ Slot& s = slot(id);
604
+ wait_idle(s);
605
+ return {s.master, s.exp_avg, s.exp_avg_sq};
606
+ }
607
+
608
+ void OffloadAdamW::load_state(int64_t id, const at::Tensor& exp_avg, const at::Tensor& exp_avg_sq,
609
+ const at::Tensor& master, int64_t step) {
610
+ Slot& s = slot(id);
611
+ wait_idle(s);
612
+ if (s.ready) s.ready->synchronize();
613
+ TORCH_CHECK(exp_avg.numel() == s.numel && exp_avg_sq.numel() == s.numel,
614
+ "load_state: moment shape mismatch for param ", id);
615
+ s.exp_avg.copy_(exp_avg.reshape({-1}));
616
+ s.exp_avg_sq.copy_(exp_avg_sq.reshape({-1}));
617
+ if (master.defined() && s.master.defined()) {
618
+ TORCH_CHECK(master.numel() == s.numel, "load_state: master shape mismatch for param ", id);
619
+ s.master.copy_(master.reshape({-1}));
620
+ } else if (s.master.defined()) {
621
+ s.master.copy_(s.param); // re-sync master with the live weights
622
+ }
623
+ s.step = step;
624
+ }
625
+
626
+ int64_t OffloadAdamW::get_step(int64_t id) { return slot(id).step; }
627
+
628
+ std::map<std::string, double> OffloadAdamW::stats() {
629
+ int64_t numel = 0;
630
+ for (auto& s : slots_) numel += s->numel;
631
+ const double last_wall =
632
+ last_finalize_ms_.load() >= step_start_ms_.load() ? last_finalize_ms_.load() - step_start_ms_.load() : 0.0;
633
+ return {{"params", static_cast<double>(slots_.size())},
634
+ {"numel", static_cast<double>(numel)},
635
+ {"threads", static_cast<double>(threads())},
636
+ {"gpu_mode", gpu_mode_ ? 1.0 : 0.0},
637
+ {"steps", static_cast<double>(steps_.load())},
638
+ {"eager_updates", static_cast<double>(steps_eager_.load())},
639
+ {"host_state_bytes", static_cast<double>(host_state_bytes_.load())},
640
+ {"pinned_state_bytes", static_cast<double>(pinned_state_bytes_.load())},
641
+ {"pageable_state_bytes", static_cast<double>(pageable_state_bytes_.load())},
642
+ {"grad_bytes_offloaded", static_cast<double>(grad_bytes_.load())},
643
+ {"param_bytes_uploaded", static_cast<double>(param_bytes_.load())},
644
+ {"update_ms_total", update_us_.load() / 1e3},
645
+ {"accumulate_ms_total", accumulate_us_.load() / 1e3},
646
+ {"ready_stall_ms_total", ready_stall_us_.load() / 1e3},
647
+ {"grad_stall_ms_total", grad_stall_us_.load() / 1e3},
648
+ {"last_step_wall_ms", gpu_mode_ ? 0.0 : last_wall}};
649
+ }
650
+
651
+ // ===========================================================================
652
+ // Benchmarks
653
+ // ===========================================================================
654
+ std::map<std::string, double> benchmark_cpu_adamw(int64_t numel, int threads) {
655
+ if (threads <= 0) threads = std::max(1u, std::thread::hardware_concurrency() / 2);
656
+ numel = std::max<int64_t>(numel, 1 << 16);
657
+ std::vector<float> p(numel, 0.5f), m(numel, 0.f), v(numel, 0.f), g(numel, 1e-3f);
658
+ const HostHp hp = make_hp(1e-3, 0.9, 0.999, 1e-8, 0.01, 1, 1.0);
659
+ double best_ms = 1e30;
660
+ for (int rep = 0; rep < 3; ++rep) {
661
+ const auto t0 = std::chrono::steady_clock::now();
662
+ std::vector<std::thread> ts;
663
+ const int64_t per = (numel + threads - 1) / threads;
664
+ for (int t = 0; t < threads; ++t) {
665
+ const int64_t b = t * per, e = std::min(numel, b + per);
666
+ if (b >= e) break;
667
+ ts.emplace_back([&, b, e] { adamw_host<float, float, false>(p.data(), m.data(), v.data(), g.data(), nullptr, b, e, hp); });
668
+ }
669
+ for (auto& t : ts) t.join();
670
+ best_ms = std::min(best_ms, elapsed_us(t0) / 1e3);
671
+ }
672
+ return {{"ms", best_ms},
673
+ {"gelems_per_s", static_cast<double>(numel) / (best_ms * 1e-3) / 1e9},
674
+ {"threads", static_cast<double>(threads)},
675
+ {"numel", static_cast<double>(numel)}};
676
+ }
677
+
678
+ std::map<std::string, double> benchmark_zero_copy_adamw(int device, int64_t numel) {
679
+ const int dev = resolve_device(device);
680
+ Runtime& rt = runtime(dev);
681
+ c10::cuda::CUDAGuard guard(static_cast<c10::DeviceIndex>(dev));
682
+ numel = std::max<int64_t>(numel, 1 << 16);
683
+ auto opts = at::TensorOptions().dtype(at::kFloat).device(at::Device(at::kCUDA, static_cast<c10::DeviceIndex>(dev)));
684
+ at::Tensor p = at::full({numel}, 0.5, opts);
685
+ at::Tensor g = at::full({numel}, 1e-3, opts);
686
+ at::Tensor m = pinned_empty({numel}, at::kFloat, "bench.exp_avg", true, false);
687
+ at::Tensor v = pinned_empty({numel}, at::kFloat, "bench.exp_avg_sq", true, false);
688
+ m.zero_();
689
+ v.zero_();
690
+ float* dm = static_cast<float*>(pinned_device_pointer(m));
691
+ float* dv = static_cast<float*>(pinned_device_pointer(v));
692
+ const HostHp h = make_hp(1e-3, 0.9, 0.999, 1e-8, 0.01, 1, 1.0);
693
+ kernels::AdamWParams hp{h.lr, h.beta1, h.beta2, h.eps, h.weight_decay, h.step_size, h.bc2_sqrt, 0.f};
694
+ const cudaStream_t s = compute_stream(dev);
695
+ TS_CUDA_CHECK(kernels::adamw_zero_copy(kernels::DType::kFloat32, p.data_ptr(), g.data_ptr(), dm, dv, nullptr,
696
+ numel, hp, nullptr, rt.sm_count, s));
697
+ Event start(dev, true), stop(dev, true);
698
+ const int iters = 3;
699
+ start.record(s);
700
+ for (int i = 0; i < iters; ++i)
701
+ TS_CUDA_CHECK(kernels::adamw_zero_copy(kernels::DType::kFloat32, p.data_ptr(), g.data_ptr(), dm, dv,
702
+ nullptr, numel, hp, nullptr, rt.sm_count, s));
703
+ stop.record(s);
704
+ const double ms = start.elapsed_ms(stop) / iters;
705
+ // 8 bytes read + 8 bytes written over PCIe per element (m and v).
706
+ return {{"ms", ms},
707
+ {"gelems_per_s", static_cast<double>(numel) / (ms * 1e-3) / 1e9},
708
+ {"pcie_gbps", static_cast<double>(numel) * 16.0 / (ms * 1e-3) / 1e9},
709
+ {"numel", static_cast<double>(numel)}};
710
+ }
711
+
712
+ } // namespace trainstream