Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
1dd31e3
Implemented passing additional arguments to pytest
fjwillemsen Sep 21, 2026
e34e1cf
Updated Julia-Python dependencies
fjwillemsen Sep 21, 2026
21070f7
Implemented energy, occupancy & frequency observer for Metal and tests
fjwillemsen Sep 21, 2026
591a701
Improved permissions handling for Metal observer
fjwillemsen Sep 22, 2026
9b913e9
Clarified Metal observer units and handling of invalid values
fjwillemsen Sep 22, 2026
6a84da1
Flush Metal observer powermetrics buffer on read
fjwillemsen Sep 22, 2026
6f63a13
Re-implement Julia backend to be non-blocking
fjwillemsen Sep 22, 2026
7a6ca83
Massive overhaul of Metal backend and observers interaction for much …
fjwillemsen Sep 24, 2026
e59c0b2
Improvements to timing and observers
fjwillemsen Sep 24, 2026
9e32582
Improvements to observers and Julia runner, extended tests
fjwillemsen Sep 24, 2026
5ed3484
test passing arguments to and from Julia kernels
benvanwerkhoven Sep 25, 2026
b7af6a7
Explicitely included 1.13 in testing, improved dependencies
fjwillemsen Oct 25, 2026
bba9bcc
Merge branch 'master' into julia
fjwillemsen Oct 25, 2026
acbe1eb
Updated Poetry workflows dependency
fjwillemsen Nov 2, 2026
b3d2656
Added Python 3.15 support and testing, dropped 3.10, removed no longe…
fjwillemsen Nov 2, 2026
7b31581
Replaced tomli for tomllib in tests
fjwillemsen Nov 2, 2026
b763543
Julia repository zstd support
fjwillemsen Nov 2, 2026
e9411e3
Implemented extended tests from #407
fjwillemsen Nov 2, 2026
df450f3
Improved time budget test
fjwillemsen Nov 2, 2026
f99f396
Improvements to observer tests
fjwillemsen Nov 3, 2026
0a580b5
Implemented a structure for defining backend-specific functions
fjwillemsen Nov 3, 2026
2e919a7
Clarification of extension structure
fjwillemsen Nov 3, 2026
5d9a304
Implemented retrieval of UUID and PCI bus ID, improved device charact…
fjwillemsen Nov 4, 2026
258217a
Renamed Julia backend extension files
fjwillemsen Nov 4, 2026
8024383
Exportable supported observables and types for NVML observers
fjwillemsen Nov 4, 2026
28f5536
Dynamic sizing of test kernel fixture
fjwillemsen Nov 4, 2026
ddb1471
Implemented tests for NVML observer with Julia
fjwillemsen Nov 4, 2026
b673540
Expanded tests for observers
fjwillemsen Nov 4, 2026
4552f72
Extended error reporting
fjwillemsen Nov 4, 2026
170b6c7
Expanded error reporting
fjwillemsen Nov 4, 2026
ad0b1ae
Merge branch 'julia' of https://github.com/benvanwerkhoven/kernel_tun…
fjwillemsen Nov 4, 2026
e49d760
Improved automatic handling of floating point types
fjwillemsen Nov 4, 2026
a2a644b
Updated tests for NVML observer with Julia
fjwillemsen Nov 5, 2026
c3fe324
Improved logging
fjwillemsen Nov 5, 2026
ae0099e
Improved outlier handling
fjwillemsen Nov 5, 2026
5716e2e
Improvements to Metal observer
fjwillemsen Nov 6, 2026
11e0d5e
Dedicated test for Metal observer
fjwillemsen Nov 6, 2026
003d6c7
Various improvements to signal handling, and extended tests accordingly
fjwillemsen Nov 6, 2026
2951d69
New Julia NVML observer to replace the deprecated pynvml version
fjwillemsen Nov 7, 2026
447099e
New NVMLContinuousObserver, extended tests etc
fjwillemsen Nov 7, 2026
f888dd2
Improved timing controls
fjwillemsen Nov 7, 2026
968a5ed
Improved outlier handling
fjwillemsen Nov 7, 2026
8552393
Improved outlier reporting
fjwillemsen Nov 7, 2026
944fab6
Improvements to the AMD energy observer
fjwillemsen Nov 8, 2026
dfba8f9
Various improvements to array handling
fjwillemsen Nov 8, 2026
772667f
Added supported float types for each backend
fjwillemsen Nov 8, 2026
f4d4c98
Merge branch 'julia' of https://github.com/KernelTuner/kernel_tuner i…
fjwillemsen Nov 8, 2026
b3f000d
Merge branch 'julia' of https://github.com/KernelTuner/kernel_tuner i…
fjwillemsen Nov 8, 2026
12e2a2e
Improved array handling performance
fjwillemsen Nov 8, 2026
e151602
Merge branch 'master' into julia
fjwillemsen Nov 9, 2026
5d71725
Bumped version number
fjwillemsen Nov 9, 2026
063e8e4
Amended changelog
fjwillemsen Nov 9, 2026
6247d3b
Avoided repeated literal
fjwillemsen Nov 9, 2026
d15f877
Set workflows to higher ubuntu version
fjwillemsen Nov 9, 2026
402488d
Refer to KernelTuner.jl in README
fjwillemsen Nov 9, 2026
f934815
Formatting
fjwillemsen Nov 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/publish-python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
with:
python-version: "3.12"
- name: Setup Poetry
uses: Gr1N/setup-poetry@v9
uses: Gr1N/setup-poetry@v10
- name: Build the source distribution and pure-Python wheel
run: |
poetry install
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/test-python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,18 @@ jobs:

strategy:
matrix:
os: [ubuntu-latest, macos-latest]
os: [ubuntu-26.04, macos-latest] # set to ubuntu-latest when it is updated to 26.04

steps:
- uses: actions/checkout@v7
- name: Setup Nox
uses: fjwillemsen/setup-nox2@v4
- name: Setup Poetry
uses: Gr1N/setup-poetry@v9
uses: Gr1N/setup-poetry@v10
- 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.13' # 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
Expand Down
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,16 @@ All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased
- Fix `np.shuffle` crash and wrong return type in `Searchspace` distributed random and LHS samplers
- Extended Julia functionality:
- Implemented Julia observers for Metal, Nvidia and AMD energy measurements.
- Improved timing stability.
- Include the new tests of #407, additional new and updated tests.
- Improved performance and data handling.
- Improved NVML and AMD observers.
- Improved signal handling.
- Added Python 3.15 support, dropped 3.10 support due to removed support from required dependencies.
- Removed tomli dependency in favor of built-in tomllib.
- Fix `np.shuffle` crash and wrong return type in `Searchspace` distributed random and LHS samplers.

## [1.5.0] - 2026-08-20
- Kernel Tuner can now be used from Julia, [see KernelTuner.jl](https://github.com/KernelTuner/KernelTuner.jl)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@


Create optimized GPU applications in any mainstream GPU
programming language (CUDA, HIP, OpenCL, OpenACC, OpenMP).
programming language (CUDA, HIP, OpenCL, OpenACC, OpenMP). For Julia, see [KernelTuner.jl](https://github.com/KernelTuner/KernelTuner.jl).

What Kernel Tuner does:

Expand Down
226 changes: 164 additions & 62 deletions kernel_tuner/backends/julia.py

Large diffs are not rendered by default.

102 changes: 48 additions & 54 deletions kernel_tuner/backends/julia_helper.jl
Original file line number Diff line number Diff line change
@@ -1,63 +1,59 @@
export to_gpuarray, launch_kernel
# Base file for Julia functions. For backend-specific functionality, see the extension files, e.g. `julia_helper_ext_cuda.jl`.

export to_gpuarray, launch_kernel, synchronize_gpu, metal_kernel_finished, metal_gpu_time_ms

# GPU timing events recorded directly around a kernel launch, specialized in the backend extension files.
# By default (backends without timing events), there are no events.
create_timing_events(backend) = (nothing, nothing)
record_timing_event(::Nothing) = nothing

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
return a
# ensure we have a separate copy to avoid unintended side effects; for arrays, the conversion to a GPU array
# already copies, while a deepcopy of a view would needlessly copy its entire parent array
# (on the CPU backend, `Array(a)` returns `a` itself for an `Array`, in which case it is copied explicitly)
isa(a, AbstractArray) || return deepcopy(a)
b = GPUArrayType(a)
return b === a ? deepcopy(a) : b
end

function launch_kernel(kernel, args::Tuple, params::Tuple, ndrange::Tuple, workgroupsize::Tuple, shmem::Int, start_evt::Any, end_evt::Any, stream::Any)
t = Inf
function launch_kernel(kernel, args::Tuple, params::Tuple, ndrange::Tuple, workgroupsize::Tuple, shmem::Int, use_metal_buffer::Bool)
launch_time = Inf
launch_time_start = nothing
start_evt = nothing
end_evt = nothing
# 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)
# Launch kernel
# Launch kernel asynchronously
mktemp() do tmppath, _
open(tmppath, "w") do tmpio
# kernel errors are printed to stdout, capture them
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, 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
start_buff = create_metal_buffer(Metal.device())
end_buff = create_metal_buffer(Metal.device())
Metal.commit!(start_buff)
start_evt, end_evt = create_timing_events(kt_julia_backend)
# Record the timing events directly around the launch, so that host-side work before the
# launch is not measured as GPU time. The GC is disabled meanwhile, as a GC pause between
# recording the start event and launching would otherwise be measured as GPU time.
gc_was_enabled = GC.enable(false)
try
launch_time_start = time_ns()
record_timing_event(start_evt)
if use_metal_buffer
# record the command buffers of this launch, used for GPU timing (see julia_helper_ext_metal.jl)
metal_launch_recorded(() -> configured_kernel(args..., val_params...; ndrange=ndrange))
else
error("Unsupported event type for timing: $(typeof(start_evt))")
configured_kernel(args..., val_params...; ndrange=ndrange) # launch the kernel (async)
end
record_timing_event(end_evt)
launch_time = float((time_ns() - launch_time_start) / 1e6) # convert to milliseconds
finally
GC.enable(gc_was_enabled)
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 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, 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)
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
# Note: kernel launch is asynchronous
# Synchronization and event recording is handled by the Python backend
catch e
e isa InterruptException && rethrow() # Ctrl-C: Kernel Tuner stops tuning (see `util.is_interrupt`)
redirect_stdout(stdout) # restore stdout
close(tmpio)
stdout_output = read(tmppath, String)
Expand All @@ -67,22 +63,20 @@ function launch_kernel(kernel, args::Tuple, params::Tuple, ndrange::Tuple, workg
end
end
end
# print any stdout output from the kernel
print(read(tmppath, String))
# # print any stdout output from the kernel (for debugging only, disabled for performance)
# print(read(tmppath, String))
end
else
error("Only KernelAbstractions kernels are supported.")
end
return t
# return the launch time and the GPU timing events (if any), which are used by the observer in the Python backend
return launch_time, launch_time_start, start_evt, end_evt
end

function create_metal_buffer(device)
# 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.")
function synchronize_gpu(launch_time_start)
Main.KernelAbstractions.synchronize(kt_julia_backend)
if launch_time_start === nothing
return nothing
end
return float((time_ns() - launch_time_start) / 1e6)
end
26 changes: 18 additions & 8 deletions kernel_tuner/backends/julia_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@
"name": "CUDA.name(CUDA.device())",
"max_threads": "CUDA.attribute(CUDA.device(), CUDA.DEVICE_ATTRIBUTE_MAX_THREADS_PER_BLOCK)",
"capability": "CUDA.capability(CUDA.device())",
"uuid": lambda d: f"NVML.uuid(CUDA.NVML.Device({d}))",
"pci_bus_id": None,
"GPUArrayType": "CuArray",
"supported_float_types": ["Float16", "BFloat16", "Float32", "Float64"],
},
"AMD": {
"pkg": "AMDGPU",
Expand All @@ -29,7 +32,10 @@
"name": "AMDGPU.HIP.name(AMDGPU.HIP.device())",
"max_threads": "AMDGPU.HIP.attribute(AMDGPU.HIP.device(), AMDGPU.HIP.hipDeviceAttributeMaxThreadsPerBlock)",
"capability": None,
"uuid": None,
"pci_bus_id": None,
"GPUArrayType": "ROCArray",
"supported_float_types": ["Float16", "BFloat16", "Float32", "Float64"],
},
"INTEL": {
"pkg": "oneAPI",
Expand All @@ -39,7 +45,10 @@
"name": "oneAPI.name(oneAPI.device())",
"max_threads": "oneAPI.compute_properties(oneAPI.device()).maxTotalGroupSize",
"capability": None,
"uuid": None,
"pci_bus_id": None,
"GPUArrayType": "oneArray",
"supported_float_types": ["Float16", "BFloat16", "Float32", "Float64"],
},
"METAL": {
"pkg": "Metal",
Expand All @@ -49,16 +58,22 @@
"name": "Metal.device().name",
"max_threads": "Int(Metal.device().maxThreadsPerThreadgroup.width)",
"capability": None,
"uuid": None,
"pci_bus_id": None,
"GPUArrayType": "MtlArray",
"supported_float_types": ["Float16", "BFloat16", "Float32"],
},
"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
"max_threads": "1024", # arbitrary as CPU doesn't have a max threads per block concept
"capability": None,
"uuid": None,
"pci_bus_id": None,
"supported_float_types": ["Float16", "Float32", "Float64"],
"GPUArrayType": "Array",
},
}
Expand Down Expand Up @@ -113,10 +128,7 @@ def julia_backend_available_metal():
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."
)
warn(f"Metal backend detected, but {supported} < 3. Metal.jl requires Metal version 3 or higher.")
else:
return True
return False
Expand All @@ -126,9 +138,7 @@ 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()
)
subprocess.check_output("ls /dev/dri/by-path/".split())
return True
except (FileNotFoundError, subprocess.CalledProcessError):
return False
7 changes: 7 additions & 0 deletions kernel_tuner/backends/julia_helper_ext_amdgpu.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Extension of the julia_helper.jl file for the AMDGPU backend. You can assume `using AMDGPU` has ran.
# GPU timing events, recorded on the stream that the kernel is launched on
create_timing_events(::ROCBackend) = (
AMDGPU.HIP.HIPEvent(AMDGPU.stream(); do_record=false, timing=true),
AMDGPU.HIP.HIPEvent(AMDGPU.stream(); do_record=false, timing=true),
)
record_timing_event(evt::AMDGPU.HIP.HIPEvent) = AMDGPU.HIP.record(evt)
4 changes: 4 additions & 0 deletions kernel_tuner/backends/julia_helper_ext_cuda.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Extension of the julia_helper.jl file for the CUDA backend. You can assume `using CUDA` has ran.
# GPU timing events, recorded on the stream that the kernel is launched on
create_timing_events(::CUDABackend) = (CuEvent(), CuEvent())
record_timing_event(evt::CuEvent) = CUDA.record(evt, CUDA.stream())
37 changes: 37 additions & 0 deletions kernel_tuner/backends/julia_helper_ext_metal.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Extension of the julia_helper.jl file for the Metal backend. You can assume `using Metal` has ran.

# GPU timing. Metal.jl batches kernel launches into shared command buffers, which are only committed later, so a kernel
# launch has no command buffer of its own to time. Instead, the command buffers that Metal.jl commits while launching are
# recorded through its profiling hook, and the batch is committed right after the launch so that the kernel starts running.
# Kernel Tuner synchronizes before every launch, so the recorded command buffers only contain this kernel.
const metal_launched_buffers = Any[]

function metal_launch_recorded(launch)
empty!(metal_launched_buffers)
previous_hook = Metal.MTL.profile_hook[]
Metal.MTL.profile_hook[] = function (cmdbuf)
push!(metal_launched_buffers, cmdbuf)
previous_hook === nothing || previous_hook(cmdbuf)
end
try
launch()
Metal.flush!() # commit the batch that holds the kernel
finally
Metal.MTL.profile_hook[] = previous_hook
end
end

metal_is_done(cmdbuf::Metal.MTL.MTLCommandBuffer) = cmdbuf.status >= Metal.MTL.MTLCommandBufferStatusCompleted # also when it failed
metal_is_done(submission) = Metal.MTL.is_completed(submission) # Metal 4 submissions

# whether the command buffers of the last launch have finished
metal_kernel_finished() = all(metal_is_done, metal_launched_buffers)

# the GPU time in milliseconds of the last launch, from the start of its first command buffer to the end of its last,
# or `nothing` if not available (e.g. not finished yet)
function metal_gpu_time_ms()
isempty(metal_launched_buffers) && return nothing
ranges = [Metal.MTL.gpu_time_range(cmdbuf) for cmdbuf in metal_launched_buffers]
any(isnothing, ranges) && return nothing
return (maximum(last, ranges) - minimum(first, ranges)) * 1e3
end
1 change: 1 addition & 0 deletions kernel_tuner/backends/julia_helper_ext_oneapi.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Extension of the julia_helper.jl file for the oneAPI backend. You can assume `using oneAPI` has ran.
Loading
Loading