tiagomonteiro0715 commited on
Commit
6f4b39f
·
0 Parent(s):

initial softmax attention kernel

Browse files
.gitignore ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.14
README.md ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ tags:
3
+ - kernel
4
+ license: apache-2.0
5
+ ---
6
+
7
+ # my_softmax_function
8
+
9
+ Fused **softmax-attention** kernel written in the [CUTLASS Python DSL](https://github.com/NVIDIA/cutlass)
10
+ (`cutlass.cute`), packaged for the Hugging Face [`kernels`](https://github.com/huggingface/kernels) Hub.
11
+
12
+ Computes, in a single online pass (max-shifted for numerical stability):
13
+
14
+ ```
15
+ out = softmax(scale * Q @ Kᵀ) @ V
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ```python
21
+ import torch
22
+ from kernels import get_kernel
23
+
24
+ k = get_kernel("monteiro-t/my_softmax_function") # replace with your repo id
25
+
26
+ Q = torch.randn(128, 64, device="cuda", dtype=torch.float32)
27
+ K = torch.randn(256, 64, device="cuda", dtype=torch.float32)
28
+ V = torch.randn(256, 64, device="cuda", dtype=torch.float32)
29
+
30
+ out = k.attention(Q, K, V) # scale defaults to 1/sqrt(d)
31
+ out = k.attention(Q, K, V, scale=0.5)
32
+ ```
33
+
34
+ ## API
35
+
36
+ | Function | Signature | Notes |
37
+ |---|---|---|
38
+ | `attention(Q, K, V, scale=None)` | `(M,d),(N,d),(N,d) -> (M,d)` | Convenience wrapper; `scale` defaults to `1/sqrt(d)`. |
39
+ | `softmax_attention(Q, K, V, scale)` | registered `torch.ops` op | `scale` required; `torch.compile`-friendly (has a fake/meta impl). |
40
+
41
+ ## Supported shapes / dtypes / hardware
42
+
43
+ | Property | Support |
44
+ |---|---|
45
+ | Dtype | `float32` only |
46
+ | Layout | 2-D, contiguous (inputs are forced contiguous) |
47
+ | Shapes | `Q: (M, d)`, `K: (N, d)`, `V: (N, d)` → `out: (M, d)` |
48
+ | Batch / heads | **Not supported** (single 2-D attention; no leading batch/head dims) |
49
+ | Masking | **None** (dense attention only) |
50
+ | Backward pass | **None** (inference / forward only) |
51
+ | Device | CUDA only |
52
+
53
+ ## Limitations
54
+
55
+ Naive one-row-per-thread implementation intended as a reference/demo kernel. No
56
+ batching, masking, dropout, or gradients. Not performance-tuned.
build.toml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [general]
2
+ name = "my_softmax_function"
3
+ backends = [
4
+ "cpu",
5
+ "cuda",
6
+ "metal",
7
+ "rocm",
8
+ "xpu",
9
+ ]
10
+
11
+ [torch]
12
+ src = []
13
+
14
+ [kernel]
flake.lock ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "nodes": {
3
+ "flake-compat": {
4
+ "locked": {
5
+ "lastModified": 1765121682,
6
+ "narHash": "sha256-4VBOP18BFeiPkyhy9o4ssBNQEvfvv1kXkasAYd0+rrA=",
7
+ "owner": "edolstra",
8
+ "repo": "flake-compat",
9
+ "rev": "65f23138d8d09a92e30f1e5c87611b23ef451bf3",
10
+ "type": "github"
11
+ },
12
+ "original": {
13
+ "owner": "edolstra",
14
+ "repo": "flake-compat",
15
+ "type": "github"
16
+ }
17
+ },
18
+ "flake-utils": {
19
+ "inputs": {
20
+ "systems": "systems"
21
+ },
22
+ "locked": {
23
+ "lastModified": 1731533236,
24
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
25
+ "owner": "numtide",
26
+ "repo": "flake-utils",
27
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
28
+ "type": "github"
29
+ },
30
+ "original": {
31
+ "owner": "numtide",
32
+ "repo": "flake-utils",
33
+ "type": "github"
34
+ }
35
+ },
36
+ "kernel-builder": {
37
+ "inputs": {
38
+ "flake-compat": "flake-compat",
39
+ "flake-utils": "flake-utils",
40
+ "nixpkgs": "nixpkgs"
41
+ },
42
+ "locked": {
43
+ "lastModified": 1775482375,
44
+ "narHash": "sha256-RUbxfJGs96jwnwSci3+8h08GB2/katOI67yZTCaV+aE=",
45
+ "owner": "huggingface",
46
+ "repo": "kernel-builder",
47
+ "rev": "dffbce5a048648febb96bbed79f3811ab6d7a577",
48
+ "type": "github"
49
+ },
50
+ "original": {
51
+ "owner": "huggingface",
52
+ "repo": "kernel-builder",
53
+ "type": "github"
54
+ }
55
+ },
56
+ "nixpkgs": {
57
+ "locked": {
58
+ "lastModified": 1766341660,
59
+ "narHash": "sha256-4yG6vx7Dddk9/zh45Y2KM82OaRD4jO3HA9r98ORzysA=",
60
+ "owner": "NixOS",
61
+ "repo": "nixpkgs",
62
+ "rev": "26861f5606e3e4d1400771b513cc63e5f70151a6",
63
+ "type": "github"
64
+ },
65
+ "original": {
66
+ "owner": "NixOS",
67
+ "ref": "nixos-unstable-small",
68
+ "repo": "nixpkgs",
69
+ "type": "github"
70
+ }
71
+ },
72
+ "root": {
73
+ "inputs": {
74
+ "kernel-builder": "kernel-builder"
75
+ }
76
+ },
77
+ "systems": {
78
+ "locked": {
79
+ "lastModified": 1681028828,
80
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
81
+ "owner": "nix-systems",
82
+ "repo": "default",
83
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
84
+ "type": "github"
85
+ },
86
+ "original": {
87
+ "owner": "nix-systems",
88
+ "repo": "default",
89
+ "type": "github"
90
+ }
91
+ }
92
+ },
93
+ "root": "root",
94
+ "version": 7
95
+ }
flake.nix ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ description = "my_softmax_function — fused softmax-attention kernel";
3
+
4
+ inputs = {
5
+ kernel-builder.url = "github:huggingface/kernel-builder";
6
+ };
7
+
8
+ outputs =
9
+ {
10
+ self,
11
+ kernel-builder,
12
+ }:
13
+ kernel-builder.lib.genFlakeOutputs {
14
+ inherit self;
15
+ path = ./.;
16
+ };
17
+ }
main.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ def main():
2
+ print("Hello from my-softmax-function!")
3
+
4
+
5
+ if __name__ == "__main__":
6
+ main()
pyproject.toml ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "my-softmax-function"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ dependencies = [
8
+ "nvidia-cutlass-dsl>=4.6.1",
9
+ "torch>=2.13.0",
10
+ ]
11
+
12
+ [dependency-groups]
13
+ dev = [
14
+ "kernels",
15
+ "pytest",
16
+ ]
tests/test_attention.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Test the built kernel through the `kernels` loader against a torch reference.
2
+
3
+ Run AFTER building (so build/torch-universal/my_softmax_function exists):
4
+
5
+ pytest tests/ -v
6
+ """
7
+
8
+ import math
9
+ from pathlib import Path
10
+
11
+ import pytest
12
+ import torch
13
+
14
+ from kernels import get_local_kernel
15
+
16
+ REPO_ROOT = Path(__file__).resolve().parent.parent
17
+
18
+ pytestmark = pytest.mark.skipif(
19
+ not torch.cuda.is_available(), reason="kernel requires CUDA"
20
+ )
21
+
22
+
23
+ def _load():
24
+ # Loads build/torch-universal/my_softmax_function from the local repo,
25
+ # exactly the way kernels.get_kernel(...) would load it from the Hub.
26
+ return get_local_kernel(REPO_ROOT, "my_softmax_function")
27
+
28
+
29
+ def _reference(Q, K, V, scale):
30
+ scores = (Q @ K.transpose(-1, -2)) * scale
31
+ probs = torch.softmax(scores, dim=-1)
32
+ return probs @ V
33
+
34
+
35
+ @pytest.mark.parametrize("M,N,d", [(1, 1, 1), (17, 33, 8), (128, 256, 64)])
36
+ def test_matches_reference(M, N, d):
37
+ kernel = _load()
38
+ torch.manual_seed(0)
39
+ Q = torch.randn(M, d, device="cuda", dtype=torch.float32)
40
+ K = torch.randn(N, d, device="cuda", dtype=torch.float32)
41
+ V = torch.randn(N, d, device="cuda", dtype=torch.float32)
42
+ scale = 1.0 / math.sqrt(d)
43
+
44
+ out = kernel.attention(Q, K, V)
45
+ ref = _reference(Q, K, V, scale)
46
+
47
+ torch.testing.assert_close(out, ref, atol=1e-3, rtol=1e-3)
48
+
49
+
50
+ def test_custom_scale():
51
+ kernel = _load()
52
+ Q = torch.randn(8, 16, device="cuda", dtype=torch.float32)
53
+ K = torch.randn(12, 16, device="cuda", dtype=torch.float32)
54
+ V = torch.randn(12, 16, device="cuda", dtype=torch.float32)
55
+
56
+ out = kernel.attention(Q, K, V, scale=0.5)
57
+ ref = _reference(Q, K, V, 0.5)
58
+ torch.testing.assert_close(out, ref, atol=1e-3, rtol=1e-3)
torch-ext/my_softmax_function/__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 ._ops import attention, softmax_attention
7
+
8
+ __all__ = ["attention", "softmax_attention"]
torch-ext/my_softmax_function/_ops.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)
torch-ext/my_softmax_function/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
+ )
uv.lock ADDED
The diff for this file is too large to render. See raw diff