diff --git a/.github/workflows/buildAndTestRyzenAI.yml b/.github/workflows/buildAndTestRyzenAI.yml index 47e1146d73f..4c10bcec300 100644 --- a/.github/workflows/buildAndTestRyzenAI.yml +++ b/.github/workflows/buildAndTestRyzenAI.yml @@ -187,4 +187,4 @@ jobs: ninja check-reference-designs ninja check-programming-guide - popd + popd \ No newline at end of file diff --git a/python/iron/__init__.py b/python/iron/__init__.py index c6982d5357e..228f4bcb0a8 100644 --- a/python/iron/__init__.py +++ b/python/iron/__init__.py @@ -1,5 +1,5 @@ from .globalbuffer import GlobalBuffer -from .kernel import Kernel +from .kernel import ExternalFunction, Kernel from .localbuffer import LocalBuffer from .program import Program from .worker import Worker, WorkerRuntimeBarrier diff --git a/python/iron/compile/compile.py b/python/iron/compile/compile.py index d6c883a6dbb..a08757c493f 100644 --- a/python/iron/compile/compile.py +++ b/python/iron/compile/compile.py @@ -15,6 +15,7 @@ def compile_cxx_core_function( source_path: str, target_arch: str, output_path: str, + include_dirs=None, compile_args=None, cwd=None, verbose=False, @@ -28,8 +29,9 @@ def compile_cxx_core_function( source_path (str): Path to C++ source. target_arch (str): Target architecture, e.g., aie2. output_path (str): Output object file path. - compile_args (list[str]): Compile arguments to peano. - cwd (str): Overrides the current working directory. + include_dirs (list[str], optional): List of include directories to add with -I. + compile_args (list[str], optional): Additional compile arguments to peano. + cwd (str, optional): Overrides the current working directory. verbose (bool): Enable verbose output. """ cmd = [ @@ -48,8 +50,16 @@ def compile_cxx_core_function( "-DNDEBUG", f"--target={target_arch}-none-unknown-elf", ] + + # Add include directories + if include_dirs: + for include_dir in include_dirs: + cmd.extend(["-I", include_dir]) + + # Add additional compile arguments if compile_args: cmd = cmd + compile_args + if verbose: print("Executing:", " ".join(cmd)) ret = subprocess.run( diff --git a/python/iron/jit.py b/python/iron/jit.py index f4567bde404..f82b69182d5 100644 --- a/python/iron/jit.py +++ b/python/iron/jit.py @@ -11,10 +11,16 @@ import hashlib import numpy as np import pyxrt as xrt +import shutil +import sys +import traceback from aie.extras.context import mlir_mod_ctx from ..utils.compile import compile_mlir_module_to_binary from ..utils.xrt import read_insts_binary +from .device import NPU1, NPU2, NPU1Col1, NPU2Col1 +from .config import get_current_device +from aie.dialects.aie import AIEDevice # The `iron.jit` decorator below caches compiled kenrels inside the `IRON_CACHE_DIR` directory. @@ -93,11 +99,15 @@ def __call__(self, *args): kernel_args = [] for tensor in args: + # Skip callable arguments since these are inlined in the kernel + if callable(tensor): + continue if not hasattr(tensor, "buffer_object"): raise TypeError( f"Expected Tensor with .buffer_object(), got {type(tensor)}" ) kernel_args.append(tensor.buffer_object()) + h = self.__kernel(opcode, self.__insts_buffer_bo, self.__n_insts, *kernel_args) r = h.wait() if r != xrt.ert_cmd_state.ERT_CMD_STATE_COMPLETED: @@ -133,18 +143,61 @@ def jit(function=None, is_placed=True, use_cache=True): @functools.wraps(function) def decorator(*args, **kwargs): - if is_placed: - with mlir_mod_ctx() as ctx: - function(*args, **kwargs) - assert ( - ctx.module.operation.verify() - ), f"Verification failed for '{function.__name__}'" - mlir_module = ctx.module - else: - mlir_module = function(*args, **kwargs) + from .kernel import ExternalFunction + + # Clear any instances from previous runs to make sure if the user provided any broken code we don't try to recompile it + ExternalFunction._instances.clear() + + # Find ExternalFunction instances in arguments and kwargs + external_kernels = [] + for arg in args: + if isinstance(arg, ExternalFunction): + external_kernels.append(arg) + for value in kwargs.values(): + if isinstance(value, ExternalFunction): + external_kernels.append(value) - # Hash of the IR string - module_hash = hash_module(mlir_module) + # Execute the function to generate MLIR + try: + if is_placed: + with mlir_mod_ctx() as ctx: + function(*args, **kwargs) + assert ( + ctx.module.operation.verify() + ), f"Verification failed for '{function.__name__}'" + mlir_module = ctx.module + else: + mlir_module = function(*args, **kwargs) + except Exception as e: + raise + + # Compile all ExternalFunction instances that were created during this JIT compilation + for func in ExternalFunction._instances: + if ( + not hasattr(func, "_compiled") or not func._compiled + ): # Don't compile if already compiled + external_kernels.append(func) + + # Determine target architecture based on device type + try: + current_device = get_current_device() + + # Determine target architecture based on device type + if isinstance(current_device, (NPU2, NPU2Col1)): + target_arch = "aie2p" + elif isinstance(current_device, (NPU1, NPU1Col1)): + target_arch = "aie2" + elif current_device in (AIEDevice.npu2, AIEDevice.npu2_1col): + target_arch = "aie2p" + elif current_device in (AIEDevice.npu1, AIEDevice.npu1_1col): + target_arch = "aie2" + else: + raise RuntimeError(f"Unsupported device type: {type(current_device)}") + except Exception as e: + raise + + # Hash of the IR string, ExternalFunction compiler options, and target architecture + module_hash = hash_module(mlir_module, external_kernels, target_arch) kernel_dir = os.path.join(IRON_CACHE_DIR, f"{module_hash}") mlir_path = os.path.join(kernel_dir, "aie.mlir") @@ -161,25 +214,120 @@ def decorator(*args, **kwargs): inst_exists = os.path.exists(inst_path) if not use_cache or not xclbin_exists or not inst_exists: - with open(mlir_path, "w", encoding="utf-8") as f: - print(mlir_module, file=f) - compile_mlir_module_to_binary( - mlir_module=mlir_module, - inst_path=inst_path, - xclbin_path=xclbin_path, - ) + try: + with open(mlir_path, "w", encoding="utf-8") as f: + print(mlir_module, file=f) + + # Compile ExternalFunctions from inside the JIT compilation directory + for func in external_kernels: + compile_external_kernel(func, kernel_dir, target_arch) + + # Compile the MLIR module + compile_mlir_module_to_binary( + mlir_module=mlir_module, + inst_path=inst_path, + xclbin_path=xclbin_path, + work_dir=kernel_dir, + ) + except Exception as e: + # Clean up cache directory on any compilation failure to avoid any corrupted objects in the cache + if os.path.exists(kernel_dir): + shutil.rmtree(kernel_dir) + raise e kernel_name = "MLIR_AIE" - return NPUKernel(xclbin_path, inst_path, kernel_name=kernel_name)( - *args, **kwargs - ) + try: + kernel = NPUKernel(xclbin_path, inst_path, kernel_name=kernel_name) + result = kernel(*args, **kwargs) + return result + except Exception as e: + raise return decorator -def hash_module(module): +def compile_external_kernel(func, kernel_dir, target_arch): """ - Hash the MLIR module to create a unique identifier. + Compile an ExternalFunction to an object file in the kernel directory. + + Args: + func: ExternalFunction instance to compile + kernel_dir: Directory to place the compiled object file + target_arch: Target architecture (e.g., "aie2" or "aie2p") + """ + # Skip if already compiled + if hasattr(func, "_compiled") and func._compiled: + return + + # Check if object file already exists in kernel directory + output_file = os.path.join(kernel_dir, func._object_file_name) + if os.path.exists(output_file): + return + + # Create source file in kernel directory + source_file = os.path.join(kernel_dir, f"{func._name}.cc") + + # Handle both source_string and source_file cases + if func._source_string is not None: + # Use source_string (write to file) + try: + with open(source_file, "w") as f: + f.write(func._source_string) + except Exception as e: + raise + elif func._source_file is not None: + # Use source_file (copy existing file) + # Check if source file exists before copying + if os.path.exists(func._source_file): + try: + shutil.copy2(func._source_file, source_file) + except Exception as e: + raise + else: + return + else: + raise ValueError("Neither source_string nor source_file is provided") + + from .compile.compile import compile_cxx_core_function + + try: + compile_cxx_core_function( + source_path=source_file, + target_arch=target_arch, + output_path=output_file, + include_dirs=func._include_dirs, + compile_args=func._compile_flags, + cwd=kernel_dir, + verbose=False, + ) + except Exception as e: + raise + + # Mark the function as compiled + func._compiled = True + + +def hash_module(module, external_kernels=None, target_arch=None): + """ + Hash the MLIR module and ExternalFunction compiler options to create a unique identifier. """ mlir_str = str(module) - return hashlib.sha256(mlir_str.encode("utf-8")).hexdigest()[:16] + + # Include ExternalFunction compiler options in the hash + if external_kernels: + compiler_options = [] + for func in external_kernels: + compiler_options.extend(func._include_dirs) + compiler_options.extend(func._compile_flags) + + # Create a combined string for hashing + combined_str = mlir_str + "|" + "|".join(compiler_options) + else: + combined_str = mlir_str + + # Include target architecture in the hash + if target_arch: + combined_str += f"|target_arch={target_arch}" + + hash_result = hashlib.sha256(combined_str.encode("utf-8")).hexdigest()[:16] + return hash_result diff --git a/python/iron/kernel.py b/python/iron/kernel.py index a46304d84c4..85678ffa16f 100644 --- a/python/iron/kernel.py +++ b/python/iron/kernel.py @@ -7,6 +7,7 @@ # (c) Copyright 2024 Advanced Micro Devices, Inc. import numpy as np +import os from .. import ir # type: ignore from ..extras.dialects.ext.func import FuncOp # type: ignore @@ -15,7 +16,36 @@ from .resolvable import Resolvable -class Kernel(Resolvable): +class BaseKernel(Resolvable): + """Base class for kernel-like objects that resolve to FuncOp.""" + + def __init__(self, name: str, arg_types: list[type[np.ndarray] | np.dtype] = []): + """Initialize base kernel. + + Args: + name (str): The name of the function + arg_types (list[type[np.ndarray] | np.dtype], optional): The type signature of the function. Defaults to []. + """ + self._name = name + self._arg_types = arg_types + self._op: FuncOp | None = None + + def resolve( + self, + loc: ir.Location | None = None, + ip: ir.InsertionPoint | None = None, + ) -> None: + """Resolve the kernel to a FuncOp. Must be implemented by subclasses.""" + raise NotImplementedError("Subclasses must implement resolve()") + + def __call__(self, *args, **kwargs): + """Call the kernel with the given arguments.""" + if not self._op: + raise ValueError("Need to resolve kernel before it can be called") + call(self._op, args, **kwargs) + + +class Kernel(BaseKernel): def __init__( self, name: str, @@ -30,10 +60,8 @@ def __init__( bin_name (str): The name of the binary (used for linking to a compute core) arg_types (list[type[np.ndarray] | np.dtype], optional): The type signature of the function. Defaults to []. """ - self._name = name + super().__init__(name, arg_types) self._bin_name = bin_name - self._arg_types = arg_types - self._op: FuncOp | None = None @property def bin_name(self) -> str: @@ -47,7 +75,115 @@ def resolve( if not self._op: self._op = external_func(self._name, inputs=self._arg_types) + +class ExternalFunction(BaseKernel): + _object_files = set() + _instances = set() + + def __init__( + self, + name: str, + source_file: str | None = None, + source_string: str | None = None, + arg_types: list[type[np.ndarray] | np.dtype] = [], + include_dirs: list[str] = [], + compile_flags: list[str] = [], + ) -> None: + """An ExternalFunction is a C/C++ source file that gets compiled to an object file and eventually resolves to a FuncOp. + If it is called, a CallOp will be generated. + + Args: + name (str): The name of the function + source_file (str): Path to the C/C++ source file + source_string (str): C/C++ source code as a string + arg_types (list[type[np.ndarray] | np.dtype], optional): The type signature of the function. Defaults to []. + include_dirs (list[str], optional): Additional include directories. Defaults to []. + compile_flags (list[str], optional): Additional compilation flags. Defaults to []. + """ + super().__init__(name, arg_types) + self._setup_source(source_file, source_string) + self._include_dirs = include_dirs + self._compile_flags = compile_flags + self._object_file_name = f"{self._name}.o" + self._compiled = False + + # Track this instance for JIT compilation + ExternalFunction._instances.add(self) + + def _setup_source(self, source_file: str | None, source_string: str | None) -> None: + """Set up the source file for compilation.""" + if source_file is not None: + self._source_file = source_file + self._source_string = None + else: + if source_string is None: + raise ValueError("source_file or source_string must be provided") + self._source_file = None + self._source_string = source_string + + def __enter__(self): + """Enter the context.""" + return self + + def __exit__(self, exc_type, exc_value, traceback): + """Exit the context.""" + pass + + @property + def bin_name(self) -> str: + return self._object_file_name + + def tile_size(self, arg_index: int = 0) -> int: + """Get the tile size from the specified array argument type. + + Args: + arg_index (int): Index of the argument to get tile size from. Defaults to 0. + + Returns: + int: The tile size (first dimension) of the specified argument. + """ + if not self._arg_types: + raise ValueError("No argument types defined") + if arg_index >= len(self._arg_types): + raise ValueError( + f"Argument index {arg_index} out of range (max: {len(self._arg_types) - 1})" + ) + + arg = self._arg_types[arg_index] + + # Handle numpy array types like np.ndarray[(16,), np.dtype[np.int32]] + if hasattr(arg, "__args__") and len(arg.__args__) > 0: + # For types like np.ndarray[(16,), np.dtype[np.int32]], the shape is in __args__[0] + shape_arg = arg.__args__[0] + if isinstance(shape_arg, tuple) and len(shape_arg) > 0: + return shape_arg[0] + + # Handle MLIR types like MemRefType(memref<16xi32>) + if ( + hasattr(arg, "shape") + and hasattr(arg.shape, "__len__") + and len(arg.shape) > 0 + ): + return arg.shape[0] + + raise ValueError( + f"Argument {arg_index} does not have a shape or is not an array type" + ) + + def arg_types(self) -> list: + """Get the argument types of the ExternalFunction.""" + return self._arg_types.copy() + + def resolve( + self, + loc: ir.Location | None = None, + ip: ir.InsertionPoint | None = None, + ) -> None: + if not self._op: + # Create the external function + self._op = external_func(self._name, inputs=self._arg_types) + def __call__(self, *args, **kwargs): if not self._op: - raise ValueError("Need to resolve Kernel before it can be called") + raise ValueError("Need to resolve ExternalFunction before it can be called") call(self._op, args, **kwargs) diff --git a/python/iron/tensor.py b/python/iron/tensor.py index 804d742e1c5..358cdd13ec3 100644 --- a/python/iron/tensor.py +++ b/python/iron/tensor.py @@ -196,6 +196,19 @@ def numpy(self): self.__sync_from_device() return self.data + def fill_(self, value): + """ + Fills the tensor with a scalar value (in-place operation). + + Parameters: + value: The scalar value to fill the tensor with. + + Note: For NPU tensors, this method syncs the filled data to device after modification. + """ + self.data.fill(value) + if self.device == "npu": + self.__sync_to_device() + @staticmethod def _ctype_from_dtype(dtype): """ diff --git a/python/iron/worker.py b/python/iron/worker.py index 78b6f837f61..9791f65c72b 100644 --- a/python/iron/worker.py +++ b/python/iron/worker.py @@ -16,7 +16,7 @@ from .device import PlacementTile, AnyComputeTile, Tile from .dataflow.objectfifo import ObjectFifoHandle, ObjectFifo from .dataflow.endpoint import ObjectFifoEndpoint -from .kernel import Kernel +from .kernel import Kernel, ExternalFunction from .globalbuffer import GlobalBuffer from .resolvable import Resolvable @@ -86,7 +86,7 @@ def do_nothing_core_fun(*args) -> None: # Check arguments to the core. Some information is saved for resolution. for arg in self.fn_args: - if isinstance(arg, Kernel): + if isinstance(arg, (Kernel, ExternalFunction)): bin_names.add(arg.bin_name) elif isinstance(arg, ObjectFifoHandle): arg.endpoint = self diff --git a/python/utils/compile.py b/python/utils/compile.py index ec1406b6f90..504d00e50a4 100644 --- a/python/utils/compile.py +++ b/python/utils/compile.py @@ -10,7 +10,9 @@ import aie.compiler.aiecc.main as aiecc -def compile_mlir_module_to_binary(mlir_module: str, inst_path: str, xclbin_path: str): +def compile_mlir_module_to_binary( + mlir_module: str, inst_path: str, xclbin_path: str, work_dir: str = None +): """ Compile an MLIR module to instruction and xclbin files using the aiecc module. @@ -18,6 +20,7 @@ def compile_mlir_module_to_binary(mlir_module: str, inst_path: str, xclbin_path: mlir_module (str): MLIR module to compile. inst_path (str): Path to the instruction binary file. xclbin_path (str): Path to the xclbin file. + work_dir (str, optional): Working directory for compilation. Defaults to None. """ args = [ @@ -29,6 +32,11 @@ def compile_mlir_module_to_binary(mlir_module: str, inst_path: str, xclbin_path: f"--xclbin-name={xclbin_path}", f"--npu-insts-name={inst_path}", ] + + # Add working directory if specified + if work_dir: + args.append(f"--tmpdir={work_dir}") + try: aiecc.run(mlir_module, args) except Exception as e: diff --git a/test/python/jit.py b/test/python/jit_compilation.py similarity index 100% rename from test/python/jit.py rename to test/python/jit_compilation.py diff --git a/test/python/jit_extern_functions.py b/test/python/jit_extern_functions.py new file mode 100644 index 00000000000..3a7d59919bd --- /dev/null +++ b/test/python/jit_extern_functions.py @@ -0,0 +1,846 @@ +# This file is licensed under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# (c) Copyright 2025 AMD Inc. + +# RUN: %run_on_npu1% %pytest %s +# RUN: %run_on_npu2% %pytest %s + +import numpy as np +import os +import tempfile +import shutil +import pytest + +import aie.iron as iron +from aie.iron import ExternalFunction, jit +from aie.iron import ObjectFifo, Worker, Runtime, Program +from aie.iron.placers import SequentialPlacer +from aie.iron.controlflow import range_ + + +@jit(is_placed=False) +def transform(input, output, func): + """Transform kernel that applies a function to input tensor and stores result in output tensor.""" + if input.shape != output.shape: + raise ValueError( + f"Input shapes are not the equal ({input.shape} != {output.shape})." + ) + num_elements = np.size(input) + + # Extract tile size from ExternalFunction (using first argument) + tile_size = func.tile_size(0) + + # Assert that input and output arrays have the same tile size + assert func.tile_size(0) == func.tile_size( + 1 + ), f"Input and output tile sizes must match: {func.tile_size(0)} != {func.tile_size(1)}" + + if num_elements % tile_size != 0: + raise ValueError( + f"Number of elements ({num_elements}) must be a multiple of {tile_size}." + ) + num_tiles = num_elements // tile_size + + if input.dtype != output.dtype: + raise ValueError( + f"Input data types are not the same ({input.dtype} != {output.dtype})." + ) + + dtype = input.dtype + + # Define tensor types + tensor_ty = np.ndarray[(num_elements,), np.dtype[dtype]] + tile_ty = np.ndarray[(tile_size,), np.dtype[dtype]] + + # AIE-array data movement with object fifos + of_in = ObjectFifo(tile_ty, name="in") + of_out = ObjectFifo(tile_ty, name="out") + + # Define a task that will run on a compute tile + def core_body(of_in, of_out, func_to_apply): + # Extract tile size from ExternalFunction (using first argument) + tile_size = func_to_apply.tile_size(0) + + # Number of sub-vector "tile" iterations + for i in range_(num_tiles): + elem_in = of_in.acquire(1) + elem_out = of_out.acquire(1) + func_to_apply(elem_in, elem_out, tile_size) + of_in.release(1) + of_out.release(1) + + # Create a worker to run the task on a compute tile + worker = Worker(core_body, fn_args=[of_in.cons(), of_out.prod(), func]) + + # Runtime operations to move data to/from the AIE-array + rt = Runtime() + with rt.sequence(tensor_ty, tensor_ty) as (A, B): + rt.start(worker) + rt.fill(of_in.prod(), A) + rt.drain(of_out.cons(), B, wait=True) + + # Place program components (assign them resources on the device) and generate an MLIR module + return Program(iron.get_current_device(), rt).resolve_program(SequentialPlacer()) + + +def test_simple_add_one(): + """Test basic ExternalFunction with simple add_one operation.""" + # Create input and output tensors + input_tensor = iron.randint(0, 100, (1024,), dtype=np.int32, device="npu") + output_tensor = iron.zeros((1024,), dtype=np.int32, device="npu") + initial_tensor = input_tensor.numpy().copy() + + # Create ExternalFunction for adding one + add_one = ExternalFunction( + "add_one", + source_string="""extern "C" { + void add_one(int* input, int* output, int tile_size) { + for (int i = 0; i < tile_size; i++) { + output[i] = input[i] + 1; + } + } + }""", + arg_types=[ + np.ndarray[(16,), np.dtype[np.int32]], + np.ndarray[(16,), np.dtype[np.int32]], + np.int32, + ], + ) + + # Apply the transform + print("Applying transform") + print(f"Input tensor: {input_tensor}") + print(f"Output tensor: {output_tensor}") + print(f"Initial tensor: {initial_tensor}") + transform(input_tensor, output_tensor, add_one) + + # Verify results + expected = initial_tensor + 1 + actual = output_tensor.numpy() + print(f"Actual tensor: {actual}") + print(f"Expected tensor: {expected}") + np.testing.assert_array_equal(actual, expected) + + +@pytest.mark.parametrize("tile_size", [8, 16, 32, 64]) +def test_different_tile_sizes(tile_size): + """Test ExternalFunction with different tile sizes.""" + # Create input and output tensors + num_elements = 1024 + input_tensor = iron.randint(0, 100, (num_elements,), dtype=np.int32, device="npu") + output_tensor = iron.zeros((num_elements,), dtype=np.int32, device="npu") + initial_tensor = input_tensor.numpy().copy() + + # Create ExternalFunction with specific tile size + add_one = ExternalFunction( + "add_one", + source_string="""extern "C" { + void add_one(int* input, int* output, int tile_size) { + for (int i = 0; i < tile_size; i++) { + output[i] = input[i] + 1; + } + } + }""", + arg_types=[ + np.ndarray[(tile_size,), np.dtype[np.int32]], + np.ndarray[(tile_size,), np.dtype[np.int32]], + np.int32, + ], + ) + + # Apply the transform + transform(input_tensor, output_tensor, add_one) + + # Verify results + expected = initial_tensor + 1 + actual = output_tensor.numpy() + np.testing.assert_array_equal(actual, expected) + + +@pytest.mark.parametrize( + "dtype,c_type", + [ + (np.int32, "int"), + (np.float32, "float"), + ], +) +def test_different_data_types(dtype, c_type): + """Test ExternalFunction with different data types.""" + # Create input and output tensors + input_tensor = iron.rand((1024,), dtype=dtype, device="npu") + output_tensor = iron.zeros((1024,), dtype=dtype, device="npu") + initial_tensor = input_tensor.numpy().copy() + + # Create ExternalFunction with specific data type + add_one = ExternalFunction( + "add_one", + source_string=f"""extern "C" {{ + void add_one({c_type}* input, {c_type}* output, int tile_size) {{ + for (int i = 0; i < tile_size; i++) {{ + output[i] = input[i] + 1.0f; + }} + }} + }}""", + arg_types=[ + np.ndarray[(16,), np.dtype[dtype]], + np.ndarray[(16,), np.dtype[dtype]], + np.int32, + ], + ) + + # Apply the transform + transform(input_tensor, output_tensor, add_one) + + # Verify results + expected = initial_tensor + 1.0 + actual = output_tensor.numpy() + np.testing.assert_array_almost_equal(actual, expected, decimal=5) + + +@pytest.mark.parametrize("value", [5, 42]) +def test_define_values(value): + """Test ExternalFunction with different define values.""" + # Create input and output tensors + input_tensor = iron.randint(0, 100, (1024,), dtype=np.int32, device="npu") + output_tensor = iron.zeros((1024,), dtype=np.int32, device="npu") + initial_tensor = input_tensor.numpy().copy() + + add_value = ExternalFunction( + "add_value", + source_string="""extern "C" { + void add_value(int* input, int* output, int tile_size) { + for (int i = 0; i < tile_size; i++) { + output[i] = input[i] + ADD_VALUE; + } + } + }""", + arg_types=[ + np.ndarray[(16,), np.dtype[np.int32]], + np.ndarray[(16,), np.dtype[np.int32]], + np.int32, + ], + compile_flags=[f"-DADD_VALUE={value}"], + ) + + # Apply the transform + transform(input_tensor, output_tensor, add_value) + + # Verify results + expected = initial_tensor + value + actual = output_tensor.numpy() + np.testing.assert_array_equal(actual, expected) + + +def test_multiple_defines(): + """Test ExternalFunction with multiple defines.""" + # Create input and output tensors + input_tensor = iron.randint(0, 100, (1024,), dtype=np.int32, device="npu") + output_tensor = iron.zeros((1024,), dtype=np.int32, device="npu") + initial_tensor = input_tensor.numpy().copy() + + # Create ExternalFunction with multiple defines + complex_op = ExternalFunction( + "complex_op", + source_string="""extern "C" { + void complex_op(int* input, int* output, int tile_size) { + for (int i = 0; i < tile_size; i++) { + #ifdef FLAG2 + output[i] = input[i] + ADD_VALUE + FLAG2_OFFSET; + #else + output[i] = input[i] + ADD_VALUE; + #endif + } + } + }""", + arg_types=[ + np.ndarray[(16,), np.dtype[np.int32]], + np.ndarray[(16,), np.dtype[np.int32]], + np.int32, + ], + compile_flags=["-DADD_VALUE=5", "-DFLAG2", "-DFLAG2_OFFSET=10"], + ) + + # Apply the transform + transform(input_tensor, output_tensor, complex_op) + + # Verify results (should add 15: 5 + 10 due to FLAG2 define) + expected = initial_tensor + 15 + actual = output_tensor.numpy() + np.testing.assert_array_equal(actual, expected) + + +def test_include_directories(): + """Test ExternalFunction with include directories.""" + # Create a temporary directory with a header file + with tempfile.TemporaryDirectory() as temp_dir: + # Create a header file + header_file = os.path.join(temp_dir, "math_ops.h") + with open(header_file, "w") as f: + f.write( + """ +#ifndef MATH_OPS_H +#define MATH_OPS_H + +#define ADD_VALUE 42 + +#endif +""" + ) + + # Create input and output tensors + input_tensor = iron.randint(0, 100, (1024,), dtype=np.int32, device="npu") + output_tensor = iron.zeros((1024,), dtype=np.int32, device="npu") + initial_tensor = input_tensor.numpy().copy() + + # Create ExternalFunction that includes the header + add_value = ExternalFunction( + "add_value", + source_string="""extern "C" { + #include "math_ops.h" + void add_value(int* input, int* output, int tile_size) { + for (int i = 0; i < tile_size; i++) { + output[i] = input[i] + ADD_VALUE; + } + } + }""", + arg_types=[ + np.ndarray[(16,), np.dtype[np.int32]], + np.ndarray[(16,), np.dtype[np.int32]], + np.int32, + ], + include_dirs=[temp_dir], + ) + + # Apply the transform + transform(input_tensor, output_tensor, add_value) + + # Verify results + expected = initial_tensor + 42 + actual = output_tensor.numpy() + np.testing.assert_array_equal(actual, expected) + + +def test_multiple_include_directories(): + """Test ExternalFunction with multiple include directories.""" + # Create temporary directories with header files + with tempfile.TemporaryDirectory() as temp_dir1, tempfile.TemporaryDirectory() as temp_dir2: + # Create header files + header1 = os.path.join(temp_dir1, "ops1.h") + with open(header1, "w") as f: + f.write("#define VALUE1 10\n") + + header2 = os.path.join(temp_dir2, "ops2.h") + with open(header2, "w") as f: + f.write("#define VALUE2 20\n") + + # Create input and output tensors + input_tensor = iron.randint(0, 100, (1024,), dtype=np.int32, device="npu") + output_tensor = iron.zeros((1024,), dtype=np.int32, device="npu") + initial_tensor = input_tensor.numpy().copy() + + # Create ExternalFunction that includes both headers + add_values = ExternalFunction( + "add_values", + source_string="""extern "C" { + #include "ops1.h" + #include "ops2.h" + void add_values(int* input, int* output, int tile_size) { + for (int i = 0; i < tile_size; i++) { + output[i] = input[i] + VALUE1 + VALUE2; + } + } + }""", + arg_types=[ + np.ndarray[(16,), np.dtype[np.int32]], + np.ndarray[(16,), np.dtype[np.int32]], + np.int32, + ], + include_dirs=[temp_dir1, temp_dir2], + ) + + # Apply the transform + transform(input_tensor, output_tensor, add_values) + + # Verify results + expected = initial_tensor + 30 # 10 + 20 + actual = output_tensor.numpy() + np.testing.assert_array_equal(actual, expected) + + +def test_caching_same_source(): + """Test that same source code produces same cached result.""" + # Create input and output tensors + input_tensor = iron.randint(0, 100, (1024,), dtype=np.int32, device="npu") + output_tensor = iron.zeros((1024,), dtype=np.int32, device="npu") + initial_tensor = input_tensor.numpy().copy() + + # Create two ExternalFunctions with identical source + add_one_1 = ExternalFunction( + "add_one_1", + source_string="""extern "C" { + void add_one_1(int* input, int* output, int tile_size) { + for (int i = 0; i < tile_size; i++) { + output[i] = input[i] + 1; + } + } + }""", + arg_types=[ + np.ndarray[(16,), np.dtype[np.int32]], + np.ndarray[(16,), np.dtype[np.int32]], + np.int32, + ], + ) + + add_one_2 = ExternalFunction( + "add_one_2", + source_string="""extern "C" { + void add_one_2(int* input, int* output, int tile_size) { + for (int i = 0; i < tile_size; i++) { + output[i] = input[i] + 1; + } + } + }""", + arg_types=[ + np.ndarray[(16,), np.dtype[np.int32]], + np.ndarray[(16,), np.dtype[np.int32]], + np.int32, + ], + ) + + # Apply both transforms + transform(input_tensor, output_tensor, add_one_1) + result1 = output_tensor.numpy().copy() + + output_tensor.fill_(0) + transform(input_tensor, output_tensor, add_one_2) + result2 = output_tensor.numpy() + + # Verify both produce same results + np.testing.assert_array_equal(result1, result2) + + +def test_context_manager(): + """Test ExternalFunction with context manager syntax.""" + # Create input and output tensors + input_tensor = iron.randint(0, 100, (1024,), dtype=np.int32, device="npu") + output_tensor = iron.zeros((1024,), dtype=np.int32, device="npu") + initial_tensor = input_tensor.numpy().copy() + + # Create ExternalFunction and use it with context manager + with ExternalFunction( + "add_one_context", + source_string="""extern "C" { + void add_one_context(int* input, int* output, int tile_size) { + for (int i = 0; i < tile_size; i++) { + output[i] = input[i] + 1; + } + } + }""", + arg_types=[ + np.ndarray[(16,), np.dtype[np.int32]], + np.ndarray[(16,), np.dtype[np.int32]], + np.int32, + ], + ) as add_one: + # Apply the transform + transform(input_tensor, output_tensor, add_one) + + # Verify results + expected = initial_tensor + 1 + actual = output_tensor.numpy() + np.testing.assert_array_equal(actual, expected) + + +def test_context_manager_with_compiler_options(): + """Test ExternalFunction with context manager and compiler options.""" + # Create input and output tensors + input_tensor = iron.randint(0, 100, (1024,), dtype=np.int32, device="npu") + output_tensor = iron.zeros((1024,), dtype=np.int32, device="npu") + initial_tensor = input_tensor.numpy().copy() + + # Create ExternalFunction with compiler options using context manager + with ExternalFunction( + "add_value_context", + source_string="""extern "C" { + void add_value_context(int* input, int* output, int tile_size) { + for (int i = 0; i < tile_size; i++) { + output[i] = input[i] + ADD_VALUE; + } + } + }""", + arg_types=[ + np.ndarray[(16,), np.dtype[np.int32]], + np.ndarray[(16,), np.dtype[np.int32]], + np.int32, + ], + compile_flags=["-DADD_VALUE=42"], + ) as add_value: + # Apply the transform + transform(input_tensor, output_tensor, add_value) + + # Verify results + expected = initial_tensor + 42 + actual = output_tensor.numpy() + np.testing.assert_array_equal(actual, expected) + + +def test_source_file(): + """Test ExternalFunction with source_file instead of source_string.""" + # Create input and output tensors + input_tensor = iron.randint(0, 100, (1024,), dtype=np.int32, device="npu") + output_tensor = iron.zeros((1024,), dtype=np.int32, device="npu") + initial_tensor = input_tensor.numpy().copy() + + # Create a temporary source file + with tempfile.NamedTemporaryFile(mode="w", suffix=".cc", delete=False) as f: + source_content = """extern "C" { + void add_one_from_file(int* input, int* output, int tile_size) { + for (int i = 0; i < tile_size; i++) { + output[i] = input[i] + 1; + } + } + }""" + f.write(source_content) + source_file_path = f.name + + try: + # Create ExternalFunction using source_file + add_one_from_file = ExternalFunction( + "add_one_from_file", + source_file=source_file_path, + arg_types=[ + np.ndarray[(16,), np.dtype[np.int32]], + np.ndarray[(16,), np.dtype[np.int32]], + np.int32, + ], + ) + + # Apply the transform + transform(input_tensor, output_tensor, add_one_from_file) + + # Verify results + expected = initial_tensor + 1 + actual = output_tensor.numpy() + np.testing.assert_array_equal(actual, expected) + + finally: + # Clean up the temporary file + os.unlink(source_file_path) + + +def test_source_file_with_compiler_options(): + """Test ExternalFunction with source_file and compiler options.""" + # Create input and output tensors + input_tensor = iron.randint(0, 100, (1024,), dtype=np.int32, device="npu") + output_tensor = iron.zeros((1024,), dtype=np.int32, device="npu") + initial_tensor = input_tensor.numpy().copy() + + # Create a temporary source file with defines + with tempfile.NamedTemporaryFile(mode="w", suffix=".cc", delete=False) as f: + source_content = """extern "C" { + void add_value_from_file(int* input, int* output, int tile_size) { + for (int i = 0; i < tile_size; i++) { + output[i] = input[i] + ADD_VALUE; + } + } + }""" + f.write(source_content) + source_file_path = f.name + + try: + # Create ExternalFunction using source_file with compiler options + add_value_from_file = ExternalFunction( + "add_value_from_file", + source_file=source_file_path, + arg_types=[ + np.ndarray[(16,), np.dtype[np.int32]], + np.ndarray[(16,), np.dtype[np.int32]], + np.int32, + ], + compile_flags=["-DADD_VALUE=25"], + ) + + # Apply the transform + transform(input_tensor, output_tensor, add_value_from_file) + + # Verify results + expected = initial_tensor + 25 + actual = output_tensor.numpy() + np.testing.assert_array_equal(actual, expected) + + finally: + # Clean up the temporary file + os.unlink(source_file_path) + + +def test_transform_with_internal_func(): + """Test transform function that creates ExternalFunction internally.""" + # Create input and output tensors + input_tensor = iron.randint(0, 100, (1024,), dtype=np.int32, device="npu") + output_tensor = iron.zeros((1024,), dtype=np.int32, device="npu") + initial_tensor = input_tensor.numpy().copy() + + # Create ExternalFunction dynamically but pass it as argument + internal_func = ExternalFunction( + "internal_add_one", + source_string="""extern "C" { + void internal_add_one(int* input, int* output, int tile_size) { + for (int i = 0; i < tile_size; i++) { + output[i] = input[i] + 1; + } + } + }""", + arg_types=[ + np.ndarray[(16,), np.dtype[np.int32]], + np.ndarray[(16,), np.dtype[np.int32]], + np.int32, + ], + ) + + # Apply the transform (ExternalFunction is passed as argument) + transform(input_tensor, output_tensor, internal_func) + + # Verify results + expected = initial_tensor + 1 + actual = output_tensor.numpy() + np.testing.assert_array_equal(actual, expected) + + +def test_caching_different_flags(): + """Test that different compile flags produce different cached results.""" + # Create input and output tensors + input_tensor = iron.randint(0, 100, (1024,), dtype=np.int32, device="npu") + output_tensor = iron.zeros((1024,), dtype=np.int32, device="npu") + initial_tensor = input_tensor.numpy().copy() + + # Create ExternalFunctions with same source but different flags + add_value_5 = ExternalFunction( + "add_value", + source_string="""extern "C" { + void add_value(int* input, int* output, int tile_size) { + for (int i = 0; i < tile_size; i++) { + output[i] = input[i] + ADD_VALUE; + } + } + }""", + arg_types=[ + np.ndarray[(16,), np.dtype[np.int32]], + np.ndarray[(16,), np.dtype[np.int32]], + np.int32, + ], + compile_flags=["-DADD_VALUE=5"], + ) + + add_value_10 = ExternalFunction( + "add_value", + source_string="""extern "C" { + void add_value(int* input, int* output, int tile_size) { + for (int i = 0; i < tile_size; i++) { + output[i] = input[i] + ADD_VALUE; + } + } + }""", + arg_types=[ + np.ndarray[(16,), np.dtype[np.int32]], + np.ndarray[(16,), np.dtype[np.int32]], + np.int32, + ], + compile_flags=["-DADD_VALUE=10"], + ) + + # Apply transforms + transform(input_tensor, output_tensor, add_value_5) + result_5 = output_tensor.numpy().copy() + + output_tensor.fill_(0) + transform(input_tensor, output_tensor, add_value_10) + result_10 = output_tensor.numpy() + + # Verify different results + expected_5 = initial_tensor + 5 + expected_10 = initial_tensor + 10 + + np.testing.assert_array_equal(result_5, expected_5) + np.testing.assert_array_equal(result_10, expected_10) + np.testing.assert_raises( + AssertionError, np.testing.assert_array_equal, result_5, result_10 + ) + + +@pytest.mark.parametrize( + "invalid_source", + [ + # Missing semicolon + """extern "C" { + void invalid_func(int* input, int* output, int tile_size) { + for (int i = 0; i < tile_size; i++) { + output[i] = input[i] + 1 // Missing semicolon + } + } + }""", + # Undefined variable + """extern "C" { + void invalid_func(int* input, int* output, int tile_size) { + for (int i = 0; i < tile_size; i++) { + output[i] = input[i] + undefined_var; + } + } + }""", + # Syntax error + """extern "C" { + void invalid_func(int* input, int* output, int tile_size) { + for (int i = 0; i < tile_size; i++) { + output[i] = input[i] + 1; + } // Missing closing brace + }""", + ], +) +def test_invalid_source(invalid_source): + """Test error handling for invalid C++ source.""" + # Create input and output tensors + input_tensor = iron.randint(0, 100, (1024,), dtype=np.int32, device="npu") + output_tensor = iron.zeros((1024,), dtype=np.int32, device="npu") + + # Create ExternalFunction with invalid C++ source + invalid_func = ExternalFunction( + "invalid_func", + source_string=invalid_source, + arg_types=[ + np.ndarray[(16,), np.dtype[np.int32]], + np.ndarray[(16,), np.dtype[np.int32]], + np.int32, + ], + ) + + # Should raise an error during compilation + with pytest.raises(Exception): + transform(input_tensor, output_tensor, invalid_func) + + +@pytest.mark.parametrize( + "input_tile_size,output_tile_size", + [ + (16, 32), # Different tile sizes + (8, 16), # Different tile sizes + (64, 32), # Different tile sizes + ], +) +def test_mismatched_tile_sizes(input_tile_size, output_tile_size): + """Test error handling for mismatched tile sizes.""" + # Create input and output tensors + input_tensor = iron.randint(0, 100, (1024,), dtype=np.int32, device="npu") + output_tensor = iron.zeros((1024,), dtype=np.int32, device="npu") + + # Create ExternalFunction with mismatched tile sizes + mismatched_func = ExternalFunction( + "mismatched_func", + source_string="""extern "C" { + void mismatched_func(int* input, int* output, int tile_size) { + for (int i = 0; i < tile_size; i++) { + output[i] = input[i] + 1; + } + } + }""", + arg_types=[ + np.ndarray[(input_tile_size,), np.dtype[np.int32]], + np.ndarray[(output_tile_size,), np.dtype[np.int32]], + np.int32, + ], + ) + + # Should raise an assertion error + with pytest.raises(AssertionError, match="Input and output tile sizes must match"): + transform(input_tensor, output_tensor, mismatched_func) + + +@pytest.mark.parametrize( + "invalid_include", + [ + "/nonexistent/directory", + "/tmp/nonexistent_header_dir", + "/usr/local/include/nonexistent", + ], +) +def test_invalid_include_directory(invalid_include): + """Test error handling for invalid include directory.""" + # Create input and output tensors + input_tensor = iron.randint(0, 100, (1024,), dtype=np.int32, device="npu") + output_tensor = iron.zeros((1024,), dtype=np.int32, device="npu") + + # Create ExternalFunction with invalid include directory + invalid_include_func = ExternalFunction( + "invalid_include_func", + source_string="""extern "C" { + #include "nonexistent.h" + void invalid_include_func(int* input, int* output, int tile_size) { + for (int i = 0; i < tile_size; i++) { + output[i] = input[i] + 1; + } + } + }""", + arg_types=[ + np.ndarray[(16,), np.dtype[np.int32]], + np.ndarray[(16,), np.dtype[np.int32]], + np.int32, + ], + include_dirs=[invalid_include], + ) + + # Should raise an error during compilation + with pytest.raises(Exception): + transform(input_tensor, output_tensor, invalid_include_func) + + +@pytest.mark.parametrize( + "compile_flags,expected_value", + [ + (["-DADD_VALUE=5"], 5), + (["-DADD_VALUE=10", "-DMULTIPLIER=2"], 20), # 10 * 2 + (["-DADD_VALUE=3", "-DOFFSET=7"], 10), # 3 + 7 + (["-DADD_VALUE=1", "-DFLAG2", "-DFLAG2_OFFSET=9"], 10), # 1 + 9 (FLAG2 enabled) + ], +) +def test_compiler_flag_combinations(compile_flags, expected_value): + """Test ExternalFunction with different combinations of compiler flags.""" + # Create input and output tensors + input_tensor = iron.randint(0, 100, (1024,), dtype=np.int32, device="npu") + output_tensor = iron.zeros((1024,), dtype=np.int32, device="npu") + initial_tensor = input_tensor.numpy().copy() + + # Create source that uses the defines + source_template = """extern "C" { + void complex_op(int* input, int* output, int tile_size) { + for (int i = 0; i < tile_size; i++) { + #ifdef MULTIPLIER + output[i] = input[i] + ADD_VALUE * MULTIPLIER; + #elif defined(FLAG2) + output[i] = input[i] + ADD_VALUE + FLAG2_OFFSET; + #elif defined(OFFSET) + output[i] = input[i] + ADD_VALUE + OFFSET; + #else + output[i] = input[i] + ADD_VALUE; + #endif + } + } + }""" + + complex_op = ExternalFunction( + "complex_op", + source_string=source_template, + arg_types=[ + np.ndarray[(16,), np.dtype[np.int32]], + np.ndarray[(16,), np.dtype[np.int32]], + np.int32, + ], + compile_flags=compile_flags, + ) + + # Apply the transform + transform(input_tensor, output_tensor, complex_op) + + # Verify results + expected = initial_tensor + expected_value + actual = output_tensor.numpy() + np.testing.assert_array_equal(actual, expected) diff --git a/test/python/jit_extern_functions_inside_jit.py b/test/python/jit_extern_functions_inside_jit.py new file mode 100644 index 00000000000..0c85d87a293 --- /dev/null +++ b/test/python/jit_extern_functions_inside_jit.py @@ -0,0 +1,307 @@ +# This file is licensed under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# (c) Copyright 2025 AMD Inc. + +# RUN: %run_on_npu1% %pytest %s +# RUN: %run_on_npu2% %pytest %s + +import numpy as np +import os +import tempfile +import shutil +import pytest + +import aie.iron as iron +from aie.iron import ExternalFunction, jit +from aie.iron import ObjectFifo, Worker, Runtime, Program +from aie.iron.placers import SequentialPlacer +from aie.iron.controlflow import range_ + + +@jit(is_placed=False) +def transform_with_internal_func_with_options(input, output): + """Transform kernel that creates ExternalFunction internally with compiler options.""" + if input.shape != output.shape: + raise ValueError( + f"Input shapes are not the equal ({input.shape} != {output.shape})." + ) + num_elements = np.size(input) + + # Create ExternalFunction inside the transform with compiler options + internal_func = ExternalFunction( + "internal_add_value", + source_string="""extern "C" { + void internal_add_value(int* input, int* output, int tile_size) { + for (int i = 0; i < tile_size; i++) { + output[i] = input[i] + ADD_VALUE; + } + } + }""", + arg_types=[ + np.ndarray[(16,), np.dtype[np.int32]], + np.ndarray[(16,), np.dtype[np.int32]], + np.int32, + ], + compile_flags=["-DADD_VALUE=1"], + ) + + # Extract tile size from ExternalFunction + tile_size = internal_func.tile_size(0) + + if num_elements % tile_size != 0: + raise ValueError( + f"Number of elements ({num_elements}) must be a multiple of {tile_size}." + ) + num_tiles = num_elements // tile_size + + if input.dtype != output.dtype: + raise ValueError( + f"Input data types are not the same ({input.dtype} != {output.dtype})." + ) + + dtype = input.dtype + + # Define tensor types + tensor_ty = np.ndarray[(num_elements,), np.dtype[dtype]] + tile_ty = np.ndarray[(tile_size,), np.dtype[dtype]] + + # AIE-array data movement with object fifos + of_in = ObjectFifo(tile_ty, name="in") + of_out = ObjectFifo(tile_ty, name="out") + + # Define a task that will run on a compute tile + def core_body(of_in, of_out, func_to_apply): + # Extract tile size from ExternalFunction + tile_size = func_to_apply.tile_size(0) + + # Number of sub-vector "tile" iterations + for i in range_(num_tiles): + elem_in = of_in.acquire(1) + elem_out = of_out.acquire(1) + func_to_apply(elem_in, elem_out, tile_size) + of_in.release(1) + of_out.release(1) + + # Create a worker to run the task on a compute tile + worker = Worker(core_body, fn_args=[of_in.cons(), of_out.prod(), internal_func]) + + # Runtime operations to move data to/from the AIE-array + rt = Runtime() + with rt.sequence(tensor_ty, tensor_ty) as (A, B): + rt.start(worker) + rt.fill(of_in.prod(), A) + rt.drain(of_out.cons(), B, wait=True) + + # Place program components and generate an MLIR module + return Program(iron.get_current_device(), rt).resolve_program(SequentialPlacer()) + + +@jit(is_placed=False) +def transform_with_internal_func_from_file(input, output): + """Transform kernel that creates ExternalFunction internally from a file.""" + if input.shape != output.shape: + raise ValueError( + f"Input shapes are not the equal ({input.shape} != {output.shape})." + ) + num_elements = np.size(input) + + # Create a temporary file with the source code inside the function + with tempfile.NamedTemporaryFile(mode="w", suffix=".cc", delete=False) as f: + f.write( + """extern "C" { + void internal_add_from_file(int* input, int* output, int tile_size) { + for (int i = 0; i < tile_size; i++) { + output[i] = input[i] + 42; + } + } + }""" + ) + temp_file_path = f.name + + # Create ExternalFunction inside the transform from a file + internal_func = ExternalFunction( + "internal_add_from_file", + source_file=temp_file_path, + arg_types=[ + np.ndarray[(16,), np.dtype[np.int32]], + np.ndarray[(16,), np.dtype[np.int32]], + np.int32, + ], + ) + + # Extract tile size from ExternalFunction + tile_size = internal_func.tile_size(0) + + if num_elements % tile_size != 0: + raise ValueError( + f"Number of elements ({num_elements}) must be a multiple of {tile_size}." + ) + num_tiles = num_elements // tile_size + + if input.dtype != output.dtype: + raise ValueError( + f"Input data types are not the same ({input.dtype} != {output.dtype})." + ) + + dtype = input.dtype + + # Define tensor types + tensor_ty = np.ndarray[(num_elements,), np.dtype[dtype]] + tile_ty = np.ndarray[(tile_size,), np.dtype[dtype]] + + # AIE-array data movement with object fifos + of_in = ObjectFifo(tile_ty, name="in") + of_out = ObjectFifo(tile_ty, name="out") + + # Define a task that will run on a compute tile + def core_body(of_in, of_out, func_to_apply): + # Extract tile size from ExternalFunction + tile_size = func_to_apply.tile_size(0) + + # Number of sub-vector "tile" iterations + for i in range_(num_tiles): + elem_in = of_in.acquire(1) + elem_out = of_out.acquire(1) + func_to_apply(elem_in, elem_out, tile_size) + of_in.release(1) + of_out.release(1) + + # Create a worker to run the task on a compute tile + worker = Worker(core_body, fn_args=[of_in.cons(), of_out.prod(), internal_func]) + + # Runtime operations to move data to/from the AIE-array + rt = Runtime() + with rt.sequence(tensor_ty, tensor_ty) as (A, B): + rt.start(worker) + rt.fill(of_in.prod(), A) + rt.drain(of_out.cons(), B, wait=True) + + # Place program components and generate an MLIR module + return Program(iron.get_current_device(), rt).resolve_program(SequentialPlacer()) + + +@jit(is_placed=False) +def transform_with_internal_func(input, output): + """Transform kernel that creates ExternalFunction internally.""" + if input.shape != output.shape: + raise ValueError( + f"Input shapes are not the equal ({input.shape} != {output.shape})." + ) + num_elements = np.size(input) + + # Create ExternalFunction inside the transform + internal_func = ExternalFunction( + "internal_add_one", + source_string="""extern "C" { + void internal_add_one(int* input, int* output, int tile_size) { + for (int i = 0; i < tile_size; i++) { + output[i] = input[i] + 1; + } + } + }""", + arg_types=[ + np.ndarray[(16,), np.dtype[np.int32]], + np.ndarray[(16,), np.dtype[np.int32]], + np.int32, + ], + ) + + # Extract tile size from ExternalFunction + tile_size = internal_func.tile_size(0) + + if num_elements % tile_size != 0: + raise ValueError( + f"Number of elements ({num_elements}) must be a multiple of {tile_size}." + ) + num_tiles = num_elements // tile_size + + if input.dtype != output.dtype: + raise ValueError( + f"Input data types are not the same ({input.dtype} != {output.dtype})." + ) + + dtype = input.dtype + + # Define tensor types + tensor_ty = np.ndarray[(num_elements,), np.dtype[dtype]] + tile_ty = np.ndarray[(tile_size,), np.dtype[dtype]] + + # AIE-array data movement with object fifos + of_in = ObjectFifo(tile_ty, name="in") + of_out = ObjectFifo(tile_ty, name="out") + + # Define a task that will run on a compute tile + def core_body(of_in, of_out, func_to_apply): + # Extract tile size from ExternalFunction + tile_size = func_to_apply.tile_size(0) + + # Number of sub-vector "tile" iterations + for i in range_(num_tiles): + elem_in = of_in.acquire(1) + elem_out = of_out.acquire(1) + func_to_apply(elem_in, elem_out, tile_size) + of_in.release(1) + of_out.release(1) + + # Create a worker to run the task on a compute tile + worker = Worker(core_body, fn_args=[of_in.cons(), of_out.prod(), internal_func]) + + # Runtime operations to move data to/from the AIE-array + rt = Runtime() + with rt.sequence(tensor_ty, tensor_ty) as (A, B): + rt.start(worker) + rt.fill(of_in.prod(), A) + rt.drain(of_out.cons(), B, wait=True) + + # Place program components and generate an MLIR module + return Program(iron.get_current_device(), rt).resolve_program(SequentialPlacer()) + + +def test_transform_with_internal_func_with_options_inside(): + """Test transform function that creates ExternalFunction internally with compiler options.""" + # Create input and output tensors + input_tensor = iron.randint(0, 100, (1024,), dtype=np.int32, device="npu") + output_tensor = iron.zeros((1024,), dtype=np.int32, device="npu") + initial_tensor = input_tensor.numpy().copy() + + # Apply the transform (ExternalFunction is created inside with hardcoded compiler options) + transform_with_internal_func_with_options(input_tensor, output_tensor) + + # Verify results + expected = initial_tensor + 1 + actual = output_tensor.numpy() + np.testing.assert_array_equal(actual, expected) + + +def test_transform_with_internal_func_inside(): + """Test transform function that creates ExternalFunction internally.""" + # Create input and output tensors + input_tensor = iron.randint(0, 100, (1024,), dtype=np.int32, device="npu") + output_tensor = iron.zeros((1024,), dtype=np.int32, device="npu") + initial_tensor = input_tensor.numpy().copy() + + # Apply the transform (ExternalFunction is created inside) + transform_with_internal_func(input_tensor, output_tensor) + + # Verify results + expected = initial_tensor + 1 + actual = output_tensor.numpy() + np.testing.assert_array_equal(actual, expected) + + +def test_transform_with_internal_func_from_file(): + """Test transform function that creates ExternalFunction from a file.""" + # Create input and output tensors + input_tensor = iron.randint(0, 100, (1024,), dtype=np.int32, device="npu") + output_tensor = iron.zeros((1024,), dtype=np.int32, device="npu") + initial_tensor = input_tensor.numpy().copy() + + # Apply the transform (ExternalFunction is created inside from file) + transform_with_internal_func_from_file(input_tensor, output_tensor) + + # Verify results + expected = initial_tensor + 42 + actual = output_tensor.numpy() + np.testing.assert_array_equal(actual, expected) diff --git a/test/python/tensor.py b/test/python/tensor.py index f1c169bd238..ddabcc90911 100644 --- a/test/python/tensor.py +++ b/test/python/tensor.py @@ -62,6 +62,34 @@ def test_arange_floats(): assert np.allclose(iron.arange(1.0, 5.0, 1.5), np.arange(1.0, 5.0, 1.5)) +@pytest.mark.parametrize("dtype", [np.int32, np.float32]) +def test_fill(dtype): + """Test the fill_ method for in-place tensor filling.""" + t = iron.zeros((2, 3), dtype=dtype, device="npu") + + # Fill with a specific value + fill_value = 42 if dtype == np.int32 else 42.5 + t.fill_(fill_value) + + # Verify the tensor is filled with the correct value + expected = np.full((2, 3), fill_value, dtype=dtype) + assert np.allclose(t.numpy(), expected) + + # Test with different value + new_fill_value = 99 if dtype == np.int32 else 99.9 + t.fill_(new_fill_value) + expected = np.full((2, 3), new_fill_value, dtype=dtype) + assert np.allclose(t.numpy(), expected) + + +def test_fill_cpu_tensor(): + """Test fill_ method on CPU tensors.""" + t = iron.zeros((2, 2), dtype=np.int32, device="cpu") + t.fill_(123) + expected = np.full((2, 2), 123, dtype=np.int32) + assert np.array_equal(t.numpy(), expected) + + @pytest.mark.parametrize("dtype", [np.int32, np.float32]) def test_zeros_like(dtype): t = iron.tensor([[1, 2], [3, 4]], dtype=dtype)