Skip to content

POCL: run kernels on Julia's threads with PoCL host kernels - #906

Open
maleadt wants to merge 9 commits into
mainfrom
tb/pocl-host-kernel-bench
Open

maleadt wants to merge 9 commits into
mainfrom
tb/pocl-host-kernel-bench

Conversation

@maleadt

@maleadt maleadt commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

The CPU back-end runs work-groups on Julia's threads through PoCL's cl_pocl_host_kernel extension (pocl_standalone_jll 7.2.3 or later). Launches bypass PoCL's command queue, events and thread pool, so launching a small kernel costs 0.1–0.3 µs instead of 4–82 µs with KA 0.10.3. Larger kernels run about as fast as with Polyester, or faster (see Known gaps for exceptions).

How it works

  • Mechanism. PoCL prepares a host kernel: a function that runs a range of work-groups on the calling thread. Launches of up to 8192 work-items run it on the launching task, unless the kernel's recent launches were slow. Larger launches use ThreadTeams.foreach_range, a team of Julia tasks that stays warm between launches.
  • Scheduling. Work-groups follow the Threads.@threads :static split: block k starts on default-pool thread k, regardless of the launching thread. A launch keeps this static split if the previous launch took < 50 µs and had the same group count. Otherwise, including on the first launch, idle threads can steal work so a slow or descheduled thread doesn't hold up the launch. ThreadTeams 1.3 steals from nearby threads first.
  • NUMA. With the same work-group count, blocks start on the same threads and work mostly on pages those threads touched first. KernelAbstractions.zeros/ones and the NUMA example therefore initialize data with a kernel.
  • Work-group fitting. Tuned work-groups shrink to a nearby divisor of the ndrange, or otherwise to a quarter, preserving vector-width multiples in small first dimensions. They also shrink to fit a host kernel's 1 MiB stack allowance on a Julia task.
  • Partial work-groups. Only partial work-groups run the bounds-checked kernel (12× slower per group on Zen 2, ~3× on AArch64); full ones run an unchecked variant in the same launch. A kernel's first launch with partial work-groups uses only the checked variant, so kernels launched once compile once.
  • Tasks. A launch doesn't yield while its kernel runs and delivers interrupts afterwards. Other tasks, timers and I/O run after launches that use the team or run long. Launches run on the launching task alone when called from a finalizer or a Threads.@threads loop, or when another task's launch is using the team.

PoCL's threads still run kernels whose explicit work-group sizes require more than the 1 MiB stack limit, kernels PoCL can't prepare as host kernels, and direct POCL calls with non-uniform sizes. JULIA_KA_CPU_THREADS can lower the thread count but cannot exceed Julia's default-pool size.

Pinning

Pin Julia's threads before the first launch: ThreadTeams reads the topology when its threads start. Team launches run block k on default-pool thread k. A launch from the interactive thread (where Julia 1.12+ runs the main task by default) runs no block itself unless a worker starts late, but small launches run entirely on it. So pin the launching thread too: start Julia with -t N,0 and use ThreadPinning.jl's pinthreads(:cores) or pinthreads(:numa), or keep the interactive thread and pass threadpool = :all to pinthreads. With fewer threads than cores, pinthreads(:numa) spreads threads over all NUMA domains. POCL_AFFINITY=1 affects only kernels running on PoCL's threads.

taskset alone isn't enough: on a Ryzen 5950X with taskset -c 0-15 and -t 16, Linux sometimes packs all 16 threads onto one CCD, and axpy 1e5 takes 5–9 µs instead of 3.

Results

Median µs per launch at d24bda8's predecessor 6290988 (same performance within noise), using Julia 1.12.7 and ThreadTeams 1.3.0. Cells show this PR (best Polyester / KA 0.10.3).

Workstations, -t N, threads restricted with taskset on Linux:

Xeon E3-1245 v6, 4 M1, 4 M3 Pro, 5 Ryzen 5950X, 16 Jetson Orin, 6
1 element 0.159 (0.005 / 6.15) 0.095 (0.004 / 13.7) 0.074 (0.004 / 5.41) 0.097 (0.004 / 4.41) 0.274 (0.009 / 25.9)
axpy 1e3 0.347 (0.407 / 7.98) 0.213 (0.277 / 10.7) 0.160 (0.237 / 10.5) 0.222 (0.311 / 13.3) 0.592 (0.947 / 38.6)
axpy 1e5 3.94 (8.91 / 21.1) 3.95 (9.83 / 77.5) 2.31 (6.06 / 29.1) 5.42* (3.86 / 26.8) 8.55 (19.2 / 94.2)
axpy 1e6 78.3 (58.7 / 157) 32.4 (52.2 / 253) 19.1 (34.5 / 112) 8.84 (18.5 / 97.6) 167 (224 / 283)
axpy 1e7 4023 (4048 / 4106) 2048 (2044 / 2182) 943 (869 / 911) 693 (553 / 2158) 2534 (3085 / 2516)
stencil 128³ 794 (793 / 889) 244 (388 / 485) 113 (126 / 150) 42.3 (45.5 / 90.7) 688 (879 / 954)

* Bimodal: 2.9–3.3 µs per process unless the scheduler packs the threads onto one CCD (see Pinning).

Servers, -t N,0, pinthreads(:cores):

2× EPYC 7502, 32 2× EPYC 7502, 64 2× Xeon Gold 6430, 32 2× Xeon Gold 6430, 64 Ampere Altra, 48
1 element 0.190 (0.006 / 19.4) 0.194 (0.006 / 19.9) 0.103 (0.005 / 64.3) 0.104 (0.004 / 64.6) 0.200 (0.008 / 82.4)
axpy 1e3 0.579 (0.314 / 160) 0.581 (0.314 / 174) 0.270 (0.313 / 153) 0.278 (0.312 / 145) 0.597 (0.683 / 208)
axpy 1e5 4.95 (4.58 / 445) 9.70 (5.13 / 385) 4.99 (5.18 / 153) 13.4 (5.17 / 226) 6.58 (5.79 / 291)
axpy 1e6 9.01 (15.1 / 1410) 13.4 (9.65 / 1769) 9.90 (20.2 / 458) 19.1 (20.0 / 589) 16.6 (41.8 / 1287)
axpy 1e7 65.1 (144 / 12874) 82.5 (73.1 / 13471) 133 (194 / 5722) 37.0 (93.3 / 5893) 363 (451 / 10606)
stencil 128³ 26.3 (26.6 / 1241) 22.2 (18.7 / 1728) 33.7 (32.8 / 746) 32.2 (27.6 / 769) 48.1 (98.5 / 1948)

At 32 threads, :cores uses one socket of the two-socket machines.

NUMA, ThreadTeams 1.3 against 1.2.1 at 64 threads, axpy 1e7, µs:

:cores :numa
2× Xeon Gold 6430 54.1 → 37.6 45.4 → 38.5
2× EPYC 7502 102 → 88.5 81.8 → 87.8

SAXPY over 2 × 500M Float32 values initialized by a kernel: 242 GB/s at 64 threads on the EPYC (both versions; Threads.@threads :static gets 240). At 32 threads, :numa spreads the threads over both sockets: 157 GB/s instead of 122 with :cores on the EPYC, 343–360 instead of 216 on the Xeon.

With a competing process on one of 4 cores (Xeon E3, -t 4,0), axpy 1e6 takes 79.7 µs (Polyester 108, KA 0.10.3 202).

Applications, ms per time step at an earlier head (f9d9337, ThreadTeams 1.2.0). Cells show this PR (KA 0.10.3; Polyester port of WaterLily's @loop):

EPYC 7502, 32 (one socket) Ryzen 5950X, 16 pinned
Oceananigans hydrostatic 180×90×10 7.71 (22.7) 5.86 (10.7)
Oceananigans nonhydrostatic 64³ 17.9 (61.9) 17.8 (31.6)
Breeze convective boundary layer 32³ 3.64 (31.2) 2.10 (7.09)
WaterLily Taylor–Green 128³ 66.4 (126; 62.9) 68.4 (94.6; 59.5)

Since those measurements, fitting work-groups to the host-kernel stack has kept WaterLily's donut body kernel off PoCL's threads: 11.3–13.2 ms per step instead of 14.4–15.0 on 16 Zen 5 threads.

Known gaps

  • A launch costs 0.1–0.3 µs, versus Polyester's 0.005 µs.
  • At 64 threads on two sockets, axpy 1e5 takes 1.9–2.6× as long as with Polyester, axpy 1e6 up to 1.4× and stencils about 1.2×.

Related

@maleadt
maleadt force-pushed the tb/pocl-host-kernel-bench branch from b3f62b1 to 93699f3 Compare October 19, 2026 17:54
@maleadt
maleadt changed the base branch from main to tb/pocl-rng-localmem October 19, 2026 17:54
@maleadt
maleadt added this pull request to stack #908 October 19, 2026 17:54
@maleadt
maleadt force-pushed the tb/pocl-host-kernel-bench branch from 93699f3 to f7a9ae0 Compare October 20, 2026 07:57
@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Benchmark Results

Show table
main ddd6405... main / ddd6405...
const/@Const/Float32/262144 0.311 ± 0.016 ms 0.256 ± 0.01 ms 1.21 ± 0.079
const/@Const/Float32/65536 0.122 ± 0.0088 ms 0.0628 ± 0.00024 ms 1.94 ± 0.14
const/@Const/Float64/262144 0.564 ± 0.021 ms 0.511 ± 0.011 ms 1.1 ± 0.047
const/@Const/Float64/65536 0.181 ± 0.0098 ms 0.128 ± 0.00084 ms 1.41 ± 0.077
const/unmarked/Float32/262144 0.443 ± 0.023 ms 0.383 ± 0.01 ms 1.16 ± 0.068
const/unmarked/Float32/65536 0.144 ± 0.0037 ms 0.0973 ± 0.0003 ms 1.48 ± 0.038
const/unmarked/Float64/262144 0.826 ± 0.014 ms 0.775 ± 0.0045 ms 1.07 ± 0.019
const/unmarked/Float64/65536 0.241 ± 0.015 ms 0.191 ± 0.0013 ms 1.26 ± 0.077
dgsem/2D/Float32/fluxdiff/element 0.553 ± 0.021 ms 0.458 ± 0.011 ms 1.21 ± 0.055
dgsem/2D/Float32/fluxdiff/node E=8 1.47 ± 0.031 ms 1.41 ± 0.01 ms 1.05 ± 0.023
dgsem/2D/Float32/fluxdiff/node halfsweep E=8 0.574 ± 0.029 ms 0.51 ± 0.011 ms 1.12 ± 0.061
dgsem/2D/Float32/fluxdiff/node halfsweep v-last E=8 0.613 ± 0.021 ms 0.527 ± 0.011 ms 1.16 ± 0.046
dgsem/2D/Float32/weak/element 0.198 ± 0.021 ms 0.144 ± 0.0009 ms 1.37 ± 0.15
dgsem/2D/Float32/weak/element private 0.346 ± 0.018 ms 0.292 ± 0.01 ms 1.19 ± 0.074
dgsem/2D/Float32/weak/node localmem E=1 0.186 ± 0.022 ms 0.115 ± 0.0011 ms 1.62 ± 0.19
dgsem/2D/Float32/weak/node localmem E=8 0.161 ± 0.019 ms 0.108 ± 0.00063 ms 1.49 ± 0.17
dgsem/2D/Float32/weak/node localmem v-last E=1 0.249 ± 0.022 ms 0.185 ± 0.0031 ms 1.34 ± 0.12
dgsem/2D/Float32/weak/node localmem v-last E=8 0.234 ± 0.019 ms 0.178 ± 0.0019 ms 1.32 ± 0.11
dgsem/2D/Float32/weak/node recompute E=8 0.465 ± 0.015 ms 0.405 ± 0.011 ms 1.15 ± 0.048
dgsem/2D/Float64/fluxdiff/element 0.557 ± 0.018 ms 0.482 ± 0.011 ms 1.16 ± 0.045
dgsem/2D/Float64/fluxdiff/node E=8 1.7 ± 0.01 ms 1.72 ± 0.0069 ms 0.988 ± 0.0071
dgsem/2D/Float64/fluxdiff/node halfsweep E=8 0.548 ± 0.016 ms 0.496 ± 0.01 ms 1.11 ± 0.039
dgsem/2D/Float64/fluxdiff/node halfsweep v-last E=8 0.615 ± 0.023 ms 0.558 ± 0.01 ms 1.1 ± 0.047
dgsem/2D/Float64/weak/element 0.217 ± 0.017 ms 0.158 ± 0.001 ms 1.38 ± 0.11
dgsem/2D/Float64/weak/element private 0.277 ± 0.016 ms 0.21 ± 0.0029 ms 1.32 ± 0.078
dgsem/2D/Float64/weak/node localmem E=1 0.174 ± 0.013 ms 0.121 ± 0.00087 ms 1.44 ± 0.11
dgsem/2D/Float64/weak/node localmem E=8 0.169 ± 0.022 ms 0.116 ± 0.00076 ms 1.46 ± 0.19
dgsem/2D/Float64/weak/node localmem v-last E=1 0.244 ± 0.014 ms 0.188 ± 0.0025 ms 1.3 ± 0.079
dgsem/2D/Float64/weak/node localmem v-last E=8 0.25 ± 0.021 ms 0.183 ± 0.0015 ms 1.36 ± 0.12
dgsem/2D/Float64/weak/node recompute E=8 0.473 ± 0.018 ms 0.411 ± 0.011 ms 1.15 ± 0.053
dgsem/3D/Float32/fluxdiff/element 1.79 ± 0.0095 ms 1.73 ± 0.0065 ms 1.03 ± 0.0067
dgsem/3D/Float32/fluxdiff/node E=2 4.34 ± 0.011 ms 4.26 ± 0.013 ms 1.02 ± 0.0041
dgsem/3D/Float32/fluxdiff/node halfsweep E=1 1.7 ± 0.012 ms 1.62 ± 0.0098 ms 1.05 ± 0.0095
dgsem/3D/Float32/fluxdiff/node halfsweep v-last E=1 1.97 ± 0.0052 ms 1.92 ± 0.0045 ms 1.03 ± 0.0037
dgsem/3D/Float32/weak/element 0.651 ± 0.024 ms 0.579 ± 0.011 ms 1.12 ± 0.046
dgsem/3D/Float32/weak/element private 0.683 ± 0.029 ms 0.62 ± 0.011 ms 1.1 ± 0.051
dgsem/3D/Float32/weak/node localmem E=1 0.608 ± 0.016 ms 0.552 ± 0.01 ms 1.1 ± 0.035
dgsem/3D/Float32/weak/node localmem E=2 0.581 ± 0.021 ms 0.517 ± 0.01 ms 1.12 ± 0.046
dgsem/3D/Float32/weak/node localmem v-last E=1 0.81 ± 0.0077 ms 0.752 ± 0.0046 ms 1.08 ± 0.012
dgsem/3D/Float32/weak/node localmem v-last E=2 0.941 ± 0.016 ms 0.843 ± 0.0022 ms 1.12 ± 0.02
dgsem/3D/Float32/weak/node recompute E=2 1.94 ± 0.03 ms 1.87 ± 0.0074 ms 1.04 ± 0.016
dgsem/3D/Float32/weak/slab E=1 0.828 ± 0.0042 ms 0.76 ± 0.0029 ms 1.09 ± 0.0069
dgsem/3D/Float32/weak/slab E=8 1.01 ± 0.024 ms 0.938 ± 0.0024 ms 1.08 ± 0.026
dgsem/3D/Float32/weak/slab double-buffered E=1 0.937 ± 0.0068 ms 0.865 ± 0.0025 ms 1.08 ± 0.0085
dgsem/3D/Float32/weak/slab double-buffered E=8 1.1 ± 0.0084 ms 1.04 ± 0.0026 ms 1.05 ± 0.0085
dgsem/3D/Float32/weak/slab double-buffered v-last E=1 0.798 ± 0.021 ms 0.713 ± 0.0086 ms 1.12 ± 0.032
dgsem/3D/Float32/weak/slab double-buffered v-last E=8 1.02 ± 0.0066 ms 0.94 ± 0.0023 ms 1.08 ± 0.0075
dgsem/3D/Float32/weak/slab v-last E=1 0.835 ± 0.022 ms 0.763 ± 0.0026 ms 1.09 ± 0.029
dgsem/3D/Float32/weak/slab v-last E=8 1.03 ± 0.023 ms 0.956 ± 0.0022 ms 1.08 ± 0.024
dgsem/3D/Float64/fluxdiff/element 1.86 ± 0.014 ms 1.8 ± 0.0046 ms 1.04 ± 0.0084
dgsem/3D/Float64/fluxdiff/node E=2 8.09 ± 0.031 ms 7.93 ± 0.017 ms 1.02 ± 0.0045
dgsem/3D/Float64/fluxdiff/node halfsweep E=1 1.79 ± 0.023 ms 1.69 ± 0.0091 ms 1.06 ± 0.015
dgsem/3D/Float64/fluxdiff/node halfsweep v-last E=1 2.07 ± 0.0074 ms 2.04 ± 0.006 ms 1.01 ± 0.0047
dgsem/3D/Float64/weak/element 0.683 ± 0.031 ms 0.627 ± 0.011 ms 1.09 ± 0.053
dgsem/3D/Float64/weak/element private 0.716 ± 0.018 ms 0.662 ± 0.01 ms 1.08 ± 0.032
dgsem/3D/Float64/weak/node localmem E=1 0.649 ± 0.02 ms 0.589 ± 0.011 ms 1.1 ± 0.039
dgsem/3D/Float64/weak/node localmem E=2 0.621 ± 0.02 ms 0.56 ± 0.011 ms 1.11 ± 0.042
dgsem/3D/Float64/weak/node localmem v-last E=1 0.829 ± 0.014 ms 0.768 ± 0.0053 ms 1.08 ± 0.02
dgsem/3D/Float64/weak/node localmem v-last E=2 0.955 ± 0.0039 ms 0.884 ± 0.0027 ms 1.08 ± 0.0055
dgsem/3D/Float64/weak/node recompute E=2 1.98 ± 0.014 ms 1.92 ± 0.0044 ms 1.03 ± 0.0076
dgsem/3D/Float64/weak/slab E=1 0.854 ± 0.0037 ms 0.843 ± 0.003 ms 1.01 ± 0.0056
dgsem/3D/Float64/weak/slab E=8 1.68 ± 0.019 ms 1.7 ± 0.0069 ms 0.991 ± 0.012
dgsem/3D/Float64/weak/slab double-buffered E=1 0.884 ± 0.0044 ms 0.807 ± 0.0027 ms 1.1 ± 0.0065
dgsem/3D/Float64/weak/slab double-buffered E=8 1.63 ± 0.013 ms 1.58 ± 0.011 ms 1.03 ± 0.011
dgsem/3D/Float64/weak/slab double-buffered v-last E=1 0.831 ± 0.017 ms 0.753 ± 0.0028 ms 1.1 ± 0.022
dgsem/3D/Float64/weak/slab double-buffered v-last E=8 1.55 ± 0.011 ms 1.49 ± 0.012 ms 1.04 ± 0.011
dgsem/3D/Float64/weak/slab v-last E=1 0.9 ± 0.02 ms 0.812 ± 0.0021 ms 1.11 ± 0.025
dgsem/3D/Float64/weak/slab v-last E=8 1.6 ± 0.012 ms 1.6 ± 0.011 ms 1 ± 0.01
launch/3D static workgroup, dynamic ndrange 10.3 ± 0.37 μs 0.201 ± 0.054 μs 51.4 ± 14
launch/3D static workgroup, static ndrange 13.1 ± 3.1 μs 0.175 ± 0.051 μs 75.2 ± 28
launch/dynamic workgroup, dynamic ndrange 11.6 ± 3.6 μs 0.145 ± 0.0016 μs 80.1 ± 25
launch/dynamic workgroup, dynamic ndrange, workgroupsize given 15.7 ± 0.43 μs 0.126 ± 0.0031 μs 125 ± 4.6
launch/static workgroup, dynamic ndrange 14.5 ± 0.63 μs 0.134 ± 0.0016 μs 108 ± 4.9
launch/static workgroup, static ndrange 10.5 ± 0.57 μs 0.123 ± 0.0022 μs 85 ± 4.9
narrow workgroup/masked/W=16 0.521 ± 0.017 ms 0.37 ± 0.011 ms 1.41 ± 0.062
narrow workgroup/masked/W=4 0.485 ± 0.018 ms 0.371 ± 0.011 ms 1.31 ± 0.061
narrow workgroup/unmasked/W=16 0.535 ± 0.022 ms 0.37 ± 0.011 ms 1.45 ± 0.073
narrow workgroup/unmasked/W=4 0.57 ± 0.022 ms 0.369 ± 0.011 ms 1.54 ± 0.076
partition/dynamic workgroup, dynamic ndrange 0.0584 ± 0.021 μs 0.0585 ± 0.024 μs 0.999 ± 0.54
partition/static workgroup, dynamic ndrange 0.0686 ± 0.012 μs 0.0649 ± 0.012 μs 1.06 ± 0.28
partition/static workgroup, static ndrange 2.46 ± 0.01 ns 1.55 ± 0 ns 1.59 ± 0.0064
saxpy/default/Float16/1024 13.8 ± 1.1 μs 0.68 ± 0.021 μs 20.2 ± 1.8
saxpy/default/Float16/1048576 0.207 ± 0.021 ms 0.155 ± 0.0066 ms 1.34 ± 0.15
saxpy/default/Float16/16384 18.7 ± 6.8 μs 2.91 ± 0.098 μs 6.42 ± 2.4
saxpy/default/Float16/2048 15.5 ± 3.2 μs 0.827 ± 0.0054 μs 18.7 ± 3.9
saxpy/default/Float16/256 10.9 ± 0.48 μs 0.542 ± 0.0032 μs 20.1 ± 0.9
saxpy/default/Float16/262144 0.0931 ± 0.017 ms 0.0397 ± 0.002 ms 2.35 ± 0.44
saxpy/default/Float16/32768 20.7 ± 8.5 μs 5.23 ± 0.18 μs 3.96 ± 1.6
saxpy/default/Float16/4096 22.2 ± 0.77 μs 1.14 ± 0.015 μs 19.4 ± 0.72
saxpy/default/Float16/512 10.6 ± 0.21 μs 0.618 ± 0.021 μs 17.1 ± 0.67
saxpy/default/Float16/64 11.7 ± 0.11 μs 0.507 ± 0.0024 μs 23.1 ± 0.24
saxpy/default/Float16/65536 26.8 ± 9.8 μs 10.3 ± 0.28 μs 2.61 ± 0.96
saxpy/default/Float32/1024 9.41 ± 4.2 μs 0.602 ± 0.0059 μs 15.6 ± 7
saxpy/default/Float32/1048576 0.293 ± 0.038 ms 0.247 ± 0.021 ms 1.19 ± 0.19
saxpy/default/Float32/16384 24.7 ± 1.2 μs 2.76 ± 0.61 μs 8.93 ± 2
saxpy/default/Float32/2048 15.3 ± 0.52 μs 0.694 ± 0.035 μs 22.1 ± 1.3
saxpy/default/Float32/256 10.9 ± 0.26 μs 0.513 ± 0.0044 μs 21.3 ± 0.54
saxpy/default/Float32/262144 0.106 ± 0.012 ms 0.0628 ± 0.0052 ms 1.69 ± 0.23
saxpy/default/Float32/32768 28.6 ± 5.3 μs 5.19 ± 0.47 μs 5.5 ± 1.1
saxpy/default/Float32/4096 15.3 ± 0.78 μs 1.06 ± 0.054 μs 14.4 ± 1
saxpy/default/Float32/512 13.3 ± 0.44 μs 0.581 ± 0.019 μs 22.9 ± 1.1
saxpy/default/Float32/64 11.7 ± 0.13 μs 0.499 ± 0.0038 μs 23.5 ± 0.31
saxpy/default/Float32/65536 0.0326 ± 0.0072 ms 13.5 ± 1.4 μs 2.41 ± 0.59
saxpy/default/Float64/1024 15.4 ± 1.1 μs 0.682 ± 0.0078 μs 22.6 ± 1.6
saxpy/default/Float64/1048576 0.633 ± 0.093 ms 0.546 ± 0.061 ms 1.16 ± 0.21
saxpy/default/Float64/16384 29.8 ± 4.8 μs 5.2 ± 0.36 μs 5.73 ± 1
saxpy/default/Float64/2048 15.8 ± 5.8 μs 1.05 ± 0.048 μs 15 ± 5.5
saxpy/default/Float64/256 14.8 ± 1.2 μs 0.543 ± 0.005 μs 27.3 ± 2.3
saxpy/default/Float64/262144 0.176 ± 0.017 ms 0.127 ± 0.012 ms 1.38 ± 0.19
saxpy/default/Float64/32768 0.034 ± 0.0039 ms 14.1 ± 1.7 μs 2.41 ± 0.41
saxpy/default/Float64/4096 16.9 ± 5.2 μs 1.63 ± 0.25 μs 10.3 ± 3.5
saxpy/default/Float64/512 13.3 ± 0.82 μs 0.6 ± 0.0052 μs 22.2 ± 1.4
saxpy/default/Float64/64 13.3 ± 2.8 μs 0.514 ± 0.004 μs 25.9 ± 5.4
saxpy/default/Float64/65536 0.0493 ± 0.0051 ms 31.6 ± 3.5 μs 1.56 ± 0.24
saxpy/static workgroup=(1024,)/Float16/1024 9.51 ± 0.62 μs 0.627 ± 0.0051 μs 15.2 ± 1
saxpy/static workgroup=(1024,)/Float16/1048576 0.214 ± 0.02 ms 0.158 ± 0.0082 ms 1.36 ± 0.14
saxpy/static workgroup=(1024,)/Float16/16384 17.1 ± 0.54 μs 3.01 ± 0.14 μs 5.69 ± 0.33
saxpy/static workgroup=(1024,)/Float16/2048 13.4 ± 1.5 μs 0.78 ± 0.0062 μs 17.2 ± 1.9
saxpy/static workgroup=(1024,)/Float16/256 15.9 ± 0.68 μs 1.07 ± 0.006 μs 14.8 ± 0.64
saxpy/static workgroup=(1024,)/Float16/262144 0.0946 ± 0.041 ms 0.0413 ± 0.0027 ms 2.29 ± 1
saxpy/static workgroup=(1024,)/Float16/32768 22.4 ± 7.3 μs 5.23 ± 0.28 μs 4.28 ± 1.4
saxpy/static workgroup=(1024,)/Float16/4096 14.7 ± 0.49 μs 1.11 ± 0.012 μs 13.2 ± 0.47
saxpy/static workgroup=(1024,)/Float16/512 12.5 ± 0.25 μs 1.35 ± 0.007 μs 9.26 ± 0.19
saxpy/static workgroup=(1024,)/Float16/64 11.4 ± 0.42 μs 0.869 ± 0.0029 μs 13.2 ± 0.49
saxpy/static workgroup=(1024,)/Float16/65536 26.4 ± 9.5 μs 10.3 ± 0.57 μs 2.58 ± 0.94
saxpy/static workgroup=(1024,)/Float32/1024 19 ± 0.53 μs 0.556 ± 0.0036 μs 34.2 ± 0.99
saxpy/static workgroup=(1024,)/Float32/1048576 0.29 ± 0.04 ms 0.247 ± 0.021 ms 1.17 ± 0.19
saxpy/static workgroup=(1024,)/Float32/16384 19.1 ± 1.1 μs 2.99 ± 1 μs 6.39 ± 2.2
saxpy/static workgroup=(1024,)/Float32/2048 15.2 ± 2.2 μs 0.648 ± 0.0067 μs 23.5 ± 3.3
saxpy/static workgroup=(1024,)/Float32/256 11.5 ± 0.92 μs 1.17 ± 0.009 μs 9.84 ± 0.79
saxpy/static workgroup=(1024,)/Float32/262144 0.105 ± 0.012 ms 0.064 ± 0.0051 ms 1.65 ± 0.24
saxpy/static workgroup=(1024,)/Float32/32768 24 ± 1.5 μs 5.02 ± 1 μs 4.79 ± 1
saxpy/static workgroup=(1024,)/Float32/4096 22.4 ± 7.3 μs 1.02 ± 0.012 μs 21.9 ± 7.1
saxpy/static workgroup=(1024,)/Float32/512 10.3 ± 0.45 μs 1.2 ± 0.009 μs 8.61 ± 0.38
saxpy/static workgroup=(1024,)/Float32/64 16.4 ± 0.51 μs 1.17 ± 0.006 μs 14 ± 0.44
saxpy/static workgroup=(1024,)/Float32/65536 0.0359 ± 0.0052 ms 13.9 ± 1.7 μs 2.57 ± 0.49
saxpy/static workgroup=(1024,)/Float64/1024 16 ± 7.5 μs 0.628 ± 0.0068 μs 25.5 ± 12
saxpy/static workgroup=(1024,)/Float64/1048576 0.587 ± 0.056 ms 0.514 ± 0.044 ms 1.14 ± 0.15
saxpy/static workgroup=(1024,)/Float64/16384 24.1 ± 5.2 μs 4.99 ± 0.62 μs 4.82 ± 1.2
saxpy/static workgroup=(1024,)/Float64/2048 15.7 ± 2.2 μs 1.01 ± 0.015 μs 15.6 ± 2.2
saxpy/static workgroup=(1024,)/Float64/256 10.4 ± 2.2 μs 1.12 ± 0.069 μs 9.34 ± 2
saxpy/static workgroup=(1024,)/Float64/262144 0.172 ± 0.022 ms 0.128 ± 0.012 ms 1.34 ± 0.22
saxpy/static workgroup=(1024,)/Float64/32768 0.0342 ± 0.0039 ms 13.3 ± 1.7 μs 2.58 ± 0.44
saxpy/static workgroup=(1024,)/Float64/4096 22.2 ± 5.6 μs 1.73 ± 0.28 μs 12.8 ± 3.8
saxpy/static workgroup=(1024,)/Float64/512 15.1 ± 2.4 μs 1.12 ± 0.03 μs 13.4 ± 2.2
saxpy/static workgroup=(1024,)/Float64/64 16.2 ± 0.41 μs 1.08 ± 0.0081 μs 15 ± 0.4
saxpy/static workgroup=(1024,)/Float64/65536 0.049 ± 0.0045 ms 0.0319 ± 0.0039 ms 1.54 ± 0.23
time_to_load 0.525 ± 0.0029 s 0.63 ± 0.0087 s 0.834 ± 0.012
main ddd6405... main / ddd6405...
const/@Const/Float32/262144 0 allocs: 0 B 0 allocs: 0 B
const/@Const/Float32/65536 0 allocs: 0 B 0 allocs: 0 B
const/@Const/Float64/262144 0 allocs: 0 B 0 allocs: 0 B
const/@Const/Float64/65536 0 allocs: 0 B 0 allocs: 0 B
const/unmarked/Float32/262144 0 allocs: 0 B 0 allocs: 0 B
const/unmarked/Float32/65536 0 allocs: 0 B 0 allocs: 0 B
const/unmarked/Float64/262144 0 allocs: 0 B 0 allocs: 0 B
const/unmarked/Float64/65536 0 allocs: 0 B 0 allocs: 0 B
dgsem/2D/Float32/fluxdiff/element 1 allocs: 16 B 1 allocs: 16 B 1
dgsem/2D/Float32/fluxdiff/node E=8 1 allocs: 32 B 1 allocs: 32 B 1
dgsem/2D/Float32/fluxdiff/node halfsweep E=8 1 allocs: 32 B 1 allocs: 32 B 1
dgsem/2D/Float32/fluxdiff/node halfsweep v-last E=8 1 allocs: 32 B 1 allocs: 32 B 1
dgsem/2D/Float32/weak/element 1 allocs: 16 B 1 allocs: 16 B 1
dgsem/2D/Float32/weak/element private 1 allocs: 16 B 1 allocs: 16 B 1
dgsem/2D/Float32/weak/node localmem E=1 1 allocs: 32 B 1 allocs: 32 B 1
dgsem/2D/Float32/weak/node localmem E=8 1 allocs: 32 B 1 allocs: 32 B 1
dgsem/2D/Float32/weak/node localmem v-last E=1 1 allocs: 32 B 1 allocs: 32 B 1
dgsem/2D/Float32/weak/node localmem v-last E=8 1 allocs: 32 B 1 allocs: 32 B 1
dgsem/2D/Float32/weak/node recompute E=8 1 allocs: 32 B 1 allocs: 32 B 1
dgsem/2D/Float64/fluxdiff/element 1 allocs: 16 B 1 allocs: 16 B 1
dgsem/2D/Float64/fluxdiff/node E=8 1 allocs: 32 B 1 allocs: 32 B 1
dgsem/2D/Float64/fluxdiff/node halfsweep E=8 1 allocs: 32 B 1 allocs: 32 B 1
dgsem/2D/Float64/fluxdiff/node halfsweep v-last E=8 1 allocs: 32 B 1 allocs: 32 B 1
dgsem/2D/Float64/weak/element 1 allocs: 16 B 1 allocs: 16 B 1
dgsem/2D/Float64/weak/element private 1 allocs: 16 B 1 allocs: 16 B 1
dgsem/2D/Float64/weak/node localmem E=1 1 allocs: 32 B 1 allocs: 32 B 1
dgsem/2D/Float64/weak/node localmem E=8 1 allocs: 32 B 1 allocs: 32 B 1
dgsem/2D/Float64/weak/node localmem v-last E=1 1 allocs: 32 B 1 allocs: 32 B 1
dgsem/2D/Float64/weak/node localmem v-last E=8 1 allocs: 32 B 1 allocs: 32 B 1
dgsem/2D/Float64/weak/node recompute E=8 1 allocs: 32 B 1 allocs: 32 B 1
dgsem/3D/Float32/fluxdiff/element 1 allocs: 16 B 1 allocs: 16 B 1
dgsem/3D/Float32/fluxdiff/node E=2 4 allocs: 0.188 kB 4 allocs: 0.188 kB 1
dgsem/3D/Float32/fluxdiff/node halfsweep E=1 1 allocs: 0.0469 kB 1 allocs: 0.0469 kB 1
dgsem/3D/Float32/fluxdiff/node halfsweep v-last E=1 1 allocs: 0.0469 kB 1 allocs: 0.0469 kB 1
dgsem/3D/Float32/weak/element 1 allocs: 16 B 1 allocs: 16 B 1
dgsem/3D/Float32/weak/element private 1 allocs: 16 B 1 allocs: 16 B 1
dgsem/3D/Float32/weak/node localmem E=1 1 allocs: 0.0469 kB 1 allocs: 0.0469 kB 1
dgsem/3D/Float32/weak/node localmem E=2 4 allocs: 0.188 kB 4 allocs: 0.188 kB 1
dgsem/3D/Float32/weak/node localmem v-last E=1 1 allocs: 0.0469 kB 1 allocs: 0.0469 kB 1
dgsem/3D/Float32/weak/node localmem v-last E=2 4 allocs: 0.188 kB 4 allocs: 0.188 kB 1
dgsem/3D/Float32/weak/node recompute E=2 4 allocs: 0.188 kB 4 allocs: 0.188 kB 1
dgsem/3D/Float32/weak/slab E=1 1 allocs: 32 B 1 allocs: 32 B 1
dgsem/3D/Float32/weak/slab E=8 1 allocs: 32 B 1 allocs: 32 B 1
dgsem/3D/Float32/weak/slab double-buffered E=1 1 allocs: 32 B 1 allocs: 32 B 1
dgsem/3D/Float32/weak/slab double-buffered E=8 1 allocs: 32 B 1 allocs: 32 B 1
dgsem/3D/Float32/weak/slab double-buffered v-last E=1 1 allocs: 32 B 1 allocs: 32 B 1
dgsem/3D/Float32/weak/slab double-buffered v-last E=8 1 allocs: 32 B 1 allocs: 32 B 1
dgsem/3D/Float32/weak/slab v-last E=1 1 allocs: 32 B 1 allocs: 32 B 1
dgsem/3D/Float32/weak/slab v-last E=8 1 allocs: 32 B 1 allocs: 32 B 1
dgsem/3D/Float64/fluxdiff/element 1 allocs: 16 B 1 allocs: 16 B 1
dgsem/3D/Float64/fluxdiff/node E=2 4 allocs: 0.188 kB 4 allocs: 0.188 kB 1
dgsem/3D/Float64/fluxdiff/node halfsweep E=1 1 allocs: 0.0469 kB 1 allocs: 0.0469 kB 1
dgsem/3D/Float64/fluxdiff/node halfsweep v-last E=1 1 allocs: 0.0469 kB 1 allocs: 0.0469 kB 1
dgsem/3D/Float64/weak/element 1 allocs: 16 B 1 allocs: 16 B 1
dgsem/3D/Float64/weak/element private 1 allocs: 16 B 1 allocs: 16 B 1
dgsem/3D/Float64/weak/node localmem E=1 1 allocs: 0.0469 kB 1 allocs: 0.0469 kB 1
dgsem/3D/Float64/weak/node localmem E=2 4 allocs: 0.188 kB 4 allocs: 0.188 kB 1
dgsem/3D/Float64/weak/node localmem v-last E=1 1 allocs: 0.0469 kB 1 allocs: 0.0469 kB 1
dgsem/3D/Float64/weak/node localmem v-last E=2 4 allocs: 0.188 kB 4 allocs: 0.188 kB 1
dgsem/3D/Float64/weak/node recompute E=2 4 allocs: 0.188 kB 4 allocs: 0.188 kB 1
dgsem/3D/Float64/weak/slab E=1 1 allocs: 32 B 1 allocs: 32 B 1
dgsem/3D/Float64/weak/slab E=8 1 allocs: 32 B 1 allocs: 32 B 1
dgsem/3D/Float64/weak/slab double-buffered E=1 1 allocs: 32 B 1 allocs: 32 B 1
dgsem/3D/Float64/weak/slab double-buffered E=8 1 allocs: 32 B 1 allocs: 32 B 1
dgsem/3D/Float64/weak/slab double-buffered v-last E=1 1 allocs: 32 B 1 allocs: 32 B 1
dgsem/3D/Float64/weak/slab double-buffered v-last E=8 1 allocs: 32 B 1 allocs: 32 B 1
dgsem/3D/Float64/weak/slab v-last E=1 1 allocs: 32 B 1 allocs: 32 B 1
dgsem/3D/Float64/weak/slab v-last E=8 1 allocs: 32 B 1 allocs: 32 B 1
launch/3D static workgroup, dynamic ndrange 4 allocs: 0.125 kB 4 allocs: 0.125 kB 1
launch/3D static workgroup, static ndrange 4 allocs: 0.125 kB 4 allocs: 0.125 kB 1
launch/dynamic workgroup, dynamic ndrange 0 allocs: 0 B 0 allocs: 0 B
launch/dynamic workgroup, dynamic ndrange, workgroupsize given 0 allocs: 0 B 0 allocs: 0 B
launch/static workgroup, dynamic ndrange 0 allocs: 0 B 0 allocs: 0 B
launch/static workgroup, static ndrange 0 allocs: 0 B 0 allocs: 0 B
narrow workgroup/masked/W=16 0 allocs: 0 B 0 allocs: 0 B
narrow workgroup/masked/W=4 0 allocs: 0 B 0 allocs: 0 B
narrow workgroup/unmasked/W=16 0 allocs: 0 B 0 allocs: 0 B
narrow workgroup/unmasked/W=4 0 allocs: 0 B 0 allocs: 0 B
partition/dynamic workgroup, dynamic ndrange 2 allocs: 0.0625 kB 2 allocs: 0.0625 kB 1
partition/static workgroup, dynamic ndrange 2 allocs: 32 B 2 allocs: 32 B 1
partition/static workgroup, static ndrange 0 allocs: 0 B 0 allocs: 0 B
saxpy/default/Float16/1024 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/default/Float16/1048576 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/default/Float16/16384 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/default/Float16/2048 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/default/Float16/256 0 allocs: 0 B 0 allocs: 0 B
saxpy/default/Float16/262144 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/default/Float16/32768 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/default/Float16/4096 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/default/Float16/512 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/default/Float16/64 0 allocs: 0 B 0 allocs: 0 B
saxpy/default/Float16/65536 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/default/Float32/1024 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/default/Float32/1048576 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/default/Float32/16384 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/default/Float32/2048 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/default/Float32/256 0 allocs: 0 B 0 allocs: 0 B
saxpy/default/Float32/262144 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/default/Float32/32768 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/default/Float32/4096 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/default/Float32/512 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/default/Float32/64 0 allocs: 0 B 0 allocs: 0 B
saxpy/default/Float32/65536 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/default/Float64/1024 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/default/Float64/1048576 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/default/Float64/16384 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/default/Float64/2048 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/default/Float64/256 0 allocs: 0 B 0 allocs: 0 B
saxpy/default/Float64/262144 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/default/Float64/32768 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/default/Float64/4096 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/default/Float64/512 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/default/Float64/64 0 allocs: 0 B 0 allocs: 0 B
saxpy/default/Float64/65536 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/static workgroup=(1024,)/Float16/1024 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/static workgroup=(1024,)/Float16/1048576 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/static workgroup=(1024,)/Float16/16384 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/static workgroup=(1024,)/Float16/2048 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/static workgroup=(1024,)/Float16/256 0 allocs: 0 B 0 allocs: 0 B
saxpy/static workgroup=(1024,)/Float16/262144 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/static workgroup=(1024,)/Float16/32768 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/static workgroup=(1024,)/Float16/4096 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/static workgroup=(1024,)/Float16/512 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/static workgroup=(1024,)/Float16/64 0 allocs: 0 B 0 allocs: 0 B
saxpy/static workgroup=(1024,)/Float16/65536 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/static workgroup=(1024,)/Float32/1024 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/static workgroup=(1024,)/Float32/1048576 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/static workgroup=(1024,)/Float32/16384 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/static workgroup=(1024,)/Float32/2048 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/static workgroup=(1024,)/Float32/256 0 allocs: 0 B 0 allocs: 0 B
saxpy/static workgroup=(1024,)/Float32/262144 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/static workgroup=(1024,)/Float32/32768 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/static workgroup=(1024,)/Float32/4096 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/static workgroup=(1024,)/Float32/512 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/static workgroup=(1024,)/Float32/64 0 allocs: 0 B 0 allocs: 0 B
saxpy/static workgroup=(1024,)/Float32/65536 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/static workgroup=(1024,)/Float64/1024 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/static workgroup=(1024,)/Float64/1048576 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/static workgroup=(1024,)/Float64/16384 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/static workgroup=(1024,)/Float64/2048 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/static workgroup=(1024,)/Float64/256 0 allocs: 0 B 0 allocs: 0 B
saxpy/static workgroup=(1024,)/Float64/262144 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/static workgroup=(1024,)/Float64/32768 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/static workgroup=(1024,)/Float64/4096 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/static workgroup=(1024,)/Float64/512 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
saxpy/static workgroup=(1024,)/Float64/64 0 allocs: 0 B 0 allocs: 0 B
saxpy/static workgroup=(1024,)/Float64/65536 3 allocs: 0.0469 kB 3 allocs: 0.0469 kB 1
time_to_load 0.2 k allocs: 11.8 kB 0.2 k allocs: 11.8 kB 1

Benchmark Plots

A plot of the benchmark results have been uploaded as an artifact to the workflow run for this PR.
Go to "Actions"->"Benchmark a pull request"->[the most recent run]->"Artifacts" (at the bottom).

@giordano

Copy link
Copy Markdown
Collaborator

This brings back some allocations that were removed with #901 👀

@maleadt

maleadt commented Oct 20, 2026

Copy link
Copy Markdown
Member Author

This brings back some allocations that were removed with #901 👀

Should be fixed.

Comment thread src/pocl/compiler/execution.jl Outdated
Comment thread src/pocl/compiler/execution.jl Outdated
@giordano

Copy link
Copy Markdown
Collaborator

I asked the bot to measure the impact of this PR on Oceananigans 👇


PR 906 cuts the cost of launching a kernel on the CPU by 10–50 times compared with KA 0.10.1, which brings it back to KA 0.9's level or better. Oceananigans workloads with many small kernels get 1.5–5 times faster. It has two costs, though: kernels that run for milliseconds get 7–33% slower with 4–8 threads, and each new kernel takes about 1.5 times as long to compile.

All three rounds finished with no errors, using Julia 1.13 and PR head 7eef96c. Each process was pinned to its own CCD, and the CCD assignment rotated between rounds. Numbers are the median over the 3 rounds of each round's fastest window; brackets give the range across rounds where it matters.

Launch cost, standalone kernels (µs per launch, at 1 / 4 / 8 threads)

kernel KA 0.9 (main) KA 0.10.1 PR 906
empty, ndrange 1 0.02 / 0.02 / 0.02 58 / 5.2 / 5.2 0.13 / 0.11 / 0.11
empty, 66² 0.13 / 2.7 / 3.0 58 / 5.9 / 6.1 0.27 / 0.63 / 0.63
trivial write, 66² 0.72 / 2.8 / 3.4 58 / 6.0 / 6.7 0.49 / 0.83 / 0.76
8 arrays as arguments, 16² 0.51 / 0.48 / 0.49 58 / 5.6 / 6.9 0.31 / 0.85 / 0.84
stencil over 8 arrays, 32³ 519 / 293 / 159 440 / 136 / 100 336 / 124 / 60
stencil over 8 arrays, 128³ (long kernel) 34.4k / 9.8k / 5.2k 22.3k / 5.7k / 3.1k 22.3k / 7.6k / 3.8k
  • KA 0.10.1 on 1 thread costs 58 µs per launch, even for an empty kernel. This probably happens because PoCL's worker thread and Julia's thread share the one core the process is pinned to. CI containers or jobs limited to one CPU would see the same. The PR removes it.
  • The PR only regresses on long kernels. The 128³ stencil is 33% slower than 0.10.1 at 4 threads and 22% slower at 8.

Oceananigans (1 / 4 / 8 threads)

workload KA 0.9 (main) KA 0.10.1 PR 906
fill_halo_regions! 16³ (µs) 1.7 / 8.7 / 14 176 / 19 / 27 1.6 / 2.1 / 2.1
fill_halo_regions! 128³ (µs) 75 / 54 / 38 256 / 43 / 58 63 / 17 / 17
compute! of an operation, 16³ (µs) 43 / 45 / 29 253 / 31 / 49 12.5 / 6.4 / 5.5
compute! of an operation, 128³ (ms) 21 / 7.5 / 4.4 5.7 / 1.55 / 0.87 4.9 / 1.40 / 0.76
hydrostatic 36×18×10 step (ms) 4.0 / 3.5 / 2.3 9.6 / 1.69 / 1.73 1.8 / 1.07 / 0.81
hydrostatic 180×90×10 step (ms) 99 / 55 / 29 58 / 22.2 / 12.5 45 / 23.8 / 14.9
nonhydrostatic 32³ step (ms) 21 / 33 / 10 37 / 13.9 / 8.5 26 / 12.0 / 4.9
  • Small grids, dominated by launch cost, get 1.6–5 times faster than with 0.10.1. At 8 threads the small hydrostatic step goes from 1.73 to 0.81 ms; that is also 2.9 times faster than main.
  • The larger 180×90×10 hydrostatic model is slower with the PR at 4 and 8 threads: +7% at 4 threads and +19% at 8 (12.5 → 14.9 ms). This held in all three rounds and matches the slowdown in the long standalone kernel. With 1 thread it is 22% faster.
  • PR timings vary more between rounds than 0.10.1's: the small hydrostatic step ranged 0.95–1.26 ms at 4 threads.

Memory allocations

  • The PR allocates exactly as much as KA 0.10.1 in every case: 56–152 B per launch, 0.06 KiB per hydrostatic step and 61–153 KiB per nonhydrostatic step.
  • Main with KA 0.9 allocates more as threads increase:
    • per launch: 2.3–5 KB at 4–8 threads, against 56–520 B on 1 thread
    • per time step: 0.5–1 MiB at 4–8 threads
    • per call to fill_halo_regions! or compute!: 8–38 KiB at 4–8 threads, where the PR allocates nothing

Compile latency

KA 0.9 KA 0.10.1 PR 906
each new kernel (median of 10) 47–53 ms 112–153 ms 169–185 ms
first kernel in the process 72–82 ms 196–219 ms 314–366 ms
small hydrostatic model: build + first step, 8 threads 18.0 + 0.4 s 26.0 + 4.8 s 30.2 + 5.3 s

The PR adds about 30–60 ms per new kernel on top of 0.10.1, which comes to about 4–9 s more before the first step of a real model finishes. Main is still far ahead here.

Bottom line: the PR fixes the launch overhead KA 0.10 introduced and beats KA 0.9 on any kernel that isn't trivially small. Before it merges, two things need fixing upstream: the 7–33% slowdown of long kernels with several threads, and the extra compile latency.

@maleadt
maleadt force-pushed the tb/pocl-host-kernel-bench branch from 7eef96c to 9581115 Compare October 21, 2026 05:46
@maleadt

maleadt commented Oct 21, 2026

Copy link
Copy Markdown
Member Author

Thanks. Analysis by Claude:

Compile latency reproduces, and it's on the KA side, not in PoCL: the PR costs the same on the release PoCL JLL, where the host path is never taken. Most of it was the host-launch code being specialized and inlined for every kernel: host_dispatch was @inline (expanded twice per kernel), the fallback for non-uniform grids compiled the queue path a second time with the keyword arguments in another order, and both paths had their own copy of the exception handling. I fixed those in the PR's commits (now 9581115). On a 9950X with a warm PoCL cache, a new kernel goes from ~172 ms to ~144 ms; main takes ~115 ms. What's left is the lookup cache and the argument packing, ~12 ms each. The extra time on a process's first kernel is ThreadTeams compiling its job closure for the launch type at run time, which KA can't precompile.

The long-kernel slowdown doesn't reproduce here. I ran stencils over 8 arrays at 128³ with 1/4/8 threads, pinned to one CCD, pinned to exactly N cores, with a CPU hog on one of the cores, and with a memory-bandwidth hog on the other CCD. The host path stays within about ±5% of KA 0.10.1. Neither more chunks per thread nor running long launches on the calling task instead of a worker task made a consistent difference. Now and then a whole process ran ~30% slower in every host mode, which may be the round-to-round spread you saw, but I couldn't pin it on anything specific to the PR.

Could you share the benchmark script, the machine (CPU, SMT on or off) and how each process was pinned (the taskset mask for each thread count)? A 4/3 ratio at 4 threads looks like one thread doing no work, and I'd like to see that happen.

@giordano

giordano commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

The machine I used for the benchmarks often has some bursts of high load, I'm running again the benchmarks now to see if the previous results still reproduce (load right now seems to be relatively low), anyway for the record below are the KA-only and Oceananigans benchmarks, the issue with longer kernels was allegedly visible already with the KA-only ones. Machine is

julia> versioninfo()
Julia Version 1.13.2
Commit 45b6d15e34b (2026-10-14 20:33 UTC)
Build Info:
  Official https://julialang.org release
Platform Info:
  OS: Linux (x86_64-linux-gnu)
  CPU: 192 × AMD Ryzen Threadripper PRO 7995WX 96-Cores
  WORD_SIZE: 64
  LLVM: libLLVM-20.1.8 (ORCJIT, znver4)
  GC: Built with stock GC
Threads: 1 default, 1 interactive, 1 GC (on 192 virtual cores)

Script to run the benchmarks is

#!/bin/bash
# Each env on its own CCD (L3), CCDs rotated across rounds; output tagged with env/threads/round.
S=/path/to/scratchpad
OUT=$S/results.txt
ENVS=(ka09 ka010 ka906)
CCD=(16 64 72)
for r in 1 2 3; do
    for i in 0 1 2; do
        (
            e=${ENVS[$i]}; base=${CCD[$(( (i + r) % 3 ))]}
            for t in 1 4 8; do
                cores="$base-$((base + t - 1))"
                for script in kalat oclat; do
                    taskset -c $cores timeout 1800 julia +1.13 --startup-file=no -t $t --project=$S/env-$e $S/$script.jl > $S/logs/$script-$e-t$t-r$r.log 2>&1
                    grep -E '^RESULT|ERROR' $S/logs/$script-$e-t$t-r$r.log | sed "s/^/env=$e t=$t r=$r cores=$cores /" >> $OUT.$e
                done
            done
        ) &
    done
    wait
done
cat $OUT.ka09 $OUT.ka010 $OUT.ka906 > $OUT
echo done > $S/matrix.done

taskset was indeed used.


KernelAbstractions only benchmarks
# Host cost, allocations and compile latency of KernelAbstractions kernels on the CPU backend.
#   taskset -c <cores> julia +1.13 -t N --project=env kalat.jl
t_start = time()
using KernelAbstractions
using Statistics: median
using Printf
t_load = time() - t_start

@kernel function empty_kernel!(a)
end

@kernel function trivial!(a)
    i = @index(Global, Linear)
    @inbounds a[i] = 1f0
end

@kernel function many_args!(a, xs)
    i = @index(Global, Linear)
    @inbounds a[i] = first(xs)[i]
end

@kernel function stencil!(out, ins)
    i, j, k = @index(Global, NTuple)
    s = zero(eltype(out))
    for x in ins
        @inbounds s += x[i, j, k] + x[i + 1, j, k] - x[i, j + 1, k] + x[i, j, k + 1]
    end
    @inbounds out[i, j, k] = s / length(ins)
end

@kernel function new_kernel!(a, v, n)
    i = @index(Global, Linear)
    @inbounds a[i] = v
end

const backend = CPU()
sync() = KernelAbstractions.synchronize(backend)

# minimum and median over `reps` windows of the time per launch of `n` back-to-back launches
function per_launch(f, n; reps = 15)
    ts = map(1:reps) do _
        t0 = time_ns()
        for _ in 1:n
            f()
        end
        sync()
        (time_ns() - t0) / n / 1e3
    end
    return minimum(ts), median(ts)
end

a1 = zeros(Float32, 1)
a66 = zeros(Float32, 66, 66)
small = zeros(Float32, 16, 16)
xs = ntuple(_ -> ones(Float32, 16, 16), 8)
out32 = zeros(Float32, 32, 32, 32)
ins32 = ntuple(_ -> rand(Float32, 33, 33, 33), 8)
out128 = zeros(Float32, 128, 128, 128)
ins128 = ntuple(_ -> rand(Float32, 129, 129, 129), 8)

cases = (
    "empty, ndrange 1"           => (() -> empty_kernel!(backend)(a1; ndrange = 1), 20_000),
    "empty, ndrange 66²"         => (() -> empty_kernel!(backend)(a66; ndrange = size(a66)), 20_000),
    "trivial write, ndrange 1"   => (() -> trivial!(backend)(a1; ndrange = 1), 20_000),
    "trivial write, 66²"         => (() -> trivial!(backend)(a66; ndrange = size(a66)), 20_000),
    "8 arrays as args, 16²"      => (() -> many_args!(backend)(small, xs; ndrange = size(small)), 20_000),
    "stencil 8 arrays, 32³"      => (() -> stencil!(backend)(out32, ins32; ndrange = size(out32)), 500),
    "stencil 8 arrays, 128³"     => (() -> stencil!(backend)(out128, ins128; ndrange = size(out128)), 10),
)

@printf("RESULT ka=%s threads=%d load_s=%.2f\n", pkgversion(KernelAbstractions), Threads.nthreads(), t_load)
for (name, (f, n)) in cases
    t_first = @elapsed (f(); sync())
    tmin, tmed = per_launch(f, n)
    m = min(n, 2000)
    f(); sync()
    b = @allocated (for _ in 1:m; f(); end; sync())
    @printf("RESULT case=%-26s first_ms=%9.1f min_us=%10.2f med_us=%10.2f B_per_launch=%8.1f\n",
            repr(name), 1e3 * t_first, tmin, tmed, b / m)
end

# first launch (compilation included) of new kernels: each `Val(n)` is a new kernel
firsts = map(1:11) do n
    @elapsed (new_kernel!(backend)(a66, 1f0, Val(n); ndrange = size(a66)); sync())
end
@printf("RESULT case=\"new kernel compile\" first_ms=%.1f median_next10_ms=%.1f\n", 1e3 * firsts[1], 1e3 * median(firsts[2:end]))
Oceananigans benchmarks
# Oceananigans workloads on the CPU: individual operations and full time steps.
# Reports first-call time (compilation), steady time per call and bytes allocated per call.
#   taskset -c <cores> julia +1.13 -t N --project=env oclat.jl
t_start = time()
using Oceananigans
using Oceananigans.BoundaryConditions: fill_halo_regions!
using Statistics: median
using Printf
using KernelAbstractions
t_load = time() - t_start
@printf("RESULT ka=%s threads=%d load_s=%.2f\n", pkgversion(KernelAbstractions), Threads.nthreads(), t_load)

# `windows` windows of `n` calls; minimum and median time per call
function steady(f, n; windows = 10)
    for _ in 1:3; f(); end
    ts = map(1:windows) do _
        (@elapsed for _ in 1:n; f(); end) / n
    end
    b = @allocated f()
    return minimum(ts), median(ts), b
end

function report(name, setup_s, f, n)
    t_first = @elapsed f()
    tmin, tmed, b = steady(f, n)
    unit, s = tmin < 1e-3 ? ("us", 1e6) : ("ms", 1e3)
    @printf("RESULT case=%-34s setup_s=%7.2f first_s=%7.2f min_%s=%10.3f med_%s=%10.3f KiB_per_call=%8.2f\n",
            repr(name), setup_s, t_first, unit, s * tmin, unit, s * tmed, b / 1024)
end

for N in (16, 128)
    t_setup = @elapsed begin
        grid = RectilinearGrid(CPU(); size = (N, N, N), extent = (1, 1, 1), halo = (3, 3, 3))
        c = CenterField(grid)
        set!(c, (x, y, z) -> x * y + z)
        op = Field(∂x(c) * ∂y(c) + c^2)
    end
    n = N == 16 ? 2000 : 20
    report("fill_halo_regions! $(N)³", t_setup, () -> fill_halo_regions!(c), n)
    report("compute! ∂x(c)*∂y(c)+c² $(N)³", 0.0, () -> compute!(op), n)
end

function hydrostatic(Nx, Ny, Nz)
    grid = LatitudeLongitudeGrid(CPU(); size = (Nx, Ny, Nz), longitude = (0, 360), latitude = (-75, 75),
                                 z = (-1000, 0), halo = (7, 7, 7))
    model = HydrostaticFreeSurfaceModel(grid;
                                        momentum_advection = WENOVectorInvariant(),
                                        tracer_advection = WENO(order = 7),
                                        buoyancy = SeawaterBuoyancy(),
                                        tracers = (:T, :S),
                                        closure = CATKEVerticalDiffusivity(),
                                        coriolis = HydrostaticSphericalCoriolis(),
                                        free_surface = SplitExplicitFreeSurface(grid; substeps = 30))
    set!(model; T = (λ, φ, z) -> 20 + 10 * z / 1000 + 0.01 * rand(), S = 35)
    return model
end

function nonhydrostatic(N)
    grid = RectilinearGrid(CPU(); size = (N, N, N), extent = (1, 1, 1), halo = (5, 5, 5))
    model = NonhydrostaticModel(grid; advection = WENO(order = 9), buoyancy = BuoyancyTracer(), tracers = :b,
                                closure = ScalarDiffusivity(ν = 1e-4, κ = 1e-4))
    set!(model; b = (x, y, z) -> z + 1e-3 * rand(), u = (x, y, z) -> 1e-3 * rand())
    return model
end

for (name, build, n) in (("hydrostatic latlon 36x18x10", () -> hydrostatic(36, 18, 10), 20),
                         ("hydrostatic latlon 180x90x10", () -> hydrostatic(180, 90, 10), 5),
                         ("nonhydrostatic 32³", () -> nonhydrostatic(32), 20))
    t_setup = @elapsed (model = build())
    report("time_step! " * name, t_setup, () -> time_step!(model, 1), n)
end

@giordano

Copy link
Copy Markdown
Collaborator

Results on new head of the branch. I'll ask it to investigate the slowdown for the larger hydrostatic model at 4-8 threads, but it'd be great if you could at least try to reproduce it. I'm using CliMA/Oceananigans.jl#5799 (of course you'll have to point KA to this branch).


The update cut the PR's extra compile time per kernel by about half and left everything else unchanged. Launch cost, allocations and almost all runtimes are the same as with the old head. The new head also looks steadier on the small hydrostatic model. The 4–8 thread slowdown of the larger hydrostatic model compared with KA 0.10.1 is still there.

All four environments ran together in each of 3 rounds, so they share today's host load. Columns are KA 0.9 (main) / KA 0.10.1 / old PR head 7eef96c / new PR head 9581115. Numbers are medians over the 3 rounds of each round's fastest window.

Launch cost, standalone kernels (µs per launch, at 1 / 4 / 8 threads)

kernel KA 0.9 KA 0.10.1 old head new head
empty, ndrange 1 0.02 / 0.02 / 0.02 57 / 4.8 / 4.9 0.11 / 0.11 / 0.10 0.18 / 0.11 / 0.11
empty, 66² 0.12 / 2.3 / 3.0 57 / 5.3 / 6.0 0.24 / 0.49 / 0.60 0.38 / 0.56 / 0.61
trivial write, 66² 0.68 / 2.6 / 3.3 58 / 5.7 / 6.7 0.47 / 0.68 / 0.74 0.47 / 0.71 / 0.74
8 arrays as arguments, 16² 0.44 / 0.44 / 0.45 57 / 5.7 / 6.9 0.29 / 0.66 / 0.81 0.30 / 0.71 / 0.82
stencil over 8 arrays, 32³ 469 / 241 / 193 442 / 145 / 100 308 / 95 / 87 322 / 95 / 70
stencil over 8 arrays, 128³ (long kernel) 32.2k / 8.7k / 4.5k 22.5k / 6.0k / 3.2k 20.6k / 5.9k / 3.0k 21.5k / 5.9k / 3.1k
  • Launch cost is the same with both PR heads.
  • Neither PR head is slower than 0.10.1 on the long 128³ kernel today. Yesterday the old head measured 22–33% slower at 4–8 threads, so that was probably host noise. The larger hydrostatic model's slowdown therefore doesn't come from kernels simply running long; something specific to Oceananigans triggers it.

Oceananigans (1 / 4 / 8 threads)

workload KA 0.9 KA 0.10.1 old head new head
fill_halo_regions! 16³ (µs) 1.5 / 9.2 / 13 177 / 19 / 26 1.5 / 2.0 / 2.1 1.6 / 2.0 / 2.3
fill_halo_regions! 128³ (µs) 67 / 51 / 37 249 / 44 / 56 57 / 16 / 10 58 / 15 / 14
compute! of an operation, 16³ (µs) 38 / 34 / 29 255 / 32 / 51 11.3 / 6.2 / 5.9 11.7 / 6.1 / 5.9
compute! of an operation, 128³ (ms) 18.5 / 7.0 / 4.5 5.4 / 1.25 / 0.85 4.3 / 1.39 / 0.72 4.6 / 1.39 / 0.67
hydrostatic 36×18×10 step (ms) 3.6 / 7.3* / 3.7 9.1 / 2.0 / 1.9 1.65 / 1.07 [0.93–6.7] / 0.79 1.74 / 0.98 [0.95–0.99] / 0.69
hydrostatic 180×90×10 step (ms) 89 / 50 / 33 60 / 18.4 / 11.2 45 / 21.3 / 13.6 46 / 22.7 / 14.0
nonhydrostatic 32³ step (ms) 19 / 21 / 15 39 / 14.0 / 7.6 24 / 11.2 / 4.7 26 / 11.4 / 4.8

* KA 0.9 was noisy on this row today (4.2–13.3 ms at 4 threads), which looks like host interference.

  • The small hydrostatic model is 2–2.7 times faster with the new head than with 0.10.1 at 4–8 threads, and 5 times faster than main. It is also steadier than the old head, which had 1 run in 3 jump to 6.7 ms at 4 threads.
  • The larger 180×90×10 hydrostatic model is still slower with either PR head than with 0.10.1. At 4 threads it takes 22.7 ms against 18.4 (+23%); at 8 threads 14.0 ms against 11.2 (+25%). With 1 thread the PR is 24% faster.

Memory allocations

These are identical for the old head, the new head and KA 0.10.1:

  • 56–152 B per launch
  • 0.06 KiB per hydrostatic step and 81–153 KiB per nonhydrostatic step (4–8 threads)
  • nothing for fill_halo_regions! and compute!

Main with KA 0.9 still allocates more as threads increase: 2.3–5 KB per launch and 0.5–1 MiB per step at 4–8 threads.

Compile latency

KA 0.9 KA 0.10.1 old head new head
each new kernel, 1 / 4 / 8 threads (ms) 48 / 42 / 43 105 / 98 / 113 161 / 153 / 164 142 / 140 / 133
first kernel in the process (ms) 63–70 181–196 279–300 261–296
small hydrostatic model: build + first step, 8 threads (s) 16.9 + 0.4 24.2 + 4.7 28.0 + 5.1 27.9 + 5.0
  • The extra compile time per new kernel, compared with 0.10.1, fell from about 55–60 ms to about 20–40 ms.
  • That doesn't yet show in model building: the new head still needs about 4 s more than 0.10.1 to build the model and take its first step, the same as the old head.
  • One round-3 run of the new head on 1 thread measured 196 ms per kernel. That same run also had outliers in the stencil and hydrostatic rows, so I excluded it as host noise.

Bottom line: the update made compilation cheaper and didn't change launch cost. Before this is better than 0.10.1 everywhere, two things remain:

  • the 23–25% slowdown of the larger hydrostatic model at 4–8 threads, which now looks specific to Oceananigans
  • about 4 s of extra time-to-first-step

@maleadt

maleadt commented Oct 21, 2026 •

Copy link
Copy Markdown
Member Author

Thanks. Analysis by Claude:

Long kernels. I reran your kalat.jl with your pinning (t physical cores, 3 rounds) on a quiet 4-core Xeon E3-1245 v6. The 128³ stencil doesn't get slower in any configuration:

128³ stencil, min per launch 1 thread 2 threads 4 threads
KA 0.10.1 28.5 ms 14.9–15.8 ms 7.67–7.71 ms
PR 28.4–28.5 ms 14.7 ms 7.70–7.82 ms

The 32³ stencil gets faster: 181 → 122 µs at 4 threads. On a busy 9950X the PR was faster than 0.10.1 at 128³ as well. If your rerun still shows the slowdown, a run with JULIA_KA_POCL_HOST_KERNELS=0 (the PR's code, but launching through PoCL's own threads) would tell whether the host-kernel path causes it.

Compile latency. Two separate costs:

@giordano

Copy link
Copy Markdown
Collaborator

Do I understand correctly that this now uses the interactive thread when available, so that if we do thread pinning we either need to use julia -t N,0 and pin to N cores, or julia -t N,M and pin to N+M cores (keeping in mind that julia -N implicitly adds 1 interactive thread)?

@giordano

giordano commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

New summary, I like that it put the "bottom line" at the top.

Bottom line

  • The PR largely removes the per-launch cost that KA 0.10 introduced on the CPU. A launch's fixed overhead drops from about 5 µs (58 µs on 1 thread) to 0.1–0.8 µs, the level of KA 0.9 or better.
  • Small and medium Oceananigans workloads get faster: a small hydrostatic model steps 2.5–3 times faster than with KA 0.10.1, and 5 times faster than main.
  • Allocations are the same as KA 0.10.1. KA 0.9 on main allocates far more at 4–8 threads.
  • Two problems remain:
    1. Each new kernel takes longer to compile: 120–145 ms, against 95–112 ms for KA 0.10.1 and 41–48 ms for KA 0.9. Building a model and taking the first step costs about 4 s more than with KA 0.10.1.
    2. The PR's static work split makes it fragile on oversubscribed cores. If any core it runs on is busy with something else, the whole launch waits for that core. Two things cause this: Julia's own interactive thread when -t N runs on N cores, and other users' processes on a shared machine. KA 0.10.1 is unaffected because PoCL schedules work dynamically. This, not slower kernels, explains the slowdown I first measured on the larger hydrostatic model.

Setup

  • Julia 1.13, three environments:
    • Oceananigans main (0b8bd0977) with KA 0.9.45
    • branch mg/ka-0.10 (df04ed7e9) with KA 0.10.1
    • branch mg/ka-0.10 with PR 906, head 7eef96c and later 9581115
  • Standalone kernels: an empty kernel, a trivial write, a kernel with 8 array arguments, and stencils over 8 arrays at 32³ and 128³. Measured per launch: time, allocations, and compile latency of new kernels.
  • Oceananigans workloads:
    • fill_halo_regions! and compute! of an operation, at 16³ and 128³
    • time steps of a hydrostatic lat-lon model (WENO, CATKE, split-explicit free surface) at 36×18×10 and 180×90×10
    • time steps of a nonhydrostatic model at 32³
  • Method:
    • 1, 4 and 8 threads, each process pinned with taskset to cores on a single CCD (no SMT siblings)
    • environments run in parallel on separate CCDs, rotated between rounds, 2–3 rounds each
    • each round's fastest window is reported, and the median over rounds is shown
  • Host: a shared 96-core Threadripper. Its load turned out to matter; see the oversubscription section.

Launch cost, allocations and compile latency (8 threads unless noted)

KA 0.9 (main) KA 0.10.1 PR 906
empty kernel, ndrange 1 (µs per launch) 0.02 5.1 (57 on 1 thread) 0.11
empty kernel, 66² (µs per launch) 3.0 6.2 0.60
8 arrays as arguments, 16² (µs per launch) 0.47 6.8 0.83
stencil over 8 arrays, 32³ (µs per launch) 72 105 43
stencil over 8 arrays, 128³ (ms per launch) 4.6 3.3 3.0
allocations (bytes per launch) 2.3–5 KB at 4–8 threads 56–152 56–152
compile, each new kernel (ms) 41–48 95–112 121–145 (9581115); 153–164 (7eef96c)
small hydrostatic model: build + first step (s) 17 + 0.4 24 + 4.7 28 + 5.0
  • Single thread: KA 0.10.1 costs 57 µs per launch, even for an empty kernel. This is probably PoCL's worker thread and Julia's thread competing for the single pinned core. The PR fixes it.
  • The PR update (7eef96c → 9581115) merged the two launch paths and stopped inlining host_dispatch. It cut the extra compile time per kernel, compared with 0.10.1, roughly in half, and changed nothing else measurable.

Oceananigans, cleanest measurement (-t N,0: no interactive thread)

4 / 8 threads KA 0.9 KA 0.10.1 PR 906 (9581115)
hydrostatic 36×18×10 step (ms) 2.6 / 1.6 2.0 / 1.8 0.78 / 0.62
hydrostatic 180×90×10 step (ms) 40.6 / 15.0 18.3 / 11.5 14.7 / 10.7
nonhydrostatic 32³ step (ms) 9.9 / 3.9 10.1 / 7.1 7.1 / 5.5
fill_halo_regions! 16³ (µs) 9.0 / 13 18 / 25 2.1 / 2.1
compute! of an operation, 16³ (µs) 26 / 24 32 / 52 6.6 / 5.7
compute! of an operation, 128³ (ms) 6.6 / 2.7 1.65 / 0.78 1.22 / 1.03
allocations per hydrostatic step 0.5–1 MiB 0.06 KiB 0.06 KiB
  • On 1 thread the PR is faster than KA 0.10.1 everywhere: the 180×90×10 hydrostatic step takes 45 ms against 59 ms, and the small hydrostatic step 1.7 ms against 9 ms.
  • Remaining PR losses: compute! on a 128³ grid at 8 threads is about 30% slower, and the nonhydrostatic step is slower than KA 0.9. Both still need confirming on a quiet host.

Why the larger model looked slower: oversubscription

  • The pinned cores were distinct physical cores. Hyperthreading was not the cause.

  • Per-kernel timing of the 180×90×10 step: each kernel's fastest launch is the same or faster with the PR. But kernels whose median launch is 50–250 µs sometimes take 3.7–8 ms, which is about one scheduler timeslice.

  • Involuntary context switches over 20 steps at 8 threads: 505–905 with the PR, against 46–214 with KA 0.10.1. With -t 8,0 the PR has 0–36, and its step time matches or beats KA 0.10.1 (10.6–12.5 ms against 12.4–12.7 ms).

  • Mechanism: on Julia 1.12 and later, -t N means N default threads plus 1 interactive thread, and the main task runs on the interactive thread. ThreadTeams' pool only covers the default threads, so the launching task runs one chunk of the kernel while N workers run (or spin on) the rest. That's N+1 busy threads on N cores. The PR splits each launch into fixed per-thread ranges, so a preempted thread holds up the whole launch. ThreadTeams' existing mitigations didn't help: oversubscription detection with yielding, and set_spin_time!(0).

  • External load has the same effect. Another user is running about 20 unpinned single-threaded Julia jobs, which keep 1–2 cores per CCD busy. Runs whose cores included one of those had the outliers:

    • the PR's 180×90×10 step at 4 threads: 38 ms instead of 14.6
    • KA 0.9 at 8 threads: 33 ms instead of 18
    • KA 0.10.1 stayed steady throughout.

    This also explains the "erratic" PR results earlier. Main (KA 0.9) has the same weakness: it went from 29–33 ms to 15 ms at 8 threads once the interactive thread was removed.

  • This matters beyond benchmarks. julia -t auto, SLURM -c N with julia -t N, and any shared node give N+1 or more busy threads on N cores.

Open items

  1. Get clean numbers for the 10–25% differences on larger models. That needs a quiet host or isolated cores: wait for the other jobs to finish, or use another machine.
  2. Suggestions for the PR author:
    • Hand out chunks dynamically for every launch, as the PR already does for long ones, so a preempted thread holds back only a small piece of work.
    • Don't let a caller outside the pool run a chunk while all N workers do, or use one fewer worker when the process has more Julia threads than CPUs.
    • Keep cutting compile latency.
  3. Explain the remaining PR losses: compute! on 128³ and the nonhydrostatic step at 8 threads.

Edit: it turned out that the compute! slowdown has the same cause as the large hydrostatic one: resources contention.

@maleadt

maleadt commented Oct 21, 2026

Copy link
Copy Markdown
Member Author

Do I understand correctly that this now uses the interactive thread when available, so that if we do thread pinning we either need to use julia -t N,0 and pin to N cores, or julia -t N,M and pin to N+M cores (keeping in mind that julia -N implicitly adds 1 interactive thread)?

Partly, yes. Launches are sized to Threads.nthreads(:default), but the launching task always runs one chunk itself, and since Julia 1.12 a script's main task runs on the interactive thread. A launch shorter than 50 µs therefore uses the interactive thread plus N−1 default workers, while the Nth worker spins idle between back-to-back launches: N+1 busy threads. After a launch that took ≥ 50 µs, the next one goes to a task on the default pool and uses exactly N. Measured on 4 pinned cores:

taskset -c 0-3 short launches long launches
-t 4 (= 4,1) 5 busy threads 4
-t 4,0 4 4

So: -t N,0 pinned to N cores, or -t N,M pinned to N+M cores. This doesn't explain the 128³ slowdown, which takes the long path.

@codecov

codecov Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.85942% with 42 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.51%. Comparing base (c10ff8c) to head (d24bda8).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
src/pocl/compiler/execution.jl 86.21% 39 Missing ⚠️
src/pocl/nanoOpenCL.jl 87.50% 2 Missing ⚠️
src/pocl/pocl.jl 93.33% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #906      +/-   ##
==========================================
+ Coverage   83.46%   83.51%   +0.04%     
==========================================
  Files          31       31              
  Lines        2571     2881     +310     
==========================================
+ Hits         2146     2406     +260     
- Misses        425      475      +50     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@maleadt
maleadt force-pushed the tb/pocl-host-kernel-bench branch from 9581115 to 7e60b1b Compare October 22, 2026 11:57
Base automatically changed from tb/pocl-rng-localmem to main October 23, 2026 13:13
@maleadt
maleadt force-pushed the tb/pocl-host-kernel-bench branch from 7e60b1b to b1cf2ca Compare October 23, 2026 13:13
@maleadt
maleadt force-pushed the tb/pocl-host-kernel-bench branch from b1cf2ca to ca7721f Compare October 26, 2026 20:44
@maleadt maleadt changed the title [benchmark] POCL: run kernels on Julia's threads with PoCL host kernels POCL: run kernels on Julia's threads with PoCL host kernels Oct 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

@giordano

Copy link
Copy Markdown
Collaborator

The bot seems to be very happy now


The new head of this PR (ca7721f) is faster than both KA 0.10.1 and KA 0.10.3 on every Oceananigans workload I measured, at 1, 4 and 8 threads, including the large hydrostatic model. That holds with the default -t N too, which used to make the PR slower. The host was idle this time (load average about 2), so rounds agree within about 3%; that's the cleanest data of the whole investigation.

Setup:

  • KA 0.9.45 with Oceananigans main at the branch's base (607c4549f)
  • KA 0.10.1 (the baseline you specified) and KA 0.10.3 (the current release, which the PR is now based on), both with mg/cuda-int32
  • The PR at ca7721f, with Oceananigans#mg/cuda-int32
  • -t N,0 means no interactive thread; -t N is Julia's default (with the interactive thread). Both run on N cores. Each environment ran on its own CCD, 2 rounds.

Oceananigans (1 / 4 / 8 threads with -t N,0; then 4 / 8 with -t N)

workload KA 0.9 KA 0.10.1 KA 0.10.3 PR ca7721f
hydrostatic 180×90×10 step (ms), -t N,0 75.5 / 21.2 / 12.2 50.7 / 15.8 / 9.8 48.8 / 15.1 / 9.4 36.8 / 10.9 / 6.6
same, -t N 43.4 / 25.9 16.0 / 9.9 14.5 / 9.6 12.9 / 8.1
hydrostatic 36×18×10 step (ms), -t N,0 3.29 / 1.25 / 0.95 7.85 / 1.62 / 1.54 7.82 / 1.53 / 1.45 1.57 / 0.65 / 0.48
same, -t N 3.41 / 1.73 1.57 / 1.51 1.53 / 1.45 0.88 / 0.53
nonhydrostatic 32³ step (ms), -t N,0 17.5 / 5.4 / 3.4 31.7 / 8.9 / 6.2 30.5 / 8.6 / 5.9 19.1 / 5.5 / 3.2
same, -t N 16.9 / 10.1 13.2 / 8.8 13.3 / 8.6 9.0 / 4.2
fill_halo_regions! 16³ (µs), -t N,0 1.43 / 8.3 / 12.7 174 / 20 / 23 175 / 21 / 25 1.40 / 1.44 / 1.51
compute! of an operation, 16³ (µs), -t N,0 41 / 22 / 24 254 / 30 / 41 253 / 32 / 40 11.3 / 5.5 / 4.7
compute! of an operation, 128³ (ms), -t N,0 18.7 / 4.9 / 2.6 4.9 / 1.20 / 0.85 4.6 / 1.17 / 0.80 4.1 / 1.15 / 0.56
  • The large hydrostatic model's slowdown is gone. At 8 threads the PR takes 6.6 ms per step, against 9.4–9.8 ms for KA 0.10.x, about 30% faster. Even with the default -t 8 it is 16–18% faster (8.1 vs 9.6–9.9 ms). The work stealing in this head fixes the oversubscription problem.
  • The interactive thread still costs the PR about 20%. For example 6.6 → 8.1 ms at 8 threads, and 4.7 → 6.9 µs for the small compute!. KA 0.10.x pays nothing for it. KA 0.9 pays far more: its step time doubles with the default -t N.
  • Single thread: KA 0.10.3 still costs 57 µs per launch, even for an empty kernel. The PR fixes that. Against KA 0.9, the only place the PR is behind is the nonhydrostatic step on 1 thread, by 9% (19.1 vs 17.5 ms).
  • The only cases the PR doesn't win:
    • compute! on 128³ at 4 threads with -t 4: 1.44 ms [1.14–1.75] against 1.18–1.38 for 0.10.x
    • the 128³ stencil with -t 4 and -t 8: within 1% of 0.10.x

Standalone kernels (µs per launch, -t N,0, 1 / 4 / 8 threads)

kernel KA 0.9 KA 0.10.1 KA 0.10.3 PR ca7721f
empty, ndrange 1 0.02 / 0.02 / 0.02 57 / 4.9 / 4.8 57 / 4.7 / 4.8 0.11 / 0.10 / 0.10
empty, 66² 0.12 / 2.2 / 2.9 57 / 5.4 / 5.7 57 / 5.0 / 5.5 0.24 / 0.27 / 0.28
8 arrays as arguments, 16² 0.40 / 0.40 / 0.42 57 / 5.5 / 6.2 57 / 5.1 / 6.0 0.28 / 0.30 / 0.33
stencil over 8 arrays, 32³ 460 / 123 / 69 422 / 136 / 99 429 / 137 / 100 315 / 84 / 43
stencil over 8 arrays, 128³ 30.6k / 8.0k / 4.3k 20.5k / 5.5k / 2.9k 21.0k / 5.5k / 2.9k 20.4k / 5.3k / 2.8k
  • The launch cost on a 66² kernel roughly halved compared with the previous head (0.28 against 0.60 µs).
  • At 4 and 8 threads, the PR's launch cost is now lower than KA 0.9's for anything bigger than a single work-item.

Allocations

Unchanged: the PR allocates exactly what KA 0.10.x does, 56–152 B per launch and 0.06 KiB per hydrostatic step. KA 0.9 allocates 2.3–5 KB per launch and 0.5–0.9 MiB per step at 4 and 8 threads.

Compile latency

KA 0.9 KA 0.10.1 KA 0.10.3 PR ca7721f
each new kernel (ms) 37–43 84–107 56–59 (one outlier at 73) 66–71 (two outliers at 83–84)
first kernel in the process (ms) 58–63 158–175 149–165 199–207
small hydrostatic model, 8 threads: build + first step (s) 15.3 + 0.35 18.6 + 3.9 19.0 + 4.1 19.5 + 4.2
  • Compile time is no longer a real cost of the PR. It is now about 10–14 ms per kernel above KA 0.10.3, against +30–40 ms with the previous heads. Building a model and taking its first step costs about 0.5 s more than with 0.10.3.
  • KA 0.10.3 itself cut compile time per kernel by about 40% compared with 0.10.1 (56 vs 92 ms).
  • KA 0.9 is still about 4 s faster to build a model and take the first step.

Bottom line: this head no longer has a scaling problem. What's left is about 10 ms of extra compile time per kernel compared with 0.10.3, and about 20% lost to Julia's interactive thread. Even with that loss, the PR is ahead of every KA 0.10 release. I didn't re-run the per-kernel breakdown this time: with the host this quiet the averages were already consistent.

Partial work-groups use the bounds-checked kernel, whose work-item loop
vectorizes poorly without masked loads and stores (AArch64: ~3x slower
for a stencil). A partial y or z dimension makes a whole slab partial.
On an M1, a 126^3 stencil takes 200 us with 126x21 work-groups instead of
287 us with 126x32.
Masked loads and stores make partial work-groups slow on Zen 2 (12x per
work-group): a 1-d launch over 1000003 elements took 26-68 us at 16
threads on an EPYC 7502, now 21 (17 for a size that divides). Shrinking
to a quarter bounds the work in partial groups; work-group sizes that
divide the ndrange stay unchanged.
Linearized launches put all work-items in the first dimension, obscuring
total-size errors: (96, 48) was reported as "(4608, 1, 1) exceeds the
maximum of (4096, 4096, 4096)". Report the total-size limit first.
The tests ran on one thread, where the CPU backend runs every launch on the
launching task, so its team of tasks was only tested by the POCL session
test. Also run them with 4 threads on Linux and macOS.
Handing a launch to PoCL's thread pool and waiting for it costs about
5 µs, even for an empty kernel. PoCL's `cl_pocl_host_kernel` extension,
in every pocl_standalone_jll since 7.2.3, runs work-groups on the calling
thread instead. On a 4-core x86 machine, a launch now costs about
0.15 µs on the launching task and 1 µs on a ThreadTeams team.

Launches of up to 8192 work-items stay on the launching task unless the
kernel's recent launches were slow. Larger ones use `foreach_range`,
which starts block `k` of a `Threads.@threads :static` split on thread
`k`, keeping data in place across launches; threads steal work when the
grid changed or the previous launch took 50 µs or more. ThreadTeams 1.3
steals from nearby threads first.

Host kernels can't yield: interrupts are deferred until the launch ends,
and tasks, timers and I/O run after team and long launches. Runs are
GC-safe, and launches from finalizers, which can't switch tasks, run
every work-group on the calling thread.

The command queue remains for explicit work-group sizes over the 1 MiB
stack limit, kernels PoCL can't prepare, and direct POCL calls with
non-uniform sizes. The thread count is capped at Julia's default pool.
Only partial work-groups need the bounds check, but a launch with any ran
the checked kernel, whose work-item loop vectorizes poorly, for all of
them. Run the full ones with a variant compiled without the check, in
the same launch, sharing arguments, geometry and exception status.
Linear launches, launches on the command queue, and launches past three
dimensions with a partial third dimension stay checked throughout.

A kernel's first launch with partial work-groups stays checked, so that
kernels used once, such as AcceleratedKernels closures, compile once: on
4 Xeon E3-1245 v6 threads, a trivial kernel's first launch takes 122 ms,
as through PoCL's queue, instead of 167 ms. If the unchecked variant
can't run as a host kernel, the checked one runs every work-group.
Work-groups whose stack estimate exceeds 1 MiB run on PoCL's command
queue instead of as host kernels. WaterLily's donut body kernel (344 B
per work-item, 4096 work-items on Zen 5) did so every step. Cap tuned
work-groups, once per kernel at link time, at the largest power of two
that fits; explicit sizes still fall back. On 16 Zen 5 threads, donut_5
takes 2.2-2.35 ms per step instead of 2.9-3.0, and donut_6 11.3-13.2 ms
instead of 14.4-15.0.

Fitting work-groups to the ndrange also left small work-item loops with
scalar remainders: axpy over 1000 work-items on 32 threads took 162 ns
with groups of 25 instead of 146 ns with groups of 32. Shrink a first
dimension below 16 vectors only to a vector-width multiple.
Describe which thread runs which work-groups, when a launch stays on the
launching task or lets other tasks run, and which kernels still run on
PoCL's threads. Pin threads with ThreadPinning.jl rather than
`POCL_AFFINITY`; the NUMA example and its measurements now do so.
@maleadt
maleadt force-pushed the tb/pocl-host-kernel-bench branch from 6290988 to d24bda8 Compare October 28, 2026 09:15
@github-actions

Copy link
Copy Markdown
Contributor

Benchmark results

Compared with c10ff8cf4, the merge base.
The environment differs from the baseline's: GPUCompiler 2.18.0 → 2.19.0, KernelInterface 0.4.3 → 0.4.4, LinearAlgebraExt 0.10.3 → 0.1.46.

Times are the minimum time of every benchmark. A change is unusual when it exceeds both three times the benchmark's typical change between consecutive runs on the default branch and a minimum per category (performance 3%, latency 5%, applications 3%, other 3%). A benchmark that starts or stops allocating is listed however few the allocations. A single run can still be a fluke, so rerun before drawing conclusions.

Geometric mean Unusual: slower Unusual: faster Compared
Performance +59.3% 0 0 31
Latency −54.5% 0 0 13
Applications +161.2% 0 0 5

No unusual changes.

All benchmarks
Benchmark Baseline This PR Change
Performance
julia/serial/arithmetic/muladd-reference 655 µs 655 µs +0.0% noise unknown
julia/serial/math/exp-f64-reference 290 µs 284 µs −1.9% noise unknown
julia/serial/narrow/masked-reference 689 µs 708 µs +2.9% noise unknown
pocl/parallel/atomics/histogram 5.13 ms 6.20 ms +20.7% noise unknown
pocl/parallel/groupreduce/add-f64 1.65 ms 13.0 ms +688.9% noise unknown
pocl/parallel/groupscan/add-i32 2.50 ms 12.9 ms +415.5% noise unknown
pocl/parallel/indexing/cartesian3d 73.7 µs 237 µs +221.7% noise unknown
pocl/parallel/indexing/linear3d 81.0 µs 243 µs +200.3% noise unknown
pocl/parallel/indexing/ntuple3d 80.7 µs 256 µs +217.1% noise unknown
pocl/parallel/indexing/ntuple4d 162 µs 973 µs +500.6% noise unknown
pocl/parallel/localmem/transpose-naive 455 µs 3.26 ms +615.7% noise unknown
pocl/parallel/localmem/transpose-tiled 125 µs 428 µs +242.1% noise unknown
pocl/parallel/private/across-barrier 164 µs 1.04 ms +536.0% noise unknown
pocl/parallel/scheduling/default 185 µs 922 µs +398.5% noise unknown
pocl/parallel/scheduling/explicit 181 µs 922 µs +408.5% noise unknown
pocl/parallel/stream/copy/cache 786 ns 704 ns −10.4% noise unknown
pocl/parallel/stream/copy/dram 6.16 ms 6.34 ms +3.0% noise unknown
pocl/parallel/stream/triad/cache 6.84 µs 1.12 µs −83.6% noise unknown
pocl/parallel/stream/triad/dram 9.34 ms 8.52 ms −8.7% noise unknown
pocl/parallel/util/copyto-small 24.0 ns 24.0 ns +0.0% noise unknown
pocl/parallel/util/zeros-large 995 µs 1.02 ms +2.7% noise unknown
pocl/parallel/util/zeros-small 651 ns 628 ns −3.5% noise unknown
pocl/serial/arithmetic/contract 1.91 ms 1.85 ms −3.2% noise unknown
pocl/serial/arithmetic/muladd 764 µs 705 µs −7.7% noise unknown
pocl/serial/const/marked 722 µs 668 µs −7.4% noise unknown
pocl/serial/const/unmarked 922 µs 860 µs −6.8% noise unknown
pocl/serial/math/exp-f64 146 µs 130 µs −11.0% noise unknown
pocl/serial/math/sin-f64 186 µs 172 µs −7.5% noise unknown
pocl/serial/narrow/masked 824 µs 619 µs −25.0% noise unknown
pocl/serial/narrow/unmasked 789 µs 634 µs −19.7% noise unknown
pocl/serial/stream/triad/dram 8.59 ms 8.52 ms −0.8% noise unknown
Latency
host/import/ka 509 ms 591 ms +16.0% noise unknown
host/precompile/ka 44.6 s 46.0 s +3.1% noise unknown
host/startup 132 ms 130 ms −1.9% noise unknown
pocl/parallel/launch/dynamic-default 4.30 µs 215 ns −95.0% noise unknown
pocl/parallel/launch/groupreduce-f64 5.03 µs 1.06 µs −79.0% noise unknown
pocl/parallel/launch/many-args 12.4 µs 677 ns −94.5% noise unknown
pocl/parallel/launch/partial-workgroup 4.19 µs 228 ns −94.6% noise unknown
pocl/serial/compile/groupreduce 1.88 s 2.03 s +8.0% noise unknown
pocl/serial/compile/localmem-barrier 719 ms 757 ms +5.3% noise unknown
pocl/serial/compile/new-kernel 110 ms 130 ms +19.1% noise unknown
pocl/serial/compile/new-workgroupsize 13.6 ms 13.6 ms +0.5% noise unknown
pocl/serial/first-kernel/cold-cache 2.05 s 1.25 s −39.0% noise unknown
pocl/serial/first-kernel/warm-cache 392 ms 461 ms +17.7% noise unknown
Applications
pocl/parallel/dgsem/fluxdiff-node 291 µs 1.97 ms +577.9% noise unknown
pocl/parallel/dgsem/weak-element 203 µs 730 µs +259.1% noise unknown
pocl/parallel/dgsem/weak-node-localmem 148 µs 647 µs +338.8% noise unknown
pocl/parallel/stencil/timestep 7.80 ms 10.2 ms +30.3% noise unknown
pocl/serial/stencil/timestep 11.1 ms 9.74 ms −12.6% noise unknown

History: https://juliagpu.org/perf/?pkg=kernelabstractions

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants