@@ -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
810end
911
1012const OpenCLCompilerConfig = CompilerConfig{SPIRVCompilerTarget, OpenCLCompilerParams}
@@ -140,16 +142,21 @@ end
140142const _toolchain = Ref {Any} ()
141143const _compiler_configs = Dict {UInt, OpenCLCompilerConfig} ()
142144function 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
150154end
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
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)
165174end
166175
@@ -180,21 +189,158 @@ function compile(@nospecialize(job::CompilerJob))
180189 end
181190end
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
184293function 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 *= " \n If 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)
200346end
0 commit comments