Note: issue drafted by the bot, but posted by me after verifying that the reproducer is accurate.
On the POCL backend, converting a signed integer that LLVM can prove is 0 or -1 to a float yields 1.0 instead of -1.0. LLVM canonicalises such conversions to sitofp i1 %c to double, where a true i1 must convert to -1.0, but the SPIR-V we emit converts it as if it were unsigned. The result is silently wrong: in Oceananigans.jl this turned an immersed boundary bottom_height = (x, y) -> ifelse(x < 2, 0, -1) into +1, so the whole grid was considered immersed.
Reproducer:
using KernelAbstractions
@kernel function k!(a, x, f)
i = @index(Global)
@inbounds a[i] = f(x[i])
end
x = [1.0, 3.0]
for (name, f, T) in [("ifelse(x < 2, 0, -1) → Float64", x -> ifelse(x < 2, 0, -1), Float64),
("ifelse(x < 2, 0, -1) → Float32", x -> ifelse(x < 2, 0, -1), Float32),
("x < 2 ? 0 : -1 → Float64", x -> x < 2 ? 0 : -1, Float64),
("-Int(x ≥ 2) → Float64", x -> -Int(x ≥ 2), Float64),
("ifelse(x < 2, 0.0, -1.0)", x -> ifelse(x < 2, 0.0, -1.0), Float64)]
a = zeros(T, 2)
k!(CPU())(a, x, f; ndrange=2); synchronize(CPU())
println(rpad(name, 34), a, " expected ", T[f(xi) for xi in x])
end
With KernelAbstractions at e444fb9, GPUCompiler v2.10.0, SPIRV_LLVM_Backend_jll v23.1.1+2, pocl_standalone_jll v7.2.1+0 and Julia v1.13.1:
ifelse(x < 2, 0, -1) → Float64 [0.0, 1.0] expected [0.0, -1.0]
ifelse(x < 2, 0, -1) → Float32 Float32[0.0, 1.0] expected Float32[0.0, -1.0]
x < 2 ? 0 : -1 → Float64 [0.0, 1.0] expected [0.0, -1.0]
-Int(x ≥ 2) → Float64 [0.0, 1.0] expected [0.0, -1.0]
ifelse(x < 2, 0.0, -1.0) [0.0, -1.0] expected [0.0, -1.0]
Storing the constant -1 into a Float64 array, or converting the elements of an Int array with Float64.(n), works.
Generated code
For f(c::Bool) = Float64(ifelse(c, 0, -1)), the optimised LLVM IR is correct:
define double @julia_f_169(i8 zeroext %"c::Bool") {
...
%1 = sitofp i1 %ifelse_cond to double
ret double %1
}
but the SPIR-V selects 1 rather than all ones before the signed conversion:
%8 = OpConstantNull %ulong
%ulong_1 = OpConstant %ulong 1
%17 = OpLogicalNotEqual %bool %16 %true
%18 = OpSelect %ulong %17 %ulong_1 %8
%19 = OpConvertSToF %double %18
OpReturnValue %19
(obtained with GPUCompiler.compile(:asm, job) for a job built from KernelAbstractions.POCL.compiler_config(KernelAbstractions.POCL.device(); kernel=false)).
So the bug is in the SPIR-V backend rather than in POCL. On LLVM main, SPIRVInstructionSelector::selectIToF handles a bool source through selectBoolToInt(..., IsSigned), which uses all ones for signed conversions, so either the backend shipped in SPIRV_LLVM_Backend_jll v23.1.1 predates a fix or the i1 is widened somewhere else (e.g. during legalisation) with a zero extension. I couldn't check with llc directly because the JLL only ships libspirv. If it's an LLVM bug that is still present upstream, a reduced test case for llc -mtriple=spirv64-unknown-unknown would be
define spir_func double @f(i1 %c) {
%r = sitofp i1 %c to double
ret double %r
}
which should select -1 (or produce -1.0 directly) for %c = true.
Note: issue drafted by the bot, but posted by me after verifying that the reproducer is accurate.
On the POCL backend, converting a signed integer that LLVM can prove is
0or-1to a float yields1.0instead of-1.0. LLVM canonicalises such conversions tositofp i1 %c to double, where a truei1must convert to-1.0, but the SPIR-V we emit converts it as if it were unsigned. The result is silently wrong: in Oceananigans.jl this turned an immersed boundarybottom_height = (x, y) -> ifelse(x < 2, 0, -1)into+1, so the whole grid was considered immersed.Reproducer:
With KernelAbstractions at e444fb9, GPUCompiler v2.10.0, SPIRV_LLVM_Backend_jll v23.1.1+2, pocl_standalone_jll v7.2.1+0 and Julia v1.13.1:
Storing the constant
-1into aFloat64array, or converting the elements of anIntarray withFloat64.(n), works.Generated code
For
f(c::Bool) = Float64(ifelse(c, 0, -1)), the optimised LLVM IR is correct:but the SPIR-V selects
1rather than all ones before the signed conversion:(obtained with
GPUCompiler.compile(:asm, job)for a job built fromKernelAbstractions.POCL.compiler_config(KernelAbstractions.POCL.device(); kernel=false)).So the bug is in the SPIR-V backend rather than in POCL. On LLVM main,
SPIRVInstructionSelector::selectIToFhandles a bool source throughselectBoolToInt(..., IsSigned), which uses all ones for signed conversions, so either the backend shipped in SPIRV_LLVM_Backend_jll v23.1.1 predates a fix or thei1is widened somewhere else (e.g. during legalisation) with a zero extension. I couldn't check withllcdirectly because the JLL only shipslibspirv. If it's an LLVM bug that is still present upstream, a reduced test case forllc -mtriple=spirv64-unknown-unknownwould bewhich should select
-1(or produce-1.0directly) for%c = true.