Publish GROOT N1.7 Thor kernel sources
Browse files- CARD.md +23 -0
- README.md +23 -0
- SYNC.md +6 -0
- VALIDATION.md +5 -0
- benchmarks/README.md +4 -0
- benchmarks/RESULTS.md +5 -0
- build.toml +24 -0
- csrc/attention_mha_masked.cu +243 -0
- csrc/attention_mha_masked.cuh +22 -0
- examples/basic_usage.py +10 -0
- flake.nix +6 -0
- tests/test_masked_mha_runtime.py +173 -0
- torch-ext/masked_mha_runtime/__init__.py +57 -0
- torch-ext/torch_binding.cpp +104 -0
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)
|