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.
- trainstream/__init__.py +62 -0
- trainstream/_build.py +201 -0
- trainstream/csrc/bindings.cpp +139 -0
- trainstream/csrc/kernels.h +49 -0
- trainstream/csrc/memory_pinner.cpp +680 -0
- trainstream/csrc/offload_adamw.cpp +712 -0
- trainstream/csrc/offload_adamw.h +154 -0
- trainstream/csrc/offload_adamw_kernel.cu +183 -0
- trainstream/csrc/trainstream.h +132 -0
- trainstream/hooks.py +547 -0
- trainstream/integrations/__init__.py +7 -0
- trainstream/integrations/huggingface.py +211 -0
- trainstream/optimizer.py +333 -0
- trainstream/profiler.py +166 -0
- trainstream/wrapper.py +191 -0
- trainstream-0.1.1.dist-info/METADATA +127 -0
- trainstream-0.1.1.dist-info/RECORD +21 -0
- trainstream-0.1.1.dist-info/WHEEL +5 -0
- trainstream-0.1.1.dist-info/licenses/LICENSE +202 -0
- trainstream-0.1.1.dist-info/licenses/NOTICE +4 -0
- trainstream-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
// TrainStream — runtime core: CUDA event pool, dedicated copy streams, the
|
|
2
|
+
// pin-logging caching host allocator, asynchronous DMA helpers and the
|
|
3
|
+
// CUDA-event-timed micro-benchmarks used by the hardware profiler.
|
|
4
|
+
|
|
5
|
+
#include "trainstream.h"
|
|
6
|
+
|
|
7
|
+
#include <c10/cuda/CUDACachingAllocator.h>
|
|
8
|
+
#include <c10/cuda/CUDAStream.h>
|
|
9
|
+
|
|
10
|
+
#include <algorithm>
|
|
11
|
+
#include <chrono>
|
|
12
|
+
#include <cstdlib>
|
|
13
|
+
#include <cstring>
|
|
14
|
+
#include <deque>
|
|
15
|
+
#include <mutex>
|
|
16
|
+
#include <unordered_map>
|
|
17
|
+
|
|
18
|
+
namespace trainstream {
|
|
19
|
+
|
|
20
|
+
namespace {
|
|
21
|
+
constexpr int kMaxDevices = 64;
|
|
22
|
+
|
|
23
|
+
// Intentionally leaked singletons: destroying CUDA objects from static
|
|
24
|
+
// destructors races with driver teardown at interpreter exit.
|
|
25
|
+
std::mutex& runtime_mutex() {
|
|
26
|
+
static auto* m = new std::mutex();
|
|
27
|
+
return *m;
|
|
28
|
+
}
|
|
29
|
+
Runtime** runtime_table() {
|
|
30
|
+
static auto* table = new Runtime*[kMaxDevices]();
|
|
31
|
+
return table;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
struct EventPool {
|
|
35
|
+
std::mutex mu;
|
|
36
|
+
std::vector<cudaEvent_t> free[kMaxDevices];
|
|
37
|
+
};
|
|
38
|
+
EventPool& event_pool() {
|
|
39
|
+
static auto* pool = new EventPool();
|
|
40
|
+
return *pool;
|
|
41
|
+
}
|
|
42
|
+
} // namespace
|
|
43
|
+
|
|
44
|
+
double now_ms() {
|
|
45
|
+
static const auto t0 = std::chrono::steady_clock::now();
|
|
46
|
+
return std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - t0).count();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ===========================================================================
|
|
50
|
+
// Event
|
|
51
|
+
// ===========================================================================
|
|
52
|
+
Event::Event(int device, bool timing) : device_(device), timing_(timing) {
|
|
53
|
+
if (!timing_) {
|
|
54
|
+
auto& pool = event_pool();
|
|
55
|
+
std::lock_guard<std::mutex> lock(pool.mu);
|
|
56
|
+
auto& bucket = pool.free[device_];
|
|
57
|
+
if (!bucket.empty()) {
|
|
58
|
+
ev_ = bucket.back();
|
|
59
|
+
bucket.pop_back();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
c10::cuda::CUDAGuard guard(static_cast<c10::DeviceIndex>(device_));
|
|
64
|
+
TS_CUDA_CHECK(cudaEventCreateWithFlags(&ev_, timing_ ? cudaEventDefault : cudaEventDisableTiming));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
Event::~Event() {
|
|
68
|
+
if (ev_ == nullptr) return;
|
|
69
|
+
if (!timing_) {
|
|
70
|
+
auto& pool = event_pool();
|
|
71
|
+
std::lock_guard<std::mutex> lock(pool.mu);
|
|
72
|
+
pool.free[device_].push_back(ev_);
|
|
73
|
+
} else {
|
|
74
|
+
cudaEventDestroy(ev_);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
void Event::record(cudaStream_t stream) {
|
|
79
|
+
c10::cuda::CUDAGuard guard(static_cast<c10::DeviceIndex>(device_));
|
|
80
|
+
TS_CUDA_CHECK(cudaEventRecord(ev_, stream));
|
|
81
|
+
recorded_ = true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
bool Event::query() const {
|
|
85
|
+
if (!recorded_) return true;
|
|
86
|
+
const cudaError_t err = cudaEventQuery(ev_);
|
|
87
|
+
if (err == cudaErrorNotReady) {
|
|
88
|
+
(void)cudaGetLastError(); // clear the sticky "not ready" status
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
TS_CUDA_CHECK(err);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
void Event::synchronize() const {
|
|
96
|
+
if (!recorded_) return;
|
|
97
|
+
TS_CUDA_CHECK(cudaEventSynchronize(ev_));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
void Event::block(cudaStream_t stream) const {
|
|
101
|
+
if (!recorded_) return; // waiting on a never-recorded event is a no-op in CUDA too
|
|
102
|
+
c10::cuda::CUDAGuard guard(static_cast<c10::DeviceIndex>(device_));
|
|
103
|
+
TS_CUDA_CHECK(cudaStreamWaitEvent(stream, ev_, 0));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
float Event::elapsed_ms(const Event& end) const {
|
|
107
|
+
TORCH_CHECK(timing_ && end.timing_, "elapsed_ms requires two timing events");
|
|
108
|
+
TORCH_CHECK(recorded_ && end.recorded_, "elapsed_ms requires both events to be recorded");
|
|
109
|
+
end.synchronize();
|
|
110
|
+
float ms = 0.f;
|
|
111
|
+
TS_CUDA_CHECK(cudaEventElapsedTime(&ms, ev_, end.ev_));
|
|
112
|
+
return ms;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
EventPtr make_event(int device, bool timing) { return std::make_shared<Event>(device, timing); }
|
|
116
|
+
|
|
117
|
+
EventPtr record_on_current(int device, bool timing) {
|
|
118
|
+
const int dev = resolve_device(device);
|
|
119
|
+
auto ev = make_event(dev, timing);
|
|
120
|
+
ev->record(compute_stream(dev));
|
|
121
|
+
return ev;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ===========================================================================
|
|
125
|
+
// Runtime
|
|
126
|
+
// ===========================================================================
|
|
127
|
+
Runtime::Runtime(int dev) : device(dev) {
|
|
128
|
+
at::globalContext().lazyInitDevice(at::kCUDA); // torch must own the CUDA context first
|
|
129
|
+
c10::cuda::CUDAGuard guard(static_cast<c10::DeviceIndex>(dev));
|
|
130
|
+
cudaDeviceProp prop{};
|
|
131
|
+
TS_CUDA_CHECK(cudaGetDeviceProperties(&prop, dev));
|
|
132
|
+
sm_count = prop.multiProcessorCount;
|
|
133
|
+
int least = 0, greatest = 0;
|
|
134
|
+
TS_CUDA_CHECK(cudaDeviceGetStreamPriorityRange(&least, &greatest));
|
|
135
|
+
// Prefetch (H2D) sits on the backward critical path -> highest priority.
|
|
136
|
+
TS_CUDA_CHECK(cudaStreamCreateWithPriority(&h2d, cudaStreamNonBlocking, greatest));
|
|
137
|
+
TS_CUDA_CHECK(cudaStreamCreateWithPriority(&d2h, cudaStreamNonBlocking, least));
|
|
138
|
+
TS_CUDA_CHECK(cudaStreamCreateWithPriority(&upd, cudaStreamNonBlocking, least));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
int resolve_device(int device) {
|
|
142
|
+
if (device >= 0) return device;
|
|
143
|
+
int cur = 0;
|
|
144
|
+
TS_CUDA_CHECK(cudaGetDevice(&cur));
|
|
145
|
+
return cur;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
Runtime& runtime(int device) {
|
|
149
|
+
const int dev = resolve_device(device);
|
|
150
|
+
TORCH_CHECK(dev >= 0 && dev < kMaxDevices, "invalid CUDA device ", dev);
|
|
151
|
+
Runtime** table = runtime_table();
|
|
152
|
+
if (Runtime* rt = table[dev]) return *rt;
|
|
153
|
+
std::lock_guard<std::mutex> lock(runtime_mutex());
|
|
154
|
+
if (table[dev] == nullptr) table[dev] = new Runtime(dev);
|
|
155
|
+
return *table[dev];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
std::vector<int> initialized_devices() {
|
|
159
|
+
std::vector<int> out;
|
|
160
|
+
Runtime** table = runtime_table();
|
|
161
|
+
for (int d = 0; d < kMaxDevices; ++d)
|
|
162
|
+
if (table[d] != nullptr) out.push_back(d);
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
cudaStream_t compute_stream(int device) {
|
|
167
|
+
return at::cuda::getCurrentCUDAStream(static_cast<c10::DeviceIndex>(device)).stream();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
void record_stream(const at::Tensor& t, cudaStream_t stream, int device) {
|
|
171
|
+
if (!t.defined() || !t.is_cuda()) return;
|
|
172
|
+
// No-op for memory that did not come from the caching allocator.
|
|
173
|
+
c10::cuda::CUDACachingAllocator::recordStream(
|
|
174
|
+
t.storage().data_ptr(),
|
|
175
|
+
c10::cuda::getStreamFromExternal(stream, static_cast<c10::DeviceIndex>(device)));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ===========================================================================
|
|
179
|
+
// Pin-logging caching host allocator
|
|
180
|
+
// ===========================================================================
|
|
181
|
+
namespace {
|
|
182
|
+
|
|
183
|
+
class PinnedAllocator {
|
|
184
|
+
public:
|
|
185
|
+
static PinnedAllocator& get() {
|
|
186
|
+
static auto* inst = new PinnedAllocator();
|
|
187
|
+
return *inst;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
void* allocate(size_t bytes, bool mapped, bool cached, const std::string& tag) {
|
|
191
|
+
const size_t size = round_size(std::max<size_t>(bytes, 64));
|
|
192
|
+
std::lock_guard<std::mutex> lock(mu_);
|
|
193
|
+
reclaim_pending_locked();
|
|
194
|
+
|
|
195
|
+
if (cached) {
|
|
196
|
+
auto& fl = free_[mapped ? 1 : 0];
|
|
197
|
+
auto it = fl.lower_bound(size);
|
|
198
|
+
// Accept a cached block if it wastes at most 25% (or one 2 MiB page).
|
|
199
|
+
if (it != fl.end() && it->first <= std::max(size + size / 4, size + (2u << 20))) {
|
|
200
|
+
void* ptr = it->second;
|
|
201
|
+
fl.erase(it);
|
|
202
|
+
auto& info = blocks_.at(ptr);
|
|
203
|
+
cached_bytes_ -= info.size;
|
|
204
|
+
info.tag = tag;
|
|
205
|
+
mark_live_locked(ptr, info);
|
|
206
|
+
++cache_hits_;
|
|
207
|
+
log_locked("hit", info.size, tag);
|
|
208
|
+
return ptr;
|
|
209
|
+
}
|
|
210
|
+
++cache_misses_;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
void* ptr = nullptr;
|
|
214
|
+
const unsigned flags = mapped ? (cudaHostAllocMapped | cudaHostAllocPortable) : cudaHostAllocPortable;
|
|
215
|
+
cudaError_t err = cudaHostAlloc(&ptr, size, flags);
|
|
216
|
+
if (err != cudaSuccess && cached_bytes_ > 0) {
|
|
217
|
+
// Out of pinnable memory: drop the cache and retry once.
|
|
218
|
+
(void)cudaGetLastError();
|
|
219
|
+
trim_locked(0);
|
|
220
|
+
err = cudaHostAlloc(&ptr, size, flags);
|
|
221
|
+
}
|
|
222
|
+
TORCH_CHECK(err == cudaSuccess, "TrainStream: cudaHostAlloc(", size,
|
|
223
|
+
" bytes) failed: ", cudaGetErrorString(err),
|
|
224
|
+
". Host RAM may be exhausted or page-locking limits reached.");
|
|
225
|
+
++host_alloc_calls_;
|
|
226
|
+
reserved_bytes_ += size;
|
|
227
|
+
if (mapped) mapped_bytes_ += size;
|
|
228
|
+
BlockInfo info{size, mapped, cached, tag};
|
|
229
|
+
blocks_.emplace(ptr, info);
|
|
230
|
+
mark_live_locked(ptr, info);
|
|
231
|
+
log_locked("alloc", size, tag);
|
|
232
|
+
return ptr;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
void release(void* ptr) {
|
|
236
|
+
std::lock_guard<std::mutex> lock(mu_);
|
|
237
|
+
auto it = blocks_.find(ptr);
|
|
238
|
+
if (it == blocks_.end()) return;
|
|
239
|
+
BlockInfo& info = it->second;
|
|
240
|
+
live_bytes_ -= info.size;
|
|
241
|
+
--live_blocks_;
|
|
242
|
+
tag_bytes_[info.tag] -= info.size;
|
|
243
|
+
if (!info.cached) {
|
|
244
|
+
log_locked("free", info.size, info.tag);
|
|
245
|
+
free_block_locked(ptr);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
log_locked("release", info.size, info.tag);
|
|
249
|
+
// The block may still be the source/target of queued DMA on our copy
|
|
250
|
+
// streams. Fence every copy stream and only recycle once all fences pass.
|
|
251
|
+
Pending p{ptr, {}};
|
|
252
|
+
try {
|
|
253
|
+
for (int dev : initialized_devices()) {
|
|
254
|
+
Runtime& rt = runtime(dev);
|
|
255
|
+
for (cudaStream_t s : {rt.d2h, rt.h2d, rt.upd}) {
|
|
256
|
+
auto ev = make_event(dev);
|
|
257
|
+
ev->record(s);
|
|
258
|
+
p.fences.push_back(std::move(ev));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} catch (...) {
|
|
262
|
+
// Could not create a fence (e.g. the device is out of memory, or CUDA is
|
|
263
|
+
// shutting down). Never recycle unfenced: drain the copy streams instead.
|
|
264
|
+
p.fences.clear();
|
|
265
|
+
(void)cudaGetLastError();
|
|
266
|
+
for (int dev : initialized_devices()) {
|
|
267
|
+
Runtime& rt = runtime(dev);
|
|
268
|
+
cudaStreamSynchronize(rt.d2h);
|
|
269
|
+
cudaStreamSynchronize(rt.h2d);
|
|
270
|
+
cudaStreamSynchronize(rt.upd);
|
|
271
|
+
}
|
|
272
|
+
(void)cudaGetLastError();
|
|
273
|
+
}
|
|
274
|
+
pending_.push_back(std::move(p));
|
|
275
|
+
cached_bytes_ += info.size;
|
|
276
|
+
if (cached_bytes_ > cache_limit_) trim_locked(cache_limit_);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
std::map<std::string, int64_t> stats() {
|
|
280
|
+
std::lock_guard<std::mutex> lock(mu_);
|
|
281
|
+
reclaim_pending_locked();
|
|
282
|
+
return {{"live_bytes", static_cast<int64_t>(live_bytes_)},
|
|
283
|
+
{"peak_live_bytes", static_cast<int64_t>(peak_live_bytes_)},
|
|
284
|
+
{"live_blocks", static_cast<int64_t>(live_blocks_)},
|
|
285
|
+
{"cached_bytes", static_cast<int64_t>(cached_bytes_)},
|
|
286
|
+
{"reserved_bytes", static_cast<int64_t>(reserved_bytes_)},
|
|
287
|
+
{"mapped_bytes", static_cast<int64_t>(mapped_bytes_)},
|
|
288
|
+
{"pending_blocks", static_cast<int64_t>(pending_.size())},
|
|
289
|
+
{"host_alloc_calls", static_cast<int64_t>(host_alloc_calls_)},
|
|
290
|
+
{"host_free_calls", static_cast<int64_t>(host_free_calls_)},
|
|
291
|
+
{"cache_hits", static_cast<int64_t>(cache_hits_)},
|
|
292
|
+
{"cache_misses", static_cast<int64_t>(cache_misses_)},
|
|
293
|
+
{"cache_limit", static_cast<int64_t>(cache_limit_)}};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
std::map<std::string, int64_t> tag_bytes() {
|
|
297
|
+
std::lock_guard<std::mutex> lock(mu_);
|
|
298
|
+
std::map<std::string, int64_t> out;
|
|
299
|
+
for (const auto& kv : tag_bytes_)
|
|
300
|
+
if (kv.second != 0) out[kv.first] = static_cast<int64_t>(kv.second);
|
|
301
|
+
return out;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
std::vector<std::tuple<double, std::string, int64_t, std::string>> log(int64_t limit) {
|
|
305
|
+
std::lock_guard<std::mutex> lock(mu_);
|
|
306
|
+
std::vector<std::tuple<double, std::string, int64_t, std::string>> out;
|
|
307
|
+
const int64_t n = static_cast<int64_t>(log_.size());
|
|
308
|
+
const int64_t start = limit > 0 && limit < n ? n - limit : 0;
|
|
309
|
+
for (int64_t i = start; i < n; ++i) {
|
|
310
|
+
const auto& e = log_[static_cast<size_t>(i)];
|
|
311
|
+
out.emplace_back(e.t_ms, e.op, static_cast<int64_t>(e.bytes), e.tag);
|
|
312
|
+
}
|
|
313
|
+
return out;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
void empty_cache() {
|
|
317
|
+
std::lock_guard<std::mutex> lock(mu_);
|
|
318
|
+
for (auto& p : pending_)
|
|
319
|
+
for (auto& f : p.fences) f->synchronize();
|
|
320
|
+
reclaim_pending_locked();
|
|
321
|
+
trim_locked(0);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
void set_limit(int64_t bytes) {
|
|
325
|
+
std::lock_guard<std::mutex> lock(mu_);
|
|
326
|
+
cache_limit_ = static_cast<size_t>(std::max<int64_t>(bytes, 0));
|
|
327
|
+
trim_locked(cache_limit_);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private:
|
|
331
|
+
struct BlockInfo {
|
|
332
|
+
size_t size;
|
|
333
|
+
bool mapped;
|
|
334
|
+
bool cached;
|
|
335
|
+
std::string tag;
|
|
336
|
+
};
|
|
337
|
+
struct Pending {
|
|
338
|
+
void* ptr;
|
|
339
|
+
std::vector<EventPtr> fences;
|
|
340
|
+
};
|
|
341
|
+
struct LogEntry {
|
|
342
|
+
double t_ms;
|
|
343
|
+
const char* op;
|
|
344
|
+
size_t bytes;
|
|
345
|
+
std::string tag;
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
static size_t round_size(size_t n) {
|
|
349
|
+
constexpr size_t kSmall = 4096, kLarge = 2u << 20;
|
|
350
|
+
if (n < (1u << 20)) return (n + kSmall - 1) / kSmall * kSmall;
|
|
351
|
+
return (n + kLarge - 1) / kLarge * kLarge;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
void mark_live_locked(void* ptr, const BlockInfo& info) {
|
|
355
|
+
(void)ptr;
|
|
356
|
+
live_bytes_ += info.size;
|
|
357
|
+
++live_blocks_;
|
|
358
|
+
peak_live_bytes_ = std::max(peak_live_bytes_, live_bytes_);
|
|
359
|
+
tag_bytes_[info.tag] += info.size;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
void reclaim_pending_locked() {
|
|
363
|
+
for (auto it = pending_.begin(); it != pending_.end();) {
|
|
364
|
+
bool done = true;
|
|
365
|
+
for (auto& f : it->fences) {
|
|
366
|
+
if (!f->query()) {
|
|
367
|
+
done = false;
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (!done) {
|
|
372
|
+
++it;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
const BlockInfo& info = blocks_.at(it->ptr);
|
|
376
|
+
free_[info.mapped ? 1 : 0].emplace(info.size, it->ptr);
|
|
377
|
+
it = pending_.erase(it);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Release free (not pending) cached blocks until cached_bytes_ <= target.
|
|
382
|
+
void trim_locked(size_t target) {
|
|
383
|
+
for (int m = 0; m < 2 && cached_bytes_ > target; ++m) {
|
|
384
|
+
auto& fl = free_[m];
|
|
385
|
+
// Largest first: fewest cudaFreeHost calls to get under the limit.
|
|
386
|
+
while (!fl.empty() && cached_bytes_ > target) {
|
|
387
|
+
auto it = std::prev(fl.end());
|
|
388
|
+
void* ptr = it->second;
|
|
389
|
+
fl.erase(it);
|
|
390
|
+
cached_bytes_ -= blocks_.at(ptr).size;
|
|
391
|
+
free_block_locked(ptr);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
void free_block_locked(void* ptr) {
|
|
397
|
+
auto it = blocks_.find(ptr);
|
|
398
|
+
const size_t size = it->second.size;
|
|
399
|
+
reserved_bytes_ -= size;
|
|
400
|
+
if (it->second.mapped) mapped_bytes_ -= size;
|
|
401
|
+
blocks_.erase(it);
|
|
402
|
+
cudaFreeHost(ptr);
|
|
403
|
+
(void)cudaGetLastError();
|
|
404
|
+
++host_free_calls_;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
void log_locked(const char* op, size_t bytes, const std::string& tag) {
|
|
408
|
+
log_.push_back({now_ms(), op, bytes, tag});
|
|
409
|
+
if (log_.size() > 4096) log_.pop_front();
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
std::mutex mu_;
|
|
413
|
+
std::unordered_map<void*, BlockInfo> blocks_;
|
|
414
|
+
std::multimap<size_t, void*> free_[2];
|
|
415
|
+
std::deque<Pending> pending_;
|
|
416
|
+
std::deque<LogEntry> log_;
|
|
417
|
+
std::map<std::string, int64_t> tag_bytes_;
|
|
418
|
+
size_t live_bytes_ = 0, peak_live_bytes_ = 0, live_blocks_ = 0;
|
|
419
|
+
size_t cached_bytes_ = 0, reserved_bytes_ = 0, mapped_bytes_ = 0;
|
|
420
|
+
size_t cache_limit_ = size_t(8) << 30;
|
|
421
|
+
uint64_t host_alloc_calls_ = 0, host_free_calls_ = 0, cache_hits_ = 0, cache_misses_ = 0;
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
} // namespace
|
|
425
|
+
|
|
426
|
+
at::Tensor pinned_empty(const std::vector<int64_t>& sizes, at::ScalarType dtype,
|
|
427
|
+
const std::string& tag, bool mapped, bool cached) {
|
|
428
|
+
int64_t numel = 1;
|
|
429
|
+
for (int64_t s : sizes) {
|
|
430
|
+
TORCH_CHECK(s >= 0, "pinned_empty: negative dimension");
|
|
431
|
+
numel *= s;
|
|
432
|
+
}
|
|
433
|
+
const size_t bytes = static_cast<size_t>(numel) * at::elementSize(dtype);
|
|
434
|
+
// Without torch's CUDA lazy-init, Tensor.is_pinned() reports false for our blocks.
|
|
435
|
+
at::globalContext().lazyInitDevice(at::kCUDA);
|
|
436
|
+
void* ptr = PinnedAllocator::get().allocate(bytes, mapped, cached, tag);
|
|
437
|
+
return at::from_blob(
|
|
438
|
+
ptr, sizes, [](void* p) { PinnedAllocator::get().release(p); },
|
|
439
|
+
at::TensorOptions().dtype(dtype).device(at::kCPU));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
std::map<std::string, int64_t> pinned_stats() { return PinnedAllocator::get().stats(); }
|
|
443
|
+
std::map<std::string, int64_t> pinned_tag_bytes() { return PinnedAllocator::get().tag_bytes(); }
|
|
444
|
+
std::vector<std::tuple<double, std::string, int64_t, std::string>> pinned_log(int64_t limit) {
|
|
445
|
+
return PinnedAllocator::get().log(limit);
|
|
446
|
+
}
|
|
447
|
+
void pinned_empty_cache() { PinnedAllocator::get().empty_cache(); }
|
|
448
|
+
void pinned_set_cache_limit(int64_t bytes) { PinnedAllocator::get().set_limit(bytes); }
|
|
449
|
+
|
|
450
|
+
void* pinned_device_pointer(const at::Tensor& t) {
|
|
451
|
+
void* dptr = nullptr;
|
|
452
|
+
TS_CUDA_CHECK(cudaHostGetDevicePointer(&dptr, t.data_ptr(), 0));
|
|
453
|
+
return dptr;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ===========================================================================
|
|
457
|
+
// Asynchronous transfers
|
|
458
|
+
// ===========================================================================
|
|
459
|
+
namespace {
|
|
460
|
+
// Copies move raw bytes, so both sides only need to be non-overlapping and
|
|
461
|
+
// dense: their elements then occupy exactly numel * itemsize bytes starting at
|
|
462
|
+
// data_ptr() (PyTorch strides are never negative). This lets a permuted layout
|
|
463
|
+
// (e.g. attention outputs in (B,T,H,D) memory order) round-trip with its exact
|
|
464
|
+
// strides instead of being forced to contiguous.
|
|
465
|
+
void check_pair(const at::Tensor& dst, const at::Tensor& src, const char* what) {
|
|
466
|
+
TORCH_CHECK(dst.is_non_overlapping_and_dense() && src.is_non_overlapping_and_dense(), what,
|
|
467
|
+
": tensors must be non-overlapping and dense");
|
|
468
|
+
TORCH_CHECK(dst.scalar_type() == src.scalar_type(), what, ": dtype mismatch (",
|
|
469
|
+
dst.scalar_type(), " vs ", src.scalar_type(), ")");
|
|
470
|
+
TORCH_CHECK(dst.numel() == src.numel(), what, ": numel mismatch (", dst.numel(), " vs ",
|
|
471
|
+
src.numel(), ")");
|
|
472
|
+
}
|
|
473
|
+
} // namespace
|
|
474
|
+
|
|
475
|
+
EventPtr d2h_async(const at::Tensor& dst, const at::Tensor& src) {
|
|
476
|
+
TORCH_CHECK(src.is_cuda(), "d2h_async: src must be a CUDA tensor");
|
|
477
|
+
TORCH_CHECK(dst.device().is_cpu(), "d2h_async: dst must be a (pinned) CPU tensor");
|
|
478
|
+
check_pair(dst, src, "d2h_async");
|
|
479
|
+
const int dev = src.get_device();
|
|
480
|
+
Runtime& rt = runtime(dev);
|
|
481
|
+
c10::cuda::CUDAGuard guard(static_cast<c10::DeviceIndex>(dev));
|
|
482
|
+
|
|
483
|
+
// Producer -> copy-stream dependency, expressed on the GPU (no host stall).
|
|
484
|
+
auto produced = make_event(dev);
|
|
485
|
+
produced->record(compute_stream(dev));
|
|
486
|
+
produced->block(rt.d2h);
|
|
487
|
+
|
|
488
|
+
const size_t bytes = src.nbytes();
|
|
489
|
+
if (bytes > 0)
|
|
490
|
+
TS_CUDA_CHECK(cudaMemcpyAsync(dst.data_ptr(), src.data_ptr(), bytes, cudaMemcpyDeviceToHost, rt.d2h));
|
|
491
|
+
// No recordStream: tagging the block makes the allocator create CUDA events
|
|
492
|
+
// inside its free path, which can fail (and abort) on a full device. The
|
|
493
|
+
// caller keeps `src` alive until the returned event has completed instead.
|
|
494
|
+
auto done = make_event(dev);
|
|
495
|
+
done->record(rt.d2h);
|
|
496
|
+
rt.bytes_d2h += bytes;
|
|
497
|
+
rt.copies_d2h += 1;
|
|
498
|
+
return done;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
EventPtr h2d_async(const at::Tensor& dst, const at::Tensor& src, const EventPtr& after) {
|
|
502
|
+
TORCH_CHECK(dst.is_cuda(), "h2d_async: dst must be a CUDA tensor");
|
|
503
|
+
TORCH_CHECK(src.device().is_cpu(), "h2d_async: src must be a (pinned) CPU tensor");
|
|
504
|
+
check_pair(dst, src, "h2d_async");
|
|
505
|
+
const int dev = dst.get_device();
|
|
506
|
+
Runtime& rt = runtime(dev);
|
|
507
|
+
c10::cuda::CUDAGuard guard(static_cast<c10::DeviceIndex>(dev));
|
|
508
|
+
if (after) after->block(rt.h2d);
|
|
509
|
+
const size_t bytes = src.nbytes();
|
|
510
|
+
if (bytes > 0)
|
|
511
|
+
TS_CUDA_CHECK(cudaMemcpyAsync(dst.data_ptr(), src.data_ptr(), bytes, cudaMemcpyHostToDevice, rt.h2d));
|
|
512
|
+
auto done = make_event(dev);
|
|
513
|
+
done->record(rt.h2d);
|
|
514
|
+
rt.bytes_h2d += bytes;
|
|
515
|
+
rt.copies_h2d += 1;
|
|
516
|
+
return done;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
std::tuple<at::Tensor, EventPtr> h2d_alloc_async(const at::Tensor& src, const EventPtr& after,
|
|
520
|
+
int device, const std::vector<int64_t>& sizes,
|
|
521
|
+
const std::vector<int64_t>& strides) {
|
|
522
|
+
TORCH_CHECK(src.device().is_cpu() && src.is_contiguous(),
|
|
523
|
+
"h2d_alloc_async: src must be a contiguous (pinned) CPU tensor");
|
|
524
|
+
TORCH_CHECK(sizes.size() == strides.size(), "h2d_alloc_async: sizes and strides must be given together");
|
|
525
|
+
const int dev = resolve_device(device);
|
|
526
|
+
Runtime& rt = runtime(dev);
|
|
527
|
+
c10::cuda::CUDAGuard guard(static_cast<c10::DeviceIndex>(dev));
|
|
528
|
+
const cudaStream_t consumer = compute_stream(dev);
|
|
529
|
+
|
|
530
|
+
at::Tensor dst;
|
|
531
|
+
{
|
|
532
|
+
// Allocate from the consumer's (compute stream's) pool so paging never
|
|
533
|
+
// splits VRAM into a second per-stream pool. The block's previous users
|
|
534
|
+
// are all queued before `freed`, so the copy waits only for those.
|
|
535
|
+
const auto opts = at::TensorOptions()
|
|
536
|
+
.dtype(src.scalar_type())
|
|
537
|
+
.device(at::Device(at::kCUDA, static_cast<c10::DeviceIndex>(dev)));
|
|
538
|
+
dst = sizes.empty() && src.dim() > 0 ? at::empty(src.sizes(), opts) : at::empty_strided(sizes, strides, opts);
|
|
539
|
+
}
|
|
540
|
+
TORCH_CHECK(dst.numel() == src.numel() && dst.is_non_overlapping_and_dense(),
|
|
541
|
+
"h2d_alloc_async: target layout must be dense with the same numel as the host buffer");
|
|
542
|
+
auto freed = make_event(dev);
|
|
543
|
+
freed->record(consumer);
|
|
544
|
+
freed->block(rt.h2d);
|
|
545
|
+
if (after) after->block(rt.h2d);
|
|
546
|
+
const size_t bytes = src.nbytes();
|
|
547
|
+
if (bytes > 0)
|
|
548
|
+
TS_CUDA_CHECK(cudaMemcpyAsync(dst.data_ptr(), src.data_ptr(), bytes, cudaMemcpyHostToDevice, rt.h2d));
|
|
549
|
+
// The consumer must wait on the returned event before using *or dropping*
|
|
550
|
+
// `dst` (TrainStream's activation pager does both).
|
|
551
|
+
auto done = make_event(dev);
|
|
552
|
+
done->record(rt.h2d);
|
|
553
|
+
rt.bytes_h2d += bytes;
|
|
554
|
+
rt.copies_h2d += 1;
|
|
555
|
+
return {dst, done};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
std::map<std::string, int64_t> transfer_stats(int device) {
|
|
559
|
+
Runtime& rt = runtime(device);
|
|
560
|
+
return {{"bytes_d2h", static_cast<int64_t>(rt.bytes_d2h.load())},
|
|
561
|
+
{"bytes_h2d", static_cast<int64_t>(rt.bytes_h2d.load())},
|
|
562
|
+
{"copies_d2h", static_cast<int64_t>(rt.copies_d2h.load())},
|
|
563
|
+
{"copies_h2d", static_cast<int64_t>(rt.copies_h2d.load())}};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// ===========================================================================
|
|
567
|
+
// Micro-benchmarks
|
|
568
|
+
// ===========================================================================
|
|
569
|
+
namespace {
|
|
570
|
+
double timed_copies(int dev, cudaStream_t s, void* dst, const void* src, size_t bytes,
|
|
571
|
+
cudaMemcpyKind kind, int iters) {
|
|
572
|
+
Event start(dev, true), stop(dev, true);
|
|
573
|
+
TS_CUDA_CHECK(cudaMemcpyAsync(dst, src, bytes, kind, s)); // warm-up (TLB, DMA setup)
|
|
574
|
+
start.record(s);
|
|
575
|
+
for (int i = 0; i < iters; ++i) TS_CUDA_CHECK(cudaMemcpyAsync(dst, src, bytes, kind, s));
|
|
576
|
+
stop.record(s);
|
|
577
|
+
return static_cast<double>(start.elapsed_ms(stop));
|
|
578
|
+
}
|
|
579
|
+
double gbps(size_t bytes, int iters, double ms) {
|
|
580
|
+
return ms <= 0 ? 0.0 : static_cast<double>(bytes) * iters / (ms * 1e-3) / 1e9;
|
|
581
|
+
}
|
|
582
|
+
} // namespace
|
|
583
|
+
|
|
584
|
+
std::map<std::string, double> benchmark_pcie(int device, int64_t bytes_i, int iters) {
|
|
585
|
+
const int dev = resolve_device(device);
|
|
586
|
+
Runtime& rt = runtime(dev);
|
|
587
|
+
c10::cuda::CUDAGuard guard(static_cast<c10::DeviceIndex>(dev));
|
|
588
|
+
const size_t bytes = static_cast<size_t>(std::max<int64_t>(bytes_i, 1 << 20));
|
|
589
|
+
iters = std::max(iters, 1);
|
|
590
|
+
|
|
591
|
+
void* host = nullptr;
|
|
592
|
+
void* host2 = nullptr;
|
|
593
|
+
TS_CUDA_CHECK(cudaHostAlloc(&host, bytes, cudaHostAllocPortable));
|
|
594
|
+
TS_CUDA_CHECK(cudaHostAlloc(&host2, bytes, cudaHostAllocPortable));
|
|
595
|
+
void* dev_a = nullptr;
|
|
596
|
+
void* dev_b = nullptr;
|
|
597
|
+
TS_CUDA_CHECK(cudaMalloc(&dev_a, bytes));
|
|
598
|
+
TS_CUDA_CHECK(cudaMalloc(&dev_b, bytes));
|
|
599
|
+
std::memset(host, 1, bytes);
|
|
600
|
+
std::memset(host2, 2, bytes);
|
|
601
|
+
|
|
602
|
+
std::map<std::string, double> out;
|
|
603
|
+
const double h2d_ms = timed_copies(dev, rt.h2d, dev_a, host, bytes, cudaMemcpyHostToDevice, iters);
|
|
604
|
+
const double d2h_ms = timed_copies(dev, rt.d2h, host, dev_a, bytes, cudaMemcpyDeviceToHost, iters);
|
|
605
|
+
out["h2d_gbps"] = gbps(bytes, iters, h2d_ms);
|
|
606
|
+
out["d2h_gbps"] = gbps(bytes, iters, d2h_ms);
|
|
607
|
+
|
|
608
|
+
// Full duplex: both copy engines busy at once.
|
|
609
|
+
{
|
|
610
|
+
Event start(dev, true), end_d2h(dev, true), stop(dev, true);
|
|
611
|
+
start.record(rt.h2d);
|
|
612
|
+
start.block(rt.d2h);
|
|
613
|
+
for (int i = 0; i < iters; ++i) {
|
|
614
|
+
TS_CUDA_CHECK(cudaMemcpyAsync(dev_a, host, bytes, cudaMemcpyHostToDevice, rt.h2d));
|
|
615
|
+
TS_CUDA_CHECK(cudaMemcpyAsync(host2, dev_b, bytes, cudaMemcpyDeviceToHost, rt.d2h));
|
|
616
|
+
}
|
|
617
|
+
end_d2h.record(rt.d2h);
|
|
618
|
+
end_d2h.block(rt.h2d);
|
|
619
|
+
stop.record(rt.h2d);
|
|
620
|
+
out["bidir_gbps"] = gbps(bytes * 2, iters, start.elapsed_ms(stop));
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Pageable baseline: the driver must bounce through its own staging buffer.
|
|
624
|
+
{
|
|
625
|
+
void* pageable = std::malloc(bytes);
|
|
626
|
+
TORCH_CHECK(pageable != nullptr, "benchmark_pcie: malloc failed");
|
|
627
|
+
std::memset(pageable, 3, bytes);
|
|
628
|
+
const int it = std::max(1, iters / 2);
|
|
629
|
+
const double ms = timed_copies(dev, rt.h2d, dev_a, pageable, bytes, cudaMemcpyHostToDevice, it);
|
|
630
|
+
out["pageable_h2d_gbps"] = gbps(bytes, it, ms);
|
|
631
|
+
std::free(pageable);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Transfer-size sweep: shows where per-DMA setup overhead stops dominating.
|
|
635
|
+
for (size_t sz = 64u << 10; sz <= std::min(bytes, size_t(64) << 20); sz *= 4) {
|
|
636
|
+
const int it = static_cast<int>(std::max<size_t>(4, std::min<size_t>(256, (size_t(256) << 20) / sz)));
|
|
637
|
+
out["sweep_h2d_" + std::to_string(sz)] =
|
|
638
|
+
gbps(sz, it, timed_copies(dev, rt.h2d, dev_a, host, sz, cudaMemcpyHostToDevice, it));
|
|
639
|
+
out["sweep_d2h_" + std::to_string(sz)] =
|
|
640
|
+
gbps(sz, it, timed_copies(dev, rt.d2h, host, dev_a, sz, cudaMemcpyDeviceToHost, it));
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Small-transfer latency (4 KiB) in microseconds.
|
|
644
|
+
{
|
|
645
|
+
const int it = 200;
|
|
646
|
+
const double ms = timed_copies(dev, rt.h2d, dev_a, host, 4096, cudaMemcpyHostToDevice, it);
|
|
647
|
+
out["latency_us_4k"] = ms * 1e3 / it;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
TS_CUDA_CHECK(cudaStreamSynchronize(rt.h2d));
|
|
651
|
+
TS_CUDA_CHECK(cudaStreamSynchronize(rt.d2h));
|
|
652
|
+
cudaFree(dev_a);
|
|
653
|
+
cudaFree(dev_b);
|
|
654
|
+
cudaFreeHost(host);
|
|
655
|
+
cudaFreeHost(host2);
|
|
656
|
+
out["bytes"] = static_cast<double>(bytes);
|
|
657
|
+
return out;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
std::map<std::string, double> benchmark_matmul(int device, int64_t n, at::ScalarType dtype,
|
|
661
|
+
int iters) {
|
|
662
|
+
const int dev = resolve_device(device);
|
|
663
|
+
c10::cuda::CUDAGuard guard(static_cast<c10::DeviceIndex>(dev));
|
|
664
|
+
auto opts = at::TensorOptions().dtype(dtype).device(at::Device(at::kCUDA, static_cast<c10::DeviceIndex>(dev)));
|
|
665
|
+
at::Tensor a = at::randn({n, n}, opts);
|
|
666
|
+
at::Tensor b = at::randn({n, n}, opts);
|
|
667
|
+
at::Tensor c = at::mm(a, b); // warm-up: cuBLAS handle + heuristics
|
|
668
|
+
c = at::mm(a, b);
|
|
669
|
+
const cudaStream_t s = compute_stream(dev);
|
|
670
|
+
Event start(dev, true), stop(dev, true);
|
|
671
|
+
iters = std::max(iters, 1);
|
|
672
|
+
start.record(s);
|
|
673
|
+
for (int i = 0; i < iters; ++i) at::mm_out(c, a, b);
|
|
674
|
+
stop.record(s);
|
|
675
|
+
const double ms = start.elapsed_ms(stop) / iters;
|
|
676
|
+
const double flops = 2.0 * static_cast<double>(n) * n * n;
|
|
677
|
+
return {{"ms_per_matmul", ms}, {"tflops", flops / (ms * 1e-3) / 1e12}, {"n", static_cast<double>(n)}};
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
} // namespace trainstream
|