From 7039e22c89903d77ac6b7afa3cde58ba5a4d76f3 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Wed, 26 Nov 2025 22:44:34 +0100 Subject: [PATCH 001/146] Implemented Julia kernel tuning functionality --- kernel_tuner/backends/julia.py | 324 ++++++++++++++++++++++++++ kernel_tuner/backends/julia_helper.jl | 34 +++ kernel_tuner/core.py | 8 + kernel_tuner/interface.py | 1 + kernel_tuner/observers/julia.py | 33 +++ kernel_tuner/observers/nvcuda.py | 5 +- kernel_tuner/util.py | 3 + 7 files changed, 407 insertions(+), 1 deletion(-) create mode 100644 kernel_tuner/backends/julia.py create mode 100644 kernel_tuner/backends/julia_helper.jl create mode 100644 kernel_tuner/observers/julia.py diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py new file mode 100644 index 000000000..85d5adf74 --- /dev/null +++ b/kernel_tuner/backends/julia.py @@ -0,0 +1,324 @@ +"""Kernel Tuner backend for running Julia CUDA.jl kernels via JuliaCall. + +This backend allows Julia kernels to be compiled, launched, and observed from Python using Kernel Tuner. + +Requirements: + pip install juliacall + and in Julia: ] add CUDA + +Notes: +- The kernel string should contain a valid Julia GPU kernel function definition. +- The kernel name must match the Julia function to be launched. +- Currently supports CuArray and scalar arguments; constant and texture memory are not implemented. +""" + +import numpy as np +from warnings import warn +from pathlib import Path + +from kernel_tuner.backends.backend import GPUBackend +from kernel_tuner.observers.julia import JuliaRuntimeObserver +from kernel_tuner.util import SkippableFailure + +try: + from juliacall import Main as jl +except ImportError: + jl = None + + +class JuliaFunctions(GPUBackend): + """Backend for running Julia kernels (CUDA.jl) through JuliaCall.""" + + units = {"time": "ms"} + last_selected_device = None + + def __init__(self, device=0, iterations=7, compiler_options=None, observers=None): + """Initialize Julia backend using JuliaCall.""" + if jl is None: + raise ImportError( + "JuliaCall not installed. Please run `pip install juliacall`." + ) + + # Ensure CUDA.jl is available + self.check_package_and_install("CUDA") + # try: + # jl.seval("import CUDA") + # except Exception: + # try: + # warn("CUDA.jl not found, attempting to install it directly.") + # jl.seval("using Pkg; Pkg.add(\"CUDA\")") + # jl.seval("import CUDA") + # except Exception as e: + # raise ImportError( + # "CUDA.jl not found in your Julia environment. " + # "Run `using Pkg; Pkg.add(\"CUDA\")` in Julia." + # ) from e + + # Initialize CUDA events + self.CUDA = jl.Main.CUDA + self.stream = self.CUDA.stream() + self.start_evt = None + self.end_evt = None + + # Select device + try: + jl.seval(f"CUDA.device!({int(device)})") + JuliaFunctions.last_selected_device = device + except Exception as e: + raise RuntimeError(f"Failed to set Julia CUDA device {device}: {e}") + + # Gather device info + try: + self.name = jl.seval("CUDA.device_name()") + cc_tuple = jl.seval("CUDA.capability()") + self.cc = f"{int(cc_tuple[0])}{int(cc_tuple[1])}" + except Exception as e: + warn(f"Could not retrieve device name and compute capability from Julia CUDA: {e}") + self.name = f"Julia-CUDA-device-{device}" + self.cc = None + self.max_threads = jl.seval("CUDA.attribute(CUDA.device(), CUDA.DEVICE_ATTRIBUTE_MAX_THREADS_PER_BLOCK)") + + # Initialize backend attributes + self.device = device + self.iterations = iterations + self.compiler_options = compiler_options or [] + self.allocations = [] + self.current_kernel = None + self.smem_size = 0 + + # setup observers + self.observers = observers or [] + self.observers.append(JuliaRuntimeObserver(self.CUDA)) + for observer in self.observers: + observer.register_device(self) + + # Include helper module + jl.include(str(Path(__file__).parent / "julia_helper.jl")) + # jl.seval( + # """ + # module KernelTunerHelper + # using CUDA + # const backend = CUDABackend() + + # export to_cuarray, launch_kernel + + # function to_cuarray(x) + # if isa(x, CuArray) + # return x + # elseif isa(x, AbstractArray) + # return CuArray(x) + # else + # return x + # end + # end + + # function launch_kernel(kernel, args::Tuple, grid::NTuple{3,Int}, block::NTuple{3,Int}, shmem::Int) + # # Check if this is a KernelAbstractions kernel + # if isdefined(Main, :KernelAbstractions) + # # # Try to get the appropriate backend type + # # backend_type = if isdefined(Main, :CUDABackend) + # # Main.CUDABackend() + # # elseif isdefined(Main, :CUDADevice) + # # Main.CUDADevice() + # # else + # # nothing + # # end + + # if backend !== nothing && applicable(kernel, backend, block) + # # KernelAbstractions.jl kernel + # workgroupsize = block + # # Calculate ndrange from grid and block + # ndrange = (grid[1] * block[1], grid[2] * block[2], grid[3] * block[3]) + # configured_kernel = kernel(backend, workgroupsize) + # configured_kernel(args..., ndrange=ndrange) + # CUDA.synchronize() + # else + # # Standard CUDA.jl kernel + # CUDA.@sync @cuda threads=block blocks=grid shmem=shmem kernel(args...) + # end + # else + # # Standard CUDA.jl kernel (KernelAbstractions not loaded) + # CUDA.@sync @cuda threads=block blocks=grid shmem=shmem kernel(args...) + # end + # end + # end + # """ + # ) + self.to_cuarray = jl.KernelTunerHelper.to_cuarray + self.launch_kernel = jl.KernelTunerHelper.launch_kernel + + # env info + self.env = { + "device_name": self.name, + "compute_capability": self.cc, + "iterations": iterations, + "compiler_options": self.compiler_options, + } + + def __del__(self): + # drop CuArray references to let Julia GC handle them + try: + for a in self.allocations: + del a + except Exception: + pass + + # ------------------------- + # Memory and argument setup + # ------------------------- + + def ready_argument_list(self, arguments): + """Convert numpy arrays to CuArray in Julia.""" + gpu_args = [] + for arg in arguments: + if isinstance(arg, np.ndarray): + try: + cu = self.to_cuarray(arg) + gpu_args.append(cu) + self.allocations.append(cu) + except Exception as e: + raise RuntimeError(f"Failed to move array to GPU: {e}") + else: + gpu_args.append(arg) + return gpu_args + + # ------------------------- + # Compilation + # ------------------------- + + def compile(self, kernel_instance): + """Define Julia kernel function from kernel_instance.kernel_string.""" + kernel_code = kernel_instance.kernel_string + kernel_name = kernel_instance.name + + # Extract all 'using' statements and check for required packages + uses = [] + for line in kernel_code.splitlines(): + stripped = line.strip() + # iterate over multiple using/import statements + if stripped.startswith("using ") or stripped.startswith("import "): + for part in stripped.split(','): + uses.append(part.replace("import ", "").replace("using ", "").strip()) + for package in uses: + self.check_package_and_install(package) + + # Wrap in a module to avoid name conflicts + module_code = f""" +module KernelTunerUserKernel + using CUDA + {kernel_code} +end + """ + try: + jl.seval(module_code) + self.current_kernel = jl.seval(f"KernelTunerUserKernel.{kernel_name}") + return self.current_kernel + except Exception as e: + raise SkippableFailure(f"Failed to compile Julia kernel: {e} \n{module_code}") + + # ------------------------- + # Kernel launch and timing + # ------------------------- + + def run_kernel(self, func, gpu_args, threads, grid, stream=None): + """Launch a compiled Julia kernel.""" + if func is None: + func = self.current_kernel + if func is None: + raise RuntimeError("No Julia kernel compiled or provided.") + + gx, gy, gz = (grid + (1,) * (3 - len(grid)))[:3] + tx, ty, tz = (threads + (1,) * (3 - len(threads)))[:3] + args_tuple = tuple(gpu_args) + + try: + self.launch_kernel(func, args_tuple, (int(gx), int(gy), int(gz)), + (int(tx), int(ty), int(tz)), int(self.smem_size)) + except Exception as e: + raise RuntimeError(f"Julia kernel launch failed: {e}") + + def start_event(self): + """Records the event that marks the start of a measurement.""" + self.start_evt = self.CUDA.CuEvent() + self.CUDA.record(self.start_evt, self.stream) + + def stop_event(self): + """Records the event that marks the end of a measurement.""" + self.end_evt = self.CUDA.CuEvent() + self.CUDA.record(self.end_evt, self.stream) + + def kernel_finished(self): + """Returns True if the kernel has finished, False otherwise.""" + return self.end_evt is not None + + @staticmethod + def synchronize(): + try: + jl.seval("CUDA.synchronize()") + except Exception as e: + raise RuntimeError(f"Julia synchronize failed: {e}") + + # ------------------------- + # Memory utilities + # ------------------------- + + @staticmethod + def memset(allocation, value, size): + try: + jl.allocation_tmp = allocation + jl.seval(f"CUDA.fill!(allocation_tmp, {int(value)})") + del jl.allocation_tmp + except Exception as e: + raise RuntimeError(f"Julia memset failed: {e}") + + @staticmethod + def memcpy_dtoh(dest, src): + try: + jl.src_tmp = src + jl.seval("host_tmp = Array(src_tmp)") + host = np.array(jl.host_tmp) + np.copyto(dest, host) + del jl.src_tmp + del jl.host_tmp + except Exception as e: + raise RuntimeError(f"Julia memcpy_dtoh failed: {e}") + + @staticmethod + def memcpy_htod(dest, src): + try: + jl.src_tmp = src + jl.seval("cu_tmp = CUDA.CuArray(src_tmp)") + cu = jl.cu_tmp + del jl.src_tmp + del jl.cu_tmp + return cu + except Exception as e: + raise RuntimeError(f"Julia memcpy_htod failed: {e}") + + def copy_constant_memory_args(self, cmem_args): + raise NotImplementedError("Constant memory not supported in Julia backend. Submit a feature request if needed.") + + def copy_shared_memory_args(self, smem_args): + self.smem_size = int(smem_args.get("size", 0)) + + def copy_texture_memory_args(self, texmem_args): + raise NotImplementedError("Texture memory not supported in Julia backend. Submit a feature request if needed.") + + # ------------------------- + # Helper functions + # ------------------------- + + def check_package_and_install(self, package): + """Checks if the Julia package is available, and installs it if not.""" + try: + jl.seval(f"import {package}") + except Exception: + try: + warn(f"{package}.jl not found, attempting to install it directly.") + jl.seval(f"using Pkg; Pkg.add(\"{package}\")") + jl.seval(f"import {package}") + except Exception as e: + raise ImportError( + f"{package}.jl not found in your Julia environment. " + f"Run `using Pkg; Pkg.add(\"{package}\)` in Julia." + ) from e diff --git a/kernel_tuner/backends/julia_helper.jl b/kernel_tuner/backends/julia_helper.jl new file mode 100644 index 000000000..2227891f8 --- /dev/null +++ b/kernel_tuner/backends/julia_helper.jl @@ -0,0 +1,34 @@ +module KernelTunerHelper + using CUDA + const backend = CUDABackend() # currently only CUDA backend is supported + + export to_cuarray, launch_kernel + + function to_cuarray(x) + if isa(x, CuArray) + return x + elseif isa(x, AbstractArray) + return CuArray(x) + else + return x + end + end + + function launch_kernel(kernel, args::Tuple, grid::NTuple{3,Int}, block::NTuple{3,Int}, shmem::Int) + # Check if this is a KernelAbstractions kernel + if isdefined(Main, :KernelAbstractions) && backend !== nothing && applicable(kernel, backend, block) + # Calculate ndrange from grid and block + workgroupsize = block + ndrange = (grid[1] * block[1], grid[2] * block[2], grid[3] * block[3]) + # Launch kernel + configured_kernel = kernel(backend, workgroupsize) + configured_kernel(args..., ndrange=ndrange) + # Synchronize to ensure kernel completion + CUDA.synchronize() + else + warn("KernelAbstractions not found, falling back to standard CUDA.jl kernel launch.") + # Standard CUDA.jl kernel (KernelAbstractions not loaded) + CUDA.@sync @cuda threads=block blocks=grid shmem=shmem kernel(args...) + end + end +end diff --git a/kernel_tuner/core.py b/kernel_tuner/core.py index 5352ced74..8d159b784 100644 --- a/kernel_tuner/core.py +++ b/kernel_tuner/core.py @@ -17,6 +17,7 @@ from kernel_tuner.backends.compiler import CompilerFunctions from kernel_tuner.backends.cupy import CupyFunctions from kernel_tuner.backends.hip import HipFunctions +from kernel_tuner.backends.julia import JuliaFunctions from kernel_tuner.backends.hypertuner import HypertunerFunctions from kernel_tuner.backends.nvcuda import CudaFunctions from kernel_tuner.backends.opencl import OpenCLFunctions @@ -314,6 +315,13 @@ def __init__( iterations=iterations, observers=observers, ) + elif lang.upper() == "JULIA": + dev = JuliaFunctions( + device, + compiler_options=compiler_options, + iterations=iterations, + observers=observers, + ) elif lang.upper() == "HYPERTUNER": dev = HypertunerFunctions( iterations=iterations, diff --git a/kernel_tuner/interface.py b/kernel_tuner/interface.py index 32e91c86f..f1f5a45f0 100644 --- a/kernel_tuner/interface.py +++ b/kernel_tuner/interface.py @@ -698,6 +698,7 @@ def preprocess_cache(filepath): # finished iterating over search space if results: # checks if results is not empty + raise ValueError(results) best_config = util.get_best_config(results, objective, objective_higher_is_better) # add the best configuration to env env["best_config"] = best_config diff --git a/kernel_tuner/observers/julia.py b/kernel_tuner/observers/julia.py new file mode 100644 index 000000000..62622f5c8 --- /dev/null +++ b/kernel_tuner/observers/julia.py @@ -0,0 +1,33 @@ +import numpy as np +from kernel_tuner.observers.observer import BenchmarkObserver + +class JuliaRuntimeObserver(BenchmarkObserver): + """Observer that measures GPU time using CUDA.CuEvent and CUDA.elapsed.""" + + def __init__(self, CUDA): + self.CUDA = CUDA + self.start = self.CUDA.CuEvent() + self.end = self.CUDA.CuEvent() + self.stream = self.CUDA.stream() # default stream + self.times = [] + + def before_start(self): + # record start event + self.CUDA.record(self.start, self.stream) + + def after_finish(self): + # record end event + self.CUDA.record(self.end, self.stream) + self.CUDA.synchronize(self.end) + + # milliseconds + ms = float(self.CUDA.elapsed(self.start, self.end)) + self.times.append(ms) + + def get_results(self): + results = { + "time": np.average(self.times), + "times": self.times.copy(), + } + self.times = [] + return results diff --git a/kernel_tuner/observers/nvcuda.py b/kernel_tuner/observers/nvcuda.py index c0a33ad5c..58076bde6 100644 --- a/kernel_tuner/observers/nvcuda.py +++ b/kernel_tuner/observers/nvcuda.py @@ -26,6 +26,9 @@ def after_finish(self): self.times.append(time) def get_results(self): - results = {"time": np.average(self.times), "times": self.times.copy()} + results = { + "time": np.average(self.times), + "times": self.times.copy() + } self.times = [] return results diff --git a/kernel_tuner/util.py b/kernel_tuner/util.py index 2d9e3f1b3..afbb5f72d 100644 --- a/kernel_tuner/util.py +++ b/kernel_tuner/util.py @@ -411,10 +411,13 @@ def delete_temp_file(filename): def detect_language(kernel_string): """Attempt to detect language from the kernel_string.""" + kernel_string = kernel_string.lower().strip() if "__global__" in kernel_string: lang = "CUDA" elif "__kernel" in kernel_string: lang = "OpenCL" + elif any(token in kernel_string for token in ["@cuda", "@kernel", "@device_code"]): + lang = "Julia" else: lang = "C" return lang From d73379e8d198773dbc5b83a8816ecb8409820ab0 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Wed, 26 Nov 2025 23:05:41 +0100 Subject: [PATCH 002/146] Implemented gathering device information from Julia --- kernel_tuner/backends/julia.py | 57 +++------------------------------- kernel_tuner/interface.py | 1 - 2 files changed, 4 insertions(+), 54 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 85d5adf74..82d4efef7 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -69,9 +69,9 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None # Gather device info try: - self.name = jl.seval("CUDA.device_name()") - cc_tuple = jl.seval("CUDA.capability()") - self.cc = f"{int(cc_tuple[0])}{int(cc_tuple[1])}" + self.name = jl.seval("CUDA.name(CUDA.device())") + cc_tuple = jl.seval("CUDA.capability(CUDA.device())") + self.cc = f"{cc_tuple.major}{cc_tuple.minor}" except Exception as e: warn(f"Could not retrieve device name and compute capability from Julia CUDA: {e}") self.name = f"Julia-CUDA-device-{device}" @@ -94,56 +94,7 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None # Include helper module jl.include(str(Path(__file__).parent / "julia_helper.jl")) - # jl.seval( - # """ - # module KernelTunerHelper - # using CUDA - # const backend = CUDABackend() - - # export to_cuarray, launch_kernel - - # function to_cuarray(x) - # if isa(x, CuArray) - # return x - # elseif isa(x, AbstractArray) - # return CuArray(x) - # else - # return x - # end - # end - - # function launch_kernel(kernel, args::Tuple, grid::NTuple{3,Int}, block::NTuple{3,Int}, shmem::Int) - # # Check if this is a KernelAbstractions kernel - # if isdefined(Main, :KernelAbstractions) - # # # Try to get the appropriate backend type - # # backend_type = if isdefined(Main, :CUDABackend) - # # Main.CUDABackend() - # # elseif isdefined(Main, :CUDADevice) - # # Main.CUDADevice() - # # else - # # nothing - # # end - - # if backend !== nothing && applicable(kernel, backend, block) - # # KernelAbstractions.jl kernel - # workgroupsize = block - # # Calculate ndrange from grid and block - # ndrange = (grid[1] * block[1], grid[2] * block[2], grid[3] * block[3]) - # configured_kernel = kernel(backend, workgroupsize) - # configured_kernel(args..., ndrange=ndrange) - # CUDA.synchronize() - # else - # # Standard CUDA.jl kernel - # CUDA.@sync @cuda threads=block blocks=grid shmem=shmem kernel(args...) - # end - # else - # # Standard CUDA.jl kernel (KernelAbstractions not loaded) - # CUDA.@sync @cuda threads=block blocks=grid shmem=shmem kernel(args...) - # end - # end - # end - # """ - # ) + self.to_cuarray = jl.KernelTunerHelper.to_cuarray self.launch_kernel = jl.KernelTunerHelper.launch_kernel diff --git a/kernel_tuner/interface.py b/kernel_tuner/interface.py index f1f5a45f0..32e91c86f 100644 --- a/kernel_tuner/interface.py +++ b/kernel_tuner/interface.py @@ -698,7 +698,6 @@ def preprocess_cache(filepath): # finished iterating over search space if results: # checks if results is not empty - raise ValueError(results) best_config = util.get_best_config(results, objective, objective_higher_is_better) # add the best configuration to env env["best_config"] = best_config From 637ce023288f764054ce1a3c672e8ef9110f3ca9 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Sun, 7 Dec 2025 11:13:34 +0100 Subject: [PATCH 003/146] Improved Julia support --- kernel_tuner/backends/julia.py | 12 ------------ kernel_tuner/core.py | 2 +- kernel_tuner/util.py | 10 +++++++--- noxfile.py | 3 +++ 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 82d4efef7..5ae02f133 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -41,18 +41,6 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None # Ensure CUDA.jl is available self.check_package_and_install("CUDA") - # try: - # jl.seval("import CUDA") - # except Exception: - # try: - # warn("CUDA.jl not found, attempting to install it directly.") - # jl.seval("using Pkg; Pkg.add(\"CUDA\")") - # jl.seval("import CUDA") - # except Exception as e: - # raise ImportError( - # "CUDA.jl not found in your Julia environment. " - # "Run `using Pkg; Pkg.add(\"CUDA\")` in Julia." - # ) from e # Initialize CUDA events self.CUDA = jl.Main.CUDA diff --git a/kernel_tuner/core.py b/kernel_tuner/core.py index 8d159b784..d7fad8453 100644 --- a/kernel_tuner/core.py +++ b/kernel_tuner/core.py @@ -206,7 +206,7 @@ def get_suffix(self, index=0): if suffix is not None: return suffix - _suffixes = {"CUDA": ".cu", "OpenCL": ".cl", "C": ".c"} + _suffixes = {"CUDA": ".cu", "OpenCL": ".cl", "C": ".c", "JULIA": ".jl"} try: return _suffixes[self.lang] except KeyError: diff --git a/kernel_tuner/util.py b/kernel_tuner/util.py index afbb5f72d..f107cf85d 100644 --- a/kernel_tuner/util.py +++ b/kernel_tuner/util.py @@ -591,8 +591,8 @@ def get_smem_args(smem_args, params): def get_temp_filename(suffix=None): """Return a string in the form of temp_X, where X is a large integer.""" tmp_file = tempfile.mkstemp( - suffix=suffix or "", prefix="temp_", dir=os.getcwd() - ) # or "" for Python 2 compatibility + suffix=suffix, prefix="temp_", dir=os.getcwd() + ) os.close(tmp_file[0]) return tmp_file[1] @@ -822,12 +822,16 @@ def prepare_kernel_string(kernel_name, kernel_string, params, grid, threads, blo kernel_string = re.sub(r"\n\s*#pragma\s+unroll\s+" + k, "\n", kernel_string) # + r"[^\S]*" else: kernel_prefix += f"constexpr int {k} = {v};\n" + elif lang.upper() == "JULIA": + kernel_prefix += f"{k} = {v}\n" else: kernel_prefix += f"#define {k} {v}\n" # since we insert defines above the original kernel code, the line numbers will be incorrect # the following preprocessor directive informs the compiler that lines should be counted from 1 - if kernel_prefix: + if lang.upper() == "JULIA": + kernel_prefix += "\n" + elif kernel_prefix: kernel_prefix += "#line 1\n" # Also replace parameter occurrences inside the kernel name diff --git a/noxfile.py b/noxfile.py index 2770bc7f1..6092ff445 100644 --- a/noxfile.py +++ b/noxfile.py @@ -153,6 +153,9 @@ def tests(session: Session) -> None: # remove temporary files leftover from the previous session session.run("rm", "-f", "temp_*.c", external=True) + session.run("rm", "-f", "temp_*.cu", external=True) + session.run("rm", "-f", "temp_*.cl", external=True) + session.run("rm", "-f", "temp_*.jl", external=True) # set extra arguments based on optional dependencies extras_args = [] From 54f9764b0b00bcc22de44426cf54c0343b1340d5 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Mon, 8 Dec 2025 18:47:07 +0100 Subject: [PATCH 004/146] Modified the kernel launch structure for Julia kernels, added JIT warmup avoidance --- kernel_tuner/backends/backend.py | 2 +- kernel_tuner/backends/compiler.py | 2 +- kernel_tuner/backends/cupy.py | 2 +- kernel_tuner/backends/hip/hip.py | 2 +- kernel_tuner/backends/hypertuner.py | 2 +- kernel_tuner/backends/julia.py | 6 ++++-- kernel_tuner/backends/julia_helper.jl | 4 ++-- kernel_tuner/backends/nvcuda.py | 2 +- kernel_tuner/backends/opencl.py | 2 +- kernel_tuner/backends/pycuda.py | 2 +- kernel_tuner/core.py | 15 +++++++++++---- kernel_tuner/observers/julia.py | 17 ++++++++++++++++- kernel_tuner/util.py | 3 ++- 13 files changed, 43 insertions(+), 18 deletions(-) diff --git a/kernel_tuner/backends/backend.py b/kernel_tuner/backends/backend.py index 6063dbb43..85a1445d2 100644 --- a/kernel_tuner/backends/backend.py +++ b/kernel_tuner/backends/backend.py @@ -38,7 +38,7 @@ def synchronize(self): pass @abstractmethod - def run_kernel(self, func, gpu_args, threads, grid, stream): + def run_kernel(self, func, gpu_args, threads, grid, stream, params): """This method must implement the execution of the kernel on the device.""" pass diff --git a/kernel_tuner/backends/compiler.py b/kernel_tuner/backends/compiler.py index 06402ff7c..65109bdb4 100644 --- a/kernel_tuner/backends/compiler.py +++ b/kernel_tuner/backends/compiler.py @@ -332,7 +332,7 @@ def synchronize(self): C backend does not support asynchronous launches""" pass - def run_kernel(self, func, c_args, threads, grid, stream=None): + def run_kernel(self, func, c_args, threads, grid, stream=None, params=None): """runs the kernel once, returns whatever the kernel returns :param func: A C function compiled for this specific configuration diff --git a/kernel_tuner/backends/cupy.py b/kernel_tuner/backends/cupy.py index 51613be7c..ece81abe4 100644 --- a/kernel_tuner/backends/cupy.py +++ b/kernel_tuner/backends/cupy.py @@ -180,7 +180,7 @@ def copy_texture_memory_args(self, texmem_args): """ raise NotImplementedError("CuPy backend does not support texture memory") - def run_kernel(self, func, gpu_args, threads, grid, stream=None): + def run_kernel(self, func, gpu_args, threads, grid, stream=None, params=None): """Runs the CUDA kernel passed as 'func'. :param func: A cupy kernel compiled for this specific kernel configuration diff --git a/kernel_tuner/backends/hip/hip.py b/kernel_tuner/backends/hip/hip.py index c4f404919..71eab6fff 100644 --- a/kernel_tuner/backends/hip/hip.py +++ b/kernel_tuner/backends/hip/hip.py @@ -222,7 +222,7 @@ def synchronize(self): hip_check(hip.hipDeviceSynchronize()) - def run_kernel(self, func, gpu_args, threads, grid, stream=None): + def run_kernel(self, func, gpu_args, threads, grid, stream=None, params=None): """Runs the HIP kernel passed as 'func'. :param func: A HIP kernel compiled for this specific kernel configuration diff --git a/kernel_tuner/backends/hypertuner.py b/kernel_tuner/backends/hypertuner.py index d6f23475a..d99ad5eea 100644 --- a/kernel_tuner/backends/hypertuner.py +++ b/kernel_tuner/backends/hypertuner.py @@ -149,7 +149,7 @@ def kernel_finished(self): def synchronize(self): return super().synchronize() - def run_kernel(self, func, gpu_args=None, threads=None, grid=None, stream=None): + def run_kernel(self, func, gpu_args=None, threads=None, grid=None, stream=None, params=None): # from cProfile import Profile # # generate the experiments file diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 5ae02f133..8e27eabc1 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -129,6 +129,7 @@ def compile(self, kernel_instance): """Define Julia kernel function from kernel_instance.kernel_string.""" kernel_code = kernel_instance.kernel_string kernel_name = kernel_instance.name + self.kernel_source = kernel_instance.kernel_source # Extract all 'using' statements and check for required packages uses = [] @@ -159,7 +160,7 @@ def compile(self, kernel_instance): # Kernel launch and timing # ------------------------- - def run_kernel(self, func, gpu_args, threads, grid, stream=None): + def run_kernel(self, func, gpu_args, threads, grid, stream=None, params=None): """Launch a compiled Julia kernel.""" if func is None: func = self.current_kernel @@ -169,9 +170,10 @@ def run_kernel(self, func, gpu_args, threads, grid, stream=None): gx, gy, gz = (grid + (1,) * (3 - len(grid)))[:3] tx, ty, tz = (threads + (1,) * (3 - len(threads)))[:3] args_tuple = tuple(gpu_args) + params = tuple(params.values()) # important: the order of params must match the order in the kernel definition try: - self.launch_kernel(func, args_tuple, (int(gx), int(gy), int(gz)), + self.launch_kernel(func, args_tuple, params, (int(gx), int(gy), int(gz)), (int(tx), int(ty), int(tz)), int(self.smem_size)) except Exception as e: raise RuntimeError(f"Julia kernel launch failed: {e}") diff --git a/kernel_tuner/backends/julia_helper.jl b/kernel_tuner/backends/julia_helper.jl index 2227891f8..5fd326e2d 100644 --- a/kernel_tuner/backends/julia_helper.jl +++ b/kernel_tuner/backends/julia_helper.jl @@ -14,7 +14,7 @@ module KernelTunerHelper end end - function launch_kernel(kernel, args::Tuple, grid::NTuple{3,Int}, block::NTuple{3,Int}, shmem::Int) + function launch_kernel(kernel, args::Tuple, params::Tuple, grid::NTuple{3,Int}, block::NTuple{3,Int}, shmem::Int) # Check if this is a KernelAbstractions kernel if isdefined(Main, :KernelAbstractions) && backend !== nothing && applicable(kernel, backend, block) # Calculate ndrange from grid and block @@ -22,7 +22,7 @@ module KernelTunerHelper ndrange = (grid[1] * block[1], grid[2] * block[2], grid[3] * block[3]) # Launch kernel configured_kernel = kernel(backend, workgroupsize) - configured_kernel(args..., ndrange=ndrange) + configured_kernel(args..., ndrange=ndrange, Val.(params)...) # Synchronize to ensure kernel completion CUDA.synchronize() else diff --git a/kernel_tuner/backends/nvcuda.py b/kernel_tuner/backends/nvcuda.py index 15259cb23..13611fa49 100644 --- a/kernel_tuner/backends/nvcuda.py +++ b/kernel_tuner/backends/nvcuda.py @@ -261,7 +261,7 @@ def copy_texture_memory_args(self, texmem_args): """ raise NotImplementedError("NVIDIA CUDA backend does not support texture memory") - def run_kernel(self, func, gpu_args, threads, grid, stream=None): + def run_kernel(self, func, gpu_args, threads, grid, stream=None, params=None): """Runs the CUDA kernel passed as 'func'. :param func: A CUDA kernel compiled for this specific kernel configuration diff --git a/kernel_tuner/backends/opencl.py b/kernel_tuner/backends/opencl.py index af3be1c00..05afddc04 100644 --- a/kernel_tuner/backends/opencl.py +++ b/kernel_tuner/backends/opencl.py @@ -136,7 +136,7 @@ def synchronize(self): """Halts execution until device has finished its tasks.""" self.queue.finish() - def run_kernel(self, func, gpu_args, threads, grid): + def run_kernel(self, func, gpu_args, threads, grid, stream=None, params=None): """Runs the OpenCL kernel passed as 'func'. :param func: An OpenCL Kernel diff --git a/kernel_tuner/backends/pycuda.py b/kernel_tuner/backends/pycuda.py index c8f3e689a..5e6d7bcb7 100644 --- a/kernel_tuner/backends/pycuda.py +++ b/kernel_tuner/backends/pycuda.py @@ -329,7 +329,7 @@ def copy_texture_memory_args(self, texmem_args): if "normalized_coordinates" in v and v["normalized_coordinates"]: tex.set_flags(tex.get_flags() | drv.TRSF_NORMALIZED_COORDINATES) - def run_kernel(self, func, gpu_args, threads, grid, stream=None): + def run_kernel(self, func, gpu_args, threads, grid, stream=None, params=None): """Runs the CUDA kernel passed as 'func'. :param func: A PyCuda kernel compiled for this specific kernel configuration diff --git a/kernel_tuner/core.py b/kernel_tuner/core.py index d7fad8453..e17403144 100644 --- a/kernel_tuner/core.py +++ b/kernel_tuner/core.py @@ -25,6 +25,7 @@ from kernel_tuner.observers.nvml import NVMLObserver from kernel_tuner.observers.observer import ContinuousObserver, OutputObserver, PrologueObserver from kernel_tuner.observers.tegra import TegraObserver +from kernel_tuner.observers.julia import JuliaJITWarmup try: import torch @@ -355,6 +356,10 @@ def __init__( if isinstance(obs, PrologueObserver): self.prologue_observers.append(obs) + # for JULIA, add the JIT warmup prologue observer + if lang.upper() == "JULIA": + self.prologue_observers.append(JuliaJITWarmup(self.dev.CUDA)) + # Take list of observers from self.dev because Backends tend to add their own observer self.benchmark_observers = [ obs for obs in self.dev.observers if not isinstance(obs, (ContinuousObserver, PrologueObserver)) @@ -366,6 +371,7 @@ def __init__( self.units = dev.units self.name = dev.name self.max_threads = dev.max_threads + self.last_instance_params = None if not quiet: print("Using: " + self.dev.name) @@ -374,7 +380,7 @@ def benchmark_prologue(self, func, gpu_args, threads, grid, result): for obs in self.prologue_observers: self.dev.synchronize() obs.before_start() - self.dev.run_kernel(func, gpu_args, threads, grid) + self.dev.run_kernel(func, gpu_args, threads, grid, params=self.last_instance_params) self.dev.synchronize() obs.after_finish() result.update(obs.get_results()) @@ -387,7 +393,7 @@ def benchmark_default(self, func, gpu_args, threads, grid, result): obs.before_start() self.dev.synchronize() self.dev.start_event() - self.dev.run_kernel(func, gpu_args, threads, grid) + self.dev.run_kernel(func, gpu_args, threads, grid, params=self.last_instance_params) self.dev.stop_event() for obs in self.benchmark_observers: obs.after_start() @@ -411,7 +417,7 @@ def benchmark_continuous(self, func, gpu_args, threads, grid, result, duration): obs.before_start() self.dev.start_event() for _ in range(iterations): - self.dev.run_kernel(func, gpu_args, threads, grid) + self.dev.run_kernel(func, gpu_args, threads, grid, params=self.last_instance_params) self.dev.stop_event() for obs in self.continuous_observers: obs.after_start() @@ -574,6 +580,7 @@ def compile_and_benchmark(self, kernel_source, gpu_args, params, kernel_options, logging.debug("compile_and_benchmark " + instance_string) instance = self.create_kernel_instance(kernel_source, kernel_options, params, verbose) + self.last_instance_params = params if isinstance(instance, util.ErrorConfig): result[to.objective] = util.InvalidConfig() else: @@ -766,7 +773,7 @@ def run_kernel(self, func, gpu_args, instance): logging.debug("grid dims (%d, %d, %d)", *instance.grid) try: - self.dev.run_kernel(func, gpu_args, instance.threads, instance.grid) + self.dev.run_kernel(func, gpu_args, instance.threads, instance.gridm, self.last_instance_params) except Exception as e: if "too many resources requested for launch" in str(e) or "OUT_OF_RESOURCES" in str(e): logging.debug("ignoring runtime failure due to too many resources required") diff --git a/kernel_tuner/observers/julia.py b/kernel_tuner/observers/julia.py index 62622f5c8..44b67df77 100644 --- a/kernel_tuner/observers/julia.py +++ b/kernel_tuner/observers/julia.py @@ -1,5 +1,5 @@ import numpy as np -from kernel_tuner.observers.observer import BenchmarkObserver +from kernel_tuner.observers.observer import BenchmarkObserver, PrologueObserver class JuliaRuntimeObserver(BenchmarkObserver): """Observer that measures GPU time using CUDA.CuEvent and CUDA.elapsed.""" @@ -31,3 +31,18 @@ def get_results(self): } self.times = [] return results + +class JuliaJITWarmup(PrologueObserver): + """Prologue observer to enforce warmup before every configuration to trigger JIT.""" + + def __init__(self, CUDA): + pass + + def before_start(self): + pass + + def after_finish(self): + pass + + def get_results(self): + return {} diff --git a/kernel_tuner/util.py b/kernel_tuner/util.py index f107cf85d..6d017ccb6 100644 --- a/kernel_tuner/util.py +++ b/kernel_tuner/util.py @@ -823,7 +823,8 @@ def prepare_kernel_string(kernel_name, kernel_string, params, grid, threads, blo else: kernel_prefix += f"constexpr int {k} = {v};\n" elif lang.upper() == "JULIA": - kernel_prefix += f"{k} = {v}\n" + # kernel_prefix += f"const {k} = {v}\n" + pass # in Julia, we can't redefine constants like this, so we skip it and give it as arguments on the kernel launch else: kernel_prefix += f"#define {k} {v}\n" From 28108c7b35c2534bdcc0f2b1c97209eca88ed01e Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Mon, 8 Dec 2025 20:06:15 +0100 Subject: [PATCH 005/146] Implement pointing to kernel file for Julia --- kernel_tuner/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kernel_tuner/util.py b/kernel_tuner/util.py index 6d017ccb6..669e47840 100644 --- a/kernel_tuner/util.py +++ b/kernel_tuner/util.py @@ -730,8 +730,8 @@ def looks_like_a_filename(kernel_source): for s in ["__global__ ", "__kernel ", "void ", "float "]: if s in kernel_source: result = False - # string must contain substring ".c", ".opencl", or ".F" - result = result and any([s in kernel_source for s in (".c", ".opencl", ".F")]) + # string must contain substring ".c", ".opencl", ".F", ".jl" + result = result and any([s in kernel_source for s in (".c", ".opencl", ".F", ".jl")]) logging.debug("kernel_source is a filename: %s" % str(result)) return result From 9df9c77d7f7f78229e56b5a8441d5c10292dad18 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Tue, 9 Dec 2025 16:21:20 +0100 Subject: [PATCH 006/146] Added backend test for Julia --- test/context.py | 1 + test/test_backend.py | 8 ++++- test/test_julia_functions.py | 60 ++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 test/test_julia_functions.py diff --git a/test/context.py b/test/context.py index bad152986..b1a13a281 100644 --- a/test/context.py +++ b/test/context.py @@ -102,6 +102,7 @@ skip_if_no_gfortran = pytest.mark.skipif( not gfortran_present, reason="No gfortran on PATH" ) +skip_if_no_julia = pytest.mark.skipif(not shutil.which("julia"), reason="No Julia on PATH") skip_if_no_openmp = pytest.mark.skipif(not openmp_present, reason="No OpenMP found") skip_if_no_openacc = pytest.mark.skipif(not openacc_present, reason="No nvc++ on PATH") skip_if_no_bayesopt_gpytorch = pytest.mark.skipif(not bayes_opt_gpytorch_present, reason="Torch and GPyTorch not installed") diff --git a/test/test_backend.py b/test/test_backend.py index e694649c1..d56bdcce0 100644 --- a/test/test_backend.py +++ b/test/test_backend.py @@ -4,8 +4,9 @@ skip_if_no_cuda, skip_if_no_opencl, skip_if_no_pycuda, + skip_if_no_julia, ) -from kernel_tuner.backends import backend, compiler, cupy, nvcuda, opencl, pycuda +from kernel_tuner.backends import backend, compiler, cupy, nvcuda, opencl, pycuda, julia class WrongBackend(backend.Backend): @@ -45,3 +46,8 @@ def test_opencl_backend(): @skip_if_no_pycuda def test_pycuda_backend(): dev = pycuda.PyCudaFunctions() + + +@skip_if_no_julia +def test_julia_backend(): + dev = julia.JuliaFunctions() diff --git a/test/test_julia_functions.py b/test/test_julia_functions.py new file mode 100644 index 000000000..1dc68652d --- /dev/null +++ b/test/test_julia_functions.py @@ -0,0 +1,60 @@ +import numpy as np +import pytest + +from kernel_tuner import tune_kernel +from kernel_tuner.backends import nvcuda +from kernel_tuner.core import KernelInstance, KernelSource + +from .context import skip_if_no_cuda +from .test_runners import env # noqa: F401 + +try: + from cuda import cuda +except Exception: + pass + + +@skip_if_no_cuda +def test_ready_argument_list(): + + size = 1000 + a = np.int32(75) + b = np.random.randn(size).astype(np.float32) + c = np.zeros_like(b) + + arguments = [c, a, b] + + dev = nvcuda.CudaFunctions(0) + gpu_args = dev.ready_argument_list(arguments) + + assert isinstance(gpu_args[0], cuda.CUdeviceptr) + assert isinstance(gpu_args[1], np.int32) + assert isinstance(gpu_args[2], cuda.CUdeviceptr) + + +@skip_if_no_cuda +def test_compile(): + + kernel_string = """ + extern "C" __global__ void vector_add(float *c, float *a, float *b, int n) { + int i = blockIdx.x * blockDim.x + threadIdx.x; + if (i 0 From 634b3685df8b26d38c4e030f6dc8a3044a2edbfa Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Tue, 9 Dec 2025 16:29:21 +0100 Subject: [PATCH 007/146] Small fixes --- kernel_tuner/core.py | 2 +- test/test_core.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/kernel_tuner/core.py b/kernel_tuner/core.py index e17403144..133ff17f6 100644 --- a/kernel_tuner/core.py +++ b/kernel_tuner/core.py @@ -773,7 +773,7 @@ def run_kernel(self, func, gpu_args, instance): logging.debug("grid dims (%d, %d, %d)", *instance.grid) try: - self.dev.run_kernel(func, gpu_args, instance.threads, instance.gridm, self.last_instance_params) + self.dev.run_kernel(func, gpu_args, instance.threads, instance.grid, self.last_instance_params) except Exception as e: if "too many resources requested for launch" in str(e) or "OUT_OF_RESOURCES" in str(e): logging.debug("ignoring runtime failure due to too many resources required") diff --git a/test/test_core.py b/test/test_core.py index 39597b86c..35783c7b2 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -108,7 +108,7 @@ def test_check_kernel_output(dev_func_interface): dev.check_kernel_output('func', answer, instance, answer, atol, None, True) dfi.refresh_memory.assert_called() - dfi.run_kernel.assert_called_once_with('func', answer, (256, 1, 1), (1, 1, 1)) + dfi.run_kernel.assert_called_once_with('func', answer, (256, 1, 1), (1, 1, 1), None) print(dfi.mock_calls) From f11fc304ec9a443369590a956c798748d19bd665 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Tue, 9 Dec 2025 19:00:44 +0100 Subject: [PATCH 008/146] Improved Julia kernel launch for general case --- kernel_tuner/backends/julia_helper.jl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/kernel_tuner/backends/julia_helper.jl b/kernel_tuner/backends/julia_helper.jl index 5fd326e2d..1d50eca2c 100644 --- a/kernel_tuner/backends/julia_helper.jl +++ b/kernel_tuner/backends/julia_helper.jl @@ -18,11 +18,13 @@ module KernelTunerHelper # Check if this is a KernelAbstractions kernel if isdefined(Main, :KernelAbstractions) && backend !== nothing && applicable(kernel, backend, block) # Calculate ndrange from grid and block - workgroupsize = block - ndrange = (grid[1] * block[1], grid[2] * block[2], grid[3] * block[3]) + # workgroupsize = block + workgroupsize = (block[1], block[2]) + # ndrange = (grid[1] * block[1], grid[2] * block[2], grid[3] * block[3]) + ndrange = (grid[1], grid[2]) # Launch kernel configured_kernel = kernel(backend, workgroupsize) - configured_kernel(args..., ndrange=ndrange, Val.(params)...) + configured_kernel(args..., Val.(params)..., ndrange=ndrange) # Synchronize to ensure kernel completion CUDA.synchronize() else From 9d9e428d3417ead3177908a1a802261c9982d31d Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Tue, 9 Dec 2025 19:29:57 +0100 Subject: [PATCH 009/146] Generalized and added ndrange passthrough --- kernel_tuner/backends/julia.py | 11 +++++++---- kernel_tuner/backends/julia_helper.jl | 13 +++---------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 8e27eabc1..849d19369 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -167,14 +167,17 @@ def run_kernel(self, func, gpu_args, threads, grid, stream=None, params=None): if func is None: raise RuntimeError("No Julia kernel compiled or provided.") - gx, gy, gz = (grid + (1,) * (3 - len(grid)))[:3] - tx, ty, tz = (threads + (1,) * (3 - len(threads)))[:3] args_tuple = tuple(gpu_args) params = tuple(params.values()) # important: the order of params must match the order in the kernel definition + # prepare ndrange and workgroupsize + remove_trailing_ones = lambda tup: tup[:len(tup) - next((int(i) for i, x in enumerate(reversed(tup)) if x != 1), len(tup))] + ndrange = remove_trailing_ones(grid) + workgroupsize = remove_trailing_ones(threads) + try: - self.launch_kernel(func, args_tuple, params, (int(gx), int(gy), int(gz)), - (int(tx), int(ty), int(tz)), int(self.smem_size)) + self.launch_kernel(func, args_tuple, params, ndrange, + workgroupsize, int(self.smem_size)) except Exception as e: raise RuntimeError(f"Julia kernel launch failed: {e}") diff --git a/kernel_tuner/backends/julia_helper.jl b/kernel_tuner/backends/julia_helper.jl index 1d50eca2c..0f668df89 100644 --- a/kernel_tuner/backends/julia_helper.jl +++ b/kernel_tuner/backends/julia_helper.jl @@ -14,23 +14,16 @@ module KernelTunerHelper end end - function launch_kernel(kernel, args::Tuple, params::Tuple, grid::NTuple{3,Int}, block::NTuple{3,Int}, shmem::Int) + function launch_kernel(kernel, args::Tuple, params::Tuple, ndrange::Tuple, workgroupsize::Tuple, shmem::Int) # Check if this is a KernelAbstractions kernel - if isdefined(Main, :KernelAbstractions) && backend !== nothing && applicable(kernel, backend, block) - # Calculate ndrange from grid and block - # workgroupsize = block - workgroupsize = (block[1], block[2]) - # ndrange = (grid[1] * block[1], grid[2] * block[2], grid[3] * block[3]) - ndrange = (grid[1], grid[2]) + if isdefined(Main, :KernelAbstractions) && backend !== nothing && applicable(kernel, backend, workgroupsize) # Launch kernel configured_kernel = kernel(backend, workgroupsize) configured_kernel(args..., Val.(params)..., ndrange=ndrange) # Synchronize to ensure kernel completion CUDA.synchronize() else - warn("KernelAbstractions not found, falling back to standard CUDA.jl kernel launch.") - # Standard CUDA.jl kernel (KernelAbstractions not loaded) - CUDA.@sync @cuda threads=block blocks=grid shmem=shmem kernel(args...) + error("Currently, only KernelAbstractions kernels are supported.") end end end From f3c2924765ee46d74004f580a43d596d89d1ad02 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Wed, 10 Dec 2025 16:47:51 +0100 Subject: [PATCH 010/146] Wrote tests for julia backend --- pyproject.toml | 2 +- test/test_julia_functions.py | 69 +++++++++++++++++++++--------------- 2 files changed, 41 insertions(+), 30 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ffc0583be..411e3bc68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "poetry.core.masonry.api" [project] name = "kernel_tuner" description = "An easy to use CUDA/OpenCL kernel tuner in Python" -version = "1.3.0" # adhere to PEP440 versioning: https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/#id55 +version = "1.4.0" # adhere to PEP440 versioning: https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/#id55 readme = "README.md" license = "Apache-2.0" authors = [ diff --git a/test/test_julia_functions.py b/test/test_julia_functions.py index 1dc68652d..05396dca1 100644 --- a/test/test_julia_functions.py +++ b/test/test_julia_functions.py @@ -2,20 +2,29 @@ import pytest from kernel_tuner import tune_kernel -from kernel_tuner.backends import nvcuda +from kernel_tuner.backends.julia import JuliaFunctions from kernel_tuner.core import KernelInstance, KernelSource -from .context import skip_if_no_cuda from .test_runners import env # noqa: F401 +from .context import skip_if_no_julia +from juliacall import ValueBase -try: - from cuda import cuda -except Exception: - pass +kernel_name = "vector_add!" +kernel_string = r""" + using KernelAbstractions + + @kernel function vector_add!(c, a, b, n) + i = @index(Global) + if i <= n + c[i] = a[i] + b[i] + end + end + """ -@skip_if_no_cuda +@skip_if_no_julia def test_ready_argument_list(): + """Ensure Julia backend correctly converts arguments into Julia objects.""" size = 1000 a = np.int32(75) @@ -24,37 +33,39 @@ def test_ready_argument_list(): arguments = [c, a, b] - dev = nvcuda.CudaFunctions(0) + dev = JuliaFunctions(0) gpu_args = dev.ready_argument_list(arguments) - assert isinstance(gpu_args[0], cuda.CUdeviceptr) - assert isinstance(gpu_args[1], np.int32) - assert isinstance(gpu_args[2], cuda.CUdeviceptr) - + # Julia CuArray maps back through PythonCall as pyjl_pointer-like proxies + # Scalars remain scalars + assert isinstance(gpu_args[0], ValueBase) # Julia GPU Array proxy + assert isinstance(gpu_args[1], np.int32) # scalar unchanged + assert isinstance(gpu_args[2], ValueBase) # Julia GPU Array proxy -@skip_if_no_cuda +@skip_if_no_julia def test_compile(): + """Check that Julia kernel code successfully compiles.""" + kernel_sources = KernelSource(kernel_name, kernel_string, "julia") + kernel_instance = KernelInstance(kernel_name, kernel_sources, kernel_string, [], None, None, dict(), []) - kernel_string = """ - extern "C" __global__ void vector_add(float *c, float *a, float *b, int n) { - int i = blockIdx.x * blockDim.x + threadIdx.x; - if (i 0 From 92179ba02eb021b34abf6f81a0316eaef9cbf039 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Wed, 10 Dec 2025 19:55:08 +0100 Subject: [PATCH 011/146] Implemented vector add test for Julia backend --- kernel_tuner/backends/julia.py | 2 ++ test/test_julia_functions.py | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 849d19369..f0ccc5b99 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -173,7 +173,9 @@ def run_kernel(self, func, gpu_args, threads, grid, stream=None, params=None): # prepare ndrange and workgroupsize remove_trailing_ones = lambda tup: tup[:len(tup) - next((int(i) for i, x in enumerate(reversed(tup)) if x != 1), len(tup))] ndrange = remove_trailing_ones(grid) + ndrange = (1,) if len(ndrange) == 0 else ndrange workgroupsize = remove_trailing_ones(threads) + workgroupsize = (1,) if len(workgroupsize) == 0 else workgroupsize try: self.launch_kernel(func, args_tuple, params, ndrange, diff --git a/test/test_julia_functions.py b/test/test_julia_functions.py index 05396dca1..f0cff9799 100644 --- a/test/test_julia_functions.py +++ b/test/test_julia_functions.py @@ -13,7 +13,9 @@ kernel_string = r""" using KernelAbstractions - @kernel function vector_add!(c, a, b, n) + @kernel function vector_add!( + c, a, b, n, ::Val{block_size_x} = Val(128) + ) where {block_size_x} i = @index(Global) if i <= n c[i] = a[i] + b[i] @@ -60,7 +62,6 @@ def test_tune_kernel(env): """Run a minimal Julia kernel tuner example.""" env[0] = kernel_name env[1] = kernel_string - env[2] = (1024,) result, _ = tune_kernel( *env, From 0a32847480ec26c6bfb098997e1011c93fc70c2e Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Wed, 10 Dec 2025 21:25:08 +0100 Subject: [PATCH 012/146] Automatic conversion of Julia vectors where necessary --- kernel_tuner/backends/julia.py | 2 +- kernel_tuner/backends/julia_helper.jl | 2 +- kernel_tuner/interface.py | 2 ++ kernel_tuner/util.py | 12 +++++++++--- test/test_julia_functions.py | 2 +- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index f0ccc5b99..f659346c8 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -266,5 +266,5 @@ def check_package_and_install(self, package): except Exception as e: raise ImportError( f"{package}.jl not found in your Julia environment. " - f"Run `using Pkg; Pkg.add(\"{package}\)` in Julia." + f"Run `using Pkg; Pkg.add(\"{package}\")` in Julia." ) from e diff --git a/kernel_tuner/backends/julia_helper.jl b/kernel_tuner/backends/julia_helper.jl index 0f668df89..2dde26040 100644 --- a/kernel_tuner/backends/julia_helper.jl +++ b/kernel_tuner/backends/julia_helper.jl @@ -23,7 +23,7 @@ module KernelTunerHelper # Synchronize to ensure kernel completion CUDA.synchronize() else - error("Currently, only KernelAbstractions kernels are supported.") + error("Only KernelAbstractions kernels are supported.") end end end diff --git a/kernel_tuner/interface.py b/kernel_tuner/interface.py index 32e91c86f..6160a5a08 100644 --- a/kernel_tuner/interface.py +++ b/kernel_tuner/interface.py @@ -594,6 +594,8 @@ def tune_kernel( kernelsource = core.KernelSource(kernel_name, kernel_source, lang, defines) + block_size_names = util.possible_julia_vector_to_list(block_size_names) + _check_user_input(kernel_name, kernelsource, arguments, block_size_names) # default objective if none is specified diff --git a/kernel_tuner/util.py b/kernel_tuner/util.py index 669e47840..43d5ea152 100644 --- a/kernel_tuner/util.py +++ b/kernel_tuner/util.py @@ -231,11 +231,11 @@ def check_block_size_names(block_size_names): if block_size_names is not None: # do some type checks for the user input if not isinstance(block_size_names, list): - raise ValueError("block_size_names should be a list of strings!") + raise ValueError("block_size_names should be a list of strings!", block_size_names, type(block_size_names)) if len(block_size_names) > 3: - raise ValueError("block_size_names should not contain more than 3 names!") + raise ValueError("block_size_names should not contain more than 3 names!", block_size_names) if not all([isinstance(name, "".__class__) for name in block_size_names]): - raise ValueError("block_size_names should contain only strings!") + raise ValueError("block_size_names should contain only strings!", block_size_names, [type(name) for name in block_size_names]) def append_default_block_size_names(block_size_names): @@ -1339,3 +1339,9 @@ def cuda_error_check(error): if error != nvrtc.nvrtcResult.NVRTC_SUCCESS: _, desc = nvrtc.nvrtcGetErrorString(error) raise RuntimeError(f"NVRTC error: {desc.decode()}") + +def possible_julia_vector_to_list(obj): + """Convert a Julia vector to a Python list if needed.""" + if obj.__class__.__name__ == "VectorValue": + return list(obj) + return obj diff --git a/test/test_julia_functions.py b/test/test_julia_functions.py index f0cff9799..5b8d88f0f 100644 --- a/test/test_julia_functions.py +++ b/test/test_julia_functions.py @@ -38,7 +38,7 @@ def test_ready_argument_list(): dev = JuliaFunctions(0) gpu_args = dev.ready_argument_list(arguments) - # Julia CuArray maps back through PythonCall as pyjl_pointer-like proxies + # Julia Array maps back through PythonCall as pyjl_pointer-like proxies # Scalars remain scalars assert isinstance(gpu_args[0], ValueBase) # Julia GPU Array proxy assert isinstance(gpu_args[1], np.int32) # scalar unchanged From 3e16e38823847a0bb2d568e25f01fb9a8bf9dacf Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Thu, 11 Dec 2025 15:32:11 -0500 Subject: [PATCH 013/146] Implemented backend-agnostic initialization of Julia --- kernel_tuner/backends/julia.py | 177 +++++++++++++++++++------- kernel_tuner/backends/julia_helper.jl | 48 +++---- 2 files changed, 160 insertions(+), 65 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index f659346c8..c48370b0f 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -35,36 +35,34 @@ class JuliaFunctions(GPUBackend): def __init__(self, device=0, iterations=7, compiler_options=None, observers=None): """Initialize Julia backend using JuliaCall.""" if jl is None: - raise ImportError( - "JuliaCall not installed. Please run `pip install juliacall`." - ) - - # Ensure CUDA.jl is available - self.check_package_and_install("CUDA") - - # Initialize CUDA events - self.CUDA = jl.Main.CUDA - self.stream = self.CUDA.stream() - self.start_evt = None - self.end_evt = None - - # Select device - try: - jl.seval(f"CUDA.device!({int(device)})") - JuliaFunctions.last_selected_device = device - except Exception as e: - raise RuntimeError(f"Failed to set Julia CUDA device {device}: {e}") - - # Gather device info - try: - self.name = jl.seval("CUDA.name(CUDA.device())") - cc_tuple = jl.seval("CUDA.capability(CUDA.device())") - self.cc = f"{cc_tuple.major}{cc_tuple.minor}" - except Exception as e: - warn(f"Could not retrieve device name and compute capability from Julia CUDA: {e}") - self.name = f"Julia-CUDA-device-{device}" - self.cc = None - self.max_threads = jl.seval("CUDA.attribute(CUDA.device(), CUDA.DEVICE_ATTRIBUTE_MAX_THREADS_PER_BLOCK)") + raise ImportError("JuliaCall not installed. Please run `pip install juliacall`.") + + # # Ensure CUDA.jl is available + # self.check_package_and_install("CUDA") + + # # Initialize CUDA events + # self.CUDA = jl.Main.CUDA + # self.stream = self.CUDA.stream() + # self.start_evt = None + # self.end_evt = None + + # # Select device + # try: + # jl.seval(f"CUDA.device!({int(device)})") + # JuliaFunctions.last_selected_device = device + # except Exception as e: + # raise RuntimeError(f"Failed to set Julia CUDA device {device}: {e}") + + # # Gather device info + # try: + # self.name = jl.seval("CUDA.name(CUDA.device())") + # cc_tuple = jl.seval("CUDA.capability(CUDA.device())") + # self.cc = f"{cc_tuple.major}{cc_tuple.minor}" + # except Exception as e: + # warn(f"Could not retrieve device name and compute capability from Julia CUDA: {e}") + # self.name = f"Julia-CUDA-device-{device}" + # self.cc = None + # self.max_threads = jl.seval("CUDA.attribute(CUDA.device(), CUDA.DEVICE_ATTRIBUTE_MAX_THREADS_PER_BLOCK)") # Initialize backend attributes self.device = device @@ -74,6 +72,11 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None self.current_kernel = None self.smem_size = 0 + # Initialize Julia backend + self.initialize_backend(device, "metal") # TODO link to user choice + self.start_evt = None + self.end_evt = None + # setup observers self.observers = observers or [] self.observers.append(JuliaRuntimeObserver(self.CUDA)) @@ -83,7 +86,7 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None # Include helper module jl.include(str(Path(__file__).parent / "julia_helper.jl")) - self.to_cuarray = jl.KernelTunerHelper.to_cuarray + self.to_gpuarray = jl.KernelTunerHelper.to_gpuarray self.launch_kernel = jl.KernelTunerHelper.launch_kernel # env info @@ -94,8 +97,95 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None "compiler_options": self.compiler_options, } + def initialize_backend(self, device, backend_name): + """Initialize for a choice of Julia backends by backend_name, one of 'cuda', 'amd', 'intel', 'metal'.""" + + # Map name → Julia module and device-selection calls + backend_map = { + "cuda": { + "pkg": "CUDA", + "module": "CUDA", + "device_select": lambda d: f"CUDA.device!({d})", + "name": "CUDA.name(CUDA.device())", + "max_threads": "CUDA.attribute(CUDA.device(), CUDA.DEVICE_ATTRIBUTE_MAX_THREADS_PER_BLOCK)", + "capability": "CUDA.capability(CUDA.device())", + }, + "amd": { + "pkg": "AMDGPU", + "module": "AMDGPU", + "device_select": lambda d: f"AMDGPU.device!({d})", + "name": "AMDGPU.name(AMDGPU.device())", + "max_threads": "AMDGPU.device_attribute(AMDGPU.device(), :maxthreadsperblock)", + "capability": None, + }, + "intel": { + "pkg": "oneAPI", + "module": "oneAPI", + "device_select": lambda d: f"oneAPI.device!({d})", + "name": "oneAPI.name(oneAPI.device())", + "max_threads": "oneAPI.device_attribute(oneAPI.device(), :max_work_group_size)", + "capability": None, + }, + "metal": { + "pkg": "Metal", + "module": "Metal", + "device_select": lambda d: "Metal.device!(Metal.device())", # only single device support in Metal.jl + "name": "Metal.name(Metal.device())", + "max_threads": "Int(Metal.device().maxThreadsPerThreadgroup.width)", + "capability": None, + }, + } + + backend_name = backend_name.lower() + if backend_name not in backend_map: + raise ValueError(f"Unknown backend: {backend_name}") + info = backend_map[backend_name] + + # Ensure the package is installed + self.check_package_and_install(info["pkg"]) + + # Bring module into Python + backend_mod = getattr(jl.Main, info["module"]) + self.backend_mod = backend_mod + + # Select device + try: + jl.seval(info["device_select"](int(device))) + self.last_selected_device = device + except Exception as e: + raise RuntimeError(f"Failed to select Julia {info['module']} device {device}: {e}") from e + + # Query device name + try: + self.name = jl.seval(info["name"]) + except Exception: + self.name = f"{backend_name}-device-{device}" + + # Query capability if available + if info["capability"] is not None: + try: + cc_tuple = jl.seval(info["capability"]) + # CUDA returns structs with major/minor fields + self.cc = f"{cc_tuple.major}{cc_tuple.minor}" + except Exception: + self.cc = None + else: + self.cc = None + + # Query max threads + try: + self.max_threads = int(jl.seval(info["max_threads"])) + except Exception: + self.max_threads = None + + # Optional: common KernelAbstractions stream abstraction + try: + self.stream = backend_mod.get_default_stream() + except Exception: + self.stream = None + def __del__(self): - # drop CuArray references to let Julia GC handle them + # drop GPUArray references to let Julia GC handle them try: for a in self.allocations: del a @@ -107,14 +197,14 @@ def __del__(self): # ------------------------- def ready_argument_list(self, arguments): - """Convert numpy arrays to CuArray in Julia.""" + """Convert numpy arrays to GPU Array in Julia.""" gpu_args = [] for arg in arguments: if isinstance(arg, np.ndarray): try: - cu = self.to_cuarray(arg) - gpu_args.append(cu) - self.allocations.append(cu) + arr = self.to_gpuarray(arg) + gpu_args.append(arr) + self.allocations.append(arr) except Exception as e: raise RuntimeError(f"Failed to move array to GPU: {e}") else: @@ -137,7 +227,7 @@ def compile(self, kernel_instance): stripped = line.strip() # iterate over multiple using/import statements if stripped.startswith("using ") or stripped.startswith("import "): - for part in stripped.split(','): + for part in stripped.split(","): uses.append(part.replace("import ", "").replace("using ", "").strip()) for package in uses: self.check_package_and_install(package) @@ -168,18 +258,19 @@ def run_kernel(self, func, gpu_args, threads, grid, stream=None, params=None): raise RuntimeError("No Julia kernel compiled or provided.") args_tuple = tuple(gpu_args) - params = tuple(params.values()) # important: the order of params must match the order in the kernel definition + params = tuple(params.values()) # important: the order of params must match the order in the kernel definition # prepare ndrange and workgroupsize - remove_trailing_ones = lambda tup: tup[:len(tup) - next((int(i) for i, x in enumerate(reversed(tup)) if x != 1), len(tup))] + remove_trailing_ones = lambda tup: tup[ + : len(tup) - next((int(i) for i, x in enumerate(reversed(tup)) if x != 1), len(tup)) + ] ndrange = remove_trailing_ones(grid) ndrange = (1,) if len(ndrange) == 0 else ndrange workgroupsize = remove_trailing_ones(threads) workgroupsize = (1,) if len(workgroupsize) == 0 else workgroupsize try: - self.launch_kernel(func, args_tuple, params, ndrange, - workgroupsize, int(self.smem_size)) + self.launch_kernel(func, args_tuple, params, ndrange, workgroupsize, int(self.smem_size)) except Exception as e: raise RuntimeError(f"Julia kernel launch failed: {e}") @@ -261,10 +352,10 @@ def check_package_and_install(self, package): except Exception: try: warn(f"{package}.jl not found, attempting to install it directly.") - jl.seval(f"using Pkg; Pkg.add(\"{package}\")") + jl.seval(f'using Pkg; Pkg.add("{package}")') jl.seval(f"import {package}") except Exception as e: raise ImportError( f"{package}.jl not found in your Julia environment. " - f"Run `using Pkg; Pkg.add(\"{package}\")` in Julia." + f'Run `using Pkg; Pkg.add("{package}")` in Julia.' ) from e diff --git a/kernel_tuner/backends/julia_helper.jl b/kernel_tuner/backends/julia_helper.jl index 2dde26040..ed15311c2 100644 --- a/kernel_tuner/backends/julia_helper.jl +++ b/kernel_tuner/backends/julia_helper.jl @@ -1,29 +1,33 @@ module KernelTunerHelper - using CUDA - const backend = CUDABackend() # currently only CUDA backend is supported +# using CUDA +# const backend = CUDABackend() # currently only CUDA backend is supported +# gpuarraytype = CuArray +using Metal +const backend = MetalBackend() +const GPUArrayType = MetalArray - export to_cuarray, launch_kernel +export to_gpuarray, launch_kernel - function to_cuarray(x) - if isa(x, CuArray) - return x - elseif isa(x, AbstractArray) - return CuArray(x) - else - return x - end +function to_gpuarray(x) + if isa(x, GPUArrayType) + return x + elseif isa(x, AbstractArray) + return GPUArrayType(x) + else + return x end +end - function launch_kernel(kernel, args::Tuple, params::Tuple, ndrange::Tuple, workgroupsize::Tuple, shmem::Int) - # Check if this is a KernelAbstractions kernel - if isdefined(Main, :KernelAbstractions) && backend !== nothing && applicable(kernel, backend, workgroupsize) - # Launch kernel - configured_kernel = kernel(backend, workgroupsize) - configured_kernel(args..., Val.(params)..., ndrange=ndrange) - # Synchronize to ensure kernel completion - CUDA.synchronize() - else - error("Only KernelAbstractions kernels are supported.") - end +function launch_kernel(kernel, args::Tuple, params::Tuple, ndrange::Tuple, workgroupsize::Tuple, shmem::Int) + # Check if this is a KernelAbstractions kernel + if isdefined(Main, :KernelAbstractions) && backend !== nothing && applicable(kernel, backend, workgroupsize) + # Launch kernel + configured_kernel = kernel(backend, workgroupsize) + configured_kernel(args..., Val.(params)..., ndrange=ndrange) + # Synchronize to ensure kernel completion + CUDA.synchronize() + else + error("Only KernelAbstractions kernels are supported.") end end +end From fadf672952697c67df9f93d0aa869f6d3b1c8d41 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Fri, 12 Dec 2025 10:32:18 -0500 Subject: [PATCH 014/146] Implemented working backend-agnostic runs for Julia --- kernel_tuner/backends/julia.py | 112 +++++++++++++++----------- kernel_tuner/backends/julia_helper.jl | 4 +- kernel_tuner/core.py | 49 ++++------- kernel_tuner/observers/julia.py | 50 ++++++++---- 4 files changed, 118 insertions(+), 97 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index c48370b0f..d86613645 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -4,7 +4,7 @@ Requirements: pip install juliacall - and in Julia: ] add CUDA + and in Julia: ] add CUDA / AMDGPU / oneAPI / Metal (will be automatically installed if not present) Notes: - The kernel string should contain a valid Julia GPU kernel function definition. @@ -37,33 +37,6 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None if jl is None: raise ImportError("JuliaCall not installed. Please run `pip install juliacall`.") - # # Ensure CUDA.jl is available - # self.check_package_and_install("CUDA") - - # # Initialize CUDA events - # self.CUDA = jl.Main.CUDA - # self.stream = self.CUDA.stream() - # self.start_evt = None - # self.end_evt = None - - # # Select device - # try: - # jl.seval(f"CUDA.device!({int(device)})") - # JuliaFunctions.last_selected_device = device - # except Exception as e: - # raise RuntimeError(f"Failed to set Julia CUDA device {device}: {e}") - - # # Gather device info - # try: - # self.name = jl.seval("CUDA.name(CUDA.device())") - # cc_tuple = jl.seval("CUDA.capability(CUDA.device())") - # self.cc = f"{cc_tuple.major}{cc_tuple.minor}" - # except Exception as e: - # warn(f"Could not retrieve device name and compute capability from Julia CUDA: {e}") - # self.name = f"Julia-CUDA-device-{device}" - # self.cc = None - # self.max_threads = jl.seval("CUDA.attribute(CUDA.device(), CUDA.DEVICE_ATTRIBUTE_MAX_THREADS_PER_BLOCK)") - # Initialize backend attributes self.device = device self.iterations = iterations @@ -73,13 +46,23 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None self.smem_size = 0 # Initialize Julia backend + self.backend = None self.initialize_backend(device, "metal") # TODO link to user choice self.start_evt = None self.end_evt = None # setup observers self.observers = observers or [] - self.observers.append(JuliaRuntimeObserver(self.CUDA)) + self.observers.append( + JuliaRuntimeObserver( + jl.Main.KernelAbstractions, + self.backend, + self.backend_mod_name, + self.stream, + self.start_evt, + self.end_evt, + ) + ) for observer in self.observers: observer.register_device(self) @@ -109,6 +92,7 @@ def initialize_backend(self, device, backend_name): "name": "CUDA.name(CUDA.device())", "max_threads": "CUDA.attribute(CUDA.device(), CUDA.DEVICE_ATTRIBUTE_MAX_THREADS_PER_BLOCK)", "capability": "CUDA.capability(CUDA.device())", + "GPUArrayType": "CuArray", }, "amd": { "pkg": "AMDGPU", @@ -117,6 +101,7 @@ def initialize_backend(self, device, backend_name): "name": "AMDGPU.name(AMDGPU.device())", "max_threads": "AMDGPU.device_attribute(AMDGPU.device(), :maxthreadsperblock)", "capability": None, + "GPUArrayType": "ROCArray", }, "intel": { "pkg": "oneAPI", @@ -125,6 +110,7 @@ def initialize_backend(self, device, backend_name): "name": "oneAPI.name(oneAPI.device())", "max_threads": "oneAPI.device_attribute(oneAPI.device(), :max_work_group_size)", "capability": None, + "GPUArrayType": "OneArray", }, "metal": { "pkg": "Metal", @@ -133,6 +119,7 @@ def initialize_backend(self, device, backend_name): "name": "Metal.name(Metal.device())", "max_threads": "Int(Metal.device().maxThreadsPerThreadgroup.width)", "capability": None, + "GPUArrayType": "MtlArray", }, } @@ -145,8 +132,13 @@ def initialize_backend(self, device, backend_name): self.check_package_and_install(info["pkg"]) # Bring module into Python - backend_mod = getattr(jl.Main, info["module"]) + self.backend_mod_name = info["module"] + backend_mod = getattr(jl.Main, self.backend_mod_name) self.backend_mod = backend_mod + jl.seval(f"using KernelAbstractions, {self.backend_mod_name}") + jl.seval(f"tmp_arr = {info['GPUArrayType']}(Float32.(zeros(2)))") + self.backend = jl.seval("KernelAbstractions.get_backend(tmp_arr)") + self.GPUArrayType = info["GPUArrayType"] # Select device try: @@ -184,6 +176,23 @@ def initialize_backend(self, device, backend_name): except Exception: self.stream = None + # Set up stream and event attributes for observers + if backend_name == "cuda": + self.start_evt = backend_mod.CuEvent() + self.end_evt = backend_mod.CuEvent() + self.stream = backend_mod.stream() + elif backend_name == "amd": + self.start_evt = backend_mod.ROCEvent() + self.end_evt = backend_mod.ROCEvent() + self.stream = backend_mod.default_stream() + elif backend_name == "intel": + # OneAPI: no events available + self.start_evt = None + self.end_evt = None + elif backend_name == "metal": + self.start_evt = None + self.end_evt = None + def __del__(self): # drop GPUArray references to let Julia GC handle them try: @@ -235,7 +244,7 @@ def compile(self, kernel_instance): # Wrap in a module to avoid name conflicts module_code = f""" module KernelTunerUserKernel - using CUDA + using {self.backend_mod_name} {kernel_code} end """ @@ -276,22 +285,24 @@ def run_kernel(self, func, gpu_args, threads, grid, stream=None, params=None): def start_event(self): """Records the event that marks the start of a measurement.""" - self.start_evt = self.CUDA.CuEvent() - self.CUDA.record(self.start_evt, self.stream) + pass + # TODO + # self.backend_mod.record(self.start_evt, self.stream) def stop_event(self): """Records the event that marks the end of a measurement.""" - self.end_evt = self.CUDA.CuEvent() - self.CUDA.record(self.end_evt, self.stream) + # self.backend_mod.record(self.end_evt, self.stream) + # TODO + pass def kernel_finished(self): """Returns True if the kernel has finished, False otherwise.""" - return self.end_evt is not None + return True # JuliaCall synchronizes on record - @staticmethod - def synchronize(): + # @staticmethod + def synchronize(self): try: - jl.seval("CUDA.synchronize()") + jl.Main.KernelAbstractions.synchronize(self.backend) except Exception as e: raise RuntimeError(f"Julia synchronize failed: {e}") @@ -301,6 +312,7 @@ def synchronize(): @staticmethod def memset(allocation, value, size): + raise NotImplementedError("memset not yet implemented for Julia backend.") try: jl.allocation_tmp = allocation jl.seval(f"CUDA.fill!(allocation_tmp, {int(value)})") @@ -320,26 +332,34 @@ def memcpy_dtoh(dest, src): except Exception as e: raise RuntimeError(f"Julia memcpy_dtoh failed: {e}") - @staticmethod + # @staticmethod def memcpy_htod(dest, src): + raise NotImplementedError("memcpy_htod not yet implemented for Julia backend.", dest, src) try: jl.src_tmp = src - jl.seval("cu_tmp = CUDA.CuArray(src_tmp)") - cu = jl.cu_tmp + jl.seval(f"arr_tmp = {self.GPUArrayType}(src_tmp)") + arr_tmp = jl.arr_tmp del jl.src_tmp - del jl.cu_tmp - return cu + del jl.arr_tmp + return arr_tmp except Exception as e: raise RuntimeError(f"Julia memcpy_htod failed: {e}") def copy_constant_memory_args(self, cmem_args): - raise NotImplementedError("Constant memory not supported in Julia backend. Submit a feature request if needed.") + raise NotImplementedError( + "Constant memory not yet supported in Julia backend. Submit a feature request if needed." + ) def copy_shared_memory_args(self, smem_args): + raise NotImplementedError( + "Shared memory not yet supported in Julia backend. Submit a feature request if needed." + ) self.smem_size = int(smem_args.get("size", 0)) def copy_texture_memory_args(self, texmem_args): - raise NotImplementedError("Texture memory not supported in Julia backend. Submit a feature request if needed.") + raise NotImplementedError( + "Texture memory not yet supported in Julia backend. Submit a feature request if needed." + ) # ------------------------- # Helper functions diff --git a/kernel_tuner/backends/julia_helper.jl b/kernel_tuner/backends/julia_helper.jl index ed15311c2..89392bae4 100644 --- a/kernel_tuner/backends/julia_helper.jl +++ b/kernel_tuner/backends/julia_helper.jl @@ -4,7 +4,7 @@ module KernelTunerHelper # gpuarraytype = CuArray using Metal const backend = MetalBackend() -const GPUArrayType = MetalArray +const GPUArrayType = MtlArray export to_gpuarray, launch_kernel @@ -25,7 +25,7 @@ function launch_kernel(kernel, args::Tuple, params::Tuple, ndrange::Tuple, workg configured_kernel = kernel(backend, workgroupsize) configured_kernel(args..., Val.(params)..., ndrange=ndrange) # Synchronize to ensure kernel completion - CUDA.synchronize() + Main.KernelAbstractions.synchronize(backend) else error("Only KernelAbstractions kernels are supported.") end diff --git a/kernel_tuner/core.py b/kernel_tuner/core.py index 133ff17f6..788884f5f 100644 --- a/kernel_tuner/core.py +++ b/kernel_tuner/core.py @@ -35,7 +35,7 @@ try: from hip._util.types import DeviceArray except ImportError: - DeviceArray = Exception # using Exception here as a type that will never be among kernel arguments + DeviceArray = Exception # using Exception here as a type that will never be among kernel arguments _KernelInstance = namedtuple( "_KernelInstance", @@ -111,15 +111,13 @@ def get_kernel_string(self, index=0, params=None): """ logging.debug("get_kernel_string called") - if hasattr(self, 'lang') and self.lang.upper() == "HYPERTUNER": + if hasattr(self, "lang") and self.lang.upper() == "HYPERTUNER": return "" kernel_source = self.kernel_sources[index] return util.get_kernel_string(kernel_source, params) - def prepare_list_of_files( - self, kernel_name, params, grid, threads, block_size_names - ): + def prepare_list_of_files(self, kernel_name, params, grid, threads, block_size_names): """Prepare the kernel string along with any additional files. The first file in the list is allowed to include or read in the others @@ -324,10 +322,7 @@ def __init__( observers=observers, ) elif lang.upper() == "HYPERTUNER": - dev = HypertunerFunctions( - iterations=iterations, - compiler_options=compiler_options - ) + dev = HypertunerFunctions(iterations=iterations, compiler_options=compiler_options) self.requires_warmup = False else: raise NotImplementedError( @@ -358,7 +353,7 @@ def __init__( # for JULIA, add the JIT warmup prologue observer if lang.upper() == "JULIA": - self.prologue_observers.append(JuliaJITWarmup(self.dev.CUDA)) + self.prologue_observers.append(JuliaJITWarmup(self.dev.backend)) # Take list of observers from self.dev because Backends tend to add their own observer self.benchmark_observers = [ @@ -408,7 +403,6 @@ def benchmark_default(self, func, gpu_args, threads, grid, result): for obs in self.benchmark_observers: result.update(obs.get_results()) - def benchmark_continuous(self, func, gpu_args, threads, grid, result, duration): """Benchmark continuously for at least 'duration' seconds.""" iterations = int(np.ceil(duration / (result["time"] / 1000))) @@ -432,7 +426,6 @@ def benchmark_continuous(self, func, gpu_args, threads, grid, result, duration): for obs in self.continuous_observers: result.update(obs.get_results()) - def set_nvml_parameters(self, instance): """Set the NVML parameters. Avoids setting time leaking into benchmark time.""" if self.use_nvml: @@ -451,7 +444,6 @@ def set_nvml_parameters(self, instance): if "tegra_gr_clock" in instance.params: self.tegra.gr_clock = instance.params["tegra_gr_clock"] - def benchmark(self, func, gpu_args, instance, verbose, objective, skip_nvml_setting=False): """Benchmark the kernel instance.""" logging.debug("benchmark " + instance.name) @@ -501,9 +493,7 @@ def benchmark(self, func, gpu_args, instance, verbose, objective, skip_nvml_sett raise e return result - def check_kernel_output( - self, func, gpu_args, instance, answer, atol, verify, verbose - ): + def check_kernel_output(self, func, gpu_args, instance, answer, atol, verify, verbose): """Runs the kernel once and checks the result against answer.""" logging.debug("check_kernel_output") @@ -514,7 +504,9 @@ def check_kernel_output( should_sync = [answer[i] is not None for i, arg in enumerate(instance.arguments)] else: - should_sync = [isinstance(arg, (np.ndarray, cp.ndarray, torch.Tensor, DeviceArray)) for arg in instance.arguments] + should_sync = [ + isinstance(arg, (np.ndarray, cp.ndarray, torch.Tensor, DeviceArray)) for arg in instance.arguments + ] # re-copy original contents of output arguments to GPU memory, to overwrite any changes # by earlier kernel runs @@ -658,9 +650,7 @@ def compile_kernel(self, instance, verbose): ] error_message = str(e.stderr) if hasattr(e, "stderr") else str(e) if any(re.search(msg, error_message) for msg in shared_mem_error_messages): - logging.debug( - "compile_kernel failed due to kernel using too much shared memory" - ) + logging.debug("compile_kernel failed due to kernel using too much shared memory") if verbose: print( f"skipping config {util.get_instance_string(instance.params)} reason: too much shared memory used" @@ -805,9 +795,7 @@ def _default_verify_function(instance, answer, result_host, atol, verbose): # for each element in the argument list, check if the types match for i, arg in enumerate(instance.arguments): if answer[i] is not None: # skip None elements in the answer list - if isinstance(answer[i], (np.ndarray, cp.ndarray)) and isinstance( - arg, (np.ndarray, cp.ndarray) - ): + if isinstance(answer[i], (np.ndarray, cp.ndarray)) and isinstance(arg, (np.ndarray, cp.ndarray)): if not np.can_cast(arg.dtype, answer[i].dtype): raise TypeError( f"Element {i} of the expected results list has a dtype that is not compatible with the dtype of the kernel output: " @@ -888,18 +876,9 @@ def _flatten(a): output_test = np.allclose(expected, result, atol=atol) if not output_test and verbose: - print( - "Error: " - + util.get_config_string(instance.params) - + " detected during correctness check" - ) - print( - "this error occurred when checking value of the %oth kernel argument" - % (i,) - ) - print( - "Printing kernel output and expected result, set verbose=False to suppress this debug print" - ) + print("Error: " + util.get_config_string(instance.params) + " detected during correctness check") + print("this error occurred when checking value of the %oth kernel argument" % (i,)) + print("Printing kernel output and expected result, set verbose=False to suppress this debug print") np.set_printoptions(edgeitems=50) print("Kernel output:") print(result) diff --git a/kernel_tuner/observers/julia.py b/kernel_tuner/observers/julia.py index 44b67df77..8c7df59a8 100644 --- a/kernel_tuner/observers/julia.py +++ b/kernel_tuner/observers/julia.py @@ -1,27 +1,48 @@ import numpy as np +from time import perf_counter +from warnings import warn from kernel_tuner.observers.observer import BenchmarkObserver, PrologueObserver + class JuliaRuntimeObserver(BenchmarkObserver): - """Observer that measures GPU time using CUDA.CuEvent and CUDA.elapsed.""" + """ + Cross-backend GPU timing for KernelAbstractions: + - CUDA: CuEvent timing + - AMDGPU: ROCEvent timing + - OneAPI: host timing + synchronize (less accurate, no events available) + - Metal: host timing + synchronize + """ + + def __init__(self, kernelabstractions, backend, backend_name, stream=None, start_event=None, end_event=None): + """Observer that measures GPU time depending on the Julia backend used.""" - def __init__(self, CUDA): - self.CUDA = CUDA - self.start = self.CUDA.CuEvent() - self.end = self.CUDA.CuEvent() - self.stream = self.CUDA.stream() # default stream + self.kernelabstractions = kernelabstractions + self.backend = backend + self.name = backend_name + self.stream = stream + self.start = start_event + self.end = end_event self.times = [] + self.t0 = None def before_start(self): - # record start event - self.CUDA.record(self.start, self.stream) + if self.start is not None: + self.backend.record(self.start, self.stream) + else: + # fallback: host-side timestamp + self.t0 = perf_counter() def after_finish(self): - # record end event - self.CUDA.record(self.end, self.stream) - self.CUDA.synchronize(self.end) + if self.end is not None: + self.backend.record(self.end, self.stream) + self.backend.synchronize(self.end) + ms = float(self.backend.elapsed(self.start, self.end)) + else: + self.kernelabstractions.synchronize(self.backend) + dt = perf_counter() - self.t0 + ms = dt * 1000.0 + warn(f"Using host-side timing for Julia {self.name} backend; results may be less accurate.") - # milliseconds - ms = float(self.CUDA.elapsed(self.start, self.end)) self.times.append(ms) def get_results(self): @@ -32,10 +53,11 @@ def get_results(self): self.times = [] return results + class JuliaJITWarmup(PrologueObserver): """Prologue observer to enforce warmup before every configuration to trigger JIT.""" - def __init__(self, CUDA): + def __init__(self, backend): pass def before_start(self): From faba09d6a162369dcc9131086109bce7a621b8e5 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Fri, 12 Dec 2025 14:13:22 -0500 Subject: [PATCH 015/146] Implemented Metal-specific precise GPU timers --- kernel_tuner/backends/julia.py | 51 +++++++++++++++++++++++++-------- kernel_tuner/observers/julia.py | 27 +++++++++++++---- 2 files changed, 60 insertions(+), 18 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index d86613645..7cccb0795 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -47,7 +47,7 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None # Initialize Julia backend self.backend = None - self.initialize_backend(device, "metal") # TODO link to user choice + self.initialize_backend(device, compiler_options["julia_backend"]) self.start_evt = None self.end_evt = None @@ -58,9 +58,9 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None jl.Main.KernelAbstractions, self.backend, self.backend_mod_name, - self.stream, - self.start_evt, - self.end_evt, + stream=self.stream, + start_event=self.start_evt, + end_event=self.end_evt, ) ) for observer in self.observers: @@ -170,6 +170,15 @@ def initialize_backend(self, device, backend_name): except Exception: self.max_threads = None + # Get the device and context + self.backend_device = self.backend_mod.device() + if backend_name == "cuda": + self.contextqueue = self.backend_mod.context + elif backend_name in ("amd", "intel"): + self.contextqueue = self.backend_mod.queue + elif backend_name == "metal": + self.contextqueue = self.backend_mod.MTLCommandQueue(self.backend_device) + # Optional: common KernelAbstractions stream abstraction try: self.stream = backend_mod.get_default_stream() @@ -190,8 +199,8 @@ def initialize_backend(self, device, backend_name): self.start_evt = None self.end_evt = None elif backend_name == "metal": - self.start_evt = None - self.end_evt = None + self.start_evt = self.start_event + self.end_evt = self.stop_event def __del__(self): # drop GPUArray references to let Julia GC handle them @@ -285,15 +294,25 @@ def run_kernel(self, func, gpu_args, threads, grid, stream=None, params=None): def start_event(self): """Records the event that marks the start of a measurement.""" - pass - # TODO - # self.backend_mod.record(self.start_evt, self.stream) + if self.backend_mod_name in ("CUDA", "AMDGPU"): + self.backend_mod.record(self.start_evt, self.stream) + elif self.backend_mod_name == "Metal": + # Because our kernel launch happens via Kernel Abstractions, we wrap our kernel between two command buffers. + # Normally you would just use one command buffer for the actual kernel. + jl.start_buf = self.create_metal_buffer() + jl.seval("Metal.commit!(start_buf)") + self.backend_mod.wait_completed(jl.start_buf) + return float(jl.start_buf.GPUEndTime) # or kernelEndTime? def stop_event(self): """Records the event that marks the end of a measurement.""" - # self.backend_mod.record(self.end_evt, self.stream) - # TODO - pass + if self.backend_mod_name in ("CUDA", "AMDGPU"): + self.backend_mod.record(self.end_evt, self.stream) + elif self.backend_mod_name == "Metal": + jl.end_buf = self.create_metal_buffer() + jl.seval("Metal.commit!(end_buf)") + self.backend_mod.wait_completed(jl.end_buf) + return float(jl.end_buf.GPUStartTime) # or kernelStartTime? def kernel_finished(self): """Returns True if the kernel has finished, False otherwise.""" @@ -379,3 +398,11 @@ def check_package_and_install(self, package): f"{package}.jl not found in your Julia environment. " f'Run `using Pkg; Pkg.add("{package}")` in Julia.' ) from e + + def create_metal_buffer(self): + """Create a Metal buffer in the command queue.""" + try: + buf = self.contextqueue.commandBuffer() + except Exception: + buf = self.backend_mod.MTLCommandBuffer(self.contextqueue) + return buf diff --git a/kernel_tuner/observers/julia.py b/kernel_tuner/observers/julia.py index 8c7df59a8..76550c0fb 100644 --- a/kernel_tuner/observers/julia.py +++ b/kernel_tuner/observers/julia.py @@ -13,12 +13,20 @@ class JuliaRuntimeObserver(BenchmarkObserver): - Metal: host timing + synchronize """ - def __init__(self, kernelabstractions, backend, backend_name, stream=None, start_event=None, end_event=None): + def __init__( + self, + kernelabstractions, + backend, + backend_name, + stream=None, + start_event=None, + end_event=None, + ): """Observer that measures GPU time depending on the Julia backend used.""" self.kernelabstractions = kernelabstractions self.backend = backend - self.name = backend_name + self.name = backend_name.lower() self.stream = stream self.start = start_event self.end = end_event @@ -27,16 +35,23 @@ def __init__(self, kernelabstractions, backend, backend_name, stream=None, start def before_start(self): if self.start is not None: - self.backend.record(self.start, self.stream) + if self.name == "metal": + self.t0 = self.start() + else: + self.backend.record(self.start, self.stream) else: # fallback: host-side timestamp self.t0 = perf_counter() def after_finish(self): if self.end is not None: - self.backend.record(self.end, self.stream) - self.backend.synchronize(self.end) - ms = float(self.backend.elapsed(self.start, self.end)) + if self.name == "metal": + elapsed_us = self.end() - self.t0 + ms = elapsed_us / 1000.0 + else: + self.backend.record(self.end, self.stream) + self.backend.synchronize(self.end) + ms = float(self.backend.elapsed(self.start, self.end)) else: self.kernelabstractions.synchronize(self.backend) dt = perf_counter() - self.t0 From 103ec800f603ab37c94c90b468e587b7c280b3a4 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Fri, 12 Dec 2025 15:03:24 -0500 Subject: [PATCH 016/146] Completed implementation of backend-agnostic generalization --- kernel_tuner/backends/julia.py | 17 +++++++++++++--- kernel_tuner/backends/julia_helper.jl | 15 +++----------- kernel_tuner/core.py | 17 ++++++++++++++++ kernel_tuner/interface.py | 29 +++++++++++++++------------ 4 files changed, 50 insertions(+), 28 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 7cccb0795..698443d80 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -36,6 +36,9 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None """Initialize Julia backend using JuliaCall.""" if jl is None: raise ImportError("JuliaCall not installed. Please run `pip install juliacall`.") + assert ( + len(compiler_options) == 1 + ), "Julia backend requires exactly one backend name: CUDA, AMDGPU, oneAPI, Metal." # Initialize backend attributes self.device = device @@ -47,7 +50,7 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None # Initialize Julia backend self.backend = None - self.initialize_backend(device, compiler_options["julia_backend"]) + self.initialize_backend(device, backend_name=compiler_options[0]) self.start_evt = None self.end_evt = None @@ -66,8 +69,16 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None for observer in self.observers: observer.register_device(self) - # Include helper module - jl.include(str(Path(__file__).parent / "julia_helper.jl")) + jl.seval( + f""" + module KernelTunerHelper + using {self.backend_mod_name} + const kt_julia_backend = {self.backend_mod_name}Backend() + const GPUArrayType = {self.GPUArrayType} + include("{str(Path(__file__).parent / "julia_helper.jl")}") + end + """ + ) self.to_gpuarray = jl.KernelTunerHelper.to_gpuarray self.launch_kernel = jl.KernelTunerHelper.launch_kernel diff --git a/kernel_tuner/backends/julia_helper.jl b/kernel_tuner/backends/julia_helper.jl index 89392bae4..96241b8f1 100644 --- a/kernel_tuner/backends/julia_helper.jl +++ b/kernel_tuner/backends/julia_helper.jl @@ -1,11 +1,3 @@ -module KernelTunerHelper -# using CUDA -# const backend = CUDABackend() # currently only CUDA backend is supported -# gpuarraytype = CuArray -using Metal -const backend = MetalBackend() -const GPUArrayType = MtlArray - export to_gpuarray, launch_kernel function to_gpuarray(x) @@ -20,14 +12,13 @@ end function launch_kernel(kernel, args::Tuple, params::Tuple, ndrange::Tuple, workgroupsize::Tuple, shmem::Int) # Check if this is a KernelAbstractions kernel - if isdefined(Main, :KernelAbstractions) && backend !== nothing && applicable(kernel, backend, workgroupsize) + if isdefined(Main, :KernelAbstractions) && kt_julia_backend !== nothing && applicable(kernel, kt_julia_backend, workgroupsize) # Launch kernel - configured_kernel = kernel(backend, workgroupsize) + configured_kernel = kernel(kt_julia_backend, workgroupsize) configured_kernel(args..., Val.(params)..., ndrange=ndrange) # Synchronize to ensure kernel completion - Main.KernelAbstractions.synchronize(backend) + Main.KernelAbstractions.synchronize(kt_julia_backend) else error("Only KernelAbstractions kernels are supported.") end end -end diff --git a/kernel_tuner/core.py b/kernel_tuner/core.py index 788884f5f..9b1aa92f0 100644 --- a/kernel_tuner/core.py +++ b/kernel_tuner/core.py @@ -222,6 +222,23 @@ def check_argument_lists(self, kernel_name, arguments): else: logging.debug("Checking of arguments list not supported yet for code generators.") + def infer_julia_backend(self): + """Infer the Julia backend from the kernel source.""" + if self.lang.upper() != "JULIA": + return None + + kernel_string = self.get_kernel_string(0) + if kernel_string.find("using CUDA") != -1: + return "cuda" + elif kernel_string.find("using AMDGPU") != -1: + return "amd" + elif kernel_string.find("using oneAPI") != -1: + return "intel" + elif kernel_string.find("using Metal") != -1: + return "metal" + else: + raise ValueError("Could not infer Julia backend from kernel source, provide it as a `compiler_option`") + class DeviceInterface(object): """Class that offers a High-Level Device Interface to the rest of the Kernel Tuner.""" diff --git a/kernel_tuner/interface.py b/kernel_tuner/interface.py index 6160a5a08..49843f00a 100644 --- a/kernel_tuner/interface.py +++ b/kernel_tuner/interface.py @@ -610,6 +610,11 @@ def tune_kernel( # ensure there is always at least three names util.append_default_block_size_names(block_size_names) + # if Julia, infer the Julia backend from the kernelsource + if kernelsource.lang == "JULIA": + if compiler_options is None: + compiler_options = [kernelsource.infer_julia_backend()] + # sort all the options into separate dicts opts = locals() kernel_options = Options([(k, opts[k]) for k in _kernel_options.keys()]) @@ -623,9 +628,9 @@ def tune_kernel( if "max_fevals" in strategy_options: tuning_options["max_fevals"] = strategy_options["max_fevals"] if "time_limit" in strategy_options: - tuning_options["time_limit"] = strategy_options["time_limit"] + tuning_options["time_limit"] = strategy_options["time_limit"] if "searchspace_construction_options" in strategy_options: - searchspace_construction_options = strategy_options["searchspace_construction_options"] + searchspace_construction_options = strategy_options["searchspace_construction_options"] # log the user inputs logging.debug("tune_kernel called") @@ -864,23 +869,19 @@ def tune_kernel_T1( output_T4=True, iterations=7, device=None, - strategy: str=None, - strategy_options: dict={}, + strategy: str = None, + strategy_options: dict = {}, ) -> tuple: """Call the tune function with a T1 input file. - + The device, strategy and strategy_options can be overridden by passing a strategy name and options, otherwise the input file specification is used. """ inputs = get_input_file(input_filepath) kernelspec: dict = inputs["KernelSpecification"] kernel_name: str = kernelspec["KernelName"] kernel_filepath = Path(kernelspec["KernelFile"]) - kernel_source = ( - kernel_filepath if kernel_filepath.exists() else Path(input_filepath).parent / kernel_filepath - ) - kernel_source = ( - kernel_source if kernel_source.exists() else Path(input_filepath).parent.parent / kernel_filepath - ) + kernel_source = kernel_filepath if kernel_filepath.exists() else Path(input_filepath).parent / kernel_filepath + kernel_source = kernel_source if kernel_source.exists() else Path(input_filepath).parent.parent / kernel_filepath assert kernel_source.exists(), f"KernelFile '{kernel_source}' does not exist at {kernel_source.resolve()}" language: str = kernelspec["Language"] problem_size = kernelspec["ProblemSize"] @@ -905,10 +906,12 @@ def tune_kernel_T1( # if it is a path, import the strategy from the file opt_path: Path = Path(strategy_options["custom_search_method_path"]) class_name: str = strategy - assert opt_path.exists(), f"Custom search method path '{opt_path}' does not exist relative to current working directory {Path.cwd()}" + assert ( + opt_path.exists() + ), f"Custom search method path '{opt_path}' does not exist relative to current working directory {Path.cwd()}" optimizer_class = import_class_from_file(opt_path, class_name) filter_keys = ["custom_search_method_path", "max_fevals", "time_limit", "constraint_aware"] - adjusted_strategy_options = {k:v for k, v in strategy_options.items() if k not in filter_keys} + adjusted_strategy_options = {k: v for k, v in strategy_options.items() if k not in filter_keys} optimizer_instance = optimizer_class(**adjusted_strategy_options) strategy = OptAlgWrapper(optimizer_instance) if "constraint_aware" not in strategy_options and hasattr(optimizer_instance, "constraint_aware"): From e853125a28bea8bbb9e1db700a8309e4955f8f17 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Fri, 12 Dec 2025 21:51:37 +0100 Subject: [PATCH 017/146] Fixed CUDA recordings to backend agnostic --- kernel_tuner/backends/julia.py | 17 ++++++++++------- kernel_tuner/observers/julia.py | 16 ++++++++++++---- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 698443d80..096b4d375 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -50,9 +50,9 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None # Initialize Julia backend self.backend = None - self.initialize_backend(device, backend_name=compiler_options[0]) self.start_evt = None self.end_evt = None + self.initialize_backend(device, backend_name=compiler_options[0]) # setup observers self.observers = observers or [] @@ -60,6 +60,7 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None JuliaRuntimeObserver( jl.Main.KernelAbstractions, self.backend, + self.backend_mod, self.backend_mod_name, stream=self.stream, start_event=self.start_evt, @@ -198,12 +199,12 @@ def initialize_backend(self, device, backend_name): # Set up stream and event attributes for observers if backend_name == "cuda": - self.start_evt = backend_mod.CuEvent() - self.end_evt = backend_mod.CuEvent() + self.start_evt = backend_mod.CuEvent + self.end_evt = backend_mod.CuEvent self.stream = backend_mod.stream() elif backend_name == "amd": - self.start_evt = backend_mod.ROCEvent() - self.end_evt = backend_mod.ROCEvent() + self.start_evt = backend_mod.ROCEvent + self.end_evt = backend_mod.ROCEvent self.stream = backend_mod.default_stream() elif backend_name == "intel": # OneAPI: no events available @@ -212,6 +213,8 @@ def initialize_backend(self, device, backend_name): elif backend_name == "metal": self.start_evt = self.start_event self.end_evt = self.stop_event + else: + raise NotImplementedError(f"Backend {backend_name} not supported in Julia backend.") def __del__(self): # drop GPUArray references to let Julia GC handle them @@ -306,7 +309,7 @@ def run_kernel(self, func, gpu_args, threads, grid, stream=None, params=None): def start_event(self): """Records the event that marks the start of a measurement.""" if self.backend_mod_name in ("CUDA", "AMDGPU"): - self.backend_mod.record(self.start_evt, self.stream) + self.backend_mod.record(self.start_evt(), self.stream) elif self.backend_mod_name == "Metal": # Because our kernel launch happens via Kernel Abstractions, we wrap our kernel between two command buffers. # Normally you would just use one command buffer for the actual kernel. @@ -318,7 +321,7 @@ def start_event(self): def stop_event(self): """Records the event that marks the end of a measurement.""" if self.backend_mod_name in ("CUDA", "AMDGPU"): - self.backend_mod.record(self.end_evt, self.stream) + self.backend_mod.record(self.end_evt(), self.stream) elif self.backend_mod_name == "Metal": jl.end_buf = self.create_metal_buffer() jl.seval("Metal.commit!(end_buf)") diff --git a/kernel_tuner/observers/julia.py b/kernel_tuner/observers/julia.py index 76550c0fb..b5502c640 100644 --- a/kernel_tuner/observers/julia.py +++ b/kernel_tuner/observers/julia.py @@ -17,6 +17,7 @@ def __init__( self, kernelabstractions, backend, + backend_mod, backend_name, stream=None, start_event=None, @@ -26,6 +27,7 @@ def __init__( self.kernelabstractions = kernelabstractions self.backend = backend + self.backend_mod = backend_mod self.name = backend_name.lower() self.stream = stream self.start = start_event @@ -33,12 +35,18 @@ def __init__( self.times = [] self.t0 = None + if self.name in ("cuda", "amdgpu"): + # initialize events for this instance of the observer + self.start = self.start() + self.end = self.end() + self.stream = backend_mod.stream() + def before_start(self): if self.start is not None: if self.name == "metal": self.t0 = self.start() else: - self.backend.record(self.start, self.stream) + self.backend_mod.record(self.start, self.stream) else: # fallback: host-side timestamp self.t0 = perf_counter() @@ -49,9 +57,9 @@ def after_finish(self): elapsed_us = self.end() - self.t0 ms = elapsed_us / 1000.0 else: - self.backend.record(self.end, self.stream) - self.backend.synchronize(self.end) - ms = float(self.backend.elapsed(self.start, self.end)) + self.backend_mod.synchronize(self.end) + self.backend_mod.record(self.end, self.stream) + ms = float(self.backend_mod.elapsed(self.start, self.end)) else: self.kernelabstractions.synchronize(self.backend) dt = perf_counter() - self.t0 From 2c2a3766d8f70a6581d132de5faae4f70bba7bc2 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Fri, 12 Dec 2025 15:52:42 -0500 Subject: [PATCH 018/146] Fixed timings for Metal Julia --- kernel_tuner/backends/julia.py | 4 ++-- kernel_tuner/observers/julia.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 698443d80..6ac56d43e 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -50,9 +50,9 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None # Initialize Julia backend self.backend = None - self.initialize_backend(device, backend_name=compiler_options[0]) self.start_evt = None self.end_evt = None + self.initialize_backend(device, backend_name=compiler_options[0]) # setup observers self.observers = observers or [] @@ -323,7 +323,7 @@ def stop_event(self): jl.end_buf = self.create_metal_buffer() jl.seval("Metal.commit!(end_buf)") self.backend_mod.wait_completed(jl.end_buf) - return float(jl.end_buf.GPUStartTime) # or kernelStartTime? + return float(jl.end_buf.GPUEndTime) # or kernelStartTime? def kernel_finished(self): """Returns True if the kernel has finished, False otherwise.""" diff --git a/kernel_tuner/observers/julia.py b/kernel_tuner/observers/julia.py index 76550c0fb..d11f7e7f4 100644 --- a/kernel_tuner/observers/julia.py +++ b/kernel_tuner/observers/julia.py @@ -46,8 +46,7 @@ def before_start(self): def after_finish(self): if self.end is not None: if self.name == "metal": - elapsed_us = self.end() - self.t0 - ms = elapsed_us / 1000.0 + ms = float((self.end() - self.t0) * 1000.0) else: self.backend.record(self.end, self.stream) self.backend.synchronize(self.end) From 0b0aa32de9775436216715c31c6ed33eb7559ef2 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Fri, 12 Dec 2025 22:23:57 +0100 Subject: [PATCH 019/146] Implemented automatic detection of device availability in Julia backend test --- test/test_julia_functions.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/test/test_julia_functions.py b/test/test_julia_functions.py index 5b8d88f0f..ae0db0a19 100644 --- a/test/test_julia_functions.py +++ b/test/test_julia_functions.py @@ -9,6 +9,27 @@ from .context import skip_if_no_julia from juliacall import ValueBase +import subprocess + +available_backend = None +try: + subprocess.check_output('nvidia-smi') + available_backend = 'cuda' +except Exception: # this command not being found can raise quite a few different errors depending on the configuration + try: + subprocess.check_output('rocm-smi') + available_backend = 'amd' + except Exception: + try: + subprocess.check_output('intel_gpu_top -J') + available_backend = 'intel' + except Exception: + try: + subprocess.check_output('system_profiler SPDisplaysDataType | grep "Metal"') + except Exception: + available_backend = 'metal' + + kernel_name = "vector_add!" kernel_string = r""" using KernelAbstractions @@ -35,7 +56,7 @@ def test_ready_argument_list(): arguments = [c, a, b] - dev = JuliaFunctions(0) + dev = JuliaFunctions(0, compiler_options=[available_backend]) gpu_args = dev.ready_argument_list(arguments) # Julia Array maps back through PythonCall as pyjl_pointer-like proxies @@ -50,7 +71,7 @@ def test_compile(): kernel_sources = KernelSource(kernel_name, kernel_string, "julia") kernel_instance = KernelInstance(kernel_name, kernel_sources, kernel_string, [], None, None, dict(), []) - dev = JuliaFunctions(0) + dev = JuliaFunctions(0, compiler_options=[available_backend]) try: dev.compile(kernel_instance) @@ -66,7 +87,8 @@ def test_tune_kernel(env): result, _ = tune_kernel( *env, lang="julia", - verbose=True + verbose=True, + compiler_options=[available_backend] ) assert len(result) > 0 From 6074932c011009583285557155220c75870b077b Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Fri, 12 Dec 2025 23:07:00 +0100 Subject: [PATCH 020/146] Automatic conversion of restrictions from Julia vectors --- kernel_tuner/interface.py | 1 + test/test_julia_functions.py | 1 + 2 files changed, 2 insertions(+) diff --git a/kernel_tuner/interface.py b/kernel_tuner/interface.py index 49843f00a..f490a8558 100644 --- a/kernel_tuner/interface.py +++ b/kernel_tuner/interface.py @@ -595,6 +595,7 @@ def tune_kernel( kernelsource = core.KernelSource(kernel_name, kernel_source, lang, defines) block_size_names = util.possible_julia_vector_to_list(block_size_names) + restrictions = util.possible_julia_vector_to_list(restrictions) _check_user_input(kernel_name, kernelsource, arguments, block_size_names) diff --git a/test/test_julia_functions.py b/test/test_julia_functions.py index ae0db0a19..37e448ab6 100644 --- a/test/test_julia_functions.py +++ b/test/test_julia_functions.py @@ -11,6 +11,7 @@ import subprocess +# try to auto-detect which backend is available available_backend = None try: subprocess.check_output('nvidia-smi') From 8a102e2360aa92a3f086ca134cc9b59ee208d259 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Sat, 13 Dec 2025 05:30:13 +0100 Subject: [PATCH 021/146] Handle tunable parameters from Julia while preserving their order --- kernel_tuner/interface.py | 7 ++++++- kernel_tuner/util.py | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/kernel_tuner/interface.py b/kernel_tuner/interface.py index f490a8558..f2ce9afb0 100644 --- a/kernel_tuner/interface.py +++ b/kernel_tuner/interface.py @@ -594,8 +594,13 @@ def tune_kernel( kernelsource = core.KernelSource(kernel_name, kernel_source, lang, defines) - block_size_names = util.possible_julia_vector_to_list(block_size_names) + if lang == "Julia": + if isinstance(tune_params, dict) or "DictValue" in tune_params.__class__.__name__: + raise ValueError("tune_params should not be a Julia dict, because it does not preserve order. Use a list of pairs instead.") + tune_params = [tuple([k, util.possible_julia_vector_to_list(tp)]) for k, tp in tune_params] + tune_params = dict(tune_params) restrictions = util.possible_julia_vector_to_list(restrictions) + block_size_names = util.possible_julia_vector_to_list(block_size_names) _check_user_input(kernel_name, kernelsource, arguments, block_size_names) diff --git a/kernel_tuner/util.py b/kernel_tuner/util.py index 43d5ea152..6dfa7b7ff 100644 --- a/kernel_tuner/util.py +++ b/kernel_tuner/util.py @@ -14,6 +14,7 @@ from pathlib import Path from types import FunctionType from typing import Union +from math import ceil, floor # to have these available in eval contexts import numpy as np from constraint import ( @@ -1340,6 +1341,7 @@ def cuda_error_check(error): _, desc = nvrtc.nvrtcGetErrorString(error) raise RuntimeError(f"NVRTC error: {desc.decode()}") + def possible_julia_vector_to_list(obj): """Convert a Julia vector to a Python list if needed.""" if obj.__class__.__name__ == "VectorValue": From 3d050827385e4040af64e07563923e38b2a71e58 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Sat, 13 Dec 2025 05:31:12 +0100 Subject: [PATCH 022/146] Implemented error capture and handling for Julia backend --- kernel_tuner/backends/julia.py | 10 ++++++---- kernel_tuner/backends/julia_helper.jl | 26 ++++++++++++++++++++++---- kernel_tuner/core.py | 4 +++- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index ba8c6837c..2e3e87d42 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -143,6 +143,10 @@ def initialize_backend(self, device, backend_name): # Ensure the package is installed self.check_package_and_install(info["pkg"]) + # # Set debug level if needed + # if backend_name == "cuda": + # jl.seval("ENV[\"JULIA_CUDA_DEBUG\"] = \"2\"") + # Bring module into Python self.backend_mod_name = info["module"] backend_mod = getattr(jl.Main, self.backend_mod_name) @@ -301,10 +305,8 @@ def run_kernel(self, func, gpu_args, threads, grid, stream=None, params=None): workgroupsize = remove_trailing_ones(threads) workgroupsize = (1,) if len(workgroupsize) == 0 else workgroupsize - try: - self.launch_kernel(func, args_tuple, params, ndrange, workgroupsize, int(self.smem_size)) - except Exception as e: - raise RuntimeError(f"Julia kernel launch failed: {e}") + # run the kernel + self.launch_kernel(func, args_tuple, params, ndrange, workgroupsize, int(self.smem_size)) def start_event(self): """Records the event that marks the start of a measurement.""" diff --git a/kernel_tuner/backends/julia_helper.jl b/kernel_tuner/backends/julia_helper.jl index 96241b8f1..3d4e3acd7 100644 --- a/kernel_tuner/backends/julia_helper.jl +++ b/kernel_tuner/backends/julia_helper.jl @@ -13,11 +13,29 @@ end function launch_kernel(kernel, args::Tuple, params::Tuple, ndrange::Tuple, workgroupsize::Tuple, shmem::Int) # Check if this is a KernelAbstractions kernel if isdefined(Main, :KernelAbstractions) && kt_julia_backend !== nothing && applicable(kernel, kt_julia_backend, workgroupsize) - # Launch kernel configured_kernel = kernel(kt_julia_backend, workgroupsize) - configured_kernel(args..., Val.(params)..., ndrange=ndrange) - # Synchronize to ensure kernel completion - Main.KernelAbstractions.synchronize(kt_julia_backend) + # Launch kernel + mktemp() do tmppath, _ + open(tmppath, "w") do tmpio + # kernel errors are printed to stdout, capture them + redirect_stdout(tmpio) do + try + configured_kernel(args..., Val.(params)..., ndrange=ndrange) + # Synchronize to ensure kernel completion + Main.KernelAbstractions.synchronize(kt_julia_backend) + catch e + redirect_stdout(stdout) # restore stdout + close(tmpio) + stdout_output = read(tmppath, String) + print("Kernel stdout during exception:\n", stdout_output) + # Rethrow the exception to be caught outside + throw(stdout_output * "\n" * sprint(showerror, e, catch_backtrace())) + end + end + end + # print any stdout output from the kernel + print(read(tmppath, String)) + end else error("Only KernelAbstractions kernels are supported.") end diff --git a/kernel_tuner/core.py b/kernel_tuner/core.py index 9b1aa92f0..eecfb2f58 100644 --- a/kernel_tuner/core.py +++ b/kernel_tuner/core.py @@ -371,6 +371,7 @@ def __init__( # for JULIA, add the JIT warmup prologue observer if lang.upper() == "JULIA": self.prologue_observers.append(JuliaJITWarmup(self.dev.backend)) + self.prologue_observers.append(JuliaJITWarmup(self.dev.backend)) # Take list of observers from self.dev because Backends tend to add their own observer self.benchmark_observers = [ @@ -496,9 +497,10 @@ def benchmark(self, func, gpu_args, instance, verbose, objective, skip_nvml_sett "too many resources requested for launch", "OUT_OF_RESOURCES", "INVALID_WORK_GROUP_SIZE", + "a bounds error was thrown during kernel execution", ] if any([skip_str in str(e) for skip_str in skippable_exceptions]): - logging.debug("benchmark fails due to runtime failure too many resources required") + logging.debug("benchmark fails due to runtime failure / too many resources required") if verbose: print( f"skipping config {util.get_instance_string(instance.params)} reason: too many resources requested for launch" From 5e01a1a70e47ab0043688141968960fcdb727742 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Tue, 20 Jan 2026 16:44:49 +0100 Subject: [PATCH 023/146] Improved automatic detection of Julia backend in test --- test/test_julia_functions.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/test/test_julia_functions.py b/test/test_julia_functions.py index 37e448ab6..5124691e0 100644 --- a/test/test_julia_functions.py +++ b/test/test_julia_functions.py @@ -14,21 +14,25 @@ # try to auto-detect which backend is available available_backend = None try: - subprocess.check_output('nvidia-smi') - available_backend = 'cuda' -except Exception: # this command not being found can raise quite a few different errors depending on the configuration + subprocess.check_output("nvidia-smi") + available_backend = "cuda" +except Exception: # this command not being found can raise quite a few different errors depending on the configuration try: - subprocess.check_output('rocm-smi') - available_backend = 'amd' + subprocess.check_output("rocm-smi") + available_backend = "amd" except Exception: try: - subprocess.check_output('intel_gpu_top -J') - available_backend = 'intel' + subprocess.check_output("intel_gpu_top -J") + available_backend = "intel" except Exception: try: - subprocess.check_output('system_profiler SPDisplaysDataType | grep "Metal"') + output = subprocess.check_output('system_profiler SPDisplaysDataType | grep "Metal"') + if b"Metal Support" in output: + available_backend = "metal" except Exception: - available_backend = 'metal' + pass +if available_backend is None: + warn("No supported GPU backend detected for Julia tests.") kernel_name = "vector_add!" From 89abfa647a0e342c4d299f78285d82da4ae8b2fd Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Thu, 29 Jan 2026 16:11:12 +0100 Subject: [PATCH 024/146] Fix for CUDA 13 event streams --- kernel_tuner/observers/julia.py | 1 + 1 file changed, 1 insertion(+) diff --git a/kernel_tuner/observers/julia.py b/kernel_tuner/observers/julia.py index 682aac743..3ac79668d 100644 --- a/kernel_tuner/observers/julia.py +++ b/kernel_tuner/observers/julia.py @@ -58,6 +58,7 @@ def after_finish(self): else: self.backend_mod.synchronize(self.end) self.backend_mod.record(self.end, self.stream) + self.backend_mod.synchronize(self.end) ms = float(self.backend_mod.elapsed(self.start, self.end)) else: self.kernelabstractions.synchronize(self.backend) From 88cf3dfe24715cb26f9387e4f7678c42b0317c1e Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Thu, 29 Jan 2026 16:30:07 +0100 Subject: [PATCH 025/146] In verbose mode, Julia launch errors are output as warnings --- kernel_tuner/core.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/kernel_tuner/core.py b/kernel_tuner/core.py index 0e4e1112a..5ab09e7c9 100644 --- a/kernel_tuner/core.py +++ b/kernel_tuner/core.py @@ -503,9 +503,15 @@ def benchmark(self, func, gpu_args, instance, verbose, objective, skip_nvml_sett if any([skip_str in str(e) for skip_str in skippable_exceptions]): logging.debug("benchmark fails due to runtime failure / too many resources required") if verbose: - print( - f"skipping config {util.get_instance_string(instance.params)} reason: too many resources requested for launch" - ) + if "Julia" in str(e): + from warnings import warn + warn( + f"skipping config {util.get_instance_string(instance.params)} reason: Julia kernel launch failed because of:\n{e}" + ) + else: + print( + f"skipping config {util.get_instance_string(instance.params)} reason: too many resources requested for launch" + ) result[objective] = util.RuntimeFailedConfig() else: logging.debug("benchmark encountered runtime failure: " + str(e)) From 1cb16339d2ce5866d895e5bed688fef8b393ecfb Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Thu, 29 Jan 2026 17:09:47 +0100 Subject: [PATCH 026/146] Updated Julia functions test --- test/test_julia_functions.py | 40 +++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/test/test_julia_functions.py b/test/test_julia_functions.py index 37e448ab6..1bdb9c818 100644 --- a/test/test_julia_functions.py +++ b/test/test_julia_functions.py @@ -1,3 +1,4 @@ +from warnings import warn import numpy as np import pytest @@ -14,22 +15,26 @@ # try to auto-detect which backend is available available_backend = None try: - subprocess.check_output('nvidia-smi') - available_backend = 'cuda' -except Exception: # this command not being found can raise quite a few different errors depending on the configuration + subprocess.check_output("nvidia-smi") + available_backend = "cuda" +except Exception: # this command not being found can raise quite a few different errors depending on the configuration try: - subprocess.check_output('rocm-smi') - available_backend = 'amd' + subprocess.check_output("rocm-smi") + available_backend = "amd" except Exception: try: - subprocess.check_output('intel_gpu_top -J') - available_backend = 'intel' + subprocess.check_output("intel_gpu_top -J") + available_backend = "intel" except Exception: try: - subprocess.check_output('system_profiler SPDisplaysDataType | grep "Metal"') + output = subprocess.check_output('system_profiler SPDisplaysDataType | grep "Metal"') + if b"Metal Support" in output: + available_backend = "metal" except Exception: - available_backend = 'metal' - + pass +if available_backend is None: + warn("No supported GPU backend detected for Julia tests.") + kernel_name = "vector_add!" kernel_string = r""" @@ -62,9 +67,10 @@ def test_ready_argument_list(): # Julia Array maps back through PythonCall as pyjl_pointer-like proxies # Scalars remain scalars - assert isinstance(gpu_args[0], ValueBase) # Julia GPU Array proxy - assert isinstance(gpu_args[1], np.int32) # scalar unchanged - assert isinstance(gpu_args[2], ValueBase) # Julia GPU Array proxy + assert isinstance(gpu_args[0], ValueBase) # Julia GPU Array proxy + assert isinstance(gpu_args[1], np.int32) # scalar unchanged + assert isinstance(gpu_args[2], ValueBase) # Julia GPU Array proxy + @skip_if_no_julia def test_compile(): @@ -79,17 +85,13 @@ def test_compile(): except Exception as e: pytest.fail("Did not expect any exception: " + str(e)) + @skip_if_no_julia def test_tune_kernel(env): """Run a minimal Julia kernel tuner example.""" env[0] = kernel_name env[1] = kernel_string - result, _ = tune_kernel( - *env, - lang="julia", - verbose=True, - compiler_options=[available_backend] - ) + result, _ = tune_kernel(*env, lang="julia", verbose=True, compiler_options=[available_backend]) assert len(result) > 0 From d9ce98ccbf79ae671e90b2e5f2d80cef8cdcda74 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Thu, 29 Jan 2026 19:19:07 +0100 Subject: [PATCH 027/146] Implemented automatic backend platform detection for Julia --- kernel_tuner/backends/julia.py | 81 +++++++++++++++++++++++++++------- test/test_julia_functions.py | 29 ++---------- 2 files changed, 68 insertions(+), 42 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 532d21040..29199a969 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -12,6 +12,7 @@ - Currently supports CuArray and scalar arguments; constant and texture memory are not implemented. """ +import subprocess import numpy as np from warnings import warn from pathlib import Path @@ -37,9 +38,21 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None """Initialize Julia backend using JuliaCall.""" if jl is None: raise ImportError("JuliaCall not installed. Please run `pip install juliacall`.") - assert ( - len(compiler_options) == 1 - ), "Julia backend requires exactly one backend name: CUDA, AMDGPU, oneAPI, Metal." + self.available_backends = self.detect_backends() + if len(compiler_options) == 1: + if compiler_options[0].upper() not in self.available_backends: + raise ValueError( + f"Requested Julia backend '{compiler_options[0]}' not available. " + f"Available backends: {self.available_backends}" + ) + backend_name = compiler_options[0].upper() + else: + if len(self.available_backends) != 1: + raise ValueError( + f"Multiple or no Julia backends detected: {self.available_backends}. " + "Please specify exactly one backend in compiler_options." + ) + backend_name = self.available_backends[0] # Initialize backend attributes self.device = device @@ -53,7 +66,7 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None self.backend = None self.start_evt = None self.end_evt = None - self.initialize_backend(device, backend_name=compiler_options[0]) + self.initialize_backend(device, backend_name=backend_name) # setup observers self.observers = observers or [] @@ -98,7 +111,7 @@ def initialize_backend(self, device, backend_name): # Map name → Julia module and device-selection calls backend_map = { - "cuda": { + "CUDA": { "pkg": "CUDA", "module": "CUDA", "device_select": lambda d: f"CUDA.device!({d})", @@ -107,7 +120,7 @@ def initialize_backend(self, device, backend_name): "capability": "CUDA.capability(CUDA.device())", "GPUArrayType": "CuArray", }, - "amd": { + "AMD": { "pkg": "AMDGPU", "module": "AMDGPU", "device_select": lambda d: f"AMDGPU.device!({d})", @@ -116,7 +129,7 @@ def initialize_backend(self, device, backend_name): "capability": None, "GPUArrayType": "ROCArray", }, - "intel": { + "INTEL": { "pkg": "oneAPI", "module": "oneAPI", "device_select": lambda d: f"oneAPI.device!({d})", @@ -125,7 +138,7 @@ def initialize_backend(self, device, backend_name): "capability": None, "GPUArrayType": "OneArray", }, - "metal": { + "METAL": { "pkg": "Metal", "module": "Metal", "device_select": lambda d: "Metal.device!(Metal.device())", # only single device support in Metal.jl @@ -136,7 +149,7 @@ def initialize_backend(self, device, backend_name): }, } - backend_name = backend_name.lower() + backend_name = backend_name.upper() if backend_name not in backend_map: raise ValueError(f"Unknown backend: {backend_name}") info = backend_map[backend_name] @@ -189,11 +202,11 @@ def initialize_backend(self, device, backend_name): # Get the device and context self.backend_device = self.backend_mod.device() - if backend_name == "cuda": + if backend_name == "CUDA": self.contextqueue = self.backend_mod.context - elif backend_name in ("amd", "intel"): + elif backend_name in ("AMD", "INTEL"): self.contextqueue = self.backend_mod.queue - elif backend_name == "metal": + elif backend_name == "METAL": self.contextqueue = self.backend_mod.MTLCommandQueue(self.backend_device) # Optional: common KernelAbstractions stream abstraction @@ -203,19 +216,19 @@ def initialize_backend(self, device, backend_name): self.stream = None # Set up stream and event attributes for observers - if backend_name == "cuda": + if backend_name == "CUDA": self.start_evt = backend_mod.CuEvent self.end_evt = backend_mod.CuEvent self.stream = backend_mod.stream() - elif backend_name == "amd": + elif backend_name == "AMD": self.start_evt = backend_mod.ROCEvent self.end_evt = backend_mod.ROCEvent self.stream = backend_mod.default_stream() - elif backend_name == "intel": + elif backend_name == "INTEL": # OneAPI: no events available self.start_evt = None self.end_evt = None - elif backend_name == "metal": + elif backend_name == "METAL": self.start_evt = self.start_event self.end_evt = self.stop_event else: @@ -426,3 +439,39 @@ def create_metal_buffer(self): except Exception: buf = self.backend_mod.MTLCommandBuffer(self.contextqueue) return buf + + def detect_backends(self): + """Detect the Julia backends available.""" + available_backends = [] + for backend_name in ["CUDA", "AMD", "INTEL", "METAL"]: + if backend_name == "CUDA": + try: + subprocess.check_output("nvidia-smi") + available_backends.append(backend_name) + except Exception: + pass + elif backend_name == "AMD": + try: + subprocess.check_output("rocm-smi") + available_backends.append(backend_name) + except Exception: + pass + elif backend_name == "INTEL": + try: + subprocess.check_output("intel_gpu_top -J") + available_backends.append(backend_name) + except Exception: + pass + elif backend_name == "METAL": + try: + output = subprocess.check_output('system_profiler SPDisplaysDataType | grep "Metal"') + if b"Metal Support" in output: + available_backends.append(backend_name) + except Exception: + pass + # try: + # jl.seval(f"import {backend_name.upper() if backend_name != 'amd' else 'AMDGPU'}") + # available_backends.append(backend_name) + # except Exception: + # pass + return available_backends diff --git a/test/test_julia_functions.py b/test/test_julia_functions.py index 1bdb9c818..8d9bf9e26 100644 --- a/test/test_julia_functions.py +++ b/test/test_julia_functions.py @@ -12,29 +12,6 @@ import subprocess -# try to auto-detect which backend is available -available_backend = None -try: - subprocess.check_output("nvidia-smi") - available_backend = "cuda" -except Exception: # this command not being found can raise quite a few different errors depending on the configuration - try: - subprocess.check_output("rocm-smi") - available_backend = "amd" - except Exception: - try: - subprocess.check_output("intel_gpu_top -J") - available_backend = "intel" - except Exception: - try: - output = subprocess.check_output('system_profiler SPDisplaysDataType | grep "Metal"') - if b"Metal Support" in output: - available_backend = "metal" - except Exception: - pass -if available_backend is None: - warn("No supported GPU backend detected for Julia tests.") - kernel_name = "vector_add!" kernel_string = r""" @@ -62,7 +39,7 @@ def test_ready_argument_list(): arguments = [c, a, b] - dev = JuliaFunctions(0, compiler_options=[available_backend]) + dev = JuliaFunctions(0) gpu_args = dev.ready_argument_list(arguments) # Julia Array maps back through PythonCall as pyjl_pointer-like proxies @@ -78,7 +55,7 @@ def test_compile(): kernel_sources = KernelSource(kernel_name, kernel_string, "julia") kernel_instance = KernelInstance(kernel_name, kernel_sources, kernel_string, [], None, None, dict(), []) - dev = JuliaFunctions(0, compiler_options=[available_backend]) + dev = JuliaFunctions(0) try: dev.compile(kernel_instance) @@ -92,6 +69,6 @@ def test_tune_kernel(env): env[0] = kernel_name env[1] = kernel_string - result, _ = tune_kernel(*env, lang="julia", verbose=True, compiler_options=[available_backend]) + result, _ = tune_kernel(*env, lang="julia", verbose=True) assert len(result) > 0 From 3904bb2fd0f628b96739a7547bd040e09e9e5f51 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Thu, 29 Jan 2026 19:19:34 +0100 Subject: [PATCH 028/146] Simplified GPU array conversion --- kernel_tuner/backends/julia_helper.jl | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/kernel_tuner/backends/julia_helper.jl b/kernel_tuner/backends/julia_helper.jl index 3d4e3acd7..8a00ec880 100644 --- a/kernel_tuner/backends/julia_helper.jl +++ b/kernel_tuner/backends/julia_helper.jl @@ -1,13 +1,10 @@ export to_gpuarray, launch_kernel -function to_gpuarray(x) - if isa(x, GPUArrayType) - return x - elseif isa(x, AbstractArray) - return GPUArrayType(x) - else - return x +function to_gpuarray(a) + if isa(a, AbstractArray) + a = gpu_array_type(a) end + return a end function launch_kernel(kernel, args::Tuple, params::Tuple, ndrange::Tuple, workgroupsize::Tuple, shmem::Int) From 7199d03ea0fba8a55218884bad9ea2328ca56b5f Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 2 Feb 2026 17:18:37 +0100 Subject: [PATCH 029/146] Improved automatic detection of Julia backends available --- kernel_tuner/backends/julia.py | 39 +++++++++++++++++++++------------- kernel_tuner/interface.py | 11 +++++++--- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 29199a969..2809bc6ce 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -13,9 +13,12 @@ """ import subprocess -import numpy as np from warnings import warn from pathlib import Path +from json import loads as json_loads, JSONDecodeError +from re import search as regex_search + +import numpy as np from kernel_tuner.backends.backend import GPUBackend from kernel_tuner.observers.julia import JuliaRuntimeObserver @@ -39,7 +42,7 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None if jl is None: raise ImportError("JuliaCall not installed. Please run `pip install juliacall`.") self.available_backends = self.detect_backends() - if len(compiler_options) == 1: + if compiler_options is not None and len(compiler_options) == 1: if compiler_options[0].upper() not in self.available_backends: raise ValueError( f"Requested Julia backend '{compiler_options[0]}' not available. " @@ -448,30 +451,36 @@ def detect_backends(self): try: subprocess.check_output("nvidia-smi") available_backends.append(backend_name) - except Exception: + except (FileNotFoundError, subprocess.CalledProcessError): pass elif backend_name == "AMD": try: subprocess.check_output("rocm-smi") available_backends.append(backend_name) - except Exception: + except (FileNotFoundError, subprocess.CalledProcessError): pass elif backend_name == "INTEL": try: - subprocess.check_output("intel_gpu_top -J") + subprocess.check_output("intel_gpu_top -J".split()) available_backends.append(backend_name) - except Exception: + except (FileNotFoundError, subprocess.CalledProcessError): pass elif backend_name == "METAL": try: - output = subprocess.check_output('system_profiler SPDisplaysDataType | grep "Metal"') - if b"Metal Support" in output: - available_backends.append(backend_name) - except Exception: + output = subprocess.check_output("system_profiler -json SPDisplaysDataType".split()) + json_output = json_loads(output)["SPDisplaysDataType"] + for gpu in json_output: + if "spdisplays_mtlgpufamilysupport" in gpu: + supported = gpu["spdisplays_mtlgpufamilysupport"].lower() + if "metal" in supported: + version = regex_search(r".*metal([\d.]+)", supported).group(1) + if float(version) < 3: + warn( + f"Metal backend detected, but {supported} < 3. " + "Metal.jl requires Metal version 3 or higher." + ) + else: + available_backends.append(backend_name) + except (FileNotFoundError, subprocess.CalledProcessError, JSONDecodeError): pass - # try: - # jl.seval(f"import {backend_name.upper() if backend_name != 'amd' else 'AMDGPU'}") - # available_backends.append(backend_name) - # except Exception: - # pass return available_backends diff --git a/kernel_tuner/interface.py b/kernel_tuner/interface.py index 66f4b0bc6..d2831821a 100644 --- a/kernel_tuner/interface.py +++ b/kernel_tuner/interface.py @@ -65,7 +65,7 @@ pyatf_strategies, random_sample, simulated_annealing, - skopt + skopt, ) from kernel_tuner.strategies.wrapper import OptAlgWrapper @@ -599,7 +599,9 @@ def tune_kernel( if lang == "Julia": if isinstance(tune_params, dict) or "DictValue" in tune_params.__class__.__name__: - raise ValueError("tune_params should not be a Julia dict, because it does not preserve order. Use a list of pairs instead.") + raise ValueError( + "tune_params should not be a Julia dict, because it does not preserve order. Use a list of pairs instead." + ) tune_params = [tuple([k, util.possible_julia_vector_to_list(tp)]) for k, tp in tune_params] tune_params = dict(tune_params) restrictions = util.possible_julia_vector_to_list(restrictions) @@ -622,7 +624,10 @@ def tune_kernel( # if Julia, infer the Julia backend from the kernelsource if kernelsource.lang == "JULIA": if compiler_options is None: - compiler_options = [kernelsource.infer_julia_backend()] + try: + compiler_options = [kernelsource.infer_julia_backend()] + except ValueError: + pass # sort all the options into separate dicts opts = locals() From 3b7d89175da5d07f98b13fec36f65bf01e8518bd Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 2 Feb 2026 18:16:41 +0100 Subject: [PATCH 030/146] Implemented Poetry and Nox installation for Julia compatabilitu --- kernel_tuner/backends/julia_helper.jl | 2 +- noxfile.py | 10 +++++++++- pyproject.toml | 1 + 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/kernel_tuner/backends/julia_helper.jl b/kernel_tuner/backends/julia_helper.jl index 8a00ec880..f63bdb14c 100644 --- a/kernel_tuner/backends/julia_helper.jl +++ b/kernel_tuner/backends/julia_helper.jl @@ -2,7 +2,7 @@ export to_gpuarray, launch_kernel function to_gpuarray(a) if isa(a, AbstractArray) - a = gpu_array_type(a) + a = GPUArrayType(a) end return a end diff --git a/noxfile.py b/noxfile.py index 5ab97f5db..eeef3307f 100644 --- a/noxfile.py +++ b/noxfile.py @@ -19,6 +19,7 @@ nox.options.stop_on_first_error = True nox.options.error_on_missing_interpreters = True nox.options.default_venv_backend = 'virtualenv' +julia_envdir = None # workspace level settings settings_file_path = Path("./noxsettings.toml") @@ -64,6 +65,7 @@ def create_settings(session: Session) -> None: nox.options.venvbackend = venvbackend if envdir is not None and len(envdir) > 0: nox.options.envdir = envdir + julia_envdir = nox.options.envdir # @session # to only run on the current python interpreter # def lint(session: Session) -> None: @@ -106,6 +108,7 @@ def tests(session: Session) -> None: install_cuda = True install_hip = True install_opencl = True + install_julia = True install_additional_tests = False small_disk = False if session.posargs: @@ -114,6 +117,7 @@ def tests(session: Session) -> None: install_cuda = False install_hip = False install_opencl = False + install_julia = False break elif arg.lower() == "skip-cuda": install_cuda = False @@ -121,6 +125,8 @@ def tests(session: Session) -> None: install_hip = False elif arg.lower() == "skip-opencl": install_opencl = False + elif arg.lower() == "skip-julia": + install_julia = False elif arg.lower() == "additional-tests": install_additional_tests = True elif arg.lower() == "small-disk": @@ -133,7 +139,7 @@ def tests(session: Session) -> None: if platform.system().lower() != 'linux': session.warn("HIP is only available on Linux, disabling dependency and tests") install_hip = False - full_install = install_cuda and install_hip and install_opencl and install_additional_tests + full_install = install_cuda and install_hip and install_opencl and install_julia and install_additional_tests # if the user has a small disk, remove the other environment caches before each session is ran if small_disk: @@ -165,6 +171,8 @@ def tests(session: Session) -> None: extras_args.extend(["-E", "hip"]) if install_opencl: extras_args.extend(["-E", "opencl"]) + if install_julia: + extras_args.extend(["-E", "julia"]) # separately install optional dependencies with weird dependencies / build process install_warning = """Installation failed, this likely means that the required hardware or drivers are missing. diff --git a/pyproject.toml b/pyproject.toml index c0a835ff7..271fd0663 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,6 +98,7 @@ cuda = ["pycuda>=2025.1", "nvidia-ml-py>=12.535.108", "pynvml>=11.4.1"] # Attent opencl = ["pyopencl"] # Attention: if pyopencl is changed here, also change `session.install("pyopencl")` in the Noxfile cuda_opencl = ["pycuda>=2024.1", "pyopencl"] # Attention: if pycuda is changed here, also change `session.install("pycuda")` in the Noxfile hip = ["hip-python"] +julia = ["juliacall>=0.9.31"] # Attention: if juliacall is changed here, also change `session.install("juliacall")` in the Noxfile tutorial = ["jupyter>=1.0.0", "matplotlib>=3.5.0", "nvidia-ml-py>=12.535.108"] # ATTENTION: if anything is changed here, run `poetry update` and `poetry export --with docs --without-hashes --format=requirements.txt --output doc/requirements.txt` From 9155447a239525cafc50de82737a2f25c2dc5e5a Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Tue, 3 Feb 2026 19:40:51 +0100 Subject: [PATCH 031/146] Implemented Nox and JuliaCall compatibility for automatically installing Julia deps for tests --- CONTRIBUTING.rst | 2 +- doc/source/dev-environment.rst | 5 +- kernel_tuner/backends/julia.py | 85 ++++++------- noxfile.py | 218 ++++++++++++++++++++++++++------- test/context.py | 2 + 5 files changed, 222 insertions(+), 90 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 7b8a46dc3..47fef18a5 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -26,7 +26,7 @@ Before creating a pull request please ensure the following: * You are working in an up-to-date development environment * You are a human developer. We are not interested in purely AI generated code contributions. -* You have written unit tests to test your additions and all unit tests pass (run :bash:`nox`). If you do not have the required hardware, you can run :bash:`nox -- skip-gpu`, or :bash:`skip-cuda`, :bash:`skip-hip`, :bash:`skip-opencl`. +* You have written unit tests to test your additions and all unit tests pass (run :bash:`nox`). If you do not have the required hardware, you can run :bash:`nox -- skip-gpu`, or :bash:`skip-cuda`, :bash:`skip-hip`, :bash:`skip-opencl`, :bash:`skip-julia`. * The examples still work and produce the same (or better) results * An entry about the change or addition is created in :bash:`CHANGELOG.md` diff --git a/doc/source/dev-environment.rst b/doc/source/dev-environment.rst index 0adb3c83e..afdbd4690 100644 --- a/doc/source/dev-environment.rst +++ b/doc/source/dev-environment.rst @@ -102,9 +102,10 @@ If you do not have fully compatible hardware or environment, you can use the fol * :bash:`nox -- skip-cuda` to skip tests involving CUDA. * :bash:`nox -- skip-hip` to skip tests involving HIP. * :bash:`nox -- skip-opencl` to skip tests involving OpenCL. -* :bash:`nox -- skip-gpu` to skip all tests on the GPU (the same as :bash:`nox -- skip-cuda skip-hip skip-opencl`), especially helpful if you don't have a GPU locally. +* :bash:`nox -- skip-julia` to skip tests involving Julia. +* :bash:`nox -- skip-gpu` to skip all tests on the GPU (the same as :bash:`nox -- skip-cuda skip-hip skip-opencl`), especially helpful if you don't have a GPU in your system. -Contributions you make to the Kernel Tuner should not break any of the tests even if you cannot run them locally! +Contributions you make to Kernel Tuner should not break any of the tests even if you cannot run them locally! Running with :bash:`pytest` will test against your local Python version and PIP packages. In this case, tests that require PyCuda and/or a CUDA capable GPU will be skipped automatically if these are not installed/present. diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 2809bc6ce..19c58fd7c 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -41,7 +41,7 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None """Initialize Julia backend using JuliaCall.""" if jl is None: raise ImportError("JuliaCall not installed. Please run `pip install juliacall`.") - self.available_backends = self.detect_backends() + self.available_backends = detect_julia_gpu_backends() if compiler_options is not None and len(compiler_options) == 1: if compiler_options[0].upper() not in self.available_backends: raise ValueError( @@ -443,44 +443,45 @@ def create_metal_buffer(self): buf = self.backend_mod.MTLCommandBuffer(self.contextqueue) return buf - def detect_backends(self): - """Detect the Julia backends available.""" - available_backends = [] - for backend_name in ["CUDA", "AMD", "INTEL", "METAL"]: - if backend_name == "CUDA": - try: - subprocess.check_output("nvidia-smi") - available_backends.append(backend_name) - except (FileNotFoundError, subprocess.CalledProcessError): - pass - elif backend_name == "AMD": - try: - subprocess.check_output("rocm-smi") - available_backends.append(backend_name) - except (FileNotFoundError, subprocess.CalledProcessError): - pass - elif backend_name == "INTEL": - try: - subprocess.check_output("intel_gpu_top -J".split()) - available_backends.append(backend_name) - except (FileNotFoundError, subprocess.CalledProcessError): - pass - elif backend_name == "METAL": - try: - output = subprocess.check_output("system_profiler -json SPDisplaysDataType".split()) - json_output = json_loads(output)["SPDisplaysDataType"] - for gpu in json_output: - if "spdisplays_mtlgpufamilysupport" in gpu: - supported = gpu["spdisplays_mtlgpufamilysupport"].lower() - if "metal" in supported: - version = regex_search(r".*metal([\d.]+)", supported).group(1) - if float(version) < 3: - warn( - f"Metal backend detected, but {supported} < 3. " - "Metal.jl requires Metal version 3 or higher." - ) - else: - available_backends.append(backend_name) - except (FileNotFoundError, subprocess.CalledProcessError, JSONDecodeError): - pass - return available_backends + +def detect_julia_gpu_backends(): + """Detect the Julia backends available.""" + available_backends = [] + for backend_name in ["CUDA", "AMD", "INTEL", "METAL"]: + if backend_name == "CUDA": + try: + subprocess.check_output("nvidia-smi") + available_backends.append(backend_name) + except (FileNotFoundError, subprocess.CalledProcessError): + pass + elif backend_name == "AMD": + try: + subprocess.check_output("rocm-smi") + available_backends.append(backend_name) + except (FileNotFoundError, subprocess.CalledProcessError): + pass + elif backend_name == "INTEL": + try: + subprocess.check_output("intel_gpu_top -J".split()) + available_backends.append(backend_name) + except (FileNotFoundError, subprocess.CalledProcessError): + pass + elif backend_name == "METAL": + try: + output = subprocess.check_output("system_profiler -json SPDisplaysDataType".split()) + json_output = json_loads(output)["SPDisplaysDataType"] + for gpu in json_output: + if "spdisplays_mtlgpufamilysupport" in gpu: + supported = gpu["spdisplays_mtlgpufamilysupport"].lower() + if "metal" in supported: + version = regex_search(r".*metal([\d.]+)", supported).group(1) + if float(version) < 3: + warn( + f"Metal backend detected, but {supported} < 3. " + "Metal.jl requires Metal version 3 or higher." + ) + else: + available_backends.append(backend_name) + except (FileNotFoundError, subprocess.CalledProcessError, JSONDecodeError): + pass + return available_backends diff --git a/noxfile.py b/noxfile.py index eeef3307f..85f4442a9 100644 --- a/noxfile.py +++ b/noxfile.py @@ -5,7 +5,6 @@ Be careful that the general setup of tests is left to pyproject.toml. """ - import platform import re from pathlib import Path @@ -18,15 +17,22 @@ python_versions_to_test = ["3.11", "3.12", "3.13", "3.14"] nox.options.stop_on_first_error = True nox.options.error_on_missing_interpreters = True -nox.options.default_venv_backend = 'virtualenv' -julia_envdir = None +nox.options.default_venv_backend = "virtualenv" +nox.options.reuse_existing_virtualenvs = True # workspace level settings settings_file_path = Path("./noxsettings.toml") -venvbackend_values = ('none', 'virtualenv', 'conda', 'mamba', 'venv') # from https://nox.thea.codes/en/stable/usage.html#changing-the-sessions-default-backend +venvbackend_values = ( + "none", + "virtualenv", + "conda", + "mamba", + "venv", +) # from https://nox.thea.codes/en/stable/usage.html#changing-the-sessions-default-backend + # TODO remove this from a session function, session is only needed to receive trigger argument -@session # to only run on the current python interpreter +@session # to only run on the current python interpreter def create_settings(session: Session) -> None: """One-time creation of noxsettings.toml.""" arg_trigger = False @@ -39,33 +45,37 @@ def create_settings(session: Session) -> None: # default values venvbackend = nox.options.default_venv_backend envdir = "" - # conversion from old notenv.txt - if noxenv_file_path.exists(): + # conversion from old noxenv.txt + if noxenv_file_path.exists(): venvbackend = noxenv_file_path.read_text().strip() noxenv_file_path.unlink() # write the settings assert venvbackend in venvbackend_values, f"{venvbackend=}, must be one of {','.join(venvbackend_values)}" - settings = (f'venvbackend = "{venvbackend}"\n' - f'envdir = "{envdir}"\n') + settings = f'venvbackend = "{venvbackend}"\n' f'envdir = "{envdir}"\n' settings_file_path.write_text(settings) # exit to make sure the user checks the settings are correct if arg_trigger: - session.warn(f"Settings file '{settings_file_path}' created, exiting. Please check settings are correct before running Nox again.") + session.warn( + f"Settings file '{settings_file_path}' created, exiting. Please check settings are correct before running Nox again." + ) exit(1) + # obtain workspace level settings from the 'noxsettings.toml' file if settings_file_path.exists(): with settings_file_path.open(mode="rb") as fp: import tomli + nox_settings = tomli.load(fp) - venvbackend = nox_settings['venvbackend'] - envdir = nox_settings['envdir'] - assert venvbackend in venvbackend_values, f"File '{settings_file_path}' has {venvbackend=}, must be one of {','.join(venvbackend_values)}" + venvbackend = nox_settings["venvbackend"] + envdir = nox_settings["envdir"] + assert ( + venvbackend in venvbackend_values + ), f"File '{settings_file_path}' has {venvbackend=}, must be one of {','.join(venvbackend_values)}" nox.options.default_venv_backend = venvbackend nox.options.venvbackend = venvbackend if envdir is not None and len(envdir) > 0: nox.options.envdir = envdir - julia_envdir = nox.options.envdir # @session # to only run on the current python interpreter # def lint(session: Session) -> None: @@ -73,31 +83,41 @@ def create_settings(session: Session) -> None: # session.install("ruff") # session.run("ruff", "--output-format=github", "--config=pyproject.toml", ".") -@session # to only run on the current python interpreter + +@session # to only run on the current python interpreter def check_poetry(session: Session) -> None: """Check whether Poetry is correctly configured.""" session.run("poetry", "check", "--no-interaction", external=True) -@session # to only run on the current python interpreter + +@session # to only run on the current python interpreter def check_development_environment(session: Session) -> None: """Check whether the development environment is up to date with the dependencies, and try to update if necessary.""" if session.posargs: - if 'github-action' in session.posargs: - session.log("Skipping development environment check on the GitHub Actions runner, as this is always up to date.") + if "github-action" in session.posargs: + session.log( + "Skipping development environment check on the GitHub Actions runner, as this is always up to date." + ) return None output: str = session.run("poetry", "install", "--sync", "--dry-run", "--with", "test", silent=True, external=True) - match = re.search(r"Package operations: (\d+) (?:install|installs), (\d+) (?:update|updates), (\d+) (?:removal|removals), \d+ skipped", output) + match = re.search( + r"Package operations: (\d+) (?:install|installs), (\d+) (?:update|updates), (\d+) (?:removal|removals), \d+ skipped", + output, + ) assert match is not None, f"Could not check development environment, reason: {output}" groups = match.groups() installs, updates, removals = int(groups[0]), int(groups[1]), int(groups[2]) if installs > 0 or updates > 0: # packages = re.findall(r"• Installing .* | • Updating .*", output, flags=re.MULTILINE) # assert packages is not None - session.warn(f""" + session.warn( + f""" Your development environment is out of date ({installs} installs, {updates} updates). Update with 'poetry install --sync', using '--with' and '-E' for optional dependencies, extras respectively. Note: {removals} packages are not in the specification (i.e. installed manually) and may be removed. - To preview changes, run 'poetry install --sync --dry-run' (with optional dependencies and extras).""") + To preview changes, run 'poetry install --sync --dry-run' (with optional dependencies and extras).""" + ) + @session(python=python_versions_to_test) # missing versions can be installed with `pyenv install ...` # do not forget check / set the versions with `pyenv global`, or `pyenv local` in case of virtual environment @@ -117,7 +137,6 @@ def tests(session: Session) -> None: install_cuda = False install_hip = False install_opencl = False - install_julia = False break elif arg.lower() == "skip-cuda": install_cuda = False @@ -136,7 +155,7 @@ def tests(session: Session) -> None: # check if there are optional dependencies that can not be installed if install_hip: - if platform.system().lower() != 'linux': + if platform.system().lower() != "linux": session.warn("HIP is only available on Linux, disabling dependency and tests") install_hip = False full_install = install_cuda and install_hip and install_opencl and install_julia and install_additional_tests @@ -144,11 +163,23 @@ def tests(session: Session) -> None: # if the user has a small disk, remove the other environment caches before each session is ran if small_disk: try: - session_folder = session.name.replace('.', '*').strip() + session_folder = session.name.replace(".", "*").strip() folders_to_delete: str = session.run( - "find", "./.nox", "-mindepth", "1", "-maxdepth", "1", "-type", "d", "-not", "-name", session_folder, - silent=True, external=True) - folders_to_delete: list[str] = folders_to_delete.split('\n') + "find", + "./.nox", + "-mindepth", + "1", + "-maxdepth", + "1", + "-type", + "d", + "-not", + "-name", + session_folder, + silent=True, + external=True, + ) + folders_to_delete: list[str] = folders_to_delete.split("\n") for folder_to_delete in folders_to_delete: if len(folder_to_delete) > 0: session.warn(f"Removing environment cache {folder_to_delete} because of 'small-disk' argument") @@ -173,60 +204,100 @@ def tests(session: Session) -> None: extras_args.extend(["-E", "opencl"]) if install_julia: extras_args.extend(["-E", "julia"]) + # set the paths to Julia install, environment and project + session_envdir = session.env["VIRTUAL_ENV"] + julia_envdir = Path(session_envdir) / ".julia" + if julia_envdir is not None: + session.env["JULIA_DEPOT_PATH"] = str(Path(julia_envdir).resolve()) + # session.env["PYTHON_JULIACALL_PROJECT"] = str(Path(julia_envdir).resolve()) # separately install optional dependencies with weird dependencies / build process install_warning = """Installation failed, this likely means that the required hardware or drivers are missing. - Run with `-- skip-gpu` or one of the more specific options (e.g. `-- skip-cuda`) to avoid this.""" + Run with `-- skip-gpu` / `-- skip-julia` or one of the more specific options (e.g. `-- skip-cuda`) to avoid this.""" if install_cuda: # use NVCC to get the CUDA version import re + nvcc_output: str = session.run("nvcc", "--version", silent=True, external=True) nvcc_output = "".join(nvcc_output.splitlines()) # convert to single string for easier REGEX cuda_version = re.match(r"^.*release ([0-9]+.[0-9]+).*$", nvcc_output, flags=re.IGNORECASE).group(1).strip() session.warn(f"Detected CUDA version: {cuda_version}") # if we need to install the CUDA extras, first install pycuda seperately, reason: # since version 2022.2 it has `oldest-supported-numpy` as a build dependency which doesn't work with Poetry - if " not found: " in session.run("pip", "show", "pycuda", external=True, silent=True, success_codes=[0,1]): + if " not found: " in session.run("pip", "show", "pycuda", external=True, silent=True, success_codes=[0, 1]): # if PyCUDA is not installed, install it session.warn("PyCUDA not installed") try: - session.install("pycuda", "--no-cache-dir", "--force-reinstall") # Attention: if changed, check `pycuda` in pyproject.toml as well + session.install( + "pycuda", "--no-cache-dir", "--force-reinstall" + ) # Attention: if changed, check `pycuda` in pyproject.toml as well except Exception as error: session.log(error) session.warn(install_warning) else: session.warn("PyCUDA installed") # if PyCUDA is already installed, check whether the CUDA version PyCUDA was installed with matches the current CUDA version - session.install("numpy") # required by pycuda.driver - pycuda_version = session.run("python", "-c", "import pycuda.driver as drv; drv.init(); print('.'.join(list(str(d) for d in drv.get_version())))", silent=True) - shortest_string, longest_string = (pycuda_version, cuda_version) if len(pycuda_version) < len(cuda_version) else (cuda_version, pycuda_version) - if longest_string[:len(shortest_string)] != shortest_string: - session.warn(f"PyCUDA was compiled with a version of CUDA ({pycuda_version}) that does not match the current version ({cuda_version}). Re-installing.") + session.install("numpy") # required by pycuda.driver + pycuda_version = session.run( + "python", + "-c", + "import pycuda.driver as drv; drv.init(); print('.'.join(list(str(d) for d in drv.get_version())))", + silent=True, + ) + shortest_string, longest_string = ( + (pycuda_version, cuda_version) + if len(pycuda_version) < len(cuda_version) + else (cuda_version, pycuda_version) + ) + if longest_string[: len(shortest_string)] != shortest_string: + session.warn( + f"PyCUDA was compiled with a version of CUDA ({pycuda_version}) that does not match the current version ({cuda_version}). Re-installing." + ) try: - session.install("pycuda", "--no-cache-dir", "--force-reinstall") # Attention: if changed, check `pycuda` in pyproject.toml as well + session.install( + "pycuda", "--no-cache-dir", "--force-reinstall" + ) # Attention: if changed, check `pycuda` in pyproject.toml as well except Exception as error: session.log(error) session.warn(install_warning) # finally, install the dependencies, optional dependencies and the package itself - poetry_env = Path(session.run_always("poetry", "env", "info", "--executable", silent=not verbose, external=True).splitlines()[-1].strip()).resolve() + poetry_env = Path( + session.run_always("poetry", "env", "info", "--executable", silent=not verbose, external=True) + .splitlines()[-1] + .strip() + ).resolve() session_env = Path(session.bin, "python/").resolve() assert poetry_env.exists(), f"{poetry_env=} does not exist" assert session_env.exists(), f"{session_env=} does not exist" # if the poetry virtualenv is not set to the session env, use requirements file export instead of Poetry install if poetry_env != session_env: - session.warn(f"Poetry env ({str(poetry_env)}) is not session env ({str(session_env)}), falling back to install via requirements export") + session.warn( + f"Poetry env ({str(poetry_env)}) is not session env ({str(session_env)}), falling back to install via requirements export" + ) requirements_file = Path(f"tmp_test_requirements_{session.name}.txt") if requirements_file.exists(): requirements_file.unlink() if verbose: - print(session.run_always('conda', 'list')) - session.run_always('poetry', 'export', '-f', 'requirements.txt', '-o', requirements_file.name, '--with=test', '--without-hashes', *extras_args, external=True, silent=not verbose) - session.install('-r', requirements_file.name) - session.install('.') + print(session.run_always("conda", "list")) + session.run_always( + "poetry", + "export", + "-f", + "requirements.txt", + "-o", + requirements_file.name, + "--with=test", + "--without-hashes", + *extras_args, + external=True, + silent=not verbose, + ) + session.install("-r", requirements_file.name) + session.install(".") requirements_file.unlink() if verbose: - print(session.run_always('conda', 'list')) + print(session.run_always("conda", "list")) else: try: session.run_always("poetry", "install", "--with", "test", *extras_args, external=True, silent=False) @@ -234,6 +305,16 @@ def tests(session: Session) -> None: session.warn(install_warning) raise error + if install_julia: + # sanitize loader env to avoid issues with Julia loading libraries from the wrong environment + for v in ["DYLD_LIBRARY_PATH", "DYLD_FALLBACK_LIBRARY_PATH"]: + session.env.pop(v, None) + # call Julia to precompile packages in the session environment + session.run("julia", "-e", "using Pkg; Pkg.precompile(); Pkg.instantiate()", external=True) + # install any additional dependencies used by the tests, as `check_package_and_install` won't work from Nox + gpu_backends_string = "".join(f'Pkg.add("{backend}"); ' for backend in detect_julia_gpu_backends()) + session.run("julia", "-e", f'using Pkg; Pkg.add("KernelAbstractions"); {gpu_backends_string}', external=True) + # if applicable, install the dependencies for additional tests if install_additional_tests and install_cuda: install_additional_warning = """ @@ -272,8 +353,55 @@ def tests(session: Session) -> None: # warn if no coverage report if not full_install: - session.warn(""" + session.warn( + """ Tests ran successfully, but only a subset. Coverage file not generated. Run with 'additional-tests' and without 'skip-gpu', 'skip-cuda' etc. to avoid this. - """) + """ + ) + + +### Helper functions ### + +import subprocess +from json import loads as json_loads, JSONDecodeError +from re import search as regex_search + + +def detect_julia_gpu_backends(): + """Detect the Julia backends available, return the assiociated package.""" + available_backends = [] + for backend_name in ["CUDA", "AMD", "INTEL", "METAL"]: + if backend_name == "CUDA": + try: + subprocess.check_output("nvidia-smi") + available_backends.append("CUDA") + except (FileNotFoundError, subprocess.CalledProcessError): + pass + elif backend_name == "AMD": + try: + subprocess.check_output("rocm-smi") + available_backends.append("AMDGPU") + except (FileNotFoundError, subprocess.CalledProcessError): + pass + elif backend_name == "INTEL": + try: + subprocess.check_output("intel_gpu_top -J".split()) + available_backends.append("oneAPI") + except (FileNotFoundError, subprocess.CalledProcessError): + pass + elif backend_name == "METAL": + try: + output = subprocess.check_output("system_profiler -json SPDisplaysDataType".split()) + json_output = json_loads(output)["SPDisplaysDataType"] + for gpu in json_output: + if "spdisplays_mtlgpufamilysupport" in gpu: + supported = gpu["spdisplays_mtlgpufamilysupport"].lower() + if "metal" in supported: + version = regex_search(r".*metal([\d.]+)", supported).group(1) + if float(version) > 3: + available_backends.append("Metal") + except (FileNotFoundError, subprocess.CalledProcessError, JSONDecodeError): + pass + return available_backends diff --git a/test/context.py b/test/context.py index b1a13a281..78fe4df70 100644 --- a/test/context.py +++ b/test/context.py @@ -129,3 +129,5 @@ def skip_backend(backend: str): pytest.skip("No nvc++ on PATH") elif backend.upper() == "HIP" and not hip_present: pytest.skip("HIP Python not installed") + elif backend.upper() == "JULIA" and not shutil.which("julia"): + pytest.skip("No Julia on PATH") From 003968e1485595d4eb3737c9f7d0f255dfaf683b Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Wed, 4 Feb 2026 11:48:09 +0100 Subject: [PATCH 032/146] Updated linting settings and applied to changed files --- .vscode/extensions.json | 25 +++--- .vscode/settings.json | 6 +- kernel_tuner/backends/backend.py | 1 + kernel_tuner/backends/compiler.py | 41 ++++----- kernel_tuner/backends/hip/hip.py | 3 +- kernel_tuner/backends/hypertuner.py | 78 +++++++++-------- kernel_tuner/backends/julia.py | 8 +- kernel_tuner/backends/nvcuda.py | 7 +- kernel_tuner/backends/opencl.py | 17 ++-- kernel_tuner/backends/pycuda.py | 18 +--- kernel_tuner/core.py | 5 +- kernel_tuner/interface.py | 13 ++- kernel_tuner/observers/hip.py | 6 +- kernel_tuner/observers/julia.py | 9 +- kernel_tuner/observers/nvcuda.py | 5 +- kernel_tuner/util.py | 49 ++++++----- noxfile.py | 29 +++---- pyproject.toml | 12 +-- test/context.py | 38 ++++----- test/test_time_budgets.py | 5 +- test/test_util_functions.py | 124 ++++++++++++---------------- test/utils/nvcuda.py | 1 + test/utils/test_directives.py | 11 +-- 23 files changed, 252 insertions(+), 259 deletions(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 035235be3..2fce92093 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,14 +1,15 @@ { - // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. - // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp - // List of extensions which should be recommended for users of this workspace. - "recommendations": [ - "ms-python.python", - "ms-python.black-formatter", - "charliermarsh.ruff", - "bungcip.better-toml", - "njpwerner.autodocstring", - ], - // List of extensions recommended by VS Code that should not be recommended for users of this workspace. - "unwantedRecommendations": [] + // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. + // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp + // List of extensions which should be recommended for users of this workspace. + "recommendations": [ + "ms-python.python", + "ms-python.black-formatter", + "charliermarsh.ruff", + "bungcip.better-toml", + "njpwerner.autodocstring", + "julialang.language-julia", + ], + // List of extensions recommended by VS Code that should not be recommended for users of this workspace. + "unwantedRecommendations": [] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 3089f374a..5ac233f9e 100755 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,9 +17,7 @@ "black-formatter.args": [ "--config=pyproject.toml" ], - "ruff.args": [ - "--config=pyproject.toml" - ], + "ruff.configuration": "pyproject.toml", "autoDocstring.docstringFormat": "google-notypes", "esbonio.sphinx.confDir": "", "python.testing.pytestArgs": [ @@ -27,4 +25,4 @@ ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, -} +} \ No newline at end of file diff --git a/kernel_tuner/backends/backend.py b/kernel_tuner/backends/backend.py index 85a1445d2..ff875efd2 100644 --- a/kernel_tuner/backends/backend.py +++ b/kernel_tuner/backends/backend.py @@ -1,4 +1,5 @@ """This module contains the interface of all kernel_tuner backends.""" + from __future__ import print_function from abc import ABC, abstractmethod diff --git a/kernel_tuner/backends/compiler.py b/kernel_tuner/backends/compiler.py index 65109bdb4..9b0c1c8b3 100644 --- a/kernel_tuner/backends/compiler.py +++ b/kernel_tuner/backends/compiler.py @@ -1,13 +1,13 @@ -""" This module contains the functionality for running and compiling C functions """ +"""This module contains the functionality for running and compiling C functions""" -from collections import namedtuple -import subprocess -import platform +import _ctypes +import ctypes as C import errno -import re import logging -import ctypes as C -import _ctypes +import platform +import re +import subprocess +from collections import namedtuple import numpy as np import numpy.ctypeslib @@ -15,10 +15,9 @@ from kernel_tuner.backends.backend import CompilerBackend from kernel_tuner.observers.compiler import CompilerRuntimeObserver from kernel_tuner.util import ( - get_temp_filename, delete_temp_file, + get_temp_filename, write_file, - SkippableFailure, ) try: @@ -88,7 +87,7 @@ class CompilerFunctions(CompilerBackend): """Class that groups the code for running and compiling C functions""" def __init__(self, iterations=7, compiler_options=None, compiler=None, observers=None): - """instantiate CFunctions object used for interacting with C code + """Instantiate CFunctions object used for interacting with C code :param iterations: Number of iterations used while benchmarking a kernel, 7 by default. :type iterations: int @@ -146,7 +145,7 @@ def __init__(self, iterations=7, compiler_options=None, compiler=None, observers self.name = platform.processor() def ready_argument_list(self, arguments): - """ready argument list to be passed to the C function + """Ready argument list to be passed to the C function :param arguments: List of arguments to be passed to the C function. The order should match the argument list on the C function. @@ -181,7 +180,7 @@ def ready_argument_list(self, arguments): return ctype_args def compile(self, kernel_instance): - """call the C compiler to compile the kernel, return the function + """Call the C compiler to compile the kernel, return the function :param kernel_instance: An object representing the specific instance of the tunable kernel in the parameter space. @@ -311,29 +310,33 @@ def compile(self, kernel_instance): def start_event(self): """Records the event that marks the start of a measurement - C backend does not use events""" + C backend does not use events + """ pass def stop_event(self): """Records the event that marks the end of a measurement - C backend does not use events""" + C backend does not use events + """ pass def kernel_finished(self): """Returns True if the kernel has finished, False otherwise - C backend does not support asynchronous launches""" + C backend does not support asynchronous launches + """ return True def synchronize(self): """Halts execution until device has finished its tasks - C backend does not support asynchronous launches""" + C backend does not support asynchronous launches + """ pass def run_kernel(self, func, c_args, threads, grid, stream=None, params=None): - """runs the kernel once, returns whatever the kernel returns + """Runs the kernel once, returns whatever the kernel returns :param func: A C function compiled for this specific configuration :type func: ctypes._FuncPtr @@ -367,7 +370,7 @@ def run_kernel(self, func, c_args, threads, grid, stream=None, params=None): return time def memset(self, allocation, value, size): - """set the memory in allocation to the value in value + """Set the memory in allocation to the value in value :param allocation: An Argument for some memory allocation unit :type allocation: Argument @@ -419,7 +422,7 @@ def refresh_memory(self, _, arguments, should_sync): self.memcpy_dtoh(arg, self.allocations[i]) def cleanup_lib(self): - """unload the previously loaded shared library""" + """Unload the previously loaded shared library""" if self.lib is None: return diff --git a/kernel_tuner/backends/hip/hip.py b/kernel_tuner/backends/hip/hip.py index 71eab6fff..0c5f9b6ee 100644 --- a/kernel_tuner/backends/hip/hip.py +++ b/kernel_tuner/backends/hip/hip.py @@ -7,8 +7,8 @@ import numpy as np from kernel_tuner.backends.backend import GPUBackend -from kernel_tuner.observers.hip import HipRuntimeObserver from kernel_tuner.backends.hip.util import hip_check +from kernel_tuner.observers.hip import HipRuntimeObserver try: from hip import hip, hiprtc @@ -32,6 +32,7 @@ hipSuccess = 0 + class HipFunctions(GPUBackend): """Class that groups the HIP functions on maintains state about the device.""" diff --git a/kernel_tuner/backends/hypertuner.py b/kernel_tuner/backends/hypertuner.py index d99ad5eea..7762ea596 100644 --- a/kernel_tuner/backends/hypertuner.py +++ b/kernel_tuner/backends/hypertuner.py @@ -27,12 +27,14 @@ def after_finish(self): self.scores.append(self.dev.last_score) def get_results(self): - results = {'score': mean(self.scores), 'scores': self.scores.copy()} + results = {"score": mean(self.scores), "scores": self.scores.copy()} self.scores = [] return results + class HypertunerFunctions(Backend): """Class for executing hyperparameter tuning.""" + units = {} def __init__(self, iterations, compiler_options=None): @@ -51,30 +53,30 @@ def __init__(self, iterations, compiler_options=None): "name": "dedispersion_milo", "folder": folder, "input_file": "dedispersion_milo.json", - "objective_performance_keys": ["time"] + "objective_performance_keys": ["time"], }, { "name": "hotspot_milo", "folder": folder, "input_file": "hotspot_milo.json", - "objective_performance_keys": ["GFLOP/s"] + "objective_performance_keys": ["GFLOP/s"], }, { "name": "convolution_milo", "folder": folder, "input_file": "convolution_milo.json", - "objective_performance_keys": ["time"] + "objective_performance_keys": ["time"], }, { "name": "gemm_milo", "folder": folder, "input_file": "gemm_milo.json", - "objective_performance_keys": ["time"] - } + "objective_performance_keys": ["time"], + }, ] # any additional settings - self.override = { - "experimental_groups_defaults": { + self.override = { + "experimental_groups_defaults": { "repeats": 25, "samples": self.iterations, "minimum_fraction_of_budget_valid": 0.1, @@ -84,10 +86,8 @@ def __init__(self, iterations, compiler_options=None): "cutoff_percentile": 0.95, "cutoff_percentile_start": 0.01, "cutoff_type": "time", - "objective_time_keys": [ - "all" - ] - } + "objective_time_keys": ["all"], + }, } # override the defaults with compiler options if provided @@ -113,7 +113,7 @@ def ready_argument_list(self, arguments): if arglist is None: arglist = [] return arglist - + def compile(self, kernel_instance): super().compile(kernel_instance) path = Path(__file__).parent.parent.parent / "hyperparamtuning" @@ -121,37 +121,47 @@ def compile(self, kernel_instance): # strategy settings strategy: str = kernel_instance.arguments[0] - hyperparams = [{'name': k, 'value': v} for k, v in kernel_instance.params.items()] + hyperparams = [{"name": k, "value": v} for k, v in kernel_instance.params.items()] hyperparams_string = "_".join(f"{k}={str(v)}" for k, v in kernel_instance.params.items()) - searchspace_strategies = [{ - "autotuner": "KernelTuner", - "name": f"{strategy.lower()}_{hyperparams_string}", - "display_name": strategy.replace('_', ' ').capitalize(), - "search_method": strategy.lower(), - 'search_method_hyperparameters': hyperparams - }] + searchspace_strategies = [ + { + "autotuner": "KernelTuner", + "name": f"{strategy.lower()}_{hyperparams_string}", + "display_name": strategy.replace("_", " ").capitalize(), + "search_method": strategy.lower(), + "search_method_hyperparameters": hyperparams, + } + ] name = kernel_instance.name if len(kernel_instance.name) > 0 else kernel_instance.kernel_source.kernel_name - experiments_filepath = generate_experiment_file(name, path, searchspace_strategies, self.applications, self.gpus, - override=self.override, generate_unique_file=True, overwrite_existing_file=True) + experiments_filepath = generate_experiment_file( + name, + path, + searchspace_strategies, + self.applications, + self.gpus, + override=self.override, + generate_unique_file=True, + overwrite_existing_file=True, + ) return str(experiments_filepath) - + def start_event(self): return super().start_event() - + def stop_event(self): return super().stop_event() - + def kernel_finished(self): super().kernel_finished() return True - + def synchronize(self): return super().synchronize() - + def run_kernel(self, func, gpu_args=None, threads=None, grid=None, stream=None, params=None): # from cProfile import Profile - + # # generate the experiments file # experiments_filepath = Path(func) @@ -161,23 +171,23 @@ def run_kernel(self, func, gpu_args=None, threads=None, grid=None, stream=None, # pr.dump_stats('diff_evo_hypertune_hotspot.prof') # self.last_score = scores[list(scores.keys())[0]]['score'] # raise ValueError(scores) - + # generate the experiments file experiments_filepath = Path(func) # run the methodology to get a fitness score for this configuration scores = get_strategy_scores(str(experiments_filepath), full_validate_on_load=False) - self.last_score = scores[list(scores.keys())[0]]['score'] + self.last_score = scores[list(scores.keys())[0]]["score"] # remove the experiments file experiments_filepath.unlink() - + def memset(self, allocation, value, size): return super().memset(allocation, value, size) - + def memcpy_dtoh(self, dest, src): return super().memcpy_dtoh(dest, src) - + def memcpy_htod(self, dest, src): return super().memcpy_htod(dest, src) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 19c58fd7c..9ad5b0de8 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -13,10 +13,11 @@ """ import subprocess -from warnings import warn +from json import JSONDecodeError +from json import loads as json_loads from pathlib import Path -from json import loads as json_loads, JSONDecodeError from re import search as regex_search +from warnings import warn import numpy as np @@ -25,8 +26,8 @@ from kernel_tuner.util import SkippableFailure try: - from juliacall import Main as jl from juliacall import JuliaError + from juliacall import Main as jl except ImportError: jl = None @@ -111,7 +112,6 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None def initialize_backend(self, device, backend_name): """Initialize for a choice of Julia backends by backend_name, one of 'cuda', 'amd', 'intel', 'metal'.""" - # Map name → Julia module and device-selection calls backend_map = { "CUDA": { diff --git a/kernel_tuner/backends/nvcuda.py b/kernel_tuner/backends/nvcuda.py index 8a316b6c0..1d42afba0 100644 --- a/kernel_tuner/backends/nvcuda.py +++ b/kernel_tuner/backends/nvcuda.py @@ -1,5 +1,4 @@ """This module contains all NVIDIA cuda-python specific kernel_tuner functions.""" -from warnings import warn import numpy as np @@ -11,11 +10,13 @@ # embedded in try block to be able to generate documentation # and run tests without cuda-python installed try: - from cuda.bindings import driver, runtime, nvrtc + from cuda.bindings import driver, nvrtc, runtime except ImportError: try: # backward compatibility hack for older cuda-python versions - from cuda import cuda as driver, cudart as runtime, nvrtc as nvrtc + from cuda import cuda as driver + from cuda import cudart as runtime + from cuda import nvrtc as nvrtc except ImportError: driver = None diff --git a/kernel_tuner/backends/opencl.py b/kernel_tuner/backends/opencl.py index 05afddc04..6a8fdaf37 100644 --- a/kernel_tuner/backends/opencl.py +++ b/kernel_tuner/backends/opencl.py @@ -1,4 +1,5 @@ """This module contains all OpenCL specific kernel_tuner functions.""" + from __future__ import print_function import numpy as np @@ -16,9 +17,7 @@ class OpenCLFunctions(GPUBackend): """Class that groups the OpenCL functions on maintains some state about the device.""" - def __init__( - self, device=0, platform=0, iterations=7, compiler_options=None, observers=None - ): + def __init__(self, device=0, platform=0, iterations=7, compiler_options=None, observers=None): """Creates OpenCL device context and reads device properties. :param device: The ID of the OpenCL device to use for benchmarking @@ -37,14 +36,10 @@ def __init__( platforms = cl.get_platforms() self.ctx = cl.Context(devices=[platforms[platform].get_devices()[device]]) - self.queue = cl.CommandQueue( - self.ctx, properties=cl.command_queue_properties.PROFILING_ENABLE - ) + self.queue = cl.CommandQueue(self.ctx, properties=cl.command_queue_properties.PROFILING_ENABLE) self.mf = cl.mem_flags # inspect device properties - self.max_threads = self.ctx.devices[0].get_info( - cl.device_info.MAX_WORK_GROUP_SIZE - ) + self.max_threads = self.ctx.devices[0].get_info(cl.device_info.MAX_WORK_GROUP_SIZE) self.compiler_options = compiler_options or [] # observer stuff @@ -108,9 +103,7 @@ def compile(self, kernel_instance): :returns: An OpenCL kernel that can be called directly. :rtype: pyopencl.Kernel """ - prg = cl.Program(self.ctx, kernel_instance.kernel_string).build( - options=self.compiler_options - ) + prg = cl.Program(self.ctx, kernel_instance.kernel_string).build(options=self.compiler_options) func = getattr(prg, kernel_instance.name) return func diff --git a/kernel_tuner/backends/pycuda.py b/kernel_tuner/backends/pycuda.py index 5e6d7bcb7..e8b4da361 100644 --- a/kernel_tuner/backends/pycuda.py +++ b/kernel_tuner/backends/pycuda.py @@ -1,4 +1,5 @@ """This module contains all CUDA specific kernel_tuner functions.""" + from __future__ import print_function import logging @@ -97,13 +98,9 @@ def _finish_up(): PyCudaFunctions.last_selected_context = self.context # inspect device properties - devprops = { - str(k): v for (k, v) in self.context.get_device().get_attributes().items() - } + devprops = {str(k): v for (k, v) in self.context.get_device().get_attributes().items()} self.max_threads = devprops["MAX_THREADS_PER_BLOCK"] - cc = str(devprops.get("COMPUTE_CAPABILITY_MAJOR", "0")) + str( - devprops.get("COMPUTE_CAPABILITY_MINOR", "0") - ) + cc = str(devprops.get("COMPUTE_CAPABILITY_MAJOR", "0")) + str(devprops.get("COMPUTE_CAPABILITY_MINOR", "0")) if cc == "00": cc = self.context.get_device().compute_capability() self.cc = str(cc) @@ -350,14 +347,7 @@ def run_kernel(self, func, gpu_args, threads, grid, stream=None, params=None): """ if stream is None: stream = self.stream - func( - *gpu_args, - block=threads, - grid=grid, - stream=stream, - shared=self.smem_size, - texrefs=self.texrefs - ) + func(*gpu_args, block=threads, grid=grid, stream=stream, shared=self.smem_size, texrefs=self.texrefs) def memset(self, allocation, value, size): """Set the memory in allocation to the value in value. diff --git a/kernel_tuner/core.py b/kernel_tuner/core.py index 5ab09e7c9..066ef30b3 100644 --- a/kernel_tuner/core.py +++ b/kernel_tuner/core.py @@ -17,15 +17,15 @@ from kernel_tuner.backends.compiler import CompilerFunctions from kernel_tuner.backends.cupy import CupyFunctions from kernel_tuner.backends.hip import HipFunctions -from kernel_tuner.backends.julia import JuliaFunctions from kernel_tuner.backends.hypertuner import HypertunerFunctions +from kernel_tuner.backends.julia import JuliaFunctions from kernel_tuner.backends.nvcuda import CudaFunctions from kernel_tuner.backends.opencl import OpenCLFunctions from kernel_tuner.backends.pycuda import PyCudaFunctions +from kernel_tuner.observers.julia import JuliaJITWarmup from kernel_tuner.observers.nvml import NVMLObserver from kernel_tuner.observers.observer import ContinuousObserver, OutputObserver, PrologueObserver from kernel_tuner.observers.tegra import TegraObserver -from kernel_tuner.observers.julia import JuliaJITWarmup try: import torch @@ -505,6 +505,7 @@ def benchmark(self, func, gpu_args, instance, verbose, objective, skip_nvml_sett if verbose: if "Julia" in str(e): from warnings import warn + warn( f"skipping config {util.get_instance_string(instance.params)} reason: Julia kernel launch failed because of:\n{e}" ) diff --git a/kernel_tuner/interface.py b/kernel_tuner/interface.py index d2831821a..2b66735da 100644 --- a/kernel_tuner/interface.py +++ b/kernel_tuner/interface.py @@ -27,13 +27,12 @@ import logging from argparse import ArgumentParser from ast import literal_eval +from copy import deepcopy from datetime import datetime from pathlib import Path from time import perf_counter -from copy import deepcopy import numpy -from constraint import Constraint import kernel_tuner.core as core import kernel_tuner.util as util @@ -740,7 +739,8 @@ def preprocess_cache(filepath): tune_kernel.__doc__ = _tune_kernel_docstring -_run_kernel_docstring = """Compile and run a single kernel +_run_kernel_docstring = ( + """Compile and run a single kernel Compiles and runs a single kernel once, given a specific instance of the kernels tuning parameters. However, instead of measuring execution time run_kernel returns the output of the kernel. @@ -766,10 +766,9 @@ def preprocess_cache(filepath): :returns: A list of numpy arrays, similar to the arguments passed to this function, containing the output after kernel execution. :rtype: list -""" % _get_docstring( - _kernel_options -) + _get_docstring( - _device_options +""" + % _get_docstring(_kernel_options) + + _get_docstring(_device_options) ) diff --git a/kernel_tuner/observers/hip.py b/kernel_tuner/observers/hip.py index 5e27fb1b5..311d54376 100644 --- a/kernel_tuner/observers/hip.py +++ b/kernel_tuner/observers/hip.py @@ -1,7 +1,7 @@ import numpy as np -from kernel_tuner.observers.observer import BenchmarkObserver from kernel_tuner.backends.hip.util import hip_check +from kernel_tuner.observers.observer import BenchmarkObserver try: from hip import hip, hiprtc @@ -15,7 +15,9 @@ class HipRuntimeObserver(BenchmarkObserver): def __init__(self, dev): if not hip or not hiprtc: - raise ImportError("Unable to import HIP Python, or check https://kerneltuner.github.io/kernel_tuner/stable/install.html#hip-and-hip-python.") + raise ImportError( + "Unable to import HIP Python, or check https://kerneltuner.github.io/kernel_tuner/stable/install.html#hip-and-hip-python." + ) self.dev = dev self.stream = dev.stream diff --git a/kernel_tuner/observers/julia.py b/kernel_tuner/observers/julia.py index 3ac79668d..2f7163de8 100644 --- a/kernel_tuner/observers/julia.py +++ b/kernel_tuner/observers/julia.py @@ -1,12 +1,14 @@ -import numpy as np from time import perf_counter from warnings import warn + +import numpy as np + from kernel_tuner.observers.observer import BenchmarkObserver, PrologueObserver class JuliaRuntimeObserver(BenchmarkObserver): - """ - Cross-backend GPU timing for KernelAbstractions: + """Cross-backend GPU timing for KernelAbstractions. + - CUDA: CuEvent timing - AMDGPU: ROCEvent timing - OneAPI: host timing + synchronize (less accurate, no events available) @@ -24,7 +26,6 @@ def __init__( end_event=None, ): """Observer that measures GPU time depending on the Julia backend used.""" - self.kernelabstractions = kernelabstractions self.backend = backend self.backend_mod = backend_mod diff --git a/kernel_tuner/observers/nvcuda.py b/kernel_tuner/observers/nvcuda.py index 6454b1191..aae6cc5cb 100644 --- a/kernel_tuner/observers/nvcuda.py +++ b/kernel_tuner/observers/nvcuda.py @@ -30,9 +30,6 @@ def after_finish(self): self.times.append(time) def get_results(self): - results = { - "time": np.average(self.times), - "times": self.times.copy() - } + results = {"time": np.average(self.times), "times": self.times.copy()} self.times = [] return results diff --git a/kernel_tuner/util.py b/kernel_tuner/util.py index b41f01df5..a11ed0480 100644 --- a/kernel_tuner/util.py +++ b/kernel_tuner/util.py @@ -1,4 +1,5 @@ """Module for kernel tuner utility functions.""" + import ast import errno import json @@ -14,7 +15,6 @@ from pathlib import Path from types import FunctionType from typing import Union -from math import ceil, floor # to have these available in eval contexts import numpy as np from constraint import ( @@ -203,14 +203,13 @@ def check_stop_criterion(to: dict) -> float: if "max_fevals" in to: if len(to.unique_results) >= to.max_fevals: raise StopCriterionReached(f"max_fevals ({to.max_fevals}) reached") - if not "time_limit" in to: + if "time_limit" not in to: return len(to.unique_results) / to.max_fevals if "time_limit" in to: time_spent = (time.perf_counter() - to.start_time) + (to.simulated_time * 1e-3) + to.startup_time if time_spent > to.time_limit: raise StopCriterionReached("time limit exceeded") return time_spent / to.time_limit - def check_tune_params_list(tune_params, observers, simulation_mode=False): @@ -232,7 +231,11 @@ def check_block_size_names(block_size_names): if len(block_size_names) > 3: raise ValueError("block_size_names should not contain more than 3 names!", block_size_names) if not all([isinstance(name, "".__class__) for name in block_size_names]): - raise ValueError("block_size_names should contain only strings!", block_size_names, [type(name) for name in block_size_names]) + raise ValueError( + "block_size_names should contain only strings!", + block_size_names, + [type(name) for name in block_size_names], + ) def append_default_block_size_names(block_size_names): @@ -268,7 +271,6 @@ def check_block_size_params_names_list(block_size_names, tune_params): return block_size_names - def check_restriction(restrict, params: dict) -> bool: """Check whether a configuration meets a search space restriction.""" # if it's a python-constraint, convert to function and execute @@ -474,7 +476,9 @@ def get_dimension_divisor(divisor, default, params): for div in divisor: divisor_num *= get_dimension_divisor(div, 1, params) else: - raise ValueError("Error: unrecognized type in grid divisor list, should be any of int, str, callable, or iterable") + raise ValueError( + "Error: unrecognized type in grid divisor list, should be any of int, str, callable, or iterable" + ) return divisor_num @@ -587,9 +591,7 @@ def get_smem_args(smem_args, params): def get_temp_filename(suffix=None): """Return a string in the form of temp_X, where X is a large integer.""" - tmp_file = tempfile.mkstemp( - suffix=suffix, prefix="temp_", dir=os.getcwd() - ) + tmp_file = tempfile.mkstemp(suffix=suffix, prefix="temp_", dir=os.getcwd()) os.close(tmp_file[0]) return tmp_file[1] @@ -813,7 +815,7 @@ def prepare_kernel_string(kernel_name, kernel_string, params, grid, threads, blo kernel_prefix += f"constexpr int {k} = {v};\n" elif lang.upper() == "JULIA": # kernel_prefix += f"const {k} = {v}\n" - pass # in Julia, we can't redefine constants like this, so we skip it and give it as arguments on the kernel launch + pass # in Julia, we can't redefine constants like this, so we skip it and give it as arguments on the kernel launch else: kernel_prefix += f"#define {k} {v}\n" @@ -920,7 +922,7 @@ def replace_params_split(match_object): return param else: return key - + # remove functionally duplicate restrictions (preserves order and whitespace) if all(isinstance(r, str) for r in restrictions): # clean the restriction strings to functional equivalence @@ -1029,7 +1031,7 @@ def get_all_lambda_asts(func): if not res: raise ValueError(f"No lambda node found in the source {source}.") except SyntaxError: - """ Ignore syntax errors on the lambda """ + """Ignore syntax errors on the lambda""" return res except OSError: raise ValueError("Could not retrieve source. Is this defined interactively or dynamically?") @@ -1040,15 +1042,18 @@ class ConstraintLambdaTransformer(ast.NodeTransformer): """Replaces any `NAME['string']` subscript with just `'string'`, if `NAME` matches the lambda argument name. """ + def __init__(self, dict_arg_name): self.dict_arg_name = dict_arg_name def visit_Subscript(self, node): # We only replace subscript expressions of the form ['some_string'] - if (isinstance(node.value, ast.Name) - and node.value.id == self.dict_arg_name - and isinstance(node.slice, ast.Constant) - and isinstance(node.slice.value, str)): + if ( + isinstance(node.value, ast.Name) + and node.value.id == self.dict_arg_name + and isinstance(node.slice, ast.Constant) + and isinstance(node.slice.value, str) + ): # Replace `dict_arg_name['some_key']` with the string used as key return ast.Name(node.slice.value) return self.generic_visit(node) @@ -1084,7 +1089,7 @@ def convert_constraint_lambdas(restrictions): try: lambda_asts = get_all_lambda_asts(c) except ValueError: - res.append(c) # it's just a plain function, not a lambda + res.append(c) # it's just a plain function, not a lambda continue for lambda_ast in lambda_asts: @@ -1093,7 +1098,9 @@ def convert_constraint_lambdas(restrictions): result = list(set(res)) if not len(result) == len(restrictions): - raise ValueError("An error occured when parsing restrictions. If you mix lambdas and string-based restrictions, please define the lambda first.") + raise ValueError( + "An error occured when parsing restrictions. If you mix lambdas and string-based restrictions, please define the lambda first." + ) return result @@ -1142,12 +1149,16 @@ def compile_restrictions( noncompiled_restrictions.append((r, [], r)) return noncompiled_restrictions + compiled_restrictions + def check_matching_problem_size(cached_problem_size, problem_size): """Check the if requested problem size matches the problem size in the cache.""" cached_problem_size_arr = np.array(cached_problem_size) problem_size_arr = np.array(problem_size) if cached_problem_size_arr.size != problem_size_arr.size or not (cached_problem_size_arr == problem_size_arr).all(): - raise ValueError(f"Cannot load cache which contains results for different problem_size, cache: {cached_problem_size}, requested: {problem_size}") + raise ValueError( + f"Cannot load cache which contains results for different problem_size, cache: {cached_problem_size}, requested: {problem_size}" + ) + def process_cache(cache, kernel_options, tuning_options, runner): """Cache file for storing tuned configurations. diff --git a/noxfile.py b/noxfile.py index 85f4442a9..bc96d2486 100644 --- a/noxfile.py +++ b/noxfile.py @@ -7,7 +7,11 @@ import platform import re +import subprocess +from json import JSONDecodeError +from json import loads as json_loads from pathlib import Path +from re import search as regex_search import nox from nox_poetry import Session, session @@ -51,12 +55,13 @@ def create_settings(session: Session) -> None: noxenv_file_path.unlink() # write the settings assert venvbackend in venvbackend_values, f"{venvbackend=}, must be one of {','.join(venvbackend_values)}" - settings = f'venvbackend = "{venvbackend}"\n' f'envdir = "{envdir}"\n' + settings = f'venvbackend = "{venvbackend}"\nenvdir = "{envdir}"\n' settings_file_path.write_text(settings) # exit to make sure the user checks the settings are correct if arg_trigger: session.warn( - f"Settings file '{settings_file_path}' created, exiting. Please check settings are correct before running Nox again." + f"Settings file '{settings_file_path}' created, exiting. " + "Please check settings are correct before running Nox again." ) exit(1) @@ -69,9 +74,9 @@ def create_settings(session: Session) -> None: nox_settings = tomli.load(fp) venvbackend = nox_settings["venvbackend"] envdir = nox_settings["envdir"] - assert ( - venvbackend in venvbackend_values - ), f"File '{settings_file_path}' has {venvbackend=}, must be one of {','.join(venvbackend_values)}" + assert venvbackend in venvbackend_values, ( + f"File '{settings_file_path}' has {venvbackend=}, must be one of {','.join(venvbackend_values)}" + ) nox.options.default_venv_backend = venvbackend nox.options.venvbackend = venvbackend if envdir is not None and len(envdir) > 0: @@ -101,7 +106,7 @@ def check_development_environment(session: Session) -> None: return None output: str = session.run("poetry", "install", "--sync", "--dry-run", "--with", "test", silent=True, external=True) match = re.search( - r"Package operations: (\d+) (?:install|installs), (\d+) (?:update|updates), (\d+) (?:removal|removals), \d+ skipped", + r"Package operations: (\d+) (?:install|installs), (\d+) (?:update|updates), (\d+) (?:removal|removals), \d+ skipped", # noqa: E501 output, ) assert match is not None, f"Could not check development environment, reason: {output}" @@ -213,7 +218,7 @@ def tests(session: Session) -> None: # separately install optional dependencies with weird dependencies / build process install_warning = """Installation failed, this likely means that the required hardware or drivers are missing. - Run with `-- skip-gpu` / `-- skip-julia` or one of the more specific options (e.g. `-- skip-cuda`) to avoid this.""" + Run with `-- skip-gpu` / `-- skip-julia` or one of the more specific options (e.g. `-- skip-cuda`) to avoid this.""" # noqa: E501 if install_cuda: # use NVCC to get the CUDA version import re @@ -236,7 +241,7 @@ def tests(session: Session) -> None: session.warn(install_warning) else: session.warn("PyCUDA installed") - # if PyCUDA is already installed, check whether the CUDA version PyCUDA was installed with matches the current CUDA version + # if PyCUDA is already installed, check whether the CUDA version PyCUDA was installed with matches the current CUDA version # noqa: E501 session.install("numpy") # required by pycuda.driver pycuda_version = session.run( "python", @@ -251,7 +256,7 @@ def tests(session: Session) -> None: ) if longest_string[: len(shortest_string)] != shortest_string: session.warn( - f"PyCUDA was compiled with a version of CUDA ({pycuda_version}) that does not match the current version ({cuda_version}). Re-installing." + f"PyCUDA was compiled with a version of CUDA ({pycuda_version}) that does not match the current version ({cuda_version}). Re-installing." # noqa: E501 ) try: session.install( @@ -273,7 +278,7 @@ def tests(session: Session) -> None: # if the poetry virtualenv is not set to the session env, use requirements file export instead of Poetry install if poetry_env != session_env: session.warn( - f"Poetry env ({str(poetry_env)}) is not session env ({str(session_env)}), falling back to install via requirements export" + f"Poetry env ({str(poetry_env)}) is not session env ({str(session_env)}), falling back to install via requirements export" # noqa: E501 ) requirements_file = Path(f"tmp_test_requirements_{session.name}.txt") if requirements_file.exists(): @@ -364,10 +369,6 @@ def tests(session: Session) -> None: ### Helper functions ### -import subprocess -from json import loads as json_loads, JSONDecodeError -from re import search as regex_search - def detect_julia_gpu_backends(): """Detect the Julia backends available, return the assiociated package.""" diff --git a/pyproject.toml b/pyproject.toml index 271fd0663..146c76b44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -126,7 +126,7 @@ pytest-cov = "^5.0.0" mock = "^5.1.0" nox = "^2024.4.15" nox-poetry = "^1.0.3" -ruff = "^0.4.8" +ruff = "^0.15.0" pep440 = "^0.1.2" tomli = "^2.0.1" # held back by Python <= 3.10, can be replaced by built-in [tomllib](https://docs.python.org/3.11/library/tomllib.html) from Python 3.11 onwards scikit-optimize = "0.10.2" @@ -148,13 +148,13 @@ line-length = 120 [tool.ruff] line-length = 120 respect-gitignore = true -exclude = ["doc", "examples"] +exclude = ["doc", "examples", "test"] +[tool.ruff.lint] select = [ + "NPY201", # numpy docstring style "E", # pycodestyle "F", # pyflakes, "D", # pydocstyle, ] -[tool.ruff.pydocstyle] -convention = "google" -[tool.ruff.lint] -select = ["NPY201"] +[tool.ruff.lint.pydocstyle] +convention = "google" \ No newline at end of file diff --git a/test/context.py b/test/context.py index 78fe4df70..f5e1d4e34 100644 --- a/test/context.py +++ b/test/context.py @@ -38,15 +38,14 @@ try: import cupy - cupy.cuda.Device( - 0 - ).attributes # triggers exception if there are no CUDA-capable devices + cupy.cuda.Device(0).attributes # triggers exception if there are no CUDA-capable devices cupy_present = True except Exception: cupy_present = False try: import cuda + print(cuda) cuda_present = True except Exception: @@ -54,6 +53,7 @@ try: from hip import hip + hip.hipDriverGetVersion() hip_present = True except (ImportError, RuntimeError): @@ -62,6 +62,7 @@ try: import botorch import torch + bayes_opt_botorch_present = True except ImportError: bayes_opt_botorch_present = False @@ -69,44 +70,41 @@ try: import gpytorch import torch + bayes_opt_gpytorch_present = True except ImportError: bayes_opt_gpytorch_present = False try: import pyatf + pyatf_present = True except ImportError: pyatf_present = False try: from autotuning_methodology.report_experiments import get_strategy_scores + methodology_present = True except ImportError: methodology_present = False -skip_if_no_pycuda = pytest.mark.skipif( - not pycuda_present, reason="PyCuda not installed or no CUDA device detected" -) +skip_if_no_pycuda = pytest.mark.skipif(not pycuda_present, reason="PyCuda not installed or no CUDA device detected") skip_if_no_pynvml = pytest.mark.skipif(not pynvml_present, reason="NVML not installed") -skip_if_no_cupy = pytest.mark.skipif( - not cupy_present, reason="CuPy not installed or no CUDA device detected" -) -skip_if_no_cuda = pytest.mark.skipif( - not cuda_present, reason="NVIDIA CUDA not installed" -) -skip_if_no_opencl = pytest.mark.skipif( - not opencl_present, reason="PyOpenCL not installed or no OpenCL device detected" -) +skip_if_no_cupy = pytest.mark.skipif(not cupy_present, reason="CuPy not installed or no CUDA device detected") +skip_if_no_cuda = pytest.mark.skipif(not cuda_present, reason="NVIDIA CUDA not installed") +skip_if_no_opencl = pytest.mark.skipif(not opencl_present, reason="PyOpenCL not installed or no OpenCL device detected") skip_if_no_gcc = pytest.mark.skipif(not gcc_present, reason="No gcc on PATH") -skip_if_no_gfortran = pytest.mark.skipif( - not gfortran_present, reason="No gfortran on PATH" -) +skip_if_no_gfortran = pytest.mark.skipif(not gfortran_present, reason="No gfortran on PATH") skip_if_no_julia = pytest.mark.skipif(not shutil.which("julia"), reason="No Julia on PATH") skip_if_no_openmp = pytest.mark.skipif(not openmp_present, reason="No OpenMP found") skip_if_no_openacc = pytest.mark.skipif(not openacc_present, reason="No nvc++ on PATH") -skip_if_no_bayesopt_gpytorch = pytest.mark.skipif(not bayes_opt_gpytorch_present, reason="Torch and GPyTorch not installed") -skip_if_no_bayesopt_botorch = pytest.mark.skipif(not bayes_opt_botorch_present, reason="Torch and BOTorch not installed") +skip_if_no_bayesopt_gpytorch = pytest.mark.skipif( + not bayes_opt_gpytorch_present, reason="Torch and GPyTorch not installed" +) +skip_if_no_bayesopt_botorch = pytest.mark.skipif( + not bayes_opt_botorch_present, reason="Torch and BOTorch not installed" +) skip_if_no_hip = pytest.mark.skipif(not hip_present, reason="No HIP Python found") skip_if_no_pyatf = pytest.mark.skipif(not pyatf_present, reason="PyATF not installed") skip_if_no_methodology = pytest.mark.skipif(not methodology_present, reason="Autotuning Methodology not found") diff --git a/test/test_time_budgets.py b/test/test_time_budgets.py index 8773801c8..edc704f70 100644 --- a/test/test_time_budgets.py +++ b/test/test_time_budgets.py @@ -1,3 +1,4 @@ +# ruff: noqa from itertools import product from time import perf_counter @@ -44,9 +45,10 @@ def env(): @skip_if_no_gcc def test_no_time_budget(env): """Ensure that a RuntimeError is raised if the startup takes longer than the time budget.""" - with raises(RuntimeError, match='startup time of the tuning process'): + with raises(RuntimeError, match="startup time of the tuning process"): tune_kernel(*env, strategy="random_sample", strategy_options={"strategy": "random_sample", "time_limit": 0.0}) + @skip_if_no_gcc def test_some_time_budget(env): """Ensure that the time limit is respected.""" @@ -66,6 +68,7 @@ def test_some_time_budget(env): # Ensure that the time limit was respected by some margin. assert perf_counter() - start_time < time_limit * 2 + @skip_if_no_gcc def test_full_time_budget(env): """Ensure that given ample time budget, the entire space is explored.""" diff --git a/test/test_util_functions.py b/test/test_util_functions.py index e785f415d..1f11cbe55 100644 --- a/test/test_util_functions.py +++ b/test/test_util_functions.py @@ -1,3 +1,4 @@ +# ruff: noqa from __future__ import print_function import json @@ -35,17 +36,13 @@ def test_get_grid_dimensions1(): assert grid[1] == 28 assert grid[2] == 1 - grid = get_grid_dimensions( - problem_size, params, (grid_div[0], None, None), block_size_names - ) + grid = get_grid_dimensions(problem_size, params, (grid_div[0], None, None), block_size_names) assert grid[0] == 25 assert grid[1] == 1024 assert grid[2] == 1 - grid = get_grid_dimensions( - problem_size, params, (None, grid_div[1], None), block_size_names - ) + grid = get_grid_dimensions(problem_size, params, (None, grid_div[1], None), block_size_names) assert grid[0] == 1024 assert grid[1] == 28 @@ -59,17 +56,13 @@ def test_get_grid_dimensions1(): assert grid[1] == 25 assert grid[2] == 1 - grid = get_grid_dimensions( - problem_size, params, ("41", 37, None), block_size_names - ) + grid = get_grid_dimensions(problem_size, params, ("41", 37, None), block_size_names) assert grid[0] == 25 assert grid[1] == 28 assert grid[2] == 1 - grid = get_grid_dimensions( - problem_size, params, (None, [2, "block_y"], None), block_size_names - ) + grid = get_grid_dimensions(problem_size, params, (None, [2, "block_y"], None), block_size_names) assert grid[0] == 1024 assert grid[1] == 14 @@ -83,9 +76,7 @@ def test_get_grid_dimensions2(): grid_div_x = ["block_x*8"] grid_div_y = ["(block_y+2)/8"] - grid = get_grid_dimensions( - problem_size, params, (grid_div_x, grid_div_y, None), block_size_names - ) + grid = get_grid_dimensions(problem_size, params, (grid_div_x, grid_div_y, None), block_size_names) assert grid[0] == 4 assert grid[1] == 256 @@ -99,9 +90,7 @@ def test_get_grid_dimensions3(): grid_div_y = ["(block_y+2)/8"] def assert_grid_dimensions(problem_size): - grid = get_grid_dimensions( - problem_size, params, (grid_div_x, grid_div_y, None), block_size_names - ) + grid = get_grid_dimensions(problem_size, params, (grid_div_x, grid_div_y, None), block_size_names) assert grid[0] == 1 assert grid[1] == 256 assert grid[2] == 1 @@ -187,15 +176,13 @@ def test_prepare_kernel_string(): defines = dict(foo=1, bar="custom", baz=lambda config: config["is"] * 5) _, output = prepare_kernel_string("this", kernel, params, grid, threads, block_size_names, "", defines) - expected = "#define foo 1\n" "#define bar custom\n" "#define baz 40\n" "#line 1\n" "this is a weird kernel" + expected = "#define foo 1\n#define bar custom\n#define baz 40\n#line 1\nthis is a weird kernel" assert output == expected # Throw exception on invalid name (for instance, a space in the name) invalid_defines = {"invalid name": "1"} with pytest.raises(ValueError): - prepare_kernel_string( - "this", kernel, params, grid, threads, block_size_names, "", invalid_defines - ) + prepare_kernel_string("this", kernel, params, grid, threads, block_size_names, "", invalid_defines) def test_prepare_kernel_string_partial_loop_unrolling(): @@ -210,9 +197,7 @@ def test_prepare_kernel_string_partial_loop_unrolling(): params = dict() params["loop_unroll_factor_monkey"] = 8 - _, output = prepare_kernel_string( - "this", kernel, params, grid, threads, block_size_names, "CUDA", None - ) + _, output = prepare_kernel_string("this", kernel, params, grid, threads, block_size_names, "CUDA", None) assert "constexpr int loop_unroll_factor_monkey = 8;" in output params["loop_unroll_factor_monkey"] = 0 @@ -220,6 +205,7 @@ def test_prepare_kernel_string_partial_loop_unrolling(): assert "constexpr int loop_unroll_factor_monkey" not in output assert "#pragma unroll loop_unroll_factor_monkey" not in output + def test_replace_param_occurrences(): kernel = "this is a weird kernel" params = dict() @@ -227,9 +213,7 @@ def test_replace_param_occurrences(): params["weird"] = 14 new_kernel = replace_param_occurrences(kernel, params) - assert ( - new_kernel == "this 8 a 14 kernel" - ) # Note: The "is" in "this" should not be replaced + assert new_kernel == "this 8 a 14 kernel" # Note: The "is" in "this" should not be replaced new_kernel = replace_param_occurrences(kernel, dict()) assert kernel == new_kernel @@ -357,9 +341,7 @@ def test_check_argument_list3(): } """ args = [np.uint16(42), np.float16([3, 4, 6]), np.int32([300])] - assert_user_warning( - check_argument_list, [kernel_name, kernel_string, args], "at position 2" - ) + assert_user_warning(check_argument_list, [kernel_name, kernel_string, args], "at position 2") def test_check_argument_list4(): @@ -369,9 +351,7 @@ def test_check_argument_list4(): } """ args = [np.uint16(42), np.float16([3, 4, 6]), np.int64([300]), np.ubyte(32)] - assert_user_warning( - check_argument_list, [kernel_name, kernel_string, args], "do not match in size" - ) + assert_user_warning(check_argument_list, [kernel_name, kernel_string, args], "do not match in size") def test_check_argument_list5(): @@ -489,18 +469,12 @@ def test_warnings(function, args, number, warning_type): # check warning does not triger when nondefault block size names are used correctly block_size_names = ["block_size_a", "block_size_b"] - tune_params = dict( - zip(["block_size_a", "block_size_b", "many_other_things"], [1, 2, 3]) - ) - test_warnings( - check_block_size_params_names_list, [block_size_names, tune_params], 0, None - ) + tune_params = dict(zip(["block_size_a", "block_size_b", "many_other_things"], [1, 2, 3])) + test_warnings(check_block_size_params_names_list, [block_size_names, tune_params], 0, None) # check that a warning is issued when none of the default names are used and no alternative names are specified block_size_names = None - tune_params = dict( - zip(["block_size_a", "block_size_b", "many_other_things"], [1, 2, 3]) - ) + tune_params = dict(zip(["block_size_a", "block_size_b", "many_other_things"], [1, 2, 3])) test_warnings( check_block_size_params_names_list, [block_size_names, tune_params], @@ -510,12 +484,8 @@ def test_warnings(function, args, number, warning_type): # check that no error is raised when any of the default block size names is being used block_size_names = None - tune_params = dict( - zip(["block_size_x", "several_other_things"], [[1, 2, 3, 4], [2, 4]]) - ) - test_warnings( - check_block_size_params_names_list, [block_size_names, tune_params], 0, None - ) + tune_params = dict(zip(["block_size_x", "several_other_things"], [[1, 2, 3, 4], [2, 4]])) + test_warnings(check_block_size_params_names_list, [block_size_names, tune_params], 0, None) def test_get_kernel_string_func(): @@ -697,10 +667,7 @@ def test_process_metrics(): # assert params["b"] == 15 # test if a metric overrides any existing metrics - params = { - "x": 15, - "b": 12 - } + params = {"x": 15, "b": 12} metrics = dict() metrics["b"] = "x" params = process_metrics(params, metrics) @@ -710,7 +677,11 @@ def test_process_metrics(): def test_parse_restrictions(): tune_params = {"block_size_x": [50, 100], "use_padding": [0, 1]} restrict = ["block_size_x != 320"] - restrictions = ["block_size_x != 320", "use_padding == 0 or block_size_x % 32 != 0", "50 <= block_size_x * use_padding < 100"] + restrictions = [ + "block_size_x != 320", + "use_padding == 0 or block_size_x % 32 != 0", + "50 <= block_size_x * use_padding < 100", + ] # test the monolithic parsed function parsed = parse_restrictions(restrict, tune_params, monolithic=True)[0] @@ -737,9 +708,9 @@ def test_check_matching_problem_size(): with pytest.raises(ValueError): check_matching_problem_size(42, 1000) with pytest.raises(ValueError): - check_matching_problem_size([42,1], 42) + check_matching_problem_size([42, 1], 42) with pytest.raises(ValueError): - check_matching_problem_size([42,0], 42) + check_matching_problem_size([42, 0], 42) with pytest.raises(ValueError): check_matching_problem_size(None, 42) # these should not error @@ -747,28 +718,39 @@ def test_check_matching_problem_size(): check_matching_problem_size([1000], 1000) check_matching_problem_size(1000, 1000) check_matching_problem_size(1000, [1000]) - check_matching_problem_size([1000,], 1000) + check_matching_problem_size( + [ + 1000, + ], + 1000, + ) def test_convert_constraint_lambdas(): - restrictions = [lambda p: 32 <= p["block_size_x"]*p["block_size_y"] <= 1024, - "32 <= block_size_x*block_size_y <= 512", - lambda p: p["block_size_z"] < 8] + restrictions = [ + lambda p: 32 <= p["block_size_x"] * p["block_size_y"] <= 1024, + "32 <= block_size_x*block_size_y <= 512", + lambda p: p["block_size_z"] < 8, + ] result = convert_constraint_lambdas(restrictions) print(result) - expected = ['32 <= block_size_x * block_size_y <= 1024', 'block_size_z < 8', '32 <= block_size_x*block_size_y <= 512'] + expected = [ + "32 <= block_size_x * block_size_y <= 1024", + "block_size_z < 8", + "32 <= block_size_x*block_size_y <= 512", + ] assert sorted(result) == sorted(expected) restrictions2 = [] - restrictions2 += [lambda p: 32 <= p["block_size_x"]*p["block_size_y"] <= 1024] + restrictions2 += [lambda p: 32 <= p["block_size_x"] * p["block_size_y"] <= 1024] restrictions2 += [lambda p: p["block_size_z"] < 8] result2 = convert_constraint_lambdas(restrictions2) print(result2) - expected2 = ['block_size_z < 8', '32 <= block_size_x * block_size_y <= 1024'] + expected2 = ["block_size_z < 8", "32 <= block_size_x * block_size_y <= 1024"] assert sorted(result2) == sorted(expected2) @@ -780,16 +762,20 @@ def test_convert_constraint_lambdas_illformatted(): That is why this test expects an exception """ - restrictions = ["32 <= block_size_x*block_size_y <= 512", - lambda p: 32 <= p["block_size_x"]*p["block_size_y"] <= 1024, - lambda p: p["block_size_z"] < 8] + restrictions = [ + "32 <= block_size_x*block_size_y <= 512", + lambda p: 32 <= p["block_size_x"] * p["block_size_y"] <= 1024, + lambda p: p["block_size_z"] < 8, + ] - expected = ['32 <= block_size_x * block_size_y <= 1024', 'block_size_z < 8', '32 <= block_size_x*block_size_y <= 512'] + expected = [ + "32 <= block_size_x * block_size_y <= 1024", + "block_size_z < 8", + "32 <= block_size_x*block_size_y <= 512", + ] try: result = convert_constraint_lambdas(restrictions) print(result) except ValueError: pass - - diff --git a/test/utils/nvcuda.py b/test/utils/nvcuda.py index 4d05440cf..73ec4c949 100644 --- a/test/utils/nvcuda.py +++ b/test/utils/nvcuda.py @@ -1,3 +1,4 @@ +# ruff: noqa: D100, D103 from kernel_tuner.utils.nvcuda import to_valid_nvrtc_gpu_arch_cc diff --git a/test/utils/test_directives.py b/test/utils/test_directives.py index 759a20a54..b96867bd6 100644 --- a/test/utils/test_directives.py +++ b/test/utils/test_directives.py @@ -1,3 +1,4 @@ +# ruff: noqa from kernel_tuner.utils.directives import * @@ -283,16 +284,10 @@ def test_extract_directive_signature(): code = "#pragma tuner start vector_add a(float*:VECTOR_SIZE) b(float*:VECTOR_SIZE) c(float*:VECTOR_SIZE) size(int:VECTOR_SIZE) \n#pragma acc" signatures = extract_directive_signature(code, acc_cxx) assert len(signatures) == 1 - assert ( - "float vector_add(float * a, float * b, float * c, int size)" - in signatures["vector_add"] - ) + assert "float vector_add(float * a, float * b, float * c, int size)" in signatures["vector_add"] signatures = extract_directive_signature(code, acc_cxx, "vector_add") assert len(signatures) == 1 - assert ( - "float vector_add(float * a, float * b, float * c, int size)" - in signatures["vector_add"] - ) + assert "float vector_add(float * a, float * b, float * c, int size)" in signatures["vector_add"] signatures = extract_directive_signature(code, acc_cxx, "vector_add_ext") assert len(signatures) == 0 code = "!$tuner start vector_add A(float*:VECTOR_SIZE) B(float*:VECTOR_SIZE) C(float*:VECTOR_SIZE) n(int:VECTOR_SIZE)\n!$acc" From 6eb53977fbbb3ea79aefd1f6f1c5b086651341ed Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Wed, 4 Feb 2026 12:02:52 +0100 Subject: [PATCH 033/146] Fixed some linting issues --- kernel_tuner/accuracy.py | 60 +++++----- kernel_tuner/utils/directives.py | 186 +++++++++++++++---------------- 2 files changed, 125 insertions(+), 121 deletions(-) diff --git a/kernel_tuner/accuracy.py b/kernel_tuner/accuracy.py index b647947c1..815593268 100644 --- a/kernel_tuner/accuracy.py +++ b/kernel_tuner/accuracy.py @@ -1,19 +1,25 @@ +"""Module for accuracy measurement and tunable parameters.""" + +import logging +import re from collections import UserDict from typing import Dict + import numpy as np -import logging -import re from kernel_tuner.observers import OutputObserver class Tunable(UserDict): + """``Tunable`` can be used as a parameter value dependent input argument when tuning kernels.""" + def __init__(self, param_key: str, arrays: Dict): - """The ``Tunable`` object can be used as an input argument when tuning - kernels. It is a container that holds several arrays internally and + """The ``Tunable`` object can be used as an input argument when tuning kernels. + + It is a container that holds several arrays internally and selects one array during benchmarking based on the value of a tunable parameter. - Example + Example: ------- Consider this example:: @@ -37,6 +43,7 @@ def __init__(self, param_key: str, arrays: Dict): self.param_key = param_key def select_for_configuration(self, params): + """Select the array for the given configuration.""" if callable(self.param_key): option = self.param_key(params) elif self.param_key in params: @@ -46,13 +53,11 @@ def select_for_configuration(self, params): if option not in self.data: list = ", ".join(map(str, self.data.keys())) - raise KeyError( - f"'{option}' is not a valid parameter value, should be one of: {list}" - ) + raise KeyError(f"'{option}' is not a valid parameter value, should be one of: {list}") return self.data[option] - def __call__(self, params): + def __call__(self, params): # noqa: D102 return self.select_for_configuration(params) @@ -70,6 +75,7 @@ def _find_bfloat16_if_available(): if dtype is None: try: from ml_dtypes import bfloat16 + dtype = bfloat16 except ImportError: pass @@ -78,6 +84,7 @@ def _find_bfloat16_if_available(): if dtype is None: try: from jax.numpy import bfloat16 + dtype = bfloat16 except ImportError: pass @@ -86,6 +93,7 @@ def _find_bfloat16_if_available(): if dtype is None: try: from tensorflow import bfloat16 + dtype = bfloat16.as_numpy_dtype except ImportError: pass @@ -100,9 +108,9 @@ def _find_bfloat16_if_available(): def _to_float_dtype(x: str) -> np.dtype: - """Convert a string to a numpy data type (``dtype``). This function recognizes - common names (such as ``f16`` or ``kfloat``), and uses ``np.dtype(x)`` as a - fallback. + """Convert a string to a numpy data type (``dtype``). + + This function recognizes common names (such as ``f16`` or ``kfloat``), and uses ``np.dtype(x)`` as a fallback. """ if isinstance(x, str): x = x.lower() @@ -123,16 +131,17 @@ def _to_float_dtype(x: str) -> np.dtype: class TunablePrecision(Tunable): - def __init__( - self, param_key: str, array: np.ndarray, dtypes: Dict[str, np.dtype] = None - ): - """The ``Tunable`` object can be used as an input argument when tuning - kernels. It is a container that internally holds several arrays + """``TunablePrecision`` can be used as a precision-level dependent input argument when tuning kernels.""" + + def __init__(self, param_key: str, array: np.ndarray, dtypes: Dict[str, np.dtype] = None): + """The ``Tunable`` object can be used as an input argument when tuning kernels. + + It is a container that internally holds several arrays containing the same data, but stored in using different levels of precision. During benchamrking, one array is selected based on the value of the tunable parameter called ``param_key``. - Example + Example: ------- Consider this example:: @@ -156,7 +165,6 @@ def __init__( if bfloat16 is not None: dtypes["bfloat16"] = bfloat16 - # If dtype is a list, convert it to a dictionary if isinstance(dtypes, (list, tuple)): dtypes = dict((name, _to_float_dtype(name)) for name in dtypes) @@ -197,7 +205,6 @@ def error_metric_from_name(user_key, EPS=1e-8): The value of `EPS` is used for relative errors to prevent division by zero. `` """ - # Prepocess the provided name: # - convert to lowercase # - remove the word "error" @@ -278,17 +285,13 @@ def metric(a, b): raise ValueError(f"invalid error metric provided: {user_key}") # cast both arguments to f64 before passing them to the metric - return lambda a, b: metric( - a.astype(np.float64, copy=False), b.astype(np.float64, copy=False) - ) + return lambda a, b: metric(a.astype(np.float64, copy=False), b.astype(np.float64, copy=False)) class AccuracyObserver(OutputObserver): - """``AccuracyObserver`` measures the error on the output produced by a kernel - by comparing the output against a reference output. + """``AccuracyObserver`` measures the error on the output produced by a kernel by comparing to a reference output. - By default, it uses the root mean-squared error (RMSE) and uses the - metric name ``"error"``. + By default, it uses the root mean-squared error (RMSE) and uses the metric name ``"error"``. """ def __init__(self, metric=None, key="error", *, atol=1e-8): @@ -303,7 +306,6 @@ def __init__(self, metric=None, key="error", *, atol=1e-8): :param atol: The tolerance used in relative metrics to prevent division by zero. It is ignored by absolute error metrics. """ - # Default metric is RMSE if not metric: metric = "rmse" @@ -317,6 +319,7 @@ def __init__(self, metric=None, key="error", *, atol=1e-8): self.result = None def process_output(self, answers, outputs): + """Process the output produced by the kernel and compare it to the reference answers.""" errors = [] for answer, output in zip(answers, outputs): @@ -326,4 +329,5 @@ def process_output(self, answers, outputs): self.result = max(errors) def get_results(self): + """Get the results produced by this observer.""" return dict([(self.key, self.result)]) diff --git a/kernel_tuner/utils/directives.py b/kernel_tuner/utils/directives.py index 36cd219b0..f3930d6b7 100644 --- a/kernel_tuner/utils/directives.py +++ b/kernel_tuner/utils/directives.py @@ -1,5 +1,8 @@ -from typing import Any, Tuple +"""Utility functions and classes for handling directives.""" + from abc import ABC, abstractmethod +from typing import Any, Tuple + import numpy as np # Function templates @@ -34,80 +37,82 @@ class Directive(ABC): - """Base class for all directives""" + """Base class for all directives.""" @abstractmethod - def get(self) -> str: + def get(self) -> str: # noqa: D102 pass class Language(ABC): - """Base class for all languages""" + """Base class for all languages.""" @abstractmethod - def get(self) -> str: + def get(self) -> str: # noqa: D102 pass class OpenACC(Directive): - """Class to represent OpenACC""" + """Class to represent OpenACC.""" - def get(self) -> str: + def get(self) -> str: # noqa: D102 return "openacc" class OpenMP(Directive): - """Class to represent OpenMP""" + """Class to represent OpenMP.""" - def get(self) -> str: + def get(self) -> str: # noqa: D102 return "openmp" class Cxx(Language): - """Class to represent C++ code""" + """Class to represent C++ code.""" - def get(self) -> str: + def get(self) -> str: # noqa: D102 return "cxx" - def end_string(self) -> str: + def end_string(self) -> str: # noqa: D102 return "#pragma tuner stop" class Fortran(Language): - """Class to represent Fortran code""" + """Class to represent Fortran code.""" - def get(self) -> str: + def get(self) -> str: # noqa: D102 return "fortran" - def end_string(self) -> str: + def end_string(self) -> str: # noqa: D102 return "!$tuner stop" class Code(object): - """Class to represent the directive and host code of the application""" + """Class to represent the directive and host code of the application.""" - def __init__(self, directive: Directive, lang: Language): + def __init__(self, directive: Directive, lang: Language): # noqa: D107 self.directive = directive self.language = lang class ArraySize(object): - """Size of an array""" + """Size of an array.""" - def __init__(self): + def __init__(self): # noqa: D102, D107 self.size = list() - def __iter__(self): + def __iter__(self): # noqa: D105 for i in self.size: yield i - def __len__(self): + def __len__(self): # noqa: D105 return len(self.size) def clear(self): + """Clear the size dimensions.""" self.size.clear() def get(self) -> int: + """Get the total size represented by this ArraySize.""" length = len(self.size) if length == 0: return 0 @@ -120,13 +125,13 @@ def get(self) -> int: return product def add(self, dim: int) -> None: - # Only allow adding valid dimensions + """Only allow adding valid dimensions.""" if dim >= 1: self.size.append(dim) def fortran_md_size(size: ArraySize) -> list: - """Format a multidimensional size into the correct Fortran string""" + """Format a multidimensional size into the correct Fortran string.""" md_size = list() for dim in size: md_size.append(f":{dim}") @@ -134,32 +139,32 @@ def fortran_md_size(size: ArraySize) -> list: def is_openacc(directive: Directive) -> bool: - """Check if a directive is OpenACC""" + """Check if a directive is OpenACC.""" return isinstance(directive, OpenACC) def is_openmp(directive: Directive) -> bool: - """Check if a directive is OpenMP""" + """Check if a directive is OpenMP.""" return isinstance(directive, OpenMP) def is_cxx(lang: Language) -> bool: - """Check if language is C++""" + """Check if language is C++.""" return isinstance(lang, Cxx) def is_fortran(lang: Language) -> bool: - """Check if language is Fortran""" + """Check if language is Fortran.""" return isinstance(lang, Fortran) def line_contains(line: str, target: str) -> bool: - """Generic helper to check if a line contains the target""" + """Generic helper to check if a line contains the target.""" return target in line def directive_contains_clause(line: str, clauses: list) -> bool: - """Check if a directive contains one clause from a list""" + """Check if a directive contains one clause from a list.""" for clause in clauses: if clause in line: return True @@ -167,7 +172,7 @@ def directive_contains_clause(line: str, clauses: list) -> bool: def line_contains_openacc_directive(line: str, lang: Language) -> bool: - """Check if line contains an OpenACC directive or not""" + """Check if line contains an OpenACC directive or not.""" if is_cxx(lang): return line_contains_openacc_directive_cxx(line) elif is_fortran(lang): @@ -176,17 +181,17 @@ def line_contains_openacc_directive(line: str, lang: Language) -> bool: def line_contains_openacc_directive_cxx(line: str) -> bool: - """Check if a line of code contains a C++ OpenACC directive or not""" + """Check if a line of code contains a C++ OpenACC directive or not.""" return line_contains(line, "#pragma acc") def line_contains_openacc_directive_fortran(line: str) -> bool: - """Check if a line of code contains a Fortran OpenACC directive or not""" + """Check if a line of code contains a Fortran OpenACC directive or not.""" return line_contains(line, "!$acc") def line_contains_openmp_directive(line: str, lang: Language) -> bool: - """Check if line contains an OpenMP directive or not""" + """Check if line contains an OpenMP directive or not.""" if is_cxx(lang): return line_contains_openmp_directive_cxx(line) elif is_fortran(lang): @@ -195,17 +200,17 @@ def line_contains_openmp_directive(line: str, lang: Language) -> bool: def line_contains_openmp_directive_cxx(line: str) -> bool: - """Check if a line of code contains a C++ OpenMP directive or not""" + """Check if a line of code contains a C++ OpenMP directive or not.""" return line_contains(line, "#pragma omp") def line_contains_openmp_directive_fortran(line: str) -> bool: - """Check if a line of code contains a Fortran OpenMP directive or not""" + """Check if a line of code contains a Fortran OpenMP directive or not.""" return line_contains(line, "!$omp") def line_contains_openacc_parallel_directive(line: str, lang: Language) -> bool: - """Check if line contains an OpenACC parallel directive or not""" + """Check if line contains an OpenACC parallel directive or not.""" if is_cxx(lang): return line_contains_openacc_parallel_directive_cxx(line) elif is_fortran(lang): @@ -214,17 +219,17 @@ def line_contains_openacc_parallel_directive(line: str, lang: Language) -> bool: def line_contains_openacc_parallel_directive_cxx(line: str) -> bool: - """Check if a line of code contains a C++ OpenACC parallel directive or not""" + """Check if a line of code contains a C++ OpenACC parallel directive or not.""" return line_contains(line, "#pragma acc parallel") def line_contains_openacc_parallel_directive_fortran(line: str) -> bool: - """Check if a line of code contains a Fortran OpenACC parallel directive or not""" + """Check if a line of code contains a Fortran OpenACC parallel directive or not.""" return line_contains(line, "!$acc parallel") def line_contains_openmp_target_directive(line: str, lang: Language) -> bool: - """Check if line contains an OpenMP target directive or not""" + """Check if line contains an OpenMP target directive or not.""" if is_cxx(lang): return line_contains_openmp_target_directive_cxx(line) elif is_fortran(lang): @@ -233,29 +238,29 @@ def line_contains_openmp_target_directive(line: str, lang: Language) -> bool: def line_contains_openmp_target_directive_cxx(line: str) -> bool: - """Check if a line of code contains a C++ OpenMP target directive or not""" + """Check if a line of code contains a C++ OpenMP target directive or not.""" return line_contains(line, "#pragma omp target") def line_contains_openmp_target_directive_fortran(line: str) -> bool: - """Check if a line of code contains a Fortran OpenMP target directive or not""" + """Check if a line of code contains a Fortran OpenMP target directive or not.""" return line_contains(line, "!$omp target") def openacc_directive_contains_data_clause(line: str) -> bool: - """Check if an OpenACC directive contains one data clause""" + """Check if an OpenACC directive contains one data clause.""" data_clauses = ["copy", "copyin", "copyout", "create", "no_create", "present", "device_ptr", "attach"] return directive_contains_clause(line, data_clauses) def openmp_directive_contains_data_clause(line: str) -> bool: - """Check if an OpenMP directive contains one data clause""" + """Check if an OpenMP directive contains one data clause.""" data_clauses = ["map"] return directive_contains_clause(line, data_clauses) def create_data_directive_openacc(name: str, size: ArraySize, lang: Language) -> str: - """Create a data directive for a given language""" + """Create a data directive for a given language.""" if is_cxx(lang): return create_data_directive_openacc_cxx(name, size) elif is_fortran(lang): @@ -264,12 +269,12 @@ def create_data_directive_openacc(name: str, size: ArraySize, lang: Language) -> def create_data_directive_openacc_cxx(name: str, size: ArraySize) -> str: - """Create C++ OpenACC code to allocate and copy data""" + """Create C++ OpenACC code to allocate and copy data.""" return f"#pragma acc enter data create({name}[:{size.get()}])\n#pragma acc update device({name}[:{size.get()}])\n" def create_data_directive_openacc_fortran(name: str, size: ArraySize) -> str: - """Create Fortran OpenACC code to allocate and copy data""" + """Create Fortran OpenACC code to allocate and copy data.""" if len(size) == 1: return f"!$acc enter data create({name}(:{size.get()}))\n!$acc update device({name}(:{size.get()}))\n" else: @@ -280,7 +285,7 @@ def create_data_directive_openacc_fortran(name: str, size: ArraySize) -> str: def create_data_directive_openmp(name: str, size: ArraySize, lang: Language) -> str: - """Create a data directive for a given language""" + """Create a data directive for a given language.""" if is_cxx(lang): return create_data_directive_openmp_cxx(name, size) elif is_fortran(lang): @@ -289,12 +294,12 @@ def create_data_directive_openmp(name: str, size: ArraySize, lang: Language) -> def create_data_directive_openmp_cxx(name: str, size: ArraySize) -> str: - """Create C++ OpenMP code to allocate and copy data""" + """Create C++ OpenMP code to allocate and copy data.""" return f"#pragma omp target enter data map(to: {name}[:{size.get()}])\n" def create_data_directive_openmp_fortran(name: str, size: ArraySize) -> str: - """Create Fortran OpenMP code to allocate and copy data""" + """Create Fortran OpenMP code to allocate and copy data.""" if len(size) == 1: return f"!$omp target enter data map(to: {name}(:{size.get()}))\n" else: @@ -303,7 +308,7 @@ def create_data_directive_openmp_fortran(name: str, size: ArraySize) -> str: def exit_data_directive_openacc(name: str, size: ArraySize, lang: Language) -> str: - """Create code to copy data back for a given language""" + """Create code to copy data back for a given language.""" if is_cxx(lang): return exit_data_directive_openacc_cxx(name, size) elif is_fortran(lang): @@ -312,12 +317,12 @@ def exit_data_directive_openacc(name: str, size: ArraySize, lang: Language) -> s def exit_data_directive_openacc_cxx(name: str, size: ArraySize) -> str: - """Create C++ OpenACC code to copy back data""" + """Create C++ OpenACC code to copy back data.""" return f"#pragma acc exit data copyout({name}[:{size.get()}])\n" def exit_data_directive_openacc_fortran(name: str, size: ArraySize) -> str: - """Create Fortran OpenACC code to copy back data""" + """Create Fortran OpenACC code to copy back data.""" if len(size) == 1: return f"!$acc exit data copyout({name}(:{size.get()}))\n" else: @@ -326,7 +331,7 @@ def exit_data_directive_openacc_fortran(name: str, size: ArraySize) -> str: def exit_data_directive_openmp(name: str, size: ArraySize, lang: Language) -> str: - """Create code to copy data back for a given language""" + """Create code to copy data back for a given language.""" if is_cxx(lang): return exit_data_directive_openmp_cxx(name, size) elif is_fortran(lang): @@ -335,12 +340,12 @@ def exit_data_directive_openmp(name: str, size: ArraySize, lang: Language) -> st def exit_data_directive_openmp_cxx(name: str, size: ArraySize) -> str: - """Create C++ OpenMP code to copy back data""" + """Create C++ OpenMP code to copy back data.""" return f"#pragma omp target exit data map(from: {name}[:{size.get()}])\n" def exit_data_directive_openmp_fortran(name: str, size: ArraySize) -> str: - """Create Fortran OpenMP code to copy back data""" + """Create Fortran OpenMP code to copy back data.""" if len(size) == 1: return f"!$omp target exit data map(from: {name}(:{size.get()}))\n" else: @@ -349,12 +354,12 @@ def exit_data_directive_openmp_fortran(name: str, size: ArraySize) -> str: def correct_kernel(kernel_name: str, line: str) -> bool: - """Checks if the line contains the correct kernel name""" + """Checks if the line contains the correct kernel name.""" return f" {kernel_name} " in line or (kernel_name in line and len(line.partition(kernel_name)[2]) == 0) def find_size_in_preprocessor(dimension: str, preprocessor: list) -> int: - """Find the dimension of a directive defined value in the preprocessor""" + """Find the dimension of a directive defined value in the preprocessor.""" ret_size = 0 for line in preprocessor: if f"#define {dimension}" in line: @@ -367,7 +372,7 @@ def find_size_in_preprocessor(dimension: str, preprocessor: list) -> int: def extract_code(start: str, stop: str, code: str, langs: Code, kernel_name: str = None) -> dict: - """Extract an arbitrary section of code""" + """Extract an arbitrary section of code.""" found_section = False sections = dict() tmp_string = list() @@ -400,7 +405,7 @@ def extract_code(start: str, stop: str, code: str, langs: Code, kernel_name: str def parse_size(size: Any, preprocessor: list = None, dimensions: dict = None) -> ArraySize: - """Converts an arbitrary object into an integer representing memory size""" + """Converts an arbitrary object into an integer representing memory size.""" ret_size = ArraySize() if type(size) is not int: try: @@ -441,7 +446,7 @@ def parse_size(size: Any, preprocessor: list = None, dimensions: dict = None) -> def wrap_timing(code: str, lang: Language) -> str: - """Helper to wrap timing code around the provided code""" + """Helper to wrap timing code around the provided code.""" if is_cxx(lang): return end_timing_cxx(start_timing_cxx(code)) elif is_fortran(lang): @@ -450,8 +455,7 @@ def wrap_timing(code: str, lang: Language) -> str: def start_timing_cxx(code: str) -> str: - """Wrap C++ timing code around the provided code""" - + """Wrap C++ timing code around the provided code.""" start = "auto kt_timing_start = std::chrono::steady_clock::now();" end = "auto kt_timing_end = std::chrono::steady_clock::now();" timing = "std::chrono::duration elapsed_time = kt_timing_end - kt_timing_start;" @@ -460,8 +464,7 @@ def start_timing_cxx(code: str) -> str: def wrap_timing_fortran(code: str) -> str: - """Wrap Fortran timing code around the provided code""" - + """Wrap Fortran timing code around the provided code.""" start = "call system_clock(kt_timing_start, kt_rate)" end = "call system_clock(kt_timing_end)" timing = "timing = (real(kt_timing_end - kt_timing_start) / real(kt_rate)) * 1e3" @@ -470,12 +473,12 @@ def wrap_timing_fortran(code: str) -> str: def end_timing_cxx(code: str) -> str: - """In C++ we need to return the measured time""" + """In C++ we need to return the measured time.""" return "\n".join([code, "return elapsed_time.count();\n"]) def wrap_data(code: str, langs: Code, data: dict, preprocessor: list = None, user_dimensions: dict = None) -> str: - """Insert data directives before and after the timed code""" + """Insert data directives before and after the timed code.""" intro = str() outro = str() for name in data.keys(): @@ -492,7 +495,7 @@ def wrap_data(code: str, langs: Code, data: dict, preprocessor: list = None, use def wrap_data_openacc(name: str, size: int, langs: Code) -> Tuple[str, str]: - """Create language specific data directives""" + """Create language specific data directives.""" if is_cxx(langs.language): intro = create_data_directive_openacc_cxx(name, size) outro = exit_data_directive_openacc_cxx(name, size) @@ -503,7 +506,7 @@ def wrap_data_openacc(name: str, size: int, langs: Code) -> Tuple[str, str]: def wrap_data_openmp(name: str, size: int, langs: Code) -> Tuple[str, str]: - """Create language specific data directives""" + """Create language specific data directives.""" if is_cxx(langs.language): intro = create_data_directive_openmp_cxx(name, size) outro = exit_data_directive_openmp_cxx(name, size) @@ -514,7 +517,7 @@ def wrap_data_openmp(name: str, size: int, langs: Code) -> Tuple[str, str]: def extract_directive_code(code: str, langs: Code, kernel_name: str = None) -> dict: - """Extract explicitly marked directive sections from code""" + """Extract explicitly marked directive sections from code.""" if is_cxx(langs.language): start_string = "#pragma tuner start" elif is_fortran(langs.language): @@ -524,7 +527,7 @@ def extract_directive_code(code: str, langs: Code, kernel_name: str = None) -> d def extract_initialization_code(code: str, langs: Code) -> str: - """Extract the initialization section from code""" + """Extract the initialization section from code.""" if is_cxx(langs.language): start_string = "#pragma tuner initialize" elif is_fortran(langs.language): @@ -538,7 +541,7 @@ def extract_initialization_code(code: str, langs: Code) -> str: def extract_deinitialization_code(code: str, langs: Code) -> str: - """Extract the deinitialization section from code""" + """Extract the deinitialization section from code.""" if is_cxx(langs.language): start_string = "#pragma tuner deinitialize" elif is_fortran(langs.language): @@ -552,7 +555,7 @@ def extract_deinitialization_code(code: str, langs: Code) -> str: def format_argument_fortran(p_type: str, p_size: int, p_name: str) -> str: - """Format the argument for Fortran code""" + """Format the argument for Fortran code.""" argument = "" if "float*" in p_type: argument = f"real (c_float), dimension({p_size}) :: {p_name}" @@ -570,8 +573,7 @@ def format_argument_fortran(p_type: str, p_size: int, p_name: str) -> str: def extract_directive_signature(code: str, langs: Code, kernel_name: str = None) -> dict: - """Extract the user defined signature for directive sections""" - + """Extract the user defined signature for directive sections.""" if is_cxx(langs.language): start_string = "#pragma tuner start" elif is_fortran(langs.language): @@ -605,9 +607,9 @@ def extract_directive_signature(code: str, langs: Code, kernel_name: str = None) if is_cxx(langs.language): signatures[name] = f"float {name}({', '.join(params)})" elif is_fortran(langs.language): - signatures[ - name - ] = f"function {name}({', '.join(params)}) result(timing)\nuse iso_c_binding\nimplicit none\n" + signatures[name] = ( + f"function {name}({', '.join(params)}) result(timing)\nuse iso_c_binding\nimplicit none\n" + ) params = list() for param in tmp_string: if len(param) == 0: @@ -619,16 +621,15 @@ def extract_directive_signature(code: str, langs: Code, kernel_name: str = None) p_type = p_type.split(":")[0] params.append(format_argument_fortran(p_type, p_size, p_name)) signatures[name] += "\n".join(params) + "\n" - signatures[ - name - ] += "integer(c_int):: kt_timing_start\nreal(c_float):: kt_rate\ninteger(c_int):: kt_timing_end\nreal(c_float):: timing\n" + signatures[name] += ( + "integer(c_int):: kt_timing_start\nreal(c_float):: kt_rate\ninteger(c_int):: kt_timing_end\nreal(c_float):: timing\n" # noqa: E501 + ) return signatures def extract_directive_data(code: str, langs: Code, kernel_name: str = None) -> dict: - """Extract the data used in the directive section""" - + """Extract the data used in the directive section.""" if is_cxx(langs.language): start_string = "#pragma tuner start" elif is_fortran(langs.language): @@ -662,7 +663,7 @@ def extract_directive_data(code: str, langs: Code, kernel_name: str = None) -> d def extract_preprocessor(code: str) -> list: - """Extract include and define statements from code""" + """Extract include and define statements from code.""" preprocessor = list() for line in code.replace("\\\n", "").split("\n"): @@ -682,8 +683,7 @@ def generate_directive_function( deinitialization: str = "", user_dimensions: dict = None, ) -> str: - """Generate tunable function for one directive""" - + """Generate tunable function for one directive.""" if is_cxx(langs.language): code = cpp_template body = start_timing_cxx(body) @@ -717,7 +717,7 @@ def generate_directive_function( def allocate_array(p_type: str, size: int) -> np.ndarray: - """Allocate a Numpy array""" + """Allocate a Numpy array.""" max_int = 1024 array = None if p_type == "float*": @@ -733,7 +733,7 @@ def allocate_array(p_type: str, size: int) -> np.ndarray: def allocate_scalar(p_type: str, size: int) -> np.number: - """Allocate a Numpy scalar""" + """Allocate a Numpy scalar.""" scalar = None if p_type == "float": scalar = np.float32(size) @@ -748,7 +748,7 @@ def allocate_scalar(p_type: str, size: int) -> np.number: def allocate_signature_memory(data: dict, preprocessor: list = None, user_dimensions: dict = None) -> list: - """Allocates the data needed by a kernel and returns the arguments array""" + """Allocates the data needed by a kernel and returns the arguments array.""" args = [] for parameter in data.keys(): @@ -763,7 +763,7 @@ def allocate_signature_memory(data: dict, preprocessor: list = None, user_dimens def add_new_line(line: str) -> str: - """Adds the new line character to the end of the line if not present""" + """Adds the new line character to the end of the line if not present.""" if line.rfind("\n") != len(line) - 1: return line + "\n" return line @@ -772,7 +772,7 @@ def add_new_line(line: str) -> str: def add_present_openacc( code: str, langs: Code, data: dict, preprocessor: list = None, user_dimensions: dict = None ) -> str: - """Add the present clause to OpenACC directive""" + """Add the present clause to OpenACC directive.""" new_body = "" for line in code.replace("\\\n", "").split("\n"): if not line_contains_openacc_parallel_directive(line, langs.language): @@ -798,12 +798,12 @@ def add_present_openacc( def add_present_openacc_cxx(name: str, size: ArraySize) -> str: - """Create present clause for C++ OpenACC directive""" + """Create present clause for C++ OpenACC directive.""" return f" present({name}[:{size.get()}]) " def add_present_openacc_fortran(name: str, size: ArraySize) -> str: - """Create present clause for Fortran OpenACC directive""" + """Create present clause for Fortran OpenACC directive.""" if len(size) == 1: return f" present({name}(:{size.get()})) " else: @@ -812,7 +812,7 @@ def add_present_openacc_fortran(name: str, size: ArraySize) -> str: def process_directives(langs: Code, source: str, user_dimensions: dict = None) -> Tuple[dict, dict]: - """Helper functions to process all the directives in the code and create tunable functions""" + """Helper functions to process all the directives in the code and create tunable functions.""" kernel_strings = dict() kernel_args = dict() preprocessor = extract_preprocessor(source) From 78b1720d216a034d56d68c75065f15368b686665 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Wed, 4 Feb 2026 15:12:14 +0100 Subject: [PATCH 034/146] Disabled reusing nox environments --- noxfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index bc96d2486..fe39a8d2e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -22,7 +22,7 @@ nox.options.stop_on_first_error = True nox.options.error_on_missing_interpreters = True nox.options.default_venv_backend = "virtualenv" -nox.options.reuse_existing_virtualenvs = True +nox.options.reuse_existing_virtualenvs = False # workspace level settings settings_file_path = Path("./noxsettings.toml") From 9dc9bc3a9e475d82616385060f6575c4e5bb8b55 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Thu, 5 Feb 2026 13:33:15 +0100 Subject: [PATCH 035/146] Improved tests, PyCUDA compatibility and updated dependencies --- kernel_tuner/core.py | 2 +- noxfile.py | 42 ++++++++++++++++++++++++------------------ pyproject.toml | 2 +- test/test_core.py | 2 +- 4 files changed, 27 insertions(+), 21 deletions(-) diff --git a/kernel_tuner/core.py b/kernel_tuner/core.py index 066ef30b3..cb6b5aa56 100644 --- a/kernel_tuner/core.py +++ b/kernel_tuner/core.py @@ -790,7 +790,7 @@ def run_kernel(self, func, gpu_args, instance): logging.debug("grid dims (%d, %d, %d)", *instance.grid) try: - self.dev.run_kernel(func, gpu_args, instance.threads, instance.grid, self.last_instance_params) + self.dev.run_kernel(func, gpu_args, instance.threads, instance.grid, params=self.last_instance_params) except Exception as e: if "too many resources requested for launch" in str(e) or "OUT_OF_RESOURCES" in str(e): logging.debug("ignoring runtime failure due to too many resources required") diff --git a/noxfile.py b/noxfile.py index fe39a8d2e..6af97cc37 100644 --- a/noxfile.py +++ b/noxfile.py @@ -82,11 +82,14 @@ def create_settings(session: Session) -> None: if envdir is not None and len(envdir) > 0: nox.options.envdir = envdir -# @session # to only run on the current python interpreter -# def lint(session: Session) -> None: -# """Ensure the code is formatted as expected.""" -# session.install("ruff") -# session.run("ruff", "--output-format=github", "--config=pyproject.toml", ".") + +@session # to only run on the current python interpreter +def lint(session: Session) -> None: + """Ensure the code is formatted as expected.""" + session.install("ruff") + session.warn("Linting errors detected:") + session.run(*"ruff check . --config=pyproject.toml --statistics --exit-zero".split()) + # session.run(*"ruff check --config=pyproject.toml --output-format=github .".split()) @session # to only run on the current python interpreter @@ -210,7 +213,7 @@ def tests(session: Session) -> None: if install_julia: extras_args.extend(["-E", "julia"]) # set the paths to Julia install, environment and project - session_envdir = session.env["VIRTUAL_ENV"] + session_envdir = nox.options.envdir if nox.options.envdir is not None else session.env["VIRTUAL_ENV"] julia_envdir = Path(session_envdir) / ".julia" if julia_envdir is not None: session.env["JULIA_DEPOT_PATH"] = str(Path(julia_envdir).resolve()) @@ -227,18 +230,21 @@ def tests(session: Session) -> None: nvcc_output = "".join(nvcc_output.splitlines()) # convert to single string for easier REGEX cuda_version = re.match(r"^.*release ([0-9]+.[0-9]+).*$", nvcc_output, flags=re.IGNORECASE).group(1).strip() session.warn(f"Detected CUDA version: {cuda_version}") - # if we need to install the CUDA extras, first install pycuda seperately, reason: - # since version 2022.2 it has `oldest-supported-numpy` as a build dependency which doesn't work with Poetry if " not found: " in session.run("pip", "show", "pycuda", external=True, silent=True, success_codes=[0, 1]): - # if PyCUDA is not installed, install it - session.warn("PyCUDA not installed") - try: - session.install( - "pycuda", "--no-cache-dir", "--force-reinstall" - ) # Attention: if changed, check `pycuda` in pyproject.toml as well - except Exception as error: - session.log(error) - session.warn(install_warning) + # if we need to install the CUDA extras, first install pycuda seperately, reason: + # between version 2022.2 and 2025.1 PyCUDA had `oldest-supported-numpy` as a build dependency which doesn't work with Poetry + # No longer needed as of 2025.1 + # commented as no longer needed + # # if PyCUDA is not installed, install it + # session.warn("PyCUDA not installed") + # try: + # session.install( + # "pycuda", "--no-cache-dir", "--force-reinstall" + # ) # Attention: if changed, check `pycuda` in pyproject.toml as well + # except Exception as error: + # session.log(error) + # session.warn(install_warning) + pass else: session.warn("PyCUDA installed") # if PyCUDA is already installed, check whether the CUDA version PyCUDA was installed with matches the current CUDA version # noqa: E501 @@ -260,7 +266,7 @@ def tests(session: Session) -> None: ) try: session.install( - "pycuda", "--no-cache-dir", "--force-reinstall" + "pycuda>=2025.1", "--no-cache-dir", "--force-reinstall" ) # Attention: if changed, check `pycuda` in pyproject.toml as well except Exception as error: session.log(error) diff --git a/pyproject.toml b/pyproject.toml index 146c76b44..0fbfa7a7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,7 +98,7 @@ cuda = ["pycuda>=2025.1", "nvidia-ml-py>=12.535.108", "pynvml>=11.4.1"] # Attent opencl = ["pyopencl"] # Attention: if pyopencl is changed here, also change `session.install("pyopencl")` in the Noxfile cuda_opencl = ["pycuda>=2024.1", "pyopencl"] # Attention: if pycuda is changed here, also change `session.install("pycuda")` in the Noxfile hip = ["hip-python"] -julia = ["juliacall>=0.9.31"] # Attention: if juliacall is changed here, also change `session.install("juliacall")` in the Noxfile +julia = ["juliacall>=0.9.31"] tutorial = ["jupyter>=1.0.0", "matplotlib>=3.5.0", "nvidia-ml-py>=12.535.108"] # ATTENTION: if anything is changed here, run `poetry update` and `poetry export --with docs --without-hashes --format=requirements.txt --output doc/requirements.txt` diff --git a/test/test_core.py b/test/test_core.py index 35783c7b2..07807d563 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -108,7 +108,7 @@ def test_check_kernel_output(dev_func_interface): dev.check_kernel_output('func', answer, instance, answer, atol, None, True) dfi.refresh_memory.assert_called() - dfi.run_kernel.assert_called_once_with('func', answer, (256, 1, 1), (1, 1, 1), None) + dfi.run_kernel.assert_called_once_with('func', answer, (256, 1, 1), (1, 1, 1), params=None) print(dfi.mock_calls) From bde2913b63bcd50d0444eba2680a08b5af9d84d6 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Sun, 8 Feb 2026 16:10:30 +0100 Subject: [PATCH 036/146] Added conversion of and interaction with Julia objects --- kernel_tuner/core.py | 7 ++++++- kernel_tuner/interface.py | 16 +++++++++++++--- kernel_tuner/util.py | 8 +++++++- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/kernel_tuner/core.py b/kernel_tuner/core.py index cb6b5aa56..95026a5bd 100644 --- a/kernel_tuner/core.py +++ b/kernel_tuner/core.py @@ -524,6 +524,11 @@ def check_kernel_output(self, func, gpu_args, instance, answer, atol, verify, ve """Runs the kernel once and checks the result against answer.""" logging.debug("check_kernel_output") + # convert juliacall array to numpy array + for i, arg in enumerate(instance.arguments): + if isinstance(answer[i], np.ndarray) and "juliacall" in str(type(arg)): + instance.arguments[i] = np.array(arg, dtype=answer[i].dtype) + # if not using custom verify function, check if the length is the same if answer: if len(instance.arguments) != len(answer): @@ -874,7 +879,7 @@ def _default_verify_function(instance, answer, result_host, atol, verbose): answer[i], np.number ): raise TypeError( - f"Element {i} of expected results list is not a numpy/cupy ndarray, torch Tensor or numpy scalar." + f"Arg or Element {i} of expected results list is {type(arg)} / {type(answer[i])}, not a numpy/cupy ndarray, torch Tensor or numpy scalar." # noqa: E501 ) else: raise TypeError(f"Element {i} of expected results list and kernel arguments have different types.") diff --git a/kernel_tuner/interface.py b/kernel_tuner/interface.py index 2b66735da..6e10ad836 100644 --- a/kernel_tuner/interface.py +++ b/kernel_tuner/interface.py @@ -605,6 +605,7 @@ def tune_kernel( tune_params = dict(tune_params) restrictions = util.possible_julia_vector_to_list(restrictions) block_size_names = util.possible_julia_vector_to_list(block_size_names) + answer = [None if a is None else numpy.array(a) for a in util.possible_julia_vector_to_list(answer)] _check_user_input(kernel_name, kernelsource, arguments, block_size_names) @@ -799,6 +800,14 @@ def run_kernel( kernelsource = core.KernelSource(kernel_name, kernel_source, lang, defines) + if lang == "Julia": + if isinstance(params, dict) or "DictValue" in params.__class__.__name__: + raise ValueError( + "tune_params should not be a Julia dict, because it does not preserve order. Use a list of pairs instead." + ) + params = [tuple([k, util.possible_julia_vector_to_list(tp)]) for k, tp in params] + params = dict(params) + _check_user_input(kernel_name, kernelsource, arguments, block_size_names) # sort options into separate dicts @@ -808,6 +817,7 @@ def run_kernel( # detect language and create the right device function interface dev = core.DeviceInterface(kernelsource, iterations=1, **device_options) + dev.last_instance_params = params # Preprocess GPU arguments. Require for handling `Tunable` arguments arguments = dev.preprocess_gpu_arguments(arguments, params) @@ -919,9 +929,9 @@ def tune_kernel_T1( # if it is a path, import the strategy from the file opt_path: Path = Path(strategy_options["custom_search_method_path"]) class_name: str = strategy - assert ( - opt_path.exists() - ), f"Custom search method path '{opt_path}' does not exist relative to current working directory {Path.cwd()}" + assert opt_path.exists(), ( + f"Custom search method path '{opt_path}' does not exist relative to current working directory {Path.cwd()}" + ) optimizer_class = import_class_from_file(opt_path, class_name) filter_keys = ["custom_search_method_path", "max_fevals", "time_limit", "constraint_aware"] adjusted_strategy_options = {k: v for k, v in strategy_options.items() if k not in filter_keys} diff --git a/kernel_tuner/util.py b/kernel_tuner/util.py index a11ed0480..cfcd6c31f 100644 --- a/kernel_tuner/util.py +++ b/kernel_tuner/util.py @@ -12,6 +12,10 @@ import time import warnings from inspect import getsource, signature +from math import ( + ceil, # noqa: F401 + floor, # noqa: F401 +) # importing here to make available for eval in restrictions / metrics / problem size etc. from pathlib import Path from types import FunctionType from typing import Union @@ -1328,5 +1332,7 @@ def dump_cache(obj: str, tuning_options): def possible_julia_vector_to_list(obj): """Convert a Julia vector to a Python list if needed.""" if obj.__class__.__name__ == "VectorValue": - return list(obj) + l = list(obj) + l = [possible_julia_vector_to_list(e) for e in l] + return l return obj From 733ce2d99a98935a188d95eda29d21fa637fb868 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Sun, 8 Feb 2026 16:17:56 +0100 Subject: [PATCH 037/146] Reduced edgeitems to prevent BlockingIOError --- kernel_tuner/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel_tuner/core.py b/kernel_tuner/core.py index 95026a5bd..e184e2e8b 100644 --- a/kernel_tuner/core.py +++ b/kernel_tuner/core.py @@ -911,7 +911,7 @@ def _flatten(a): print("Error: " + util.get_config_string(instance.params) + " detected during correctness check") print("this error occurred when checking value of the %oth kernel argument" % (i,)) print("Printing kernel output and expected result, set verbose=False to suppress this debug print") - np.set_printoptions(edgeitems=50) + np.set_printoptions(edgeitems=30) print("Kernel output:") print(result) print("Expected:") From 8a7b5bb1535cad82046982a2816ffb6024f8483c Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 9 Feb 2026 14:48:33 +0100 Subject: [PATCH 038/146] Implemented Julia Device to Host and Host to Device copies, improved error types --- kernel_tuner/backends/julia.py | 49 +++++++++++---------------- kernel_tuner/backends/julia_helper.jl | 1 + kernel_tuner/core.py | 4 +-- 3 files changed, 23 insertions(+), 31 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 9ad5b0de8..0e97b3f25 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -172,6 +172,7 @@ def initialize_backend(self, device, backend_name): jl.seval(f"tmp_arr = {info['GPUArrayType']}(Float32.(zeros(2)))") self.backend = jl.seval("KernelAbstractions.get_backend(tmp_arr)") self.GPUArrayType = info["GPUArrayType"] + jl.seval("tmp_arr = nothing; GC.gc()") # free temporary array # Select device try: @@ -244,24 +245,22 @@ def __del__(self): del a except Exception: pass + jl.seval("GC.gc()") # ------------------------- # Memory and argument setup # ------------------------- def ready_argument_list(self, arguments): - """Convert numpy arrays to GPU Array in Julia.""" + """Convert arrays to GPU Array in Julia.""" gpu_args = [] for arg in arguments: - if isinstance(arg, np.ndarray): - try: - arr = self.to_gpuarray(arg) - gpu_args.append(arr) - self.allocations.append(arr) - except Exception as e: - raise RuntimeError(f"Failed to move array to GPU: {e}") - else: - gpu_args.append(arg) + try: + arr = self.to_gpuarray(arg) + gpu_args.append(arr) + self.allocations.append(arr) + except Exception as e: + raise RuntimeError(f"Failed to move array to GPU: {e}") return gpu_args # ------------------------- @@ -360,6 +359,7 @@ def synchronize(self): jl.Main.KernelAbstractions.synchronize(self.backend) except JuliaError as e: raise RuntimeError(f"Julia synchronize failed: {e}") + jl.seval("GC.gc()") # trigger GC to free GPU memory after synchronization # ------------------------- # Memory utilities @@ -377,28 +377,20 @@ def memset(allocation, value, size): @staticmethod def memcpy_dtoh(dest, src): + """Perform a device to host memory copy.""" try: - jl.src_tmp = src - jl.seval("host_tmp = Array(src_tmp)") - host = np.array(jl.host_tmp) - np.copyto(dest, host) - del jl.src_tmp - del jl.host_tmp + np.copyto(dest, src) except JuliaError as e: raise RuntimeError(f"Julia memcpy_dtoh failed: {e}") - # @staticmethod + @staticmethod def memcpy_htod(dest, src): - raise NotImplementedError("memcpy_htod not yet implemented for Julia backend.", dest, src) - try: - jl.src_tmp = src - jl.seval(f"arr_tmp = {self.GPUArrayType}(src_tmp)") - arr_tmp = jl.arr_tmp - del jl.src_tmp - del jl.arr_tmp - return arr_tmp - except JuliaError as e: - raise RuntimeError(f"Julia memcpy_htod failed: {e}") + """Perform a host to device memory copy.""" + dest_ptr = repr(jl.UInt64(jl.pointer_from_objref(dest))) + jl.dest_tmp = dest + jl.src_tmp = src + jl.seval("copyto!(dest_tmp, Array(src_tmp))") + assert dest_ptr == repr(jl.UInt64(jl.pointer_from_objref(jl.dest_tmp))) def copy_constant_memory_args(self, cmem_args): raise NotImplementedError( @@ -431,8 +423,7 @@ def check_package_and_install(self, package): jl.seval(f"import {package}") except Exception as e: raise ImportError( - f"{package}.jl not found in your Julia environment. " - f'Run `using Pkg; Pkg.add("{package}")` in Julia.' + f'{package}.jl not found in your Julia environment. Run `using Pkg; Pkg.add("{package}")` in Julia.' ) from e def create_metal_buffer(self): diff --git a/kernel_tuner/backends/julia_helper.jl b/kernel_tuner/backends/julia_helper.jl index f63bdb14c..d1fe2ade6 100644 --- a/kernel_tuner/backends/julia_helper.jl +++ b/kernel_tuner/backends/julia_helper.jl @@ -1,6 +1,7 @@ export to_gpuarray, launch_kernel function to_gpuarray(a) + a = deepcopy(a) # ensure we have a separate copy of the array to avoid unintended side effects if isa(a, AbstractArray) a = GPUArrayType(a) end diff --git a/kernel_tuner/core.py b/kernel_tuner/core.py index e184e2e8b..b196278e3 100644 --- a/kernel_tuner/core.py +++ b/kernel_tuner/core.py @@ -837,7 +837,7 @@ def _default_verify_function(instance, answer, result_host, atol, verbose): + "." ) if answer[i].size != arg.size: - raise TypeError( + raise ValueError( f"Element {i} of the expected results list has a size different from " + "the kernel argument: " + str(answer[i].size) @@ -855,7 +855,7 @@ def _default_verify_function(instance, answer, result_host, atol, verbose): + "." ) if answer[i].size() != arg.size(): - raise TypeError( + raise ValueError( f"Element {i} of the expected results list has a size different from " + "the kernel argument: " + str(answer[i].size) From b906fa9fd076a2c9da89a84d0fce00bd8e3ab26a Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 9 Feb 2026 17:20:25 +0100 Subject: [PATCH 039/146] Quick fix for non-Julia compatibility --- kernel_tuner/interface.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kernel_tuner/interface.py b/kernel_tuner/interface.py index 6e10ad836..01c37e933 100644 --- a/kernel_tuner/interface.py +++ b/kernel_tuner/interface.py @@ -597,15 +597,16 @@ def tune_kernel( kernelsource = core.KernelSource(kernel_name, kernel_source, lang, defines) if lang == "Julia": + # TODO implement the case where Kernel Tuner is called from Julia but the target language is not Julia if isinstance(tune_params, dict) or "DictValue" in tune_params.__class__.__name__: raise ValueError( "tune_params should not be a Julia dict, because it does not preserve order. Use a list of pairs instead." ) tune_params = [tuple([k, util.possible_julia_vector_to_list(tp)]) for k, tp in tune_params] tune_params = dict(tune_params) + answer = [None if a is None else numpy.array(a) for a in util.possible_julia_vector_to_list(answer)] restrictions = util.possible_julia_vector_to_list(restrictions) block_size_names = util.possible_julia_vector_to_list(block_size_names) - answer = [None if a is None else numpy.array(a) for a in util.possible_julia_vector_to_list(answer)] _check_user_input(kernel_name, kernelsource, arguments, block_size_names) From 6ed180fed0d1591b18ceea16f420d0b71a258e72 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Thu, 26 Feb 2026 15:45:39 +0100 Subject: [PATCH 040/146] Fixed juliapkg to automatically find julia install --- noxfile.py | 1 + pyproject.toml | 2 +- test/test_julia_functions.py | 3 --- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/noxfile.py b/noxfile.py index 6af97cc37..12d7d37ed 100644 --- a/noxfile.py +++ b/noxfile.py @@ -217,6 +217,7 @@ def tests(session: Session) -> None: julia_envdir = Path(session_envdir) / ".julia" if julia_envdir is not None: session.env["JULIA_DEPOT_PATH"] = str(Path(julia_envdir).resolve()) + # session.env["JULIAUP_DEPOT_PATH"] = session.env["JULIA_DEPOT_PATH"] # session.env["PYTHON_JULIACALL_PROJECT"] = str(Path(julia_envdir).resolve()) # separately install optional dependencies with weird dependencies / build process diff --git a/pyproject.toml b/pyproject.toml index 0fbfa7a7b..e7691b128 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,7 +98,7 @@ cuda = ["pycuda>=2025.1", "nvidia-ml-py>=12.535.108", "pynvml>=11.4.1"] # Attent opencl = ["pyopencl"] # Attention: if pyopencl is changed here, also change `session.install("pyopencl")` in the Noxfile cuda_opencl = ["pycuda>=2024.1", "pyopencl"] # Attention: if pycuda is changed here, also change `session.install("pycuda")` in the Noxfile hip = ["hip-python"] -julia = ["juliacall>=0.9.31"] +julia = ["juliapkg>=0.1.23", "juliacall>=0.9.31"] tutorial = ["jupyter>=1.0.0", "matplotlib>=3.5.0", "nvidia-ml-py>=12.535.108"] # ATTENTION: if anything is changed here, run `poetry update` and `poetry export --with docs --without-hashes --format=requirements.txt --output doc/requirements.txt` diff --git a/test/test_julia_functions.py b/test/test_julia_functions.py index 8d9bf9e26..e821384cc 100644 --- a/test/test_julia_functions.py +++ b/test/test_julia_functions.py @@ -10,8 +10,6 @@ from .context import skip_if_no_julia from juliacall import ValueBase -import subprocess - kernel_name = "vector_add!" kernel_string = r""" @@ -31,7 +29,6 @@ @skip_if_no_julia def test_ready_argument_list(): """Ensure Julia backend correctly converts arguments into Julia objects.""" - size = 1000 a = np.int32(75) b = np.random.randn(size).astype(np.float32) From e20a60756350aa6547c32b08fcfdee18a4d53086 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Thu, 5 Mar 2026 15:08:07 +0100 Subject: [PATCH 041/146] Julia backend working for Intel GPUs --- kernel_tuner/backends/julia.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 0e97b3f25..ebbb29fc8 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -135,11 +135,11 @@ def initialize_backend(self, device, backend_name): "INTEL": { "pkg": "oneAPI", "module": "oneAPI", - "device_select": lambda d: f"oneAPI.device!({d})", + "device_select": lambda d: f"devices(first(drivers()))[{d}])", "name": "oneAPI.name(oneAPI.device())", - "max_threads": "oneAPI.device_attribute(oneAPI.device(), :max_work_group_size)", + "max_threads": "oneAPI.compute_properties(oneAPI.device()).maxTotalGroupSize", "capability": None, - "GPUArrayType": "OneArray", + "GPUArrayType": "oneArray", }, "METAL": { "pkg": "Metal", @@ -176,7 +176,7 @@ def initialize_backend(self, device, backend_name): # Select device try: - jl.seval(info["device_select"](int(device))) + jl.seval(info["device_select"](int(device) + 1)) # Julia uses 1-based indexing self.last_selected_device = device except Exception as e: raise RuntimeError(f"Failed to select Julia {info['module']} device {device}: {e}") from e @@ -208,8 +208,12 @@ def initialize_backend(self, device, backend_name): self.backend_device = self.backend_mod.device() if backend_name == "CUDA": self.contextqueue = self.backend_mod.context - elif backend_name in ("AMD", "INTEL"): + elif backend_name == "AMD": self.contextqueue = self.backend_mod.queue + # elif backend_name == "INTEL": + # self.contextqueue = jl.seval( + # f"ZeCommandQueue(ZeContext(first(drivers())), devices(first(drivers()))[{int(device) + 1}]))" + # ) elif backend_name == "METAL": self.contextqueue = self.backend_mod.MTLCommandQueue(self.backend_device) From 8420eca13b24ac00bee249edbaba48bd08384837 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Thu, 5 Mar 2026 15:42:23 +0100 Subject: [PATCH 042/146] No Julia conversion on None inputs --- kernel_tuner/interface.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kernel_tuner/interface.py b/kernel_tuner/interface.py index 01c37e933..a8aa59710 100644 --- a/kernel_tuner/interface.py +++ b/kernel_tuner/interface.py @@ -604,7 +604,8 @@ def tune_kernel( ) tune_params = [tuple([k, util.possible_julia_vector_to_list(tp)]) for k, tp in tune_params] tune_params = dict(tune_params) - answer = [None if a is None else numpy.array(a) for a in util.possible_julia_vector_to_list(answer)] + if answer is not None: + answer = [None if a is None else numpy.array(a) for a in util.possible_julia_vector_to_list(answer)] restrictions = util.possible_julia_vector_to_list(restrictions) block_size_names = util.possible_julia_vector_to_list(block_size_names) From ba1e4cd9c89e72126048714f01ba8c713748a28f Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Tue, 24 Mar 2026 13:35:54 -0500 Subject: [PATCH 043/146] Changes to improve Intel compatibility --- kernel_tuner/backends/julia.py | 13 +++++++++---- kernel_tuner/interface.py | 5 +++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 0e97b3f25..e519f4eb8 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -43,6 +43,7 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None if jl is None: raise ImportError("JuliaCall not installed. Please run `pip install juliacall`.") self.available_backends = detect_julia_gpu_backends() + self.available_backends = "INTEL" if compiler_options is not None and len(compiler_options) == 1: if compiler_options[0].upper() not in self.available_backends: raise ValueError( @@ -137,9 +138,9 @@ def initialize_backend(self, device, backend_name): "module": "oneAPI", "device_select": lambda d: f"oneAPI.device!({d})", "name": "oneAPI.name(oneAPI.device())", - "max_threads": "oneAPI.device_attribute(oneAPI.device(), :max_work_group_size)", + "max_threads": "oneAPI.compute_properties(oneAPI.device()).maxTotalGroupSize", "capability": None, - "GPUArrayType": "OneArray", + "GPUArrayType": "oneArray", }, "METAL": { "pkg": "Metal", @@ -176,7 +177,7 @@ def initialize_backend(self, device, backend_name): # Select device try: - jl.seval(info["device_select"](int(device))) + jl.seval(info["device_select"](int(device) + 1)) self.last_selected_device = device except Exception as e: raise RuntimeError(f"Failed to select Julia {info['module']} device {device}: {e}") from e @@ -208,8 +209,12 @@ def initialize_backend(self, device, backend_name): self.backend_device = self.backend_mod.device() if backend_name == "CUDA": self.contextqueue = self.backend_mod.context - elif backend_name in ("AMD", "INTEL"): + elif backend_name == "AMD": self.contextqueue = self.backend_mod.queue + #elif backend_name == "INTEL": + # self.contextqueue = jl.seval( + # f"ZeCommandQueue(ZeContext(first(drivers())), devices(first(drivers()))[{int(device) + 1}]))" + # ) elif backend_name == "METAL": self.contextqueue = self.backend_mod.MTLCommandQueue(self.backend_device) diff --git a/kernel_tuner/interface.py b/kernel_tuner/interface.py index 01c37e933..69fd97656 100644 --- a/kernel_tuner/interface.py +++ b/kernel_tuner/interface.py @@ -604,7 +604,8 @@ def tune_kernel( ) tune_params = [tuple([k, util.possible_julia_vector_to_list(tp)]) for k, tp in tune_params] tune_params = dict(tune_params) - answer = [None if a is None else numpy.array(a) for a in util.possible_julia_vector_to_list(answer)] + if answer is not None: + answer = [None if a is None else numpy.array(a) for a in util.possible_julia_vector_to_list(answer)] restrictions = util.possible_julia_vector_to_list(restrictions) block_size_names = util.possible_julia_vector_to_list(block_size_names) @@ -698,7 +699,7 @@ def preprocess_cache(filepath): # create search space tuning_options.restrictions_unmodified = deepcopy(restrictions) - searchspace = Searchspace(tune_params, restrictions, runner.dev.max_threads, **searchspace_construction_options) + searchspace = Searchspace(tune_params, restrictions, max_threads=runner.dev.max_threads, **searchspace_construction_options) restrictions = searchspace._modified_restrictions tuning_options.restrictions = restrictions if verbose: From e5aa60265d0daac5bb687b0d9eb214646289c2f9 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Tue, 24 Mar 2026 20:01:15 +0100 Subject: [PATCH 044/146] Improved device selection for Julia --- kernel_tuner/backends/julia.py | 4 +++- kernel_tuner/interface.py | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 015e26d4d..36abcfd3a 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -177,7 +177,9 @@ def initialize_backend(self, device, backend_name): # Select device try: - jl.seval(info["device_select"](int(device) + 1)) # Julia uses 1-based indexing + if int(device) == 0: + device = 1 # Julia uses 1-based indexing + jl.seval(info["device_select"](int(device))) self.last_selected_device = device except Exception as e: raise RuntimeError(f"Failed to select Julia {info['module']} device {device}: {e}") from e diff --git a/kernel_tuner/interface.py b/kernel_tuner/interface.py index 69fd97656..f08210f75 100644 --- a/kernel_tuner/interface.py +++ b/kernel_tuner/interface.py @@ -606,6 +606,8 @@ def tune_kernel( tune_params = dict(tune_params) if answer is not None: answer = [None if a is None else numpy.array(a) for a in util.possible_julia_vector_to_list(answer)] + if device == 0: + device = 1 # Julia uses 1-based indexing for devices restrictions = util.possible_julia_vector_to_list(restrictions) block_size_names = util.possible_julia_vector_to_list(block_size_names) From 8076ee56156e669a2230a1dffd7bcf0a32570594 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Wed, 25 Mar 2026 15:21:42 +0100 Subject: [PATCH 045/146] Improved device selection support for CUDA and Intel platforms --- kernel_tuner/backends/julia.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 36abcfd3a..c163e6bdc 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -136,7 +136,7 @@ def initialize_backend(self, device, backend_name): "INTEL": { "pkg": "oneAPI", "module": "oneAPI", - "device_select": lambda d: f"devices(first(drivers()))[{d}])", + "device_select": lambda d: f"devices(first(drivers()))[{d}]", "name": "oneAPI.name(oneAPI.device())", "max_threads": "oneAPI.compute_properties(oneAPI.device()).maxTotalGroupSize", "capability": None, @@ -177,8 +177,8 @@ def initialize_backend(self, device, backend_name): # Select device try: - if int(device) == 0: - device = 1 # Julia uses 1-based indexing + if int(device) == 0 and not info["pkg"] == "CUDA": + device = 1 # Julia uses 1-based indexing, but the CUDA backend uses 0-based so we skip that jl.seval(info["device_select"](int(device))) self.last_selected_device = device except Exception as e: From ddba6a41feb6ea093ae259e3ad770ba692cae9cf Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Wed, 25 Mar 2026 15:25:14 +0100 Subject: [PATCH 046/146] Improved device selection support for CUDA and Intel platforms --- kernel_tuner/backends/julia.py | 1 - 1 file changed, 1 deletion(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index c163e6bdc..a11a13a18 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -43,7 +43,6 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None if jl is None: raise ImportError("JuliaCall not installed. Please run `pip install juliacall`.") self.available_backends = detect_julia_gpu_backends() - self.available_backends = "INTEL" if compiler_options is not None and len(compiler_options) == 1: if compiler_options[0].upper() not in self.available_backends: raise ValueError( From 0feb054c588cd361335c5c314db2ea0b1ca1261b Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Wed, 25 Mar 2026 15:34:52 +0100 Subject: [PATCH 047/146] Updated device selection for AMD --- kernel_tuner/backends/julia.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index a11a13a18..c743d89f4 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -126,7 +126,7 @@ def initialize_backend(self, device, backend_name): "AMD": { "pkg": "AMDGPU", "module": "AMDGPU", - "device_select": lambda d: f"AMDGPU.device!({d})", + "device_select": lambda d: f"AMDGPU.device!(AMDGPU.devices()[{d}])", "name": "AMDGPU.name(AMDGPU.device())", "max_threads": "AMDGPU.device_attribute(AMDGPU.device(), :maxthreadsperblock)", "capability": None, From 75dc87051403a571c9579cc448c62f8bc191fe72 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Wed, 25 Mar 2026 16:06:52 +0100 Subject: [PATCH 048/146] Extended timeout to allow Garbage Collection to complete --- test/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/conftest.py b/test/conftest.py index 1539a6cdf..a1867210e 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -4,4 +4,4 @@ def pytest_collection_modifyitems(items): for item in items: if item.get_closest_marker('timeout') is None: - item.add_marker(pytest.mark.timeout(60)) \ No newline at end of file + item.add_marker(pytest.mark.timeout(180)) \ No newline at end of file From dcf55b99eb20855ef230897673e77f31c300ac06 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Wed, 25 Mar 2026 17:11:08 +0100 Subject: [PATCH 049/146] Removed redundant device selection logic --- kernel_tuner/interface.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/kernel_tuner/interface.py b/kernel_tuner/interface.py index f08210f75..69fd97656 100644 --- a/kernel_tuner/interface.py +++ b/kernel_tuner/interface.py @@ -606,8 +606,6 @@ def tune_kernel( tune_params = dict(tune_params) if answer is not None: answer = [None if a is None else numpy.array(a) for a in util.possible_julia_vector_to_list(answer)] - if device == 0: - device = 1 # Julia uses 1-based indexing for devices restrictions = util.possible_julia_vector_to_list(restrictions) block_size_names = util.possible_julia_vector_to_list(block_size_names) From 493c2c980b324751bd72f2653d740fe27ad8b065 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Tue, 31 Mar 2026 18:18:31 +0200 Subject: [PATCH 050/146] Minor improvements to tests --- noxfile.py | 2 +- test/test_julia_functions.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/noxfile.py b/noxfile.py index 12d7d37ed..d593e654b 100644 --- a/noxfile.py +++ b/noxfile.py @@ -18,7 +18,7 @@ # set the test parameters verbose = False -python_versions_to_test = ["3.11", "3.12", "3.13", "3.14"] +python_versions_to_test = ["3.14", "3.13", "3.12", "3.11"] nox.options.stop_on_first_error = True nox.options.error_on_missing_interpreters = True nox.options.default_venv_backend = "virtualenv" diff --git a/test/test_julia_functions.py b/test/test_julia_functions.py index e821384cc..f1fbe1c55 100644 --- a/test/test_julia_functions.py +++ b/test/test_julia_functions.py @@ -42,7 +42,7 @@ def test_ready_argument_list(): # Julia Array maps back through PythonCall as pyjl_pointer-like proxies # Scalars remain scalars assert isinstance(gpu_args[0], ValueBase) # Julia GPU Array proxy - assert isinstance(gpu_args[1], np.int32) # scalar unchanged + assert isinstance(gpu_args[1], (int, np.int32)) # scalar unchanged assert isinstance(gpu_args[2], ValueBase) # Julia GPU Array proxy From 129ee93a16ab0e8610e725a094d6aa3c442d8b09 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Tue, 31 Mar 2026 18:18:54 +0200 Subject: [PATCH 051/146] Expanded documentation for dev environment --- doc/source/dev-environment.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/source/dev-environment.rst b/doc/source/dev-environment.rst index afdbd4690..275f40039 100644 --- a/doc/source/dev-environment.rst +++ b/doc/source/dev-environment.rst @@ -40,7 +40,7 @@ Steps with :bash:`sudo` access (e.g. on a local device): * Activate the environment with :bash:`pyenv activate kerneltuner`. * Make sure :bash:`which python` and :bash:`which pip` point to the expected Python location and version. * Update Pip with :bash:`pip install --upgrade pip`. -#. Install the project, dependencies and extras: :bash:`poetry install --with test,docs -E cuda -E opencl -E hip`, leaving out :bash:`-E cuda`, :bash:`-E opencl` or :bash:`-E hip` if this does not apply on your system. To go all-out, use :bash:`--all-extras` +#. Install the project, dependencies and extras: :bash:`poetry install --with test,docs -E cuda -E opencl -E hip -E julia`, leaving out :bash:`-E cuda`, :bash:`-E opencl` etc. if this does not apply on your system. To go all-out, use :bash:`--all-extras` * Depending on the environment, it may be necessary or convenient to install extra packages such as :bash:`cupy-cuda11x` / :bash:`cupy-cuda12x`, and :bash:`cuda-python`. These are currently not defined as dependencies for kernel-tuner, but can be part of tests. * Do not forget to make sure the paths are set correctly. If you're using CUDA, the desired CUDA version should be in :bash:`$PATH`, :bash:`$LD_LIBARY_PATH` and :bash:`$CPATH`. * Re-open the shell for changes to take effect. @@ -72,8 +72,9 @@ Steps without :bash:`sudo` access (e.g. on a cluster): #. `Install Poetry `__. * Use :bash:`curl -sSL https://install.python-poetry.org | python3 -` to install Poetry. * Add the poetry export plugin with :bash:`poetry self add poetry-plugin-export`. -#. Install the project, dependencies and extras: :bash:`poetry install --with test,docs -E cuda -E opencl -E hip`, leaving out :bash:`-E cuda`, :bash:`-E opencl` or :bash:`-E hip` if this does not apply on your system. To go all-out, use :bash:`--all-extras`. +#. Install the project, dependencies and extras: :bash:`poetry install --with test,docs -E cuda -E opencl -E hip -E julia`, leaving out :bash:`-E cuda`, :bash:`-E opencl` etc. if this does not apply on your system. To go all-out, use :bash:`--all-extras`. * If you run into "keyring" or other seemingly weird issues, this is a known issue with Poetry on some systems. Do: :bash:`pip install keyring`, :bash:`python3 -m keyring --disable`. + * Kernel Tuner has a dependency on Python-Constraint, which provides binaries for most systems. On some older systems, these binaries may not be compatible (on Linux usually due to an outdated LDD version, check with :bash:`ldd --version`, must be >=2.35). Without binaries, your system will try to build it yourself, so make sure build tools are available (e.g. with :bash:`module load gcc/13.2.0`). * Depending on the environment, it may be necessary or convenient to install extra packages such as :bash:`cupy-cuda11x` / :bash:`cupy-cuda12x`, and :bash:`cuda-python`. These are currently not defined as dependencies for kernel-tuner, but can be part of tests. * Verify that your development environment has no missing installs or updates with :bash:`poetry install --sync --dry-run --with test`. #. Check if the environment is setup correctly by running :bash:`pytest`. All tests should pass, except if you're not on a GPU node, or one or more extras has been left out in the previous step, then these tests will skip gracefully. From 08767da4db8beef5112646fcc75f1d2187d6438c Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Tue, 31 Mar 2026 18:39:08 +0200 Subject: [PATCH 052/146] Improved automatic check for Intel GPUs, improved tests --- kernel_tuner/backends/julia.py | 4 +++- noxfile.py | 15 ++++++++++++--- test/test_core.py | 4 ++-- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index c743d89f4..f0644c73a 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -459,7 +459,9 @@ def detect_julia_gpu_backends(): pass elif backend_name == "INTEL": try: - subprocess.check_output("intel_gpu_top -J".split()) + subprocess.check_output( + "ls /dev/dri/by-path/".split() + ) # not a perfect check but should work in most cases available_backends.append(backend_name) except (FileNotFoundError, subprocess.CalledProcessError): pass diff --git a/noxfile.py b/noxfile.py index d593e654b..9272cf9e8 100644 --- a/noxfile.py +++ b/noxfile.py @@ -12,6 +12,7 @@ from json import loads as json_loads from pathlib import Path from re import search as regex_search +from warnings import warn import nox from nox_poetry import Session, session @@ -377,6 +378,7 @@ def tests(session: Session) -> None: ### Helper functions ### +# lifted from backends/julia.py def detect_julia_gpu_backends(): """Detect the Julia backends available, return the assiociated package.""" available_backends = [] @@ -395,7 +397,9 @@ def detect_julia_gpu_backends(): pass elif backend_name == "INTEL": try: - subprocess.check_output("intel_gpu_top -J".split()) + subprocess.check_output( + "ls /dev/dri/by-path/".split() + ) # not a perfect check but should work in most cases available_backends.append("oneAPI") except (FileNotFoundError, subprocess.CalledProcessError): pass @@ -408,8 +412,13 @@ def detect_julia_gpu_backends(): supported = gpu["spdisplays_mtlgpufamilysupport"].lower() if "metal" in supported: version = regex_search(r".*metal([\d.]+)", supported).group(1) - if float(version) > 3: - available_backends.append("Metal") + if float(version) < 3: + warn( + f"Metal backend detected, but {supported} < 3. " + "Metal.jl requires Metal version 3 or higher." + ) + else: + available_backends.append(backend_name) except (FileNotFoundError, subprocess.CalledProcessError, JSONDecodeError): pass return available_backends diff --git a/test/test_core.py b/test/test_core.py index 07807d563..e85b31b2b 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -150,7 +150,7 @@ def test_default_verify_function_arrays(): core._default_verify_function(instance, ans, result_host, 0, False) print("_default_verify_function failed to throw an exception") assert False - except TypeError: + except (TypeError, ValueError): assert True for result_host in [result_host, result_host2]: @@ -173,7 +173,7 @@ def test_default_verify_function_scalar(): core._default_verify_function(instance, ans, result_host, 0, False) print("_default_verify_function failed to throw an exception") assert False - except TypeError: + except (TypeError, ValueError): assert True assert core._default_verify_function(instance, answer, result_host, 0.1, False) From d780b9dfe81d0a371894fea9bdad72f05b62e11c Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Tue, 31 Mar 2026 19:08:24 +0200 Subject: [PATCH 053/146] Refined automatic backend check test --- kernel_tuner/backends/julia.py | 21 ++++++++++++--------- noxfile.py | 21 ++++++++++++--------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index f0644c73a..e08a3f7f7 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -444,7 +444,7 @@ def create_metal_buffer(self): def detect_julia_gpu_backends(): """Detect the Julia backends available.""" available_backends = [] - for backend_name in ["CUDA", "AMD", "INTEL", "METAL"]: + for backend_name in ["CUDA", "AMD", "METAL", "INTEL"]: if backend_name == "CUDA": try: subprocess.check_output("nvidia-smi") @@ -457,14 +457,6 @@ def detect_julia_gpu_backends(): available_backends.append(backend_name) except (FileNotFoundError, subprocess.CalledProcessError): pass - elif backend_name == "INTEL": - try: - subprocess.check_output( - "ls /dev/dri/by-path/".split() - ) # not a perfect check but should work in most cases - available_backends.append(backend_name) - except (FileNotFoundError, subprocess.CalledProcessError): - pass elif backend_name == "METAL": try: output = subprocess.check_output("system_profiler -json SPDisplaysDataType".split()) @@ -483,4 +475,15 @@ def detect_julia_gpu_backends(): available_backends.append(backend_name) except (FileNotFoundError, subprocess.CalledProcessError, JSONDecodeError): pass + elif backend_name == "INTEL": + # this can give false positives for other backends too, so skip if we've already detected another backend + if len(available_backends) > 0: + continue + try: + subprocess.check_output( + "ls /dev/dri/by-path/".split() + ) # not a perfect check but should work in most cases + available_backends.append(backend_name) + except (FileNotFoundError, subprocess.CalledProcessError): + pass return available_backends diff --git a/noxfile.py b/noxfile.py index 9272cf9e8..9ecfe9b11 100644 --- a/noxfile.py +++ b/noxfile.py @@ -382,7 +382,7 @@ def tests(session: Session) -> None: def detect_julia_gpu_backends(): """Detect the Julia backends available, return the assiociated package.""" available_backends = [] - for backend_name in ["CUDA", "AMD", "INTEL", "METAL"]: + for backend_name in ["CUDA", "AMD", "METAL", "INTEL"]: if backend_name == "CUDA": try: subprocess.check_output("nvidia-smi") @@ -395,14 +395,6 @@ def detect_julia_gpu_backends(): available_backends.append("AMDGPU") except (FileNotFoundError, subprocess.CalledProcessError): pass - elif backend_name == "INTEL": - try: - subprocess.check_output( - "ls /dev/dri/by-path/".split() - ) # not a perfect check but should work in most cases - available_backends.append("oneAPI") - except (FileNotFoundError, subprocess.CalledProcessError): - pass elif backend_name == "METAL": try: output = subprocess.check_output("system_profiler -json SPDisplaysDataType".split()) @@ -421,4 +413,15 @@ def detect_julia_gpu_backends(): available_backends.append(backend_name) except (FileNotFoundError, subprocess.CalledProcessError, JSONDecodeError): pass + elif backend_name == "INTEL": + # this can give false positives for other backends too, so skip if we've already detected another backend + if len(available_backends) > 0: + continue + try: + subprocess.check_output( + "ls /dev/dri/by-path/".split() + ) # not a perfect check but should work in most cases + available_backends.append("oneAPI") + except (FileNotFoundError, subprocess.CalledProcessError): + pass return available_backends From 5dff37c8e787a8be06995b07f6d9f8b9894c6b2e Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Tue, 31 Mar 2026 22:30:40 +0200 Subject: [PATCH 054/146] Improved automatic detection of Julia backends in Kernel Tuner and testing --- kernel_tuner/backends/julia.py | 94 +------------------------ kernel_tuner/backends/julia_helper.py | 98 +++++++++++++++++++++++++++ noxfile.py | 68 +++---------------- 3 files changed, 110 insertions(+), 150 deletions(-) create mode 100644 kernel_tuner/backends/julia_helper.py diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index e08a3f7f7..a4b467554 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -12,11 +12,7 @@ - Currently supports CuArray and scalar arguments; constant and texture memory are not implemented. """ -import subprocess -from json import JSONDecodeError -from json import loads as json_loads from pathlib import Path -from re import search as regex_search from warnings import warn import numpy as np @@ -25,6 +21,8 @@ from kernel_tuner.observers.julia import JuliaRuntimeObserver from kernel_tuner.util import SkippableFailure +from .julia_helper import backend_map, detect_julia_gpu_backends + try: from juliacall import JuliaError from juliacall import Main as jl @@ -112,46 +110,6 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None def initialize_backend(self, device, backend_name): """Initialize for a choice of Julia backends by backend_name, one of 'cuda', 'amd', 'intel', 'metal'.""" - # Map name → Julia module and device-selection calls - backend_map = { - "CUDA": { - "pkg": "CUDA", - "module": "CUDA", - "device_select": lambda d: f"CUDA.device!({d})", - "name": "CUDA.name(CUDA.device())", - "max_threads": "CUDA.attribute(CUDA.device(), CUDA.DEVICE_ATTRIBUTE_MAX_THREADS_PER_BLOCK)", - "capability": "CUDA.capability(CUDA.device())", - "GPUArrayType": "CuArray", - }, - "AMD": { - "pkg": "AMDGPU", - "module": "AMDGPU", - "device_select": lambda d: f"AMDGPU.device!(AMDGPU.devices()[{d}])", - "name": "AMDGPU.name(AMDGPU.device())", - "max_threads": "AMDGPU.device_attribute(AMDGPU.device(), :maxthreadsperblock)", - "capability": None, - "GPUArrayType": "ROCArray", - }, - "INTEL": { - "pkg": "oneAPI", - "module": "oneAPI", - "device_select": lambda d: f"devices(first(drivers()))[{d}]", - "name": "oneAPI.name(oneAPI.device())", - "max_threads": "oneAPI.compute_properties(oneAPI.device()).maxTotalGroupSize", - "capability": None, - "GPUArrayType": "oneArray", - }, - "METAL": { - "pkg": "Metal", - "module": "Metal", - "device_select": lambda d: "Metal.device!(Metal.device())", # only single device support in Metal.jl - "name": "Metal.name(Metal.device())", - "max_threads": "Int(Metal.device().maxThreadsPerThreadgroup.width)", - "capability": None, - "GPUArrayType": "MtlArray", - }, - } - backend_name = backend_name.upper() if backend_name not in backend_map: raise ValueError(f"Unknown backend: {backend_name}") @@ -439,51 +397,3 @@ def create_metal_buffer(self): except Exception: buf = self.backend_mod.MTLCommandBuffer(self.contextqueue) return buf - - -def detect_julia_gpu_backends(): - """Detect the Julia backends available.""" - available_backends = [] - for backend_name in ["CUDA", "AMD", "METAL", "INTEL"]: - if backend_name == "CUDA": - try: - subprocess.check_output("nvidia-smi") - available_backends.append(backend_name) - except (FileNotFoundError, subprocess.CalledProcessError): - pass - elif backend_name == "AMD": - try: - subprocess.check_output("rocm-smi") - available_backends.append(backend_name) - except (FileNotFoundError, subprocess.CalledProcessError): - pass - elif backend_name == "METAL": - try: - output = subprocess.check_output("system_profiler -json SPDisplaysDataType".split()) - json_output = json_loads(output)["SPDisplaysDataType"] - for gpu in json_output: - if "spdisplays_mtlgpufamilysupport" in gpu: - supported = gpu["spdisplays_mtlgpufamilysupport"].lower() - if "metal" in supported: - version = regex_search(r".*metal([\d.]+)", supported).group(1) - if float(version) < 3: - warn( - f"Metal backend detected, but {supported} < 3. " - "Metal.jl requires Metal version 3 or higher." - ) - else: - available_backends.append(backend_name) - except (FileNotFoundError, subprocess.CalledProcessError, JSONDecodeError): - pass - elif backend_name == "INTEL": - # this can give false positives for other backends too, so skip if we've already detected another backend - if len(available_backends) > 0: - continue - try: - subprocess.check_output( - "ls /dev/dri/by-path/".split() - ) # not a perfect check but should work in most cases - available_backends.append(backend_name) - except (FileNotFoundError, subprocess.CalledProcessError): - pass - return available_backends diff --git a/kernel_tuner/backends/julia_helper.py b/kernel_tuner/backends/julia_helper.py new file mode 100644 index 000000000..e4b52c4ce --- /dev/null +++ b/kernel_tuner/backends/julia_helper.py @@ -0,0 +1,98 @@ +"""Helper functions for Julia backend detection and interaction. + +We might want to consider moving this to a utility module or Julia package as it can be useful. +""" + +import subprocess +from json import JSONDecodeError +from json import loads as json_loads +from re import search as regex_search +from warnings import warn + +# Map name → Julia module and device-selection calls +backend_map = { + "CUDA": { + "pkg": "CUDA", + "module": "CUDA", + "device_select": lambda d: f"CUDA.device!({d})", + "name": "CUDA.name(CUDA.device())", + "max_threads": "CUDA.attribute(CUDA.device(), CUDA.DEVICE_ATTRIBUTE_MAX_THREADS_PER_BLOCK)", + "capability": "CUDA.capability(CUDA.device())", + "GPUArrayType": "CuArray", + }, + "AMD": { + "pkg": "AMDGPU", + "module": "AMDGPU", + "device_select": lambda d: f"AMDGPU.device!(AMDGPU.devices()[{d}])", + "name": "AMDGPU.name(AMDGPU.device())", + "max_threads": "AMDGPU.device_attribute(AMDGPU.device(), :maxthreadsperblock)", + "capability": None, + "GPUArrayType": "ROCArray", + }, + "INTEL": { + "pkg": "oneAPI", + "module": "oneAPI", + "device_select": lambda d: f"devices(first(drivers()))[{d}]", + "name": "oneAPI.name(oneAPI.device())", + "max_threads": "oneAPI.compute_properties(oneAPI.device()).maxTotalGroupSize", + "capability": None, + "GPUArrayType": "oneArray", + }, + "METAL": { + "pkg": "Metal", + "module": "Metal", + "device_select": lambda d: "Metal.device!(Metal.device())", # only single device support in Metal.jl + "name": "Metal.name(Metal.device())", + "max_threads": "Int(Metal.device().maxThreadsPerThreadgroup.width)", + "capability": None, + "GPUArrayType": "MtlArray", + }, +} + + +def detect_julia_gpu_backends(): + """Detect the Julia backends available.""" + available_backends = [] + for backend_name in ["CUDA", "AMD", "METAL", "INTEL"]: + if backend_name == "CUDA": + try: + subprocess.check_output("nvidia-smi") + available_backends.append(backend_name) + except (FileNotFoundError, subprocess.CalledProcessError): + pass + elif backend_name == "AMD": + try: + subprocess.check_output("rocm-smi") + available_backends.append(backend_name) + except (FileNotFoundError, subprocess.CalledProcessError): + pass + elif backend_name == "METAL": + try: + output = subprocess.check_output("system_profiler -json SPDisplaysDataType".split()) + json_output = json_loads(output)["SPDisplaysDataType"] + for gpu in json_output: + if "spdisplays_mtlgpufamilysupport" in gpu: + supported = gpu["spdisplays_mtlgpufamilysupport"].lower() + if "metal" in supported: + version = regex_search(r".*metal([\d.]+)", supported).group(1) + if float(version) < 3: + warn( + f"Metal backend detected, but {supported} < 3. " + "Metal.jl requires Metal version 3 or higher." + ) + else: + available_backends.append(backend_name) + except (FileNotFoundError, subprocess.CalledProcessError, JSONDecodeError): + pass + elif backend_name == "INTEL": + # this can give false positives for other backends too, so skip if we've already detected another backend + if len(available_backends) > 0: + continue + try: + subprocess.check_output( + "ls /dev/dri/by-path/".split() + ) # not a perfect check but should work in most cases + available_backends.append(backend_name) + except (FileNotFoundError, subprocess.CalledProcessError): + pass + return available_backends diff --git a/noxfile.py b/noxfile.py index 9ecfe9b11..3bbd764fb 100644 --- a/noxfile.py +++ b/noxfile.py @@ -7,16 +7,18 @@ import platform import re -import subprocess -from json import JSONDecodeError -from json import loads as json_loads +import sys from pathlib import Path -from re import search as regex_search -from warnings import warn import nox from nox_poetry import Session, session +# Create a repository root path to access the Julia helper functions +# TODO this should be changed to something more robust +REPO_ROOT = Path(__file__).parent.joinpath(Path("kernel_tuner/backends")).resolve() +sys.path.append(str(REPO_ROOT)) +from julia_helper import backend_map, detect_julia_gpu_backends # noqa: E402, F401 + # set the test parameters verbose = False python_versions_to_test = ["3.14", "3.13", "3.12", "3.11"] @@ -325,7 +327,9 @@ def tests(session: Session) -> None: # call Julia to precompile packages in the session environment session.run("julia", "-e", "using Pkg; Pkg.precompile(); Pkg.instantiate()", external=True) # install any additional dependencies used by the tests, as `check_package_and_install` won't work from Nox - gpu_backends_string = "".join(f'Pkg.add("{backend}"); ' for backend in detect_julia_gpu_backends()) + gpu_backends_string = "".join( + f'Pkg.add("{backend_map[backend]["pkg"]}"); ' for backend in detect_julia_gpu_backends() + ) session.run("julia", "-e", f'using Pkg; Pkg.add("KernelAbstractions"); {gpu_backends_string}', external=True) # if applicable, install the dependencies for additional tests @@ -373,55 +377,3 @@ def tests(session: Session) -> None: Run with 'additional-tests' and without 'skip-gpu', 'skip-cuda' etc. to avoid this. """ ) - - -### Helper functions ### - - -# lifted from backends/julia.py -def detect_julia_gpu_backends(): - """Detect the Julia backends available, return the assiociated package.""" - available_backends = [] - for backend_name in ["CUDA", "AMD", "METAL", "INTEL"]: - if backend_name == "CUDA": - try: - subprocess.check_output("nvidia-smi") - available_backends.append("CUDA") - except (FileNotFoundError, subprocess.CalledProcessError): - pass - elif backend_name == "AMD": - try: - subprocess.check_output("rocm-smi") - available_backends.append("AMDGPU") - except (FileNotFoundError, subprocess.CalledProcessError): - pass - elif backend_name == "METAL": - try: - output = subprocess.check_output("system_profiler -json SPDisplaysDataType".split()) - json_output = json_loads(output)["SPDisplaysDataType"] - for gpu in json_output: - if "spdisplays_mtlgpufamilysupport" in gpu: - supported = gpu["spdisplays_mtlgpufamilysupport"].lower() - if "metal" in supported: - version = regex_search(r".*metal([\d.]+)", supported).group(1) - if float(version) < 3: - warn( - f"Metal backend detected, but {supported} < 3. " - "Metal.jl requires Metal version 3 or higher." - ) - else: - available_backends.append(backend_name) - except (FileNotFoundError, subprocess.CalledProcessError, JSONDecodeError): - pass - elif backend_name == "INTEL": - # this can give false positives for other backends too, so skip if we've already detected another backend - if len(available_backends) > 0: - continue - try: - subprocess.check_output( - "ls /dev/dri/by-path/".split() - ) # not a perfect check but should work in most cases - available_backends.append("oneAPI") - except (FileNotFoundError, subprocess.CalledProcessError): - pass - return available_backends From bde773a61974ce54e3faf2ec635c03985506471d Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Fri, 3 Apr 2026 23:34:32 +0200 Subject: [PATCH 055/146] Added block size name augmentation to run_kernel --- kernel_tuner/interface.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/kernel_tuner/interface.py b/kernel_tuner/interface.py index 69fd97656..ff8a0763c 100644 --- a/kernel_tuner/interface.py +++ b/kernel_tuner/interface.py @@ -699,7 +699,9 @@ def preprocess_cache(filepath): # create search space tuning_options.restrictions_unmodified = deepcopy(restrictions) - searchspace = Searchspace(tune_params, restrictions, max_threads=runner.dev.max_threads, **searchspace_construction_options) + searchspace = Searchspace( + tune_params, restrictions, max_threads=runner.dev.max_threads, **searchspace_construction_options + ) restrictions = searchspace._modified_restrictions tuning_options.restrictions = restrictions if verbose: @@ -809,6 +811,9 @@ def run_kernel( ) params = [tuple([k, util.possible_julia_vector_to_list(tp)]) for k, tp in params] params = dict(params) + block_size_names = util.possible_julia_vector_to_list(block_size_names) + # ensure there is always at least three names + util.append_default_block_size_names(block_size_names) _check_user_input(kernel_name, kernelsource, arguments, block_size_names) From 36ef75c509387d21ac151e0966ffb5bfd052baa2 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Sat, 4 Apr 2026 09:40:35 +0200 Subject: [PATCH 056/146] Added synchronization detection for Julia arrays, more specific reporting of differences between expected and output arrays --- kernel_tuner/core.py | 60 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/kernel_tuner/core.py b/kernel_tuner/core.py index b196278e3..6e484391a 100644 --- a/kernel_tuner/core.py +++ b/kernel_tuner/core.py @@ -4,6 +4,7 @@ import re import time from collections import namedtuple +from warnings import warn import numpy as np @@ -504,8 +505,6 @@ def benchmark(self, func, gpu_args, instance, verbose, objective, skip_nvml_sett logging.debug("benchmark fails due to runtime failure / too many resources required") if verbose: if "Julia" in str(e): - from warnings import warn - warn( f"skipping config {util.get_instance_string(instance.params)} reason: Julia kernel launch failed because of:\n{e}" ) @@ -526,7 +525,7 @@ def check_kernel_output(self, func, gpu_args, instance, answer, atol, verify, ve # convert juliacall array to numpy array for i, arg in enumerate(instance.arguments): - if isinstance(answer[i], np.ndarray) and "juliacall" in str(type(arg)): + if isinstance(answer[i], np.ndarray) and "ArrayValue" in str(type(arg)): instance.arguments[i] = np.array(arg, dtype=answer[i].dtype) # if not using custom verify function, check if the length is the same @@ -534,10 +533,14 @@ def check_kernel_output(self, func, gpu_args, instance, answer, atol, verify, ve if len(instance.arguments) != len(answer): raise TypeError("The length of argument list and provided results do not match.") - should_sync = [answer[i] is not None for i, arg in enumerate(instance.arguments)] + # for Julia arrays, we always want to sync + should_sync = [ + answer[i] is not None or "ArrayValue" in str(type(arg)) for i, arg in enumerate(instance.arguments) + ] else: should_sync = [ - isinstance(arg, (np.ndarray, cp.ndarray, torch.Tensor, DeviceArray)) for arg in instance.arguments + isinstance(arg, (np.ndarray, cp.ndarray, torch.Tensor, DeviceArray)) or "ArrayValue" in str(type(arg)) + for arg in instance.arguments ] # re-copy original contents of output arguments to GPU memory, to overwrite any changes @@ -901,21 +904,58 @@ def _flatten(a): result = _ravel(result_host[i]) expected = _flatten(expected) if any([isinstance(array, cp.ndarray) for array in [expected, result]]): - output_test = cp.allclose(expected, result, atol=atol) + expected_nan = cp.isnan(expected) + output_test = cp.allclose(expected, result, atol=atol, equal_nan=expected_nan.any()) elif isinstance(expected, torch.Tensor) and isinstance(result, torch.Tensor): - output_test = torch.allclose(expected, result, atol=atol) + expected_nan = torch.isnan(expected) + output_test = torch.allclose(expected, result, atol=atol, equal_nan=expected_nan.any()) else: - output_test = np.allclose(expected, result, atol=atol) + expected_nan = np.isnan(expected) + output_test = np.allclose(expected, result, atol=atol, equal_nan=expected_nan.any()) + if expected_nan.any(): + warn( + f"Answer contains {expected_nan.sum()} NaNs. NaN values will now be considered equal in comparison." + ) if not output_test and verbose: print("Error: " + util.get_config_string(instance.params) + " detected during correctness check") print("this error occurred when checking value of the %oth kernel argument" % (i,)) print("Printing kernel output and expected result, set verbose=False to suppress this debug print") np.set_printoptions(edgeitems=30) - print("Kernel output:") + print(f"Kernel output ({np.shape(result)}):") print(result) - print("Expected:") + print(f"Expected ({np.shape(expected)}):") print(expected) + # check if there are NaNs in the output or expected, if so, print where they are + if any([isinstance(array, cp.ndarray) for array in [expected, result]]): + if cp.isnan(result).any(): + print("NaNs in kernel output at indices:", cp.where(cp.isnan(result))) + if cp.isnan(expected).any(): + print("NaNs in expected result at indices:", cp.where(cp.isnan(expected))) + elif isinstance(expected, torch.Tensor) and isinstance(result, torch.Tensor): + if torch.isnan(result).any(): + print("NaNs in kernel output at indices:", torch.where(torch.isnan(result))) + if torch.isnan(expected).any(): + print("NaNs in expected result at indices:", torch.where(torch.isnan(expected))) + else: + if np.isnan(result).any(): + print("NaNs in kernel output at indices:", np.where(np.isnan(result))) + if np.isnan(expected).any(): + print("NaNs in expected result at indices:", np.where(np.isnan(expected))) + # print only the elements that are different + print("Difference at specific elements:") + if any([isinstance(array, cp.ndarray) for array in [expected, result]]): + diff = cp.abs(expected - result) + indices = cp.where(diff > atol) + print(diff[indices]) + elif isinstance(expected, torch.Tensor) and isinstance(result, torch.Tensor): + diff = torch.abs(expected - result) + indices = torch.where(diff > atol) + print(diff[indices]) + else: + diff = np.abs(expected - result) + indices = np.where(diff > atol) + print(diff[indices]) correct = correct and output_test if not correct: From 0b33f859cf41a20af6c17be6d4b39379e4ef2546 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Sat, 4 Apr 2026 13:43:24 +0200 Subject: [PATCH 057/146] Linting --- kernel_tuner/runners/sequential.py | 40 ++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/kernel_tuner/runners/sequential.py b/kernel_tuner/runners/sequential.py index 5e53093be..bfb49dfe8 100644 --- a/kernel_tuner/runners/sequential.py +++ b/kernel_tuner/runners/sequential.py @@ -1,4 +1,5 @@ """The default runner for sequentially tuning the parameter space.""" + import logging from datetime import datetime, timezone from time import perf_counter @@ -28,7 +29,7 @@ def __init__(self, kernel_source, kernel_options, device_options, iterations, ob each kernel instance. :type iterations: int """ - #detect language and create high-level device interface + # detect language and create high-level device interface self.dev = DeviceInterface(kernel_source, iterations=iterations, observers=observers, **device_options) self.units = self.dev.units @@ -41,7 +42,7 @@ def __init__(self, kernel_source, kernel_options, device_options, iterations, ob self.last_strategy_time = 0 self.kernel_options = kernel_options - #move data to the GPU + # move data to the GPU self.gpu_args = self.dev.ready_argument_list(kernel_options.arguments) def get_environment(self, tuning_options): @@ -62,7 +63,7 @@ def run(self, parameter_space, tuning_options): :rtype: dict()) """ - logging.debug('sequential runner started for ' + self.kernel_options.kernel_name) + logging.debug("sequential runner started for " + self.kernel_options.kernel_name) results = [] @@ -77,33 +78,46 @@ def run(self, parameter_space, tuning_options): x_int = ",".join([str(i) for i in element]) if tuning_options.cache and x_int in tuning_options.cache: params.update(tuning_options.cache[x_int]) - params['compile_time'] = 0 - params['verification_time'] = 0 - params['benchmark_time'] = 0 + params["compile_time"] = 0 + params["verification_time"] = 0 + params["benchmark_time"] = 0 else: # attempt to warmup the GPU by running the first config in the parameter space and ignoring the result if not self.warmed_up: warmup_time = perf_counter() - self.dev.compile_and_benchmark(self.kernel_source, self.gpu_args, params, self.kernel_options, tuning_options) + self.dev.compile_and_benchmark( + self.kernel_source, self.gpu_args, params, self.kernel_options, tuning_options + ) self.warmed_up = True warmup_time = 1e3 * (perf_counter() - warmup_time) - result = self.dev.compile_and_benchmark(self.kernel_source, self.gpu_args, params, self.kernel_options, tuning_options) + result = self.dev.compile_and_benchmark( + self.kernel_source, self.gpu_args, params, self.kernel_options, tuning_options + ) params.update(result) if tuning_options.objective in result and isinstance(result[tuning_options.objective], ErrorConfig): - logging.debug('kernel configuration was skipped silently due to compile or runtime failure') + logging.debug("kernel configuration was skipped silently due to compile or runtime failure") # only compute metrics on configs that have not errored if tuning_options.metrics and not isinstance(params.get(tuning_options.objective), ErrorConfig): params = process_metrics(params, tuning_options.metrics) # get the framework time by estimating based on other times - total_time = 1000 * ((perf_counter() - self.start_time) - warmup_time) - params['strategy_time'] = self.last_strategy_time - params['framework_time'] = max(total_time - (params['compile_time'] + params['verification_time'] + params['benchmark_time'] + params['strategy_time']), 0) - params['timestamp'] = str(datetime.now(timezone.utc)) + total_time = 1000 * ((perf_counter() - self.start_time) - warmup_time) + params["strategy_time"] = self.last_strategy_time + params["framework_time"] = max( + total_time + - ( + params["compile_time"] + + params["verification_time"] + + params["benchmark_time"] + + params["strategy_time"] + ), + 0, + ) + params["timestamp"] = str(datetime.now(timezone.utc)) self.start_time = perf_counter() if result: From 8a028beacd6a88e4c320d6d6f66a4639d1cb4091 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Sat, 4 Apr 2026 13:45:33 +0200 Subject: [PATCH 058/146] Implemented Tunable structure for answers, improved efficiency --- kernel_tuner/backends/julia.py | 1 - kernel_tuner/core.py | 8 +++++++- kernel_tuner/interface.py | 2 -- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index a4b467554..ea0cf5913 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -323,7 +323,6 @@ def synchronize(self): jl.Main.KernelAbstractions.synchronize(self.backend) except JuliaError as e: raise RuntimeError(f"Julia synchronize failed: {e}") - jl.seval("GC.gc()") # trigger GC to free GPU memory after synchronization # ------------------------- # Memory utilities diff --git a/kernel_tuner/core.py b/kernel_tuner/core.py index 6e484391a..2ea683e58 100644 --- a/kernel_tuner/core.py +++ b/kernel_tuner/core.py @@ -523,7 +523,13 @@ def check_kernel_output(self, func, gpu_args, instance, answer, atol, verify, ve """Runs the kernel once and checks the result against answer.""" logging.debug("check_kernel_output") - # convert juliacall array to numpy array + # get the answer for this parameter configuration + if isinstance(answer, Tunable): + answer = answer.select_for_configuration(instance.params) + + # convert juliacall arrays to numpy arrays where necessary + if answer is not None: + answer = [None if a is None else np.array(a) for a in util.possible_julia_vector_to_list(answer)] for i, arg in enumerate(instance.arguments): if isinstance(answer[i], np.ndarray) and "ArrayValue" in str(type(arg)): instance.arguments[i] = np.array(arg, dtype=answer[i].dtype) diff --git a/kernel_tuner/interface.py b/kernel_tuner/interface.py index ff8a0763c..fa1acc3e4 100644 --- a/kernel_tuner/interface.py +++ b/kernel_tuner/interface.py @@ -604,8 +604,6 @@ def tune_kernel( ) tune_params = [tuple([k, util.possible_julia_vector_to_list(tp)]) for k, tp in tune_params] tune_params = dict(tune_params) - if answer is not None: - answer = [None if a is None else numpy.array(a) for a in util.possible_julia_vector_to_list(answer)] restrictions = util.possible_julia_vector_to_list(restrictions) block_size_names = util.possible_julia_vector_to_list(block_size_names) From f349332c8121f5244bd8303146a15edfb99c7e88 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Sat, 4 Apr 2026 19:38:52 +0200 Subject: [PATCH 059/146] Implemented nested Tunable data type --- kernel_tuner/accuracy.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/kernel_tuner/accuracy.py b/kernel_tuner/accuracy.py index 815593268..e1bfc8882 100644 --- a/kernel_tuner/accuracy.py +++ b/kernel_tuner/accuracy.py @@ -55,7 +55,12 @@ def select_for_configuration(self, params): list = ", ".join(map(str, self.data.keys())) raise KeyError(f"'{option}' is not a valid parameter value, should be one of: {list}") - return self.data[option] + # continue recursively until we find a non-Tunable + el = self.data[option] + if isinstance(el, Tunable): + return el.select_for_configuration(params) + else: + return el def __call__(self, params): # noqa: D102 return self.select_for_configuration(params) From f2ac100d8de70a346c7b692265da6969955340f4 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Sun, 5 Apr 2026 01:07:36 +0200 Subject: [PATCH 060/146] Added globals for temporary arrays --- kernel_tuner/backends/julia.py | 1 + 1 file changed, 1 insertion(+) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index ea0cf5913..460710d79 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -88,6 +88,7 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None jl.seval( f""" + global dest_tmp, src_tmp # for memcpy_htod module KernelTunerHelper using {self.backend_mod_name} const kt_julia_backend = {self.backend_mod_name}Backend() From 31d03909d8cb57efe58ca8edf11dc626c073cef1 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Wed, 8 Apr 2026 16:23:19 +0200 Subject: [PATCH 061/146] Improved timings on AMD --- kernel_tuner/backends/julia.py | 16 ++++++++++------ kernel_tuner/observers/julia.py | 11 ++++++++++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 460710d79..56ccd45e2 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -169,8 +169,8 @@ def initialize_backend(self, device, backend_name): self.backend_device = self.backend_mod.device() if backend_name == "CUDA": self.contextqueue = self.backend_mod.context - elif backend_name == "AMD": - self.contextqueue = self.backend_mod.queue + # elif backend_name == "AMD": + # self.contextqueue = self.backend_mod.queue # elif backend_name == "INTEL": # self.contextqueue = jl.seval( # f"ZeCommandQueue(ZeContext(first(drivers())), devices(first(drivers()))[{int(device) + 1}]))" @@ -190,8 +190,8 @@ def initialize_backend(self, device, backend_name): self.end_evt = backend_mod.CuEvent self.stream = backend_mod.stream() elif backend_name == "AMD": - self.start_evt = backend_mod.ROCEvent - self.end_evt = backend_mod.ROCEvent + self.start_evt = backend_mod.HIPEvent + self.end_evt = backend_mod.HIPEvent self.stream = backend_mod.default_stream() elif backend_name == "INTEL": # OneAPI: no events available @@ -294,8 +294,10 @@ def run_kernel(self, func, gpu_args, threads, grid, stream=None, params=None): def start_event(self): """Records the event that marks the start of a measurement.""" - if self.backend_mod_name in ("CUDA", "AMDGPU"): + if self.backend_mod_name == "CUDA": self.backend_mod.record(self.start_evt(), self.stream) + elif self.backend_mod_name == "AMDGPU": + self.backend_mod.record(self.start_evt()) elif self.backend_mod_name == "Metal": # Because our kernel launch happens via Kernel Abstractions, we wrap our kernel between two command buffers. # Normally you would just use one command buffer for the actual kernel. @@ -306,8 +308,10 @@ def start_event(self): def stop_event(self): """Records the event that marks the end of a measurement.""" - if self.backend_mod_name in ("CUDA", "AMDGPU"): + if self.backend_mod_name == "CUDA": self.backend_mod.record(self.end_evt(), self.stream) + elif self.backend_mod_name == "AMDGPU": + self.backend_mod.record(self.end_evt()) elif self.backend_mod_name == "Metal": jl.end_buf = self.create_metal_buffer() jl.seval("Metal.commit!(end_buf)") diff --git a/kernel_tuner/observers/julia.py b/kernel_tuner/observers/julia.py index 2f7163de8..ce3a3c697 100644 --- a/kernel_tuner/observers/julia.py +++ b/kernel_tuner/observers/julia.py @@ -36,16 +36,22 @@ def __init__( self.times = [] self.t0 = None - if self.name in ("cuda", "amdgpu"): + if self.name == "cuda": # initialize events for this instance of the observer self.start = self.start() self.end = self.end() self.stream = backend_mod.stream() + elif self.name == "amdgpu": + self.stream = backend_mod.default_stream() + self.start = self.start(self.stream, timing=True) + self.end = self.end(self.stream, timing=True) def before_start(self): if self.start is not None: if self.name == "metal": self.t0 = self.start() + elif self.name == "amdgpu": + self.backend_mod.record(self.start) else: self.backend_mod.record(self.start, self.stream) else: @@ -56,6 +62,9 @@ def after_finish(self): if self.end is not None: if self.name == "metal": ms = float((self.end() - self.t0) * 1000.0) + elif self.name == "amdgpu": + self.backend_mod.record(self.start) + ms = float(self.backend_mod.elapsed(self.start, self.end) * 1000.0) else: self.backend_mod.synchronize(self.end) self.backend_mod.record(self.end, self.stream) From a85cd968e598b6ef15920b493dfb40177e6f3b97 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Wed, 8 Apr 2026 16:33:39 +0200 Subject: [PATCH 062/146] Improved timings on AMD --- kernel_tuner/backends/julia.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 56ccd45e2..5baf8d3af 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -190,8 +190,8 @@ def initialize_backend(self, device, backend_name): self.end_evt = backend_mod.CuEvent self.stream = backend_mod.stream() elif backend_name == "AMD": - self.start_evt = backend_mod.HIPEvent - self.end_evt = backend_mod.HIPEvent + self.start_evt = backend_mod.HIP.HIPEvent + self.end_evt = backend_mod.HIP.HIPEvent self.stream = backend_mod.default_stream() elif backend_name == "INTEL": # OneAPI: no events available From 62fba7b578f7c3897815fc345668e79fae349726 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Wed, 8 Apr 2026 16:41:36 +0200 Subject: [PATCH 063/146] Improved timings on AMD --- kernel_tuner/backends/julia.py | 6 +++--- kernel_tuner/backends/julia_helper.py | 10 +++++----- kernel_tuner/core.py | 2 +- kernel_tuner/observers/julia.py | 8 ++++---- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 5baf8d3af..8dc20e458 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -4,7 +4,7 @@ Requirements: pip install juliacall - and in Julia: ] add CUDA / AMDGPU / oneAPI / Metal (will be automatically installed if not present) + and in Julia: ] add CUDA / ROCBackend / oneAPI / Metal (will be automatically installed if not present) Notes: - The kernel string should contain a valid Julia GPU kernel function definition. @@ -296,7 +296,7 @@ def start_event(self): """Records the event that marks the start of a measurement.""" if self.backend_mod_name == "CUDA": self.backend_mod.record(self.start_evt(), self.stream) - elif self.backend_mod_name == "AMDGPU": + elif self.backend_mod_name == "ROCBackend": self.backend_mod.record(self.start_evt()) elif self.backend_mod_name == "Metal": # Because our kernel launch happens via Kernel Abstractions, we wrap our kernel between two command buffers. @@ -310,7 +310,7 @@ def stop_event(self): """Records the event that marks the end of a measurement.""" if self.backend_mod_name == "CUDA": self.backend_mod.record(self.end_evt(), self.stream) - elif self.backend_mod_name == "AMDGPU": + elif self.backend_mod_name == "ROCBackend": self.backend_mod.record(self.end_evt()) elif self.backend_mod_name == "Metal": jl.end_buf = self.create_metal_buffer() diff --git a/kernel_tuner/backends/julia_helper.py b/kernel_tuner/backends/julia_helper.py index e4b52c4ce..499d18ac8 100644 --- a/kernel_tuner/backends/julia_helper.py +++ b/kernel_tuner/backends/julia_helper.py @@ -21,11 +21,11 @@ "GPUArrayType": "CuArray", }, "AMD": { - "pkg": "AMDGPU", - "module": "AMDGPU", - "device_select": lambda d: f"AMDGPU.device!(AMDGPU.devices()[{d}])", - "name": "AMDGPU.name(AMDGPU.device())", - "max_threads": "AMDGPU.device_attribute(AMDGPU.device(), :maxthreadsperblock)", + "pkg": "ROCBackend", + "module": "ROCBackend", + "device_select": lambda d: f"ROCBackend.device!(ROCBackend.devices()[{d}])", + "name": "ROCBackend.name(ROCBackend.device())", + "max_threads": "ROCBackend.device_attribute(ROCBackend.device(), :maxthreadsperblock)", "capability": None, "GPUArrayType": "ROCArray", }, diff --git a/kernel_tuner/core.py b/kernel_tuner/core.py index 2ea683e58..36aabcde4 100644 --- a/kernel_tuner/core.py +++ b/kernel_tuner/core.py @@ -231,7 +231,7 @@ def infer_julia_backend(self): kernel_string = self.get_kernel_string(0) if kernel_string.find("using CUDA") != -1: return "cuda" - elif kernel_string.find("using AMDGPU") != -1: + elif kernel_string.find("using ROCBackend") != -1: return "amd" elif kernel_string.find("using oneAPI") != -1: return "intel" diff --git a/kernel_tuner/observers/julia.py b/kernel_tuner/observers/julia.py index ce3a3c697..a14f354d0 100644 --- a/kernel_tuner/observers/julia.py +++ b/kernel_tuner/observers/julia.py @@ -10,7 +10,7 @@ class JuliaRuntimeObserver(BenchmarkObserver): """Cross-backend GPU timing for KernelAbstractions. - CUDA: CuEvent timing - - AMDGPU: ROCEvent timing + - ROCBackend: ROCEvent timing - OneAPI: host timing + synchronize (less accurate, no events available) - Metal: host timing + synchronize """ @@ -41,7 +41,7 @@ def __init__( self.start = self.start() self.end = self.end() self.stream = backend_mod.stream() - elif self.name == "amdgpu": + elif self.name == "rocbackend": self.stream = backend_mod.default_stream() self.start = self.start(self.stream, timing=True) self.end = self.end(self.stream, timing=True) @@ -50,7 +50,7 @@ def before_start(self): if self.start is not None: if self.name == "metal": self.t0 = self.start() - elif self.name == "amdgpu": + elif self.name == "rocbackend": self.backend_mod.record(self.start) else: self.backend_mod.record(self.start, self.stream) @@ -62,7 +62,7 @@ def after_finish(self): if self.end is not None: if self.name == "metal": ms = float((self.end() - self.t0) * 1000.0) - elif self.name == "amdgpu": + elif self.name == "rocbackend": self.backend_mod.record(self.start) ms = float(self.backend_mod.elapsed(self.start, self.end) * 1000.0) else: From 3250aa38cb077bc0627449d4a98cd6e58805c6bf Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Wed, 8 Apr 2026 16:46:15 +0200 Subject: [PATCH 064/146] Updated AMD backend name --- kernel_tuner/backends/julia_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel_tuner/backends/julia_helper.py b/kernel_tuner/backends/julia_helper.py index 499d18ac8..524c51310 100644 --- a/kernel_tuner/backends/julia_helper.py +++ b/kernel_tuner/backends/julia_helper.py @@ -21,7 +21,7 @@ "GPUArrayType": "CuArray", }, "AMD": { - "pkg": "ROCBackend", + "pkg": "AMDGPU", "module": "ROCBackend", "device_select": lambda d: f"ROCBackend.device!(ROCBackend.devices()[{d}])", "name": "ROCBackend.name(ROCBackend.device())", From f0b5b56486a0122998c2ce921380cefa8216264f Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Wed, 8 Apr 2026 16:54:50 +0200 Subject: [PATCH 065/146] Updated AMD backend name --- kernel_tuner/backends/julia.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 8dc20e458..94da6e749 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -125,9 +125,9 @@ def initialize_backend(self, device, backend_name): # Bring module into Python self.backend_mod_name = info["module"] + jl.seval(f"using KernelAbstractions, {info['pkg']}") backend_mod = getattr(jl.Main, self.backend_mod_name) self.backend_mod = backend_mod - jl.seval(f"using KernelAbstractions, {self.backend_mod_name}") jl.seval(f"tmp_arr = {info['GPUArrayType']}(Float32.(zeros(2)))") self.backend = jl.seval("KernelAbstractions.get_backend(tmp_arr)") self.GPUArrayType = info["GPUArrayType"] From 22a51f12b93c0ca4081900371b81f54efe138ad0 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Wed, 8 Apr 2026 17:08:12 +0200 Subject: [PATCH 066/146] Updated AMD backend name --- kernel_tuner/backends/julia_helper.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/kernel_tuner/backends/julia_helper.py b/kernel_tuner/backends/julia_helper.py index 524c51310..0427444c5 100644 --- a/kernel_tuner/backends/julia_helper.py +++ b/kernel_tuner/backends/julia_helper.py @@ -23,9 +23,9 @@ "AMD": { "pkg": "AMDGPU", "module": "ROCBackend", - "device_select": lambda d: f"ROCBackend.device!(ROCBackend.devices()[{d}])", - "name": "ROCBackend.name(ROCBackend.device())", - "max_threads": "ROCBackend.device_attribute(ROCBackend.device(), :maxthreadsperblock)", + "device_select": lambda d: f"AMDGPU.device!(AMDGPU.devices()[{d}])", + "name": "HIP.name(HIP.device())", + "max_threads": "HIP.attribute(dev, HIP.hipDeviceAttributeMaxThreadsPerBlock)", "capability": None, "GPUArrayType": "ROCArray", }, From b258bd0e28808619502a53a7d7118f8cc1d5d1ce Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Wed, 8 Apr 2026 17:16:54 +0200 Subject: [PATCH 067/146] Updated AMD backend name --- kernel_tuner/backends/julia.py | 4 ++-- kernel_tuner/backends/julia_helper.py | 2 +- kernel_tuner/observers/julia.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 94da6e749..8389b9026 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -296,7 +296,7 @@ def start_event(self): """Records the event that marks the start of a measurement.""" if self.backend_mod_name == "CUDA": self.backend_mod.record(self.start_evt(), self.stream) - elif self.backend_mod_name == "ROCBackend": + elif self.backend_mod_name == "AMDGPU": self.backend_mod.record(self.start_evt()) elif self.backend_mod_name == "Metal": # Because our kernel launch happens via Kernel Abstractions, we wrap our kernel between two command buffers. @@ -310,7 +310,7 @@ def stop_event(self): """Records the event that marks the end of a measurement.""" if self.backend_mod_name == "CUDA": self.backend_mod.record(self.end_evt(), self.stream) - elif self.backend_mod_name == "ROCBackend": + elif self.backend_mod_name == "AMDGPU": self.backend_mod.record(self.end_evt()) elif self.backend_mod_name == "Metal": jl.end_buf = self.create_metal_buffer() diff --git a/kernel_tuner/backends/julia_helper.py b/kernel_tuner/backends/julia_helper.py index 0427444c5..255ab5bc2 100644 --- a/kernel_tuner/backends/julia_helper.py +++ b/kernel_tuner/backends/julia_helper.py @@ -22,7 +22,7 @@ }, "AMD": { "pkg": "AMDGPU", - "module": "ROCBackend", + "module": "AMDGPU", "device_select": lambda d: f"AMDGPU.device!(AMDGPU.devices()[{d}])", "name": "HIP.name(HIP.device())", "max_threads": "HIP.attribute(dev, HIP.hipDeviceAttributeMaxThreadsPerBlock)", diff --git a/kernel_tuner/observers/julia.py b/kernel_tuner/observers/julia.py index a14f354d0..ef83f0b6b 100644 --- a/kernel_tuner/observers/julia.py +++ b/kernel_tuner/observers/julia.py @@ -41,7 +41,7 @@ def __init__( self.start = self.start() self.end = self.end() self.stream = backend_mod.stream() - elif self.name == "rocbackend": + elif self.name == "amdgpu": self.stream = backend_mod.default_stream() self.start = self.start(self.stream, timing=True) self.end = self.end(self.stream, timing=True) @@ -50,7 +50,7 @@ def before_start(self): if self.start is not None: if self.name == "metal": self.t0 = self.start() - elif self.name == "rocbackend": + elif self.name == "amdgpu": self.backend_mod.record(self.start) else: self.backend_mod.record(self.start, self.stream) @@ -62,7 +62,7 @@ def after_finish(self): if self.end is not None: if self.name == "metal": ms = float((self.end() - self.t0) * 1000.0) - elif self.name == "rocbackend": + elif self.name == "amdgpu": self.backend_mod.record(self.start) ms = float(self.backend_mod.elapsed(self.start, self.end) * 1000.0) else: From 3db1c8f72583df649ed57b9c620153681e487bf2 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Wed, 8 Apr 2026 17:22:38 +0200 Subject: [PATCH 068/146] Added module backend string specification --- kernel_tuner/backends/julia.py | 3 ++- kernel_tuner/backends/julia_helper.py | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 8389b9026..1b6ecfc50 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -91,7 +91,7 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None global dest_tmp, src_tmp # for memcpy_htod module KernelTunerHelper using {self.backend_mod_name} - const kt_julia_backend = {self.backend_mod_name}Backend() + const kt_julia_backend = {self.backend_mod_instname}() const GPUArrayType = {self.GPUArrayType} include("{str(Path(__file__).parent / "julia_helper.jl")}") end @@ -125,6 +125,7 @@ def initialize_backend(self, device, backend_name): # Bring module into Python self.backend_mod_name = info["module"] + self.backend_mod_instname = info["module_backend"] jl.seval(f"using KernelAbstractions, {info['pkg']}") backend_mod = getattr(jl.Main, self.backend_mod_name) self.backend_mod = backend_mod diff --git a/kernel_tuner/backends/julia_helper.py b/kernel_tuner/backends/julia_helper.py index 255ab5bc2..d3ac9dae9 100644 --- a/kernel_tuner/backends/julia_helper.py +++ b/kernel_tuner/backends/julia_helper.py @@ -14,6 +14,7 @@ "CUDA": { "pkg": "CUDA", "module": "CUDA", + "module_backend": "CUDABackend", "device_select": lambda d: f"CUDA.device!({d})", "name": "CUDA.name(CUDA.device())", "max_threads": "CUDA.attribute(CUDA.device(), CUDA.DEVICE_ATTRIBUTE_MAX_THREADS_PER_BLOCK)", @@ -23,6 +24,7 @@ "AMD": { "pkg": "AMDGPU", "module": "AMDGPU", + "module_backend": "ROCBackend", "device_select": lambda d: f"AMDGPU.device!(AMDGPU.devices()[{d}])", "name": "HIP.name(HIP.device())", "max_threads": "HIP.attribute(dev, HIP.hipDeviceAttributeMaxThreadsPerBlock)", @@ -32,6 +34,7 @@ "INTEL": { "pkg": "oneAPI", "module": "oneAPI", + "module_backend": "oneAPIBackend", "device_select": lambda d: f"devices(first(drivers()))[{d}]", "name": "oneAPI.name(oneAPI.device())", "max_threads": "oneAPI.compute_properties(oneAPI.device()).maxTotalGroupSize", @@ -41,6 +44,7 @@ "METAL": { "pkg": "Metal", "module": "Metal", + "module_backend": "MetalBackend", "device_select": lambda d: "Metal.device!(Metal.device())", # only single device support in Metal.jl "name": "Metal.name(Metal.device())", "max_threads": "Int(Metal.device().maxThreadsPerThreadgroup.width)", From aef9aec244db3ea467500783bc952d8a7c7c71a1 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Wed, 8 Apr 2026 17:28:45 +0200 Subject: [PATCH 069/146] Adjusted AMDGPU specifications retrieval --- kernel_tuner/backends/julia_helper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kernel_tuner/backends/julia_helper.py b/kernel_tuner/backends/julia_helper.py index d3ac9dae9..a39a082be 100644 --- a/kernel_tuner/backends/julia_helper.py +++ b/kernel_tuner/backends/julia_helper.py @@ -26,8 +26,8 @@ "module": "AMDGPU", "module_backend": "ROCBackend", "device_select": lambda d: f"AMDGPU.device!(AMDGPU.devices()[{d}])", - "name": "HIP.name(HIP.device())", - "max_threads": "HIP.attribute(dev, HIP.hipDeviceAttributeMaxThreadsPerBlock)", + "name": "AMDGPU.HIP.name(AMDGPU.HIP.device())", + "max_threads": "AMDGPU.HIP.attribute(AMDGPU.HIP.device(), AMDGPU.HIP.hipDeviceAttributeMaxThreadsPerBlock)", "capability": None, "GPUArrayType": "ROCArray", }, From d4824ce3343769a8c8f8bed77182e0eabbfa8dba Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Wed, 8 Apr 2026 17:32:37 +0200 Subject: [PATCH 070/146] Adjusted AMD timings --- kernel_tuner/observers/julia.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel_tuner/observers/julia.py b/kernel_tuner/observers/julia.py index ef83f0b6b..3157ad1bb 100644 --- a/kernel_tuner/observers/julia.py +++ b/kernel_tuner/observers/julia.py @@ -63,7 +63,7 @@ def after_finish(self): if self.name == "metal": ms = float((self.end() - self.t0) * 1000.0) elif self.name == "amdgpu": - self.backend_mod.record(self.start) + self.backend_mod.record(self.end) ms = float(self.backend_mod.elapsed(self.start, self.end) * 1000.0) else: self.backend_mod.synchronize(self.end) From 6a2026a4b243f988d61fc602bfef34dba1a8f218 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Wed, 8 Apr 2026 17:55:53 +0200 Subject: [PATCH 071/146] Updated AMD event recording --- kernel_tuner/backends/julia.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 1b6ecfc50..508b66ff9 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -298,7 +298,7 @@ def start_event(self): if self.backend_mod_name == "CUDA": self.backend_mod.record(self.start_evt(), self.stream) elif self.backend_mod_name == "AMDGPU": - self.backend_mod.record(self.start_evt()) + self.backend_mod.HIP.record(self.start_evt()) elif self.backend_mod_name == "Metal": # Because our kernel launch happens via Kernel Abstractions, we wrap our kernel between two command buffers. # Normally you would just use one command buffer for the actual kernel. @@ -312,7 +312,7 @@ def stop_event(self): if self.backend_mod_name == "CUDA": self.backend_mod.record(self.end_evt(), self.stream) elif self.backend_mod_name == "AMDGPU": - self.backend_mod.record(self.end_evt()) + self.backend_mod.HIP.record(self.end_evt()) elif self.backend_mod_name == "Metal": jl.end_buf = self.create_metal_buffer() jl.seval("Metal.commit!(end_buf)") From 66f5c7ae929ea3a7abebc2ac21909906698e79e1 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Wed, 8 Apr 2026 19:54:22 +0200 Subject: [PATCH 072/146] Updated AMD event record --- kernel_tuner/observers/julia.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/kernel_tuner/observers/julia.py b/kernel_tuner/observers/julia.py index 3157ad1bb..9a3a450cb 100644 --- a/kernel_tuner/observers/julia.py +++ b/kernel_tuner/observers/julia.py @@ -51,7 +51,7 @@ def before_start(self): if self.name == "metal": self.t0 = self.start() elif self.name == "amdgpu": - self.backend_mod.record(self.start) + self.backend_mod.HIP.record(self.start) else: self.backend_mod.record(self.start, self.stream) else: @@ -63,8 +63,8 @@ def after_finish(self): if self.name == "metal": ms = float((self.end() - self.t0) * 1000.0) elif self.name == "amdgpu": - self.backend_mod.record(self.end) - ms = float(self.backend_mod.elapsed(self.start, self.end) * 1000.0) + self.backend_mod.HIP.record(self.end) + ms = float(self.backend_mod.HIP.elapsed(self.start, self.end) * 1000.0) else: self.backend_mod.synchronize(self.end) self.backend_mod.record(self.end, self.stream) From 2c79c43d2a615dd90a5c61ad722827cf395f40a2 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Wed, 8 Apr 2026 23:05:20 +0200 Subject: [PATCH 073/146] HIP event creation --- kernel_tuner/backends/julia.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 1b6ecfc50..240a02856 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -187,13 +187,13 @@ def initialize_backend(self, device, backend_name): # Set up stream and event attributes for observers if backend_name == "CUDA": + self.stream = backend_mod.stream() self.start_evt = backend_mod.CuEvent self.end_evt = backend_mod.CuEvent - self.stream = backend_mod.stream() elif backend_name == "AMD": - self.start_evt = backend_mod.HIP.HIPEvent - self.end_evt = backend_mod.HIP.HIPEvent self.stream = backend_mod.default_stream() + self.start_evt = self.create_hip_event + self.end_evt = self.create_hip_event elif backend_name == "INTEL": # OneAPI: no events available self.start_evt = None @@ -395,6 +395,9 @@ def check_package_and_install(self, package): f'{package}.jl not found in your Julia environment. Run `using Pkg; Pkg.add("{package}")` in Julia.' ) from e + def create_hip_event(self): + return self.backend_mod.HIP.HIPEvent(self.stream(); do_record=false, timing=true) + def create_metal_buffer(self): """Create a Metal buffer in the command queue.""" try: From dedc822ad4c3b914655e99dd8234adc2748963cd Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Wed, 8 Apr 2026 23:31:39 +0200 Subject: [PATCH 074/146] Improved event handling for AMD --- kernel_tuner/backends/julia.py | 6 +++--- kernel_tuner/observers/julia.py | 15 +++++++++------ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 6779acf68..004f925a7 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -298,7 +298,7 @@ def start_event(self): if self.backend_mod_name == "CUDA": self.backend_mod.record(self.start_evt(), self.stream) elif self.backend_mod_name == "AMDGPU": - self.backend_mod.HIP.record(self.start_evt()) + self.backend_mod.HIP.record(self.start_evt(self.stream, timing=True)) elif self.backend_mod_name == "Metal": # Because our kernel launch happens via Kernel Abstractions, we wrap our kernel between two command buffers. # Normally you would just use one command buffer for the actual kernel. @@ -312,7 +312,7 @@ def stop_event(self): if self.backend_mod_name == "CUDA": self.backend_mod.record(self.end_evt(), self.stream) elif self.backend_mod_name == "AMDGPU": - self.backend_mod.HIP.record(self.end_evt()) + self.backend_mod.HIP.record(self.end_evt(self.stream, timing=True)) elif self.backend_mod_name == "Metal": jl.end_buf = self.create_metal_buffer() jl.seval("Metal.commit!(end_buf)") @@ -396,7 +396,7 @@ def check_package_and_install(self, package): ) from e def create_hip_event(self): - return self.backend_mod.HIP.HIPEvent(self.stream(); do_record=false, timing=true) + return self.backend_mod.HIP.HIPEvent(self.stream(); timing=true) def create_metal_buffer(self): """Create a Metal buffer in the command queue.""" diff --git a/kernel_tuner/observers/julia.py b/kernel_tuner/observers/julia.py index 9a3a450cb..241b9501c 100644 --- a/kernel_tuner/observers/julia.py +++ b/kernel_tuner/observers/julia.py @@ -43,28 +43,28 @@ def __init__( self.stream = backend_mod.stream() elif self.name == "amdgpu": self.stream = backend_mod.default_stream() - self.start = self.start(self.stream, timing=True) - self.end = self.end(self.stream, timing=True) + # self.start = self.start(self.stream, timing=True) + # self.end = self.end(self.stream, timing=True) def before_start(self): if self.start is not None: if self.name == "metal": self.t0 = self.start() elif self.name == "amdgpu": - self.backend_mod.HIP.record(self.start) + self.t0 = self.backend_mod.HIP.record(self.create_hip_event()) else: self.backend_mod.record(self.start, self.stream) else: # fallback: host-side timestamp self.t0 = perf_counter() - + def after_finish(self): if self.end is not None: if self.name == "metal": ms = float((self.end() - self.t0) * 1000.0) elif self.name == "amdgpu": - self.backend_mod.HIP.record(self.end) - ms = float(self.backend_mod.HIP.elapsed(self.start, self.end) * 1000.0) + t1 = self.backend_mod.HIP.record(self.create_hip_event()) + ms = float(self.backend_mod.HIP.elapsed(self.t0, t1) * 1000.0) else: self.backend_mod.synchronize(self.end) self.backend_mod.record(self.end, self.stream) @@ -86,6 +86,9 @@ def get_results(self): self.times = [] return results + def create_hip_event(self): + return self.backend_mod.HIP.HIPEvent(self.stream; timing=true) + class JuliaJITWarmup(PrologueObserver): """Prologue observer to enforce warmup before every configuration to trigger JIT.""" From ac32109cd3c1b75a059707a6dcf0ba4be72ac0b3 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Thu, 9 Apr 2026 10:58:36 +0200 Subject: [PATCH 075/146] Much improved host-side kernel timing, Metal device name retrieval --- kernel_tuner/backends/julia.py | 13 ++++++----- kernel_tuner/backends/julia_helper.jl | 10 ++++++--- kernel_tuner/backends/julia_helper.py | 2 +- kernel_tuner/observers/julia.py | 32 ++++++++++++++++++--------- 4 files changed, 37 insertions(+), 20 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 004f925a7..87a75a34d 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -68,6 +68,7 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None self.backend = None self.start_evt = None self.end_evt = None + self.host_time = None self.initialize_backend(device, backend_name=backend_name) # setup observers @@ -75,6 +76,7 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None self.observers.append( JuliaRuntimeObserver( jl.Main.KernelAbstractions, + self, self.backend, self.backend_mod, self.backend_mod_name, @@ -145,7 +147,7 @@ def initialize_backend(self, device, backend_name): # Query device name try: - self.name = jl.seval(info["name"]) + self.name = str(jl.seval(info["name"])) except JuliaError: self.name = f"{backend_name}-device-{device}" @@ -238,6 +240,7 @@ def compile(self, kernel_instance): kernel_code = kernel_instance.kernel_string kernel_name = kernel_instance.name self.kernel_source = kernel_instance.kernel_source + self.host_time = None # reset host time for this kernel instance # Extract all 'using' statements and check for required packages uses = [] @@ -289,7 +292,7 @@ def run_kernel(self, func, gpu_args, threads, grid, stream=None, params=None): # run the kernel try: - self.launch_kernel(func, args_tuple, params, ndrange, workgroupsize, int(self.smem_size)) + self.host_time = self.launch_kernel(func, args_tuple, params, ndrange, workgroupsize, int(self.smem_size)) except JuliaError as e: raise SkippableFailure(f"Julia kernel launch failed for {params=}: {e}") @@ -305,7 +308,7 @@ def start_event(self): jl.start_buf = self.create_metal_buffer() jl.seval("Metal.commit!(start_buf)") self.backend_mod.wait_completed(jl.start_buf) - return float(jl.start_buf.GPUEndTime) # or kernelEndTime? + return float(jl.start_buf.GPUEndTime) def stop_event(self): """Records the event that marks the end of a measurement.""" @@ -317,7 +320,7 @@ def stop_event(self): jl.end_buf = self.create_metal_buffer() jl.seval("Metal.commit!(end_buf)") self.backend_mod.wait_completed(jl.end_buf) - return float(jl.end_buf.GPUEndTime) # or kernelStartTime? + return float(jl.end_buf.GPUStartTime) def kernel_finished(self): """Returns True if the kernel has finished, False otherwise.""" @@ -396,7 +399,7 @@ def check_package_and_install(self, package): ) from e def create_hip_event(self): - return self.backend_mod.HIP.HIPEvent(self.stream(); timing=true) + return self.backend_mod.HIP.HIPEvent(self.stream, timing=True) def create_metal_buffer(self): """Create a Metal buffer in the command queue.""" diff --git a/kernel_tuner/backends/julia_helper.jl b/kernel_tuner/backends/julia_helper.jl index d1fe2ade6..17e9edbac 100644 --- a/kernel_tuner/backends/julia_helper.jl +++ b/kernel_tuner/backends/julia_helper.jl @@ -9,6 +9,7 @@ function to_gpuarray(a) end function launch_kernel(kernel, args::Tuple, params::Tuple, ndrange::Tuple, workgroupsize::Tuple, shmem::Int) + t = Inf # Check if this is a KernelAbstractions kernel if isdefined(Main, :KernelAbstractions) && kt_julia_backend !== nothing && applicable(kernel, kt_julia_backend, workgroupsize) configured_kernel = kernel(kt_julia_backend, workgroupsize) @@ -18,9 +19,11 @@ function launch_kernel(kernel, args::Tuple, params::Tuple, ndrange::Tuple, workg # kernel errors are printed to stdout, capture them redirect_stdout(tmpio) do try - configured_kernel(args..., Val.(params)..., ndrange=ndrange) - # Synchronize to ensure kernel completion - Main.KernelAbstractions.synchronize(kt_julia_backend) + val_params = Val.(params) # convert parameters to Val types for kernel invocation + start = time_ns() # simple host-side timing as fallback in case of issues with GPU timing + configured_kernel(args..., val_params...; ndrange=ndrange) # launch the kernel + Main.KernelAbstractions.synchronize(kt_julia_backend) # synchronize to ensure kernel completion + t = float((time_ns() - start) / 1e6) # convert to milliseconds catch e redirect_stdout(stdout) # restore stdout close(tmpio) @@ -37,4 +40,5 @@ function launch_kernel(kernel, args::Tuple, params::Tuple, ndrange::Tuple, workg else error("Only KernelAbstractions kernels are supported.") end + return t end diff --git a/kernel_tuner/backends/julia_helper.py b/kernel_tuner/backends/julia_helper.py index a39a082be..d68598e4c 100644 --- a/kernel_tuner/backends/julia_helper.py +++ b/kernel_tuner/backends/julia_helper.py @@ -46,7 +46,7 @@ "module": "Metal", "module_backend": "MetalBackend", "device_select": lambda d: "Metal.device!(Metal.device())", # only single device support in Metal.jl - "name": "Metal.name(Metal.device())", + "name": "Metal.device().name", "max_threads": "Int(Metal.device().maxThreadsPerThreadgroup.width)", "capability": None, "GPUArrayType": "MtlArray", diff --git a/kernel_tuner/observers/julia.py b/kernel_tuner/observers/julia.py index 241b9501c..e24950d00 100644 --- a/kernel_tuner/observers/julia.py +++ b/kernel_tuner/observers/julia.py @@ -10,7 +10,7 @@ class JuliaRuntimeObserver(BenchmarkObserver): """Cross-backend GPU timing for KernelAbstractions. - CUDA: CuEvent timing - - ROCBackend: ROCEvent timing + - ROCBackend: HIPEvent timing - OneAPI: host timing + synchronize (less accurate, no events available) - Metal: host timing + synchronize """ @@ -18,18 +18,20 @@ class JuliaRuntimeObserver(BenchmarkObserver): def __init__( self, kernelabstractions, - backend, - backend_mod, - backend_name, + kt_backend, + jl_backend, + jl_backend_mod, + jl_backend_name, stream=None, start_event=None, end_event=None, ): """Observer that measures GPU time depending on the Julia backend used.""" self.kernelabstractions = kernelabstractions - self.backend = backend - self.backend_mod = backend_mod - self.name = backend_name.lower() + self.kt_backend = kt_backend + self.backend = jl_backend + self.backend_mod = jl_backend_mod + self.name = jl_backend_name.lower() self.stream = stream self.start = start_event self.end = end_event @@ -40,9 +42,9 @@ def __init__( # initialize events for this instance of the observer self.start = self.start() self.end = self.end() - self.stream = backend_mod.stream() + self.stream = self.backend_mod.stream() elif self.name == "amdgpu": - self.stream = backend_mod.default_stream() + self.stream = self.backend_mod.default_stream() # self.start = self.start(self.stream, timing=True) # self.end = self.end(self.stream, timing=True) @@ -57,7 +59,7 @@ def before_start(self): else: # fallback: host-side timestamp self.t0 = perf_counter() - + def after_finish(self): if self.end is not None: if self.name == "metal": @@ -76,6 +78,14 @@ def after_finish(self): ms = dt * 1000.0 warn(f"Using host-side timing for Julia {self.name} backend; results may be less accurate.") + if ms > self.kt_backend.host_time: + if ms > 1.25 * self.kt_backend.host_time: + warn( + f"Measured GPU time {ms:.3f} ms is substantially greater than host time {self.kt_backend.host_time:.3f} ms; " + "this may indicate an issue with the timing measurement. Using host time instead." + ) + ms = self.kt_backend.host_time + self.times.append(ms) def get_results(self): @@ -87,7 +97,7 @@ def get_results(self): return results def create_hip_event(self): - return self.backend_mod.HIP.HIPEvent(self.stream; timing=true) + return self.backend_mod.HIP.HIPEvent(self.stream, timing=True) class JuliaJITWarmup(PrologueObserver): From 9a1714a1be50a04460e97c72e1e74c7b1cae7523 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Thu, 9 Apr 2026 11:24:18 +0200 Subject: [PATCH 076/146] Improved CUDA and AMD GPU timings --- kernel_tuner/backends/julia.py | 7 ++----- kernel_tuner/observers/julia.py | 19 +++++++------------ 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 87a75a34d..fc1fb7736 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -194,8 +194,8 @@ def initialize_backend(self, device, backend_name): self.end_evt = backend_mod.CuEvent elif backend_name == "AMD": self.stream = backend_mod.default_stream() - self.start_evt = self.create_hip_event - self.end_evt = self.create_hip_event + self.start_evt = backend_mod.HIP.HIPEvent + self.end_evt = backend_mod.HIP.HIPEvent elif backend_name == "INTEL": # OneAPI: no events available self.start_evt = None @@ -398,9 +398,6 @@ def check_package_and_install(self, package): f'{package}.jl not found in your Julia environment. Run `using Pkg; Pkg.add("{package}")` in Julia.' ) from e - def create_hip_event(self): - return self.backend_mod.HIP.HIPEvent(self.stream, timing=True) - def create_metal_buffer(self): """Create a Metal buffer in the command queue.""" try: diff --git a/kernel_tuner/observers/julia.py b/kernel_tuner/observers/julia.py index e24950d00..7ddd6b358 100644 --- a/kernel_tuner/observers/julia.py +++ b/kernel_tuner/observers/julia.py @@ -40,20 +40,20 @@ def __init__( if self.name == "cuda": # initialize events for this instance of the observer + self.stream = self.backend_mod.stream() self.start = self.start() self.end = self.end() - self.stream = self.backend_mod.stream() elif self.name == "amdgpu": self.stream = self.backend_mod.default_stream() - # self.start = self.start(self.stream, timing=True) - # self.end = self.end(self.stream, timing=True) + self.start = self.start(self.stream, timing=True) + self.end = self.end(self.stream, timing=True) def before_start(self): if self.start is not None: if self.name == "metal": self.t0 = self.start() elif self.name == "amdgpu": - self.t0 = self.backend_mod.HIP.record(self.create_hip_event()) + self.t0 = self.backend_mod.HIP.record(self.start) else: self.backend_mod.record(self.start, self.stream) else: @@ -65,13 +65,11 @@ def after_finish(self): if self.name == "metal": ms = float((self.end() - self.t0) * 1000.0) elif self.name == "amdgpu": - t1 = self.backend_mod.HIP.record(self.create_hip_event()) + t1 = self.backend_mod.HIP.record(self.end) ms = float(self.backend_mod.HIP.elapsed(self.t0, t1) * 1000.0) else: - self.backend_mod.synchronize(self.end) self.backend_mod.record(self.end, self.stream) - self.backend_mod.synchronize(self.end) - ms = float(self.backend_mod.elapsed(self.start, self.end)) + ms = float(self.backend_mod.elapsed(self.start, self.end) * 1000.0) else: self.kernelabstractions.synchronize(self.backend) dt = perf_counter() - self.t0 @@ -79,7 +77,7 @@ def after_finish(self): warn(f"Using host-side timing for Julia {self.name} backend; results may be less accurate.") if ms > self.kt_backend.host_time: - if ms > 1.25 * self.kt_backend.host_time: + if ms > 1.25 * self.kt_backend.host_time and self.end is not None: warn( f"Measured GPU time {ms:.3f} ms is substantially greater than host time {self.kt_backend.host_time:.3f} ms; " "this may indicate an issue with the timing measurement. Using host time instead." @@ -96,9 +94,6 @@ def get_results(self): self.times = [] return results - def create_hip_event(self): - return self.backend_mod.HIP.HIPEvent(self.stream, timing=True) - class JuliaJITWarmup(PrologueObserver): """Prologue observer to enforce warmup before every configuration to trigger JIT.""" From 8dfdfad2a8cfacc302e685ac0264fbb256284594 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Fri, 10 Apr 2026 15:06:21 +0200 Subject: [PATCH 077/146] Minor improvements to AMD GPU-side timing --- kernel_tuner/backends/julia.py | 6 +++--- kernel_tuner/backends/julia_helper.py | 2 +- kernel_tuner/observers/julia.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index fc1fb7736..a98e44377 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -193,7 +193,7 @@ def initialize_backend(self, device, backend_name): self.start_evt = backend_mod.CuEvent self.end_evt = backend_mod.CuEvent elif backend_name == "AMD": - self.stream = backend_mod.default_stream() + self.stream = backend_mod.stream() self.start_evt = backend_mod.HIP.HIPEvent self.end_evt = backend_mod.HIP.HIPEvent elif backend_name == "INTEL": @@ -301,7 +301,7 @@ def start_event(self): if self.backend_mod_name == "CUDA": self.backend_mod.record(self.start_evt(), self.stream) elif self.backend_mod_name == "AMDGPU": - self.backend_mod.HIP.record(self.start_evt(self.stream, timing=True)) + self.backend_mod.HIP.record(self.start_evt(self.stream, do_record=False, timing=True)) elif self.backend_mod_name == "Metal": # Because our kernel launch happens via Kernel Abstractions, we wrap our kernel between two command buffers. # Normally you would just use one command buffer for the actual kernel. @@ -315,7 +315,7 @@ def stop_event(self): if self.backend_mod_name == "CUDA": self.backend_mod.record(self.end_evt(), self.stream) elif self.backend_mod_name == "AMDGPU": - self.backend_mod.HIP.record(self.end_evt(self.stream, timing=True)) + self.backend_mod.HIP.record(self.end_evt(self.stream, do_record=False, timing=True)) elif self.backend_mod_name == "Metal": jl.end_buf = self.create_metal_buffer() jl.seval("Metal.commit!(end_buf)") diff --git a/kernel_tuner/backends/julia_helper.py b/kernel_tuner/backends/julia_helper.py index d68598e4c..9f5a661f7 100644 --- a/kernel_tuner/backends/julia_helper.py +++ b/kernel_tuner/backends/julia_helper.py @@ -35,7 +35,7 @@ "pkg": "oneAPI", "module": "oneAPI", "module_backend": "oneAPIBackend", - "device_select": lambda d: f"devices(first(drivers()))[{d}]", + "device_select": lambda d: f"device!(devices(first(drivers()))[{d}])", "name": "oneAPI.name(oneAPI.device())", "max_threads": "oneAPI.compute_properties(oneAPI.device()).maxTotalGroupSize", "capability": None, diff --git a/kernel_tuner/observers/julia.py b/kernel_tuner/observers/julia.py index 7ddd6b358..5e135d71f 100644 --- a/kernel_tuner/observers/julia.py +++ b/kernel_tuner/observers/julia.py @@ -44,9 +44,9 @@ def __init__( self.start = self.start() self.end = self.end() elif self.name == "amdgpu": - self.stream = self.backend_mod.default_stream() - self.start = self.start(self.stream, timing=True) - self.end = self.end(self.stream, timing=True) + self.stream = self.backend_mod.stream() + self.start = self.start(self.stream, do_record=False, timing=True) + self.end = self.end(self.stream, do_record=False, timing=True) def before_start(self): if self.start is not None: From 73c6331d56c4cc9613b31bc356913571ccd5617c Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Fri, 10 Apr 2026 15:34:45 +0200 Subject: [PATCH 078/146] Test for AMD GPU-side timing --- kernel_tuner/observers/julia.py | 1 + 1 file changed, 1 insertion(+) diff --git a/kernel_tuner/observers/julia.py b/kernel_tuner/observers/julia.py index 5e135d71f..e255d75ae 100644 --- a/kernel_tuner/observers/julia.py +++ b/kernel_tuner/observers/julia.py @@ -47,6 +47,7 @@ def __init__( self.stream = self.backend_mod.stream() self.start = self.start(self.stream, do_record=False, timing=True) self.end = self.end(self.stream, do_record=False, timing=True) + raise ValueError(type(self.start), type(self.end), type(self.stream), self.start, self.end) def before_start(self): if self.start is not None: From 7d9cb76d55368f1be20f962cd0e654fbf65a4612 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Fri, 10 Apr 2026 15:47:08 +0200 Subject: [PATCH 079/146] Test for AMD GPU-side timing --- kernel_tuner/observers/julia.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kernel_tuner/observers/julia.py b/kernel_tuner/observers/julia.py index e255d75ae..0fa1d4b91 100644 --- a/kernel_tuner/observers/julia.py +++ b/kernel_tuner/observers/julia.py @@ -47,7 +47,6 @@ def __init__( self.stream = self.backend_mod.stream() self.start = self.start(self.stream, do_record=False, timing=True) self.end = self.end(self.stream, do_record=False, timing=True) - raise ValueError(type(self.start), type(self.end), type(self.stream), self.start, self.end) def before_start(self): if self.start is not None: @@ -58,6 +57,7 @@ def before_start(self): else: self.backend_mod.record(self.start, self.stream) else: + raise ValueError("Should not be None") # fallback: host-side timestamp self.t0 = perf_counter() @@ -72,6 +72,7 @@ def after_finish(self): self.backend_mod.record(self.end, self.stream) ms = float(self.backend_mod.elapsed(self.start, self.end) * 1000.0) else: + raise ValueError("Should not be None") self.kernelabstractions.synchronize(self.backend) dt = perf_counter() - self.t0 ms = dt * 1000.0 From 0cfacc05e16111718bb7f360cc02ee35f7c6a09e Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Fri, 10 Apr 2026 15:52:44 +0200 Subject: [PATCH 080/146] Test for AMD GPU-side timing --- kernel_tuner/observers/julia.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/kernel_tuner/observers/julia.py b/kernel_tuner/observers/julia.py index 0fa1d4b91..6601e8609 100644 --- a/kernel_tuner/observers/julia.py +++ b/kernel_tuner/observers/julia.py @@ -57,7 +57,6 @@ def before_start(self): else: self.backend_mod.record(self.start, self.stream) else: - raise ValueError("Should not be None") # fallback: host-side timestamp self.t0 = perf_counter() @@ -68,11 +67,11 @@ def after_finish(self): elif self.name == "amdgpu": t1 = self.backend_mod.HIP.record(self.end) ms = float(self.backend_mod.HIP.elapsed(self.t0, t1) * 1000.0) + warn(self.t0, t1, ms) else: self.backend_mod.record(self.end, self.stream) ms = float(self.backend_mod.elapsed(self.start, self.end) * 1000.0) else: - raise ValueError("Should not be None") self.kernelabstractions.synchronize(self.backend) dt = perf_counter() - self.t0 ms = dt * 1000.0 From d7c88482d89c9adbfbe88638b663f13eef13d8fd Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Fri, 10 Apr 2026 15:56:30 +0200 Subject: [PATCH 081/146] Test for AMD GPU-side timing --- kernel_tuner/observers/julia.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel_tuner/observers/julia.py b/kernel_tuner/observers/julia.py index 6601e8609..bd8af11a5 100644 --- a/kernel_tuner/observers/julia.py +++ b/kernel_tuner/observers/julia.py @@ -67,7 +67,7 @@ def after_finish(self): elif self.name == "amdgpu": t1 = self.backend_mod.HIP.record(self.end) ms = float(self.backend_mod.HIP.elapsed(self.t0, t1) * 1000.0) - warn(self.t0, t1, ms) + warn(f"T0: {self.t0}, T1: {t1}, elapsed: {ms} ms") else: self.backend_mod.record(self.end, self.stream) ms = float(self.backend_mod.elapsed(self.start, self.end) * 1000.0) From 14a5d36788f98e896db6ae87586a45f9f8f0b398 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Fri, 10 Apr 2026 18:59:35 +0200 Subject: [PATCH 082/146] Improvements for AMD and Nvidia GPU-side events --- kernel_tuner/backends/julia.py | 16 ++++++++++++---- kernel_tuner/observers/julia.py | 8 ++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index a98e44377..0ca21d9f5 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -299,9 +299,13 @@ def run_kernel(self, func, gpu_args, threads, grid, stream=None, params=None): def start_event(self): """Records the event that marks the start of a measurement.""" if self.backend_mod_name == "CUDA": - self.backend_mod.record(self.start_evt(), self.stream) + evt = self.start_evt() + self.backend_mod.record(evt, self.stream) + return evt elif self.backend_mod_name == "AMDGPU": - self.backend_mod.HIP.record(self.start_evt(self.stream, do_record=False, timing=True)) + evt = self.start_evt(self.stream, do_record=False, timing=True) + self.backend_mod.HIP.record(evt) + return evt elif self.backend_mod_name == "Metal": # Because our kernel launch happens via Kernel Abstractions, we wrap our kernel between two command buffers. # Normally you would just use one command buffer for the actual kernel. @@ -313,9 +317,13 @@ def start_event(self): def stop_event(self): """Records the event that marks the end of a measurement.""" if self.backend_mod_name == "CUDA": - self.backend_mod.record(self.end_evt(), self.stream) + evt = self.end_evt() + self.backend_mod.record(evt, self.stream) + return evt elif self.backend_mod_name == "AMDGPU": - self.backend_mod.HIP.record(self.end_evt(self.stream, do_record=False, timing=True)) + evt = self.end_evt(self.stream, do_record=False, timing=True) + self.backend_mod.HIP.record(evt) + return evt elif self.backend_mod_name == "Metal": jl.end_buf = self.create_metal_buffer() jl.seval("Metal.commit!(end_buf)") diff --git a/kernel_tuner/observers/julia.py b/kernel_tuner/observers/julia.py index bd8af11a5..358008f7c 100644 --- a/kernel_tuner/observers/julia.py +++ b/kernel_tuner/observers/julia.py @@ -53,7 +53,7 @@ def before_start(self): if self.name == "metal": self.t0 = self.start() elif self.name == "amdgpu": - self.t0 = self.backend_mod.HIP.record(self.start) + self.backend_mod.HIP.record(self.start) else: self.backend_mod.record(self.start, self.stream) else: @@ -61,13 +61,13 @@ def before_start(self): self.t0 = perf_counter() def after_finish(self): + ms = None if self.end is not None: if self.name == "metal": ms = float((self.end() - self.t0) * 1000.0) elif self.name == "amdgpu": - t1 = self.backend_mod.HIP.record(self.end) - ms = float(self.backend_mod.HIP.elapsed(self.t0, t1) * 1000.0) - warn(f"T0: {self.t0}, T1: {t1}, elapsed: {ms} ms") + self.backend_mod.HIP.record(self.end) + ms = float(self.backend_mod.HIP.elapsed(self.start, self.end) * 1000.0) else: self.backend_mod.record(self.end, self.stream) ms = float(self.backend_mod.elapsed(self.start, self.end) * 1000.0) From 32c1ecf6a44e64fa34552b6525656af6dc6bead3 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 13 Apr 2026 23:25:48 +0200 Subject: [PATCH 083/146] Added debug tracking --- kernel_tuner/observers/julia.py | 1 + 1 file changed, 1 insertion(+) diff --git a/kernel_tuner/observers/julia.py b/kernel_tuner/observers/julia.py index 358008f7c..9377c4ade 100644 --- a/kernel_tuner/observers/julia.py +++ b/kernel_tuner/observers/julia.py @@ -68,6 +68,7 @@ def after_finish(self): elif self.name == "amdgpu": self.backend_mod.HIP.record(self.end) ms = float(self.backend_mod.HIP.elapsed(self.start, self.end) * 1000.0) + warn(f"s: {self.start}, e: {self.end}, ms: {ms}, ms(r): {self.backend_mod.HIP.elapsed(self.end, self.start)}") else: self.backend_mod.record(self.end, self.stream) ms = float(self.backend_mod.elapsed(self.start, self.end) * 1000.0) From 77113f5942e3ca5d0316daa89f8cc2292b36f86e Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Fri, 17 Apr 2026 14:28:01 +0200 Subject: [PATCH 084/146] Added synchronize after end events --- kernel_tuner/observers/julia.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kernel_tuner/observers/julia.py b/kernel_tuner/observers/julia.py index 9377c4ade..39183464e 100644 --- a/kernel_tuner/observers/julia.py +++ b/kernel_tuner/observers/julia.py @@ -67,10 +67,12 @@ def after_finish(self): ms = float((self.end() - self.t0) * 1000.0) elif self.name == "amdgpu": self.backend_mod.HIP.record(self.end) + self.backend_mod.synchronize(self.end) ms = float(self.backend_mod.HIP.elapsed(self.start, self.end) * 1000.0) warn(f"s: {self.start}, e: {self.end}, ms: {ms}, ms(r): {self.backend_mod.HIP.elapsed(self.end, self.start)}") else: self.backend_mod.record(self.end, self.stream) + self.backend_mod.synchronize(self.end) ms = float(self.backend_mod.elapsed(self.start, self.end) * 1000.0) else: self.kernelabstractions.synchronize(self.backend) From 291ddbf07256a4fd5aad1683a1c77e9d7dfc9612 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Fri, 17 Apr 2026 14:57:05 +0200 Subject: [PATCH 085/146] Added synchronize after end events --- kernel_tuner/observers/julia.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel_tuner/observers/julia.py b/kernel_tuner/observers/julia.py index 39183464e..5c1b8e20e 100644 --- a/kernel_tuner/observers/julia.py +++ b/kernel_tuner/observers/julia.py @@ -67,7 +67,7 @@ def after_finish(self): ms = float((self.end() - self.t0) * 1000.0) elif self.name == "amdgpu": self.backend_mod.HIP.record(self.end) - self.backend_mod.synchronize(self.end) + self.backend_mod.HIP.synchronize(self.end) ms = float(self.backend_mod.HIP.elapsed(self.start, self.end) * 1000.0) warn(f"s: {self.start}, e: {self.end}, ms: {ms}, ms(r): {self.backend_mod.HIP.elapsed(self.end, self.start)}") else: From ee855e8e4363e386c60eaf795b8b72902a7f30c9 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Fri, 17 Apr 2026 15:25:29 +0200 Subject: [PATCH 086/146] Improved timing stability --- kernel_tuner/observers/julia.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/kernel_tuner/observers/julia.py b/kernel_tuner/observers/julia.py index 5c1b8e20e..ca679f507 100644 --- a/kernel_tuner/observers/julia.py +++ b/kernel_tuner/observers/julia.py @@ -69,7 +69,6 @@ def after_finish(self): self.backend_mod.HIP.record(self.end) self.backend_mod.HIP.synchronize(self.end) ms = float(self.backend_mod.HIP.elapsed(self.start, self.end) * 1000.0) - warn(f"s: {self.start}, e: {self.end}, ms: {ms}, ms(r): {self.backend_mod.HIP.elapsed(self.end, self.start)}") else: self.backend_mod.record(self.end, self.stream) self.backend_mod.synchronize(self.end) @@ -81,12 +80,16 @@ def after_finish(self): warn(f"Using host-side timing for Julia {self.name} backend; results may be less accurate.") if ms > self.kt_backend.host_time: - if ms > 1.25 * self.kt_backend.host_time and self.end is not None: + if ms < 1 and (ms > 1.5 * self.kt_backend.host_time and self.end is not None): + warn( + f"Measured GPU time {ms:.3f} ms is greater than host time {self.kt_backend.host_time:.3f} ms; " + "this may happen with very short execution times." + ) + elif ms > 1.5 * self.kt_backend.host_time and self.end is not None: warn( f"Measured GPU time {ms:.3f} ms is substantially greater than host time {self.kt_backend.host_time:.3f} ms; " - "this may indicate an issue with the timing measurement. Using host time instead." + "this may indicate an issue with the timing measurement." ) - ms = self.kt_backend.host_time self.times.append(ms) From e32703d95f98066f52c63dc1e51a177b77f0f9d8 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Fri, 17 Apr 2026 20:18:21 +0200 Subject: [PATCH 087/146] Moved CUDA and AMD event recording closer to kernel launch (experimental) --- kernel_tuner/backends/julia.py | 11 ++++++++++- kernel_tuner/backends/julia_helper.jl | 22 +++++++++++++++++++++- kernel_tuner/observers/julia.py | 18 +++++++++++------- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 0ca21d9f5..0236166f9 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -87,6 +87,9 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None ) for observer in self.observers: observer.register_device(self) + self.start_evt_instance = self.observers[-1].start + self.end_evt_instance = self.observers[-1].end + self.stream_instance = self.observers[-1].stream jl.seval( f""" @@ -94,6 +97,7 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None module KernelTunerHelper using {self.backend_mod_name} const kt_julia_backend = {self.backend_mod_instname}() + const event_type = {self.backend_mod.CuEvent} const GPUArrayType = {self.GPUArrayType} include("{str(Path(__file__).parent / "julia_helper.jl")}") end @@ -292,7 +296,8 @@ def run_kernel(self, func, gpu_args, threads, grid, stream=None, params=None): # run the kernel try: - self.host_time = self.launch_kernel(func, args_tuple, params, ndrange, workgroupsize, int(self.smem_size)) + self.host_time = self.launch_kernel(func, args_tuple, params, ndrange, workgroupsize, int(self.smem_size), + self.start_evt_instance, self.end_evt_instance, self.stream_instance) except JuliaError as e: raise SkippableFailure(f"Julia kernel launch failed for {params=}: {e}") @@ -301,10 +306,12 @@ def start_event(self): if self.backend_mod_name == "CUDA": evt = self.start_evt() self.backend_mod.record(evt, self.stream) + self.backend_mod.synchronize(evt) return evt elif self.backend_mod_name == "AMDGPU": evt = self.start_evt(self.stream, do_record=False, timing=True) self.backend_mod.HIP.record(evt) + self.backend_mod.HIP.synchronize(evt) return evt elif self.backend_mod_name == "Metal": # Because our kernel launch happens via Kernel Abstractions, we wrap our kernel between two command buffers. @@ -319,10 +326,12 @@ def stop_event(self): if self.backend_mod_name == "CUDA": evt = self.end_evt() self.backend_mod.record(evt, self.stream) + self.backend_mod.synchronize(evt) return evt elif self.backend_mod_name == "AMDGPU": evt = self.end_evt(self.stream, do_record=False, timing=True) self.backend_mod.HIP.record(evt) + self.backend_mod.HIP.synchronize(evt) return evt elif self.backend_mod_name == "Metal": jl.end_buf = self.create_metal_buffer() diff --git a/kernel_tuner/backends/julia_helper.jl b/kernel_tuner/backends/julia_helper.jl index 17e9edbac..f9de4948b 100644 --- a/kernel_tuner/backends/julia_helper.jl +++ b/kernel_tuner/backends/julia_helper.jl @@ -8,7 +8,7 @@ function to_gpuarray(a) return a end -function launch_kernel(kernel, args::Tuple, params::Tuple, ndrange::Tuple, workgroupsize::Tuple, shmem::Int) +function launch_kernel(kernel, args::Tuple, params::Tuple, ndrange::Tuple, workgroupsize::Tuple, shmem::Int, start_evt::Any, end_evt::Any, stream::Any) t = Inf # Check if this is a KernelAbstractions kernel if isdefined(Main, :KernelAbstractions) && kt_julia_backend !== nothing && applicable(kernel, kt_julia_backend, workgroupsize) @@ -21,8 +21,28 @@ function launch_kernel(kernel, args::Tuple, params::Tuple, ndrange::Tuple, workg try val_params = Val.(params) # convert parameters to Val types for kernel invocation start = time_ns() # simple host-side timing as fallback in case of issues with GPU timing + if start_evt !== nothing + if isa(start_evt, event_type) + Main.CUDA.record(start_evt, stream) + elseif isa(start_evt, event_type) + Main.AMDGPU.HIP.record(start_evt) + else + error("Unsupported event type for timing: $(typeof(start_evt))") + end + end configured_kernel(args..., val_params...; ndrange=ndrange) # launch the kernel Main.KernelAbstractions.synchronize(kt_julia_backend) # synchronize to ensure kernel completion + if end_evt !== nothing + if isa(end_evt, event_type) + Main.CUDA.record(end_evt, stream) + Main.CUDA.synchronize(end_evt) # ensure the event is recorded before we read it + elseif isa(end_evt, event_type) + Main.AMDGPU.HIP.record(end_evt) + Main.AMDGPU.HIP.synchronize(end_evt) # ensure the event is recorded before we read it + else + error("Unsupported event type for timing: $(typeof(end_evt))") + end + end t = float((time_ns() - start) / 1e6) # convert to milliseconds catch e redirect_stdout(stdout) # restore stdout diff --git a/kernel_tuner/observers/julia.py b/kernel_tuner/observers/julia.py index ca679f507..dd8deb0ba 100644 --- a/kernel_tuner/observers/julia.py +++ b/kernel_tuner/observers/julia.py @@ -52,10 +52,14 @@ def before_start(self): if self.start is not None: if self.name == "metal": self.t0 = self.start() + elif self.name == "cuda": + # the events are recorded in the julia_helper kernel launch code + pass elif self.name == "amdgpu": - self.backend_mod.HIP.record(self.start) + # the events are recorded in the julia_helper kernel launch code + pass else: - self.backend_mod.record(self.start, self.stream) + raise ValueError(f"Unsupported backend for timing: {self.name}") else: # fallback: host-side timestamp self.t0 = perf_counter() @@ -65,14 +69,14 @@ def after_finish(self): if self.end is not None: if self.name == "metal": ms = float((self.end() - self.t0) * 1000.0) + elif self.name == "cuda": + # the events are recorded in the julia_helper kernel launch code + ms = float(self.backend_mod.elapsed(self.start, self.end) * 1000.0) elif self.name == "amdgpu": - self.backend_mod.HIP.record(self.end) - self.backend_mod.HIP.synchronize(self.end) + # the events are recorded in the julia_helper kernel launch code ms = float(self.backend_mod.HIP.elapsed(self.start, self.end) * 1000.0) else: - self.backend_mod.record(self.end, self.stream) - self.backend_mod.synchronize(self.end) - ms = float(self.backend_mod.elapsed(self.start, self.end) * 1000.0) + raise ValueError(f"Unsupported backend for timing: {self.name}") else: self.kernelabstractions.synchronize(self.backend) dt = perf_counter() - self.t0 From b537f1052bb4de9e5743833f27084d2550e301c8 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Mon, 20 Apr 2026 14:32:34 +0200 Subject: [PATCH 088/146] Fixed an issue with GPU-side timing events --- kernel_tuner/backends/julia.py | 1 - kernel_tuner/backends/julia_helper.jl | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 0236166f9..06177e7fb 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -97,7 +97,6 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None module KernelTunerHelper using {self.backend_mod_name} const kt_julia_backend = {self.backend_mod_instname}() - const event_type = {self.backend_mod.CuEvent} const GPUArrayType = {self.GPUArrayType} include("{str(Path(__file__).parent / "julia_helper.jl")}") end diff --git a/kernel_tuner/backends/julia_helper.jl b/kernel_tuner/backends/julia_helper.jl index f9de4948b..70db18bbb 100644 --- a/kernel_tuner/backends/julia_helper.jl +++ b/kernel_tuner/backends/julia_helper.jl @@ -22,9 +22,9 @@ function launch_kernel(kernel, args::Tuple, params::Tuple, ndrange::Tuple, workg val_params = Val.(params) # convert parameters to Val types for kernel invocation start = time_ns() # simple host-side timing as fallback in case of issues with GPU timing if start_evt !== nothing - if isa(start_evt, event_type) + if isa(start_evt, CuEvent) Main.CUDA.record(start_evt, stream) - elseif isa(start_evt, event_type) + elseif isa(start_evt, HIPEvent) Main.AMDGPU.HIP.record(start_evt) else error("Unsupported event type for timing: $(typeof(start_evt))") @@ -33,10 +33,10 @@ function launch_kernel(kernel, args::Tuple, params::Tuple, ndrange::Tuple, workg configured_kernel(args..., val_params...; ndrange=ndrange) # launch the kernel Main.KernelAbstractions.synchronize(kt_julia_backend) # synchronize to ensure kernel completion if end_evt !== nothing - if isa(end_evt, event_type) + if isa(end_evt, CuEvent) Main.CUDA.record(end_evt, stream) Main.CUDA.synchronize(end_evt) # ensure the event is recorded before we read it - elseif isa(end_evt, event_type) + elseif isa(end_evt, HIPEvent) Main.AMDGPU.HIP.record(end_evt) Main.AMDGPU.HIP.synchronize(end_evt) # ensure the event is recorded before we read it else From f873c547f0097a56ec217282f86963964bc78cf6 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Mon, 20 Apr 2026 14:48:27 +0200 Subject: [PATCH 089/146] Fixed an issue with GPU-side timing events --- kernel_tuner/backends/julia_helper.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/kernel_tuner/backends/julia_helper.jl b/kernel_tuner/backends/julia_helper.jl index 70db18bbb..a9bfad1fd 100644 --- a/kernel_tuner/backends/julia_helper.jl +++ b/kernel_tuner/backends/julia_helper.jl @@ -22,9 +22,9 @@ function launch_kernel(kernel, args::Tuple, params::Tuple, ndrange::Tuple, workg val_params = Val.(params) # convert parameters to Val types for kernel invocation start = time_ns() # simple host-side timing as fallback in case of issues with GPU timing if start_evt !== nothing - if isa(start_evt, CuEvent) + if isdefined(Main, :CUDA) && isa(start_evt, CuEvent) Main.CUDA.record(start_evt, stream) - elseif isa(start_evt, HIPEvent) + elseif isdefined(Main, :AMDGPU) && isa(start_evt, HIPEvent) Main.AMDGPU.HIP.record(start_evt) else error("Unsupported event type for timing: $(typeof(start_evt))") @@ -33,10 +33,10 @@ function launch_kernel(kernel, args::Tuple, params::Tuple, ndrange::Tuple, workg configured_kernel(args..., val_params...; ndrange=ndrange) # launch the kernel Main.KernelAbstractions.synchronize(kt_julia_backend) # synchronize to ensure kernel completion if end_evt !== nothing - if isa(end_evt, CuEvent) + if isdefined(Main, :CUDA) && isa(end_evt, CuEvent) Main.CUDA.record(end_evt, stream) Main.CUDA.synchronize(end_evt) # ensure the event is recorded before we read it - elseif isa(end_evt, HIPEvent) + elseif isdefined(Main, :AMDGPU) && isa(end_evt, HIPEvent) Main.AMDGPU.HIP.record(end_evt) Main.AMDGPU.HIP.synchronize(end_evt) # ensure the event is recorded before we read it else From 3f7621f264f06e6090036beeb5e038ea5ac97b5c Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 20 Apr 2026 15:56:49 +0200 Subject: [PATCH 090/146] Fixed an issue with GPU-side timing events for Metal --- kernel_tuner/backends/julia_helper.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kernel_tuner/backends/julia_helper.jl b/kernel_tuner/backends/julia_helper.jl index a9bfad1fd..d51b8e0ba 100644 --- a/kernel_tuner/backends/julia_helper.jl +++ b/kernel_tuner/backends/julia_helper.jl @@ -26,6 +26,7 @@ function launch_kernel(kernel, args::Tuple, params::Tuple, ndrange::Tuple, workg Main.CUDA.record(start_evt, stream) elseif isdefined(Main, :AMDGPU) && isa(start_evt, HIPEvent) Main.AMDGPU.HIP.record(start_evt) + elseif isdefined(Main, :Metal) # Metal timing is processed in the observer else error("Unsupported event type for timing: $(typeof(start_evt))") end @@ -39,6 +40,7 @@ function launch_kernel(kernel, args::Tuple, params::Tuple, ndrange::Tuple, workg elseif isdefined(Main, :AMDGPU) && isa(end_evt, HIPEvent) Main.AMDGPU.HIP.record(end_evt) Main.AMDGPU.HIP.synchronize(end_evt) # ensure the event is recorded before we read it + elseif isdefined(Main, :Metal) # Metal timing is processed in the observer else error("Unsupported event type for timing: $(typeof(end_evt))") end From d60dcb66173e717fc0fa27b9d46049019d6c3114 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 20 Apr 2026 22:11:43 +0200 Subject: [PATCH 091/146] Substantially more precise Metal timing --- kernel_tuner/backends/julia.py | 18 +++++++++++------ kernel_tuner/backends/julia_helper.jl | 28 ++++++++++++++++++++++++--- kernel_tuner/observers/julia.py | 10 +++++++--- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 06177e7fb..9069568e9 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -87,9 +87,6 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None ) for observer in self.observers: observer.register_device(self) - self.start_evt_instance = self.observers[-1].start - self.end_evt_instance = self.observers[-1].end - self.stream_instance = self.observers[-1].stream jl.seval( f""" @@ -295,8 +292,17 @@ def run_kernel(self, func, gpu_args, threads, grid, stream=None, params=None): # run the kernel try: - self.host_time = self.launch_kernel(func, args_tuple, params, ndrange, workgroupsize, int(self.smem_size), - self.start_evt_instance, self.end_evt_instance, self.stream_instance) + self.host_time = self.launch_kernel( + func, + args_tuple, + params, + ndrange, + workgroupsize, + int(self.smem_size), + self.observers[-1].start, + self.observers[-1].end, + self.observers[-1].stream, + ) except JuliaError as e: raise SkippableFailure(f"Julia kernel launch failed for {params=}: {e}") @@ -314,7 +320,7 @@ def start_event(self): return evt elif self.backend_mod_name == "Metal": # Because our kernel launch happens via Kernel Abstractions, we wrap our kernel between two command buffers. - # Normally you would just use one command buffer for the actual kernel. + # Normally you would just use one command buffer for the actual kernel and take GPUEndTime - GPUStartTime. jl.start_buf = self.create_metal_buffer() jl.seval("Metal.commit!(start_buf)") self.backend_mod.wait_completed(jl.start_buf) diff --git a/kernel_tuner/backends/julia_helper.jl b/kernel_tuner/backends/julia_helper.jl index d51b8e0ba..d15ad6e07 100644 --- a/kernel_tuner/backends/julia_helper.jl +++ b/kernel_tuner/backends/julia_helper.jl @@ -20,13 +20,19 @@ function launch_kernel(kernel, args::Tuple, params::Tuple, ndrange::Tuple, workg redirect_stdout(tmpio) do try val_params = Val.(params) # convert parameters to Val types for kernel invocation + start_buff = nothing + end_buff = nothing start = time_ns() # simple host-side timing as fallback in case of issues with GPU timing if start_evt !== nothing if isdefined(Main, :CUDA) && isa(start_evt, CuEvent) Main.CUDA.record(start_evt, stream) elseif isdefined(Main, :AMDGPU) && isa(start_evt, HIPEvent) Main.AMDGPU.HIP.record(start_evt) - elseif isdefined(Main, :Metal) # Metal timing is processed in the observer + elseif isdefined(Main, :Metal) + # prepare the next command buffers for timing as they can only be used once + start_buff = create_metal_buffer(Metal.device()) + end_buff = create_metal_buffer(Metal.device()) + Metal.commit!(start_buff) else error("Unsupported event type for timing: $(typeof(start_evt))") end @@ -40,12 +46,17 @@ function launch_kernel(kernel, args::Tuple, params::Tuple, ndrange::Tuple, workg elseif isdefined(Main, :AMDGPU) && isa(end_evt, HIPEvent) Main.AMDGPU.HIP.record(end_evt) Main.AMDGPU.HIP.synchronize(end_evt) # ensure the event is recorded before we read it - elseif isdefined(Main, :Metal) # Metal timing is processed in the observer + elseif isdefined(Main, :Metal) + Metal.commit!(end_buff) + Metal.wait_completed(end_buff) # ensure the command buffer is completed before we read the time + t = (float(end_buff.GPUStartTime) - float(start_buff.GPUEndTime)) * 1000 else error("Unsupported event type for timing: $(typeof(end_evt))") end + else + # host-side timing fallback if events are not available + t = float((time_ns() - start) / 1e6) # convert to milliseconds end - t = float((time_ns() - start) / 1e6) # convert to milliseconds catch e redirect_stdout(stdout) # restore stdout close(tmpio) @@ -64,3 +75,14 @@ function launch_kernel(kernel, args::Tuple, params::Tuple, ndrange::Tuple, workg end return t end + +function create_metal_buffer(device::Metal.MTLDevice) + # Create a Metal buffer in the command queue for timing + if isdefined(Main, :Metal) + contextqueue = Main.Metal.MTLCommandQueue(device) + return Metal.MTLCommandBuffer(contextqueue) + # return contextqueue.commandBuffer() + else + error("Metal backend is not available.") + end +end diff --git a/kernel_tuner/observers/julia.py b/kernel_tuner/observers/julia.py index dd8deb0ba..1035f5407 100644 --- a/kernel_tuner/observers/julia.py +++ b/kernel_tuner/observers/julia.py @@ -11,8 +11,8 @@ class JuliaRuntimeObserver(BenchmarkObserver): - CUDA: CuEvent timing - ROCBackend: HIPEvent timing - - OneAPI: host timing + synchronize (less accurate, no events available) - - Metal: host timing + synchronize + - OneAPI: host-side timing (less accurate, no events available) + - Metal: timing by wrapping the kernel launch between two command buffers and measuring the time between them """ def __init__( @@ -68,7 +68,11 @@ def after_finish(self): ms = None if self.end is not None: if self.name == "metal": - ms = float((self.end() - self.t0) * 1000.0) + ms_observer = float((self.end() - self.t0) * 1000.0) + ms_helper = self.kt_backend.host_time + ms = min( + ms_observer, ms_helper + ) # take the minimum of the two measurements to mitigate overhead of command buffer timing elif self.name == "cuda": # the events are recorded in the julia_helper kernel launch code ms = float(self.backend_mod.elapsed(self.start, self.end) * 1000.0) From eacdad0453b86e20f8110843d90f38ba90ed356d Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 20 Apr 2026 22:13:18 +0200 Subject: [PATCH 092/146] Fixed an issue with GPU-side timing events for AMD --- kernel_tuner/backends/julia_helper.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kernel_tuner/backends/julia_helper.jl b/kernel_tuner/backends/julia_helper.jl index d15ad6e07..f1d78b907 100644 --- a/kernel_tuner/backends/julia_helper.jl +++ b/kernel_tuner/backends/julia_helper.jl @@ -26,7 +26,7 @@ function launch_kernel(kernel, args::Tuple, params::Tuple, ndrange::Tuple, workg if start_evt !== nothing if isdefined(Main, :CUDA) && isa(start_evt, CuEvent) Main.CUDA.record(start_evt, stream) - elseif isdefined(Main, :AMDGPU) && isa(start_evt, HIPEvent) + elseif isdefined(Main, :AMDGPU) && isa(start_evt, HIP.HIPEvent) Main.AMDGPU.HIP.record(start_evt) elseif isdefined(Main, :Metal) # prepare the next command buffers for timing as they can only be used once @@ -43,7 +43,7 @@ function launch_kernel(kernel, args::Tuple, params::Tuple, ndrange::Tuple, workg if isdefined(Main, :CUDA) && isa(end_evt, CuEvent) Main.CUDA.record(end_evt, stream) Main.CUDA.synchronize(end_evt) # ensure the event is recorded before we read it - elseif isdefined(Main, :AMDGPU) && isa(end_evt, HIPEvent) + elseif isdefined(Main, :AMDGPU) && isa(end_evt, HIP.HIPEvent) Main.AMDGPU.HIP.record(end_evt) Main.AMDGPU.HIP.synchronize(end_evt) # ensure the event is recorded before we read it elseif isdefined(Main, :Metal) From e001e45897cc44253f1804d933fb65de9d7208f5 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Tue, 21 Apr 2026 00:12:38 +0200 Subject: [PATCH 093/146] Resolved unintended Metal dependency in function signature --- kernel_tuner/backends/julia_helper.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel_tuner/backends/julia_helper.jl b/kernel_tuner/backends/julia_helper.jl index f1d78b907..55a1c0589 100644 --- a/kernel_tuner/backends/julia_helper.jl +++ b/kernel_tuner/backends/julia_helper.jl @@ -76,7 +76,7 @@ function launch_kernel(kernel, args::Tuple, params::Tuple, ndrange::Tuple, workg return t end -function create_metal_buffer(device::Metal.MTLDevice) +function create_metal_buffer(device) # Create a Metal buffer in the command queue for timing if isdefined(Main, :Metal) contextqueue = Main.Metal.MTLCommandQueue(device) From e1289bb198e7d900f0621b98e715c46cb00a89b4 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Tue, 21 Apr 2026 11:39:23 +0200 Subject: [PATCH 094/146] Fixed an issue with GPU-side timing events for AMD --- kernel_tuner/backends/julia_helper.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel_tuner/backends/julia_helper.jl b/kernel_tuner/backends/julia_helper.jl index 55a1c0589..9e479f3d4 100644 --- a/kernel_tuner/backends/julia_helper.jl +++ b/kernel_tuner/backends/julia_helper.jl @@ -26,7 +26,7 @@ function launch_kernel(kernel, args::Tuple, params::Tuple, ndrange::Tuple, workg if start_evt !== nothing if isdefined(Main, :CUDA) && isa(start_evt, CuEvent) Main.CUDA.record(start_evt, stream) - elseif isdefined(Main, :AMDGPU) && isa(start_evt, HIP.HIPEvent) + elseif isdefined(Main, :AMDGPU) && isa(start_evt, AMDGPU.HIP.HIPEvent) Main.AMDGPU.HIP.record(start_evt) elseif isdefined(Main, :Metal) # prepare the next command buffers for timing as they can only be used once From 8a502df953733fdb39bbbc72d325c78b8e0b4393 Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Tue, 21 Apr 2026 15:47:21 +0200 Subject: [PATCH 095/146] Fixed an issue with GPU-side timing events AMD --- kernel_tuner/backends/julia_helper.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel_tuner/backends/julia_helper.jl b/kernel_tuner/backends/julia_helper.jl index 9e479f3d4..852421cf7 100644 --- a/kernel_tuner/backends/julia_helper.jl +++ b/kernel_tuner/backends/julia_helper.jl @@ -43,7 +43,7 @@ function launch_kernel(kernel, args::Tuple, params::Tuple, ndrange::Tuple, workg if isdefined(Main, :CUDA) && isa(end_evt, CuEvent) Main.CUDA.record(end_evt, stream) Main.CUDA.synchronize(end_evt) # ensure the event is recorded before we read it - elseif isdefined(Main, :AMDGPU) && isa(end_evt, HIP.HIPEvent) + elseif isdefined(Main, :AMDGPU) && isa(end_evt, AMDGPU.HIP.HIPEvent) Main.AMDGPU.HIP.record(end_evt) Main.AMDGPU.HIP.synchronize(end_evt) # ensure the event is recorded before we read it elseif isdefined(Main, :Metal) From 96c60fd68f2d5bdf6102667fc22d8f723ae75533 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Fri, 24 Apr 2026 14:14:38 +0200 Subject: [PATCH 096/146] Added option to raise errors from Julia backend --- kernel_tuner/backends/julia.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 9069568e9..ad4a4064a 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -40,6 +40,19 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None """Initialize Julia backend using JuliaCall.""" if jl is None: raise ImportError("JuliaCall not installed. Please run `pip install juliacall`.") + + # process compiler options + self.raise_errors = False + for c in compiler_options or []: + if c.lower().startswith("raise_errors="): + raise_errors_str = c.split("=", 1)[1].strip().lower() + if raise_errors_str in ("true", "1", "yes"): + self.raise_errors = True + elif raise_errors_str in ("false", "0", "no"): + self.raise_errors = False + else: + raise ValueError(f"Invalid value for raise_errors: {raise_errors_str}. Use true/false.") + compiler_options.remove(c) self.available_backends = detect_julia_gpu_backends() if compiler_options is not None and len(compiler_options) == 1: if compiler_options[0].upper() not in self.available_backends: @@ -304,7 +317,10 @@ def run_kernel(self, func, gpu_args, threads, grid, stream=None, params=None): self.observers[-1].stream, ) except JuliaError as e: - raise SkippableFailure(f"Julia kernel launch failed for {params=}: {e}") + if self.raise_errors: + raise e + else: + raise SkippableFailure(f"Julia kernel launch failed for {params=}: {e}") def start_event(self): """Records the event that marks the start of a measurement.""" From 238932db90192813b09b5945d2cdf166f67cf072 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Fri, 3 Jul 2026 17:59:58 +0200 Subject: [PATCH 097/146] Improved Julia compatibility and automatic handling of conversions --- kernel_tuner/core.py | 13 ++++++-- kernel_tuner/interface.py | 13 ++++++-- kernel_tuner/util.py | 70 ++++++++++++++++++++++----------------- pyproject.toml | 2 +- 4 files changed, 60 insertions(+), 38 deletions(-) diff --git a/kernel_tuner/core.py b/kernel_tuner/core.py index 36aabcde4..5c4edbe8a 100644 --- a/kernel_tuner/core.py +++ b/kernel_tuner/core.py @@ -219,7 +219,7 @@ def check_argument_lists(self, kernel_name, arguments): """ for i, f in enumerate(self.kernel_sources): if not callable(f): - util.check_argument_list(kernel_name, self.get_kernel_string(i), arguments) + util.check_argument_list(kernel_name, self.get_kernel_string(i), arguments, lang=self.lang) else: logging.debug("Checking of arguments list not supported yet for code generators.") @@ -344,7 +344,7 @@ def __init__( self.requires_warmup = False else: raise NotImplementedError( - "Sorry, support for languages other than CUDA, OpenCL, HIP, C, and Fortran is not implemented yet" + "Sorry, support for languages other than CUDA, OpenCL, HIP, C, Julia, Fortran is not implemented yet" ) self.dev = dev @@ -563,7 +563,7 @@ def check_kernel_output(self, func, gpu_args, instance, answer, atol, verify, ve result_host = [] for i, arg in enumerate(instance.arguments): if should_sync[i]: - if isinstance(arg, (np.ndarray, cp.ndarray)): + if isinstance(arg, (np.ndarray, cp.ndarray)) or arg.__class__.__name__ == "VectorValue": result_host.append(np.zeros_like(arg)) self.dev.memcpy_dtoh(result_host[-1], gpu_args[i]) elif isinstance(arg, torch.Tensor) and isinstance(answer[i], torch.Tensor): @@ -576,6 +576,7 @@ def check_kernel_output(self, func, gpu_args, instance, answer, atol, verify, ve else: # We should sync this argument, but we do not know how to transfer this type of argument # What do we do? Should we throw an error? + warn(f"Argument {i} is of type {type(arg)} and should be synchronized, but is not implemented.") result_host.append(None) else: result_host.append(None) @@ -836,6 +837,12 @@ def _default_verify_function(instance, answer, result_host, atol, verbose): # for each element in the argument list, check if the types match for i, arg in enumerate(instance.arguments): if answer[i] is not None: # skip None elements in the answer list + # convert Julia VectorValues to numpy arrays for verification + if arg.__class__.__name__ == "VectorValue": + arg = np.array(arg) + if answer[i].__class__.__name__ == "VectorValue": + answer[i] = np.array(answer[i]) + if isinstance(answer[i], (np.ndarray, cp.ndarray)) and isinstance(arg, (np.ndarray, cp.ndarray)): if not np.can_cast(arg.dtype, answer[i].dtype): raise TypeError( diff --git a/kernel_tuner/interface.py b/kernel_tuner/interface.py index fa1acc3e4..7f100706c 100644 --- a/kernel_tuner/interface.py +++ b/kernel_tuner/interface.py @@ -596,14 +596,17 @@ def tune_kernel( kernelsource = core.KernelSource(kernel_name, kernel_source, lang, defines) - if lang == "Julia": - # TODO implement the case where Kernel Tuner is called from Julia but the target language is not Julia + if lang is not None and lang.upper() == "JULIA": + # TODO implement & test the case where Kernel Tuner is called from Julia but the target language is not Julia if isinstance(tune_params, dict) or "DictValue" in tune_params.__class__.__name__: raise ValueError( "tune_params should not be a Julia dict, because it does not preserve order. Use a list of pairs instead." ) tune_params = [tuple([k, util.possible_julia_vector_to_list(tp)]) for k, tp in tune_params] tune_params = dict(tune_params) + answer = [ + numpy.array(a) if isinstance(a, (list, tuple)) else a for a in util.possible_julia_vector_to_list(answer) + ] restrictions = util.possible_julia_vector_to_list(restrictions) block_size_names = util.possible_julia_vector_to_list(block_size_names) @@ -838,7 +841,7 @@ def run_kernel( raise RuntimeError("cannot create kernel instance, too many threads per block") # see if the kernel arguments have correct type - util.check_argument_list(instance.name, instance.kernel_string, arguments) + util.check_argument_list(instance.name, instance.kernel_string, arguments, lang=lang) # compile the kernel func = dev.compile_kernel(instance, False) @@ -874,6 +877,10 @@ def run_kernel( results.append(numpy.zeros_like(arg)) dev.memcpy_dtoh(results[-1], gpu_args[i]) + # for Julia, convert the results back to Julia arrays + # if lang and lang.lower() == "julia": + # results = [util.possible_list_to_julia_vector(r) for r in results] + return results diff --git a/kernel_tuner/util.py b/kernel_tuner/util.py index cfcd6c31f..7394b92f4 100644 --- a/kernel_tuner/util.py +++ b/kernel_tuner/util.py @@ -135,54 +135,62 @@ def check_argument_type(dtype, kernel_argument): return False # unknown dtype. do not throw exception to still allow kernel to run. -def check_argument_list(kernel_name, kernel_string, args): +def check_argument_list(kernel_name, kernel_string, args, lang=None): """Raise an exception if kernel arguments do not match host arguments.""" kernel_arguments = list() collected_errors = list() + # Find all kernel argument lists in the kernel string for iterator in re.finditer(kernel_name + "[ \n\t]*" + r"\(", kernel_string): kernel_start = iterator.end() kernel_end = kernel_string.find(")", kernel_start) if kernel_start != 0: kernel_arguments.append(kernel_string[kernel_start:kernel_end].split(",")) + # Check each set of kernel arguments for arguments_set, arguments in enumerate(kernel_arguments): collected_errors.append(list()) if len(arguments) != len(args): - collected_errors[arguments_set].append("Kernel and host argument lists do not match in size.") + collected_errors[arguments_set].append( + f"Kernel ({len(arguments)}) and host argument ({len(args)}) lists do not match in size." + ) continue - for i, arg in enumerate(args): - kernel_argument = arguments[i] - - # Handle tunable arguments - if isinstance(arg, Tunable): - continue - - # Handle numpy arrays and other array types - if not isinstance(arg, (np.ndarray, np.generic, cp.ndarray, torch.Tensor, DeviceArray)): - raise TypeError( - f"Argument at position {i} of type: {type(arg)} should be of type " - "np.ndarray, numpy scalar, or HIP Python DeviceArray type" + # Check each argument in the kernel argument list + if lang is None or lang.upper() != "JULIA": + for i, arg in enumerate(args): + kernel_argument = arguments[i] + + # Handle tunable arguments + if isinstance(arg, Tunable): + continue + + # Handle numpy arrays and other array types + if not isinstance(arg, (np.ndarray, np.generic, cp.ndarray, torch.Tensor, DeviceArray)): + if arg.__class__.__name__ == "VectorValue": + continue # skip for Julia, types are commonly not specified in the kernel arguments + raise TypeError( + f"Argument at position {i} of type: {type(arg)} should be of type " + "np.ndarray, numpy scalar, HIP Python DeviceArray, Julia VectorValue type" + ) + + correct = True + if isinstance(arg, np.ndarray): + if "*" not in kernel_argument: + correct = False + + if isinstance(arg, DeviceArray): + str_dtype = str(np.dtype(arg.typestr)) + else: + str_dtype = str(arg.dtype) + + if correct and check_argument_type(str_dtype, kernel_argument): + continue + + collected_errors[arguments_set].append( + f"Argument at position {i} of dtype: {str_dtype} does not match {kernel_argument}." ) - correct = True - if isinstance(arg, np.ndarray): - if "*" not in kernel_argument: - correct = False - - if isinstance(arg, DeviceArray): - str_dtype = str(np.dtype(arg.typestr)) - else: - str_dtype = str(arg.dtype) - - if correct and check_argument_type(str_dtype, kernel_argument): - continue - - collected_errors[arguments_set].append( - f"Argument at position {i} of dtype: {str_dtype} does not match {kernel_argument}." - ) - if not collected_errors[arguments_set]: # We assume that if there is a possible list of arguments that matches with the provided one # it is the right one diff --git a/pyproject.toml b/pyproject.toml index e7691b128..1f9c7fe07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "poetry.core.masonry.api" [project] name = "kernel_tuner" description = "An easy to use CUDA/OpenCL kernel tuner in Python" -version = "1.4.0" # adhere to PEP440 versioning: https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/#id55 +version = "1.5.0" # adhere to PEP440 versioning: https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/#id55 readme = "README.md" license = "Apache-2.0" authors = [ From 79009bdeabd226f222c4c9784b7f93d00cbc3165 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Wed, 22 Jul 2026 18:38:28 +0200 Subject: [PATCH 098/146] Implemented the CPU backend for Julia --- kernel_tuner/backends/julia.py | 86 +++++++++++++++++++-------- kernel_tuner/backends/julia_helper.py | 12 ++++ 2 files changed, 73 insertions(+), 25 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index ad4a4064a..ce54ad0a2 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -63,10 +63,13 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None backend_name = compiler_options[0].upper() else: if len(self.available_backends) != 1: - raise ValueError( - f"Multiple or no Julia backends detected: {self.available_backends}. " - "Please specify exactly one backend in compiler_options." - ) + if "CPU" in self.available_backends and len(self.available_backends) == 2: + self.available_backends.remove("CPU") + else: + raise ValueError( + f"Multiple or no Julia backends detected: {self.available_backends}. " + "Please specify exactly one backend in compiler_options." + ) backend_name = self.available_backends[0] # Initialize backend attributes @@ -101,17 +104,31 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None for observer in self.observers: observer.register_device(self) - jl.seval( - f""" - global dest_tmp, src_tmp # for memcpy_htod - module KernelTunerHelper - using {self.backend_mod_name} - const kt_julia_backend = {self.backend_mod_instname}() - const GPUArrayType = {self.GPUArrayType} - include("{str(Path(__file__).parent / "julia_helper.jl")}") - end - """ - ) + # setup Julia module for kernel launch + if self.backend_mod_name == "CPU": + jl.seval( + f""" + global dest_tmp, src_tmp # for memcpy_htod + module KernelTunerHelper + using KernelAbstractions + const kt_julia_backend = CPU() + const GPUArrayType = {self.GPUArrayType} + include("{str(Path(__file__).parent / "julia_helper.jl")}") + end + """ + ) + else: + jl.seval( + f""" + global dest_tmp, src_tmp # for memcpy_htod + module KernelTunerHelper + using {self.backend_mod_name} + const kt_julia_backend = {self.backend_mod_instname}() + const GPUArrayType = {self.GPUArrayType} + include("{str(Path(__file__).parent / "julia_helper.jl")}") + end + """ + ) self.to_gpuarray = jl.KernelTunerHelper.to_gpuarray self.launch_kernel = jl.KernelTunerHelper.launch_kernel @@ -130,18 +147,23 @@ def initialize_backend(self, device, backend_name): if backend_name not in backend_map: raise ValueError(f"Unknown backend: {backend_name}") info = backend_map[backend_name] + backend_pkg = info["pkg"] - # Ensure the package is installed - self.check_package_and_install(info["pkg"]) + if backend_pkg is not None: + # Ensure the package is installed + self.check_package_and_install(backend_pkg) - # # Set debug level if needed - # if backend_name == "cuda": - # jl.seval("ENV[\"JULIA_CUDA_DEBUG\"] = \"2\"") + # # Set debug level if needed + # if backend_name == "cuda": + # jl.seval("ENV[\"JULIA_CUDA_DEBUG\"] = \"2\"") # Bring module into Python self.backend_mod_name = info["module"] self.backend_mod_instname = info["module_backend"] - jl.seval(f"using KernelAbstractions, {info['pkg']}") + # jl.seval(f"using KernelAbstractions, {info['module']}") + jl.seval(f"using KernelAbstractions") + if backend_pkg is not None: + jl.seval(f"using {info['module']}") backend_mod = getattr(jl.Main, self.backend_mod_name) self.backend_mod = backend_mod jl.seval(f"tmp_arr = {info['GPUArrayType']}(Float32.(zeros(2)))") @@ -151,7 +173,7 @@ def initialize_backend(self, device, backend_name): # Select device try: - if int(device) == 0 and not info["pkg"] == "CUDA": + if int(device) == 0 and not backend_pkg == "CUDA": device = 1 # Julia uses 1-based indexing, but the CUDA backend uses 0-based so we skip that jl.seval(info["device_select"](int(device))) self.last_selected_device = device @@ -182,7 +204,10 @@ def initialize_backend(self, device, backend_name): self.max_threads = None # Get the device and context - self.backend_device = self.backend_mod.device() + try: + self.backend_device = self.backend_mod.device() + except Exception: + self.backend_device = None if backend_name == "CUDA": self.contextqueue = self.backend_mod.context # elif backend_name == "AMD": @@ -216,6 +241,10 @@ def initialize_backend(self, device, backend_name): elif backend_name == "METAL": self.start_evt = self.start_event self.end_evt = self.stop_event + elif backend_name == "CPU": + # CPU, use host-side timing + self.start_evt = None + self.end_evt = None else: raise NotImplementedError(f"Backend {backend_name} not supported in Julia backend.") @@ -267,12 +296,19 @@ def compile(self, kernel_instance): self.check_package_and_install(package) # Wrap in a module to avoid name conflicts - module_code = f""" + if self.backend_mod_name == "CPU": + module_code = f""" +module KernelTunerUserKernel + {kernel_code} +end + """ + else: + module_code = f""" module KernelTunerUserKernel using {self.backend_mod_name} {kernel_code} end - """ + """ try: jl.seval(module_code) self.current_kernel = jl.seval(f"KernelTunerUserKernel.{kernel_name}") diff --git a/kernel_tuner/backends/julia_helper.py b/kernel_tuner/backends/julia_helper.py index 9f5a661f7..7cea89395 100644 --- a/kernel_tuner/backends/julia_helper.py +++ b/kernel_tuner/backends/julia_helper.py @@ -51,6 +51,16 @@ "capability": None, "GPUArrayType": "MtlArray", }, + "CPU": { + "pkg": None, + "module": "CPU", + "module_backend": "CPU", + "device_select": lambda d: "nothing", + "name": "CPU", + "max_threads": "1024", # arbitrary as CPU doesn't have a max threads per block concept + "capability": None, + "GPUArrayType": "Array", + }, } @@ -99,4 +109,6 @@ def detect_julia_gpu_backends(): available_backends.append(backend_name) except (FileNotFoundError, subprocess.CalledProcessError): pass + + available_backends.append("CPU") # always add CPU backend last return available_backends From 7278ebaf3627cd34669e1d1df178606497cc7bcb Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 27 Jul 2026 16:02:01 +0200 Subject: [PATCH 099/146] Now working with Nox --- noxfile.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/noxfile.py b/noxfile.py index 6c6d39f71..610c9fa78 100644 --- a/noxfile.py +++ b/noxfile.py @@ -141,12 +141,14 @@ def tests(session: Session) -> None: install_julia = True install_additional_tests = False small_disk = False + skip_gpu = False if session.posargs: for arg in session.posargs: if arg.lower() == "skip-gpu": install_cuda = False install_hip = False install_opencl = False + skip_gpu = True break elif arg.lower() == "skip-cuda": install_cuda = False @@ -326,9 +328,12 @@ def tests(session: Session) -> None: # call Julia to precompile packages in the session environment session.run("julia", "-e", "using Pkg; Pkg.precompile(); Pkg.instantiate()", external=True) # install any additional dependencies used by the tests, as `check_package_and_install` won't work from Nox - gpu_backends_string = "".join( - f'Pkg.add("{backend_map[backend]["pkg"]}"); ' for backend in detect_julia_gpu_backends() - ) + if not skip_gpu: + gpu_backends_string = "".join( + f'Pkg.add("{backend_map[backend]["pkg"]}"); ' if backend_map[backend]["pkg"] else "" for backend in detect_julia_gpu_backends() + ) + else: + gpu_backends_string = "" session.run("julia", "-e", f'using Pkg; Pkg.add("KernelAbstractions"); {gpu_backends_string}', external=True) # if applicable, install the dependencies for additional tests From e075ee8f609127fad29204c4fe9d14c295bce7c9 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Wed, 5 Aug 2026 20:07:38 +0200 Subject: [PATCH 100/146] Improved calculation of ndrange --- kernel_tuner/backends/julia.py | 10 ++++++++-- kernel_tuner/interface.py | 4 ++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index ce54ad0a2..4505601c5 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -330,11 +330,17 @@ def run_kernel(self, func, gpu_args, threads, grid, stream=None, params=None): args_tuple = tuple(gpu_args) params = tuple(params.values()) # important: the order of params must match the order in the kernel definition - # prepare ndrange and workgroupsize remove_trailing_ones = lambda tup: tup[ : len(tup) - next((int(i) for i, x in enumerate(reversed(tup)) if x != 1), len(tup)) ] - ndrange = remove_trailing_ones(grid) + + # Kernel Tuner's grid is number of workgroups; KernelAbstractions' ndrange is number of global work items + if len(grid) != len(threads): + raise ValueError(f"grid and threads must have equal rank: {grid=}, {threads=}") + global_size = tuple(int(g) * int(t) for g, t in zip(grid, threads)) + ndrange = remove_trailing_ones(global_size) + + # prepare launch parameters ndrange = (1,) if len(ndrange) == 0 else ndrange workgroupsize = remove_trailing_ones(threads) workgroupsize = (1,) if len(workgroupsize) == 0 else workgroupsize diff --git a/kernel_tuner/interface.py b/kernel_tuner/interface.py index f834321ac..9d2413e30 100644 --- a/kernel_tuner/interface.py +++ b/kernel_tuner/interface.py @@ -632,6 +632,7 @@ def tune_kernel( kernelsource = core.KernelSource(kernel_name, kernel_source, lang, defines) + # Convert Julia types if lang is not None and lang.upper() == "JULIA": # TODO implement & test the case where Kernel Tuner is called from Julia but the target language is not Julia if isinstance(tune_params, dict) or "DictValue" in tune_params.__class__.__name__: @@ -644,6 +645,9 @@ def tune_kernel( answer = [ numpy.array(a) if isinstance(a, (list, tuple)) else a for a in util.possible_julia_vector_to_list(answer) ] + grid_div_x = util.possible_julia_vector_to_list(grid_div_x) + grid_div_y = util.possible_julia_vector_to_list(grid_div_y) + grid_div_z = util.possible_julia_vector_to_list(grid_div_z) restrictions = util.possible_julia_vector_to_list(restrictions) block_size_names = util.possible_julia_vector_to_list(block_size_names) From fa3f18f3806915bcc147175c746ee98659381058 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Thu, 6 Aug 2026 15:12:01 +0200 Subject: [PATCH 101/146] Improved parameter passing structure via immutable JuliaKernel class to prevent parallelization issues --- kernel_tuner/backends/compiler.py | 2 +- kernel_tuner/backends/cupy.py | 2 +- kernel_tuner/backends/hip/hip.py | 2 +- kernel_tuner/backends/hypertuner.py | 2 +- kernel_tuner/backends/julia.py | 25 ++++++++++++++++--------- kernel_tuner/backends/nvcuda.py | 2 +- kernel_tuner/backends/opencl.py | 2 +- kernel_tuner/backends/pycuda.py | 2 +- kernel_tuner/core.py | 9 ++++----- kernel_tuner/interface.py | 1 - 10 files changed, 27 insertions(+), 22 deletions(-) diff --git a/kernel_tuner/backends/compiler.py b/kernel_tuner/backends/compiler.py index 9b0c1c8b3..ec2b2da6f 100644 --- a/kernel_tuner/backends/compiler.py +++ b/kernel_tuner/backends/compiler.py @@ -335,7 +335,7 @@ def synchronize(self): """ pass - def run_kernel(self, func, c_args, threads, grid, stream=None, params=None): + def run_kernel(self, func, c_args, threads, grid, stream=None): """Runs the kernel once, returns whatever the kernel returns :param func: A C function compiled for this specific configuration diff --git a/kernel_tuner/backends/cupy.py b/kernel_tuner/backends/cupy.py index 72dfd52c4..91554c0e4 100644 --- a/kernel_tuner/backends/cupy.py +++ b/kernel_tuner/backends/cupy.py @@ -187,7 +187,7 @@ def copy_texture_memory_args(self, texmem_args): """ raise NotImplementedError("CuPy backend does not support texture memory") - def run_kernel(self, func, gpu_args, threads, grid, stream=None, params=None): + def run_kernel(self, func, gpu_args, threads, grid, stream=None): """Runs the CUDA kernel passed as 'func'. :param func: A cupy kernel compiled for this specific kernel configuration diff --git a/kernel_tuner/backends/hip/hip.py b/kernel_tuner/backends/hip/hip.py index f76db09cd..848b4d46b 100644 --- a/kernel_tuner/backends/hip/hip.py +++ b/kernel_tuner/backends/hip/hip.py @@ -265,7 +265,7 @@ def synchronize(self): hip_check(hip.hipDeviceSynchronize()) - def run_kernel(self, func, gpu_args, threads, grid, stream=None, params=None): + def run_kernel(self, func, gpu_args, threads, grid, stream=None): """Runs the HIP kernel passed as 'func'. :param func: A HIP kernel compiled for this specific kernel configuration diff --git a/kernel_tuner/backends/hypertuner.py b/kernel_tuner/backends/hypertuner.py index 7762ea596..1e7566d59 100644 --- a/kernel_tuner/backends/hypertuner.py +++ b/kernel_tuner/backends/hypertuner.py @@ -159,7 +159,7 @@ def kernel_finished(self): def synchronize(self): return super().synchronize() - def run_kernel(self, func, gpu_args=None, threads=None, grid=None, stream=None, params=None): + def run_kernel(self, func, gpu_args=None, threads=None, grid=None, stream=None): # from cProfile import Profile # # generate the experiments file diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 4505601c5..7f7c164ee 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -14,6 +14,7 @@ from pathlib import Path from warnings import warn +from dataclasses import dataclass import numpy as np @@ -30,6 +31,13 @@ jl = None +@dataclass(frozen=True) +class JuliaKernel: + """Immutable Julia kernel representation, particularly useful for caching and parallel tuning.""" + function: object + params: tuple + + class JuliaFunctions(GPUBackend): """Backend for running Julia kernels (CUDA.jl) through JuliaCall.""" @@ -99,7 +107,7 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None stream=self.stream, start_event=self.start_evt, end_event=self.end_evt, - ) + ) # TODO this single stateful default observer currently prevents parallel tuning ) for observer in self.observers: observer.register_device(self) @@ -311,8 +319,8 @@ def compile(self, kernel_instance): """ try: jl.seval(module_code) - self.current_kernel = jl.seval(f"KernelTunerUserKernel.{kernel_name}") - return self.current_kernel + function = jl.seval(f"KernelTunerUserKernel.{kernel_name}") + return JuliaKernel(function, tuple(kernel_instance.params.values())) # important: the order of params must match the order in the kernel definition except Exception as e: raise SkippableFailure(f"Failed to compile Julia kernel: {e} \n{module_code}") @@ -320,15 +328,14 @@ def compile(self, kernel_instance): # Kernel launch and timing # ------------------------- - def run_kernel(self, func, gpu_args, threads, grid, stream=None, params=None): + def run_kernel(self, func, gpu_args, threads, grid, stream=None): """Launch a compiled Julia kernel.""" - if func is None: - func = self.current_kernel - if func is None: + if func is None or not isinstance(func, JuliaKernel): raise RuntimeError("No Julia kernel compiled or provided.") args_tuple = tuple(gpu_args) - params = tuple(params.values()) # important: the order of params must match the order in the kernel definition + julia_func = func.function + params = func.params remove_trailing_ones = lambda tup: tup[ : len(tup) - next((int(i) for i, x in enumerate(reversed(tup)) if x != 1), len(tup)) @@ -348,7 +355,7 @@ def run_kernel(self, func, gpu_args, threads, grid, stream=None, params=None): # run the kernel try: self.host_time = self.launch_kernel( - func, + julia_func, args_tuple, params, ndrange, diff --git a/kernel_tuner/backends/nvcuda.py b/kernel_tuner/backends/nvcuda.py index a0e7c4b8d..f9f28def3 100644 --- a/kernel_tuner/backends/nvcuda.py +++ b/kernel_tuner/backends/nvcuda.py @@ -363,7 +363,7 @@ def copy_texture_memory_args(self, texmem_args): """ raise NotImplementedError("NVIDIA CUDA backend does not support texture memory") - def run_kernel(self, func, gpu_args, threads, grid, stream=None, params=None): + def run_kernel(self, func, gpu_args, threads, grid, stream=None): """Runs the CUDA kernel passed as 'func'. :param func: A CUDA kernel compiled for this specific kernel configuration diff --git a/kernel_tuner/backends/opencl.py b/kernel_tuner/backends/opencl.py index 6a8fdaf37..5f7ea83f5 100644 --- a/kernel_tuner/backends/opencl.py +++ b/kernel_tuner/backends/opencl.py @@ -129,7 +129,7 @@ def synchronize(self): """Halts execution until device has finished its tasks.""" self.queue.finish() - def run_kernel(self, func, gpu_args, threads, grid, stream=None, params=None): + def run_kernel(self, func, gpu_args, threads, grid, stream=None): """Runs the OpenCL kernel passed as 'func'. :param func: An OpenCL Kernel diff --git a/kernel_tuner/backends/pycuda.py b/kernel_tuner/backends/pycuda.py index 467022361..1fc8c6ab1 100644 --- a/kernel_tuner/backends/pycuda.py +++ b/kernel_tuner/backends/pycuda.py @@ -327,7 +327,7 @@ def copy_texture_memory_args(self, texmem_args): if "normalized_coordinates" in v and v["normalized_coordinates"]: tex.set_flags(tex.get_flags() | drv.TRSF_NORMALIZED_COORDINATES) - def run_kernel(self, func, gpu_args, threads, grid, stream=None, params=None): + def run_kernel(self, func, gpu_args, threads, grid, stream=None): """Runs the CUDA kernel passed as 'func'. :param func: A PyCuda kernel compiled for this specific kernel configuration diff --git a/kernel_tuner/core.py b/kernel_tuner/core.py index 0024d75fe..f01e400a4 100644 --- a/kernel_tuner/core.py +++ b/kernel_tuner/core.py @@ -415,7 +415,7 @@ def benchmark_prologue(self, func, gpu_args, threads, grid, result): for obs in self.prologue_observers: self.dev.synchronize() obs.before_start() - self.dev.run_kernel(func, gpu_args, threads, grid, params=self.last_instance_params) + self.dev.run_kernel(func, gpu_args, threads, grid) self.dev.synchronize() obs.after_finish() result.update(obs.get_results()) @@ -428,7 +428,7 @@ def benchmark_default(self, func, gpu_args, threads, grid, result): obs.before_start() self.dev.synchronize() self.dev.start_event() - self.dev.run_kernel(func, gpu_args, threads, grid, params=self.last_instance_params) + self.dev.run_kernel(func, gpu_args, threads, grid) self.dev.stop_event() for obs in self.benchmark_observers: obs.after_start() @@ -451,7 +451,7 @@ def benchmark_continuous(self, func, gpu_args, threads, grid, result, duration): obs.before_start() self.dev.start_event() for _ in range(iterations): - self.dev.run_kernel(func, gpu_args, threads, grid, params=self.last_instance_params) + self.dev.run_kernel(func, gpu_args, threads, grid) self.dev.stop_event() for obs in self.continuous_observers: obs.after_start() @@ -646,7 +646,6 @@ def compile_and_benchmark(self, kernel_source, gpu_args, params, kernel_options, logging.debug("compile_and_benchmark " + instance_string) instance = self.create_kernel_instance(kernel_source, kernel_options, params, verbose) - self.last_instance_params = params if isinstance(instance, util.ErrorConfig): result['__error__'] = util.InvalidConfig() else: @@ -839,7 +838,7 @@ def run_kernel(self, func, gpu_args, instance): logging.debug("grid dims (%d, %d, %d)", *instance.grid) try: - self.dev.run_kernel(func, gpu_args, instance.threads, instance.grid, params=self.last_instance_params) + self.dev.run_kernel(func, gpu_args, instance.threads, instance.grid) except Exception as e: if "too many resources requested for launch" in str(e) or "OUT_OF_RESOURCES" in str(e): logging.debug("ignoring runtime failure due to too many resources required") diff --git a/kernel_tuner/interface.py b/kernel_tuner/interface.py index 9d2413e30..6e33481d9 100644 --- a/kernel_tuner/interface.py +++ b/kernel_tuner/interface.py @@ -965,7 +965,6 @@ def run_kernel( # detect language and create the right device function interface dev = core.DeviceInterface(kernelsource, iterations=1, **device_options) - dev.last_instance_params = params # Preprocess GPU arguments. Require for handling `Tunable` arguments arguments = dev.preprocess_gpu_arguments(arguments, params) From c2a7fcfafab395d07e01bf372be4119495fd63d1 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Thu, 6 Aug 2026 15:45:59 +0200 Subject: [PATCH 102/146] Added conversion of strategy options --- kernel_tuner/interface.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/kernel_tuner/interface.py b/kernel_tuner/interface.py index 6e33481d9..9ac3452bc 100644 --- a/kernel_tuner/interface.py +++ b/kernel_tuner/interface.py @@ -639,8 +639,7 @@ def tune_kernel( raise ValueError( "tune_params should not be a Julia dict, because it does not preserve order. Use a list of pairs instead." ) - tune_params = [tuple([k, util.possible_julia_vector_to_list(tp)]) for k, tp in tune_params] - tune_params = dict(tune_params) + tune_params = dict([tuple([k, util.possible_julia_vector_to_list(tp)]) for k, tp in tune_params]) if answer is not None: answer = [ numpy.array(a) if isinstance(a, (list, tuple)) else a for a in util.possible_julia_vector_to_list(answer) @@ -648,6 +647,8 @@ def tune_kernel( grid_div_x = util.possible_julia_vector_to_list(grid_div_x) grid_div_y = util.possible_julia_vector_to_list(grid_div_y) grid_div_z = util.possible_julia_vector_to_list(grid_div_z) + if strategy_options is not None: + strategy_options = dict([tuple([k, util.possible_julia_vector_to_list(o)]) for k, o in strategy_options]) restrictions = util.possible_julia_vector_to_list(restrictions) block_size_names = util.possible_julia_vector_to_list(block_size_names) From d171e13248cf4aecf244c93bca71c0bd43d38053 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Fri, 7 Aug 2026 14:15:46 +0200 Subject: [PATCH 103/146] Fix Julia undue argument list warning --- kernel_tuner/util.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/kernel_tuner/util.py b/kernel_tuner/util.py index d3450a7aa..da237d3be 100644 --- a/kernel_tuner/util.py +++ b/kernel_tuner/util.py @@ -178,6 +178,10 @@ def check_argument_list(kernel_name, kernel_string, args, lang=None): # Check each set of kernel arguments for arguments_set, arguments in enumerate(kernel_arguments): + + # check arguments and signature lengths + if lang.upper() == "JULIA" and len(arguments) > len(args): + continue # for Julia tunable parameters are added to the kernel signature collected_errors.append(list()) if len(arguments) != len(args): collected_errors[arguments_set].append( From f2acd8887cb65b10849dbc5e288c3bb048a4f6e3 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 10 Aug 2026 09:31:17 +0200 Subject: [PATCH 104/146] Restored linting in noxfile to commented --- noxfile.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/noxfile.py b/noxfile.py index 610c9fa78..84d929732 100644 --- a/noxfile.py +++ b/noxfile.py @@ -86,13 +86,13 @@ def create_settings(session: Session) -> None: nox.options.envdir = envdir -@session # to only run on the current python interpreter -def lint(session: Session) -> None: - """Ensure the code is formatted as expected.""" - session.install("ruff") - session.warn("Linting errors detected:") - session.run(*"ruff check . --config=pyproject.toml --statistics --exit-zero".split()) - # session.run(*"ruff check --config=pyproject.toml --output-format=github .".split()) +# @session # to only run on the current python interpreter +# def lint(session: Session) -> None: +# """Ensure the code is formatted as expected.""" +# session.install("ruff") +# session.warn("Linting errors detected:") +# session.run(*"ruff check . --config=pyproject.toml --statistics --exit-zero".split()) +# session.run(*"ruff check --config=pyproject.toml --output-format=github .".split()) @session # to only run on the current python interpreter From 4c204a492e9622496398dac1eb8883c832492915 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 10 Aug 2026 09:41:03 +0200 Subject: [PATCH 105/146] Removed unused parameters from backend interface --- kernel_tuner/backends/backend.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/kernel_tuner/backends/backend.py b/kernel_tuner/backends/backend.py index ff875efd2..6063dbb43 100644 --- a/kernel_tuner/backends/backend.py +++ b/kernel_tuner/backends/backend.py @@ -1,5 +1,4 @@ """This module contains the interface of all kernel_tuner backends.""" - from __future__ import print_function from abc import ABC, abstractmethod @@ -39,7 +38,7 @@ def synchronize(self): pass @abstractmethod - def run_kernel(self, func, gpu_args, threads, grid, stream, params): + def run_kernel(self, func, gpu_args, threads, grid, stream): """This method must implement the execution of the kernel on the device.""" pass From b1d1245ae79580378480303a511b632ae8293eea Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 10 Aug 2026 10:10:04 +0200 Subject: [PATCH 106/146] Added Julia setup to github action test --- .github/workflows/test-python-package.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/test-python-package.yml b/.github/workflows/test-python-package.yml index f86240c7f..6a9853ca4 100644 --- a/.github/workflows/test-python-package.yml +++ b/.github/workflows/test-python-package.yml @@ -30,9 +30,18 @@ jobs: - name: Setup Poetry uses: Gr1N/setup-poetry@v9 - run: poetry self add poetry-plugin-export + # - name: Run tests with Nox (no Julia) + # run: | + # pip install nox-poetry + # nox -- skip-gpu skip-julia github-action + - uses: actions/checkout@v6 + - uses: julia-actions/setup-julia@v3 + with: + version: '1.11' - name: Run tests with Nox run: | pip install nox-poetry + nox -- skip-gpu skip-julia github-action nox -- skip-gpu github-action # - name: Upload Coverage report to CodeCov # uses: codecov/codecov-action@v3 From 0858f6a849f87c99cd5e2a02ea3e29efda4d452f Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 10 Aug 2026 10:45:48 +0200 Subject: [PATCH 107/146] Added SonarQube to project settings --- .vscode/settings.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index 5ac233f9e..f5337c24f 100755 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -25,4 +25,8 @@ ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, + "sonarlint.connectedMode.project": { + "projectKey": "KernelTuner_kernel_tuner", + "connectionId": "kerneltuner", + } } \ No newline at end of file From 2093fbeff48cc83e494381bb82a01bc241b45ceb Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 10 Aug 2026 10:46:50 +0200 Subject: [PATCH 108/146] Fixed an issue where parameter could be None --- kernel_tuner/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel_tuner/util.py b/kernel_tuner/util.py index da237d3be..aac0d2e36 100644 --- a/kernel_tuner/util.py +++ b/kernel_tuner/util.py @@ -180,7 +180,7 @@ def check_argument_list(kernel_name, kernel_string, args, lang=None): for arguments_set, arguments in enumerate(kernel_arguments): # check arguments and signature lengths - if lang.upper() == "JULIA" and len(arguments) > len(args): + if lang and lang.upper() == "JULIA" and len(arguments) > len(args): continue # for Julia tunable parameters are added to the kernel signature collected_errors.append(list()) if len(arguments) != len(args): From 038ce1c2617879995df4fafbcbc9257080a2bcd8 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 10 Aug 2026 10:47:14 +0200 Subject: [PATCH 109/146] Fixed an issue with pymoo module name --- kernel_tuner/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel_tuner/util.py b/kernel_tuner/util.py index aac0d2e36..4399215ed 100644 --- a/kernel_tuner/util.py +++ b/kernel_tuner/util.py @@ -570,7 +570,7 @@ def get_pareto_results( objective_higher_is_better: list[bool], mark_optima=True ): - from pymoo.util.nds.find_non_dominated import find_non_dominated + from pymoo.util.nds.non_dominated_sorting import find_non_dominated assert isinstance(results, list) assert isinstance(objectives, list) From 1e39e3cdc70bdbb88821443ee5bd27f295c98284 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 10 Aug 2026 10:47:36 +0200 Subject: [PATCH 110/146] Added comments to Julia warmup observer --- kernel_tuner/observers/julia.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/kernel_tuner/observers/julia.py b/kernel_tuner/observers/julia.py index 1035f5407..4e9585970 100644 --- a/kernel_tuner/observers/julia.py +++ b/kernel_tuner/observers/julia.py @@ -114,12 +114,15 @@ class JuliaJITWarmup(PrologueObserver): """Prologue observer to enforce warmup before every configuration to trigger JIT.""" def __init__(self, backend): + """Not implemented, just to trigger JIT.""" pass def before_start(self): + """Not implemented, just to trigger JIT.""" pass def after_finish(self): + """Not implemented, just to trigger JIT.""" pass def get_results(self): From 52836cc5773d97de660a7c489db237a0c87ff574 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 10 Aug 2026 11:50:29 +0200 Subject: [PATCH 111/146] Refactored to simplify --- kernel_tuner/backends/julia_helper.py | 87 +++++++++++++++++---------- 1 file changed, 56 insertions(+), 31 deletions(-) diff --git a/kernel_tuner/backends/julia_helper.py b/kernel_tuner/backends/julia_helper.py index 7cea89395..083d40b23 100644 --- a/kernel_tuner/backends/julia_helper.py +++ b/kernel_tuner/backends/julia_helper.py @@ -69,46 +69,71 @@ def detect_julia_gpu_backends(): available_backends = [] for backend_name in ["CUDA", "AMD", "METAL", "INTEL"]: if backend_name == "CUDA": - try: - subprocess.check_output("nvidia-smi") + if julia_backend_available_cuda(): available_backends.append(backend_name) - except (FileNotFoundError, subprocess.CalledProcessError): - pass elif backend_name == "AMD": - try: - subprocess.check_output("rocm-smi") + if julia_backend_available_amd(): available_backends.append(backend_name) - except (FileNotFoundError, subprocess.CalledProcessError): - pass elif backend_name == "METAL": - try: - output = subprocess.check_output("system_profiler -json SPDisplaysDataType".split()) - json_output = json_loads(output)["SPDisplaysDataType"] - for gpu in json_output: - if "spdisplays_mtlgpufamilysupport" in gpu: - supported = gpu["spdisplays_mtlgpufamilysupport"].lower() - if "metal" in supported: - version = regex_search(r".*metal([\d.]+)", supported).group(1) - if float(version) < 3: - warn( - f"Metal backend detected, but {supported} < 3. " - "Metal.jl requires Metal version 3 or higher." - ) - else: - available_backends.append(backend_name) - except (FileNotFoundError, subprocess.CalledProcessError, JSONDecodeError): - pass + if julia_backend_available_metal(): + available_backends.append(backend_name) elif backend_name == "INTEL": # this can give false positives for other backends too, so skip if we've already detected another backend if len(available_backends) > 0: continue - try: - subprocess.check_output( - "ls /dev/dri/by-path/".split() - ) # not a perfect check but should work in most cases + if julia_backend_available_intel(): available_backends.append(backend_name) - except (FileNotFoundError, subprocess.CalledProcessError): - pass available_backends.append("CPU") # always add CPU backend last return available_backends + + +def julia_backend_available_cuda(): + """Check if CUDA backend is available.""" + try: + subprocess.check_output("nvidia-smi") + return True + except (FileNotFoundError, subprocess.CalledProcessError): + return False + + +def julia_backend_available_amd(): + """Check if AMD backend is available.""" + try: + subprocess.check_output("rocm-smi") + return True + except (FileNotFoundError, subprocess.CalledProcessError): + return False + + +def julia_backend_available_metal(): + """Check if Metal backend is available.""" + try: + output = subprocess.check_output("system_profiler -json SPDisplaysDataType".split()) + json_output = json_loads(output)["SPDisplaysDataType"] + for gpu in json_output: + if "spdisplays_mtlgpufamilysupport" in gpu: + supported = gpu["spdisplays_mtlgpufamilysupport"].lower() + if "metal" in supported: + version = regex_search(r".*metal([\d.]+)", supported).group(1) + if float(version) < 3: + warn( + f"Metal backend detected, but {supported} < 3. " + "Metal.jl requires Metal version 3 or higher." + ) + else: + return True + except (FileNotFoundError, subprocess.CalledProcessError, JSONDecodeError): + pass + return False + + +def julia_backend_available_intel(): + """Check if Intel backend is available. May give false positives if other backends are present.""" + try: + subprocess.check_output( + "ls /dev/dri/by-path/".split() + ) # not a perfect check but should work in most cases + return True + except (FileNotFoundError, subprocess.CalledProcessError): + return False From 8160ada3bf9d4f77308481201e65befbe89ba348 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 10 Aug 2026 11:53:17 +0200 Subject: [PATCH 112/146] Refactored to simplify --- kernel_tuner/backends/julia_helper.py | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/kernel_tuner/backends/julia_helper.py b/kernel_tuner/backends/julia_helper.py index 083d40b23..54da0c762 100644 --- a/kernel_tuner/backends/julia_helper.py +++ b/kernel_tuner/backends/julia_helper.py @@ -67,22 +67,16 @@ def detect_julia_gpu_backends(): """Detect the Julia backends available.""" available_backends = [] - for backend_name in ["CUDA", "AMD", "METAL", "INTEL"]: - if backend_name == "CUDA": - if julia_backend_available_cuda(): - available_backends.append(backend_name) - elif backend_name == "AMD": - if julia_backend_available_amd(): - available_backends.append(backend_name) - elif backend_name == "METAL": - if julia_backend_available_metal(): - available_backends.append(backend_name) - elif backend_name == "INTEL": - # this can give false positives for other backends too, so skip if we've already detected another backend - if len(available_backends) > 0: - continue - if julia_backend_available_intel(): - available_backends.append(backend_name) + if julia_backend_available_cuda(): + available_backends.append(backend_name) + if julia_backend_available_amd(): + available_backends.append(backend_name) + if julia_backend_available_metal(): + available_backends.append(backend_name) + if len(available_backends) == 0: + # this can give false positives for other backends too, so skip if we've already detected another backend + if julia_backend_available_intel(): + available_backends.append(backend_name) available_backends.append("CPU") # always add CPU backend last return available_backends From ec5add557a78a55e0c8d957d2f15a8fa87da8f52 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 10 Aug 2026 12:02:47 +0200 Subject: [PATCH 113/146] Various improvements to code quality --- kernel_tuner/backends/julia.py | 19 ++++++++++--------- kernel_tuner/backends/julia_helper.py | 3 ++- kernel_tuner/core.py | 12 +++++++----- kernel_tuner/observers/julia.py | 3 ++- kernel_tuner/util.py | 14 ++++++++------ 5 files changed, 29 insertions(+), 22 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 7f7c164ee..50dd3c6ab 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -169,7 +169,7 @@ def initialize_backend(self, device, backend_name): self.backend_mod_name = info["module"] self.backend_mod_instname = info["module_backend"] # jl.seval(f"using KernelAbstractions, {info['module']}") - jl.seval(f"using KernelAbstractions") + jl.seval("using KernelAbstractions") if backend_pkg is not None: jl.seval(f"using {info['module']}") backend_mod = getattr(jl.Main, self.backend_mod_name) @@ -181,8 +181,9 @@ def initialize_backend(self, device, backend_name): # Select device try: - if int(device) == 0 and not backend_pkg == "CUDA": - device = 1 # Julia uses 1-based indexing, but the CUDA backend uses 0-based so we skip that + if int(device) == 0 and backend_pkg != "CUDA": + # Julia uses 1-based indexing, but the CUDA backend uses 0-based so we skip that + device = 1 jl.seval(info["device_select"](int(device))) self.last_selected_device = device except Exception as e: @@ -427,12 +428,12 @@ def synchronize(self): @staticmethod def memset(allocation, value, size): raise NotImplementedError("memset not yet implemented for Julia backend.") - try: - jl.allocation_tmp = allocation - jl.seval(f"CUDA.fill!(allocation_tmp, {int(value)})") - del jl.allocation_tmp - except JuliaError as e: - raise RuntimeError(f"Julia memset failed: {e}") + # try: + # jl.allocation_tmp = allocation + # jl.seval(f"CUDA.fill!(allocation_tmp, {int(value)})") + # del jl.allocation_tmp + # except JuliaError as e: + # raise RuntimeError(f"Julia memset failed: {e}") @staticmethod def memcpy_dtoh(dest, src): diff --git a/kernel_tuner/backends/julia_helper.py b/kernel_tuner/backends/julia_helper.py index 54da0c762..d621f60c8 100644 --- a/kernel_tuner/backends/julia_helper.py +++ b/kernel_tuner/backends/julia_helper.py @@ -125,9 +125,10 @@ def julia_backend_available_metal(): def julia_backend_available_intel(): """Check if Intel backend is available. May give false positives if other backends are present.""" try: + # not a perfect check but should work in most cases subprocess.check_output( "ls /dev/dri/by-path/".split() - ) # not a perfect check but should work in most cases + ) return True except (FileNotFoundError, subprocess.CalledProcessError): return False diff --git a/kernel_tuner/core.py b/kernel_tuner/core.py index f01e400a4..a73d0e5e1 100644 --- a/kernel_tuner/core.py +++ b/kernel_tuner/core.py @@ -219,20 +219,22 @@ def check_argument_lists(self, kernel_name, arguments): def infer_julia_backend(self): """Infer the Julia backend from the kernel source.""" + backend = None if self.lang.upper() != "JULIA": - return None + return backend kernel_string = self.get_kernel_string(0) if kernel_string.find("using CUDA") != -1: - return "cuda" + backend = "cuda" elif kernel_string.find("using ROCBackend") != -1: - return "amd" + backend = "amd" elif kernel_string.find("using oneAPI") != -1: - return "intel" + backend = "intel" elif kernel_string.find("using Metal") != -1: - return "metal" + backend = "metal" else: raise ValueError("Could not infer Julia backend from kernel source, provide it as a `compiler_option`") + return backend def instantiate_observer(observer, args): diff --git a/kernel_tuner/observers/julia.py b/kernel_tuner/observers/julia.py index 4e9585970..16fa11991 100644 --- a/kernel_tuner/observers/julia.py +++ b/kernel_tuner/observers/julia.py @@ -70,9 +70,10 @@ def after_finish(self): if self.name == "metal": ms_observer = float((self.end() - self.t0) * 1000.0) ms_helper = self.kt_backend.host_time + # take the minimum of the two measurements to mitigate overhead of command buffer timing ms = min( ms_observer, ms_helper - ) # take the minimum of the two measurements to mitigate overhead of command buffer timing + ) elif self.name == "cuda": # the events are recorded in the julia_helper kernel launch code ms = float(self.backend_mod.elapsed(self.start, self.end) * 1000.0) diff --git a/kernel_tuner/util.py b/kernel_tuner/util.py index 4399215ed..f53aa5109 100644 --- a/kernel_tuner/util.py +++ b/kernel_tuner/util.py @@ -181,7 +181,8 @@ def check_argument_list(kernel_name, kernel_string, args, lang=None): # check arguments and signature lengths if lang and lang.upper() == "JULIA" and len(arguments) > len(args): - continue # for Julia tunable parameters are added to the kernel signature + # for Julia tunable parameters are added to the kernel signature + continue collected_errors.append(list()) if len(arguments) != len(args): collected_errors[arguments_set].append( @@ -201,16 +202,16 @@ def check_argument_list(kernel_name, kernel_string, args, lang=None): # Handle numpy arrays and other array types if not isinstance(arg, (np.ndarray, np.generic, torch.Tensor, DeviceArray) + cupy_ndarray): if arg.__class__.__name__ == "VectorValue": - continue # skip for Julia, types are commonly not specified in the kernel arguments + # skip for Julia, types are commonly not specified in the kernel arguments + continue raise TypeError( f"Argument at position {i} of type: {type(arg)} should be of type " "np.ndarray, numpy scalar, HIP Python DeviceArray, Julia VectorValue type" ) correct = True - if isinstance(arg, np.ndarray): - if "*" not in kernel_argument: - correct = False + if isinstance(arg, np.ndarray) and "*" not in kernel_argument: + correct = False if isinstance(arg, DeviceArray): str_dtype = str(np.dtype(arg.typestr)) @@ -1018,7 +1019,8 @@ def prepare_kernel_string(kernel_name, kernel_string, params, grid, threads, blo kernel_prefix += f"constexpr int {k} = {v};\n" elif lang.upper() == "JULIA": # kernel_prefix += f"const {k} = {v}\n" - pass # in Julia, we can't redefine constants like this, so we skip it and give it as arguments on the kernel launch + # in Julia, we can't redefine constants like this, so we skip it and give it as arguments on the kernel launch + pass else: kernel_prefix += f"#define {k} {v}\n" From 54b489230a4f0e3d28210721db4260e028facf97 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 10 Aug 2026 12:16:49 +0200 Subject: [PATCH 114/146] Much simplified _default_verify_function --- kernel_tuner/core.py | 47 +++++++++++--------------------------------- 1 file changed, 11 insertions(+), 36 deletions(-) diff --git a/kernel_tuner/core.py b/kernel_tuner/core.py index a73d0e5e1..458c83a12 100644 --- a/kernel_tuner/core.py +++ b/kernel_tuner/core.py @@ -956,15 +956,10 @@ def _flatten(a): result = _ravel(result_host[i]) expected = _flatten(expected) cp = _get_cupy() - if cp is not None and any([isinstance(array, cp.ndarray) for array in [expected, result]]): - expected_nan = cp.isnan(expected) - output_test = cp.allclose(expected, result, atol=atol, equal_nan=expected_nan.any()) - elif isinstance(expected, torch.Tensor) and isinstance(result, torch.Tensor): - expected_nan = torch.isnan(expected) - output_test = torch.allclose(expected, result, atol=atol, equal_nan=expected_nan.any()) - else: - expected_nan = np.isnan(expected) - output_test = np.allclose(expected, result, atol=atol, equal_nan=expected_nan.any()) + has_cp_array = any([isinstance(array, cp.ndarray) for array in [expected, result]]) + lib = cp if has_cp_array else torch if isinstance(expected, torch.Tensor) and isinstance(result, torch.Tensor) else np + expected_nan = lib.isnan(expected) + output_test = lib.allclose(expected, result, atol=atol, equal_nan=expected_nan.any()) if expected_nan.any(): warn( f"Answer contains {expected_nan.sum()} NaNs. NaN values will now be considered equal in comparison." @@ -980,35 +975,15 @@ def _flatten(a): print(f"Expected ({np.shape(expected)}):") print(expected) # check if there are NaNs in the output or expected, if so, print where they are - if cp is not None and any([isinstance(array, cp.ndarray) for array in [expected, result]]): - if cp.isnan(result).any(): - print("NaNs in kernel output at indices:", cp.where(cp.isnan(result))) - if cp.isnan(expected).any(): - print("NaNs in expected result at indices:", cp.where(cp.isnan(expected))) - elif isinstance(expected, torch.Tensor) and isinstance(result, torch.Tensor): - if torch.isnan(result).any(): - print("NaNs in kernel output at indices:", torch.where(torch.isnan(result))) - if torch.isnan(expected).any(): - print("NaNs in expected result at indices:", torch.where(torch.isnan(expected))) - else: - if np.isnan(result).any(): - print("NaNs in kernel output at indices:", np.where(np.isnan(result))) - if np.isnan(expected).any(): - print("NaNs in expected result at indices:", np.where(np.isnan(expected))) + if lib.isnan(result).any(): + print("NaNs in kernel output at indices:", lib.where(lib.isnan(result))) + if lib.isnan(expected).any(): + print("NaNs in expected result at indices:", lib.where(lib.isnan(expected))) # print only the elements that are different print("Difference at specific elements:") - if cp is not None and any([isinstance(array, cp.ndarray) for array in [expected, result]]): - diff = cp.abs(expected - result) - indices = cp.where(diff > atol) - print(diff[indices]) - elif isinstance(expected, torch.Tensor) and isinstance(result, torch.Tensor): - diff = torch.abs(expected - result) - indices = torch.where(diff > atol) - print(diff[indices]) - else: - diff = np.abs(expected - result) - indices = np.where(diff > atol) - print(diff[indices]) + diff = lib.abs(expected - result) + indices = lib.where(diff > atol) + print(diff[indices]) correct = correct and output_test if not correct: From a6183889e04777583fe5bc99fb75dc6669a95208 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 10 Aug 2026 12:19:52 +0200 Subject: [PATCH 115/146] Defined Julia GPU backend names --- kernel_tuner/backends/julia_helper.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/kernel_tuner/backends/julia_helper.py b/kernel_tuner/backends/julia_helper.py index d621f60c8..34b92a1d5 100644 --- a/kernel_tuner/backends/julia_helper.py +++ b/kernel_tuner/backends/julia_helper.py @@ -68,15 +68,15 @@ def detect_julia_gpu_backends(): """Detect the Julia backends available.""" available_backends = [] if julia_backend_available_cuda(): - available_backends.append(backend_name) + available_backends.append("CUDA") if julia_backend_available_amd(): - available_backends.append(backend_name) + available_backends.append("AMD") if julia_backend_available_metal(): - available_backends.append(backend_name) + available_backends.append("METAL") if len(available_backends) == 0: # this can give false positives for other backends too, so skip if we've already detected another backend if julia_backend_available_intel(): - available_backends.append(backend_name) + available_backends.append("INTEL") available_backends.append("CPU") # always add CPU backend last return available_backends From f1157be9df8e56da272a138ac2bab3540e52cc2a Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 10 Aug 2026 12:27:45 +0200 Subject: [PATCH 116/146] Fix an issue with unitialized cupy --- kernel_tuner/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel_tuner/core.py b/kernel_tuner/core.py index 458c83a12..d2b17f4cc 100644 --- a/kernel_tuner/core.py +++ b/kernel_tuner/core.py @@ -956,7 +956,7 @@ def _flatten(a): result = _ravel(result_host[i]) expected = _flatten(expected) cp = _get_cupy() - has_cp_array = any([isinstance(array, cp.ndarray) for array in [expected, result]]) + has_cp_array = False if not cp else any([isinstance(array, cp.ndarray) for array in [expected, result]]) lib = cp if has_cp_array else torch if isinstance(expected, torch.Tensor) and isinstance(result, torch.Tensor) else np expected_nan = lib.isnan(expected) output_test = lib.allclose(expected, result, atol=atol, equal_nan=expected_nan.any()) From 972ad0e30d87468d8b3ef56145a1d6844cbad949 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 10 Aug 2026 12:33:25 +0200 Subject: [PATCH 117/146] Split Julia backend streams initialization into separate function' --- kernel_tuner/backends/julia.py | 60 ++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 50dd3c6ab..6acbea17c 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -228,34 +228,7 @@ def initialize_backend(self, device, backend_name): elif backend_name == "METAL": self.contextqueue = self.backend_mod.MTLCommandQueue(self.backend_device) - # Optional: common KernelAbstractions stream abstraction - try: - self.stream = backend_mod.get_default_stream() - except Exception: - self.stream = None - - # Set up stream and event attributes for observers - if backend_name == "CUDA": - self.stream = backend_mod.stream() - self.start_evt = backend_mod.CuEvent - self.end_evt = backend_mod.CuEvent - elif backend_name == "AMD": - self.stream = backend_mod.stream() - self.start_evt = backend_mod.HIP.HIPEvent - self.end_evt = backend_mod.HIP.HIPEvent - elif backend_name == "INTEL": - # OneAPI: no events available - self.start_evt = None - self.end_evt = None - elif backend_name == "METAL": - self.start_evt = self.start_event - self.end_evt = self.stop_event - elif backend_name == "CPU": - # CPU, use host-side timing - self.start_evt = None - self.end_evt = None - else: - raise NotImplementedError(f"Backend {backend_name} not supported in Julia backend.") + self.setup_streams(backend_name) def __del__(self): # drop GPUArray references to let Julia GC handle them @@ -493,3 +466,34 @@ def create_metal_buffer(self): except Exception: buf = self.backend_mod.MTLCommandBuffer(self.contextqueue) return buf + + def setup_streams(self, backend_name: str): + """Set up stream and event attributes for observers.""" + # Optional: common KernelAbstractions stream abstraction + try: + self.stream = backend_mod.get_default_stream() + except Exception: + self.stream = None + + # Set up stream and event attributes for observers + if backend_name == "CUDA": + self.stream = backend_mod.stream() + self.start_evt = backend_mod.CuEvent + self.end_evt = backend_mod.CuEvent + elif backend_name == "AMD": + self.stream = backend_mod.stream() + self.start_evt = backend_mod.HIP.HIPEvent + self.end_evt = backend_mod.HIP.HIPEvent + elif backend_name == "INTEL": + # OneAPI: no events available + self.start_evt = None + self.end_evt = None + elif backend_name == "METAL": + self.start_evt = self.start_event + self.end_evt = self.stop_event + elif backend_name == "CPU": + # CPU, use host-side timing + self.start_evt = None + self.end_evt = None + else: + raise NotImplementedError(f"Backend {backend_name} not supported in Julia backend.") From ae70d6475d41d181e9bfa9d55e60827cee6bea71 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 10 Aug 2026 12:39:11 +0200 Subject: [PATCH 118/146] Split Julia backend streams initialization into separate function' --- kernel_tuner/backends/julia.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 6acbea17c..d262ad980 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -471,19 +471,19 @@ def setup_streams(self, backend_name: str): """Set up stream and event attributes for observers.""" # Optional: common KernelAbstractions stream abstraction try: - self.stream = backend_mod.get_default_stream() + self.stream = self.backend_mod.get_default_stream() except Exception: self.stream = None # Set up stream and event attributes for observers if backend_name == "CUDA": - self.stream = backend_mod.stream() - self.start_evt = backend_mod.CuEvent - self.end_evt = backend_mod.CuEvent + self.stream = self.backend_mod.stream() + self.start_evt = self.backend_mod.CuEvent + self.end_evt = self.backend_mod.CuEvent elif backend_name == "AMD": - self.stream = backend_mod.stream() - self.start_evt = backend_mod.HIP.HIPEvent - self.end_evt = backend_mod.HIP.HIPEvent + self.stream = self.backend_mod.stream() + self.start_evt = self.backend_mod.HIP.HIPEvent + self.end_evt = self.backend_mod.HIP.HIPEvent elif backend_name == "INTEL": # OneAPI: no events available self.start_evt = None From ee59bb9f82e0c23b07ac96a30f7fb96cd77bea6a Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 10 Aug 2026 13:10:08 +0200 Subject: [PATCH 119/146] Removed duplicate nox test instantiation --- .github/workflows/test-python-package.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test-python-package.yml b/.github/workflows/test-python-package.yml index 6a9853ca4..f64a7411d 100644 --- a/.github/workflows/test-python-package.yml +++ b/.github/workflows/test-python-package.yml @@ -41,7 +41,6 @@ jobs: - name: Run tests with Nox run: | pip install nox-poetry - nox -- skip-gpu skip-julia github-action nox -- skip-gpu github-action # - name: Upload Coverage report to CodeCov # uses: codecov/codecov-action@v3 From e431b369872313875193bb3e851bdb8dbf097ed0 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 10 Aug 2026 14:15:02 +0200 Subject: [PATCH 120/146] Split Julia tests into separate CI segment to reduce compute time --- .github/workflows/test-python-package.yml | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test-python-package.yml b/.github/workflows/test-python-package.yml index f64a7411d..800562100 100644 --- a/.github/workflows/test-python-package.yml +++ b/.github/workflows/test-python-package.yml @@ -24,24 +24,23 @@ jobs: os: [ubuntu-latest, macos-latest] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup Nox uses: fjwillemsen/setup-nox2@v3.0.0 - name: Setup Poetry uses: Gr1N/setup-poetry@v9 - run: poetry self add poetry-plugin-export - # - name: Run tests with Nox (no Julia) - # run: | - # pip install nox-poetry - # nox -- skip-gpu skip-julia github-action - - uses: actions/checkout@v6 + - name: Run tests with Nox + run: | + pip install nox-poetry + nox -- skip-gpu skip-julia github-action - uses: julia-actions/setup-julia@v3 with: - version: '1.11' - - name: Run tests with Nox + version: '1.12' + - name: Run Julia tests with Nox run: | pip install nox-poetry - nox -- skip-gpu github-action + nox --session "tests(python='3.14')" skip-gpu github-action # - name: Upload Coverage report to CodeCov # uses: codecov/codecov-action@v3 # with: From e202ea34aa62dd663f728bac3eb67018a63ba2c8 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 10 Aug 2026 14:44:47 +0200 Subject: [PATCH 121/146] Rewrote various functions to reduce complexity --- kernel_tuner/backends/julia.py | 70 +++++++++++++++------------ kernel_tuner/backends/julia_helper.py | 26 +++++----- 2 files changed, 53 insertions(+), 43 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index d262ad980..6eb474624 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -49,36 +49,10 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None if jl is None: raise ImportError("JuliaCall not installed. Please run `pip install juliacall`.") - # process compiler options - self.raise_errors = False - for c in compiler_options or []: - if c.lower().startswith("raise_errors="): - raise_errors_str = c.split("=", 1)[1].strip().lower() - if raise_errors_str in ("true", "1", "yes"): - self.raise_errors = True - elif raise_errors_str in ("false", "0", "no"): - self.raise_errors = False - else: - raise ValueError(f"Invalid value for raise_errors: {raise_errors_str}. Use true/false.") - compiler_options.remove(c) + # process passed options and backends + self.process_options(compiler_options) self.available_backends = detect_julia_gpu_backends() - if compiler_options is not None and len(compiler_options) == 1: - if compiler_options[0].upper() not in self.available_backends: - raise ValueError( - f"Requested Julia backend '{compiler_options[0]}' not available. " - f"Available backends: {self.available_backends}" - ) - backend_name = compiler_options[0].upper() - else: - if len(self.available_backends) != 1: - if "CPU" in self.available_backends and len(self.available_backends) == 2: - self.available_backends.remove("CPU") - else: - raise ValueError( - f"Multiple or no Julia backends detected: {self.available_backends}. " - "Please specify exactly one backend in compiler_options." - ) - backend_name = self.available_backends[0] + backend_name = self.verify_backends_with_options(compiler_options) # Initialize backend attributes self.device = device @@ -98,6 +72,7 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None # setup observers self.observers = observers or [] self.observers.append( + # TODO this single stateful default observer currently prevents parallel tuning JuliaRuntimeObserver( jl.Main.KernelAbstractions, self, @@ -107,7 +82,7 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None stream=self.stream, start_event=self.start_evt, end_event=self.end_evt, - ) # TODO this single stateful default observer currently prevents parallel tuning + ) ) for observer in self.observers: observer.register_device(self) @@ -497,3 +472,38 @@ def setup_streams(self, backend_name: str): self.end_evt = None else: raise NotImplementedError(f"Backend {backend_name} not supported in Julia backend.") + + def process_compiler_options(self, compiler_options=None): + """Process the given compiler options.""" + self.raise_errors = False + for c in compiler_options or []: + if c.lower().startswith("raise_errors="): + raise_errors_str = c.split("=", 1)[1].strip().lower() + if raise_errors_str in ("true", "1", "yes"): + self.raise_errors = True + elif raise_errors_str in ("false", "0", "no"): + self.raise_errors = False + else: + raise ValueError(f"Invalid value for raise_errors: {raise_errors_str}. Use true/false.") + compiler_options.remove(c) + + def verify_backends_with_options(self, compiler_options=None): + """Verify that the requested backend is available.""" + if compiler_options is not None and len(compiler_options) == 1: + if compiler_options[0].upper() not in self.available_backends: + raise ValueError( + f"Requested Julia backend '{compiler_options[0]}' not available. " + f"Available backends: {self.available_backends}" + ) + backend_name = compiler_options[0].upper() + else: + if len(self.available_backends) != 1: + if "CPU" in self.available_backends and len(self.available_backends) == 2: + self.available_backends.remove("CPU") + else: + raise ValueError( + f"Multiple or no Julia backends detected: {self.available_backends}. " + "Please specify exactly one backend in compiler_options." + ) + backend_name = self.available_backends[0] + return backend_name diff --git a/kernel_tuner/backends/julia_helper.py b/kernel_tuner/backends/julia_helper.py index 34b92a1d5..0b30bc7fd 100644 --- a/kernel_tuner/backends/julia_helper.py +++ b/kernel_tuner/backends/julia_helper.py @@ -105,20 +105,20 @@ def julia_backend_available_metal(): try: output = subprocess.check_output("system_profiler -json SPDisplaysDataType".split()) json_output = json_loads(output)["SPDisplaysDataType"] - for gpu in json_output: - if "spdisplays_mtlgpufamilysupport" in gpu: - supported = gpu["spdisplays_mtlgpufamilysupport"].lower() - if "metal" in supported: - version = regex_search(r".*metal([\d.]+)", supported).group(1) - if float(version) < 3: - warn( - f"Metal backend detected, but {supported} < 3. " - "Metal.jl requires Metal version 3 or higher." - ) - else: - return True except (FileNotFoundError, subprocess.CalledProcessError, JSONDecodeError): - pass + return False + for gpu in json_output: + if "spdisplays_mtlgpufamilysupport" in gpu: + supported = gpu["spdisplays_mtlgpufamilysupport"].lower() + if "metal" in supported: + version = regex_search(r".*metal([\d.]+)", supported).group(1) + if float(version) < 3: + warn( + f"Metal backend detected, but {supported} < 3. " + "Metal.jl requires Metal version 3 or higher." + ) + else: + return True return False From f4fca3512117db7566ba9a86cfb92c21a86b2122 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 10 Aug 2026 15:53:58 +0200 Subject: [PATCH 122/146] Rewrote various functions to reduce complexity --- kernel_tuner/core.py | 77 +++++++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 37 deletions(-) diff --git a/kernel_tuner/core.py b/kernel_tuner/core.py index d2b17f4cc..4e30194ef 100644 --- a/kernel_tuner/core.py +++ b/kernel_tuner/core.py @@ -508,12 +508,11 @@ def benchmark(self, func, gpu_args, instance, verbose, objective, skip_nvml_sett self.benchmark_prologue(func, gpu_args, instance.threads, instance.grid, result) self.benchmark_default(func, gpu_args, instance.threads, instance.grid, result) - if self.continuous_observers: - duration = 1 - for obs in self.continuous_observers: - obs.results = result - duration = max(duration, obs.continuous_duration) - + duration = 1 + for obs in self.continuous_observers: + obs.results = result + duration = max(duration, obs.continuous_duration) + if len(self.continuous_observers) > 0: self.benchmark_continuous(func, gpu_args, instance.threads, instance.grid, result, duration) except Exception as e: @@ -530,15 +529,14 @@ def benchmark(self, func, gpu_args, instance, verbose, objective, skip_nvml_sett ] if any([skip_str in str(e) for skip_str in skippable_exceptions]): logging.debug("benchmark fails due to runtime failure / too many resources required") - if verbose: - if "Julia" in str(e): - warn( - f"skipping config {util.get_instance_string(instance.params)} reason: Julia kernel launch failed because of:\n{e}" - ) - else: - print( - f"skipping config {util.get_instance_string(instance.params)} reason: too many resources requested for launch" - ) + if "julia" in str(e).lower() and verbose: + warn( + f"skipping config {util.get_instance_string(instance.params)} reason: Julia kernel launch failed because of:\n{e}" + ) + elif verbose: + print( + f"skipping config {util.get_instance_string(instance.params)} reason: too many resources requested for launch" + ) result['__error__'] = util.RuntimeFailedConfig() else: logging.debug("benchmark encountered runtime failure: " + str(e)) @@ -592,28 +590,7 @@ def check_kernel_output(self, func, gpu_args, instance, answer, atol, verify, ve return # retrieve gpu results to host memory - result_host = [] - for i, arg in enumerate(instance.arguments): - if should_sync[i]: - cp = _get_cupy() - cupy_ndarray = (cp.ndarray,) if cp is not None else () - if isinstance(arg, (np.ndarray,) + cupy_ndarray) or arg.__class__.__name__ == "VectorValue": - result_host.append(np.zeros_like(arg)) - self.dev.memcpy_dtoh(result_host[-1], gpu_args[i]) - elif isinstance(arg, torch.Tensor) and isinstance(answer[i], torch.Tensor): - if not answer[i].is_cuda: - # if the answer is on the host, copy gpu output to host as well - result_host.append(torch.zeros_like(answer[i])) - self.dev.memcpy_dtoh(result_host[-1], gpu_args[i].tensor) - else: - result_host.append(gpu_args[i].tensor) - else: - # We should sync this argument, but we do not know how to transfer this type of argument - # What do we do? Should we throw an error? - warn(f"Argument {i} is of type {type(arg)} and should be synchronized, but is not implemented.") - result_host.append(None) - else: - result_host.append(None) + result_host = self.retrieve_results_to_host(instance.arguments, should_sync, gpu_args, answer) # Call the output observers for obs in self.output_observers: @@ -850,6 +827,32 @@ def run_kernel(self, func, gpu_args, instance): raise e return True + def retrieve_results_to_host(self, arguments: list, should_sync: list[bool], gpu_args, answer: list): + """Retrieve results from device to host memory for all arguments that should be synchronized.""" + result_host = [] + for i, arg in enumerate(arguments): + if not should_sync[i]: + result_host.append(None) + continue + cp = _get_cupy() + cupy_ndarray = (cp.ndarray,) if cp is not None else () + if isinstance(arg, (np.ndarray,) + cupy_ndarray) or arg.__class__.__name__ == "VectorValue": + result_host.append(np.zeros_like(arg)) + self.dev.memcpy_dtoh(result_host[-1], gpu_args[i]) + elif isinstance(arg, torch.Tensor) and isinstance(answer[i], torch.Tensor): + if not answer[i].is_cuda: + # if the answer is on the host, copy gpu output to host as well + result_host.append(torch.zeros_like(answer[i])) + self.dev.memcpy_dtoh(result_host[-1], gpu_args[i].tensor) + else: + result_host.append(gpu_args[i].tensor) + else: + # We should sync this argument, but we do not know how to transfer this type of argument + # What do we do? Should we throw an error? + warn(f"Argument {i} is of type {type(arg)} and should be synchronized, but is not implemented.") + result_host.append(None) + return result_host + def _preprocess_gpu_arguments(old_arguments, params): """Get a flat list of arguments based on the configuration given by `params`.""" From e072fccd13eb001cbc55ecbf2d1eca9ee6a6cca2 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 10 Aug 2026 17:21:01 +0200 Subject: [PATCH 123/146] Rewrote various functions to reduce complexity --- kernel_tuner/backends/julia.py | 2 +- kernel_tuner/interface.py | 20 ++------- kernel_tuner/util.py | 82 ++++++++++++++++++++-------------- 3 files changed, 53 insertions(+), 51 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 6eb474624..5c88c0495 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -50,7 +50,7 @@ def __init__(self, device=0, iterations=7, compiler_options=None, observers=None raise ImportError("JuliaCall not installed. Please run `pip install juliacall`.") # process passed options and backends - self.process_options(compiler_options) + self.process_compiler_options(compiler_options) self.available_backends = detect_julia_gpu_backends() backend_name = self.verify_backends_with_options(compiler_options) diff --git a/kernel_tuner/interface.py b/kernel_tuner/interface.py index 9ac3452bc..387aec344 100644 --- a/kernel_tuner/interface.py +++ b/kernel_tuner/interface.py @@ -635,11 +635,7 @@ def tune_kernel( # Convert Julia types if lang is not None and lang.upper() == "JULIA": # TODO implement & test the case where Kernel Tuner is called from Julia but the target language is not Julia - if isinstance(tune_params, dict) or "DictValue" in tune_params.__class__.__name__: - raise ValueError( - "tune_params should not be a Julia dict, because it does not preserve order. Use a list of pairs instead." - ) - tune_params = dict([tuple([k, util.possible_julia_vector_to_list(tp)]) for k, tp in tune_params]) + tune_params = util.julia_list_of_pairs_to_dict(tune_params) if answer is not None: answer = [ numpy.array(a) if isinstance(a, (list, tuple)) else a for a in util.possible_julia_vector_to_list(answer) @@ -735,7 +731,6 @@ def tune_kernel( strategy = strategy_map["brute_force"] # select the runner for this job based on input - # TODO: we could use the "match case" syntax when removing support for 3.9 tuning_options.simulated_time = 0 # Get runner from environment if possible @@ -946,13 +941,8 @@ def run_kernel( kernelsource = core.KernelSource(kernel_name, kernel_source, lang, defines) - if lang == "Julia": - if isinstance(params, dict) or "DictValue" in params.__class__.__name__: - raise ValueError( - "tune_params should not be a Julia dict, because it does not preserve order. Use a list of pairs instead." - ) - params = [tuple([k, util.possible_julia_vector_to_list(tp)]) for k, tp in params] - params = dict(params) + if lang is not None and lang.upper() == "JULIA": + params = util.julia_list_of_pairs_to_dict(params) block_size_names = util.possible_julia_vector_to_list(block_size_names) # ensure there is always at least three names util.append_default_block_size_names(block_size_names) @@ -1017,10 +1007,6 @@ def run_kernel( results.append(numpy.zeros_like(arg)) dev.memcpy_dtoh(results[-1], gpu_args[i]) - # for Julia, convert the results back to Julia arrays - # if lang and lang.lower() == "julia": - # results = [util.possible_list_to_julia_vector(r) for r in results] - return results diff --git a/kernel_tuner/util.py b/kernel_tuner/util.py index f53aa5109..3c707d859 100644 --- a/kernel_tuner/util.py +++ b/kernel_tuner/util.py @@ -164,8 +164,6 @@ def check_argument_type(dtype, kernel_argument): def check_argument_list(kernel_name, kernel_string, args, lang=None): """Raise an exception if kernel arguments do not match host arguments.""" - cp = _get_cupy() - cupy_ndarray = (cp.ndarray,) if cp is not None else () kernel_arguments = list() collected_errors = list() @@ -191,38 +189,12 @@ def check_argument_list(kernel_name, kernel_string, args, lang=None): continue # Check each argument in the kernel argument list - if lang is None or lang.upper() != "JULIA": - for i, arg in enumerate(args): - kernel_argument = arguments[i] - - # Handle tunable arguments - if isinstance(arg, Tunable): - continue - - # Handle numpy arrays and other array types - if not isinstance(arg, (np.ndarray, np.generic, torch.Tensor, DeviceArray) + cupy_ndarray): - if arg.__class__.__name__ == "VectorValue": - # skip for Julia, types are commonly not specified in the kernel arguments - continue - raise TypeError( - f"Argument at position {i} of type: {type(arg)} should be of type " - "np.ndarray, numpy scalar, HIP Python DeviceArray, Julia VectorValue type" - ) - - correct = True - if isinstance(arg, np.ndarray) and "*" not in kernel_argument: - correct = False - - if isinstance(arg, DeviceArray): - str_dtype = str(np.dtype(arg.typestr)) - else: - str_dtype = str(arg.dtype) - - if correct and check_argument_type(str_dtype, kernel_argument): - continue - + for i, arg in enumerate(args): + kernel_argument = arguments[i] + correct, str_dtype = check_individual_arguments(i, arg, kernel_argument) + if not correct: collected_errors[arguments_set].append( - f"Argument at position {i} of dtype: {str_dtype} does not match {kernel_argument}." + f"Argument at position {str(i)} of dtype: {str_dtype} does not match {kernel_argument}." ) if not collected_errors[arguments_set]: @@ -234,6 +206,40 @@ def check_argument_list(kernel_name, kernel_string, args, lang=None): warnings.warn(errors[0], UserWarning) +def check_individual_arguments(i, arg, kernel_argument): + """Check whether the host argument matches the kernel argument.""" + correct = True + cp = _get_cupy() + cupy_ndarray = (cp.ndarray,) if cp is not None else () + + # Handle tunable arguments + if isinstance(arg, Tunable): + return correct, "" + + # Handle numpy arrays and other array types + if not isinstance(arg, (np.ndarray, np.generic, torch.Tensor, DeviceArray) + cupy_ndarray): + if arg.__class__.__name__ == "VectorValue": + # skip for Julia, types are commonly not specified in the kernel arguments + return correct, "" + raise TypeError( + f"Argument at position {i} of type: {type(arg)} should be of type " + "np.ndarray, numpy scalar, HIP Python DeviceArray, Julia VectorValue type" + ) + + if isinstance(arg, np.ndarray) and "*" not in kernel_argument: + correct = False + + if isinstance(arg, DeviceArray): + str_dtype = str(np.dtype(arg.typestr)) + else: + str_dtype = str(arg.dtype) + + if correct: + return check_argument_type(str_dtype, kernel_argument), str_dtype + + return False, str_dtype + + class Timer: """Measures elapsed wall-clock time.""" def __init__(self): @@ -1548,6 +1554,16 @@ def possible_julia_vector_to_list(obj): return obj +def julia_list_of_pairs_to_dict(params): + if isinstance(params, dict) or "DictValue" in params.__class__.__name__: + raise ValueError( + f"params {params} should not be a Julia dict, because it does not preserve order. Use a list of pairs instead." + ) + params = [tuple([k, possible_julia_vector_to_list(tp)]) for k, tp in params] + params = dict(params) + return params + + def infer_restrictions_from_cache(cache: dict): param_names = cache["tune_params_keys"] valid_param_config_set = set( From c984b69d964f892f7623add1877699ccf8ad43ed Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 10 Aug 2026 17:26:44 +0200 Subject: [PATCH 124/146] Updated CI test flow --- .github/workflows/test-python-package.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-python-package.yml b/.github/workflows/test-python-package.yml index 800562100..599a0ff1e 100644 --- a/.github/workflows/test-python-package.yml +++ b/.github/workflows/test-python-package.yml @@ -30,13 +30,13 @@ jobs: - name: Setup Poetry uses: Gr1N/setup-poetry@v9 - run: poetry self add poetry-plugin-export + - uses: julia-actions/setup-julia@v3 + with: + version: '1.12' - name: Run tests with Nox run: | pip install nox-poetry nox -- skip-gpu skip-julia github-action - - uses: julia-actions/setup-julia@v3 - with: - version: '1.12' - name: Run Julia tests with Nox run: | pip install nox-poetry From 8bb9d4edbe89a58b90846ea5ddcba0261d9e54dc Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 10 Aug 2026 17:27:06 +0200 Subject: [PATCH 125/146] Commented not yet implemented feature --- kernel_tuner/backends/julia.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index 5c88c0495..cc7e3ad83 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -409,7 +409,7 @@ def copy_shared_memory_args(self, smem_args): raise NotImplementedError( "Shared memory not yet supported in Julia backend. Submit a feature request if needed." ) - self.smem_size = int(smem_args.get("size", 0)) + # self.smem_size = int(smem_args.get("size", 0)) def copy_texture_memory_args(self, texmem_args): raise NotImplementedError( From 83aab9c1d09c46f4a7048632f07ebda6cff6b589 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 10 Aug 2026 18:01:47 +0200 Subject: [PATCH 126/146] Improved handling of argument checking --- kernel_tuner/util.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/kernel_tuner/util.py b/kernel_tuner/util.py index 3c707d859..7e3a3ec84 100644 --- a/kernel_tuner/util.py +++ b/kernel_tuner/util.py @@ -188,6 +188,11 @@ def check_argument_list(kernel_name, kernel_string, args, lang=None): ) continue + # skip checking for Julia, as types are commonly not specified in the kernel arguments + if lang and lang.upper() == "JULIA": + collected_errors.pop(arguments_set) + continue + # Check each argument in the kernel argument list for i, arg in enumerate(args): kernel_argument = arguments[i] From f2d2df15408c303727ac86bccd174f6d36ba6f2d Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 10 Aug 2026 18:02:03 +0200 Subject: [PATCH 127/146] Enable multiple positional arguments for nox --- noxfile.py | 1 - 1 file changed, 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 84d929732..c162f8317 100644 --- a/noxfile.py +++ b/noxfile.py @@ -149,7 +149,6 @@ def tests(session: Session) -> None: install_hip = False install_opencl = False skip_gpu = True - break elif arg.lower() == "skip-cuda": install_cuda = False elif arg.lower() == "skip-hip": From 55f74a257d3de0a712e70fa3ccbbdcb52934d482 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 10 Aug 2026 18:06:09 +0200 Subject: [PATCH 128/146] Enable multiple positional arguments for nox --- noxfile.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/noxfile.py b/noxfile.py index c162f8317..23bad3710 100644 --- a/noxfile.py +++ b/noxfile.py @@ -161,6 +161,9 @@ def tests(session: Session) -> None: install_additional_tests = True elif arg.lower() == "small-disk": small_disk = True + elif arg.lower() == "github-action": + # argument used in other sessions + pass else: raise ValueError(f"Unrecognized argument {arg}") From 2aadbb4390f06ba1d8808950a21c82e16ea865ad Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Mon, 10 Aug 2026 18:14:52 +0200 Subject: [PATCH 129/146] Improved session specification for Nox --- .github/workflows/test-python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-python-package.yml b/.github/workflows/test-python-package.yml index 599a0ff1e..497ac9959 100644 --- a/.github/workflows/test-python-package.yml +++ b/.github/workflows/test-python-package.yml @@ -40,7 +40,7 @@ jobs: - name: Run Julia tests with Nox run: | pip install nox-poetry - nox --session "tests(python='3.14')" skip-gpu github-action + nox --session tests-3.14 -- skip-gpu github-action # - name: Upload Coverage report to CodeCov # uses: codecov/codecov-action@v3 # with: From ec13c6df93dbd852ea099aaf4d50ca30bb08e443 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Tue, 11 Aug 2026 11:02:44 +0200 Subject: [PATCH 130/146] Switched to using JuliaPKG for managing environments, made it work with isolated nox session environments --- kernel_tuner/backends/julia.py | 4 +++- noxfile.py | 36 +++++++++++++++++++++++++++------- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/kernel_tuner/backends/julia.py b/kernel_tuner/backends/julia.py index cc7e3ad83..16fe88723 100644 --- a/kernel_tuner/backends/julia.py +++ b/kernel_tuner/backends/julia.py @@ -427,7 +427,9 @@ def check_package_and_install(self, package): except Exception: try: warn(f"{package}.jl not found, attempting to install it directly.") - jl.seval(f'using Pkg; Pkg.add("{package}")') + import juliapkg + juliapkg.add(package) + juliapkg.resolve() jl.seval(f"import {package}") except Exception as e: raise ImportError( diff --git a/noxfile.py b/noxfile.py index 23bad3710..c054a2d90 100644 --- a/noxfile.py +++ b/noxfile.py @@ -134,11 +134,13 @@ def check_development_environment(session: Session) -> None: def tests(session: Session) -> None: """Run the tests for the specified Python versions.""" session.log(f"Testing on Python {session.python}") + env_vars = {} # check if optional dependencies have been disabled by user arguments (e.g. `nox -- skip-gpu`, `nox -- skip-cuda`) install_cuda = True install_hip = True install_opencl = True install_julia = True + julia_use_gpu = True install_additional_tests = False small_disk = False skip_gpu = False @@ -148,6 +150,7 @@ def tests(session: Session) -> None: install_cuda = False install_hip = False install_opencl = False + julia_use_gpu = False skip_gpu = True elif arg.lower() == "skip-cuda": install_cuda = False @@ -157,6 +160,9 @@ def tests(session: Session) -> None: install_opencl = False elif arg.lower() == "skip-julia": install_julia = False + julia_use_gpu = False + elif arg.lower() == "julia-use-gpu": + julia_use_gpu = True elif arg.lower() == "additional-tests": install_additional_tests = True elif arg.lower() == "small-disk": @@ -166,6 +172,8 @@ def tests(session: Session) -> None: pass else: raise ValueError(f"Unrecognized argument {arg}") + if install_julia == False and julia_use_gpu == True: + raise ValueError("Cannot use Julia GPU backend if Julia is disabled") # check if there are optional dependencies that can not be installed if install_hip: @@ -323,20 +331,34 @@ def tests(session: Session) -> None: session.warn(install_warning) raise error + # if applicable, install julia dependencies in the session environment if install_julia: # sanitize loader env to avoid issues with Julia loading libraries from the wrong environment for v in ["DYLD_LIBRARY_PATH", "DYLD_FALLBACK_LIBRARY_PATH"]: session.env.pop(v, None) - # call Julia to precompile packages in the session environment - session.run("julia", "-e", "using Pkg; Pkg.precompile(); Pkg.instantiate()", external=True) + # call JuliaPKG to precompile packages in the session environment + session.run( + "python", "-c", + "import juliapkg; juliapkg.resolve()", + ) # install any additional dependencies used by the tests, as `check_package_and_install` won't work from Nox - if not skip_gpu: + if julia_use_gpu: gpu_backends_string = "".join( - f'Pkg.add("{backend_map[backend]["pkg"]}"); ' if backend_map[backend]["pkg"] else "" for backend in detect_julia_gpu_backends() + f'juliapkg.add("{backend_map[backend]["pkg"]}"); ' if backend_map[backend]["pkg"] else "" for backend in detect_julia_gpu_backends() ) else: gpu_backends_string = "" - session.run("julia", "-e", f'using Pkg; Pkg.add("KernelAbstractions"); {gpu_backends_string}', external=True) + session.run( + "python", "-c", + f"import juliapkg; juliapkg.add('KernelAbstractions'); {gpu_backends_string} juliapkg.resolve();", + ) + # retrieve the project path for this isolated session environment and pass it as an environment variable + julia_project_path = session.run( + "python", "-c", + "import juliapkg; print(juliapkg.project())", + silent=True + ).strip() + env_vars["PYTHON_JULIAPKG_PROJECT"] = julia_project_path # if applicable, install the dependencies for additional tests if install_additional_tests and install_cuda: @@ -368,11 +390,11 @@ def tests(session: Session) -> None: # for the last Python version session if all optional dependencies are enabled: if session.python == python_versions_to_test[-1] and full_install: # run pytest on the package to generate the correct coverage report - session.run("pytest", external=False) + session.run("pytest", external=False, env=env_vars) else: # for the other Python version sessions: # run pytest without coverage reporting - session.run("pytest", "--no-cov", external=False) + session.run("pytest", "--no-cov", external=False, env=env_vars) # warn if no coverage report if not full_install: From 303ed2711cf82d82e9b872e0cc4ea55593ba08d1 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Tue, 11 Aug 2026 12:28:12 +0200 Subject: [PATCH 131/146] Added julia compat specifier --- noxfile.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/noxfile.py b/noxfile.py index c054a2d90..04579d11e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -337,9 +337,10 @@ def tests(session: Session) -> None: for v in ["DYLD_LIBRARY_PATH", "DYLD_FALLBACK_LIBRARY_PATH"]: session.env.pop(v, None) # call JuliaPKG to precompile packages in the session environment + preamble = "import juliapkg; juliapkg.require_julia('1.11, 2')" session.run( "python", "-c", - "import juliapkg; juliapkg.resolve()", + f"{preamble}; juliapkg.resolve()", ) # install any additional dependencies used by the tests, as `check_package_and_install` won't work from Nox if julia_use_gpu: @@ -350,12 +351,12 @@ def tests(session: Session) -> None: gpu_backends_string = "" session.run( "python", "-c", - f"import juliapkg; juliapkg.add('KernelAbstractions'); {gpu_backends_string} juliapkg.resolve();", + f"{preamble}; juliapkg.add('KernelAbstractions'); {gpu_backends_string} juliapkg.resolve();", ) # retrieve the project path for this isolated session environment and pass it as an environment variable julia_project_path = session.run( "python", "-c", - "import juliapkg; print(juliapkg.project())", + f"{preamble}; print(juliapkg.project())", silent=True ).strip() env_vars["PYTHON_JULIAPKG_PROJECT"] = julia_project_path From 1441e97f66a2346a1bbdde8890793f1c566275ba Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Tue, 11 Aug 2026 13:24:47 +0200 Subject: [PATCH 132/146] Improved Julia compat version handling for CI and Nox --- .github/workflows/test-python-package.yml | 2 +- noxfile.py | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-python-package.yml b/.github/workflows/test-python-package.yml index 497ac9959..31f2f7837 100644 --- a/.github/workflows/test-python-package.yml +++ b/.github/workflows/test-python-package.yml @@ -32,7 +32,7 @@ jobs: - run: poetry self add poetry-plugin-export - uses: julia-actions/setup-julia@v3 with: - version: '1.12' + version: '1.12' # when changed, also see `require_julia` in noxfile.py and the Julia version in Project.toml - name: Run tests with Nox run: | pip install nox-poetry diff --git a/noxfile.py b/noxfile.py index 04579d11e..0f578f10c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -144,6 +144,7 @@ def tests(session: Session) -> None: install_additional_tests = False small_disk = False skip_gpu = False + github_action = False if session.posargs: for arg in session.posargs: if arg.lower() == "skip-gpu": @@ -168,12 +169,13 @@ def tests(session: Session) -> None: elif arg.lower() == "small-disk": small_disk = True elif arg.lower() == "github-action": - # argument used in other sessions - pass + github_action = True else: raise ValueError(f"Unrecognized argument {arg}") if install_julia == False and julia_use_gpu == True: raise ValueError("Cannot use Julia GPU backend if Julia is disabled") + if skip_gpu == True and julia_use_gpu == True: + print("With both `skip-gpu` and `julia-use-gpu` given, the latter takes precedence. Julia GPU backends will be used if available.") # check if there are optional dependencies that can not be installed if install_hip: @@ -336,8 +338,13 @@ def tests(session: Session) -> None: # sanitize loader env to avoid issues with Julia loading libraries from the wrong environment for v in ["DYLD_LIBRARY_PATH", "DYLD_FALLBACK_LIBRARY_PATH"]: session.env.pop(v, None) + # set the Julia version + # when changed, also see `require_julia` in Project.toml and the Julia version in the GitHub Actions workflow + if github_action: + preamble = "import juliapkg; juliapkg.require_julia('1.12')" + else: + preamble = "import juliapkg; juliapkg.require_julia('1.11, 2')" # call JuliaPKG to precompile packages in the session environment - preamble = "import juliapkg; juliapkg.require_julia('1.11, 2')" session.run( "python", "-c", f"{preamble}; juliapkg.resolve()", From 6b9be350ca5afed33b98aabef4d8005923dd311e Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Tue, 11 Aug 2026 14:57:00 +0200 Subject: [PATCH 133/146] Switch back to Julia 1.11 for CI tests --- .github/workflows/test-python-package.yml | 2 +- noxfile.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-python-package.yml b/.github/workflows/test-python-package.yml index 31f2f7837..b8d231c74 100644 --- a/.github/workflows/test-python-package.yml +++ b/.github/workflows/test-python-package.yml @@ -32,7 +32,7 @@ jobs: - run: poetry self add poetry-plugin-export - uses: julia-actions/setup-julia@v3 with: - version: '1.12' # when changed, also see `require_julia` in noxfile.py and the Julia version in Project.toml + version: '1.11' # when changed, also see `require_julia` in noxfile.py and the Julia version in Project.toml - name: Run tests with Nox run: | pip install nox-poetry diff --git a/noxfile.py b/noxfile.py index 0f578f10c..b7ba4cb90 100644 --- a/noxfile.py +++ b/noxfile.py @@ -341,7 +341,7 @@ def tests(session: Session) -> None: # set the Julia version # when changed, also see `require_julia` in Project.toml and the Julia version in the GitHub Actions workflow if github_action: - preamble = "import juliapkg; juliapkg.require_julia('1.12')" + preamble = "import juliapkg; juliapkg.require_julia('1.11')" else: preamble = "import juliapkg; juliapkg.require_julia('1.11, 2')" # call JuliaPKG to precompile packages in the session environment From 98e97d839bc408522382682d02f71141046b8575 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Tue, 11 Aug 2026 16:36:58 +0200 Subject: [PATCH 134/146] Switch back to Julia 1.12 for CI tests --- .github/workflows/test-python-package.yml | 2 +- noxfile.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-python-package.yml b/.github/workflows/test-python-package.yml index b8d231c74..31f2f7837 100644 --- a/.github/workflows/test-python-package.yml +++ b/.github/workflows/test-python-package.yml @@ -32,7 +32,7 @@ jobs: - run: poetry self add poetry-plugin-export - uses: julia-actions/setup-julia@v3 with: - version: '1.11' # when changed, also see `require_julia` in noxfile.py and the Julia version in Project.toml + version: '1.12' # when changed, also see `require_julia` in noxfile.py and the Julia version in Project.toml - name: Run tests with Nox run: | pip install nox-poetry diff --git a/noxfile.py b/noxfile.py index b7ba4cb90..0f578f10c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -341,7 +341,7 @@ def tests(session: Session) -> None: # set the Julia version # when changed, also see `require_julia` in Project.toml and the Julia version in the GitHub Actions workflow if github_action: - preamble = "import juliapkg; juliapkg.require_julia('1.11')" + preamble = "import juliapkg; juliapkg.require_julia('1.12')" else: preamble = "import juliapkg; juliapkg.require_julia('1.11, 2')" # call JuliaPKG to precompile packages in the session environment From 476bce8ee259c4dc15405d7753d077456336cdba Mon Sep 17 00:00:00 2001 From: fjwillemsen Date: Tue, 11 Aug 2026 16:40:08 +0200 Subject: [PATCH 135/146] Consistent setting of environment variables in Nox --- noxfile.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/noxfile.py b/noxfile.py index 0f578f10c..8d0df1c29 100644 --- a/noxfile.py +++ b/noxfile.py @@ -233,6 +233,7 @@ def tests(session: Session) -> None: julia_envdir = Path(session_envdir) / ".julia" if julia_envdir is not None: session.env["JULIA_DEPOT_PATH"] = str(Path(julia_envdir).resolve()) + env_vars["JULIA_DEPOT_PATH"] = str(Path(julia_envdir).resolve()) # session.env["JULIAUP_DEPOT_PATH"] = session.env["JULIA_DEPOT_PATH"] # session.env["PYTHON_JULIACALL_PROJECT"] = str(Path(julia_envdir).resolve()) @@ -349,6 +350,14 @@ def tests(session: Session) -> None: "python", "-c", f"{preamble}; juliapkg.resolve()", ) + # retrieve the project path for this isolated session environment and pass it as an environment variable + julia_project_path = session.run( + "python", "-c", + f"{preamble}; print(juliapkg.project())", + silent=True + ).strip() + session.env["PYTHON_JULIAPKG_PROJECT"] = julia_project_path + env_vars["PYTHON_JULIAPKG_PROJECT"] = julia_project_path # install any additional dependencies used by the tests, as `check_package_and_install` won't work from Nox if julia_use_gpu: gpu_backends_string = "".join( @@ -358,15 +367,8 @@ def tests(session: Session) -> None: gpu_backends_string = "" session.run( "python", "-c", - f"{preamble}; juliapkg.add('KernelAbstractions'); {gpu_backends_string} juliapkg.resolve();", + f"{preamble}; juliapkg.resolve(); juliapkg.add('KernelAbstractions'); {gpu_backends_string} juliapkg.resolve();", ) - # retrieve the project path for this isolated session environment and pass it as an environment variable - julia_project_path = session.run( - "python", "-c", - f"{preamble}; print(juliapkg.project())", - silent=True - ).strip() - env_vars["PYTHON_JULIAPKG_PROJECT"] = julia_project_path # if applicable, install the dependencies for additional tests if install_additional_tests and install_cuda: From 14ccd8d8fd202142f848a33c0835d044a5ee9d2a Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Tue, 11 Aug 2026 17:05:07 +0200 Subject: [PATCH 136/146] Testing forced resolution --- noxfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 8d0df1c29..bcec5c3cb 100644 --- a/noxfile.py +++ b/noxfile.py @@ -348,7 +348,7 @@ def tests(session: Session) -> None: # call JuliaPKG to precompile packages in the session environment session.run( "python", "-c", - f"{preamble}; juliapkg.resolve()", + f"{preamble}; juliapkg.resolve(update=true)", ) # retrieve the project path for this isolated session environment and pass it as an environment variable julia_project_path = session.run( From fbb0fd883692a4c4a4b8bf2068ed66e4460acd28 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Tue, 11 Aug 2026 17:10:06 +0200 Subject: [PATCH 137/146] Testing forced resolution --- noxfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index bcec5c3cb..be2b5ba2f 100644 --- a/noxfile.py +++ b/noxfile.py @@ -348,7 +348,7 @@ def tests(session: Session) -> None: # call JuliaPKG to precompile packages in the session environment session.run( "python", "-c", - f"{preamble}; juliapkg.resolve(update=true)", + f"{preamble}; juliapkg.resolve(update=True)", ) # retrieve the project path for this isolated session environment and pass it as an environment variable julia_project_path = session.run( From d3a6cdb18f75639ae56bf59cd84a0f0e3517a923 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Tue, 11 Aug 2026 17:26:33 +0200 Subject: [PATCH 138/146] Testing forced deletion of old nox folder --- .github/workflows/test-python-package.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test-python-package.yml b/.github/workflows/test-python-package.yml index 31f2f7837..f2a8d5369 100644 --- a/.github/workflows/test-python-package.yml +++ b/.github/workflows/test-python-package.yml @@ -35,10 +35,12 @@ jobs: version: '1.12' # when changed, also see `require_julia` in noxfile.py and the Julia version in Project.toml - name: Run tests with Nox run: | + rm -rf .nox pip install nox-poetry nox -- skip-gpu skip-julia github-action - name: Run Julia tests with Nox run: | + rm -rf .nox pip install nox-poetry nox --session tests-3.14 -- skip-gpu github-action # - name: Upload Coverage report to CodeCov From 53443ce17d78fae5cda41a43289b3b612168da6e Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Tue, 11 Aug 2026 17:51:06 +0200 Subject: [PATCH 139/146] Switch back to Julia 1.11 for CI tests --- .github/workflows/test-python-package.yml | 2 +- noxfile.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-python-package.yml b/.github/workflows/test-python-package.yml index f2a8d5369..2c554583d 100644 --- a/.github/workflows/test-python-package.yml +++ b/.github/workflows/test-python-package.yml @@ -32,7 +32,7 @@ jobs: - run: poetry self add poetry-plugin-export - uses: julia-actions/setup-julia@v3 with: - version: '1.12' # when changed, also see `require_julia` in noxfile.py and the Julia version in Project.toml + version: '1.11' # when changed, also see `require_julia` in noxfile.py and the Julia version in Project.toml - name: Run tests with Nox run: | rm -rf .nox diff --git a/noxfile.py b/noxfile.py index be2b5ba2f..eb43aa63e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -342,7 +342,7 @@ def tests(session: Session) -> None: # set the Julia version # when changed, also see `require_julia` in Project.toml and the Julia version in the GitHub Actions workflow if github_action: - preamble = "import juliapkg; juliapkg.require_julia('1.12')" + preamble = "import juliapkg; juliapkg.require_julia('1.11')" else: preamble = "import juliapkg; juliapkg.require_julia('1.11, 2')" # call JuliaPKG to precompile packages in the session environment From 4b9d757a803a89dba55788983310a433d87c36b7 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Tue, 11 Aug 2026 18:01:39 +0200 Subject: [PATCH 140/146] Create registries folder in CI --- .github/workflows/test-python-package.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-python-package.yml b/.github/workflows/test-python-package.yml index 2c554583d..be12ba92c 100644 --- a/.github/workflows/test-python-package.yml +++ b/.github/workflows/test-python-package.yml @@ -41,6 +41,7 @@ jobs: - name: Run Julia tests with Nox run: | rm -rf .nox + mkdir -p /Users/runner/.julia/registries pip install nox-poetry nox --session tests-3.14 -- skip-gpu github-action # - name: Upload Coverage report to CodeCov From 06ac66fad6a24ef2b6d1209fe880018ea95ee190 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Tue, 11 Aug 2026 18:37:30 +0200 Subject: [PATCH 141/146] Create registries folder in CI --- .github/workflows/test-python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-python-package.yml b/.github/workflows/test-python-package.yml index be12ba92c..9925b3717 100644 --- a/.github/workflows/test-python-package.yml +++ b/.github/workflows/test-python-package.yml @@ -41,7 +41,7 @@ jobs: - name: Run Julia tests with Nox run: | rm -rf .nox - mkdir -p /Users/runner/.julia/registries + [ -d "/Users/runner" ] && mkdir -p /Users/runner/.julia/registries pip install nox-poetry nox --session tests-3.14 -- skip-gpu github-action # - name: Upload Coverage report to CodeCov From 5b7ed9198c71f4c4ad80a26d7c2394c5962fe853 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Wed, 12 Aug 2026 12:09:42 +0200 Subject: [PATCH 142/146] Provide more overhead for timed tests if running on a CI --- test/context.py | 2 ++ test/test_time_budgets.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/test/context.py b/test/context.py index bd13c4304..c49a762f3 100644 --- a/test/context.py +++ b/test/context.py @@ -2,6 +2,7 @@ import subprocess import sys import ctypes.util +from os import environ import pytest @@ -35,6 +36,7 @@ gfortran_present = shutil.which("gfortran") is not None openmp_present = ctypes.util.find_library('gomp') is not None openacc_present = shutil.which("nvc++") is not None +running_on_ci = any([environ.get(CI, "false").lower() == "true" for CI in ["GITHUB_ACTIONS", "TRAVIS", "CIRCLECI", "GITLAB_CI"]]) try: import cupy diff --git a/test/test_time_budgets.py b/test/test_time_budgets.py index edc704f70..16b10fd0f 100644 --- a/test/test_time_budgets.py +++ b/test/test_time_budgets.py @@ -8,7 +8,7 @@ from kernel_tuner import tune_kernel -from .context import skip_if_no_gcc +from .context import skip_if_no_gcc, running_on_ci @pytest.fixture @@ -66,7 +66,7 @@ def test_some_time_budget(env): assert 0 < len(res) < size_all # Ensure that the time limit was respected by some margin. - assert perf_counter() - start_time < time_limit * 2 + assert perf_counter() - start_time < (time_limit * 2) * (1.5 if running_on_ci else 1.0) # allow for more overhead on CI @skip_if_no_gcc From c5f82e9b89543432b606ce18483f5fa3e52d75ef Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Wed, 12 Aug 2026 12:23:13 +0200 Subject: [PATCH 143/146] Improved required directory structure creation before tests --- .github/workflows/test-python-package.yml | 2 +- noxfile.py | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-python-package.yml b/.github/workflows/test-python-package.yml index 9925b3717..b59e63955 100644 --- a/.github/workflows/test-python-package.yml +++ b/.github/workflows/test-python-package.yml @@ -41,9 +41,9 @@ jobs: - name: Run Julia tests with Nox run: | rm -rf .nox - [ -d "/Users/runner" ] && mkdir -p /Users/runner/.julia/registries pip install nox-poetry nox --session tests-3.14 -- skip-gpu github-action + # [ -d "~" ] && mkdir -p ~/.julia/registries # - name: Upload Coverage report to CodeCov # uses: codecov/codecov-action@v3 # with: diff --git a/noxfile.py b/noxfile.py index eb43aa63e..5ca273b10 100644 --- a/noxfile.py +++ b/noxfile.py @@ -342,9 +342,9 @@ def tests(session: Session) -> None: # set the Julia version # when changed, also see `require_julia` in Project.toml and the Julia version in the GitHub Actions workflow if github_action: - preamble = "import juliapkg; juliapkg.require_julia('1.11')" + preamble = "import juliapkg; juliapkg.require_julia('1.11')" # must match the setup-julia action else: - preamble = "import juliapkg; juliapkg.require_julia('1.11, 2')" + preamble = "import juliapkg; juliapkg.require_julia('1.11, 2')" # meaning >= 1.11, < 2.0 # call JuliaPKG to precompile packages in the session environment session.run( "python", "-c", @@ -356,6 +356,11 @@ def tests(session: Session) -> None: f"{preamble}; print(juliapkg.project())", silent=True ).strip() + # create the .julia/registries directory if it doesn't exist to avoid juliapkg.add() crash + session.run( + "bash", "-c", + "[ -d '~' ] && mkdir -p ~/.julia/registries" + ) session.env["PYTHON_JULIAPKG_PROJECT"] = julia_project_path env_vars["PYTHON_JULIAPKG_PROJECT"] = julia_project_path # install any additional dependencies used by the tests, as `check_package_and_install` won't work from Nox From b4224ab948035aa49c2b3d7a93b54d4c69d25a23 Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Wed, 12 Aug 2026 12:42:40 +0200 Subject: [PATCH 144/146] Improved required directory structure creation before tests --- noxfile.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 5ca273b10..f93da3e5a 100644 --- a/noxfile.py +++ b/noxfile.py @@ -359,7 +359,8 @@ def tests(session: Session) -> None: # create the .julia/registries directory if it doesn't exist to avoid juliapkg.add() crash session.run( "bash", "-c", - "[ -d '~' ] && mkdir -p ~/.julia/registries" + "[ -d ~ ] && mkdir -p ~/.julia/registries", + external=True ) session.env["PYTHON_JULIAPKG_PROJECT"] = julia_project_path env_vars["PYTHON_JULIAPKG_PROJECT"] = julia_project_path From 3473bad57a21d091a7b7d58f42fe0f2d7f0476cd Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Fri, 14 Aug 2026 18:09:35 +0200 Subject: [PATCH 145/146] Updated setup-nox github action, python-constraint dependency --- .github/workflows/test-python-package.yml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-python-package.yml b/.github/workflows/test-python-package.yml index b59e63955..d1a151032 100644 --- a/.github/workflows/test-python-package.yml +++ b/.github/workflows/test-python-package.yml @@ -26,7 +26,7 @@ jobs: steps: - uses: actions/checkout@v7 - name: Setup Nox - uses: fjwillemsen/setup-nox2@v3.0.0 + uses: fjwillemsen/setup-nox2@v4 - name: Setup Poetry uses: Gr1N/setup-poetry@v9 - run: poetry self add poetry-plugin-export diff --git a/pyproject.toml b/pyproject.toml index 29e3104b8..ec9542ba3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,7 @@ dependencies = [ "scipy>=1.14.1", # Python >=3.13 needs scipy >=1.14 "packaging", # required by file_utils "jsonschema", - "python-constraint2>=2.4.0", + "python-constraint2>=2.5.0", "xmltodict", "pandas>=2.0.0", "scikit-learn>=1.0.2", From 9df30d1bfee3aa88cc740e461d0554227f14bece Mon Sep 17 00:00:00 2001 From: Floris-Jan Willemsen Date: Fri, 14 Aug 2026 20:38:20 +0200 Subject: [PATCH 146/146] Improved automatic detection of Julia kernel arguments --- kernel_tuner/util.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/kernel_tuner/util.py b/kernel_tuner/util.py index 7e3a3ec84..b68eb04fb 100644 --- a/kernel_tuner/util.py +++ b/kernel_tuner/util.py @@ -168,9 +168,15 @@ def check_argument_list(kernel_name, kernel_string, args, lang=None): collected_errors = list() # Find all kernel argument lists in the kernel string + if lang and lang.upper() == "JULIA": + # for Julia, multiple kernels may be specified in one file, and remove the normally included tunable parameters + kernel_string = kernel_string.split(kernel_name)[1] + kernel_string = kernel_name + kernel_string + kernel_string = kernel_string.split("::Val")[0].rstrip() for iterator in re.finditer(kernel_name + "[ \n\t]*" + r"\(", kernel_string): kernel_start = iterator.end() - kernel_end = kernel_string.find(")", kernel_start) + # for Julia, search until the last ',' as there may be e.g. `@Const(Min)` arguments + kernel_end = kernel_string.find(r".*(\,)" if lang and lang.upper() == "JULIA" else ")", kernel_start) if kernel_start != 0: kernel_arguments.append(kernel_string[kernel_start:kernel_end].split(",")) @@ -179,7 +185,7 @@ def check_argument_list(kernel_name, kernel_string, args, lang=None): # check arguments and signature lengths if lang and lang.upper() == "JULIA" and len(arguments) > len(args): - # for Julia tunable parameters are added to the kernel signature + # for Julia additional parameters may be passed continue collected_errors.append(list()) if len(arguments) != len(args):