Skip to content

Run full workgroups of kernels without @synchronize unmasked on the CPU - #862

Draft
vchuravy wants to merge 1 commit into
mainfrom
vc/unmasked-full-groups-nosync
Draft

vchuravy wants to merge 1 commit into
mainfrom
vc/unmasked-full-groups-nosync

Conversation

@vchuravy

Copy link
Copy Markdown
Member

@kernel runs the body under a per-work-item bounds mask, also in workgroups that lie entirely inside the ndrange. On the CPU backend the mask keeps PoCL from vectorizing the work-item loop. Math functions then become one scalar libm call per work-item instead of using PoCL's vector math library (SLEEF), and cheap bodies pay for masked loads and stores (#845, #860).

This is #849 restricted to kernels without @synchronize. For those, the body is emitted twice: unmasked when the whole workgroup lies inside the ndrange (a test that is the same for all of its work-items), and masked otherwise.

  • Backends: __fullgroup decides. Only the PoCL backend enables it; elsewhere it's false and the unmasked copy is removed when compiling. On CUDA and AMDGPU it gave nothing measurable for kernels without @synchronize (±3%), see the comment on Run full workgroups without the bounds mask on the CPU backend #849.
  • Why not kernels with @synchronize: duplicating each region, as Run full workgroups without the bounds mask on the CPU backend #849 did, defines variables carried across a barrier on two paths, and Julia then emits UndefVarError checks that LLVM doesn't remove. Those kernels keep the single masked body.
  • Closures: each copy of the body is a scope of its own (let). Otherwise a local captured by a closure is assigned twice and gets boxed, which turns into dynamic calls.

Measurements

y[i] = f(x[i]) over 2^18 Float64, 1 thread, Ryzen 9 5950X, ns per element. These numbers are from #849's branch, which does the same for kernels like these; see the correction on #860:

f main full workgroups unmasked unsafe_indices = true Julia map!
sin 6.96 1.66 1.67 4.10
exp 8.05 1.43 1.44 3.01
log 7.18 2.88 2.67 4.55
x^1.7 46.3 9.38 9.48 20.2
tan 11.7 2.91 2.59 7.53

On this branch, measured with the machine heavily loaded (load average ~20), masked kernels run at 0.8–1.1x the time of the unsafe_indices ones, where main's are 3.4–8x slower. A 7-point stencil over 128³ with the default workgroup size goes from 1655 to 633 µs.

Tests

  • New codegen checks (test/codegen_checks.jl):
    • a kernel without @synchronize gets two copies of the body with a dynamic ndrange, and one with a static ndrange where every workgroup is full;
    • a kernel with a top-level @synchronize keeps one body and no exception paths. This check fails if the restriction is removed;
    • a kernel with a closure capturing a local compiles without runtime calls. This check fails without the let scoping.
  • Adjusted checks:
  • Full test suite: passes locally on Julia 1.10 and 1.12.

Closes #845. Supersedes #849.

🤖 Generated with Claude Code

… CPU

`@kernel` runs the body under a per-work-item bounds mask, also in
workgroups that lie entirely inside the ndrange. On the CPU backend the
mask keeps PoCL from vectorizing the work-item loop, so math functions
(`sin`, `exp`, `^`, ...) end up as scalar libm calls instead of PoCL's
vector math library, and cheap bodies pay for masked loads and stores
(#845, #860).

For kernels without `@synchronize`, emit the body twice: unmasked when
the whole workgroup lies inside the ndrange (the same for all of its
work-items), and masked otherwise. `__fullgroup` decides; only the PoCL
backend enables it, elsewhere the unmasked copy is removed when
compiling. Each copy is a scope of its own, so that a local captured by a
closure isn't assigned twice, which would box it.

Kernels with `@synchronize` keep a single masked body: duplicating each
region would define variables carried across a barrier on two paths and
leave `UndefVarError` checks that LLVM can't remove (#849).

Assisted-by: Claude Code
@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Results

Show table
main 9b28985... main / 9b28985...
const/@Const/Float32/262144 0.542 ± 0.032 ms 0.311 ± 0.014 ms 1.74 ± 0.13
const/@Const/Float32/65536 0.172 ± 0.023 ms 0.161 ± 0.0047 ms 1.07 ± 0.15
const/@Const/Float64/262144 0.893 ± 0.015 ms 0.559 ± 0.021 ms 1.6 ± 0.065
const/@Const/Float64/65536 0.371 ± 0.016 ms 0.185 ± 0.012 ms 2.01 ± 0.15
const/unmarked/Float32/262144 2.25 ± 0.045 ms 2.23 ± 0.021 ms 1.01 ± 0.023
const/unmarked/Float32/65536 0.606 ± 0.022 ms 0.609 ± 0.031 ms 0.995 ± 0.062
const/unmarked/Float64/262144 3.11 ± 0.018 ms 3.12 ± 0.019 ms 0.998 ± 0.0082
const/unmarked/Float64/65536 0.807 ± 0.012 ms 0.817 ± 0.018 ms 0.988 ± 0.026
launch/3D static workgroup, dynamic ndrange 20.1 ± 0.51 μs 10.6 ± 0.35 μs 1.9 ± 0.079
launch/3D static workgroup, static ndrange 24.8 ± 1 μs 10.7 ± 13 μs 2.33 ± 2.9
launch/dynamic workgroup, dynamic ndrange 11.8 ± 0.23 μs 11.9 ± 0.21 μs 0.987 ± 0.026
launch/dynamic workgroup, dynamic ndrange, workgroupsize given 12.2 ± 0.64 μs 11.7 ± 0.31 μs 1.04 ± 0.061
launch/static workgroup, dynamic ndrange 12 ± 3.1 μs 11.6 ± 0.2 μs 1.04 ± 0.27
launch/static workgroup, static ndrange 10.7 ± 6.4 μs 10.5 ± 0.45 μs 1.02 ± 0.61
partition/dynamic workgroup, dynamic ndrange 0.0582 ± 0.0099 μs 0.0615 ± 0.0017 μs 0.947 ± 0.16
partition/static workgroup, dynamic ndrange 0.0549 ± 0.012 μs 0.0577 ± 0.012 μs 0.95 ± 0.28
partition/static workgroup, static ndrange 1.55 ± 0.01 ns 1.55 ± 0.01 ns 1 ± 0.0091
saxpy/default/Float16/1024 15.5 ± 0.77 μs 14.3 ± 1.1 μs 1.08 ± 0.097
saxpy/default/Float16/1048576 1.48 ± 0.021 ms 0.243 ± 0.031 ms 6.08 ± 0.78
saxpy/default/Float16/16384 0.056 ± 0.019 ms 18.8 ± 1 μs 2.99 ± 1
saxpy/default/Float16/2048 17.8 ± 1.4 μs 15 ± 1.6 μs 1.19 ± 0.16
saxpy/default/Float16/256 11.4 ± 0.23 μs 11.4 ± 12 μs 0.998 ± 1.1
saxpy/default/Float16/262144 0.416 ± 0.022 ms 0.105 ± 0.012 ms 3.98 ± 0.49
saxpy/default/Float16/32768 0.101 ± 0.02 ms 22.3 ± 5.3 μs 4.54 ± 1.4
saxpy/default/Float16/4096 20.7 ± 0.68 μs 16.2 ± 0.79 μs 1.28 ± 0.075
saxpy/default/Float16/512 11.4 ± 0.28 μs 10.7 ± 0.42 μs 1.06 ± 0.049
saxpy/default/Float16/64 11.2 ± 0.43 μs 11 ± 0.4 μs 1.02 ± 0.054
saxpy/default/Float16/65536 0.144 ± 0.019 ms 0.0379 ± 0.0088 ms 3.81 ± 1
saxpy/default/Float32/1024 16.1 ± 11 μs 14.2 ± 1.4 μs 1.13 ± 0.82
saxpy/default/Float32/1048576 0.797 ± 0.095 ms 0.335 ± 0.047 ms 2.38 ± 0.44
saxpy/default/Float32/16384 27.3 ± 11 μs 21.9 ± 7.2 μs 1.25 ± 0.64
saxpy/default/Float32/2048 16.7 ± 1.1 μs 25.1 ± 1.4 μs 0.666 ± 0.059
saxpy/default/Float32/256 11.4 ± 0.63 μs 10.9 ± 2.9 μs 1.05 ± 0.29
saxpy/default/Float32/262144 0.22 ± 0.034 ms 0.117 ± 0.014 ms 1.88 ± 0.37
saxpy/default/Float32/32768 0.0399 ± 0.021 ms 26.5 ± 7.3 μs 1.5 ± 0.88
saxpy/default/Float32/4096 18.1 ± 0.69 μs 17.7 ± 9.1 μs 1.02 ± 0.53
saxpy/default/Float32/512 11.3 ± 15 μs 10.2 ± 4.8 μs 1.1 ± 1.6
saxpy/default/Float32/64 16.5 ± 0.47 μs 23.5 ± 8.2 μs 0.704 ± 0.25
saxpy/default/Float32/65536 0.0978 ± 0.053 ms 0.0332 ± 0.004 ms 2.94 ± 1.6
saxpy/default/Float64/1024 25.9 ± 9.5 μs 16 ± 1.1 μs 1.62 ± 0.6
saxpy/default/Float64/1048576 1.37 ± 0.22 ms 0.816 ± 0.1 ms 1.68 ± 0.34
saxpy/default/Float64/16384 30.8 ± 3.3 μs 26.6 ± 2.6 μs 1.16 ± 0.17
saxpy/default/Float64/2048 17.7 ± 8.7 μs 17 ± 7.6 μs 1.04 ± 0.69
saxpy/default/Float64/256 14.3 ± 5.5 μs 24.6 ± 11 μs 0.583 ± 0.34
saxpy/default/Float64/262144 0.291 ± 0.045 ms 0.2 ± 0.029 ms 1.46 ± 0.31
saxpy/default/Float64/32768 0.0452 ± 0.0079 ms 0.0365 ± 0.0062 ms 1.24 ± 0.3
saxpy/default/Float64/4096 19.3 ± 8.5 μs 25 ± 6.6 μs 0.771 ± 0.39
saxpy/default/Float64/512 16 ± 5.9 μs 25 ± 1.2 μs 0.639 ± 0.24
saxpy/default/Float64/64 11.5 ± 0.57 μs 11.4 ± 5.3 μs 1.01 ± 0.48
saxpy/default/Float64/65536 0.113 ± 0.021 ms 0.0563 ± 0.0068 ms 2.01 ± 0.45
saxpy/static workgroup=(1024,)/Float16/1024 15.2 ± 4.4 μs 14.3 ± 2.8 μs 1.07 ± 0.37
saxpy/static workgroup=(1024,)/Float16/1048576 1.48 ± 0.024 ms 0.246 ± 0.024 ms 6 ± 0.58
saxpy/static workgroup=(1024,)/Float16/16384 0.0381 ± 0.0019 ms 26.6 ± 1.2 μs 1.43 ± 0.096
saxpy/static workgroup=(1024,)/Float16/2048 17.4 ± 1.8 μs 15.1 ± 2 μs 1.15 ± 0.19
saxpy/static workgroup=(1024,)/Float16/256 11.7 ± 0.56 μs 11.6 ± 0.61 μs 1.01 ± 0.072
saxpy/static workgroup=(1024,)/Float16/262144 0.415 ± 0.022 ms 0.107 ± 0.012 ms 3.86 ± 0.49
saxpy/static workgroup=(1024,)/Float16/32768 0.0961 ± 0.015 ms 31.3 ± 1.6 μs 3.07 ± 0.51
saxpy/static workgroup=(1024,)/Float16/4096 20.8 ± 0.67 μs 24.5 ± 9 μs 0.849 ± 0.31
saxpy/static workgroup=(1024,)/Float16/512 11.6 ± 0.38 μs 24.5 ± 1.1 μs 0.473 ± 0.026
saxpy/static workgroup=(1024,)/Float16/64 12 ± 0.51 μs 16.5 ± 0.62 μs 0.723 ± 0.041
saxpy/static workgroup=(1024,)/Float16/65536 0.148 ± 0.019 ms 0.0384 ± 0.0098 ms 3.85 ± 1.1
saxpy/static workgroup=(1024,)/Float32/1024 16 ± 5.8 μs 15.5 ± 1.7 μs 1.03 ± 0.39
saxpy/static workgroup=(1024,)/Float32/1048576 0.845 ± 0.082 ms 0.326 ± 0.052 ms 2.59 ± 0.49
saxpy/static workgroup=(1024,)/Float32/16384 27.2 ± 2.3 μs 22.5 ± 7 μs 1.21 ± 0.39
saxpy/static workgroup=(1024,)/Float32/2048 16.4 ± 1.4 μs 16.3 ± 3.3 μs 1 ± 0.22
saxpy/static workgroup=(1024,)/Float32/256 24.8 ± 7.8 μs 16.5 ± 13 μs 1.5 ± 1.3
saxpy/static workgroup=(1024,)/Float32/262144 0.233 ± 0.031 ms 0.118 ± 0.013 ms 1.97 ± 0.35
saxpy/static workgroup=(1024,)/Float32/32768 0.0588 ± 0.022 ms 25.2 ± 8.2 μs 2.33 ± 1.1
saxpy/static workgroup=(1024,)/Float32/4096 29.4 ± 0.89 μs 16.5 ± 9.3 μs 1.78 ± 1
saxpy/static workgroup=(1024,)/Float32/512 10.5 ± 4.3 μs 25.7 ± 0.95 μs 0.409 ± 0.17
saxpy/static workgroup=(1024,)/Float32/64 11.8 ± 0.21 μs 17.3 ± 7.9 μs 0.681 ± 0.31
saxpy/static workgroup=(1024,)/Float32/65536 0.101 ± 0.045 ms 0.0348 ± 0.0068 ms 2.91 ± 1.4
saxpy/static workgroup=(1024,)/Float64/1024 16 ± 0.58 μs 15.9 ± 0.64 μs 1.01 ± 0.055
saxpy/static workgroup=(1024,)/Float64/1048576 1.43 ± 0.19 ms 0.76 ± 0.11 ms 1.88 ± 0.36
saxpy/static workgroup=(1024,)/Float64/16384 31.5 ± 3.1 μs 0.0324 ± 0.009 ms 0.974 ± 0.29
saxpy/static workgroup=(1024,)/Float64/2048 17 ± 0.61 μs 16.8 ± 0.71 μs 1.01 ± 0.056
saxpy/static workgroup=(1024,)/Float64/256 11.1 ± 4.8 μs 10.9 ± 4.6 μs 1.01 ± 0.61
saxpy/static workgroup=(1024,)/Float64/262144 0.291 ± 0.037 ms 0.187 ± 0.023 ms 1.56 ± 0.28
saxpy/static workgroup=(1024,)/Float64/32768 0.0465 ± 0.0058 ms 0.039 ± 0.0091 ms 1.19 ± 0.32
saxpy/static workgroup=(1024,)/Float64/4096 19.8 ± 9.7 μs 18.1 ± 0.96 μs 1.09 ± 0.54
saxpy/static workgroup=(1024,)/Float64/512 14.9 ± 1.8 μs 15 ± 1.5 μs 0.992 ± 0.15
saxpy/static workgroup=(1024,)/Float64/64 11.7 ± 0.24 μs 11.7 ± 0.85 μs 0.998 ± 0.076
saxpy/static workgroup=(1024,)/Float64/65536 0.122 ± 0.022 ms 0.0526 ± 0.0067 ms 2.31 ± 0.52
time_to_load 0.539 ± 0.0097 s 0.564 ± 0.015 s 0.956 ± 0.03
main 9b28985... main / 9b28985...
const/@Const/Float32/262144 9 allocs: 0.203 kB 9 allocs: 0.203 kB 1
const/@Const/Float32/65536 9 allocs: 0.203 kB 9 allocs: 0.203 kB 1
const/@Const/Float64/262144 9 allocs: 0.203 kB 9 allocs: 0.203 kB 1
const/@Const/Float64/65536 9 allocs: 0.203 kB 9 allocs: 0.203 kB 1
const/unmarked/Float32/262144 9 allocs: 0.203 kB 9 allocs: 0.203 kB 1
const/unmarked/Float32/65536 9 allocs: 0.203 kB 9 allocs: 0.203 kB 1
const/unmarked/Float64/262144 9 allocs: 0.203 kB 9 allocs: 0.203 kB 1
const/unmarked/Float64/65536 9 allocs: 0.203 kB 9 allocs: 0.203 kB 1
launch/3D static workgroup, dynamic ndrange 9 allocs: 0.219 kB 9 allocs: 0.219 kB 1
launch/3D static workgroup, static ndrange 9 allocs: 0.219 kB 9 allocs: 0.219 kB 1
launch/dynamic workgroup, dynamic ndrange 5 allocs: 0.0938 kB 5 allocs: 0.0938 kB 1
launch/dynamic workgroup, dynamic ndrange, workgroupsize given 5 allocs: 0.0938 kB 5 allocs: 0.0938 kB 1
launch/static workgroup, dynamic ndrange 5 allocs: 0.0938 kB 5 allocs: 0.0938 kB 1
launch/static workgroup, static ndrange 5 allocs: 0.0938 kB 5 allocs: 0.0938 kB 1
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 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/default/Float16/1048576 12 allocs: 0.25 kB 12 allocs: 0.25 kB 1
saxpy/default/Float16/16384 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/default/Float16/2048 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/default/Float16/256 5 allocs: 0.0938 kB 5 allocs: 0.0938 kB 1
saxpy/default/Float16/262144 12 allocs: 0.25 kB 8 allocs: 0.141 kB 1.78
saxpy/default/Float16/32768 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/default/Float16/4096 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/default/Float16/512 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/default/Float16/64 5 allocs: 0.0938 kB 5 allocs: 0.0938 kB 1
saxpy/default/Float16/65536 12 allocs: 0.25 kB 8 allocs: 0.141 kB 1.78
saxpy/default/Float32/1024 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/default/Float32/1048576 12 allocs: 0.25 kB 12 allocs: 0.25 kB 1
saxpy/default/Float32/16384 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/default/Float32/2048 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/default/Float32/256 5 allocs: 0.0938 kB 5 allocs: 0.0938 kB 1
saxpy/default/Float32/262144 12 allocs: 0.25 kB 8 allocs: 0.141 kB 1.78
saxpy/default/Float32/32768 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/default/Float32/4096 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/default/Float32/512 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/default/Float32/64 5 allocs: 0.0938 kB 5 allocs: 0.0938 kB 1
saxpy/default/Float32/65536 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/default/Float64/1024 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/default/Float64/1048576 12 allocs: 0.25 kB 12 allocs: 0.25 kB 1
saxpy/default/Float64/16384 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/default/Float64/2048 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/default/Float64/256 5 allocs: 0.0938 kB 5 allocs: 0.0938 kB 1
saxpy/default/Float64/262144 12 allocs: 0.25 kB 12 allocs: 0.25 kB 1
saxpy/default/Float64/32768 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/default/Float64/4096 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/default/Float64/512 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/default/Float64/64 5 allocs: 0.0938 kB 5 allocs: 0.0938 kB 1
saxpy/default/Float64/65536 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/static workgroup=(1024,)/Float16/1024 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/static workgroup=(1024,)/Float16/1048576 12 allocs: 0.25 kB 12 allocs: 0.25 kB 1
saxpy/static workgroup=(1024,)/Float16/16384 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/static workgroup=(1024,)/Float16/2048 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/static workgroup=(1024,)/Float16/256 5 allocs: 0.0938 kB 5 allocs: 0.0938 kB 1
saxpy/static workgroup=(1024,)/Float16/262144 12 allocs: 0.25 kB 8 allocs: 0.141 kB 1.78
saxpy/static workgroup=(1024,)/Float16/32768 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/static workgroup=(1024,)/Float16/4096 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/static workgroup=(1024,)/Float16/512 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/static workgroup=(1024,)/Float16/64 5 allocs: 0.0938 kB 5 allocs: 0.0938 kB 1
saxpy/static workgroup=(1024,)/Float16/65536 12 allocs: 0.25 kB 8 allocs: 0.141 kB 1.78
saxpy/static workgroup=(1024,)/Float32/1024 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/static workgroup=(1024,)/Float32/1048576 12 allocs: 0.25 kB 12 allocs: 0.25 kB 1
saxpy/static workgroup=(1024,)/Float32/16384 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/static workgroup=(1024,)/Float32/2048 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/static workgroup=(1024,)/Float32/256 5 allocs: 0.0938 kB 5 allocs: 0.0938 kB 1
saxpy/static workgroup=(1024,)/Float32/262144 12 allocs: 0.25 kB 8 allocs: 0.141 kB 1.78
saxpy/static workgroup=(1024,)/Float32/32768 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/static workgroup=(1024,)/Float32/4096 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/static workgroup=(1024,)/Float32/512 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/static workgroup=(1024,)/Float32/64 5 allocs: 0.0938 kB 5 allocs: 0.0938 kB 1
saxpy/static workgroup=(1024,)/Float32/65536 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/static workgroup=(1024,)/Float64/1024 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/static workgroup=(1024,)/Float64/1048576 12 allocs: 0.25 kB 12 allocs: 0.25 kB 1
saxpy/static workgroup=(1024,)/Float64/16384 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/static workgroup=(1024,)/Float64/2048 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/static workgroup=(1024,)/Float64/256 5 allocs: 0.0938 kB 5 allocs: 0.0938 kB 1
saxpy/static workgroup=(1024,)/Float64/262144 12 allocs: 0.25 kB 12 allocs: 0.25 kB 1
saxpy/static workgroup=(1024,)/Float64/32768 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/static workgroup=(1024,)/Float64/4096 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/static workgroup=(1024,)/Float64/512 8 allocs: 0.141 kB 8 allocs: 0.141 kB 1
saxpy/static workgroup=(1024,)/Float64/64 5 allocs: 0.0938 kB 5 allocs: 0.0938 kB 1
saxpy/static workgroup=(1024,)/Float64/65536 8 allocs: 0.141 kB 8 allocs: 0.141 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).

@vchuravy

vchuravy commented Oct 10, 2026 •

Copy link
Copy Markdown
Member Author

Running full workgroups unmasked makes some barrier-free kernels much slower on PoCL. Trixi.jl's flux differencing "full sweep" kernel (trixi-framework/Trixi.jl#3329) goes from 9.0 ms to 207–229 ms on 1 thread, and a minimal kernel of the same shape from 2.2 ms to 71 ms.

Cause: PoCL's ImplicitLoopBarriers pass ("horizontal inner-loop parallelization") puts implicit barriers around an innermost loop if all work-items execute it the same number of times. The loop must have a single exit, and its entry and exit condition must be uniform. With the mask, the loop sits inside the per-work-item bounds check, so the pass doesn't apply. In the unmasked copy it does. The work-items then run the loop in lockstep, and every value live across the loop needs a per-work-item context array. In the minimal kernel below that is 9 memsets of the context per workgroup; in Trixi's kernel, 14 memsets of 32 KiB each per workgroup (4096 work-items) and a ~1.5 MB stack frame.

POCL_FORCE_PARALLEL_OUTER_LOOP=0 restores the performance. It disables the pass exactly for kernels without barriers (!hasWorkgroupBarriers), which are the kernels this PR unmasks. It doesn't change anything on main.

using KernelAbstractions, BenchmarkTools, Random
# One work-item per node of an N×N element; each work-item loops over its partners in a
# rolled loop (no `@synchronize`), like a flux differencing "full sweep" kernel.
@inline function twopoint(ul::NTuple{4, T}, ur::NTuple{4, T}) where {T}
    vl = ul[2] / ul[1]; vr = ur[2] / ur[1]
    p = T(0.5) * (ul[4] / ul[1] + ur[4] / ur[1])
    v = T(0.5) * (vl + vr)
    return (v * (ul[1] + ur[1]), v * (ul[2] + ur[2]) + p, v * (ul[3] + ur[3]), v * (ul[4] + ur[4] + p))
end

# Keep the partner loop rolled, as Julia does for Trixi's larger two-point fluxes
macro rolled(ex)
    push!(ex.args[2].args, Expr(:loopinfo, (Symbol("llvm.loop.unroll.disable"),)))
    return esc(ex)
end

@kernel inbounds=true function fullsweep!(du, @Const(u), @Const(D), ::Val{N}) where {N}
    i, j, e = @index(Global, NTuple)
    ui = ntuple(v -> @inbounds(u[v, i, j, e]), Val(4))
    acc = ntuple(_ -> zero(eltype(du)), Val(4))
    @rolled for ii in 1:N
        if ii != i
            f = twopoint(ui, ntuple(v -> @inbounds(u[v, ii, j, e]), Val(4)))
            acc = acc .+ D[i, ii] .* f
        end
    end
    for v in 1:4
        du[v, i, j, e] = acc[v]
    end
end

const N = 4
nelem = 128^2
Random.seed!(1)
u = 1 .+ rand(4, N, N, nelem)
D = rand(N, N)
du = similar(u)
backend = CPU()
kernel! = fullsweep!(backend)  # default workgroup size, as in Trixi
f!() = (kernel!(du, u, D, Val(N); ndrange = (N, N, nelem)); KernelAbstractions.synchronize(backend))
f!()
t = @belapsed f!() evals = 1 seconds = 5
println("KA ", pkgdir(KernelAbstractions), " POCL_FORCE_PARALLEL_OUTER_LOOP=", get(ENV, "POCL_FORCE_PARALLEL_OUTER_LOOP", "unset"),
        ": ", round(t * 1e3; digits = 2), " ms, sum(du) = ", sum(du))

KA 1c2be35 (main) vs. 9b28985 (this PR), Julia 1.12.7, JULIA_KA_CPU_THREADS=1, Ryzen 9 5950X, a separate POCL_CACHE_DIR per row:

POCL_FORCE_PARALLEL_OUTER_LOOP time memset calls in the kernel
main unset 2.16 ms 0
main 0 2.11 ms 0
this PR unset 70.7 ms 9
this PR 0 2.05 ms 0

du is identical in all four runs. @rolled only stands in for a loop body large enough that Julia doesn't unroll the loop itself, as in Trixi's kernel. With @unroll, there is no loop for the pass to act on. The closures need their own @inbounds: otherwise, the bounds checks give the loop several exits and the pass doesn't apply either.

Suggestions:

  • Set POCL_FORCE_PARALLEL_OUTER_LOOP=0 for the PoCL backend, e.g., in ENV before PoCL is initialized, unless the user set it.
  • Note that PoCL's kernel cache key does not include this option (nor LLVM options set through POCL_DEBUG_LLVM_OPTS). Binaries compiled with a different setting are reused from the cache, so the default cache directory would need to depend on the setting.

For the Trixi kernels themselves, this PR gave no speedup on PoCL. They have small work-item loops (4 nodes per direction), which LLVM's loop vectorizer doesn't vectorize below its tiny-trip-count threshold of 16 unless vectorizer-min-trip-count=1 is set (see #868), and their partner loops have branches. A symmetric "line" kernel with one work-item per grid line does vectorize once it is unmasked (unsafe_indices=true) and gets that option: 4.5 → 2.05 ms for the volume integral. It uses @synchronize, though, so this PR doesn't unmask it.

(Investigated with the assistance of Claude Code.)

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.

CPU backend: the bounds mask makes kernel bodies up to 2.3x slower, even when the workgroups tile the ndrange

1 participant