Skip to content

Commit 1ea7b47

Browse files
committed
Generic autotune bindings from helion pytorch/helion#1385
1 parent 473af38 commit 1ea7b47

1 file changed

Lines changed: 181 additions & 0 deletions

File tree

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
"""CuTeDSL kernel autotuned with Helion's generic autotune API.
2+
3+
Compare with example_helion_autotune.py which uses the old adapter layer
4+
(TunableKernel subclass, HelionAutotuner, KernelAdapter, etc.).
5+
6+
This version uses the upstream helion.autotuner.generic.autotune() function
7+
directly -- just pass tunables, compile_fn, baseline_fn, and args.
8+
"""
9+
10+
from operator import add
11+
12+
import torch
13+
14+
import cutlass
15+
import cutlass.cute as cute
16+
from cutlass.cute.runtime import from_dlpack
17+
18+
from helion.autotuner import PowerOfTwoFragment
19+
from helion.autotuner.generic import autotune
20+
from helion.runtime.config import Config
21+
22+
from transformer_nuggets.cute.cache import compile_and_cache
23+
from transformer_nuggets.cute.utils import get_tensor_alignment
24+
from transformer_nuggets.cute.base import CuteOp
25+
from transformer_nuggets.utils.benchmark import benchmark_cuda_function_in_microseconds
26+
27+
28+
class ElementwiseAddOp(CuteOp[[torch.Tensor, torch.Tensor], torch.Tensor]):
29+
tunables = {
30+
"thr_m": PowerOfTwoFragment(4, 16, 8),
31+
"thr_n": PowerOfTwoFragment(16, 64, 32),
32+
"val_m": PowerOfTwoFragment(2, 8, 2),
33+
"val_n": PowerOfTwoFragment(4, 16, 8),
34+
}
35+
36+
def __init__(self):
37+
super().__init__()
38+
self.op = add
39+
40+
def compile(self, config: Config):
41+
thr_m, thr_n = config["thr_m"], config["thr_n"]
42+
val_m, val_n = config["val_m"], config["val_n"]
43+
44+
def run(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
45+
M, N = a.shape
46+
c = torch.empty(M, N, device="cuda", dtype=a.dtype)
47+
48+
mA = from_dlpack(
49+
a, assumed_align=get_tensor_alignment(a, dim=-1)
50+
).mark_layout_dynamic()
51+
mB = from_dlpack(
52+
b, assumed_align=get_tensor_alignment(b, dim=-1)
53+
).mark_layout_dynamic()
54+
mC = from_dlpack(
55+
c, assumed_align=get_tensor_alignment(c, dim=-1)
56+
).mark_layout_dynamic()
57+
58+
dim_orders = tuple(reversed(a.dim_order()))
59+
cache_key = f"add_{thr_m}_{thr_n}_{val_m}_{val_n}_{dim_orders}"
60+
61+
compile_and_cache(
62+
self,
63+
cache_key,
64+
self.op,
65+
mA,
66+
mB,
67+
mC,
68+
thr_m,
69+
thr_n,
70+
val_m,
71+
val_n,
72+
dim_orders,
73+
)(mA, mB, mC)
74+
return c
75+
76+
return run
77+
78+
def baseline(self, a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
79+
return a + b
80+
81+
def interface(self, a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
82+
raise NotImplementedError
83+
84+
@cute.kernel
85+
def kernel(
86+
self,
87+
op: cutlass.Constexpr,
88+
gA: cute.Tensor,
89+
gB: cute.Tensor,
90+
gC: cute.Tensor,
91+
tv_layout: cute.Layout,
92+
):
93+
tidx, _, _ = cute.arch.thread_idx()
94+
bidx, _, _ = cute.arch.block_idx()
95+
96+
blk_coord = ((None, None), bidx)
97+
blkA = gA[blk_coord]
98+
blkB = gB[blk_coord]
99+
blkC = gC[blk_coord]
100+
101+
tidfrgA = cute.composition(blkA, tv_layout)
102+
tidfrgB = cute.composition(blkB, tv_layout)
103+
tidfrgC = cute.composition(blkC, tv_layout)
104+
105+
thr_coord = (tidx, None)
106+
thrA = tidfrgA[thr_coord]
107+
thrB = tidfrgB[thr_coord]
108+
thrC = tidfrgC[thr_coord]
109+
110+
thrC[None] = op(thrA.load(), thrB.load())
111+
112+
@cute.jit
113+
def __call__(
114+
self,
115+
op: cutlass.Constexpr,
116+
mA: cute.Tensor,
117+
mB: cute.Tensor,
118+
mC: cute.Tensor,
119+
thr_m: cutlass.Constexpr,
120+
thr_n: cutlass.Constexpr,
121+
val_m: cutlass.Constexpr,
122+
val_n: cutlass.Constexpr,
123+
order: cutlass.Constexpr,
124+
):
125+
thr_layout = cute.make_ordered_layout((thr_m, thr_n), order)
126+
val_layout = cute.make_ordered_layout((val_m, val_n), order)
127+
tiler_mn, tv_layout = cute.make_layout_tv(thr_layout, val_layout)
128+
129+
gA = cute.zipped_divide(mA, tiler_mn)
130+
gB = cute.zipped_divide(mB, tiler_mn)
131+
gC = cute.zipped_divide(mC, tiler_mn)
132+
133+
self.kernel(op, gA, gB, gC, tv_layout).launch(
134+
grid=[cute.size(gC, mode=[1]), 1, 1],
135+
block=[cute.size(tv_layout, mode=[0]), 1, 1],
136+
)
137+
138+
139+
_op = ElementwiseAddOp()
140+
_config_cache: dict[str, Config] = {}
141+
142+
143+
def autotuned_add(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
144+
M, N = a.shape
145+
cache_key = f"add_{M}x{N}_{a.dtype}"
146+
147+
if cache_key not in _config_cache:
148+
_config_cache[cache_key] = autotune(
149+
tunables=_op.tunables,
150+
compile_fn=_op.compile,
151+
baseline_fn=_op.baseline,
152+
args=(a, b),
153+
algorithm="PatternSearch",
154+
autotune_accuracy_check=True,
155+
autotune_ignore_errors=True,
156+
max_generations=3,
157+
initial_population=20,
158+
)
159+
print(f"Best config for {M}x{N}: {dict(_config_cache[cache_key])}")
160+
161+
return _op.compile(_config_cache[cache_key])(a, b)
162+
163+
164+
if __name__ == "__main__":
165+
from rich import print as rprint
166+
167+
shapes = [(1024, 1024), (2048, 2048), (4096, 4096)]
168+
169+
for M, N in shapes:
170+
print(f"\n--- {M} x {N} ---")
171+
a = torch.randn(M, N, device="cuda", dtype=torch.float16)
172+
b = torch.randn(M, N, device="cuda", dtype=torch.float16)
173+
174+
out = autotuned_add(a, b)
175+
torch.testing.assert_close(out, a + b)
176+
177+
time_torch = benchmark_cuda_function_in_microseconds(lambda: a + b)
178+
time_cute = benchmark_cuda_function_in_microseconds(lambda: autotuned_add(a, b))
179+
180+
rprint(f" PyTorch: {time_torch:.1f} us ({M * N * 3 * 2 / time_torch * 1e-3:.1f} GB/s)")
181+
rprint(f" CuTeDSL: {time_cute:.1f} us ({M * N * 3 * 2 / time_cute * 1e-3:.1f} GB/s)")

0 commit comments

Comments
 (0)