sparse-delta-memory 2.0.0__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.
- sparse_delta_memory/__init__.py +13 -0
- sparse_delta_memory/backward.py +367 -0
- sparse_delta_memory/cache.py +70 -0
- sparse_delta_memory/cuda/__init__.py +3 -0
- sparse_delta_memory/cuda/_build.py +254 -0
- sparse_delta_memory/cuda/_device.py +158 -0
- sparse_delta_memory/cuda/_devmemo.py +64 -0
- sparse_delta_memory/cuda/_image.py +128 -0
- sparse_delta_memory/cuda/bwd_steps_cuda.py +111 -0
- sparse_delta_memory/cuda/gather_cuda.py +796 -0
- sparse_delta_memory/cuda/gather_ns_cuda.py +134 -0
- sparse_delta_memory/cuda/gx_gds_cuda.py +1297 -0
- sparse_delta_memory/cuda/gx_gemm_cuda.py +857 -0
- sparse_delta_memory/cuda/gx_pa2_cuda.py +1320 -0
- sparse_delta_memory/cuda/mover_da2_cuda.py +1802 -0
- sparse_delta_memory/cuda/mover_fx_cuda.py +2229 -0
- sparse_delta_memory/cuda/pa_dc_cuda.py +1500 -0
- sparse_delta_memory/cuda/phase1_bwd_cuda.py +852 -0
- sparse_delta_memory/cuda/phase1_common.cuh +181 -0
- sparse_delta_memory/cuda/phase1_cuda.py +3801 -0
- sparse_delta_memory/cuda/product_key_topk_anyf_cuda.py +668 -0
- sparse_delta_memory/cuda/product_key_topk_cuda.py +735 -0
- sparse_delta_memory/cuda/px_qrec_cuda.py +167 -0
- sparse_delta_memory/cuda/sdm_copy.cuh +90 -0
- sparse_delta_memory/cuda/sdm_plancap.cuh +150 -0
- sparse_delta_memory/cuda_graph.py +605 -0
- sparse_delta_memory/decode.py +140 -0
- sparse_delta_memory/forward.py +464 -0
- sparse_delta_memory/layer.py +663 -0
- sparse_delta_memory/memory_ops.py +1069 -0
- sparse_delta_memory/memory_stats.py +221 -0
- sparse_delta_memory/movers.py +198 -0
- sparse_delta_memory/phase2_kernels.py +267 -0
- sparse_delta_memory/product_key_topk.py +113 -0
- sparse_delta_memory/wy_solve.py +273 -0
- sparse_delta_memory-2.0.0.dist-info/METADATA +482 -0
- sparse_delta_memory-2.0.0.dist-info/RECORD +40 -0
- sparse_delta_memory-2.0.0.dist-info/WHEEL +5 -0
- sparse_delta_memory-2.0.0.dist-info/licenses/LICENSE +407 -0
- sparse_delta_memory-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
"""px_qrec_cuda: compact read records (qrec8) for the deterministic backward's read-side mover (GDS), built on a
|
|
3
|
+
side stream, one chunk step at a time.
|
|
4
|
+
|
|
5
|
+
qrec8[c, j] = {absolute slot, f32 bits of __fmul_rn(f32(q_val), expf(log_q))} for sorted read position j of chunk c
|
|
6
|
+
(the order of Phase1.spay_q[c]); padding and invalid entries are {0, 0}. The records are bitwise equal to words 1-2 of
|
|
7
|
+
Phase 1's qrec (DET_QREC) and to Phase 1's own qrec8 (DET_QREC8): the same inputs (the saved q_idx, q_val, log_q) and
|
|
8
|
+
the same expression.
|
|
9
|
+
|
|
10
|
+
build_qrec8_steps(...) returns (qrec8 int32 [Nt*P, C*R, 2], events). events[ti] is recorded once rows
|
|
11
|
+
[ti*P, (ti+1)*P) are written (every step is queued up front, in backward order Nt-1..0), and the consumer
|
|
12
|
+
(gx_gds_cuda) waits on events[ti] before step ti. events is None when one launch on the current stream builds every
|
|
13
|
+
step (CUDA graph capture, or Nt == 1).
|
|
14
|
+
|
|
15
|
+
Why the side-stream path is safe:
|
|
16
|
+
- Fork: side.wait_stream(cur) orders every side launch after the work already queued on the current stream (the inputs
|
|
17
|
+
are complete; qrec8, allocated on the current stream just before, is not used by queued work).
|
|
18
|
+
- Join: the consumer waits on events[ti] on the current stream before step ti, the only reader of those rows. Every
|
|
19
|
+
step is waited on, so all side work is complete, as seen from the current stream, once the backward loop has run.
|
|
20
|
+
- Allocator: qrec8, every input and every contiguous copy made here are marked record_stream(side), so the caching
|
|
21
|
+
allocator keeps their blocks from reuse until the side work queued so far has completed. No host synchronization."""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import functools
|
|
26
|
+
|
|
27
|
+
import torch
|
|
28
|
+
|
|
29
|
+
from ._build import load_cuda_extension
|
|
30
|
+
|
|
31
|
+
__all__ = ["build_qrec8", "build_qrec8_steps"]
|
|
32
|
+
|
|
33
|
+
_CPP_SOURCE = r"""
|
|
34
|
+
void px_qrec8(at::Tensor spay_q, at::Tensor q_idx, at::Tensor q_val, at::Tensor log_q, at::Tensor out,
|
|
35
|
+
int64_t c0, int64_t c1);
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
_CUDA_SOURCE = r"""
|
|
39
|
+
#include <torch/extension.h>
|
|
40
|
+
#include <ATen/cuda/CUDAContext.h>
|
|
41
|
+
#include <c10/cuda/CUDAException.h>
|
|
42
|
+
#include <c10/cuda/CUDAGuard.h>
|
|
43
|
+
#include <cuda_bf16.h>
|
|
44
|
+
|
|
45
|
+
namespace {
|
|
46
|
+
constexpr uint32_t kInvalid = 1u << 17;
|
|
47
|
+
__device__ __forceinline__ float f_of(float v) { return v; }
|
|
48
|
+
__device__ __forceinline__ float f_of(__nv_bfloat16 v) { return __bfloat162float(v); }
|
|
49
|
+
|
|
50
|
+
// One thread per sorted read position of chunks [c0, c1): the records of qrec words 1-2.
|
|
51
|
+
template <typename IdxT, typename ValT>
|
|
52
|
+
__global__ void __launch_bounds__(256) px_qrec8_records_kernel(
|
|
53
|
+
const int32_t* __restrict__ spay_q, const IdxT* __restrict__ q_idx, const ValT* __restrict__ q_val,
|
|
54
|
+
const float* __restrict__ log_q, int2* __restrict__ out, int64_t first, int64_t total, int n, int R) {
|
|
55
|
+
const int64_t i = first + static_cast<int64_t>(blockIdx.x) * blockDim.x + threadIdx.x;
|
|
56
|
+
if (i >= total) return;
|
|
57
|
+
const int64_t chunk = i / n;
|
|
58
|
+
const uint32_t e = static_cast<uint32_t>(spay_q[i]);
|
|
59
|
+
int slot = 0;
|
|
60
|
+
float qs = 0.0f;
|
|
61
|
+
if (!(e & kInvalid)) {
|
|
62
|
+
const int64_t o = chunk * n + static_cast<int64_t>((e >> 9) & 0xFFu) * R + (e & 0xFFu);
|
|
63
|
+
slot = static_cast<int>(q_idx[o]);
|
|
64
|
+
qs = __fmul_rn(f_of(q_val[o]), expf(log_q[o]));
|
|
65
|
+
}
|
|
66
|
+
__stcs(out + i, make_int2(slot, __float_as_int(qs)));
|
|
67
|
+
}
|
|
68
|
+
} // namespace
|
|
69
|
+
|
|
70
|
+
void px_qrec8(at::Tensor spay_q, at::Tensor q_idx, at::Tensor q_val, at::Tensor log_q, at::Tensor out,
|
|
71
|
+
int64_t c0, int64_t c1) {
|
|
72
|
+
const c10::cuda::CUDAGuard guard(spay_q.device());
|
|
73
|
+
TORCH_CHECK(spay_q.dim() == 2 && spay_q.scalar_type() == at::kInt && spay_q.is_contiguous(), "px_qrec8: spay_q");
|
|
74
|
+
const int64_t N = spay_q.size(0), n = spay_q.size(1);
|
|
75
|
+
TORCH_CHECK(q_idx.dim() == 3 && q_idx.size(0) == N && q_idx.size(1) * q_idx.size(2) == n &&
|
|
76
|
+
(q_idx.scalar_type() == at::kInt || q_idx.scalar_type() == at::kLong) && q_idx.is_contiguous(),
|
|
77
|
+
"px_qrec8: q_idx [N, C, R] int32/int64");
|
|
78
|
+
TORCH_CHECK(q_val.sizes() == q_idx.sizes() && q_val.is_contiguous() &&
|
|
79
|
+
(q_val.scalar_type() == at::kFloat || q_val.scalar_type() == at::kBFloat16), "px_qrec8: q_val");
|
|
80
|
+
TORCH_CHECK(log_q.sizes() == q_idx.sizes() && log_q.is_contiguous() && log_q.scalar_type() == at::kFloat,
|
|
81
|
+
"px_qrec8: log_q");
|
|
82
|
+
TORCH_CHECK(out.numel() == N * n * 2 && out.scalar_type() == at::kInt && out.is_contiguous() &&
|
|
83
|
+
(reinterpret_cast<uintptr_t>(out.data_ptr()) & 7) == 0, "px_qrec8: out [N, n, 2] int32");
|
|
84
|
+
for (const at::Tensor* t : {&q_idx, &q_val, &log_q, &out})
|
|
85
|
+
TORCH_CHECK(t->device() == spay_q.device(), "px_qrec8: one device");
|
|
86
|
+
TORCH_CHECK(0 <= c0 && c0 <= c1 && c1 <= N, "px_qrec8: chunk range");
|
|
87
|
+
const int64_t first = c0 * n, total = c1 * n;
|
|
88
|
+
if (total == first) return;
|
|
89
|
+
const int R = static_cast<int>(q_idx.size(2));
|
|
90
|
+
const int blocks = static_cast<int>((total - first + 255) / 256);
|
|
91
|
+
auto stream = at::cuda::getCurrentCUDAStream();
|
|
92
|
+
auto* o2 = reinterpret_cast<int2*>(out.data_ptr<int32_t>());
|
|
93
|
+
#define PX_Q8(IDX, VAL) \
|
|
94
|
+
px_qrec8_records_kernel<IDX, VAL><<<blocks, 256, 0, stream>>>( \
|
|
95
|
+
spay_q.data_ptr<int32_t>(), q_idx.data_ptr<IDX>(), reinterpret_cast<const VAL*>(q_val.data_ptr()), \
|
|
96
|
+
log_q.data_ptr<float>(), o2, first, total, static_cast<int>(n), R)
|
|
97
|
+
const bool i64 = q_idx.scalar_type() == at::kLong, bf = q_val.scalar_type() == at::kBFloat16;
|
|
98
|
+
if (i64 && bf) { PX_Q8(int64_t, __nv_bfloat16); }
|
|
99
|
+
else if (i64) { PX_Q8(int64_t, float); }
|
|
100
|
+
else if (bf) { PX_Q8(int32_t, __nv_bfloat16); }
|
|
101
|
+
else { PX_Q8(int32_t, float); }
|
|
102
|
+
#undef PX_Q8
|
|
103
|
+
C10_CUDA_KERNEL_LAUNCH_CHECK();
|
|
104
|
+
}
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@functools.cache
|
|
109
|
+
def _module():
|
|
110
|
+
return load_cuda_extension(
|
|
111
|
+
"sdm_px_qrec",
|
|
112
|
+
cpp_sources=_CPP_SOURCE,
|
|
113
|
+
cuda_sources=_CUDA_SOURCE,
|
|
114
|
+
functions=("px_qrec8",),
|
|
115
|
+
extra_cuda_cflags=("-O3",),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def build_qrec8(spay_q, q_idx, q_val, log_q):
|
|
120
|
+
"""qrec8 int32 [N, C*R, 2] in one launch on the current stream (see the module docstring)."""
|
|
121
|
+
n = spay_q.shape[-1]
|
|
122
|
+
sq = spay_q.reshape(-1, n).contiguous()
|
|
123
|
+
N = sq.shape[0]
|
|
124
|
+
out = torch.empty(N, n, 2, device=sq.device, dtype=torch.int32)
|
|
125
|
+
_module().px_qrec8(sq, q_idx.reshape(N, -1, q_idx.shape[-1]).contiguous(),
|
|
126
|
+
q_val.reshape(N, -1, q_val.shape[-1]).contiguous(),
|
|
127
|
+
log_q.reshape(N, -1, log_q.shape[-1]).contiguous(), out, 0, N)
|
|
128
|
+
return out
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
_SIDE = {}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def build_qrec8_steps(spay_q, q_idx, q_val, log_q, *, P, Nt):
|
|
135
|
+
"""(qrec8 int32 [Nt*P, C*R, 2], events or None): records built per chunk step on a side stream, steps queued in
|
|
136
|
+
backward order; events[ti] marks rows [ti*P, (ti+1)*P) as written. Under CUDA graph capture on the inputs' device,
|
|
137
|
+
or for Nt == 1, one launch on the current stream builds every step (events None). Same bits either way. Slots are
|
|
138
|
+
narrowed to int32 as in Phase 1's qrec: the forward that produced spay_q / log_q has checked the absolute-slot bound."""
|
|
139
|
+
n = spay_q.shape[-1]
|
|
140
|
+
N = spay_q.numel() // n
|
|
141
|
+
if N != Nt * P:
|
|
142
|
+
raise ValueError(f"build_qrec8_steps: {N} chunks != Nt * P = {Nt} * {P}")
|
|
143
|
+
dev = spay_q.device
|
|
144
|
+
with torch.cuda.device(dev):
|
|
145
|
+
if Nt <= 1 or torch.cuda.is_current_stream_capturing():
|
|
146
|
+
return build_qrec8(spay_q, q_idx, q_val, log_q), None
|
|
147
|
+
sq = spay_q.reshape(-1, n).contiguous()
|
|
148
|
+
qi = q_idx.reshape(N, -1, q_idx.shape[-1]).contiguous()
|
|
149
|
+
qv = q_val.reshape(N, -1, q_val.shape[-1]).contiguous()
|
|
150
|
+
lq = log_q.reshape(N, -1, log_q.shape[-1]).contiguous()
|
|
151
|
+
cur = torch.cuda.current_stream(dev)
|
|
152
|
+
side = _SIDE.get(dev)
|
|
153
|
+
if side is None:
|
|
154
|
+
side = _SIDE.setdefault(dev, torch.cuda.Stream(device=dev))
|
|
155
|
+
out = torch.empty(N, n, 2, device=dev, dtype=torch.int32)
|
|
156
|
+
mod = _module()
|
|
157
|
+
side.wait_stream(cur)
|
|
158
|
+
events = [None] * Nt
|
|
159
|
+
with torch.cuda.stream(side):
|
|
160
|
+
for ti in reversed(range(Nt)):
|
|
161
|
+
mod.px_qrec8(sq, qi, qv, lq, out, ti * P, (ti + 1) * P)
|
|
162
|
+
ev = torch.cuda.Event()
|
|
163
|
+
ev.record(side)
|
|
164
|
+
events[ti] = ev
|
|
165
|
+
for t in (out, sq, qi, qv, lq):
|
|
166
|
+
t.record_stream(side)
|
|
167
|
+
return out, events
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// sdm_copy.cuh: the routing predicate's device mirror and the portable copy layer of the fast kernels.
|
|
2
|
+
//
|
|
3
|
+
// SDM_PORTABLE_ARCH is defined in every device pass except the modern ones (sm_90 / sm_100 / sm_103: H100, B200,
|
|
4
|
+
// GB300), mirroring cuda/_device.py backend(): the modern passes keep their preprocessed text, every other device pass
|
|
5
|
+
// (sm_80, 86, 89, 120, ...) compiles the portable side of the kernels' arch guards, so an sm_120 build runs the same
|
|
6
|
+
// device branches as sm_86 / 89. Guards: `#ifdef SDM_PORTABLE_ARCH` (a portable device pass) and
|
|
7
|
+
// `#if defined(__CUDA_ARCH__) && !defined(SDM_PORTABLE_ARCH)` (a modern device pass); the host pass defines neither.
|
|
8
|
+
//
|
|
9
|
+
// sdmcp: cooperative global -> shared copies with cp.async for the portable side. Every copying thread issues its pieces
|
|
10
|
+
// and commits its own groups; a thread's cp.async wait covers its own copies only, so the bytes reach the other threads
|
|
11
|
+
// through a barrier passed AFTER every copier waited (publish_cta / publish_named), or through an mbarrier the copies
|
|
12
|
+
// arrive on (arrive_on). No bulk copies, no transaction counts, no PDL, no clusters. Every call is made by all threads of
|
|
13
|
+
// the copying group at the same program point, never from a thread-0 branch (the barriers would deadlock). Only portable
|
|
14
|
+
// device passes see this code (no modern pass gets a token of it); below sm_80 (no cp.async) every call traps, so
|
|
15
|
+
// multi-architecture builds keep compiling.
|
|
16
|
+
#pragma once
|
|
17
|
+
#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ != 900 && __CUDA_ARCH__ != 1000 && __CUDA_ARCH__ != 1030
|
|
18
|
+
#define SDM_PORTABLE_ARCH 1
|
|
19
|
+
#endif
|
|
20
|
+
|
|
21
|
+
#ifdef SDM_PORTABLE_ARCH
|
|
22
|
+
#include <cstdint>
|
|
23
|
+
#if __CUDA_ARCH__ >= 800
|
|
24
|
+
#define SDMCP_ASM(...) asm volatile(__VA_ARGS__)
|
|
25
|
+
#else
|
|
26
|
+
#define SDMCP_ASM(...) __trap()
|
|
27
|
+
#endif
|
|
28
|
+
namespace sdmcp {
|
|
29
|
+
__device__ __forceinline__ uint32_t saddr(const void* p) { return static_cast<uint32_t>(__cvta_generic_to_shared(p)); }
|
|
30
|
+
// 16 bytes through L2 only (coherent with what the previous kernel wrote); the bytes past src_bytes (0..16) are
|
|
31
|
+
// zero-filled (src_bytes 0 reads nothing, but `g` must still be a valid global address).
|
|
32
|
+
__device__ __forceinline__ void cp16(void* s, const void* g, int src_bytes = 16) {
|
|
33
|
+
SDMCP_ASM("cp.async.cg.shared.global [%0], [%1], 16, %2;\n" ::"r"(saddr(s)), "l"(g), "r"(src_bytes) : "memory");
|
|
34
|
+
}
|
|
35
|
+
// 8 / 4 bytes (rows that are not whole 16-byte vectors), through L1 and L2; zero fill past src_bytes.
|
|
36
|
+
__device__ __forceinline__ void cp8(void* s, const void* g, int src_bytes = 8) {
|
|
37
|
+
SDMCP_ASM("cp.async.ca.shared.global [%0], [%1], 8, %2;\n" ::"r"(saddr(s)), "l"(g), "r"(src_bytes) : "memory");
|
|
38
|
+
}
|
|
39
|
+
__device__ __forceinline__ void cp4(void* s, const void* g, int src_bytes = 4) {
|
|
40
|
+
SDMCP_ASM("cp.async.ca.shared.global [%0], [%1], 4, %2;\n" ::"r"(saddr(s)), "l"(g), "r"(src_bytes) : "memory");
|
|
41
|
+
}
|
|
42
|
+
__device__ __forceinline__ void commit() { SDMCP_ASM("cp.async.commit_group;\n" ::: "memory"); }
|
|
43
|
+
// This thread's groups: at most N of the newest still pending (wait<N>), or none (wait_all).
|
|
44
|
+
template <int N>
|
|
45
|
+
__device__ __forceinline__ void wait() { SDMCP_ASM("cp.async.wait_group %0;\n" ::"n"(N) : "memory"); }
|
|
46
|
+
__device__ __forceinline__ void wait_all() { SDMCP_ASM("cp.async.wait_all;\n" ::: "memory"); }
|
|
47
|
+
// Cooperative copy of `rows` rows of `row_bytes` bytes (a multiple of 16; every row start 16-byte aligned on both
|
|
48
|
+
// sides): row r from g + r * g_stride to s + r * s_stride (bytes), the 16-byte pieces dealt round-robin over the threads
|
|
49
|
+
// tid = 0 .. nthr - 1 of the copying group. Issues only: the caller commits.
|
|
50
|
+
__device__ __forceinline__ void copy_rows16(void* s, int s_stride, const void* g, int64_t g_stride, int rows,
|
|
51
|
+
int row_bytes, int tid, int nthr) {
|
|
52
|
+
const int per = row_bytes >> 4;
|
|
53
|
+
for (int i = tid; i < rows * per; i += nthr) {
|
|
54
|
+
const int r = i / per;
|
|
55
|
+
const int c = (i - r * per) << 4;
|
|
56
|
+
cp16(static_cast<char*>(s) + r * s_stride + c, static_cast<const char*>(g) + r * g_stride + c);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Every thread of the CTA: its own groups have landed, then the CTA barrier publishes every copier's bytes.
|
|
60
|
+
__device__ __forceinline__ void publish_cta() {
|
|
61
|
+
wait_all();
|
|
62
|
+
__syncthreads();
|
|
63
|
+
}
|
|
64
|
+
// The same for the nthr threads (a multiple of 32) that use named barrier `id` (1..15; 0 is __syncthreads').
|
|
65
|
+
__device__ __forceinline__ void publish_named(int id, int nthr) {
|
|
66
|
+
wait_all();
|
|
67
|
+
asm volatile("bar.sync %0, %1;\n" ::"r"(id), "r"(nthr) : "memory");
|
|
68
|
+
}
|
|
69
|
+
// mbarrier forms (sm_80): init with the number of copying threads; arrive_on = one arrival of this thread that the
|
|
70
|
+
// system makes when ALL of its prior cp.async copies have landed (.noinc: it counts against the init count); consumers
|
|
71
|
+
// spin on test_parity(bar, phase & 1).
|
|
72
|
+
__device__ __forceinline__ void mbar_init(uint64_t* bar, uint32_t count) {
|
|
73
|
+
SDMCP_ASM("mbarrier.init.shared.b64 [%0], %1;\n" ::"r"(saddr(bar)), "r"(count) : "memory");
|
|
74
|
+
}
|
|
75
|
+
__device__ __forceinline__ void arrive_on(uint64_t* bar) {
|
|
76
|
+
SDMCP_ASM("cp.async.mbarrier.arrive.noinc.shared.b64 [%0];\n" ::"r"(saddr(bar)) : "memory");
|
|
77
|
+
}
|
|
78
|
+
__device__ __forceinline__ bool test_parity(uint64_t* bar, uint32_t parity) {
|
|
79
|
+
uint32_t done = 0u;
|
|
80
|
+
#if __CUDA_ARCH__ >= 800
|
|
81
|
+
asm volatile("{\n .reg .pred p;\n mbarrier.test_wait.parity.shared.b64 p, [%1], %2;\n selp.u32 %0, 1, 0, p;\n}\n"
|
|
82
|
+
: "=r"(done) : "r"(saddr(bar)), "r"(parity) : "memory");
|
|
83
|
+
#else
|
|
84
|
+
__trap();
|
|
85
|
+
#endif
|
|
86
|
+
return done != 0u;
|
|
87
|
+
}
|
|
88
|
+
} // namespace sdmcp
|
|
89
|
+
#undef SDMCP_ASM
|
|
90
|
+
#endif // SDM_PORTABLE_ARCH
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// sdm_plancap.cuh: launch capture for the backward's per-step plans (host code only: no device code).
|
|
2
|
+
//
|
|
3
|
+
// A plan builder runs a per-step launcher's own host function ONCE, at the step loop's largest step, with this thread's
|
|
4
|
+
// capture slot set: every check of the host function runs as for a real call, and its single kernel launch is RECORDED
|
|
5
|
+
// instead of issued -- the kernel (its host stub address), grid, block, dynamic shared memory and every parameter by
|
|
6
|
+
// value, converted to the kernel's own parameter type as <<<>>> and cudaLaunchKernelEx(&cfg, kernel, args...) convert the
|
|
7
|
+
// arguments. A plan's step launch patches only the parameters that vary per step (the step index, row offsets, valid
|
|
8
|
+
// tokens) and issues the launch with cudaLaunchKernel (what <<<>>> issues) or, with PDL, with cudaLaunchKernelExC and
|
|
9
|
+
// programmatic stream serialization (what cudaLaunchKernelEx(&cfg, kernel, args...) issues with that attribute): the
|
|
10
|
+
// same kernel, configuration, parameters and stream as the host function's own launch.
|
|
11
|
+
#pragma once
|
|
12
|
+
#include <ATen/ATen.h>
|
|
13
|
+
#include <cuda_runtime.h>
|
|
14
|
+
#include <c10/util/Exception.h>
|
|
15
|
+
#include <cstdint>
|
|
16
|
+
#include <cstring>
|
|
17
|
+
#include <type_traits>
|
|
18
|
+
#include <utility>
|
|
19
|
+
|
|
20
|
+
namespace sdm_plancap {
|
|
21
|
+
|
|
22
|
+
constexpr int kMaxParams = 32;
|
|
23
|
+
constexpr int kBufBytes = 768;
|
|
24
|
+
|
|
25
|
+
struct Launch {
|
|
26
|
+
int32_t set; // 1 once the planned call recorded its launch (0: it launched nothing)
|
|
27
|
+
int32_t nparams;
|
|
28
|
+
const void* fn; // the kernel's host stub address (what <<<>>> passes to cudaLaunchKernel)
|
|
29
|
+
uint32_t grid[3];
|
|
30
|
+
uint32_t block[3];
|
|
31
|
+
uint64_t smem;
|
|
32
|
+
int32_t off[kMaxParams]; // byte offset of parameter i in buf
|
|
33
|
+
int32_t size[kMaxParams]; // sizeof(parameter i's type)
|
|
34
|
+
alignas(16) unsigned char buf[kBufBytes];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// This thread's capture slot: non-null only while a plan builder runs its host function.
|
|
38
|
+
inline Launch*& slot() {
|
|
39
|
+
static thread_local Launch* p = nullptr;
|
|
40
|
+
return p;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
template <typename T>
|
|
44
|
+
inline void put(Launch& c, int& pos, const T& v) {
|
|
45
|
+
static_assert(std::is_trivially_copyable<T>::value, "plan capture: kernel parameters must be trivially copyable");
|
|
46
|
+
constexpr int al = alignof(T) < 16 ? static_cast<int>(alignof(T)) : 16;
|
|
47
|
+
pos = (pos + al - 1) / al * al;
|
|
48
|
+
TORCH_CHECK(c.nparams < kMaxParams && pos + static_cast<int>(sizeof(T)) <= kBufBytes,
|
|
49
|
+
"plan capture: too many kernel parameter bytes");
|
|
50
|
+
std::memcpy(c.buf + pos, &v, sizeof(T));
|
|
51
|
+
c.off[c.nparams] = pos;
|
|
52
|
+
c.size[c.nparams] = static_cast<int32_t>(sizeof(T));
|
|
53
|
+
++c.nparams;
|
|
54
|
+
pos += static_cast<int>(sizeof(T));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Inside a plan builder: record the launch (each argument converted to the kernel's parameter type) and return true; the
|
|
58
|
+
// host function then returns without launching. Outside one: false (the host function launches as it always did).
|
|
59
|
+
template <typename... P, typename... A>
|
|
60
|
+
inline bool take(void (*kernel)(P...), dim3 grid, dim3 block, size_t smem, A&&... args) {
|
|
61
|
+
static_assert(sizeof...(P) == sizeof...(A), "plan capture: argument count != kernel parameter count");
|
|
62
|
+
Launch* c = slot();
|
|
63
|
+
if (c == nullptr) return false;
|
|
64
|
+
TORCH_CHECK(c->set == 0, "plan capture: the planned call launched more than one kernel");
|
|
65
|
+
c->set = 1;
|
|
66
|
+
c->nparams = 0;
|
|
67
|
+
c->fn = reinterpret_cast<const void*>(kernel);
|
|
68
|
+
c->grid[0] = grid.x; c->grid[1] = grid.y; c->grid[2] = grid.z;
|
|
69
|
+
c->block[0] = block.x; c->block[1] = block.y; c->block[2] = block.z;
|
|
70
|
+
c->smem = static_cast<uint64_t>(smem);
|
|
71
|
+
int pos = 0;
|
|
72
|
+
(put<typename std::decay<P>::type>(*c, pos, static_cast<typename std::decay<P>::type>(std::forward<A>(args))), ...);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Parameter i of a captured launch as type T (nullptr when the recorded size differs: the caller refuses the plan).
|
|
77
|
+
template <typename T>
|
|
78
|
+
inline T* param(Launch& c, int i) {
|
|
79
|
+
if (i < 0 || i >= c.nparams || c.size[i] != static_cast<int32_t>(sizeof(T))) return nullptr;
|
|
80
|
+
return reinterpret_cast<T*>(c.buf + c.off[i]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Issue a captured launch on `stream`: cudaLaunchKernel (= <<<>>>), or with pdl cudaLaunchKernelExC + programmatic stream
|
|
84
|
+
// serialization (= cudaLaunchKernelEx(&cfg, kernel, args...) with that attribute). Returns 0 or a cudaError_t (the
|
|
85
|
+
// launch's, else cudaGetLastError()'s right after it, as C10_CUDA_KERNEL_LAUNCH_CHECK probes); an error state is consumed.
|
|
86
|
+
inline int64_t launch(const Launch& c, bool pdl, cudaStream_t stream) {
|
|
87
|
+
void* params[kMaxParams];
|
|
88
|
+
for (int i = 0; i < c.nparams; ++i) params[i] = const_cast<unsigned char*>(c.buf) + c.off[i];
|
|
89
|
+
const dim3 grid(c.grid[0], c.grid[1], c.grid[2]);
|
|
90
|
+
const dim3 block(c.block[0], c.block[1], c.block[2]);
|
|
91
|
+
cudaError_t e;
|
|
92
|
+
if (pdl) {
|
|
93
|
+
cudaLaunchAttribute attr[1];
|
|
94
|
+
attr[0].id = cudaLaunchAttributeProgrammaticStreamSerialization;
|
|
95
|
+
attr[0].val.programmaticStreamSerializationAllowed = 1;
|
|
96
|
+
cudaLaunchConfig_t cfg = {};
|
|
97
|
+
cfg.gridDim = grid;
|
|
98
|
+
cfg.blockDim = block;
|
|
99
|
+
cfg.dynamicSmemBytes = static_cast<size_t>(c.smem);
|
|
100
|
+
cfg.stream = stream;
|
|
101
|
+
cfg.attrs = attr;
|
|
102
|
+
cfg.numAttrs = 1;
|
|
103
|
+
e = cudaLaunchKernelExC(&cfg, c.fn, params);
|
|
104
|
+
} else {
|
|
105
|
+
e = cudaLaunchKernel(c.fn, grid, block, params, static_cast<size_t>(c.smem), stream);
|
|
106
|
+
}
|
|
107
|
+
if (e == cudaSuccess) e = cudaGetLastError();
|
|
108
|
+
else (void)cudaGetLastError();
|
|
109
|
+
return static_cast<int64_t>(e);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// RAII: set this thread's slot to `c` for one planned call (refuses nesting).
|
|
113
|
+
struct Scope {
|
|
114
|
+
explicit Scope(Launch* c) {
|
|
115
|
+
TORCH_CHECK(slot() == nullptr, "plan capture: nested plan builders");
|
|
116
|
+
std::memset(c, 0, sizeof(Launch));
|
|
117
|
+
slot() = c;
|
|
118
|
+
}
|
|
119
|
+
~Scope() { slot() = nullptr; }
|
|
120
|
+
Scope(const Scope&) = delete;
|
|
121
|
+
Scope& operator=(const Scope&) = delete;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// The current device, or -1 (error state consumed).
|
|
125
|
+
inline int current_device() {
|
|
126
|
+
int dev = -1;
|
|
127
|
+
if (cudaGetDevice(&dev) != cudaSuccess) {
|
|
128
|
+
(void)cudaGetLastError();
|
|
129
|
+
return -1;
|
|
130
|
+
}
|
|
131
|
+
return dev;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// A plan as a CPU uint8 tensor (the bytes of the trivially copyable plan struct), and back.
|
|
135
|
+
template <typename T>
|
|
136
|
+
inline at::Tensor to_tensor(const T& pl) {
|
|
137
|
+
static_assert(std::is_trivially_copyable<T>::value, "plan: trivially copyable");
|
|
138
|
+
at::Tensor t = at::empty({static_cast<int64_t>(sizeof(T))}, at::TensorOptions().dtype(at::kByte));
|
|
139
|
+
std::memcpy(t.data_ptr<uint8_t>(), &pl, sizeof(T));
|
|
140
|
+
return t;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
template <typename T>
|
|
144
|
+
inline bool from_bytes(T& pl, const void* bytes, int64_t n, uint64_t magic) {
|
|
145
|
+
if (n != static_cast<int64_t>(sizeof(T))) return false;
|
|
146
|
+
std::memcpy(&pl, bytes, sizeof(T));
|
|
147
|
+
return pl.magic == magic;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
} // namespace sdm_plancap
|