Skip to content

Commit 17fc9b0

Browse files
maleadtclaude
andauthored
Experimental support for targets without SPIR-V capabilities (#319)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent fc97028 commit 17fc9b0

7 files changed

Lines changed: 298 additions & 66 deletions

File tree

.github/workflows/Test.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,14 @@ jobs:
148148
Pkg.activate(".")
149149
Pkg.test(; test_args=`--quickfail --verbose --platform=pocl`)
150150
151+
- name: Upload compilation dumps
152+
if: always()
153+
uses: actions/upload-artifact@v4
154+
with:
155+
name: compilation-dumps-${{ matrix.version }}-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.memory_backend }}-pocl-${{ matrix.pocl }}
156+
path: ${{ runner.temp }}/opencl-compilation-dumps/
157+
if-no-files-found: ignore
158+
151159
- uses: julia-actions/julia-processcoverage@v1
152160
- uses: codecov/codecov-action@v7
153161
with:

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ SPIRVIntrinsics = "71d1d633-e7e8-4a92-83a1-de8814b09ba8"
2323
SPIRV_LLVM_Backend_jll = "4376b9bf-cff8-51b6-bb48-39421dff0d0c"
2424
SPIRV_Tools_jll = "6ac6d60f-d740-5983-97d7-a4482c0689f4"
2525
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
26+
spirv2clc_jll = "f0274c0c-8c8a-59f1-85b7-f7d60330c5fb"
2627

2728
[sources]
2829
SPIRVIntrinsics = {path = "lib/intrinsics"}
@@ -46,3 +47,4 @@ SPIRV_LLVM_Backend_jll = "22"
4647
SPIRV_Tools_jll = "2025.1"
4748
StaticArrays = "1"
4849
julia = "1.10"
50+
spirv2clc_jll = "0.2.0"

src/OpenCL.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ module OpenCL
22

33
using GPUCompiler
44
using LLVM, LLVM.Interop
5-
using SPIRV_LLVM_Backend_jll, SPIRV_Tools_jll
5+
using SPIRV_LLVM_Backend_jll, SPIRV_Tools_jll, spirv2clc_jll
66
using Adapt
77
using Reexport
88
using GPUArrays

src/compiler/capabilities.jl

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,49 @@ end
4040

4141
has_opencl_c_feature(dev, feat) = feat in opencl_c_features(dev)
4242

43+
# OpenCL 3.0: CL_DEVICE_OPENCL_C_ALL_VERSIONS lists every OpenCL C version the device accepts as
44+
# an array of cl_name_version {cl_version (4 bytes); char name[64]}. This is the query to trust:
45+
# the legacy CL_DEVICE_OPENCL_C_VERSION string reports "1.2" on both NVIDIA and pocl even though
46+
# pocl accepts up to 3.0. Returns the highest version, falling back to the legacy string (or 1.2)
47+
# on devices that don't support the 3.0 query.
48+
function max_opencl_c_version(dev::cl.Device)::VersionNumber
49+
CL_DEVICE_OPENCL_C_ALL_VERSIONS = 0x1066
50+
try
51+
sz = Ref{Csize_t}(0)
52+
cl.clGetDeviceInfo(dev, CL_DEVICE_OPENCL_C_ALL_VERSIONS, 0, C_NULL, sz)
53+
if sz[] != 0
54+
buf = Vector{UInt8}(undef, sz[])
55+
cl.clGetDeviceInfo(dev, CL_DEVICE_OPENCL_C_ALL_VERSIONS, sz[], buf, C_NULL)
56+
entry = 68 # sizeof(cl_name_version)
57+
best = v"0"
58+
for off in 0:entry:(length(buf) - entry)
59+
ver = reinterpret(UInt32, buf[(off + 1):(off + 4)])[1] # cl_version bitfield
60+
vn = VersionNumber(ver >> 22, (ver >> 12) & 0x3ff) # major[31:22], minor[21:12]
61+
vn > best && (best = vn)
62+
end
63+
best > v"0" && return best
64+
end
65+
catch
66+
end
67+
return legacy_opencl_c_version(dev)
68+
end
69+
70+
# Pre-3.0 fallback: parse the "OpenCL C <major>.<minor> <vendor>" string.
71+
function legacy_opencl_c_version(dev::cl.Device)::VersionNumber
72+
CL_DEVICE_OPENCL_C_VERSION = 0x103d
73+
try
74+
sz = Ref{Csize_t}(0)
75+
cl.clGetDeviceInfo(dev, CL_DEVICE_OPENCL_C_VERSION, 0, C_NULL, sz)
76+
buf = Vector{UInt8}(undef, sz[])
77+
cl.clGetDeviceInfo(dev, CL_DEVICE_OPENCL_C_VERSION, sz[], buf, C_NULL)
78+
str = String(buf[1:(end - 1)]) # drop the trailing NUL
79+
m = match(r"OpenCL C (\d+)\.(\d+)", str)
80+
m !== nothing && return VersionNumber(parse(Int, m[1]), parse(Int, m[2]))
81+
catch
82+
end
83+
return v"1.2" # conservative default
84+
end
85+
4386
const FEATURES = Feature[
4487
Feature(:fp16, dev -> "cl_khr_fp16" in dev.extensions),
4588
Feature(:fp64, dev -> "cl_khr_fp64" in dev.extensions),

src/compiler/compilation.jl

Lines changed: 162 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ Base.@kwdef struct OpenCLCompilerParams <: AbstractCompilerParams
55
sub_group_size::Union{Nothing,Int} = nothing
66
# optional features the target device supports, exposed to kernels via `has_feature`
77
features::FeatureSet = zero(FeatureSet)
8+
# requested backend policy; kept here so the cache keys on it (the backends share a context)
9+
program_backend::Symbol = :auto
810
end
911

1012
const OpenCLCompilerConfig = CompilerConfig{SPIRVCompilerTarget, OpenCLCompilerParams}
@@ -140,16 +142,21 @@ end
140142
const _toolchain = Ref{Any}()
141143
const _compiler_configs = Dict{UInt, OpenCLCompilerConfig}()
142144
function compiler_config(dev::cl.Device; kwargs...)
143-
h = hash(dev, hash(kwargs))
145+
# key on the policy, not the resolved backend: resolving queries the device, so defer it to link
146+
backend = program_backend()
147+
h = hash(dev, hash(backend, hash(kwargs)))
144148
config = get(_compiler_configs, h, nothing)
145149
if config === nothing
146-
config = _compiler_config(dev; kwargs...)
150+
config = _compiler_config(dev, backend; kwargs...)
147151
_compiler_configs[h] = config
148152
end
149153
return config
150154
end
151155
@inline _sub_group_size(dev) = "cl_intel_required_subgroup_size" in dev.extensions ? cl.sub_group_size(dev) : nothing
152-
@noinline function _compiler_config(dev; kernel=true, name=nothing, always_inline=false,
156+
157+
const SPIRV_VERSION = v"1.4"
158+
159+
@noinline function _compiler_config(dev, backend; kernel=true, name=nothing, always_inline=false,
153160
sub_group_size::Union{Nothing,Int}=_sub_group_size(dev), kwargs...)
154161
supports_fp16 = "cl_khr_fp16" in dev.extensions
155162
supports_fp64 = "cl_khr_fp64" in dev.extensions
@@ -159,8 +166,10 @@ end
159166
end
160167

161168
# create GPUCompiler objects
162-
target = SPIRVCompilerTarget(; supports_fp16, supports_fp64, validate=true, kwargs...)
163-
params = OpenCLCompilerParams(; sub_group_size, features=device_features(dev))
169+
target = SPIRVCompilerTarget(; version=SPIRV_VERSION, supports_fp16, supports_fp64,
170+
validate=true, kwargs...)
171+
params = OpenCLCompilerParams(; sub_group_size, features=device_features(dev),
172+
program_backend=backend)
164173
CompilerConfig(target, params; kernel, name, always_inline)
165174
end
166175

@@ -180,21 +189,158 @@ function compile(@nospecialize(job::CompilerJob))
180189
end
181190
end
182191

192+
function run_and_collect(cmd)
193+
stdout = Pipe()
194+
proc = run(pipeline(ignorestatus(cmd); stdout, stderr=stdout), wait=false)
195+
close(stdout.in)
196+
197+
reader = Threads.@spawn String(read(stdout))
198+
Base.wait(proc)
199+
log = strip(fetch(reader))
200+
201+
return proc, log
202+
end
203+
204+
# How kernels are fed to the driver:
205+
# - `:auto` — SPIR-V (clCreateProgramWithIL) when the device supports IL programs, otherwise
206+
# OpenCL C source translated from the SPIR-V via spirv2clc.
207+
# - `:spirv` — always SPIR-V IL; error if the device doesn't support it.
208+
# - `:opencl` — always OpenCL C source. Useful to exercise pocl's source path on a device that
209+
# also supports IL.
210+
# Task-local, like `cl.device`/`cl.queue`; `resolve_program_backend` maps it to a concrete backend.
211+
212+
"""
213+
program_backend() -> Symbol
214+
215+
The requested program backend for the current task (`:auto`, `:spirv`, or `:opencl`).
216+
"""
217+
program_backend() = get(task_local_storage(), :CLProgramBackend, :auto)::Symbol
218+
219+
"""
220+
program_backend!(mode::Symbol)
221+
program_backend!(f::Function, mode::Symbol)
222+
223+
Select how kernels are fed to the driver for the current task: `:auto` (default), `:spirv` (SPIR-V
224+
IL), or `:opencl` (OpenCL C source). The second form applies `mode` only for the duration of `f`.
225+
"""
226+
function program_backend!(mode::Symbol)
227+
mode in (:auto, :spirv, :opencl) ||
228+
throw(ArgumentError("invalid program backend $mode (expected :auto, :spirv or :opencl)"))
229+
task_local_storage(:CLProgramBackend, mode)
230+
return mode
231+
end
232+
233+
function program_backend!(f::Base.Callable, mode::Symbol)
234+
old = program_backend()
235+
program_backend!(mode)
236+
try
237+
f()
238+
finally
239+
program_backend!(old)
240+
end
241+
end
242+
243+
# Map the policy to a concrete backend for `dev`. Done in `link`, off the per-launch path.
244+
function resolve_program_backend(dev::cl.Device, mode::Symbol = program_backend())
245+
mode === :opencl && return :opencl
246+
mode in (:auto, :spirv) ||
247+
throw(ArgumentError("invalid program backend $mode (expected :auto, :spirv or :opencl)"))
248+
if "cl_khr_il_program" in dev.extensions
249+
return :spirv
250+
elseif mode === :spirv
251+
error("Device '$(dev.name)' does not support SPIR-V IL programs")
252+
else # :auto
253+
@warn "Device '$(dev.name)' lacks IL support; using OpenCL C translation." maxlog=1 _id=Symbol(dev.name)
254+
return :opencl
255+
end
256+
end
257+
258+
# Dump compilation artifacts (`extension => data` pairs sharing one random base name, e.g.
259+
# `dump_artifacts(".spv" => spv, ".cl" => src)`) to a directory for later inspection. The
260+
# directory is `JULIA_OPENCL_DUMP_DIR` if set, else `$RUNNER_TEMP/opencl-compilation-dumps` on
261+
# GitHub Actions, else `tempdir()`. On CI the files are surfaced as downloadable artifacts
262+
# (`buildkite-agent artifact upload`, or a GitHub Actions `::notice`). Returns the written paths.
263+
function dump_artifacts(artifacts::Pair{String}...)
264+
on_github = get(ENV, "GITHUB_ACTIONS", "false") == "true"
265+
dir = if haskey(ENV, "JULIA_OPENCL_DUMP_DIR")
266+
mkpath(ENV["JULIA_OPENCL_DUMP_DIR"])
267+
elseif on_github
268+
mkpath(joinpath(get(ENV, "RUNNER_TEMP", tempdir()), "opencl-compilation-dumps"))
269+
else
270+
tempdir()
271+
end
272+
stem = tempname(dir; cleanup=false)
273+
274+
paths = String[]
275+
for (ext, data) in artifacts
276+
path = stem * ext
277+
write(path, data)
278+
push!(paths, path)
279+
end
280+
281+
if parse(Bool, get(ENV, "BUILDKITE", "false"))
282+
for path in paths
283+
run(`buildkite-agent artifact upload $path`)
284+
end
285+
elseif on_github
286+
println("::notice title=OpenCL compilation dump::wrote $(join(basename.(paths), ", ")) to $dir")
287+
end
288+
289+
return paths
290+
end
291+
183292
# link into an executable kernel
184293
function link(@nospecialize(job::CompilerJob), compiled)
185-
prog = if "cl_khr_il_program" in cl.device().extensions
186-
cl.Program(; il=compiled.obj)
294+
spirv_bitcode = compiled.obj
295+
clc_source = nothing
296+
build_options = ""
297+
298+
dev = cl.device()
299+
backend = resolve_program_backend(dev, job.config.params.program_backend)
300+
301+
prog = if backend === :spirv
302+
cl.Program(; il=spirv_bitcode)
187303
else
188-
error("Your device does not support SPIR-V, which is currently required for native execution.")
189-
# XXX: kpet/spirv2clc#87, caused by KhronosGroup/SPIRV-LLVM-Translator#2029
190-
source = mktempdir() do dir
191-
il = joinpath(dir, "kernel.spv")
192-
write(il, compiled.obj)
193-
cmd = `spirv2clc $il`
194-
read(cmd, String)
304+
# Target the device's highest OpenCL C version
305+
clc_version = max_opencl_c_version(dev)
306+
cl_std = "CL$(clc_version.major).$(clc_version.minor)"
307+
308+
# Be consistent with the SPIR-V version we generated code for
309+
spv = job.config.target.version
310+
spirv_version = "$(spv.major).$(spv.minor)"
311+
312+
spirv_path = tempname(cleanup=false) * ".spv"
313+
write(spirv_path, spirv_bitcode)
314+
proc, log = run_and_collect(`$(spirv2clc_jll.spirv2clc()) --spirv-version=$spirv_version --cl-std=$cl_std $spirv_path`)
315+
rm(spirv_path)
316+
if !success(proc)
317+
spv_file, = dump_artifacts(".spv" => spirv_bitcode)
318+
error("Failed to translate SPIR-V to OpenCL C source code:\n$(log)\n" *
319+
"If you think this is a bug, please file an issue and attach $spv_file")
195320
end
196-
cl.Program(; source)
321+
322+
clc_source = strip(log)
323+
build_options = "-cl-std=$cl_std"
324+
cl.Program(; source=clc_source)
325+
end
326+
327+
# optionally dump the artifacts of every kernel (e.g. to debug a runtime miscompile)
328+
if haskey(ENV, "JULIA_OPENCL_DUMP_DIR")
329+
clc_source === nothing ? dump_artifacts(".spv" => spirv_bitcode) :
330+
dump_artifacts(".spv" => spirv_bitcode, ".cl" => clc_source)
331+
end
332+
333+
try
334+
cl.build!(prog; options=build_options)
335+
catch e
336+
files = clc_source === nothing ?
337+
dump_artifacts(".spv" => spirv_bitcode) :
338+
dump_artifacts(".spv" => spirv_bitcode, ".cl" => clc_source)
339+
340+
# `build!` already renders the source and build log; keep that and point at the artifacts
341+
msg = sprint(showerror, e)
342+
msg *= "\nIf you think this is a bug, please file an issue and attach $(join(files, " and "))"
343+
error(msg)
197344
end
198-
cl.build!(prog)
199345
(; kernel=cl.Kernel(prog, compiled.entry), compiled.device_rng)
200346
end

test/execution.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ end
139139
OpenCL.code_llvm(io, () -> nothing, (); dump_module = true, backend = :llvm)
140140
end
141141
if Int === Int64
142-
@test occursin("target triple = \"spirv64-unknown-unknown-unknown\"", llvm_backend_llvm)
142+
@test occursin("target triple = \"spirv64v1.4-unknown-unknown-unknown\"", llvm_backend_llvm)
143143
end
144144

145145
llvm_backend_khronos = sprint() do io

0 commit comments

Comments
 (0)