tiagomonteiro0715 commited on
Commit ·
490d9fe
1
Parent(s): f5ebe93
add built softmax attention kernel
Browse files- .gitignore +4 -1
- build/torch-cuda/__init__.py +8 -0
- build/torch-cuda/_kernel.py +68 -0
- build/torch-cuda/_ops.py +8 -0
- build/torch-cuda/attention_v3.py +98 -0
- build/torch-cuda/metadata.json +5 -0
- build/torch-cuda/my_softmax_function/__init__.py +26 -0
- result +0 -1
.gitignore
CHANGED
|
@@ -1,10 +1,13 @@
|
|
| 1 |
# Python-generated files
|
| 2 |
__pycache__/
|
| 3 |
*.py[oc]
|
| 4 |
-
build/
|
| 5 |
dist/
|
| 6 |
wheels/
|
| 7 |
*.egg-info
|
| 8 |
|
| 9 |
# Virtual environments
|
| 10 |
.venv
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# Python-generated files
|
| 2 |
__pycache__/
|
| 3 |
*.py[oc]
|
|
|
|
| 4 |
dist/
|
| 5 |
wheels/
|
| 6 |
*.egg-info
|
| 7 |
|
| 8 |
# Virtual environments
|
| 9 |
.venv
|
| 10 |
+
|
| 11 |
+
# Nix build symlink (do NOT commit; build/ is the real artifact and IS tracked)
|
| 12 |
+
result
|
| 13 |
+
result-*
|
build/torch-cuda/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""my_softmax_function — fused softmax-attention kernel (CUTLASS Python DSL).
|
| 2 |
+
|
| 3 |
+
Public API exposed to `kernels` consumers.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from ._kernel import attention, softmax_attention
|
| 7 |
+
|
| 8 |
+
__all__ = ["attention", "softmax_attention"]
|
build/torch-cuda/_kernel.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Public torch custom-op wrapper around the CUTLASS-DSL softmax-attention kernel.
|
| 2 |
+
|
| 3 |
+
This is the API a Hugging Face `kernels` consumer sees: a normal, registered
|
| 4 |
+
`torch.ops` op that works with autograd tracing / `torch.compile`, not the raw
|
| 5 |
+
`@cute.kernel`.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import math
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
from cutlass.cute.runtime import from_dlpack
|
| 12 |
+
|
| 13 |
+
from .attention_v3 import solve
|
| 14 |
+
|
| 15 |
+
_OP_NAME = "my_softmax_function::softmax_attention"
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@torch.library.custom_op(_OP_NAME, mutates_args=())
|
| 19 |
+
def softmax_attention(
|
| 20 |
+
Q: torch.Tensor, # (M, d)
|
| 21 |
+
K: torch.Tensor, # (N, d)
|
| 22 |
+
V: torch.Tensor, # (N, d)
|
| 23 |
+
scale: float,
|
| 24 |
+
) -> torch.Tensor: # (M, d)
|
| 25 |
+
"""Fused, max-shifted softmax attention: softmax(scale * Q @ K^T) @ V.
|
| 26 |
+
|
| 27 |
+
Q, K, V must be contiguous, 2-D, float32, and on the same CUDA device.
|
| 28 |
+
"""
|
| 29 |
+
if not (Q.is_cuda and K.is_cuda and V.is_cuda):
|
| 30 |
+
raise ValueError("Q, K, V must be CUDA tensors")
|
| 31 |
+
if not (Q.dim() == K.dim() == V.dim() == 2):
|
| 32 |
+
raise ValueError("Q, K, V must be 2-D (M,d)/(N,d)/(N,d)")
|
| 33 |
+
|
| 34 |
+
M, d = Q.shape
|
| 35 |
+
N = K.shape[0]
|
| 36 |
+
if K.shape[1] != d or V.shape[1] != d or V.shape[0] != N:
|
| 37 |
+
raise ValueError("shape mismatch between Q/K/V")
|
| 38 |
+
|
| 39 |
+
Q = Q.contiguous()
|
| 40 |
+
K = K.contiguous()
|
| 41 |
+
V = V.contiguous()
|
| 42 |
+
output = torch.empty((M, d), dtype=torch.float32, device=Q.device)
|
| 43 |
+
|
| 44 |
+
solve(
|
| 45 |
+
from_dlpack(Q),
|
| 46 |
+
from_dlpack(K),
|
| 47 |
+
from_dlpack(V),
|
| 48 |
+
from_dlpack(output),
|
| 49 |
+
M,
|
| 50 |
+
N,
|
| 51 |
+
d,
|
| 52 |
+
float(scale),
|
| 53 |
+
)
|
| 54 |
+
return output
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
@softmax_attention.register_fake
|
| 58 |
+
def _(Q, K, V, scale):
|
| 59 |
+
# Shape/dtype/device metadata only — no compute. Lets torch.compile trace.
|
| 60 |
+
M, d = Q.shape
|
| 61 |
+
return Q.new_empty((M, d))
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def attention(Q, K, V, scale=None):
|
| 65 |
+
"""Convenience entry point with a default 1/sqrt(d) scale."""
|
| 66 |
+
if scale is None:
|
| 67 |
+
scale = 1.0 / math.sqrt(Q.shape[-1])
|
| 68 |
+
return torch.ops.my_softmax_function.softmax_attention(Q, K, V, scale)
|
build/torch-cuda/_ops.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
ops = torch.ops._my_softmax_function_2af76fc
|
| 3 |
+
|
| 4 |
+
def add_op_namespace_prefix(op_name: str):
|
| 5 |
+
"""
|
| 6 |
+
Prefix op by namespace.
|
| 7 |
+
"""
|
| 8 |
+
return f"_my_softmax_function_2af76fc::{op_name}"
|
build/torch-cuda/attention_v3.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cutlass
|
| 2 |
+
import cutlass.cute as cute
|
| 3 |
+
from cutlass.cute.runtime import from_dlpack
|
| 4 |
+
|
| 5 |
+
NEG_BIG = -3.0e38 # finite stand-in for -inf
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def transpose_matrix(T: cute.Tensor, i, j):
|
| 9 |
+
"""Element (i, j) of T-transpose: a pure index swap, no data movement."""
|
| 10 |
+
return T[j, i]
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def matrix_multiplication(acc, A: cute.Tensor, B: cute.Tensor, row, col, k,
|
| 14 |
+
transpose_b=False):
|
| 15 |
+
"""One MAC step of C[row, col] = sum_k A[row, k] * B[k, col].
|
| 16 |
+
transpose_b is a Python bool, resolved at trace time."""
|
| 17 |
+
b = transpose_matrix(B, k, col) if transpose_b else B[k, col]
|
| 18 |
+
return acc + A[row, k] * b
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def softmax_computation(score, row_max, denom):
|
| 22 |
+
"""One softmax probability, max-shifted for stability. Pass denom=1.0 for the
|
| 23 |
+
unnormalized numerator (online softmax normalizes once at the end)."""
|
| 24 |
+
return cute.math.exp(score - row_max) / denom
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@cute.kernel
|
| 28 |
+
def softmax_attention(
|
| 29 |
+
Q: cute.Tensor, # (M, d)
|
| 30 |
+
K: cute.Tensor, # (N, d)
|
| 31 |
+
V: cute.Tensor, # (N, d)
|
| 32 |
+
output: cute.Tensor, # (M, d)
|
| 33 |
+
M: cutlass.Int32,
|
| 34 |
+
N: cutlass.Int32,
|
| 35 |
+
d: cutlass.Int32,
|
| 36 |
+
host_scale: cutlass.Float32,
|
| 37 |
+
):
|
| 38 |
+
bx, _, _ = cute.arch.block_idx()
|
| 39 |
+
bdx, _, _ = cute.arch.block_dim()
|
| 40 |
+
tx, _, _ = cute.arch.thread_idx()
|
| 41 |
+
|
| 42 |
+
row = bx * bdx + tx
|
| 43 |
+
|
| 44 |
+
if row < M:
|
| 45 |
+
scale = host_scale
|
| 46 |
+
|
| 47 |
+
# output[row, :] is the running accumulator
|
| 48 |
+
for k in cutlass.range(d):
|
| 49 |
+
output[row, k] = cutlass.Float32(0.0)
|
| 50 |
+
|
| 51 |
+
running_max = cutlass.Float32(NEG_BIG)
|
| 52 |
+
running_sum = cutlass.Float32(0.0)
|
| 53 |
+
|
| 54 |
+
# single pass: score, max, denominator and V-accumulation together
|
| 55 |
+
for n in cutlass.range(N):
|
| 56 |
+
s = cutlass.Float32(0.0)
|
| 57 |
+
for k in cutlass.range(d):
|
| 58 |
+
s = matrix_multiplication(s, Q, K, row, n, k, transpose_b=True)
|
| 59 |
+
s = s * scale
|
| 60 |
+
|
| 61 |
+
new_max = running_max
|
| 62 |
+
if s > new_max:
|
| 63 |
+
new_max = s
|
| 64 |
+
|
| 65 |
+
# rescales everything accumulated under the old max
|
| 66 |
+
correction = softmax_computation(running_max, new_max, 1.0)
|
| 67 |
+
p = softmax_computation(s, new_max, 1.0)
|
| 68 |
+
|
| 69 |
+
running_sum = running_sum * correction + p
|
| 70 |
+
running_max = new_max
|
| 71 |
+
|
| 72 |
+
for k in cutlass.range(d):
|
| 73 |
+
output[row, k] = output[row, k] * correction + p * V[n, k]
|
| 74 |
+
|
| 75 |
+
# normalize once, with a reciprocal instead of d divisions
|
| 76 |
+
inv_sum = 1.0 / running_sum
|
| 77 |
+
for k in cutlass.range(d):
|
| 78 |
+
output[row, k] = output[row, k] * inv_sum
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
@cute.jit
|
| 82 |
+
def solve(
|
| 83 |
+
Q: cute.Tensor,
|
| 84 |
+
K: cute.Tensor,
|
| 85 |
+
V: cute.Tensor,
|
| 86 |
+
output: cute.Tensor,
|
| 87 |
+
M: cutlass.Int32,
|
| 88 |
+
N: cutlass.Int32,
|
| 89 |
+
d: cutlass.Int32,
|
| 90 |
+
host_scale: cutlass.Float32,
|
| 91 |
+
):
|
| 92 |
+
block_size = 256
|
| 93 |
+
grid_size = (M + block_size - 1) // block_size
|
| 94 |
+
|
| 95 |
+
softmax_attention(Q, K, V, output, M, N, d, host_scale).launch(
|
| 96 |
+
grid=(grid_size, 1, 1),
|
| 97 |
+
block=(block_size, 1, 1),
|
| 98 |
+
)
|
build/torch-cuda/metadata.json
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"python-depends": [
|
| 3 |
+
"nvidia-cutlass-dsl"
|
| 4 |
+
]
|
| 5 |
+
}
|
build/torch-cuda/my_softmax_function/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import ctypes
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
import importlib
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from types import ModuleType
|
| 7 |
+
|
| 8 |
+
def _import_from_path(file_path: Path) -> ModuleType:
|
| 9 |
+
# We cannot use the module name as-is, after adding it to `sys.modules`,
|
| 10 |
+
# it would also be used for other imports. So, we make a module name that
|
| 11 |
+
# depends on the path for it to be unique using the hex-encoded hash of
|
| 12 |
+
# the path.
|
| 13 |
+
path_hash = "{:x}".format(ctypes.c_size_t(hash(file_path.absolute())).value)
|
| 14 |
+
module_name = path_hash
|
| 15 |
+
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
| 16 |
+
if spec is None:
|
| 17 |
+
raise ImportError(f"Cannot load spec for {module_name} from {file_path}")
|
| 18 |
+
module = importlib.util.module_from_spec(spec)
|
| 19 |
+
if module is None:
|
| 20 |
+
raise ImportError(f"Cannot load module {module_name} from spec")
|
| 21 |
+
sys.modules[module_name] = module
|
| 22 |
+
spec.loader.exec_module(module) # type: ignore
|
| 23 |
+
return module
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
globals().update(vars(_import_from_path(Path(__file__).parent.parent / "__init__.py")))
|
result
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
/nix/store/4mkpzj01sp4ixa9wbig2hiqfaavhxqck-torch-ext-bundle
|
|
|
|
|
|