Repository navigation
CPU backend: math functions are scalar libm calls per work-item; PoCL never uses its vector math library #860
Description
On the PoCL-based CPU backend, sin, cos, exp, log, pow, tan, … in a kernel become one scalar libm call per work-item. They are 1.5–2.5x slower than the same loop in plain Julia, and keep PoCL from vectorizing the work-item loop. Ideally these functions would use a vectorized math library, which PoCL is built with but doesn't use for kernels.
What happens
- Julia side: SPIRVIntrinsics replaces
Base.sin(::Float64)etc. with the OpenCL builtins (@builtin_ccall("sin", …)→_Z3sind), so Julia's own implementation (and itsDomainErrorcheck) isn't used. - PoCL's kernel library:
pocl_standalone_jllis built withENABLE_HOST_CPU_VECTORIZE_LIBMVEC=ONagainst SLEEF'slibsleefgnuabi(YggdrasilP/pocl/common.jl). In that configuration PoCL's builtins forsin,cos,exp,exp2,log,pow,sqrt,tanare thin wrappers aroundllvm.sin.f64and friends, so that "the vectorizer can use vector variants". - But the vectorizer never gets them. In PoCL 7.2 (
lib/CL/pocl_llvm_wg.cc) the vector library is registered only ininitPassManagerForCodeGen, i.e. for code generation after the work-group passes and the loop vectorizer have run. The equivalentTargetLibraryInfosetup in the work-group pipeline (PoCLModulePassManager::build) is under#if 0, and that's still the case on PoCL main.- So the loop vectorizer sees
llvm.sin.f64without a vector variant, reports that vectorization is not beneficial, and the kernel ends up with a scalar call to glibc'ssinper work-item (nmon the kernel object:U sin,U exp,U log). POCL_DEBUG_LLVM_OPTS=-vector-library=LIBMVECis ignored ("unknown LLVM option"), so there's no runtime workaround.
- So the loop vectorizer sees
- Not all functions take that path:
sincos,sinpi,expm1,log1paren't in that list and use libclc/SLEEF implementations instead.sinpigets inlined and is faster than Julia's.
Cost
ns per element, one work-item per element vs. a plain Julia loop over a Vector{Float64}. KA main d048363, pocl_standalone_jll 7.2.2, Julia 1.12, 1 thread, Ryzen 9 5950X (AVX2), pinned; the machine was shared, so ±10%:
| function | KA kernel | Julia loop |
|---|---|---|
| copy | 0.38 | 0.13 |
sin |
6.55 | 4.00 |
cos |
6.31 | 4.03 |
sincos |
7.36 | 5.79 |
tan |
9.74 | 7.16 |
exp |
7.14 | 2.80 |
log |
5.99 | 4.24 |
^ (Float64) |
42.7 | 18.5 |
log1p |
14.4 | 5.6 |
expm1 |
7.7 | 3.5 |
sinpi |
2.54 | 4.64 |
@fastmath exp in the kernel takes 2.19 ns, because it uses Julia's exp_impl_fast instead of the builtin.
In Trixi.jl the source terms of elixir_euler_source_terms.jl (sin/cos per node) cost noticeably more on the KA path than on its Polyester path, as part of the gap analysed in #859's investigation.
Options
- Fix it in PoCL (preferred): register the vector library (
createTLII(…, LIBMVEC/SLEEF)) in the work-group pass pipeline as well, so the loop vectorizer can call_ZGVdN4v_sinetc. from SLEEF across work-items. This needs a PoCL patch (upstream, plus a Yggdrasil backport like the other PoCL patches) and should then work for every kernel without changes in Julia. It still only helps where PoCL vectorizes the work-item loop at all, which it currently doesn't for kernels with inner loops or non-inlined calls. - Use Julia's implementations on the CPU: override
sin/cos/exp/… in the PoCL method table with Base's implementations, which inline and give the vectorizer plain arithmetic. I have a prototype that does this. Float64 results are bitwise identical to Base in 99.98–100% of cases, and PoCL's final kernel then contains no calls.- It needs a 64-bit-limb
paynehanek, since the SPIR-V backend rejectsUInt128. - It needs workarounds for OpenCL's ban on recursion, e.g. in
^. - Float32
exploses accuracy (1.12 vs 0.84 ulp), because Base relies on FMAs andcontractdoesn't survive SPIR-V; that's a separate issue.
- It needs a 64-bit-limb
- Bind SLEEF's vector functions directly (as GPU back-ends do with their vendor libraries). That's harder to use from per-work-item code.
With (1) the math stays in PoCL and vectorizes where the loop does. (2) helps kernels that don't vectorize, and fixes the loss of @fastmath. The two aren't exclusive.
Activity
vchuravy commentedon Oct 10, 2026
Correction: the diagnosis above is wrong. PoCL does use its vector math library. What stops these kernels from vectorizing is KA's per-work-item bounds mask (#845), not PoCL.
I built
pocl_standalone_jll7.2.2 locally with the vector library registered in the work-group pipeline (the "fix" proposed as option 1). It changes nothing: the kernel is still not vectorized and takes the same time. But the released PoCL (7.2.2+4) already vectorizes the same function once the kernel has no bounds mask:ns per element,
y[i] = f(x[i])over 2^18Float64, 1 thread, Ryzen 9 5950X, KA main d048363:f@kernel(masked)unsafe_indices = true(no mask)map!sincosexplogx^1.7tansqrtPOCL_VECTORIZER_REMARKS=1, the unmasked kernels report "vectorized loop (vectorization width: 4)", and their results differ from Base by about 1 ulp, i.e. SLEEF's vector functions are being called. The masked ones report "the cost-model indicates that vectorization is not beneficial".llvm.sin.f64tollvm.sin.v4f64, and PoCL's code generation, which has the vector library registered (initPassManagerForCodeGen), maps that to_ZGVdN4v_sin. The#if 0block in the work-group pipeline doesn't matter.So the fix is on KA's side: run work-groups that lie entirely inside the ndrange without the mask. The closed #849 does that, and gives 3.5–5.6x for math-heavy element-wise kernels, faster than a plain Julia loop. #849 had a problem with kernels containing
@synchronize(variables carried across a barrier getUndefVarErrorchecks), so a version limited to kernels without@synchronizewould be the way to go. Options 2 and 3 above are only relevant for kernels that can't be vectorized for other reasons.