Skip to content

Commit 57be17c

Browse files
[lang] Add example of host-constructed program
Signed-off-by: Asher Mancinelli <amancinelli@nvidia.com>
1 parent 440a44e commit 57be17c

1 file changed

Lines changed: 204 additions & 0 deletions

File tree

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
# SPDX-FileCopyrightText: Copyright (c) <2025> NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
from typing import Callable
6+
from abc import abstractmethod
7+
import torch
8+
import cuda.lang as cl
9+
from dataclasses import dataclass, fields, replace
10+
import pprint
11+
12+
__doc__ = """
13+
Demonstrates the flexibility of frozen dataclasses by constructing a dynamic
14+
program out of dataclasses on the host, analyzing the program on the host,
15+
and executing the program on the device.
16+
"""
17+
18+
19+
@dataclass(frozen=True)
20+
class Context:
21+
tensor: cl.Array
22+
iv: int = 0
23+
n: int = 0
24+
25+
26+
@dataclass(frozen=True)
27+
class AST:
28+
@abstractmethod
29+
def __call__(self, context: Context) -> Context: ...
30+
31+
def __str__(self):
32+
return pprint.pformat(self, indent=2, width=60)
33+
34+
def visit(self, f):
35+
f(self)
36+
for field in fields(self):
37+
attr = getattr(self, field.name)
38+
attr.visit(f)
39+
40+
41+
@dataclass(frozen=True)
42+
class ProgN(AST):
43+
body: tuple
44+
45+
def __call__(self, context):
46+
for expr in cl.static_iter(self.body):
47+
context = expr(context)
48+
return context
49+
50+
def visit(self, f):
51+
f(self)
52+
for expression in self.body:
53+
expression.visit(f)
54+
55+
56+
@dataclass(frozen=True)
57+
class If(AST):
58+
condition: AST
59+
then: AST
60+
else_: AST
61+
62+
def __call__(self, context):
63+
if self.condition(context):
64+
context = self.then(context)
65+
else:
66+
context = self.else_(context)
67+
return context
68+
69+
70+
@dataclass(frozen=True)
71+
class Loop(AST):
72+
condition: AST
73+
body: AST
74+
75+
def __call__(self, context):
76+
while self.condition(context):
77+
context = self.body(context)
78+
return context
79+
80+
81+
@dataclass(frozen=True)
82+
class ForN(AST):
83+
get_n: AST
84+
body: AST
85+
86+
def __call__(self, context):
87+
context = self.get_n(context)
88+
for iv in range(context.n):
89+
context = replace(context, iv=iv)
90+
context = self.body(context)
91+
return context
92+
93+
94+
@dataclass(frozen=True)
95+
class Call(AST):
96+
function: Callable
97+
98+
def __call__(self, context):
99+
return self.function(context)
100+
101+
def visit(self, f):
102+
f(self)
103+
104+
105+
def assign_to_tensor(context: Context):
106+
context.tensor[context.iv] = context.iv
107+
return context
108+
109+
110+
def get_tensor_length(context):
111+
return replace(context, n=context.tensor.shape[0])
112+
113+
114+
def print_tensor_element(context):
115+
print("Assigned to tensor element", context.iv)
116+
return context
117+
118+
119+
def printme(message):
120+
def do_print(context):
121+
print(message)
122+
return context
123+
124+
return Call(do_print)
125+
126+
127+
def iv_is_even(context):
128+
return context.iv % 2 == 0
129+
130+
131+
schedule = ProgN(
132+
(
133+
printme("start kernel"),
134+
ForN(
135+
get_n=Call(get_tensor_length),
136+
body=If(
137+
condition=Call(iv_is_even),
138+
then=ProgN(
139+
(
140+
Call(assign_to_tensor),
141+
Call(print_tensor_element),
142+
)
143+
),
144+
else_=printme("skipping odd iteration"),
145+
),
146+
),
147+
printme("end kernel"),
148+
)
149+
)
150+
151+
152+
@dataclass
153+
class Visitor:
154+
seen_progn: bool = False
155+
seen_nested_progn: bool = False
156+
157+
def __call__(self, node):
158+
got_progn = isinstance(node, ProgN)
159+
self.seen_nested_progn |= self.seen_progn and got_progn
160+
self.seen_progn = self.seen_progn or got_progn
161+
162+
163+
def analyze_program(program):
164+
"""Example analysis traversing and analyzing the program on the host"""
165+
visitor = Visitor()
166+
program.visit(visitor)
167+
assert visitor.seen_nested_progn
168+
169+
170+
def test_device_lisp():
171+
analyze_program(schedule)
172+
import subprocess
173+
import sys
174+
from test.util import filecheck
175+
176+
args = [sys.executable, __file__]
177+
out = subprocess.run(args, capture_output=True, text=True, check=True)
178+
filecheck(
179+
out.stdout,
180+
"""
181+
CHECK: start kernel
182+
CHECK-NEXT: Assigned to tensor element 0
183+
CHECK-NEXT: skipping odd iteration
184+
CHECK-NEXT: Assigned to tensor element 2
185+
CHECK-NEXT: skipping odd iteration
186+
CHECK-NEXT: Assigned to tensor element 4
187+
CHECK-NEXT: skipping odd iteration
188+
CHECK-NEXT: Assigned to tensor element 6
189+
CHECK-NEXT: skipping odd iteration
190+
CHECK-NEXT: end kernel
191+
""",
192+
)
193+
194+
195+
if __name__ == "__main__":
196+
197+
@cl.kernel
198+
def kernel(tensor):
199+
schedule(Context(tensor))
200+
201+
out = torch.ones(8, dtype=torch.int8).cuda()
202+
cl.launch(torch.cuda.current_stream(), (1,), (1,), kernel, (out,))
203+
out = out.cpu().tolist()
204+
assert out == [0, 1, 2, 1, 4, 1, 6, 1]

0 commit comments

Comments
 (0)