liangsu9988 commited on
Commit
77368d6
·
verified ·
1 Parent(s): c7aafa3

Publish GROOT N1.7 Thor kernel sources

Browse files
CARD.md ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ tags: [kernel, cuda, attention, inference, cuda-graphs]
3
+ library_name: kernels
4
+ license: apache-2.0
5
+ ---
6
+
7
+ # Masked MHA Runtime
8
+
9
+ Allocation-free FP16/BF16 attention that masks padded logits inside softmax,
10
+ removing the per-call `-inf` pre-fill. BF16 accepts fused-QKV token strides.
11
+
12
+ ## API
13
+
14
+ - `forward(q, k, v, *, scale=None)`
15
+ - `forward_static(q, k, v, *, logits, out, scale=None)`
16
+ - `allocate_workspace(q, k)`
17
+
18
+ Inputs use `(sequence, heads, head_dim)`. `forward_static` is the CUDA Graph
19
+ hot-path API: allocate `logits` and `out` once and reuse their addresses.
20
+ Rows wider than 1024 keys use a deterministic multi-pass softmax.
21
+
22
+ This package contains the native masked-MHA execution path validated in
23
+ FlashRT's GROOT N1.7 Thor runtime. It is separate from FlashAttention-4.
README.md ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # masked-mha-runtime
2
+
3
+ FlashRT native SM110 masked FP16/BF16 MHA for fixed-shape CUDA Graph runtimes.
4
+ It masks padded logits during softmax, supports fused-QKV token strides, and
5
+ keeps caller-owned logits/output buffers stable across graph replay.
6
+
7
+ ```python
8
+ from kernels import get_kernel
9
+ import torch
10
+
11
+ ops = get_kernel("flashrt/masked-mha-runtime", version=1)
12
+ logits = ops.allocate_workspace(q, k)
13
+ out = torch.empty_like(q, memory_format=torch.contiguous_format)
14
+ ops.forward_static(q, k, v, logits=logits, out=out)
15
+ ```
16
+
17
+ Public functions are `allocate_workspace`, `forward_static`, and `forward`.
18
+ Inputs use `(sequence, heads, head_dim)`. The production GROOT gate covers
19
+ DiT `(41, 32, 48)`, ViT/LLM sequence lengths `277/1024`, padded boundaries
20
+ `1025/2048`, FP16 and BF16, fused strides, and bitwise CUDA Graph replay.
21
+
22
+ See [CARD.md](CARD.md) for the complete contract. Source provenance is FlashRT
23
+ commit `24df793f4fa2d50780aea03b644208c6e0cb4162`.
SYNC.md ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # Source sync
2
+
3
+ - Upstream: `flashrt-project/FlashRT`
4
+ - Commit: `24df793f4fa2d50780aea03b644208c6e0cb4162`
5
+ - Source: `csrc/kernels/attention_mha_masked.cu`
6
+ - Local changes: Tensor validation and `torch.library` binding only.
VALIDATION.md ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # Validation
2
+
3
+ Release requires FP16 and BF16 parity against PyTorch SDPA at key lengths
4
+ 41, 277, 1024, 1025, and 2048; poisoned padded scratch; CUDA Graph replay;
5
+ source and installed-artifact runs on SM110; and native-vs-package timing.
benchmarks/README.md ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ # Benchmark
2
+
3
+ Benchmark the installed artifact against PyTorch SDPA and the matching
4
+ FlashRT native symbol on the same Thor host. Report both static-buffer paths.
benchmarks/RESULTS.md ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # Results
2
+
3
+ Pending the clean SM110 installed-artifact benchmark. No speedup claim is
4
+ published until the package is measured against both PyTorch SDPA and the
5
+ FlashRT native symbol on the same Thor host.
build.toml ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [general]
2
+ name = "masked-mha-runtime"
3
+ license = "Apache-2.0"
4
+ version = 1
5
+ backends = ["cuda"]
6
+
7
+ [general.cuda]
8
+ minver = "13"
9
+
10
+ [general.hub]
11
+ repo-id = "flashrt/masked-mha-runtime"
12
+
13
+ [torch]
14
+ include = ["csrc"]
15
+ src = ["torch-ext/torch_binding.cpp"]
16
+
17
+ [kernel.masked_mha_runtime]
18
+ backend = "cuda"
19
+ depends = ["torch"]
20
+ include = ["csrc"]
21
+ cuda-minver = "13"
22
+ cuda-capabilities = ["11.0a"]
23
+ cuda-flags = ["-O3", "--use_fast_math"]
24
+ src = ["csrc/attention_mha_masked.cu", "csrc/attention_mha_masked.cuh"]
csrc/attention_mha_masked.cu ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ============================================================================
2
+ // FlashRT — MHA attention without the -inf logits pre-fill.
3
+ //
4
+ // The plain ``attention_mha_{fp16,bf16}`` kernels softmax over the padded
5
+ // logits width, so callers must pre-fill the whole (NH, max_q, max_kv)
6
+ // scratch with -inf every invocation — a full DRAM sweep per layer. These
7
+ // variants run a column-masked softmax that reads and writes only the
8
+ // valid S_kv columns (row stride = padded width); the PV GEMM already
9
+ // uses k = S_kv, so the padding is never read anywhere and the pre-fill
10
+ // disappears.
11
+ //
12
+ // Any S_kv is supported: rows up to SMM_MAX_COLS use a register-tiled
13
+ // softmax sized to the row, wider rows fall back to a multi-pass kernel
14
+ // that holds no per-column registers.
15
+ //
16
+ // Additive: new symbols only.
17
+ // ============================================================================
18
+ #include <cuda_runtime.h>
19
+ #include <cuda_fp16.h>
20
+ #include <cuda_bf16.h>
21
+ #include <cublas_v2.h>
22
+
23
+ #define SMM_WARP 32
24
+ #define SMM_MAX_COLS 1024
25
+ #define SMM_ITERS (SMM_MAX_COLS / SMM_WARP)
26
+
27
+ namespace {
28
+
29
+ template <typename T>
30
+ __device__ __forceinline__ float to_f(T v);
31
+ template <>
32
+ __device__ __forceinline__ float to_f<__half>(__half v) { return __half2float(v); }
33
+ template <>
34
+ __device__ __forceinline__ float to_f<__nv_bfloat16>(__nv_bfloat16 v) { return __bfloat162float(v); }
35
+
36
+ template <typename T>
37
+ __device__ __forceinline__ T from_f(float v);
38
+ template <>
39
+ __device__ __forceinline__ __half from_f<__half>(float v) { return __float2half(v); }
40
+ template <>
41
+ __device__ __forceinline__ __nv_bfloat16 from_f<__nv_bfloat16>(float v) { return __float2bfloat16(v); }
42
+
43
+ // Row-wise softmax over the first ``cols_valid`` of each ``cols_pad``-wide
44
+ // row. Padding columns are neither read nor written.
45
+ //
46
+ // ITERS is the per-thread register tile, dispatched from the actual valid
47
+ // column count: a 41-key DiT self-attention row needs 2 registers, not the
48
+ // 32 a worst-case 1024-key row would. Sizing it per call keeps the loops
49
+ // fully unrolled without paying occupancy for columns that do not exist.
50
+ template <typename T, int ITERS>
51
+ __global__ void softmax_masked_kernel(T* data, int rows, int cols_pad,
52
+ int cols_valid) {
53
+ const int lane = threadIdx.x % SMM_WARP;
54
+ const int row = blockIdx.x;
55
+ if (row >= rows) return;
56
+
57
+ T* src = data + (long)row * cols_pad;
58
+
59
+ float reg[ITERS];
60
+ float mx = -1e30f;
61
+ #pragma unroll
62
+ for (int it = 0; it < ITERS; ++it) {
63
+ const int c = it * SMM_WARP + lane;
64
+ if (c < cols_valid) {
65
+ reg[it] = to_f<T>(src[c]);
66
+ mx = fmaxf(mx, reg[it]);
67
+ } else {
68
+ reg[it] = -1e30f;
69
+ }
70
+ }
71
+ #pragma unroll
72
+ for (int o = 16; o > 0; o >>= 1)
73
+ mx = fmaxf(mx, __shfl_xor_sync(0xffffffffu, mx, o));
74
+
75
+ float sum = 0.f;
76
+ #pragma unroll
77
+ for (int it = 0; it < ITERS; ++it) {
78
+ const int c = it * SMM_WARP + lane;
79
+ if (c < cols_valid) {
80
+ reg[it] = __expf(reg[it] - mx);
81
+ sum += reg[it];
82
+ }
83
+ }
84
+ #pragma unroll
85
+ for (int o = 16; o > 0; o >>= 1)
86
+ sum += __shfl_xor_sync(0xffffffffu, sum, o);
87
+ const float inv = 1.0f / sum;
88
+
89
+ #pragma unroll
90
+ for (int it = 0; it < ITERS; ++it) {
91
+ const int c = it * SMM_WARP + lane;
92
+ if (c < cols_valid)
93
+ src[c] = from_f<T>(reg[it] * inv);
94
+ }
95
+ }
96
+
97
+ // Register-tiled variants above cap at SMM_MAX_COLS columns. Rows wider
98
+ // than that go through this multi-pass kernel instead: it keeps no
99
+ // per-column registers, so it is correct for any width. Three passes over
100
+ // the row (max, exp+sum, scale) make it slower than the tiled path, which
101
+ // is why it only runs past the tiled path's reach.
102
+ template <typename T>
103
+ __global__ void softmax_masked_wide_kernel(T* data, int rows, int cols_pad,
104
+ int cols_valid) {
105
+ const int lane = threadIdx.x % SMM_WARP;
106
+ const int row = blockIdx.x;
107
+ if (row >= rows) return;
108
+
109
+ T* src = data + (long)row * cols_pad;
110
+
111
+ float mx = -1e30f;
112
+ for (int c = lane; c < cols_valid; c += SMM_WARP)
113
+ mx = fmaxf(mx, to_f<T>(src[c]));
114
+ #pragma unroll
115
+ for (int o = 16; o > 0; o >>= 1)
116
+ mx = fmaxf(mx, __shfl_xor_sync(0xffffffffu, mx, o));
117
+
118
+ float sum = 0.f;
119
+ for (int c = lane; c < cols_valid; c += SMM_WARP) {
120
+ const float e = __expf(to_f<T>(src[c]) - mx);
121
+ src[c] = from_f<T>(e);
122
+ sum += e;
123
+ }
124
+ #pragma unroll
125
+ for (int o = 16; o > 0; o >>= 1)
126
+ sum += __shfl_xor_sync(0xffffffffu, sum, o);
127
+ const float inv = 1.0f / sum;
128
+
129
+ for (int c = lane; c < cols_valid; c += SMM_WARP)
130
+ src[c] = from_f<T>(to_f<T>(src[c]) * inv);
131
+ }
132
+
133
+ template <typename T>
134
+ inline void launch_softmax_masked(T* data, int rows, int cols_pad,
135
+ int cols_valid, cudaStream_t stream) {
136
+ const int iters = (cols_valid + SMM_WARP - 1) / SMM_WARP;
137
+ if (iters > SMM_ITERS) {
138
+ softmax_masked_wide_kernel<T><<<rows, SMM_WARP, 0, stream>>>(
139
+ data, rows, cols_pad, cols_valid);
140
+ return;
141
+ }
142
+ if (iters <= 2) {
143
+ softmax_masked_kernel<T, 2><<<rows, SMM_WARP, 0, stream>>>(
144
+ data, rows, cols_pad, cols_valid);
145
+ } else if (iters <= 4) {
146
+ softmax_masked_kernel<T, 4><<<rows, SMM_WARP, 0, stream>>>(
147
+ data, rows, cols_pad, cols_valid);
148
+ } else if (iters <= 8) {
149
+ softmax_masked_kernel<T, 8><<<rows, SMM_WARP, 0, stream>>>(
150
+ data, rows, cols_pad, cols_valid);
151
+ } else if (iters <= 16) {
152
+ softmax_masked_kernel<T, 16><<<rows, SMM_WARP, 0, stream>>>(
153
+ data, rows, cols_pad, cols_valid);
154
+ } else {
155
+ softmax_masked_kernel<T, SMM_ITERS><<<rows, SMM_WARP, 0, stream>>>(
156
+ data, rows, cols_pad, cols_valid);
157
+ }
158
+ }
159
+
160
+ } // namespace
161
+
162
+ extern "C" {
163
+
164
+ void attention_mha_fp16_masked(
165
+ cublasHandle_t handle,
166
+ const __half* Q, const __half* K, const __half* V,
167
+ __half* logits, __half* out,
168
+ int S_q, int S_kv, int NH, int HD,
169
+ float attn_scale, cudaStream_t stream) {
170
+ cublasSetStream(handle, stream);
171
+ const int S_kv_pad = ((S_kv + 7) / 8) * 8;
172
+ float zero = 0.0f, one = 1.0f;
173
+ const long long strideC = (long long)S_q * S_kv_pad;
174
+
175
+ cublasGemmStridedBatchedEx(handle,
176
+ CUBLAS_OP_T, CUBLAS_OP_N,
177
+ S_kv, S_q, HD,
178
+ &attn_scale,
179
+ K, CUDA_R_16F, NH * HD, (long long)HD,
180
+ Q, CUDA_R_16F, NH * HD, (long long)HD,
181
+ &zero,
182
+ logits, CUDA_R_16F, S_kv_pad, strideC,
183
+ NH,
184
+ CUBLAS_COMPUTE_32F, CUBLAS_GEMM_DEFAULT);
185
+
186
+ launch_softmax_masked<__half>(logits, NH * S_q, S_kv_pad, S_kv, stream);
187
+
188
+ cublasGemmStridedBatchedEx(handle,
189
+ CUBLAS_OP_N, CUBLAS_OP_N,
190
+ HD, S_q, S_kv,
191
+ &one,
192
+ V, CUDA_R_16F, NH * HD, (long long)HD,
193
+ logits, CUDA_R_16F, S_kv_pad, strideC,
194
+ &zero,
195
+ out, CUDA_R_16F, NH * HD, (long long)HD,
196
+ NH,
197
+ CUBLAS_COMPUTE_32F, CUBLAS_GEMM_DEFAULT);
198
+ }
199
+
200
+ void attention_mha_bf16_masked(
201
+ cublasHandle_t handle,
202
+ const __nv_bfloat16* Q, const __nv_bfloat16* K, const __nv_bfloat16* V,
203
+ __nv_bfloat16* logits, __nv_bfloat16* out,
204
+ int S_q, int S_kv, int NH, int HD,
205
+ float attn_scale, int logits_kv_stride, int qkv_token_stride,
206
+ cudaStream_t stream) {
207
+ cublasSetStream(handle, stream);
208
+ const int S_kv_pad = ((S_kv + 7) / 8) * 8;
209
+ const int kv_stride = (logits_kv_stride > 0) ? logits_kv_stride : S_kv_pad;
210
+ // Token stride (elements) of the Q/K/V sources. NH*HD for packed
211
+ // per-site buffers; 3*NH*HD lets the fused-QKV GEMM output be read in
212
+ // place (no split copies).
213
+ const int tstride = (qkv_token_stride > 0) ? qkv_token_stride : NH * HD;
214
+ float zero = 0.0f, one = 1.0f;
215
+ const long long strideC = (long long)S_q * kv_stride;
216
+
217
+ cublasGemmStridedBatchedEx(handle,
218
+ CUBLAS_OP_T, CUBLAS_OP_N,
219
+ S_kv, S_q, HD,
220
+ &attn_scale,
221
+ K, CUDA_R_16BF, tstride, (long long)HD,
222
+ Q, CUDA_R_16BF, tstride, (long long)HD,
223
+ &zero,
224
+ logits, CUDA_R_16BF, kv_stride, strideC,
225
+ NH,
226
+ CUBLAS_COMPUTE_32F, CUBLAS_GEMM_DEFAULT);
227
+
228
+ launch_softmax_masked<__nv_bfloat16>(logits, NH * S_q, kv_stride, S_kv,
229
+ stream);
230
+
231
+ cublasGemmStridedBatchedEx(handle,
232
+ CUBLAS_OP_N, CUBLAS_OP_N,
233
+ HD, S_q, S_kv,
234
+ &one,
235
+ V, CUDA_R_16BF, tstride, (long long)HD,
236
+ logits, CUDA_R_16BF, kv_stride, strideC,
237
+ &zero,
238
+ out, CUDA_R_16BF, NH * HD, (long long)HD,
239
+ NH,
240
+ CUBLAS_COMPUTE_32F, CUBLAS_GEMM_DEFAULT);
241
+ }
242
+
243
+ } // extern "C"
csrc/attention_mha_masked.cuh ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+
3
+ #include <cublas_v2.h>
4
+ #include <cuda_bf16.h>
5
+ #include <cuda_fp16.h>
6
+ #include <cuda_runtime.h>
7
+
8
+ extern "C" {
9
+
10
+ void attention_mha_fp16_masked(
11
+ cublasHandle_t handle, const __half* q, const __half* k,
12
+ const __half* v, __half* logits, __half* out, int sequence_q,
13
+ int sequence_kv, int heads, int head_dim, float scale,
14
+ cudaStream_t stream);
15
+ void attention_mha_bf16_masked(
16
+ cublasHandle_t handle, const __nv_bfloat16* q,
17
+ const __nv_bfloat16* k, const __nv_bfloat16* v,
18
+ __nv_bfloat16* logits, __nv_bfloat16* out, int sequence_q,
19
+ int sequence_kv, int heads, int head_dim, float scale,
20
+ int logits_kv_stride, int qkv_token_stride, cudaStream_t stream);
21
+
22
+ }
examples/basic_usage.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from kernels import get_kernel
3
+
4
+ mha = get_kernel("flashrt/masked-mha-runtime", version=1)
5
+ q = torch.randn((41, 32, 48), device="cuda", dtype=torch.bfloat16)
6
+ k = torch.randn_like(q)
7
+ v = torch.randn_like(q)
8
+ logits = mha.allocate_workspace(q, k)
9
+ out = torch.empty_like(q)
10
+ mha.forward_static(q, k, v, logits=logits, out=out)
flake.nix ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ description = "Flake for FlashRT masked MHA runtime kernels";
3
+ inputs.kernel-builder.url = "github:huggingface/kernels/870e825d881664e39f9287a27a74ef63ff3c545e";
4
+ outputs = { self, kernel-builder }:
5
+ kernel-builder.lib.genKernelFlakeOutputs { inherit self; path = ./.; };
6
+ }
tests/test_masked_mha_runtime.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Strict source and installed-artifact tests for masked-mha-runtime."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import importlib
8
+ import os
9
+ import re
10
+ import subprocess
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ import torch
15
+
16
+
17
+ ROOT = Path(__file__).resolve().parents[2]
18
+ PACKAGE = ROOT / "masked-mha-runtime"
19
+ REGISTRATION_INCLUDE = (
20
+ ROOT.parent / "kernels" / "kernel-builder" / "src" / "pyproject"
21
+ / "templates" / "torch"
22
+ )
23
+
24
+
25
+ class SourceOps:
26
+ def __init__(self, namespace: str):
27
+ self.ops = getattr(torch.ops, namespace)
28
+
29
+ @staticmethod
30
+ def allocate_workspace(q, k):
31
+ stride = (k.shape[0] + 7) // 8 * 8
32
+ return torch.empty(
33
+ (q.shape[1], q.shape[0], stride), device=q.device, dtype=q.dtype
34
+ )
35
+
36
+ def forward_static(self, q, k, v, *, logits, out, scale=None):
37
+ scale = q.shape[-1] ** -0.5 if scale is None else scale
38
+ self.ops.forward_static(q, k, v, logits, out, float(scale))
39
+ return out
40
+
41
+
42
+ def load_source_ops():
43
+ from torch.utils.cpp_extension import load
44
+
45
+ nvcc = subprocess.check_output(
46
+ ["nvcc", "--version"], text=True
47
+ )
48
+ match = re.search(r"release\s+(\d+)\.", nvcc)
49
+ torch_cuda_major = int(torch.version.cuda.split(".", 1)[0])
50
+ if match and int(match.group(1)) != torch_cuda_major:
51
+ raise RuntimeError(
52
+ "source test requires PyTorch and nvcc from the same CUDA major; "
53
+ f"torch={torch.version.cuda}, nvcc={match.group(1)}.x. Use the "
54
+ "installed artifact or a matching isolated build environment."
55
+ )
56
+
57
+ major, minor = torch.cuda.get_device_capability(0)
58
+ os.environ.setdefault("TORCH_CUDA_ARCH_LIST", f"{major}.{minor}")
59
+ namespace = "masked_mha_runtime_source_test"
60
+ load(
61
+ name=namespace,
62
+ sources=[
63
+ str(PACKAGE / "torch-ext" / "torch_binding.cpp"),
64
+ str(PACKAGE / "csrc" / "attention_mha_masked.cu"),
65
+ ],
66
+ extra_include_paths=[str(PACKAGE / "csrc"), str(REGISTRATION_INCLUDE)],
67
+ extra_cflags=["-O3", "-DCUDA_KERNEL"],
68
+ extra_cuda_cflags=["-O3", "--use_fast_math", "-DCUDA_KERNEL"],
69
+ extra_ldflags=["-lcublas"],
70
+ is_python_module=False,
71
+ verbose=False,
72
+ )
73
+ return SourceOps(namespace)
74
+
75
+
76
+ def load_installed_ops(artifact):
77
+ if artifact:
78
+ sys.path.insert(0, artifact)
79
+ try:
80
+ return importlib.import_module("masked_mha_runtime")
81
+ finally:
82
+ if artifact:
83
+ sys.path.remove(artifact)
84
+
85
+
86
+ def metrics(got, ref):
87
+ diff = (got.float() - ref.float()).abs()
88
+ cosine = torch.nn.functional.cosine_similarity(
89
+ got.float().flatten(), ref.float().flatten(), dim=0
90
+ ).item()
91
+ return float(diff.max()), float(torch.quantile(diff.flatten(), 0.99)), float(cosine)
92
+
93
+
94
+ def run_case(ops, dtype, sq, sk, heads, dim, fused_stride=False):
95
+ torch.manual_seed(1000 + sq + sk + dim)
96
+ if fused_stride:
97
+ packed = torch.randn((sk, 3, heads, dim), device="cuda", dtype=dtype)
98
+ q = packed[:sq, 0]
99
+ k = packed[:, 1]
100
+ v = packed[:, 2]
101
+ else:
102
+ q = torch.randn((sq, heads, dim), device="cuda", dtype=dtype)
103
+ k = torch.randn((sk, heads, dim), device="cuda", dtype=dtype)
104
+ v = torch.randn_like(k)
105
+ logits = ops.allocate_workspace(q, k)
106
+ logits.fill_(float("nan"))
107
+ out = torch.empty_like(q, memory_format=torch.contiguous_format)
108
+ got = ops.forward_static(q, k, v, logits=logits, out=out)
109
+ torch.cuda.synchronize()
110
+ ref = torch.nn.functional.scaled_dot_product_attention(
111
+ q.permute(1, 0, 2).unsqueeze(0).float(),
112
+ k.permute(1, 0, 2).unsqueeze(0).float(),
113
+ v.permute(1, 0, 2).unsqueeze(0).float(),
114
+ ).squeeze(0).permute(1, 0, 2).to(dtype)
115
+ max_abs, p99_abs, cosine = metrics(got, ref)
116
+ atol = 0.00390625 if dtype is torch.float16 else 0.015625
117
+ if not torch.isfinite(got.float()).all() or cosine < 0.999 or p99_abs > atol:
118
+ raise AssertionError(
119
+ f"dtype={dtype} sq={sq} sk={sk} h={heads} d={dim}: "
120
+ f"max={max_abs} p99={p99_abs} cos={cosine}"
121
+ )
122
+
123
+ # Preserve fused-QKV token strides. Cloning each view separately can
124
+ # normalize a size-one query dimension and destroy the shared stride.
125
+ static_q = q
126
+ static_k = k
127
+ static_v = v
128
+ graph = torch.cuda.CUDAGraph()
129
+ torch.cuda.synchronize()
130
+ with torch.cuda.graph(graph):
131
+ ops.forward_static(
132
+ static_q, static_k, static_v, logits=logits, out=out
133
+ )
134
+ graph.replay()
135
+ first = out.clone()
136
+ graph.replay()
137
+ torch.cuda.synchronize()
138
+ if not torch.equal(out, first):
139
+ raise AssertionError("CUDA Graph replay is not bitwise deterministic")
140
+ print(
141
+ f"PASS {dtype} sq={sq} sk={sk} h={heads} d={dim} "
142
+ f"fused_stride={fused_stride} max={max_abs:.6f} "
143
+ f"p99={p99_abs:.6f} cos={cosine:.8f}"
144
+ )
145
+
146
+
147
+ def main():
148
+ parser = argparse.ArgumentParser()
149
+ parser.add_argument("--backend", choices=["source", "installed"], default="source")
150
+ parser.add_argument("--artifact")
151
+ parser.add_argument("--mode", choices=["smoke", "full"], default="smoke")
152
+ args = parser.parse_args()
153
+ if not torch.cuda.is_available():
154
+ raise SystemExit("CUDA is required")
155
+ ops = load_source_ops() if args.backend == "source" else load_installed_ops(args.artifact)
156
+ cases = [
157
+ (torch.float16, 41, 41, 32, 48, False),
158
+ (torch.bfloat16, 41, 41, 32, 48, True),
159
+ ]
160
+ if args.mode == "full":
161
+ cases.extend([
162
+ (torch.float16, 1, 277, 16, 128, False),
163
+ (torch.bfloat16, 1, 1024, 1, 16, True),
164
+ (torch.bfloat16, 1, 1025, 1, 16, True),
165
+ (torch.bfloat16, 1, 2048, 1, 16, True),
166
+ ])
167
+ for case in cases:
168
+ run_case(ops, *case)
169
+ print(f"masked-mha-runtime {args.backend} {args.mode}: passed {len(cases)}/{len(cases)}")
170
+
171
+
172
+ if __name__ == "__main__":
173
+ main()
torch-ext/masked_mha_runtime/__init__.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Allocation-free masked MHA runtime operators."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from typing import Optional
7
+
8
+ import torch
9
+
10
+ from ._ops import add_op_namespace_prefix, ops
11
+
12
+
13
+ @torch.library.register_fake(add_op_namespace_prefix("forward_static"))
14
+ def _forward_static_fake(q, k, v, logits, out, scale: float) -> None:
15
+ del scale
16
+ if q.dim() != 3 or k.dim() != 3 or v.dim() != 3:
17
+ raise RuntimeError("q/k/v must have shape (sequence, heads, head_dim)")
18
+ if out.shape != q.shape:
19
+ raise RuntimeError("out must match q")
20
+ if logits.dim() != 3 or logits.shape[:2] != (q.shape[1], q.shape[0]):
21
+ raise RuntimeError("logits must have shape (heads, sequence_q, stride)")
22
+ if logits.shape[2] < k.shape[0]:
23
+ raise RuntimeError("logits stride must cover sequence_kv")
24
+
25
+
26
+ def allocate_workspace(q: torch.Tensor, k: torch.Tensor) -> torch.Tensor:
27
+ """Allocate padded logits scratch once, outside the hot path."""
28
+ stride = (k.shape[0] + 7) // 8 * 8
29
+ return torch.empty(
30
+ (q.shape[1], q.shape[0], stride), device=q.device, dtype=q.dtype
31
+ )
32
+
33
+
34
+ def forward_static(
35
+ q: torch.Tensor,
36
+ k: torch.Tensor,
37
+ v: torch.Tensor,
38
+ *,
39
+ logits: torch.Tensor,
40
+ out: torch.Tensor,
41
+ scale: Optional[float] = None,
42
+ ) -> torch.Tensor:
43
+ """Run MHA without pre-filling padded logits; all buffers are caller-owned."""
44
+ if scale is None:
45
+ scale = 1.0 / math.sqrt(q.shape[-1])
46
+ ops.forward_static(q, k, v, logits, out, float(scale))
47
+ return out
48
+
49
+
50
+ def forward(q, k, v, *, scale: Optional[float] = None):
51
+ """Convenience allocation wrapper; use ``forward_static`` in hot paths."""
52
+ logits = allocate_workspace(q, k)
53
+ out = torch.empty_like(q, memory_format=torch.contiguous_format)
54
+ return forward_static(q, k, v, logits=logits, out=out, scale=scale)
55
+
56
+
57
+ __all__ = ["allocate_workspace", "forward", "forward_static"]
torch-ext/torch_binding.cpp ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include <torch/all.h>
2
+ #include <torch/library.h>
3
+
4
+ #include <ATen/cuda/CUDAContext.h>
5
+ #include <c10/cuda/CUDAGuard.h>
6
+ #include <c10/cuda/CUDAException.h>
7
+
8
+ #include <limits>
9
+
10
+ #include "attention_mha_masked.cuh"
11
+ #include "registration.h"
12
+
13
+ namespace {
14
+
15
+ int checked_int(int64_t value, const char* name) {
16
+ TORCH_CHECK(value > 0 && value <= std::numeric_limits<int>::max(),
17
+ name, " must fit in a positive int");
18
+ return static_cast<int>(value);
19
+ }
20
+
21
+ void check_qkv(torch::Tensor const& tensor, const char* name,
22
+ c10::ScalarType dtype) {
23
+ TORCH_CHECK(tensor.is_cuda(), name, " must be CUDA");
24
+ TORCH_CHECK(tensor.scalar_type() == dtype, name, " has the wrong dtype");
25
+ TORCH_CHECK(tensor.dim() == 3, name, " must have shape (S, H, D)");
26
+ TORCH_CHECK(tensor.stride(2) == 1 && tensor.stride(1) == tensor.size(2),
27
+ name, " must be contiguous within each token");
28
+ }
29
+
30
+ void masked_mha_forward_static(
31
+ torch::Tensor const& q, torch::Tensor const& k, torch::Tensor const& v,
32
+ torch::Tensor& logits, torch::Tensor& out, double scale) {
33
+ TORCH_CHECK(q.scalar_type() == torch::kFloat16 ||
34
+ q.scalar_type() == torch::kBFloat16,
35
+ "q must be FP16 or BF16");
36
+ check_qkv(q, "q", q.scalar_type());
37
+ check_qkv(k, "k", q.scalar_type());
38
+ check_qkv(v, "v", q.scalar_type());
39
+ TORCH_CHECK(q.size(1) == k.size(1) && q.size(1) == v.size(1) &&
40
+ q.size(2) == k.size(2) && q.size(2) == v.size(2) &&
41
+ k.size(0) == v.size(0),
42
+ "q/k/v head shapes must match");
43
+ TORCH_CHECK(q.get_device() == k.get_device() &&
44
+ q.get_device() == v.get_device(),
45
+ "q/k/v must be on the same device");
46
+ TORCH_CHECK(out.is_cuda() && out.is_contiguous() &&
47
+ out.scalar_type() == q.scalar_type() &&
48
+ out.sizes() == q.sizes(),
49
+ "out must be contiguous and match q");
50
+ TORCH_CHECK(logits.is_cuda() && logits.scalar_type() == q.scalar_type() &&
51
+ logits.dim() == 3 && logits.size(0) == q.size(1) &&
52
+ logits.size(1) == q.size(0) &&
53
+ logits.size(2) >= k.size(0) && logits.stride(2) == 1,
54
+ "logits must have shape (H, S_q, stride >= S_kv)");
55
+ TORCH_CHECK(logits.get_device() == q.get_device() &&
56
+ out.get_device() == q.get_device(),
57
+ "outputs must be on the q device");
58
+ TORCH_CHECK(logits.stride(1) == logits.size(2) &&
59
+ logits.stride(0) == logits.size(1) * logits.size(2),
60
+ "logits must use a dense padded row stride");
61
+
62
+ c10::cuda::CUDAGuard guard(q.device());
63
+ auto stream = at::cuda::getCurrentCUDAStream(q.get_device()).stream();
64
+ auto handle = at::cuda::getCurrentCUDABlasHandle();
65
+ const int sq = checked_int(q.size(0), "S_q");
66
+ const int sk = checked_int(k.size(0), "S_kv");
67
+ const int heads = checked_int(q.size(1), "heads");
68
+ const int dim = checked_int(q.size(2), "head_dim");
69
+
70
+ if (q.scalar_type() == torch::kFloat16) {
71
+ TORCH_CHECK(q.stride(0) == heads * dim &&
72
+ k.stride(0) == heads * dim &&
73
+ v.stride(0) == heads * dim,
74
+ "FP16 q/k/v must be contiguous across tokens");
75
+ attention_mha_fp16_masked(
76
+ handle, static_cast<const __half*>(q.data_ptr()),
77
+ static_cast<const __half*>(k.data_ptr()),
78
+ static_cast<const __half*>(v.data_ptr()),
79
+ static_cast<__half*>(logits.data_ptr()),
80
+ static_cast<__half*>(out.data_ptr()), sq, sk, heads, dim,
81
+ static_cast<float>(scale), stream);
82
+ } else {
83
+ TORCH_CHECK(q.stride(0) == k.stride(0) && q.stride(0) == v.stride(0),
84
+ "BF16 q/k/v must share one token stride");
85
+ attention_mha_bf16_masked(
86
+ handle, static_cast<const __nv_bfloat16*>(q.data_ptr()),
87
+ static_cast<const __nv_bfloat16*>(k.data_ptr()),
88
+ static_cast<const __nv_bfloat16*>(v.data_ptr()),
89
+ static_cast<__nv_bfloat16*>(logits.data_ptr()),
90
+ static_cast<__nv_bfloat16*>(out.data_ptr()), sq, sk, heads, dim,
91
+ static_cast<float>(scale), checked_int(logits.size(2), "logits stride"),
92
+ checked_int(q.stride(0), "qkv token stride"), stream);
93
+ }
94
+ C10_CUDA_KERNEL_LAUNCH_CHECK();
95
+ }
96
+
97
+ } // namespace
98
+
99
+ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
100
+ ops.def("forward_static(Tensor q, Tensor k, Tensor v, Tensor! logits, Tensor! out, float scale) -> ()");
101
+ ops.impl("forward_static", torch::kCUDA, &masked_mha_forward_static);
102
+ }
103
+
104
+ REGISTER_EXTENSION(TORCH_EXTENSION_NAME)