Skip to content

CPU backend: math functions are scalar libm calls per work-item; PoCL never uses its vector math library #860

Open

Description

@vchuravy

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

  1. Julia side: SPIRVIntrinsics replaces Base.sin(::Float64) etc. with the OpenCL builtins (@builtin_ccall("sin", …) → _Z3sind), so Julia's own implementation (and its DomainError check) isn't used.
  2. PoCL's kernel library: pocl_standalone_jll is built with ENABLE_HOST_CPU_VECTORIZE_LIBMVEC=ON against SLEEF's libsleefgnuabi (Yggdrasil P/pocl/common.jl). In that configuration PoCL's builtins for sin, cos, exp, exp2, log, pow, sqrt, tan are thin wrappers around llvm.sin.f64 and friends, so that "the vectorizer can use vector variants".
  3. But the vectorizer never gets them. In PoCL 7.2 (lib/CL/pocl_llvm_wg.cc) the vector library is registered only in initPassManagerForCodeGen, i.e. for code generation after the work-group passes and the loop vectorizer have run. The equivalent TargetLibraryInfo setup 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.f64 without a vector variant, reports that vectorization is not beneficial, and the kernel ends up with a scalar call to glibc's sin per work-item (nm on the kernel object: U sin, U exp, U log).
    • POCL_DEBUG_LLVM_OPTS=-vector-library=LIBMVEC is ignored ("unknown LLVM option"), so there's no runtime workaround.
  4. Not all functions take that path: sincos, sinpi, expm1, log1p aren't in that list and use libclc/SLEEF implementations instead. sinpi gets 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

  1. 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_sin etc. 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.
  2. 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 rejects UInt128.
    • It needs workarounds for OpenCL's ban on recursion, e.g. in ^.
    • Float32 exp loses accuracy (1.12 vs 0.84 ulp), because Base relies on FMAs and contract doesn't survive SPIR-V; that's a separate issue.
  3. 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

  1. vchuravy commented on Oct 10, 2026

    @vchuravy
    MemberAuthor

    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_jll 7.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^18 Float64, 1 thread, Ryzen 9 5950X, KA main d048363:

    f normal @kernel (masked) unsafe_indices = true (no mask) KA #849 branch (full work-groups unmasked) Julia map!
    sin 6.96 1.67 1.66 4.10
    cos 7.22 1.99 2.11 4.10
    exp 8.05 1.44 1.43 3.01
    log 7.18 2.67 2.88 4.55
    x^1.7 46.3 9.48 9.38 20.2
    tan 11.7 2.59 2.91 7.53
    sqrt 2.10 0.60 0.60 2.04
    • Unmasked kernels vectorize. With POCL_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".
    • Likely mechanism (inferred): the loop vectorizer widens llvm.sin.f64 to llvm.sin.v4f64, and PoCL's code generation, which has the vector library registered (initPassManagerForCodeGen), maps that to _ZGVdN4v_sin. The #if 0 block 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 get UndefVarError checks), so a version limited to kernels without @synchronize would be the way to go. Options 2 and 3 above are only relevant for kernels that can't be vectorized for other reasons.

  2. added this to the 0.10.1 milestone on Oct 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions