Skip to content

Commit 9a80eab

Browse files
committed
Add cute cache
stack-info: PR: #57, branch: drisspg/stack/15
1 parent 3bd431f commit 9a80eab

4 files changed

Lines changed: 283 additions & 14 deletions

File tree

test_cute_cache.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#!/usr/bin/env python3
2+
"""Test script to demonstrate CUTE kernel caching"""
3+
4+
import torch
5+
from operator import add, mul
6+
import cutlass
7+
import cutlass.cute as cute
8+
from cutlass.cute.runtime import from_dlpack
9+
10+
from transformer_nuggets.cute import cute_compile_and_cache, get_cache_stats, clear_cute_cache
11+
12+
13+
@cute.kernel
14+
def simple_elementwise_kernel(
15+
op: cutlass.Constexpr,
16+
gA: cute.Tensor,
17+
gB: cute.Tensor,
18+
gC: cute.Tensor,
19+
):
20+
tidx, _, _ = cute.arch.thread_idx()
21+
bidx, _, _ = cute.arch.block_idx()
22+
bdim, _, _ = cute.arch.block_dim()
23+
24+
thread_idx = bidx * bdim + tidx
25+
26+
m, n = gA.shape
27+
ni = thread_idx % n
28+
mi = thread_idx // n
29+
30+
a_val = gA[mi, ni]
31+
b_val = gB[mi, ni]
32+
33+
gC[mi, ni] = op(a_val, b_val)
34+
35+
36+
def cached_elementwise(
37+
op: cutlass.Constexpr,
38+
mA: cute.Tensor,
39+
mB: cute.Tensor,
40+
mC: cute.Tensor,
41+
):
42+
# Define the kernel with @cute.jit
43+
@cute.jit
44+
def kernel(op, mA, mB, mC):
45+
num_threads_per_block = 256
46+
m, n = mA.shape
47+
48+
kernel_op = simple_elementwise_kernel(op, mA, mB, mC)
49+
kernel_op.launch(
50+
grid=((m * n) // num_threads_per_block, 1, 1), block=(num_threads_per_block, 1, 1)
51+
)
52+
53+
# Use explicit caching
54+
compiled_kernel = cute_compile_and_cache(kernel, op, mA, mB, mC)
55+
return compiled_kernel(op, mA, mB, mC)
56+
57+
58+
def main():
59+
print("CUTE Kernel Caching Demo")
60+
print("=" * 50)
61+
62+
# Create test tensors
63+
M, N = 1024, 1024
64+
a = torch.randn(M, N, device="cuda", dtype=torch.float16)
65+
b = torch.randn(M, N, device="cuda", dtype=torch.float16)
66+
c = torch.zeros(M, N, device="cuda", dtype=torch.float16)
67+
68+
# Convert to CUTE tensors
69+
a_ = from_dlpack(a, assumed_align=16)
70+
b_ = from_dlpack(b, assumed_align=16)
71+
c_ = from_dlpack(c, assumed_align=16)
72+
73+
print("\n1. First call with 'add' - should compile:")
74+
cached_elementwise(add, a_, b_, c_)
75+
torch.testing.assert_close(c, a + b)
76+
print("✓ Addition result correct")
77+
78+
print("\n2. Second call with 'add' - should hit cache:")
79+
c.zero_()
80+
cached_elementwise(add, a_, b_, c_)
81+
82+
print("\n3. First call with 'mul' - should compile (different op):")
83+
c.zero_()
84+
cached_elementwise(mul, a_, b_, c_)
85+
torch.testing.assert_close(c, a * b)
86+
print("✓ Multiplication result correct")
87+
88+
print("\n4. Different tensor sizes - should compile:")
89+
a2 = torch.randn(512, 512, device="cuda", dtype=torch.float16)
90+
b2 = torch.randn(512, 512, device="cuda", dtype=torch.float16)
91+
c2 = torch.zeros(512, 512, device="cuda", dtype=torch.float16)
92+
93+
a2_ = from_dlpack(a2, assumed_align=16)
94+
b2_ = from_dlpack(b2, assumed_align=16)
95+
c2_ = from_dlpack(c2, assumed_align=16)
96+
97+
cached_elementwise(add, a2_, b2_, c2_)
98+
99+
print("\n5. Same size as #4 - should hit cache:")
100+
cached_elementwise(add, a2_, b2_, c2_)
101+
102+
print("\n" + "=" * 50)
103+
print("Final Cache Statistics:")
104+
stats = get_cache_stats()
105+
print(f" Total calls: {stats['total']}")
106+
print(f" Cache hits: {stats['hits']}")
107+
print(f" Cache misses: {stats['misses']}")
108+
print(f" Hit rate: {stats['hit_rate']:.2%}")
109+
print(f" Unique kernels cached: {stats['cache_size']}")
110+
111+
print("\nClearing cache...")
112+
clear_cute_cache()
113+
stats = get_cache_stats()
114+
print(f" Cache size after clear: {stats['cache_size']}")
115+
116+
117+
if __name__ == "__main__":
118+
main()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from transformer_nuggets.cute.cache import (
2+
cute_compile_and_cache,
3+
clear_cute_cache,
4+
get_cache_stats,
5+
)

transformer_nuggets/cute/add.py

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from cutlass.cute.runtime import from_dlpack
88

99
from transformer_nuggets.utils.benchmark import benchmark_cuda_function_in_microseconds
10+
from transformer_nuggets.cute.cache import cute_compile_and_cache, get_cache_stats
1011

1112

1213
@cute.kernel
@@ -72,25 +73,31 @@ def naive_elementwise_add(mA: cute.Tensor, mB: cute.Tensor, mC: cute.Tensor):
7273
)
7374

7475

75-
@cute.jit
7676
def elementwise_op(
7777
op: cutlass.Constexpr,
7878
mA: cute.Tensor,
7979
mB: cute.Tensor,
8080
mC: cute.Tensor,
8181
):
82-
thr_layout = cute.make_layout((4, 32), stride=(32, 1))
83-
val_layout = cute.make_layout((4, 8), stride=(8, 1))
84-
tiler_mn, tv_layout = cute.make_layout_tv(thr_layout, val_layout)
82+
# Define the kernel with @cute.jit
83+
@cute.jit
84+
def kernel(op, mA, mB, mC):
85+
thr_layout = cute.make_layout((4, 32), stride=(32, 1))
86+
val_layout = cute.make_layout((4, 8), stride=(8, 1))
87+
tiler_mn, tv_layout = cute.make_layout_tv(thr_layout, val_layout)
8588

86-
gA = cute.zipped_divide(mA, tiler_mn)
87-
gB = cute.zipped_divide(mB, tiler_mn)
88-
gC = cute.zipped_divide(mC, tiler_mn)
89+
gA = cute.zipped_divide(mA, tiler_mn)
90+
gB = cute.zipped_divide(mB, tiler_mn)
91+
gC = cute.zipped_divide(mC, tiler_mn)
8992

90-
elementwise_apply_kernel(op, gA, gB, gC, tv_layout).launch(
91-
grid=[cute.size(gC, mode=[1]), 1, 1],
92-
block=[cute.size(tv_layout, mode=[0]), 1, 1],
93-
)
93+
elementwise_apply_kernel(op, gA, gB, gC, tv_layout).launch(
94+
grid=[cute.size(gC, mode=[1]), 1, 1],
95+
block=[cute.size(tv_layout, mode=[0]), 1, 1],
96+
)
97+
98+
# Use explicit caching
99+
compiled_kernel = cute_compile_and_cache(kernel, op, mA, mB, mC)
100+
return compiled_kernel(op, mA, mB, mC)
94101

95102

96103
def benchmark(callable, *, num_warmups=5, num_iterations=200):
@@ -123,7 +130,9 @@ def benchmark(callable, *, num_warmups=5, num_iterations=200):
123130
c.zero_()
124131

125132
naive_elementwise_add_compiled = cute.compile(naive_elementwise_add, a_, b_, c_)
126-
elementwise_add_compiled = cute.compile(elementwise_op, add, a_, b_, c_)
133+
134+
# Note: elementwise_op is now cached automatically!
135+
# First call will compile, subsequent calls use cache
127136

128137
print("\n" + "=" * 50)
129138
print("BENCHMARK RESULTS")
@@ -135,5 +144,12 @@ def benchmark(callable, *, num_warmups=5, num_iterations=200):
135144
print("2. PyTorch add (baseline):")
136145
benchmark(partial(torch.add, a, b, out=c))
137146

138-
print("3. Optimized elementwise add kernel:")
139-
benchmark(partial(elementwise_add_compiled, a_, b_, c_))
147+
print("3. Optimized elementwise add kernel (with caching):")
148+
benchmark(partial(elementwise_op, add, a_, b_, c_))
149+
150+
print("\nCache Statistics:")
151+
stats = get_cache_stats()
152+
print(f" Cache hits: {stats['hits']}")
153+
print(f" Cache misses: {stats['misses']}")
154+
print(f" Hit rate: {stats['hit_rate']:.2%}")
155+
print(f" Cache size: {stats['cache_size']}")

transformer_nuggets/cute/cache.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import hashlib
2+
import logging
3+
import threading
4+
from typing import Any
5+
from collections.abc import Callable
6+
7+
import cutlass.cute as cute
8+
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
class CuteKernelCache:
14+
"""Global cache for compiled CUTE kernels"""
15+
16+
def __init__(self):
17+
self._cache: dict[str, Any] = {}
18+
self._stats = {"hits": 0, "misses": 0}
19+
self._lock = threading.Lock()
20+
21+
def get(self, key: str) -> Any | None:
22+
with self._lock:
23+
if key in self._cache:
24+
self._stats["hits"] += 1
25+
return self._cache[key]
26+
self._stats["misses"] += 1
27+
return None
28+
29+
def set(self, key: str, value: Any) -> None:
30+
with self._lock:
31+
self._cache[key] = value
32+
33+
def clear(self) -> None:
34+
with self._lock:
35+
self._cache.clear()
36+
self._stats = {"hits": 0, "misses": 0}
37+
38+
def get_stats(self) -> dict[str, int | float]:
39+
with self._lock:
40+
total = self._stats["hits"] + self._stats["misses"]
41+
hit_rate = self._stats["hits"] / total if total > 0 else 0
42+
return {
43+
**self._stats,
44+
"total": total,
45+
"hit_rate": hit_rate,
46+
"cache_size": len(self._cache),
47+
}
48+
49+
50+
# Global cache instance
51+
_kernel_cache = CuteKernelCache()
52+
53+
54+
def _generate_cache_key(*args, **kwargs) -> str:
55+
"""Generate a cache key from function arguments"""
56+
key_parts = []
57+
58+
for arg in args:
59+
if isinstance(arg, cute.Tensor):
60+
# For cute tensors, use shape and dtype
61+
key_parts.append(f"tensor_shape={arg.shape}_dtype={arg._dtype}")
62+
elif hasattr(arg, "__name__"):
63+
# For functions/operators like add, mul
64+
key_parts.append(f"op={arg.__name__}")
65+
else:
66+
# For other types, use string representation
67+
key_parts.append(str(arg))
68+
69+
# Add kwargs
70+
for k, v in sorted(kwargs.items()):
71+
key_parts.append(f"{k}={v}")
72+
73+
# Create hash of the key
74+
key_str = "_".join(key_parts)
75+
return hashlib.md5(key_str.encode()).hexdigest()
76+
77+
78+
def cute_compile_and_cache(func: Callable, *args, **kwargs):
79+
"""
80+
Compile a @cute.jit decorated function and cache the result.
81+
82+
Args:
83+
func: A function decorated with @cute.jit
84+
*args: Arguments to pass to cute.compile and for cache key generation
85+
**kwargs: Keyword arguments to pass to cute.compile and for cache key generation
86+
87+
Returns:
88+
Compiled kernel that can be executed
89+
90+
Example:
91+
@cute.jit
92+
def my_kernel(a, b, c):
93+
# kernel implementation
94+
...
95+
96+
# Cache the compilation
97+
compiled_kernel = cute_compile_and_cache(my_kernel, tensor_a, tensor_b, tensor_c)
98+
result = compiled_kernel(tensor_a, tensor_b, tensor_c)
99+
"""
100+
# Generate cache key from function and arguments
101+
cache_key = _generate_cache_key(*args, **kwargs)
102+
cache_key = f"{func.__name__}_{cache_key}"
103+
104+
# Check cache
105+
compiled_kernel = _kernel_cache.get(cache_key)
106+
107+
if compiled_kernel is not None:
108+
logger.debug(f"Cache hit for {func.__name__} (key: {cache_key[:8]}...)")
109+
return compiled_kernel
110+
111+
logger.debug(f"Cache miss for {func.__name__} (key: {cache_key[:8]}...) - Compiling...")
112+
113+
# Compile the kernel
114+
compiled_kernel = cute.compile(func, *args, **kwargs)
115+
116+
# Cache the compiled kernel
117+
_kernel_cache.set(cache_key, compiled_kernel)
118+
119+
return compiled_kernel
120+
121+
122+
# Utility functions
123+
def clear_cute_cache():
124+
"""Clear all cached kernels"""
125+
_kernel_cache.clear()
126+
127+
128+
def get_cache_stats():
129+
"""Get cache statistics"""
130+
return _kernel_cache.get_stats()

0 commit comments

Comments
 (0)