Skip to content

Commit a706ad2

Browse files
Add external C functions support for JIT (Xilinx#2475)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent fa05f4f commit a706ad2

12 files changed

Lines changed: 1532 additions & 36 deletions

File tree

.github/workflows/buildAndTestRyzenAI.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,4 +187,4 @@ jobs:
187187
ninja check-reference-designs
188188
ninja check-programming-guide
189189
190-
popd
190+
popd

python/iron/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from .globalbuffer import GlobalBuffer
2-
from .kernel import Kernel
2+
from .kernel import ExternalFunction, Kernel
33
from .localbuffer import LocalBuffer
44
from .program import Program
55
from .worker import Worker, WorkerRuntimeBarrier

python/iron/compile/compile.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ def compile_cxx_core_function(
1515
source_path: str,
1616
target_arch: str,
1717
output_path: str,
18+
include_dirs=None,
1819
compile_args=None,
1920
cwd=None,
2021
verbose=False,
@@ -28,8 +29,9 @@ def compile_cxx_core_function(
2829
source_path (str): Path to C++ source.
2930
target_arch (str): Target architecture, e.g., aie2.
3031
output_path (str): Output object file path.
31-
compile_args (list[str]): Compile arguments to peano.
32-
cwd (str): Overrides the current working directory.
32+
include_dirs (list[str], optional): List of include directories to add with -I.
33+
compile_args (list[str], optional): Additional compile arguments to peano.
34+
cwd (str, optional): Overrides the current working directory.
3335
verbose (bool): Enable verbose output.
3436
"""
3537
cmd = [
@@ -48,8 +50,16 @@ def compile_cxx_core_function(
4850
"-DNDEBUG",
4951
f"--target={target_arch}-none-unknown-elf",
5052
]
53+
54+
# Add include directories
55+
if include_dirs:
56+
for include_dir in include_dirs:
57+
cmd.extend(["-I", include_dir])
58+
59+
# Add additional compile arguments
5160
if compile_args:
5261
cmd = cmd + compile_args
62+
5363
if verbose:
5464
print("Executing:", " ".join(cmd))
5565
ret = subprocess.run(

python/iron/jit.py

Lines changed: 172 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,16 @@
1111
import hashlib
1212
import numpy as np
1313
import pyxrt as xrt
14+
import shutil
15+
import sys
16+
import traceback
1417

1518
from aie.extras.context import mlir_mod_ctx
1619
from ..utils.compile import compile_mlir_module_to_binary
1720
from ..utils.xrt import read_insts_binary
21+
from .device import NPU1, NPU2, NPU1Col1, NPU2Col1
22+
from .config import get_current_device
23+
from aie.dialects.aie import AIEDevice
1824

1925

2026
# The `iron.jit` decorator below caches compiled kenrels inside the `IRON_CACHE_DIR` directory.
@@ -93,11 +99,15 @@ def __call__(self, *args):
9399
kernel_args = []
94100

95101
for tensor in args:
102+
# Skip callable arguments since these are inlined in the kernel
103+
if callable(tensor):
104+
continue
96105
if not hasattr(tensor, "buffer_object"):
97106
raise TypeError(
98107
f"Expected Tensor with .buffer_object(), got {type(tensor)}"
99108
)
100109
kernel_args.append(tensor.buffer_object())
110+
101111
h = self.__kernel(opcode, self.__insts_buffer_bo, self.__n_insts, *kernel_args)
102112
r = h.wait()
103113
if r != xrt.ert_cmd_state.ERT_CMD_STATE_COMPLETED:
@@ -133,18 +143,61 @@ def jit(function=None, is_placed=True, use_cache=True):
133143

134144
@functools.wraps(function)
135145
def decorator(*args, **kwargs):
136-
if is_placed:
137-
with mlir_mod_ctx() as ctx:
138-
function(*args, **kwargs)
139-
assert (
140-
ctx.module.operation.verify()
141-
), f"Verification failed for '{function.__name__}'"
142-
mlir_module = ctx.module
143-
else:
144-
mlir_module = function(*args, **kwargs)
146+
from .kernel import ExternalFunction
147+
148+
# Clear any instances from previous runs to make sure if the user provided any broken code we don't try to recompile it
149+
ExternalFunction._instances.clear()
150+
151+
# Find ExternalFunction instances in arguments and kwargs
152+
external_kernels = []
153+
for arg in args:
154+
if isinstance(arg, ExternalFunction):
155+
external_kernels.append(arg)
156+
for value in kwargs.values():
157+
if isinstance(value, ExternalFunction):
158+
external_kernels.append(value)
145159

146-
# Hash of the IR string
147-
module_hash = hash_module(mlir_module)
160+
# Execute the function to generate MLIR
161+
try:
162+
if is_placed:
163+
with mlir_mod_ctx() as ctx:
164+
function(*args, **kwargs)
165+
assert (
166+
ctx.module.operation.verify()
167+
), f"Verification failed for '{function.__name__}'"
168+
mlir_module = ctx.module
169+
else:
170+
mlir_module = function(*args, **kwargs)
171+
except Exception as e:
172+
raise
173+
174+
# Compile all ExternalFunction instances that were created during this JIT compilation
175+
for func in ExternalFunction._instances:
176+
if (
177+
not hasattr(func, "_compiled") or not func._compiled
178+
): # Don't compile if already compiled
179+
external_kernels.append(func)
180+
181+
# Determine target architecture based on device type
182+
try:
183+
current_device = get_current_device()
184+
185+
# Determine target architecture based on device type
186+
if isinstance(current_device, (NPU2, NPU2Col1)):
187+
target_arch = "aie2p"
188+
elif isinstance(current_device, (NPU1, NPU1Col1)):
189+
target_arch = "aie2"
190+
elif current_device in (AIEDevice.npu2, AIEDevice.npu2_1col):
191+
target_arch = "aie2p"
192+
elif current_device in (AIEDevice.npu1, AIEDevice.npu1_1col):
193+
target_arch = "aie2"
194+
else:
195+
raise RuntimeError(f"Unsupported device type: {type(current_device)}")
196+
except Exception as e:
197+
raise
198+
199+
# Hash of the IR string, ExternalFunction compiler options, and target architecture
200+
module_hash = hash_module(mlir_module, external_kernels, target_arch)
148201
kernel_dir = os.path.join(IRON_CACHE_DIR, f"{module_hash}")
149202
mlir_path = os.path.join(kernel_dir, "aie.mlir")
150203

@@ -161,25 +214,120 @@ def decorator(*args, **kwargs):
161214
inst_exists = os.path.exists(inst_path)
162215

163216
if not use_cache or not xclbin_exists or not inst_exists:
164-
with open(mlir_path, "w", encoding="utf-8") as f:
165-
print(mlir_module, file=f)
166-
compile_mlir_module_to_binary(
167-
mlir_module=mlir_module,
168-
inst_path=inst_path,
169-
xclbin_path=xclbin_path,
170-
)
217+
try:
218+
with open(mlir_path, "w", encoding="utf-8") as f:
219+
print(mlir_module, file=f)
220+
221+
# Compile ExternalFunctions from inside the JIT compilation directory
222+
for func in external_kernels:
223+
compile_external_kernel(func, kernel_dir, target_arch)
224+
225+
# Compile the MLIR module
226+
compile_mlir_module_to_binary(
227+
mlir_module=mlir_module,
228+
inst_path=inst_path,
229+
xclbin_path=xclbin_path,
230+
work_dir=kernel_dir,
231+
)
232+
except Exception as e:
233+
# Clean up cache directory on any compilation failure to avoid any corrupted objects in the cache
234+
if os.path.exists(kernel_dir):
235+
shutil.rmtree(kernel_dir)
236+
raise e
171237

172238
kernel_name = "MLIR_AIE"
173-
return NPUKernel(xclbin_path, inst_path, kernel_name=kernel_name)(
174-
*args, **kwargs
175-
)
239+
try:
240+
kernel = NPUKernel(xclbin_path, inst_path, kernel_name=kernel_name)
241+
result = kernel(*args, **kwargs)
242+
return result
243+
except Exception as e:
244+
raise
176245

177246
return decorator
178247

179248

180-
def hash_module(module):
249+
def compile_external_kernel(func, kernel_dir, target_arch):
181250
"""
182-
Hash the MLIR module to create a unique identifier.
251+
Compile an ExternalFunction to an object file in the kernel directory.
252+
253+
Args:
254+
func: ExternalFunction instance to compile
255+
kernel_dir: Directory to place the compiled object file
256+
target_arch: Target architecture (e.g., "aie2" or "aie2p")
257+
"""
258+
# Skip if already compiled
259+
if hasattr(func, "_compiled") and func._compiled:
260+
return
261+
262+
# Check if object file already exists in kernel directory
263+
output_file = os.path.join(kernel_dir, func._object_file_name)
264+
if os.path.exists(output_file):
265+
return
266+
267+
# Create source file in kernel directory
268+
source_file = os.path.join(kernel_dir, f"{func._name}.cc")
269+
270+
# Handle both source_string and source_file cases
271+
if func._source_string is not None:
272+
# Use source_string (write to file)
273+
try:
274+
with open(source_file, "w") as f:
275+
f.write(func._source_string)
276+
except Exception as e:
277+
raise
278+
elif func._source_file is not None:
279+
# Use source_file (copy existing file)
280+
# Check if source file exists before copying
281+
if os.path.exists(func._source_file):
282+
try:
283+
shutil.copy2(func._source_file, source_file)
284+
except Exception as e:
285+
raise
286+
else:
287+
return
288+
else:
289+
raise ValueError("Neither source_string nor source_file is provided")
290+
291+
from .compile.compile import compile_cxx_core_function
292+
293+
try:
294+
compile_cxx_core_function(
295+
source_path=source_file,
296+
target_arch=target_arch,
297+
output_path=output_file,
298+
include_dirs=func._include_dirs,
299+
compile_args=func._compile_flags,
300+
cwd=kernel_dir,
301+
verbose=False,
302+
)
303+
except Exception as e:
304+
raise
305+
306+
# Mark the function as compiled
307+
func._compiled = True
308+
309+
310+
def hash_module(module, external_kernels=None, target_arch=None):
311+
"""
312+
Hash the MLIR module and ExternalFunction compiler options to create a unique identifier.
183313
"""
184314
mlir_str = str(module)
185-
return hashlib.sha256(mlir_str.encode("utf-8")).hexdigest()[:16]
315+
316+
# Include ExternalFunction compiler options in the hash
317+
if external_kernels:
318+
compiler_options = []
319+
for func in external_kernels:
320+
compiler_options.extend(func._include_dirs)
321+
compiler_options.extend(func._compile_flags)
322+
323+
# Create a combined string for hashing
324+
combined_str = mlir_str + "|" + "|".join(compiler_options)
325+
else:
326+
combined_str = mlir_str
327+
328+
# Include target architecture in the hash
329+
if target_arch:
330+
combined_str += f"|target_arch={target_arch}"
331+
332+
hash_result = hashlib.sha256(combined_str.encode("utf-8")).hexdigest()[:16]
333+
return hash_result

0 commit comments

Comments
 (0)