From a6cf99fc4457fe35a6115b93fae959e1f9202687 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Mon, 19 Oct 2026 06:48:36 +0200 Subject: [PATCH 01/17] Add a `julia_passes` setting selecting the provider of Julia's passes `CompilerConfig` gains `julia_passes`: `:cpp` for the passes of Julia's code generator (wrapped by `LLVM.Interop`), `:ports` for their ports in JuliaIR.jl. The default comes from the `julia_passes` preference. Being a config field, it is part of the identity of every cached LLVM artifact, but not of the inference caches. JuliaIR.jl is a weak dependency: its extension tells GPUCompiler how to register the ports and which of Julia's passes they replace. Loading it alone changes nothing; selecting `:ports` without it is an error before compilation starts. Until JuliaIR.jl is registered, the test environment takes it from `[sources]`. --- Project.toml | 3 +++ ext/JuliaIRExt.jl | 38 ++++++++++++++++++++++++++++++++++ src/GPUCompiler.jl | 1 + src/driver.jl | 1 + src/interface.jl | 26 ++++++++++++++++------- src/julia_passes.jl | 50 +++++++++++++++++++++++++++++++++++++++++++++ test/Project.toml | 4 ++++ 7 files changed, 116 insertions(+), 7 deletions(-) create mode 100644 ext/JuliaIRExt.jl create mode 100644 src/julia_passes.jl diff --git a/Project.toml b/Project.toml index f50ee3299..4ce29828d 100644 --- a/Project.toml +++ b/Project.toml @@ -29,6 +29,7 @@ tree_sitter_spirv_jll = "f0e86581-c468-54df-a4de-3266e11a3c86" AMDGPU_LLVM_Backend_jll = "cc5c0156-bd05-5a77-8a68-bb0aafb29019" Cthulhu = "f68482b8-f384-11e8-15f7-abe071a5a75f" Highlights = "eafb193a-b7ab-5a9e-9068-77385905fa72" +JuliaIR = "3913dba9-7ba9-44aa-bf76-1cfa3b3073b1" LLVMDowngrader_jll = "f52de702-fb25-5922-94ba-81dd59b07444" NVPTX_LLVM_Backend_jll = "ef6e0fe3-e6ef-59c0-bde6-4989574699e0" SPIRV_LLVM_Backend_jll = "4376b9bf-cff8-51b6-bb48-39421dff0d0c" @@ -37,6 +38,7 @@ SPIRV_LLVM_Translator_jll = "4a5d46fc-d8cf-5151-a261-86b458210efb" [extensions] CthulhuExt = "Cthulhu" HighlightsExt = "Highlights" +JuliaIRExt = "JuliaIR" [compat] AMDGPU_LLVM_Backend_jll = "23.1.3" @@ -45,6 +47,7 @@ Cthulhu = "2, 3" ExprTools = "0.1" Highlights = "0.6" InteractiveUtils = "1" +JuliaIR = "0.1" LLVM = "10.4" LLVMDowngrader_jll = "0.11, 0.12" Libdl = "1" diff --git a/ext/JuliaIRExt.jl b/ext/JuliaIRExt.jl new file mode 100644 index 000000000..7095ec8c5 --- /dev/null +++ b/ext/JuliaIRExt.jl @@ -0,0 +1,38 @@ +# Julia's LLVM passes as ported to Julia by JuliaIR.jl, for jobs that select them with +# `julia_passes=:ports`. Loading this extension alone does not change generated code. +module JuliaIRExt + +import GPUCompiler +import JuliaIR + +# The port of each of Julia's passes in GPUCompiler's pipeline, by the name of the +# `LLVM.Interop` constructor of the C++ pass. The ones JuliaIR.jl does not port yet are +# `nothing`, and run as the C++ pass. +const PORTS = Dict{Symbol,Union{Nothing,String}}( + :AllocOptPass => "jl-alloc-opt", + :CombineMulAddPass => "jl-combine-muladd", + :DemoteFloat16Pass => "jl-demote-float16", + :ExpandAtomicModifyPass => "jl-expand-atomic-modify", + :GCInvariantVerifierPass => "jl-gc-invariant-verifier", + :LateLowerGCPass => "jl-late-lower-gc-frame", + :LowerExcHandlersPass => "jl-lower-exc-handlers", + :LowerSIMDLoopPass => "jl-lower-simdloop", + :PropagateJuliaAddrspacesPass => "jl-propagate-addrspaces", + :RemoveNIPass => "jl-remove-ni", + + :FinalLowerGCPass => nothing, + :JuliaLICMPass => nothing, + :LowerPTLSPass => nothing, + :RemoveJuliaAddrspacesPass => nothing, +) + +function __init__() + available = JuliaIR.passes() + for port in values(PORTS) + port === nothing || port in available || + error("JuliaIR.jl does not provide the pass $port") + end + GPUCompiler.pass_ports[] = GPUCompiler.PassPorts(pb -> JuliaIR.register!(pb), PORTS) +end + +end # module JuliaIRExt diff --git a/src/GPUCompiler.jl b/src/GPUCompiler.jl index 8231c94b7..bde9e2afb 100644 --- a/src/GPUCompiler.jl +++ b/src/GPUCompiler.jl @@ -77,6 +77,7 @@ include("deprecated.jl") include("jlgen.jl") include("irgen.jl") include("determinism.jl") +include("julia_passes.jl") include("optim.jl") include("validation.jl") include("rtlib.jl") diff --git a/src/driver.jl b/src/driver.jl index 32c00b117..32008ae86 100644 --- a/src/driver.jl +++ b/src/driver.jl @@ -108,6 +108,7 @@ function compile_unhooked(output::Symbol, @nospecialize(job::CompilerJob); if context(; throw_error=false) === nothing error("No active LLVM context. Use `JuliaContext()` do-block syntax to create one.") end + check_julia_passes(job) @tracepoint "Validation" begin check_method(job) # not optional diff --git a/src/interface.jl b/src/interface.jl index 00dcfea30..657c6e680 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -143,7 +143,8 @@ else end const CONFIG_KWARGS = [:kernel, :name, :entry_abi, :always_inline, :opt_level, - :debug_level, :libraries, :optimize, :cleanup, :validate, :strip] + :debug_level, :libraries, :optimize, :cleanup, :validate, :strip, + :julia_passes] """ CompilerConfig(target, params; kernel=true, entry_abi=:specfunc, name=nothing, @@ -177,6 +178,11 @@ Several keyword arguments can be used to customize the compilation process: - `cleanup`: run cleanup passes on the code (default: true) - `validate`: enable optional validation of input and outputs (default: true) - `strip`: strip non-functional metadata and debug information (default: false) +- `julia_passes`: the implementation of Julia's LLVM passes (e.g. GC lowering) that + optimization uses: `:cpp` for the passes of Julia's code generator, or `:ports` for + their ports in JuliaIR.jl, which must then be loaded. The default comes from the + `julia_passes` preference of GPUCompiler (`:cpp` if unset). See + [`GPUCompiler.julia_pass`](@ref) for which passes are ported. """ struct CompilerConfig{T,P} target::T @@ -193,6 +199,7 @@ struct CompilerConfig{T,P} cleanup::Bool validate::Bool strip::Bool + julia_passes::Symbol # internal toplevel::Bool @@ -206,10 +213,13 @@ struct CompilerConfig{T,P} always_inline=false, opt_level=2, debug_level=Base.JLOptions().debug_level, optimize=toplevel, libraries=toplevel, cleanup=toplevel, validate=toplevel, - strip=false, only_entry=false) + strip=false, julia_passes=JULIA_PASSES, only_entry=false) if entry_abi ∉ (:specfunc, :func) error("Unknown entry_abi=$entry_abi") end + if julia_passes ∉ (:cpp, :ports) + error("Unknown julia_passes=$julia_passes") + end owner = @static if HAS_INTEGRATED_CACHE cache_owner(target, params, always_inline) else @@ -217,8 +227,8 @@ struct CompilerConfig{T,P} end new{typeof(target), typeof(params)}(target, params, kernel, name, entry_abi, always_inline, opt_level, debug_level, libraries, - optimize, cleanup, validate, strip, toplevel, - only_entry, owner) + optimize, cleanup, validate, strip, + julia_passes, toplevel, only_entry, owner) end end @@ -228,7 +238,8 @@ function CompilerConfig(cfg::CompilerConfig; target=cfg.target, params=cfg.param always_inline=cfg.always_inline, opt_level=cfg.opt_level, debug_level=cfg.debug_level, libraries=cfg.libraries, optimize=cfg.optimize, cleanup=cfg.cleanup, - validate=cfg.validate, strip=cfg.strip, toplevel=cfg.toplevel, + validate=cfg.validate, strip=cfg.strip, + julia_passes=cfg.julia_passes, toplevel=cfg.toplevel, only_entry=cfg.only_entry) # deriving a non-toplevel job disables certain features # XXX: should we keep track if any of these were set explicitly in the first place? @@ -240,8 +251,8 @@ function CompilerConfig(cfg::CompilerConfig; target=cfg.target, params=cfg.param validate = false end CompilerConfig(target, params; kernel, entry_abi, name, always_inline, opt_level, - debug_level, libraries, optimize, cleanup, validate, strip, toplevel, - only_entry) + debug_level, libraries, optimize, cleanup, validate, strip, + julia_passes, toplevel, only_entry) end function Base.show(io::IO, @nospecialize(cfg::CompilerConfig{T})) where {T} @@ -263,6 +274,7 @@ function Base.hash(cfg::CompilerConfig, h::UInt) h = hash(cfg.cleanup, h) h = hash(cfg.validate, h) h = hash(cfg.strip, h) + h = hash(cfg.julia_passes, h) h = hash(cfg.toplevel, h) h = hash(cfg.only_entry, h) # `cache_owner` deliberately is not hashed: its target/params/inlining components were diff --git a/src/julia_passes.jl b/src/julia_passes.jl new file mode 100644 index 000000000..6dade4d3c --- /dev/null +++ b/src/julia_passes.jl @@ -0,0 +1,50 @@ +# Julia's LLVM passes, from Julia's code generator or as ported to Julia by JuliaIR.jl + +# The default of `CompilerConfig`'s `julia_passes`. Set with +# `Preferences.set_preferences!(GPUCompiler, "julia_passes" => "ports")`. +const JULIA_PASSES = Symbol(@load_preference("julia_passes", "cpp")) +JULIA_PASSES in (:cpp, :ports) || + error("Invalid julia_passes preference $(repr(String(JULIA_PASSES))); expected \"cpp\" or \"ports\"") + +# The ports of Julia's passes, supplied by the JuliaIRExt extension: a function registering +# them with a `PassBuilder`, and the pipeline name of the port of each of Julia's passes by +# the name of the `LLVM.Interop` constructor of the C++ pass (`nothing` if not ported). +struct PassPorts + register!::Function + names::Dict{Symbol,Union{Nothing,String}} +end +const pass_ports = Ref{Union{Nothing,PassPorts}}(nothing) + +function check_julia_passes(@nospecialize(job::CompilerJob)) + job.config.julia_passes === :ports || return + pass_ports[] === nothing && + error("julia_passes=:ports requires JuliaIR.jl to be loaded (`using JuliaIR`)") + return +end + +""" + julia_pass(job, name::Symbol) + +The Julia pass `name` (the name of its constructor in `LLVM.Interop`, e.g. +`:AllocOptPass`) for use in the optimization pipeline of `job`: Julia's C++ pass, or its +port in JuliaIR.jl if `job.config.julia_passes == :ports`. + +Passes that JuliaIR.jl does not port yet run as the C++ pass in either mode; the +JuliaIRExt extension lists them. +""" +function julia_pass(@nospecialize(job::CompilerJob), name::Symbol) + if job.config.julia_passes === :ports + ports = pass_ports[]::PassPorts + haskey(ports.names, name) || error("Unknown Julia pass $name") + port = ports.names[name] + port === nothing || return port + end + return getfield(LLVM.Interop, name)() +end + +# make the ports usable by name in the pipeline of `pb` +function register_julia_passes!(pb::PassBuilder, @nospecialize(job::CompilerJob)) + job.config.julia_passes === :ports || return + Base.invokelatest((pass_ports[]::PassPorts).register!, pb) + return +end diff --git a/test/Project.toml b/test/Project.toml index 8d2eba274..575672fcc 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -6,6 +6,7 @@ FileCheck = "4e644321-382b-4b05-b0b6-5d23c3d944fb" GPUCompiler = "61eb1bfa-7361-4325-ad38-22787b887f55" Highlights = "eafb193a-b7ab-5a9e-9068-77385905fa72" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" +JuliaIR = "3913dba9-7ba9-44aa-bf76-1cfa3b3073b1" LLVM = "929cbde3-209d-540e-8aea-75f648917ca0" LLVMDowngrader_jll = "f52de702-fb25-5922-94ba-81dd59b07444" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" @@ -21,6 +22,9 @@ StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" demumble_jll = "1e29f10c-031c-5a83-9565-69cddfc27673" +[sources] +JuliaIR = {url = "https://github.com/JuliaLLVM/JuliaIR.jl", rev = "main"} + [compat] Aqua = "0.8" ParallelTestRunner = "2" From 9e45061873ec8beeced08c160a70c07b039ac1c5 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Mon, 19 Oct 2026 06:48:36 +0200 Subject: [PATCH 02/17] Build the Julia passes of the pipeline from the selected provider `optimize!` now asks `julia_pass` for each of Julia's passes, which returns the C++ pass or, with `julia_passes=:ports`, the name of the port, registered on the pass builder. Passes JuliaIR.jl does not port yet (FinalLowerGC, JuliaLICM, LowerPTLS, RemoveJuliaAddrspaces) run as the C++ pass in both modes; the extension lists them explicitly. --- src/optim.jl | 45 +++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/src/optim.jl b/src/optim.jl index 9c426dbdb..68efe7b2b 100644 --- a/src/optim.jl +++ b/src/optim.jl @@ -57,6 +57,7 @@ function optimize!(@nospecialize(job::CompilerJob), mod::LLVM.Module, @dispose pb=PassBuilder() begin tti === nothing || LLVM.target_transform_info!(pb, tti) + register_julia_passes!(pb, job) register!(pb, GPULowerCPUFeaturesPass(job)) register!(pb, GPULowerPTLSPass(job)) register!(pb, GPULowerGCFramePass(job, relocs)) @@ -121,7 +122,7 @@ end function buildEarlySimplificationPipeline(mpm, @nospecialize(job::CompilerJob), opt_level) if should_verify() add!(mpm, FunctionPassManager()) do fpm - add!(fpm, GCInvariantVerifierPass()) + add!(fpm, julia_pass(job, :GCInvariantVerifierPass)) end add!(mpm, VerifierPass()) end @@ -135,7 +136,7 @@ function buildEarlySimplificationPipeline(mpm, @nospecialize(job::CompilerJob), add!(mpm, FunctionPassManager()) do fpm add!(fpm, LowerExpectIntrinsicPass()) if opt_level >= 2 - add!(fpm, PropagateJuliaAddrspacesPass()) + add!(fpm, julia_pass(job, :PropagateJuliaAddrspacesPass)) end # DCE must come before simplifycfg: codegen can generate unused # statements that would otherwise alter how simplifycfg optimizes the CFG. @@ -168,7 +169,7 @@ function buildEarlyOptimizerPipeline(mpm, @nospecialize(job::CompilerJob), opt_l end if opt_level >= 2 add!(cgpm, FunctionPassManager()) do fpm - add!(fpm, AllocOptPass()) + add!(fpm, julia_pass(job, :AllocOptPass)) add!(fpm, Float2IntPass()) add!(fpm, LowerConstantIntrinsicsPass()) end @@ -187,7 +188,7 @@ function buildEarlyOptimizerPipeline(mpm, @nospecialize(job::CompilerJob), opt_l add!(fpm, LibCallsShrinkWrapPass()) add!(fpm, ReassociatePass()) add!(fpm, ConstraintEliminationPass()) - add!(fpm, AllocOptPass()) + add!(fpm, julia_pass(job, :AllocOptPass)) else add!(fpm, EarlyCSEPass()) add!(fpm, instcombine_pass(job)) @@ -203,17 +204,17 @@ end function buildLoopOptimizerPipeline(fpm, @nospecialize(job::CompilerJob), opt_level) add!(fpm, LoopPassManager(; use_memory_ssa=true)) do lpm - add!(lpm, LowerSIMDLoopPass()) + add!(lpm, julia_pass(job, :LowerSIMDLoopPass)) if opt_level >= 2 add!(lpm, LoopInstSimplifyPass()) add!(lpm, LoopSimplifyCFGPass()) # run LICM with AllowSpeculation=false before rotation to avoid # speculating loads that rotation can hoist more precisely. add!(lpm, LICMPass(; allowspeculation=false)) - add!(lpm, JuliaLICMPass()) + add!(lpm, julia_pass(job, :JuliaLICMPass)) add!(lpm, LoopRotatePass()) add!(lpm, LICMPass()) - add!(lpm, JuliaLICMPass()) + add!(lpm, julia_pass(job, :JuliaLICMPass)) add!(lpm, SimpleLoopUnswitchPass(nontrivial=true, trivial=true)) end if LLVM.version() >= v"17" @@ -241,7 +242,7 @@ end function buildScalarOptimizerPipeline(fpm, @nospecialize(job::CompilerJob), opt_level) if opt_level >= 2 - add!(fpm, AllocOptPass()) + add!(fpm, julia_pass(job, :AllocOptPass)) add!(fpm, SROAPass()) if can_vectorize(job) add!(fpm, VectorCombinePass()) @@ -259,7 +260,7 @@ function buildScalarOptimizerPipeline(fpm, @nospecialize(job::CompilerJob), opt_ add!(fpm, JumpThreadingPass()) add!(fpm, ConstraintEliminationPass()) elseif opt_level >= 1 - add!(fpm, AllocOptPass()) + add!(fpm, julia_pass(job, :AllocOptPass)) add!(fpm, SROAPass()) add!(fpm, MemCpyOptPass()) add!(fpm, SCCPPass()) @@ -276,10 +277,10 @@ function buildScalarOptimizerPipeline(fpm, @nospecialize(job::CompilerJob), opt_ add!(fpm, PeepholeCallbacks(; opt_level)) end add!(fpm, simplifycfg_pass(job, AggressiveSimplifyCFGOptions)) - add!(fpm, AllocOptPass()) + add!(fpm, julia_pass(job, :AllocOptPass)) add!(fpm, LoopPassManager(; use_memory_ssa=true)) do lpm add!(lpm, LICMPass()) - add!(lpm, JuliaLICMPass()) + add!(lpm, julia_pass(job, :JuliaLICMPass)) end add!(fpm, simplifycfg_pass(job, AggressiveSimplifyCFGOptions)) add!(fpm, instcombine_pass(job)) @@ -326,7 +327,7 @@ function buildVectorPipeline(fpm, @nospecialize(job::CompilerJob), opt_level) end function buildIntrinsicLoweringPipeline(mpm, @nospecialize(job::CompilerJob), opt_level) - add!(mpm, RemoveNIPass()) + add!(mpm, julia_pass(job, :RemoveNIPass)) # lower GC intrinsics if !uses_julia_runtime(job) @@ -365,24 +366,24 @@ function buildIntrinsicLoweringPipeline(mpm, @nospecialize(job::CompilerJob), op add!(mpm, FunctionPassManager()) do fpm # lower exception handling if uses_julia_runtime(job) && VERSION < v"1.13.0-DEV.36" - add!(fpm, LowerExcHandlersPass()) + add!(fpm, julia_pass(job, :LowerExcHandlersPass)) end - add!(fpm, GCInvariantVerifierPass()) - add!(fpm, LateLowerGCPass()) + add!(fpm, julia_pass(job, :GCInvariantVerifierPass)) + add!(fpm, julia_pass(job, :LateLowerGCPass)) # FinalLowerGCPass moved from a module pass to a function pass in # JuliaLang/julia#51081 (1.11.0-DEV.208). if uses_julia_runtime(job) && VERSION >= v"1.11.0-DEV.208" - add!(fpm, FinalLowerGCPass()) + add!(fpm, julia_pass(job, :FinalLowerGCPass)) end # codegen emits atomic modifications (e.g. `@atomic x.f += 1`) as calls to the # `julia.atomicmodify` pseudo-intrinsic, which Julia expands after GC lowering # (JuliaLang/julia#57010) @static if VERSION >= v"1.13.0-DEV.321" - add!(fpm, ExpandAtomicModifyPass()) + add!(fpm, julia_pass(job, :ExpandAtomicModifyPass)) end end if uses_julia_runtime(job) && VERSION < v"1.11.0-DEV.208" - add!(mpm, FinalLowerGCPass()) + add!(mpm, julia_pass(job, :FinalLowerGCPass)) end if opt_level >= 2 @@ -395,7 +396,7 @@ function buildIntrinsicLoweringPipeline(mpm, @nospecialize(job::CompilerJob), op # lower PTLS intrinsics if uses_julia_runtime(job) - add!(mpm, LowerPTLSPass()) + add!(mpm, julia_pass(job, :LowerPTLSPass)) end if opt_level >= 1 @@ -413,7 +414,7 @@ function buildIntrinsicLoweringPipeline(mpm, @nospecialize(job::CompilerJob), op end # remove Julia address spaces - add!(mpm, RemoveJuliaAddrspacesPass()) + add!(mpm, julia_pass(job, :RemoveJuliaAddrspacesPass)) # Julia's operand bundles confuse the inliner, so repeat here now they are gone. # FIXME: we should fix the inliner so that inlined code gets optimized early-on @@ -427,7 +428,7 @@ function buildCleanupPipeline(mpm, @nospecialize(job::CompilerJob), opt_level) if opt_level >= 2 add!(mpm, FunctionPassManager()) do fpm if VERSION < v"1.12.0-DEV.1390" - add!(fpm, CombineMulAddPass()) + add!(fpm, julia_pass(job, :CombineMulAddPass)) end add!(fpm, DivRemPairsPass()) end @@ -439,7 +440,7 @@ function buildCleanupPipeline(mpm, @nospecialize(job::CompilerJob), opt_level) add!(fpm, AnnotationRemarksPass()) end add!(mpm, FunctionPassManager()) do fpm - add!(fpm, DemoteFloat16Pass()) + add!(fpm, julia_pass(job, :DemoteFloat16Pass)) if opt_level >= 2 add!(fpm, GVNPass()) end From d8aac5a595be0eeaf439b010c2c092db2d263141 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Mon, 19 Oct 2026 06:50:52 +0200 Subject: [PATCH 03/17] Compile in opaque-pointer contexts when using the ports of Julia's passes JuliaIR.jl's passes only support opaque pointers, while Julia 1.10 and 1.11 use typed pointers by default. With `julia_passes=:ports`, the reflection entry points create opaque contexts, and `JuliaContext()` does so too when the preference selects the ports, so that the many callers that create their own context follow the preference. Compiling a `:ports` job in a typed context is an error. Without the preference, `JuliaContext()` keeps LLVM's default. The precompilation workload pins the C++ passes, as JuliaIR.jl is not loaded while GPUCompiler precompiles. --- src/driver.jl | 9 ++++++--- src/julia_passes.jl | 6 ++++++ src/precompile.jl | 6 ++++-- src/reflection.jl | 6 ++++-- src/spirv.jl | 3 ++- 5 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/driver.jl b/src/driver.jl index 32008ae86..116d23642 100644 --- a/src/driver.jl +++ b/src/driver.jl @@ -18,12 +18,15 @@ export JuliaContext # JuliaContext helper below, which returns a local context on Julia 1.9, and the global # unique context on all other versions. Once we only support Julia 1.9, we'll deprecate # this helper to a regular `Context()` call. -function JuliaContext(; opaque_pointers=nothing) +# +# By default, the context uses opaque pointers if the `julia_passes` preference selects the +# ports of Julia's passes, which require them, and LLVM's default otherwise. +function JuliaContext(; opaque_pointers=context_opaque_pointers(JULIA_PASSES)) # XXX: remove ThreadSafeContext(; opaque_pointers) end -function JuliaContext(f; kwargs...) - ThreadSafeContext(; kwargs...) do ts_ctx +function JuliaContext(f; opaque_pointers=context_opaque_pointers(JULIA_PASSES)) + ThreadSafeContext(; opaque_pointers) do ts_ctx # for now, also activate the underlying context # XXX: this is wrong; we can't expose the underlying LLVM context, but should # instead always go through the callback in order to unlock it properly. diff --git a/src/julia_passes.jl b/src/julia_passes.jl index 6dade4d3c..65ad091f4 100644 --- a/src/julia_passes.jl +++ b/src/julia_passes.jl @@ -19,9 +19,15 @@ function check_julia_passes(@nospecialize(job::CompilerJob)) job.config.julia_passes === :ports || return pass_ports[] === nothing && error("julia_passes=:ports requires JuliaIR.jl to be loaded (`using JuliaIR`)") + supports_typed_pointers(context()) && + error("julia_passes=:ports requires a context with opaque pointers; create it with `JuliaContext(; opaque_pointers=true)`") return end +# The `opaque_pointers` argument for contexts to compile in with `julia_passes`: the ports +# only support opaque pointers, which Julia 1.10 and 1.11 do not use by default. +context_opaque_pointers(julia_passes::Symbol) = julia_passes === :ports ? true : nothing + """ julia_pass(job, name::Symbol) diff --git a/src/precompile.jl b/src/precompile.jl index 9f35c1281..b7056732d 100644 --- a/src/precompile.jl +++ b/src/precompile.jl @@ -32,10 +32,12 @@ import REPL params = precompile_module.DummyCompilerParams() # XXX: on Windows, compiling the GPU runtime leaks GPU code in the native cache, # so prevent building the runtime library (see JuliaGPU/GPUCompiler.jl#601) - config = CompilerConfig(target, params; libraries=false) + # JuliaIR.jl is not loaded here, so precompile the C++ passes even if the + # `julia_passes` preference selects the ports + config = CompilerConfig(target, params; libraries=false, julia_passes=:cpp) job = CompilerJob(source, config) - JuliaContext() do ctx + JuliaContext(; opaque_pointers=nothing) do ctx _, meta = GPUCompiler.compile(:asm, job) dispose(meta.ir) end diff --git a/src/reflection.jl b/src/reflection.jl index 9fbc7a60f..4dc794582 100644 --- a/src/reflection.jl +++ b/src/reflection.jl @@ -264,7 +264,8 @@ function code_llvm(io::IO, @nospecialize(job::CompilerJob); optimize::Bool=job.c debuginfo::Symbol=:default, dump_module::Bool=false, kwargs...) # NOTE: jl_dump_function_ir supports stripping metadata, so don't do it in the driver config = CompilerConfig(job.config; validate=false, strip=false, optimize) - str = JuliaContext() do ctx + opaque_pointers = context_opaque_pointers(config.julia_passes) + str = JuliaContext(; opaque_pointers) do ctx ir, meta = compile(:llvm, CompilerJob(job; config)) ts_mod = ThreadSafeModule(ir) entry_fn = meta.entry @@ -297,7 +298,8 @@ See also: [`@device_code_native`](@ref), `InteractiveUtils.code_llvm` function code_native(io::IO, @nospecialize(job::CompilerJob); raw::Bool=false, dump_module::Bool=false) config = CompilerConfig(job.config; strip=!raw, only_entry=!dump_module, validate=false) - asm = JuliaContext() do ctx + opaque_pointers = context_opaque_pointers(config.julia_passes) + asm = JuliaContext(; opaque_pointers) do ctx local asm, meta = compile(:asm, CompilerJob(job; config)) dispose(meta.ir) asm diff --git a/src/spirv.jl b/src/spirv.jl index f23c1779a..ac44c9f88 100644 --- a/src/spirv.jl +++ b/src/spirv.jl @@ -375,7 +375,8 @@ source_code(target::SPIRVCompilerTarget) = "spirv" # reimplementation that uses the SPIR-V disassembler, giving much more pleasant output function code_native(io::IO, job::CompilerJob{SPIRVCompilerTarget}; raw::Bool=false, dump_module::Bool=false) config = CompilerConfig(job.config; strip=!raw, only_entry=!dump_module, validate=false) - obj = JuliaContext() do ctx + opaque_pointers = context_opaque_pointers(config.julia_passes) + obj = JuliaContext(; opaque_pointers) do ctx local obj, meta = compile(:obj, CompilerJob(job; config)) dispose(meta.ir) obj From 3a4faed0eec945739a6d68b4a233a95d99720a54 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Mon, 19 Oct 2026 08:04:13 +0200 Subject: [PATCH 04/17] Key cached runtime bitcode on the pointer mode of the context On Julia 1.10 and 1.11, contexts may use typed or opaque pointers, and compiling with the ports of Julia's passes makes opaque ones common there. The bitcode of runtime functions is cached per configuration, so a function compiled in one kind of context was read back in the other, which fails for opaque bitcode in a typed context. Recompile it instead. --- src/rtlib.jl | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/rtlib.jl b/src/rtlib.jl index dd389b845..60cb5ad72 100644 --- a/src/rtlib.jl +++ b/src/rtlib.jl @@ -85,7 +85,10 @@ end mutable struct RuntimeFunctionResults bitcode::Union{Nothing,Vector{UInt8}} relocations::Relocations - RuntimeFunctionResults() = new(nothing, Relocations()) + # whether `bitcode` uses opaque pointers: on Julia 1.10 and 1.11, contexts can use + # either, and bitcode with opaque pointers can't be read in a typed context + opaque_pointers::Bool + RuntimeFunctionResults() = new(nothing, Relocations(), false) end # Compile a single runtime function and link it into `mod`. The renamed bitcode is @@ -99,7 +102,8 @@ function emit_function!(mod, relocs::Relocations, config::CompilerConfig, # Don't run a standalone inference walk on a miss: `compile_unhooked` below drives # inference itself. ci, res = runtime_function_results(rt_job) - if res !== nothing && res.bitcode !== nothing + opaque_pointers = !supports_typed_pointers(context()) + if res !== nothing && res.bitcode !== nothing && res.opaque_pointers == opaque_pointers link_relocatable!(mod, relocs, parse(LLVM.Module, res.bitcode), res.relocations) @@ -139,6 +143,7 @@ function emit_function!(mod, relocs::Relocations, config::CompilerConfig, res === nothing && (res = runtime_results(RuntimeFunctionResults, ci, rt_job.config)) res.bitcode = take!(io) res.relocations = meta.relocations + res.opaque_pointers = opaque_pointers end link_relocatable!(mod, relocs, new_mod, meta.relocations) From 8a3870d01cafce2cf7e8232af164135675da031e Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Mon, 19 Oct 2026 07:04:56 +0200 Subject: [PATCH 05/17] Make the test suite pass with the ports of Julia's passes Running the suite with the `julia_passes` preference set to "ports" needs JuliaIR.jl loaded, also in the processes some tests spawn. The test of Metal's lowering of atomics on thread-private objects pins the C++ passes: JuliaIR.jl's AllocOpt uses the escape analysis of Julia 1.13 on every version, which removes the object, so the IR it lowers would not occur. --- test/examples.jl | 4 +++- test/gcn.jl | 2 +- test/metal.jl | 11 +++++++---- test/metal/reproducibility.jl | 2 +- test/runtests.jl | 4 ++++ test/spirv/atomics.jl | 2 +- test/spirv/imaging.jl | 2 +- 7 files changed, 18 insertions(+), 9 deletions(-) diff --git a/test/examples.jl b/test/examples.jl index aef6d4ccf..1e666edcb 100644 --- a/test/examples.jl +++ b/test/examples.jl @@ -17,7 +17,9 @@ filter!(file -> !occursin("Kaleidoscope", file), files) cd(dir) do examples = relpath.(files, Ref(dir)) @testset for example in examples - cmd = `$(Base.julia_cmd()) --project=$(Base.active_project())` + # with JuliaIR.jl loaded, for when the preference selects the ports of Julia's passes + cmd = `$(Base.julia_cmd()) --project=$(Base.active_project()) + -e 'using JuliaIR; include(popfirst!(ARGS))'` @test success(pipeline(`$cmd $example`, stderr=stderr)) end end diff --git a/test/gcn.jl b/test/gcn.jl index 62321bfb0..3c39f6b3e 100644 --- a/test/gcn.jl +++ b/test/gcn.jl @@ -561,7 +561,7 @@ end # the back-end crashes the process, so generate code in another one script = """ - using GPUCompiler, LLVM + using GPUCompiler, LLVM, JuliaIR include($(repr(joinpath(@__DIR__, "helpers", "runtime.jl")))) include($(repr(joinpath(@__DIR__, "helpers", "gcn.jl")))) kernel_i8(p::Core.LLVMPtr{Int8,1}, x::Int8) = diff --git a/test/metal.jl b/test/metal.jl index 658a79e6c..1c5a2f9be 100644 --- a/test/metal.jl +++ b/test/metal.jl @@ -92,7 +92,7 @@ end Tuple{Core.LLVMPtr{Int32,1}, Core.LLVMPtr{Int32,1}, Int32, Int32, Int}; dump_module=true, kernel=true) end - @test Context() do ctx + @test JuliaContext() do ctx verify(parse(LLVM.Module, ir)) true end @@ -126,7 +126,7 @@ end # (by position, as older Julia versions do not name the parameters) function kernel_parameter_attributes(ir) - Context() do ctx + JuliaContext() do ctx m = parse(LLVM.Module, ir) f = only(filter(f -> startswith(f.name, "_Z6kernel"), collect(m.functions))) [Dict(LLVM.kind(attr) => LLVM.value(attr) @@ -529,7 +529,7 @@ end ret void } """ - @dispose ctx=Context() begin + JuliaContext() do ctx mod = parse(LLVM.Module, ir) GPUCompiler.merge_byte_gep_chains!(mod) f = mod.functions["k"] @@ -3150,7 +3150,10 @@ end source = methodinstance(typeof(mod.kernel), Tuple{Core.LLVMPtr{Float32,1}, Float32}, Base.get_world_counter()) target = MetalCompilerTarget(; macos=v"15", metal=v"3.2", air=v"2.7") - job = CompilerJob(source, CompilerConfig(target, Metal.CompilerParams(); kernel=true)) + # JuliaIR.jl's `AllocOpt` uses the escape analysis of 1.13 on every version, which + # removes the object altogether, so test the lowering with Julia's C++ passes + job = CompilerJob(source, CompilerConfig(target, Metal.CompilerParams(); kernel=true, + julia_passes=:cpp)) @test @filecheck begin @check "alloca" @check "cmpxchg" diff --git a/test/metal/reproducibility.jl b/test/metal/reproducibility.jl index b0a6eb13e..606ca1308 100644 --- a/test/metal/reproducibility.jl +++ b/test/metal/reproducibility.jl @@ -8,7 +8,7 @@ helpers = joinpath(@__DIR__, "..", "helpers") script = """ - using GPUCompiler, LLVM, LLVMDowngrader_jll + using GPUCompiler, LLVM, LLVMDowngrader_jll, JuliaIR include(joinpath($(repr(helpers)), "runtime.jl")) include(joinpath($(repr(helpers)), "metal.jl")) diff --git a/test/runtests.jl b/test/runtests.jl index 44fd5629f..a66c92ce7 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,12 +2,16 @@ using ParallelTestRunner using Test import GPUCompiler, LLVM using GPUCompiler, LLVM +# provides the ports of Julia's passes; loading it changes nothing unless they are selected +# (with the `julia_passes` preference, or per test) +using JuliaIR using SPIRV_LLVM_Backend_jll, SPIRV_LLVM_Translator_jll, SPIRV_Tools_jll using NVPTX_LLVM_Backend_jll using AMDGPU_LLVM_Backend_jll const init_code = quote using GPUCompiler, LLVM, LLVM.IR, LLVM.Build, LLVM.Passes, LLVM.ORC + using JuliaIR using SPIRV_LLVM_Backend_jll, SPIRV_LLVM_Translator_jll, SPIRV_Tools_jll using LLVMDowngrader_jll using NVPTX_LLVM_Backend_jll diff --git a/test/spirv/atomics.jl b/test/spirv/atomics.jl index f1ca7f373..431e38701 100644 --- a/test/spirv/atomics.jl +++ b/test/spirv/atomics.jl @@ -717,7 +717,7 @@ end # e.g. when reflecting, which compiles without validation. the Khronos translator can exit # the process on what it doesn't support, so try that in another process script = """ - using GPUCompiler, LLVM + using GPUCompiler, LLVM, JuliaIR include($(repr(joinpath(@__DIR__, "..", "helpers", "runtime.jl")))) include($(repr(joinpath(@__DIR__, "..", "helpers", "spirv.jl")))) import .SPIRV: Atomics diff --git a/test/spirv/imaging.jl b/test/spirv/imaging.jl index 17639fbb3..ad9f3185d 100644 --- a/test/spirv/imaging.jl +++ b/test/spirv/imaging.jl @@ -3,7 +3,7 @@ # mode in a separate process, because code coverage disables package images, and with them # imaging mode during precompilation. script = """ - using GPUCompiler, LLVM, LLVM.IR + using GPUCompiler, LLVM, LLVM.IR, JuliaIR include($(repr(joinpath(@__DIR__, "..", "helpers", "runtime.jl")))) include($(repr(joinpath(@__DIR__, "..", "helpers", "spirv.jl")))) From f2458b74d7bd064976a8eeb4114f80d690f1f4dd Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Mon, 19 Oct 2026 07:04:56 +0200 Subject: [PATCH 06/17] Test the ports of Julia's passes against the C++ passes Covers the configuration, the providers and the passes left to C++, the requirements (JuliaIR.jl loaded, opaque pointers), and, through JuliaIR.jl's ledger, that the ports run. A differential compiles a corpus of kernels (allocations, `@simd` loops, Float16, exceptions, Int128 and aggregate arguments, closures) for every target with both providers in opaque-pointer contexts and requires identical IR, modulo name counters. On Julia 1.10 and 1.11, a second set judges the switch from typed to opaque pointers that the ports need, with the C++ passes: PTX code is identical, other targets compile or fail alike. --- test/julia_passes.jl | 246 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 test/julia_passes.jl diff --git a/test/julia_passes.jl b/test/julia_passes.jl new file mode 100644 index 000000000..0d82223af --- /dev/null +++ b/test/julia_passes.jl @@ -0,0 +1,246 @@ +@testset "configuration" begin + job, _ = PTX.create_job(identity, (Int,)) + @test job.config.julia_passes === GPUCompiler.JULIA_PASSES + + cpp, _ = PTX.create_job(identity, (Int,); julia_passes=:cpp) + ports, _ = PTX.create_job(identity, (Int,); julia_passes=:ports) + @test cpp.config.julia_passes === :cpp + @test ports.config.julia_passes === :ports + # part of the identity of cached artifacts + @test cpp.config != ports.config + @test hash(cpp.config) != hash(ports.config) + # preserved by derived configs, e.g., of nested jobs + @test CompilerConfig(ports.config; toplevel=false).julia_passes === :ports + + @test_throws "Unknown julia_passes=llvm" PTX.create_job(identity, (Int,); julia_passes=:llvm) +end + +@testset "providers" begin + cpp, _ = PTX.create_job(identity, (Int,); julia_passes=:cpp) + ports, _ = PTX.create_job(identity, (Int,); julia_passes=:ports) + @test string(GPUCompiler.julia_pass(cpp, :AllocOptPass)) == "AllocOpt" + @test GPUCompiler.julia_pass(ports, :AllocOptPass) == "jl-alloc-opt" + @test_throws "Unknown Julia pass" GPUCompiler.julia_pass(ports, :NoSuchPass) + + # the passes JuliaIR.jl does not port yet, which run as the C++ pass in either mode + names = GPUCompiler.pass_ports[].names + @test sort([name for (name, port) in names if port === nothing]) == + [:FinalLowerGCPass, :JuliaLICMPass, :LowerPTLSPass, :RemoveJuliaAddrspacesPass] + @test string(GPUCompiler.julia_pass(ports, :JuliaLICMPass)) == "JuliaLICM" +end + +@testset "requirements" begin + kernel() = nothing + job, _ = PTX.create_job(kernel, (); julia_passes=:ports) + + ports = GPUCompiler.pass_ports[] + try + GPUCompiler.pass_ports[] = nothing + JuliaContext(; opaque_pointers=true) do ctx + @test_throws "requires JuliaIR.jl to be loaded" GPUCompiler.compile(:llvm, job) + end + finally + GPUCompiler.pass_ports[] = ports + end + + if VERSION < v"1.12" + JuliaContext(; opaque_pointers=false) do ctx + @test_throws "requires a context with opaque pointers" GPUCompiler.compile(:llvm, job) + end + end + + # reflection picks a suitable context + @test occursin("define", sprint(io -> GPUCompiler.code_llvm(io, job))) +end + +## differential: the C++ passes and the ports give the same IR + +# Two compilations differ in the counters codegen appends to the names of functions (and +# of the symbols derived from them in assembly), e.g. `julia_kernel_123`. +normalize_ir(ir::String) = + replace(ir, r"(? s"\1\2_\3_N") + +# The IR of a kernel. Without validation, which rejects some of the corpus on some targets +# (e.g. `Float64` on Metal) after optimization. +function compile_with(julia_passes, backend, f, tt; kwargs...) + JuliaContext(; opaque_pointers=true) do ctx + job, _ = backend.create_job(f, tt; julia_passes, validate=false, kwargs...) + ir, meta = GPUCompiler.compile(:llvm, job) + str = string(ir) + LLVM.dispose(ir) + normalize_ir(str) + end +end + +function same_ir(backend, f, tt; kwargs...) + cpp = compile_with(:cpp, backend, f, tt; kwargs...) + ports = compile_with(:ports, backend, f, tt; kwargs...) + cpp == ports && return true + @error "IR differs between the C++ passes and the ports" f tt kwargs cpp ports + return false +end + +module Corpus + mutable struct Box; x::Int; end + struct Pair2; a::Float32; b::Int128; end + + alloc(p, i) = (b = Box(i); unsafe_store!(p, b.x + 1); nothing) + ref(p, i) = (r = Ref(i); unsafe_store!(p, r[] * 2); nothing) + function simd(p, n) + s = 0.0f0 + @simd for i in 1:n + @inbounds s += unsafe_load(p, i) + end + unsafe_store!(p, s) + nothing + end + f16(p::Ptr{Float16}, x::Float16) = (unsafe_store!(p, x * x + Float16(1)); nothing) + throws(p, i) = (i > 10 && throw(BoundsError()); unsafe_store!(p, i); nothing) + i128(p::Ptr{Int128}, x::Int128) = (unsafe_store!(p, x * x); nothing) + tuple(p, t::NTuple{4,Float32}) = (unsafe_store!(p, sum(t)); nothing) + aggregate(p::Ptr{Pair2}, x::Pair2) = (unsafe_store!(p, Pair2(x.a + 1f0, x.b)); nothing) + function loop(p, n) + for i in 1:n, j in 1:n + unsafe_store!(p, unsafe_load(p, i) + j, i) + end + nothing + end + closure(p, x) = (f = y -> y + x; unsafe_store!(p, f(1)); nothing) + complex(p::Ptr{ComplexF32}, z::ComplexF32) = (unsafe_store!(p, z * z + abs(z)); nothing) + math(p::Ptr{Float64}, x::Float64) = + (unsafe_store!(p, sqrt(abs(x)) + muladd(x, x, 1.0) + copysign(x, -1.0)); nothing) + div(p::Ptr{Int}, a::Int, b::Int) = (unsafe_store!(p, Base.div(a, b) + rem(a, b)); nothing) + + # with device pointers, for targets that reject host pointers in kernels + device_alloc(p::Core.LLVMPtr{Int,1}, i) = (b = Box(i); unsafe_store!(p, b.x + 1); nothing) + device_tuple(p::Core.LLVMPtr{Float32,1}, t::NTuple{4,Float32}) = + (unsafe_store!(p, sum(t)); nothing) + function device_simd(p::Core.LLVMPtr{Float32,1}, n::Int32) + s = 0.0f0 + @simd for i in Int32(1):n + @inbounds s += unsafe_load(p, i) + end + unsafe_store!(p, s) + nothing + end + + const DEVICE_KERNELS = [ + (device_alloc, (Core.LLVMPtr{Int,1}, Int)), + (device_tuple, (Core.LLVMPtr{Float32,1}, NTuple{4,Float32})), + (device_simd, (Core.LLVMPtr{Float32,1}, Int32)), + ] + + const KERNELS = [ + (alloc, (Ptr{Int}, Int)), (ref, (Ptr{Int}, Int)), (simd, (Ptr{Float32}, Int)), + (f16, (Ptr{Float16}, Float16)), (throws, (Ptr{Int}, Int)), + (i128, (Ptr{Int128}, Int128)), (tuple, (Ptr{Float32}, NTuple{4,Float32})), + (aggregate, (Ptr{Pair2}, Pair2)), (loop, (Ptr{Int}, Int)), + (closure, (Ptr{Int}, Int)), (complex, (Ptr{ComplexF32}, ComplexF32)), + (math, (Ptr{Float64}, Float64)), (div, (Ptr{Int}, Int, Int)), + ] +end + +@testset "ports run" begin + # JuliaIR.jl records every pass it runs in its ledger + function passes_run(julia_passes, f, tt) + ledger = tempname() + old = JuliaIR.LEDGER[] + try + JuliaIR.LEDGER[] = ledger + compile_with(julia_passes, PTX, f, tt; kernel=true) + finally + JuliaIR.LEDGER[] = old + end + isfile(ledger) || return Set{String}() + Set(split(line, '\t')[3] for line in eachline(ledger)) + end + + @test isempty(passes_run(:cpp, Corpus.simd, (Ptr{Float32}, Int))) + ran = union(passes_run(:ports, Corpus.simd, (Ptr{Float32}, Int)), + passes_run(:ports, Corpus.alloc, (Ptr{Int}, Int))) + expected = ["jl-alloc-opt", "jl-demote-float16", "jl-gc-invariant-verifier", + "jl-late-lower-gc-frame", "jl-lower-simdloop", "jl-propagate-addrspaces", + "jl-remove-ni"] + VERSION < v"1.12.0-DEV.1390" && push!(expected, "jl-combine-muladd") + VERSION >= v"1.13.0-DEV.321" && push!(expected, "jl-expand-atomic-modify") + @test sort!(collect(ran)) == sort!(expected) +end + +# Compiling with both providers in opaque-pointer contexts, so that the comparison is +# independent of the typed pointers of Julia 1.10 and 1.11 (see below). +describe(kwargs) = join(("$k=$v" for (k, v) in pairs(kwargs)), " ") + +function differential_cases() + cases = Any[] + backends = Any[("PTX", PTX, (;)), ("SPIR-V", SPIRV, (; backend=:llvm)), + ("Metal", Metal, (;)), ("native", Native, (;)), ("BPF", BPF, (;))] + # see runtests.jl + LLVM.is_asserts() || push!(backends, ("GCN", GCN, (;))) + for (name, backend, kwargs) in backends, + (f, tt) in [Corpus.KERNELS; Corpus.DEVICE_KERNELS], kernel in (false, true) + # Metal kernels can't take host pointers + name == "Metal" && kernel && (f, tt) ∉ Corpus.DEVICE_KERNELS && continue + push!(cases, (name, backend, f, tt, (; kernel, kwargs...))) + end + return cases +end + +@testset "differential" begin + @testset "$name $(nameof(f)) $(describe(kwargs))" for (name, backend, f, tt, kwargs) in differential_cases() + @test same_ir(backend, f, tt; kwargs...) + end + @testset "PTX opt_level=$opt_level" for opt_level in (0, 1, 3) + @test same_ir(PTX, Corpus.simd, (Ptr{Float32}, Int); kernel=true, opt_level) + @test same_ir(PTX, Corpus.alloc, (Ptr{Int}, Int); kernel=true, opt_level) + end +end + +# The ports require opaque pointers, so with them, Julia 1.10 and 1.11 generate code in +# opaque-pointer contexts instead of typed ones. Judge that switch separately, with the C++ +# passes and down to machine code. +@static if VERSION < v"1.12" +@testset "typed and opaque pointers" begin + function asm_with(opaque_pointers, backend, f, tt; kwargs...) + JuliaContext(; opaque_pointers) do ctx + job, _ = backend.create_job(f, tt; julia_passes=:cpp, validate=false, kwargs...) + asm, meta = GPUCompiler.compile(:asm, job) + LLVM.dispose(meta.ir) + # without comments, which name basic blocks + replace(normalize_ir(String(asm)), r"//.*" => "", r"[ \t]+" => " ") + end + end + + # the back-ends that generate machine code outside of Julia's LLVM, see runtests.jl + have_ptx = NVPTX_LLVM_Backend_jll.is_available() + have_gcn = AMDGPU_LLVM_Backend_jll.is_available() && !LLVM.is_asserts() + have_spirv = SPIRV_LLVM_Backend_jll.is_available() && + SPIRV_LLVM_Translator_jll.is_available() && SPIRV_Tools_jll.is_available() + + have_ptx && @testset "PTX $(nameof(f)) kernel=$kernel" for (f, tt) in [Corpus.KERNELS; + Corpus.DEVICE_KERNELS], + kernel in (false, true) + @test asm_with(false, PTX, f, tt; kernel) == asm_with(true, PTX, f, tt; kernel) + end + + # elsewhere, the code differs in register allocation (native), debug locations (GCN) + # or the form of instructions (Metal's downgraded IR), so only require that it is + # generated. Metal and SPIR-V kernels can't take host pointers. + cases = Any[] + for (f, tt) in Corpus.KERNELS, kernel in (false, true) + push!(cases, ("native", Native, f, tt, (; kernel))) + have_gcn && push!(cases, ("GCN", GCN, f, tt, (; kernel))) + kernel || push!(cases, ("Metal", Metal, f, tt, (; kernel))) + end + for (f, tt) in Corpus.DEVICE_KERNELS, kernel in (false, true) + push!(cases, ("Metal", Metal, f, tt, (; kernel))) + if have_spirv + push!(cases, ("SPIR-V", SPIRV, f, tt, (; kernel, backend=:llvm))) + push!(cases, ("SPIR-V", SPIRV, f, tt, (; kernel, backend=:khronos))) + end + end + @testset "$name $(nameof(f)) $(describe(kwargs))" for (name, backend, f, tt, kwargs) in cases + @test asm_with(false, backend, f, tt; kwargs...) isa String + @test asm_with(true, backend, f, tt; kwargs...) isa String + end +end +end From 44504cb5fb1f520b5381d411f347eb6b75590e53 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Mon, 19 Oct 2026 07:09:04 +0200 Subject: [PATCH 07/17] Check the arity in argument classification `classify_arguments` treats `Type{T}` arguments as ghosts, which matches codegen only when the signature keys them as `Core.TypeEgal`, as GPUCompiler's `methodinstance` does. A job built from another MethodInstance could get a boxed `Type{T}` parameter, misaligning every later argument without an error. Before optimization the number of parameters is known, so check it. --- src/irgen.jl | 8 ++++++++ test/native.jl | 13 +++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/irgen.jl b/src/irgen.jl index 679e8b23a..a07d59c69 100644 --- a/src/irgen.jl +++ b/src/irgen.jl @@ -620,6 +620,14 @@ function classify_arguments(@nospecialize(job::CompilerJob), codegen_ft::LLVM.Fu codegen_i += 1 end + # codegen omits ghost arguments, and `Type{T}` ones only if the signature uses + # `Core.TypeEgal` keys like GPUCompiler's `methodinstance` does; otherwise the arguments + # would be classified against the wrong parameters. after optimization, back-ends may + # have added parameters. + if !post_optimization && codegen_i - 1 != length(codegen_types) + error("Cannot classify the arguments of $(job.source): the signature has $(codegen_i - 1) non-ghost arguments, but the function generated for it has $(length(codegen_types)) parameters") + end + return args end diff --git a/test/native.jl b/test/native.jl index 8bf1008a6..92abc4a32 100644 --- a/test/native.jl +++ b/test/native.jl @@ -346,6 +346,19 @@ end @test Base.isdispatchtuple(mi.specTypes) end +@testset "argument classification checks the arity" begin + # e.g. codegen passing a `Type{T}` argument that the signature does not key as + # `Core.TypeEgal`, which `classify_arguments` treats as a ghost + job, _ = Native.create_job(identity, (Int,)) + JuliaContext() do ctx + ft = LLVM.FunctionType(LLVM.VoidType(), [LLVM.Int64Type(), LLVM.Int64Type()]) + @test_throws "the function generated for it has 2 parameters" GPUCompiler.classify_arguments(job, ft) + ft = LLVM.FunctionType(LLVM.VoidType(), [LLVM.Int64Type()]) + args = GPUCompiler.classify_arguments(job, ft) + @test [arg.idx for arg in args] == [nothing, 1] + end +end + # on 1.10, `methodinstance` is the generic, allocating lookup VERSION >= v"1.11.0-DEV.1552" && @testset "method instance lookups don't allocate" begin # back-ends look up the method instance on every kernel launch From 5f9638be6b784d1e9dfea743e5c6b39095b71867 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Mon, 19 Oct 2026 07:36:06 +0200 Subject: [PATCH 08/17] Use the C++ passes when precompiling without JuliaIR.jl With the preference selecting the ports, precompilation workloads of packages that don't depend on JuliaIR.jl (GPUCompiler's own, or a back-end's) would fail, as they compile in a process without it. There, configurations now default to the C++ passes. That is part of the configuration, so whatever such a workload caches is not used for configurations selecting the ports. --- src/driver.jl | 8 ++++---- src/interface.jl | 3 ++- src/julia_passes.jl | 12 ++++++++++++ src/precompile.jl | 6 ++---- test/julia_passes.jl | 26 ++++++++++++++++++++++++++ test/native/precompile.jl | 4 +++- 6 files changed, 49 insertions(+), 10 deletions(-) diff --git a/src/driver.jl b/src/driver.jl index 116d23642..599235a89 100644 --- a/src/driver.jl +++ b/src/driver.jl @@ -19,13 +19,13 @@ export JuliaContext # unique context on all other versions. Once we only support Julia 1.9, we'll deprecate # this helper to a regular `Context()` call. # -# By default, the context uses opaque pointers if the `julia_passes` preference selects the -# ports of Julia's passes, which require them, and LLVM's default otherwise. -function JuliaContext(; opaque_pointers=context_opaque_pointers(JULIA_PASSES)) +# By default, the context uses opaque pointers if configurations select the ports of Julia's +# passes by default, which require them, and LLVM's default otherwise. +function JuliaContext(; opaque_pointers=context_opaque_pointers(default_julia_passes())) # XXX: remove ThreadSafeContext(; opaque_pointers) end -function JuliaContext(f; opaque_pointers=context_opaque_pointers(JULIA_PASSES)) +function JuliaContext(f; opaque_pointers=context_opaque_pointers(default_julia_passes())) ThreadSafeContext(; opaque_pointers) do ts_ctx # for now, also activate the underlying context # XXX: this is wrong; we can't expose the underlying LLVM context, but should diff --git a/src/interface.jl b/src/interface.jl index 657c6e680..a621b311a 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -213,7 +213,8 @@ struct CompilerConfig{T,P} always_inline=false, opt_level=2, debug_level=Base.JLOptions().debug_level, optimize=toplevel, libraries=toplevel, cleanup=toplevel, validate=toplevel, - strip=false, julia_passes=JULIA_PASSES, only_entry=false) + strip=false, julia_passes=default_julia_passes(), + only_entry=false) if entry_abi ∉ (:specfunc, :func) error("Unknown entry_abi=$entry_abi") end diff --git a/src/julia_passes.jl b/src/julia_passes.jl index 65ad091f4..1d2edcd37 100644 --- a/src/julia_passes.jl +++ b/src/julia_passes.jl @@ -15,6 +15,18 @@ struct PassPorts end const pass_ports = Ref{Union{Nothing,PassPorts}}(nothing) +# The default of `CompilerConfig`'s `julia_passes`: the preference, except while +# precompiling without JuliaIR.jl loaded, e.g., in the workload of a package that does not +# depend on it. That uses the C++ passes; since the setting is part of the configuration, +# what it caches is not used for configurations that select the ports. +function default_julia_passes() + if JULIA_PASSES === :ports && pass_ports[] === nothing && + ccall(:jl_generating_output, Cint, ()) == 1 + return :cpp + end + return JULIA_PASSES +end + function check_julia_passes(@nospecialize(job::CompilerJob)) job.config.julia_passes === :ports || return pass_ports[] === nothing && diff --git a/src/precompile.jl b/src/precompile.jl index b7056732d..9f35c1281 100644 --- a/src/precompile.jl +++ b/src/precompile.jl @@ -32,12 +32,10 @@ import REPL params = precompile_module.DummyCompilerParams() # XXX: on Windows, compiling the GPU runtime leaks GPU code in the native cache, # so prevent building the runtime library (see JuliaGPU/GPUCompiler.jl#601) - # JuliaIR.jl is not loaded here, so precompile the C++ passes even if the - # `julia_passes` preference selects the ports - config = CompilerConfig(target, params; libraries=false, julia_passes=:cpp) + config = CompilerConfig(target, params; libraries=false) job = CompilerJob(source, config) - JuliaContext(; opaque_pointers=nothing) do ctx + JuliaContext() do ctx _, meta = GPUCompiler.compile(:asm, job) dispose(meta.ir) end diff --git a/test/julia_passes.jl b/test/julia_passes.jl index 0d82223af..7212d42e9 100644 --- a/test/julia_passes.jl +++ b/test/julia_passes.jl @@ -53,6 +53,32 @@ end @test occursin("define", sprint(io -> GPUCompiler.code_llvm(io, job))) end +# Packages that do not depend on JuliaIR.jl may compile code while precompiling, e.g., in +# their workload, in a process without it. +VERSION >= v"1.11" && precompile_test_harness("precompilation without JuliaIR.jl") do load_path + create_standalone(load_path, "NativeCompiler", "native.jl") + write(joinpath(load_path, "PortsWorkload.jl"), :( + module PortsWorkload + import NativeCompiler + using NativeCompiler.GPUCompiler + + kernel(x) = x + 1 + const julia_passes = let + job, _ = NativeCompiler.Native.create_job(kernel, (Int,)) + JuliaContext() do ctx + GPUCompiler.compile(:llvm, job) + end + job.config.julia_passes + end + end) |> string) + Base.compilecache(Base.PkgId("PortsWorkload")) + + # such compilations use the C++ passes; that is part of their configuration, so + # results cached for them are not used for configurations with the ports + mod = Base.require(Base.PkgId("PortsWorkload")) + @test Base.invokelatest(getglobal, mod, :julia_passes) === :cpp +end + ## differential: the C++ passes and the ports give the same IR # Two compilations differ in the counters codegen appends to the names of functions (and diff --git a/test/native/precompile.jl b/test/native/precompile.jl index 675c9fa0b..194bf6fb1 100644 --- a/test/native/precompile.jl +++ b/test/native/precompile.jl @@ -110,8 +110,10 @@ precompile_test_harness("Inference caching") do load_path using NativeBackend + # precompilation without JuliaIR.jl uses Julia's C++ passes, also when the + # preference selects the ports persistent_job, _ = NativeCompiler.Native.create_job( - NativeBackend.persistent_kernel, (Int,); relocations=:patch) + NativeBackend.persistent_kernel, (Int,); relocations=:patch, julia_passes=:cpp) persistent_res = GPUCompiler.cached_results(NativeBackend.Results, persistent_job) @test persistent_res !== nothing if GPUCompiler.supports_relocatable_ir() From 3a37949be9e2c609970a157e21349b838d49de3a Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Mon, 19 Oct 2026 07:09:15 +0200 Subject: [PATCH 09/17] Document the choice between Julia's passes and their ports Explains the `julia_passes` setting and its preference, what the ports require, and how to run the test suite with them. --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index fc8988a23..def8d1cbe 100644 --- a/README.md +++ b/README.md @@ -51,3 +51,28 @@ last commit of a pull request: Back-ends are named `cuda`, `amdgpu`, `metal`, `oneapi` and `opencl` (all lowercase), or by their target: `ptx` for CUDA.jl, `gcn` for AMDGPU.jl, and `spirv` for both oneAPI.jl and OpenCL.jl. + + +## Julia's LLVM passes + +GPUCompiler's optimization pipeline includes several of the LLVM passes of Julia's code +generator, e.g., to lower GC intrinsics or to optimize allocations. These normally come from +Julia's C++ implementation, but can also be taken from [JuliaIR.jl](https://github.com/JuliaLLVM/JuliaIR.jl), +which ports them to Julia. This is selected per compilation with the `julia_passes` keyword +argument of `CompilerConfig` (`:cpp` or `:ports`), whose default comes from a preference: + +```julia +using Preferences, GPUCompiler +set_preferences!(GPUCompiler, "julia_passes" => "ports") +``` + +The preference takes effect after restarting Julia. While precompiling packages that do not +load JuliaIR.jl, configurations keep using the C++ passes. + +The ports require JuliaIR.jl to be loaded (`using JuliaIR`), and opaque pointers: on Julia +1.10 and 1.11, contexts created by `JuliaContext()` use them when the preference selects the +ports, and other contexts must be created with `JuliaContext(; opaque_pointers=true)`. Passes +that JuliaIR.jl does not port yet run as Julia's C++ pass in both modes; the JuliaIR.jl +extension (`ext/JuliaIRExt.jl`) lists them. + +To run the test suite with the ports, set the preference in `test/LocalPreferences.toml`. From 2a8c76892168c6b1bd8d57c4025b3519884f1f60 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Mon, 19 Oct 2026 11:07:35 +0200 Subject: [PATCH 10/17] Skip the typed-pointer comparison when Julia uses opaque pointers With `--opaque-pointers` on Julia 1.10 and 1.11, Julia's code generator does not support typed-pointer contexts, so the code generated in one differs and the comparison of typed with opaque pointers fails. Julia then never generates typed IR, so there is nothing to compare. --- test/julia_passes.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/julia_passes.jl b/test/julia_passes.jl index 7212d42e9..078613a99 100644 --- a/test/julia_passes.jl +++ b/test/julia_passes.jl @@ -223,8 +223,9 @@ end # The ports require opaque pointers, so with them, Julia 1.10 and 1.11 generate code in # opaque-pointer contexts instead of typed ones. Judge that switch separately, with the C++ -# passes and down to machine code. -@static if VERSION < v"1.12" +# passes and down to machine code. Not when Julia itself uses opaque pointers +# (`--opaque-pointers`), as its code generator then does not support typed contexts. +@static if VERSION < v"1.12" && Context(LLVM.supports_typed_pointers) @testset "typed and opaque pointers" begin function asm_with(opaque_pointers, backend, f, tt; kwargs...) JuliaContext(; opaque_pointers) do ctx From 12aca4a1bf65778234b0eaeae57617ba704ddd54 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Mon, 19 Oct 2026 11:07:42 +0200 Subject: [PATCH 11/17] Load JuliaIR.jl when the preference selects the ports Packages building on GPUCompiler do not load JuliaIR.jl, so with the julia_passes preference set to "ports", their compilations failed for want of the ports unless the user loaded it. When the preference selects the ports, GPUCompiler now loads JuliaIR.jl at initialization if the environment provides it (not while precompiling), and the extension registers the ports once GPUCompiler has loaded. This also makes the preference usable in a package's test suite, by adding JuliaIR.jl to its test environment. --- README.md | 12 +++++++----- src/GPUCompiler.jl | 3 +++ src/julia_passes.jl | 15 +++++++++++++++ test/julia_passes.jl | 8 ++++++++ 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index def8d1cbe..ee2a468b7 100644 --- a/README.md +++ b/README.md @@ -69,10 +69,12 @@ set_preferences!(GPUCompiler, "julia_passes" => "ports") The preference takes effect after restarting Julia. While precompiling packages that do not load JuliaIR.jl, configurations keep using the C++ passes. -The ports require JuliaIR.jl to be loaded (`using JuliaIR`), and opaque pointers: on Julia -1.10 and 1.11, contexts created by `JuliaContext()` use them when the preference selects the -ports, and other contexts must be created with `JuliaContext(; opaque_pointers=true)`. Passes -that JuliaIR.jl does not port yet run as Julia's C++ pass in both modes; the JuliaIR.jl -extension (`ext/JuliaIRExt.jl`) lists them. +The ports require JuliaIR.jl to be loaded. When the preference selects them, loading +GPUCompiler also loads JuliaIR.jl if the active environment provides it; otherwise, load it +with `using JuliaIR`. They also require opaque pointers: on Julia 1.10 and 1.11, contexts +created by `JuliaContext()` use them when the preference selects the ports, and other +contexts must be created with `JuliaContext(; opaque_pointers=true)`. Passes that JuliaIR.jl +does not port yet run as Julia's C++ pass in both modes; the JuliaIR.jl extension +(`ext/JuliaIRExt.jl`) lists them. To run the test suite with the ports, set the preference in `test/LocalPreferences.toml`. diff --git a/src/GPUCompiler.jl b/src/GPUCompiler.jl index bde9e2afb..07c1d7dc4 100644 --- a/src/GPUCompiler.jl +++ b/src/GPUCompiler.jl @@ -105,6 +105,9 @@ function __init__() Tracy.@register_tracepoints() end register_deferred_codegen() + + # last, as it loads other packages + load_julia_passes() end end # module diff --git a/src/julia_passes.jl b/src/julia_passes.jl index 1d2edcd37..1961bec65 100644 --- a/src/julia_passes.jl +++ b/src/julia_passes.jl @@ -27,6 +27,21 @@ function default_julia_passes() return JULIA_PASSES end +const JULIAIR = Base.PkgId(Base.UUID("3913dba9-7ba9-44aa-bf76-1cfa3b3073b1"), "JuliaIR") + +# When the preference selects the ports, load JuliaIR.jl if the environment provides it, +# so that code using GPUCompiler through other packages (e.g. their test suites) gets the +# ports without loading JuliaIR.jl itself. Its extension then registers the ports, once +# GPUCompiler has finished loading. +function load_julia_passes() + JULIA_PASSES === :ports || return + ccall(:jl_generating_output, Cint, ()) == 1 && return + haskey(Base.loaded_modules, JULIAIR) && return + Base.locate_package(JULIAIR) === nothing && return + Base.require(JULIAIR) + return +end + function check_julia_passes(@nospecialize(job::CompilerJob)) job.config.julia_passes === :ports || return pass_ports[] === nothing && diff --git a/test/julia_passes.jl b/test/julia_passes.jl index 078613a99..fe1c225c2 100644 --- a/test/julia_passes.jl +++ b/test/julia_passes.jl @@ -53,6 +53,14 @@ end @test occursin("define", sprint(io -> GPUCompiler.code_llvm(io, job))) end +@testset "loading" begin + # with the preference selecting the ports, loading GPUCompiler loads JuliaIR.jl from + # the environment, and the extension registers the ports + code = "import GPUCompiler; print(GPUCompiler.pass_ports[] !== nothing)" + out = read(`$(Base.julia_cmd()) --startup-file=no --project=$(Base.active_project()) -e $code`, String) + @test out == string(GPUCompiler.JULIA_PASSES === :ports) +end + # Packages that do not depend on JuliaIR.jl may compile code while precompiling, e.g., in # their workload, in a process without it. VERSION >= v"1.11" && precompile_test_harness("precompilation without JuliaIR.jl") do load_path From 919ba4fd53d2ef0620357220aad06c773b56c722 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Mon, 19 Oct 2026 13:58:54 +0200 Subject: [PATCH 12/17] Use the ports of all of Julia's passes JuliaIR.jl now ports RemoveJuliaAddrspaces, FinalLowerGC, LowerPTLS and JuliaLICM too, so with julia_passes=:ports every one of Julia's passes in the pipeline is a port and none falls back to the C++ pass. The ledger test now also expects JuliaLICM and RemoveJuliaAddrspaces, and FinalLowerGC and LowerPTLS for a target that uses Julia's runtime. [only cuda oneapi amdgpu metal opencl] --- README.md | 4 +--- ext/JuliaIRExt.jl | 17 +++++++---------- src/julia_passes.jl | 10 +++------- test/julia_passes.jl | 24 +++++++++++++++--------- 4 files changed, 26 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index ee2a468b7..08731862a 100644 --- a/README.md +++ b/README.md @@ -73,8 +73,6 @@ The ports require JuliaIR.jl to be loaded. When the preference selects them, loa GPUCompiler also loads JuliaIR.jl if the active environment provides it; otherwise, load it with `using JuliaIR`. They also require opaque pointers: on Julia 1.10 and 1.11, contexts created by `JuliaContext()` use them when the preference selects the ports, and other -contexts must be created with `JuliaContext(; opaque_pointers=true)`. Passes that JuliaIR.jl -does not port yet run as Julia's C++ pass in both modes; the JuliaIR.jl extension -(`ext/JuliaIRExt.jl`) lists them. +contexts must be created with `JuliaContext(; opaque_pointers=true)`. To run the test suite with the ports, set the preference in `test/LocalPreferences.toml`. diff --git a/ext/JuliaIRExt.jl b/ext/JuliaIRExt.jl index 7095ec8c5..63c7f31e3 100644 --- a/ext/JuliaIRExt.jl +++ b/ext/JuliaIRExt.jl @@ -6,31 +6,28 @@ import GPUCompiler import JuliaIR # The port of each of Julia's passes in GPUCompiler's pipeline, by the name of the -# `LLVM.Interop` constructor of the C++ pass. The ones JuliaIR.jl does not port yet are -# `nothing`, and run as the C++ pass. -const PORTS = Dict{Symbol,Union{Nothing,String}}( +# `LLVM.Interop` constructor of the C++ pass. +const PORTS = Dict{Symbol,String}( :AllocOptPass => "jl-alloc-opt", :CombineMulAddPass => "jl-combine-muladd", :DemoteFloat16Pass => "jl-demote-float16", :ExpandAtomicModifyPass => "jl-expand-atomic-modify", + :FinalLowerGCPass => "jl-final-lower-gc", :GCInvariantVerifierPass => "jl-gc-invariant-verifier", + :JuliaLICMPass => "jl-licm", :LateLowerGCPass => "jl-late-lower-gc-frame", :LowerExcHandlersPass => "jl-lower-exc-handlers", + :LowerPTLSPass => "jl-lower-ptls", :LowerSIMDLoopPass => "jl-lower-simdloop", :PropagateJuliaAddrspacesPass => "jl-propagate-addrspaces", + :RemoveJuliaAddrspacesPass => "jl-remove-addrspaces", :RemoveNIPass => "jl-remove-ni", - - :FinalLowerGCPass => nothing, - :JuliaLICMPass => nothing, - :LowerPTLSPass => nothing, - :RemoveJuliaAddrspacesPass => nothing, ) function __init__() available = JuliaIR.passes() for port in values(PORTS) - port === nothing || port in available || - error("JuliaIR.jl does not provide the pass $port") + port in available || error("JuliaIR.jl does not provide the pass $port") end GPUCompiler.pass_ports[] = GPUCompiler.PassPorts(pb -> JuliaIR.register!(pb), PORTS) end diff --git a/src/julia_passes.jl b/src/julia_passes.jl index 1961bec65..1bb3a0cae 100644 --- a/src/julia_passes.jl +++ b/src/julia_passes.jl @@ -8,10 +8,10 @@ JULIA_PASSES in (:cpp, :ports) || # The ports of Julia's passes, supplied by the JuliaIRExt extension: a function registering # them with a `PassBuilder`, and the pipeline name of the port of each of Julia's passes by -# the name of the `LLVM.Interop` constructor of the C++ pass (`nothing` if not ported). +# the name of the `LLVM.Interop` constructor of the C++ pass. struct PassPorts register!::Function - names::Dict{Symbol,Union{Nothing,String}} + names::Dict{Symbol,String} end const pass_ports = Ref{Union{Nothing,PassPorts}}(nothing) @@ -61,16 +61,12 @@ context_opaque_pointers(julia_passes::Symbol) = julia_passes === :ports ? true : The Julia pass `name` (the name of its constructor in `LLVM.Interop`, e.g. `:AllocOptPass`) for use in the optimization pipeline of `job`: Julia's C++ pass, or its port in JuliaIR.jl if `job.config.julia_passes == :ports`. - -Passes that JuliaIR.jl does not port yet run as the C++ pass in either mode; the -JuliaIRExt extension lists them. """ function julia_pass(@nospecialize(job::CompilerJob), name::Symbol) if job.config.julia_passes === :ports ports = pass_ports[]::PassPorts haskey(ports.names, name) || error("Unknown Julia pass $name") - port = ports.names[name] - port === nothing || return port + return ports.names[name] end return getfield(LLVM.Interop, name)() end diff --git a/test/julia_passes.jl b/test/julia_passes.jl index fe1c225c2..4b0bd9cfa 100644 --- a/test/julia_passes.jl +++ b/test/julia_passes.jl @@ -22,11 +22,12 @@ end @test GPUCompiler.julia_pass(ports, :AllocOptPass) == "jl-alloc-opt" @test_throws "Unknown Julia pass" GPUCompiler.julia_pass(ports, :NoSuchPass) - # the passes JuliaIR.jl does not port yet, which run as the C++ pass in either mode - names = GPUCompiler.pass_ports[].names - @test sort([name for (name, port) in names if port === nothing]) == - [:FinalLowerGCPass, :JuliaLICMPass, :LowerPTLSPass, :RemoveJuliaAddrspacesPass] - @test string(GPUCompiler.julia_pass(ports, :JuliaLICMPass)) == "JuliaLICM" + # every one of Julia's passes in the pipeline has a port + @test GPUCompiler.julia_pass(ports, :JuliaLICMPass) == "jl-licm" + @test GPUCompiler.julia_pass(ports, :FinalLowerGCPass) == "jl-final-lower-gc" + @test GPUCompiler.julia_pass(ports, :LowerPTLSPass) == "jl-lower-ptls" + @test GPUCompiler.julia_pass(ports, :RemoveJuliaAddrspacesPass) == "jl-remove-addrspaces" + @test length(GPUCompiler.pass_ports[].names) == 14 end @testset "requirements" begin @@ -176,12 +177,12 @@ end @testset "ports run" begin # JuliaIR.jl records every pass it runs in its ledger - function passes_run(julia_passes, f, tt) + function passes_run(julia_passes, f, tt; backend=PTX, kernel=true) ledger = tempname() old = JuliaIR.LEDGER[] try JuliaIR.LEDGER[] = ledger - compile_with(julia_passes, PTX, f, tt; kernel=true) + compile_with(julia_passes, backend, f, tt; kernel) finally JuliaIR.LEDGER[] = old end @@ -193,11 +194,16 @@ end ran = union(passes_run(:ports, Corpus.simd, (Ptr{Float32}, Int)), passes_run(:ports, Corpus.alloc, (Ptr{Int}, Int))) expected = ["jl-alloc-opt", "jl-demote-float16", "jl-gc-invariant-verifier", - "jl-late-lower-gc-frame", "jl-lower-simdloop", "jl-propagate-addrspaces", - "jl-remove-ni"] + "jl-late-lower-gc-frame", "jl-licm", "jl-lower-simdloop", + "jl-propagate-addrspaces", "jl-remove-addrspaces", "jl-remove-ni"] VERSION < v"1.12.0-DEV.1390" && push!(expected, "jl-combine-muladd") VERSION >= v"1.13.0-DEV.321" && push!(expected, "jl-expand-atomic-modify") @test sort!(collect(ran)) == sort!(expected) + + # targets that use Julia's runtime also lower the GC frame and the GC stack getters + ran = passes_run(:ports, Corpus.alloc, (Ptr{Int}, Int); backend=Native, kernel=false) + @test "jl-final-lower-gc" in ran + @test "jl-lower-ptls" in ran end # Compiling with both providers in opaque-pointer contexts, so that the comparison is From 02f48c76ae0e24cded75f053ed50a80af55cf1a5 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Mon, 19 Oct 2026 14:36:38 +0200 Subject: [PATCH 13/17] Give the precompiled package of the ports' workload test its own name Both test/native/precompile.jl and the test of precompilation without JuliaIR.jl created a package named NativeCompiler. When one ran after the other on the same worker, the cache of the second test's package depended on a NativeCompiler other than the loaded one, so it was loaded from source in the test process: the workload then reported the ports, and native/precompile found results and inferred code that should only exist in the precompilation process. --- test/julia_passes.jl | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/julia_passes.jl b/test/julia_passes.jl index 4b0bd9cfa..464c1dd19 100644 --- a/test/julia_passes.jl +++ b/test/julia_passes.jl @@ -65,15 +65,17 @@ end # Packages that do not depend on JuliaIR.jl may compile code while precompiling, e.g., in # their workload, in a process without it. VERSION >= v"1.11" && precompile_test_harness("precompilation without JuliaIR.jl") do load_path - create_standalone(load_path, "NativeCompiler", "native.jl") + # not named like the package of test/native/precompile.jl, which a worker may have + # loaded already, so that the cache of the workload would not match it + create_standalone(load_path, "PortsCompiler", "native.jl") write(joinpath(load_path, "PortsWorkload.jl"), :( module PortsWorkload - import NativeCompiler - using NativeCompiler.GPUCompiler + import PortsCompiler + using PortsCompiler.GPUCompiler kernel(x) = x + 1 const julia_passes = let - job, _ = NativeCompiler.Native.create_job(kernel, (Int,)) + job, _ = PortsCompiler.Native.create_job(kernel, (Int,)) JuliaContext() do ctx GPUCompiler.compile(:llvm, job) end From 0e15b8e4b22588d2aeb3e5f61f00653157321456 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Mon, 19 Oct 2026 14:40:26 +0200 Subject: [PATCH 14/17] Allow typed and opaque pointers to copy padding differently On Julia 1.10 and 1.11 on AArch64, where Julia aligns Int128 to 16 bytes, the PTX of the aggregate kernel taking its argument by reference differs between typed and opaque pointers: with opaque pointers the argument's padding is copied too. Only the padding bytes of the stored value differ, so there only require the code to be generated. --- test/julia_passes.jl | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/julia_passes.jl b/test/julia_passes.jl index 464c1dd19..3eb7cb055 100644 --- a/test/julia_passes.jl +++ b/test/julia_passes.jl @@ -262,7 +262,15 @@ end have_ptx && @testset "PTX $(nameof(f)) kernel=$kernel" for (f, tt) in [Corpus.KERNELS; Corpus.DEVICE_KERNELS], kernel in (false, true) - @test asm_with(false, PTX, f, tt; kernel) == asm_with(true, PTX, f, tt; kernel) + if f === Corpus.aggregate && !kernel && Base.datatype_alignment(Int128) > 8 + # Where Julia aligns Int128 to 16 bytes before 1.12 (AArch64, not x86), the + # padding of the aggregate argument, passed by reference, is copied with + # opaque pointers but not with typed ones; only padding bytes differ. + @test asm_with(false, PTX, f, tt; kernel) isa String + @test asm_with(true, PTX, f, tt; kernel) isa String + else + @test asm_with(false, PTX, f, tt; kernel) == asm_with(true, PTX, f, tt; kernel) + end end # elsewhere, the code differs in register allocation (native), debug locations (GCN) From 634f438e3e033aa3ca95a02cdb770c2fc0c2bcf9 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Mon, 19 Oct 2026 16:43:26 +0200 Subject: [PATCH 15/17] Only compare GCN code when the in-process LLVM has the AMDGPU target Julia's LLVM is built without the AMDGPU target on macOS, so GCNCompilerTarget has no datalayout there and Julia emits allocas in addrspace(0). Since LLVM 23 the verifier rejects those for an amdgcn triple, aborting the julia_passes test on macOS with Julia nightly. The GCN test suite itself is skipped on macOS already. --- test/julia_passes.jl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/julia_passes.jl b/test/julia_passes.jl index 3eb7cb055..2e8e39ad7 100644 --- a/test/julia_passes.jl +++ b/test/julia_passes.jl @@ -216,8 +216,12 @@ function differential_cases() cases = Any[] backends = Any[("PTX", PTX, (;)), ("SPIR-V", SPIRV, (; backend=:llvm)), ("Metal", Metal, (;)), ("native", Native, (;)), ("BPF", BPF, (;))] - # see runtests.jl - LLVM.is_asserts() || push!(backends, ("GCN", GCN, (;))) + # see runtests.jl. the GCN datalayout comes from the in-process AMDGPU target, which + # Julia's LLVM lacks on macOS; without it, allocas stay in addrspace(0), which LLVM 23's + # verifier rejects for amdgcn. + if !LLVM.is_asserts() && :AMDGPU in LLVM.backends() + push!(backends, ("GCN", GCN, (;))) + end for (name, backend, kwargs) in backends, (f, tt) in [Corpus.KERNELS; Corpus.DEVICE_KERNELS], kernel in (false, true) # Metal kernels can't take host pointers From 504cead4ad40e6ac1bf60d44b9b301be81208dd4 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Mon, 19 Oct 2026 19:33:18 +0200 Subject: [PATCH 16/17] Lower cancellation points on Julia 1.14 for targets using its runtime From 1.14, `Base.@cancel_check` emits `julia.cancellation_point()` calls, which Julia lowers with CancellationLowering right before LateLowerGCFrame (JuliaLang/julia#62604). GPUCompiler never ran that pass, so native jobs using Julia's runtime kept the calls unlowered. Run it there, as the port with `julia_passes=:ports`, or else by its pipeline name since LLVM.jl has no constructor for it. Other targets have no task to deliver a cancellation to. --- ext/JuliaIRExt.jl | 4 +++- src/julia_passes.jl | 13 +++++++++++-- src/optim.jl | 6 ++++++ test/julia_passes.jl | 7 ++++++- test/native.jl | 17 +++++++++++++++++ 5 files changed, 43 insertions(+), 4 deletions(-) diff --git a/ext/JuliaIRExt.jl b/ext/JuliaIRExt.jl index 63c7f31e3..ba410e936 100644 --- a/ext/JuliaIRExt.jl +++ b/ext/JuliaIRExt.jl @@ -6,9 +6,11 @@ import GPUCompiler import JuliaIR # The port of each of Julia's passes in GPUCompiler's pipeline, by the name of the -# `LLVM.Interop` constructor of the C++ pass. +# `LLVM.Interop` constructor of the C++ pass (or the name it would have, see +# `GPUCompiler.CPP_PASS_NAMES`). const PORTS = Dict{Symbol,String}( :AllocOptPass => "jl-alloc-opt", + :CancellationLoweringPass => "jl-cancellation-lowering", :CombineMulAddPass => "jl-combine-muladd", :DemoteFloat16Pass => "jl-demote-float16", :ExpandAtomicModifyPass => "jl-expand-atomic-modify", diff --git a/src/julia_passes.jl b/src/julia_passes.jl index 1bb3a0cae..c96a3e1e3 100644 --- a/src/julia_passes.jl +++ b/src/julia_passes.jl @@ -60,7 +60,8 @@ context_opaque_pointers(julia_passes::Symbol) = julia_passes === :ports ? true : The Julia pass `name` (the name of its constructor in `LLVM.Interop`, e.g. `:AllocOptPass`) for use in the optimization pipeline of `job`: Julia's C++ pass, or its -port in JuliaIR.jl if `job.config.julia_passes == :ports`. +port in JuliaIR.jl if `job.config.julia_passes == :ports`. Passes that `LLVM.Interop` has +no constructor for are named as if it had one, e.g. `:CancellationLoweringPass`. """ function julia_pass(@nospecialize(job::CompilerJob), name::Symbol) if job.config.julia_passes === :ports @@ -68,9 +69,17 @@ function julia_pass(@nospecialize(job::CompilerJob), name::Symbol) haskey(ports.names, name) || error("Unknown Julia pass $name") return ports.names[name] end - return getfield(LLVM.Interop, name)() + isdefined(LLVM.Interop, name) && return getfield(LLVM.Interop, name)() + haskey(CPP_PASS_NAMES, name) || error("Unknown Julia pass $name") + return CPP_PASS_NAMES[name] end +# Julia's passes that `LLVM.Interop` has no constructor for: their names in the pipelines +# of Julia's code generator, which registers them with every `PassBuilder`. +const CPP_PASS_NAMES = Dict{Symbol,String}( + :CancellationLoweringPass => "CancellationLowering", +) + # make the ports usable by name in the pipeline of `pb` function register_julia_passes!(pb::PassBuilder, @nospecialize(job::CompilerJob)) job.config.julia_passes === :ports || return diff --git a/src/optim.jl b/src/optim.jl index 68efe7b2b..bbc8c206d 100644 --- a/src/optim.jl +++ b/src/optim.jl @@ -368,6 +368,12 @@ function buildIntrinsicLoweringPipeline(mpm, @nospecialize(job::CompilerJob), op if uses_julia_runtime(job) && VERSION < v"1.13.0-DEV.36" add!(fpm, julia_pass(job, :LowerExcHandlersPass)) end + # lower the cancellation points of `Base.@cancel_check` to `setjmp`s into the task, + # before GC lowering (JuliaLang/julia#62604). Only Julia's runtime can deliver a + # cancellation. + if uses_julia_runtime(job) && VERSION >= v"1.14.0-DEV.2872" + add!(fpm, julia_pass(job, :CancellationLoweringPass)) + end add!(fpm, julia_pass(job, :GCInvariantVerifierPass)) add!(fpm, julia_pass(job, :LateLowerGCPass)) # FinalLowerGCPass moved from a module pass to a function pass in diff --git a/test/julia_passes.jl b/test/julia_passes.jl index 2e8e39ad7..e45a1481c 100644 --- a/test/julia_passes.jl +++ b/test/julia_passes.jl @@ -21,13 +21,17 @@ end @test string(GPUCompiler.julia_pass(cpp, :AllocOptPass)) == "AllocOpt" @test GPUCompiler.julia_pass(ports, :AllocOptPass) == "jl-alloc-opt" @test_throws "Unknown Julia pass" GPUCompiler.julia_pass(ports, :NoSuchPass) + @test_throws "Unknown Julia pass" GPUCompiler.julia_pass(cpp, :NoSuchPass) + # passes that LLVM.jl has no constructor for, by their pipeline name + @test GPUCompiler.julia_pass(cpp, :CancellationLoweringPass) == "CancellationLowering" # every one of Julia's passes in the pipeline has a port @test GPUCompiler.julia_pass(ports, :JuliaLICMPass) == "jl-licm" @test GPUCompiler.julia_pass(ports, :FinalLowerGCPass) == "jl-final-lower-gc" @test GPUCompiler.julia_pass(ports, :LowerPTLSPass) == "jl-lower-ptls" @test GPUCompiler.julia_pass(ports, :RemoveJuliaAddrspacesPass) == "jl-remove-addrspaces" - @test length(GPUCompiler.pass_ports[].names) == 14 + @test GPUCompiler.julia_pass(ports, :CancellationLoweringPass) == "jl-cancellation-lowering" + @test length(GPUCompiler.pass_ports[].names) == 15 end @testset "requirements" begin @@ -206,6 +210,7 @@ end ran = passes_run(:ports, Corpus.alloc, (Ptr{Int}, Int); backend=Native, kernel=false) @test "jl-final-lower-gc" in ran @test "jl-lower-ptls" in ran + @test ("jl-cancellation-lowering" in ran) == (VERSION >= v"1.14.0-DEV.2872") end # Compiling with both providers in opaque-pointer contexts, so that the comparison is diff --git a/test/native.jl b/test/native.jl index 92abc4a32..64a342b74 100644 --- a/test/native.jl +++ b/test/native.jl @@ -1056,6 +1056,23 @@ end end end +@static if VERSION >= v"1.14.0-DEV.2872" +@testset "cancellation points" begin + # from Julia 1.14, `@cancel_check` emits a `julia.cancellation_point()` call, which + # has to be lowered to a `setjmp` into the task (JuliaLang/julia#62604) + mod = @eval module $(gensym()) + f(x) = (Base.@cancel_check; x + 1) + end + + @testset "$julia_passes" for julia_passes in (:cpp, :ports) + @test @filecheck implicit_check_not=["julia.cancellation_point"] begin + @check "setjmp" + Native.code_llvm(mod.f, Tuple{Int}; julia_passes, dump_module=true) + end + end +end +end + @testset "tracked pointers" begin mod = @eval module $(gensym()) function kernel(a) From 36731e68d64edd00a61a2958eb6ef1d6ddc0b995 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Wed, 28 Oct 2026 20:42:36 +0100 Subject: [PATCH 17/17] Test both Julia pass providers with registered JuliaIR 0.1 Resolve JuliaIR through General in the test and downstream environments. Keep both provider variants and their ledger checks in CI. [only cuda, amdgpu, metal, oneapi, opencl] --- .buildkite/downstream.yml | 165 +++++++++++++++++++++++++++++++++++++ .ci/juliair.jl | 127 ++++++++++++++++++++++++++++ .github/workflows/Test.yml | 39 +++++---- test/Project.toml | 4 +- 4 files changed, 318 insertions(+), 17 deletions(-) create mode 100644 .ci/juliair.jl diff --git a/.buildkite/downstream.yml b/.buildkite/downstream.yml index 084a8ee08..f03faebc3 100644 --- a/.buildkite/downstream.yml +++ b/.buildkite/downstream.yml @@ -4,6 +4,14 @@ # `src/` and `ext/`, `Project.toml` and the CI configuration, except the other back-ends' # target files. The `if:` conditions implement the commit message tags documented in the # Development section of the README; keep both in sync when adding a step. +# +# Every package also gets a "(ports)" step, +# which runs its tests with the ports of Julia's passes (the `julia_passes = "ports"` +# preference, with JuliaIR.jl set up by `.ci/juliair.jl` in an environment of its own, +# as the default one persists on some agents) and fails unless JuliaIR.jl's +# ledger shows that they ran. `[only ports]` selects only the ports steps and +# `[skip ports]` skips them; a package's own tags (e.g. `[only cuda]`) select both of its +# steps. Steps are skipped on draft pull requests unless a tag selects them. steps: - label: "CUDA.jl" plugins: @@ -34,6 +42,7 @@ steps: - "ext/**" - "Project.toml" - ".buildkite/**" + - ".ci/**" exclude: - "src/{gcn,spirv,metal,bpf,native}.jl" if: | @@ -44,6 +53,39 @@ steps: soft_fail: - exit_status: 3 + - label: "CUDA.jl (ports)" + plugins: + - JuliaCI/julia#v1: + version: "1.13" + command: | + unset LD_LIBRARY_PATH + echo "--- :julia: Instantiating project" + # an environment of its own, so that the preference doesn't reach the other steps + export JULIA_PROJECT="$$(mktemp -d)" + julia .ci/juliair.jl downstream CUDA + + export JULIAIR_LEDGER="$$PWD/juliair-ledger.tsv" + rm -f "$$JULIAIR_LEDGER" + julia -e 'println("+++ :julia: Running tests") + using Pkg + Pkg.test("CUDA")' + + echo "+++ :julia: Checking that the ports ran" + julia .ci/juliair.jl check-ledger "$$JULIAIR_LEDGER" + agents: + queue: "cuda" + env: + JULIA_CPU_THREADS: "3" + if_changed: + include: *sources + exclude: + - "src/{gcn,spirv,metal,bpf,native}.jl" + if: | + build.message !~ /\[skip [^\]]*(tests|cuda|ptx|ports)/ && + (build.message =~ /\[only [^\]]*(cuda|ptx|ports)/ || + build.message !~ /\[only / && !build.pull_request.draft) + timeout_in_minutes: 120 + - label: "oneAPI.jl" plugins: - JuliaCI/julia#v1: @@ -77,6 +119,39 @@ steps: soft_fail: - exit_status: 3 + - label: "oneAPI.jl (ports)" + plugins: + - JuliaCI/julia#v1: + version: "1.13" + command: | + echo "--- :julia: Instantiating project" + # an environment of its own, so that the preference doesn't reach the other steps + export JULIA_PROJECT="$$(mktemp -d)" + julia .ci/juliair.jl downstream oneAPI + julia -e 'using Pkg; + println("+++ :julia: Building support library") + include(joinpath(Pkg.devdir(), "oneAPI", "deps", "build_ci.jl"))' + + export JULIAIR_LEDGER="$$PWD/juliair-ledger.tsv" + rm -f "$$JULIAIR_LEDGER" + julia -e 'println("+++ :julia: Running tests") + using Pkg + Pkg.test("oneAPI")' + + echo "+++ :julia: Checking that the ports ran" + julia .ci/juliair.jl check-ledger "$$JULIAIR_LEDGER" + agents: + queue: "oneapi" + if_changed: + include: *sources + exclude: + - "src/{ptx,gcn,metal,bpf,native}.jl" + if: | + build.message !~ /\[skip [^\]]*(tests|oneapi|spirv|ports)/ && + (build.message =~ /\[only [^\]]*(oneapi|spirv|ports)/ || + build.message !~ /\[only / && !build.pull_request.draft) + timeout_in_minutes: 60 + - label: "AMDGPU.jl" plugins: - JuliaCI/julia#v1: @@ -106,6 +181,36 @@ steps: soft_fail: - exit_status: 3 + - label: "AMDGPU.jl (ports)" + plugins: + - JuliaCI/julia#v1: + version: "1.13" + command: | + echo "--- :julia: Instantiating project" + # an environment of its own, so that the preference doesn't reach the other steps + export JULIA_PROJECT="$$(mktemp -d)" + julia .ci/juliair.jl downstream AMDGPU + + export JULIAIR_LEDGER="$$PWD/juliair-ledger.tsv" + rm -f "$$JULIAIR_LEDGER" + julia -e 'println("+++ :julia: Running tests") + using Pkg + Pkg.test("AMDGPU")' + + echo "+++ :julia: Checking that the ports ran" + julia .ci/juliair.jl check-ledger "$$JULIAIR_LEDGER" + agents: + queue: "rocm" + if_changed: + include: *sources + exclude: + - "src/{ptx,spirv,metal,bpf,native}.jl" + if: | + build.message !~ /\[skip [^\]]*(tests|amdgpu|gcn|ports)/ && + (build.message =~ /\[only [^\]]*(amdgpu|gcn|ports)/ || + build.message !~ /\[only / && !build.pull_request.draft) + timeout_in_minutes: 60 + - label: "Metal.jl" plugins: - JuliaCI/julia#v1: @@ -146,6 +251,36 @@ steps: soft_fail: - exit_status: 3 + - label: "Metal.jl (ports)" + plugins: + - JuliaCI/julia#v1: + version: "1.13" + command: | + echo "--- :julia: Instantiating project" + # an environment of its own, so that the preference doesn't reach the other steps + export JULIA_PROJECT="$$(mktemp -d)" + JULIA_PKG_PRECOMPILE_AUTO=0 julia .ci/juliair.jl downstream Metal --workspace + + export JULIAIR_LEDGER="$$PWD/juliair-ledger.tsv" + rm -f "$$JULIAIR_LEDGER" + julia -e 'println("+++ :julia: Running tests") + using Pkg + Pkg.test("Metal")' + + echo "+++ :julia: Checking that the ports ran" + julia .ci/juliair.jl check-ledger "$$JULIAIR_LEDGER" + agents: + queue: "metal" + if_changed: + include: *sources + exclude: + - "src/{ptx,gcn,spirv,bpf,native}.jl" + if: | + build.message !~ /\[skip [^\]]*(tests|metal|ports)/ && + (build.message =~ /\[only [^\]]*(metal|ports)/ || + build.message !~ /\[only / && !build.pull_request.draft) + timeout_in_minutes: 120 + - label: "OpenCL.jl" plugins: - JuliaCI/julia#v1: @@ -176,5 +311,35 @@ steps: soft_fail: - exit_status: 3 + - label: "OpenCL.jl (ports)" + plugins: + - JuliaCI/julia#v1: + version: "1.13" + command: | + echo "--- :julia: Instantiating project" + # an environment of its own, so that the preference doesn't reach the other steps + export JULIA_PROJECT="$$(mktemp -d)" + julia .ci/juliair.jl downstream OpenCL SPIRVIntrinsics + + export JULIAIR_LEDGER="$$PWD/juliair-ledger.tsv" + rm -f "$$JULIAIR_LEDGER" + julia -e 'println("+++ :julia: Running tests") + using Pkg + Pkg.test("OpenCL")' + + echo "+++ :julia: Checking that the ports ran" + julia .ci/juliair.jl check-ledger "$$JULIAIR_LEDGER" + agents: + queue: "cuda" + if_changed: + include: *sources + exclude: + - "src/{ptx,gcn,metal,bpf,native}.jl" + if: | + build.message !~ /\[skip [^\]]*(tests|opencl|spirv|ports)/ && + (build.message =~ /\[only [^\]]*(opencl|spirv|ports)/ || + build.message !~ /\[only / && !build.pull_request.draft) + timeout_in_minutes: 120 + env: JULIA_PKG_SERVER_REGISTRY_PREFERENCE: "eager" # see freshly-registered versions without cloning the registry diff --git a/.ci/juliair.jl b/.ci/juliair.jl new file mode 100644 index 000000000..f29132e78 --- /dev/null +++ b/.ci/juliair.jl @@ -0,0 +1,127 @@ +# Set up CI environments for the selected Julia pass provider. +# +# julia .ci/juliair.jl test (cpp|ports) +# +# Prepares GPUCompiler's test environment (`test/`), for `julia --project=test +# test/runtests.jl`, with the `julia_passes` preference set to the given provider. +# +# julia .ci/juliair.jl downstream PACKAGE [PACKAGES...] [--workspace] +# +# Develops GPUCompiler and the downstream PACKAGE (and further PACKAGES, e.g. packages of +# PACKAGE's repository) into the active environment, for `Pkg.test(PACKAGE)` with the +# ports: JuliaIR.jl is added too, and to PACKAGE's test environment, where GPUCompiler +# loads it. The preference goes into the active environment (in CI, a temporary one), +# which `Pkg.test` passes on to the test environment; GPUCompiler is added to PACKAGE's +# test dependencies so that it applies there. `--workspace` also instantiates PACKAGE's +# workspace against GPUCompiler (Metal.jl's tests run examples in its workspace +# environments). +# +# julia .ci/juliair.jl check-ledger FILE +# +# Fails unless FILE, the JuliaIR.jl ledger (`JULIAIR_LEDGER`) of a run with the ports, +# shows that the core ports ran. + +using Pkg, TOML + +const ROOT = dirname(@__DIR__) +const GPUCOMPILER_UUID = "61eb1bfa-7361-4325-ad38-22787b887f55" +const JULIAIR_UUID = "3913dba9-7ba9-44aa-bf76-1cfa3b3073b1" + +function write_preferences(dir, mode) + # an environment that doesn't exist yet (Pkg creates it on the first change) + mkpath(dir) + file = joinpath(dir, "LocalPreferences.toml") + open(io -> TOML.print(io, Dict("GPUCompiler" => Dict("julia_passes" => mode))), file, "w") + println("$file:") + print(read(file, String)) +end + +function test(mode) + mode in ("cpp", "ports") || error("Unknown provider $mode; expected cpp or ports") + write_preferences(joinpath(ROOT, "test"), mode) + Pkg.activate(joinpath(ROOT, "test")) + # Julia 1.10 and 1.11 have no workspaces, so the test environment needs GPUCompiler + VERSION < v"1.12" && Pkg.develop(path=ROOT) + Pkg.instantiate() + Pkg.status() +end + +function downstream(packages, workspace) + package = first(packages) + env = dirname(Base.active_project()) + write_preferences(env, "ports") + + Pkg.develop(path=ROOT) + Pkg.add(PackageSpec(name="JuliaIR", version="0.1")) + Pkg.develop([PackageSpec(name=name) for name in packages]) + + checkout = joinpath(Pkg.devdir(), package) + if workspace + # before adding JuliaIR.jl to the test environment, which is part of the workspace + # and could then not be resolved; the examples that need the workspace run in the + # test process, which has loaded GPUCompiler and JuliaIR.jl already + Pkg.activate(checkout) + Pkg.develop(path=ROOT) + Pkg.instantiate(workspace=true) + Pkg.activate(env) + end + test_project = joinpath(checkout, "test", "Project.toml") + isfile(test_project) || error("$package has no test/Project.toml") + add = Dict("GPUCompiler" => GPUCOMPILER_UUID, "JuliaIR" => JULIAIR_UUID) + # by editing the text, as rewriting it with TOML.print reorders the file + deps = get(TOML.parsefile(test_project), "deps", Dict()) + lines = [k * " = \"" * v * "\"" for (k, v) in add if !haskey(deps, k)] + if !isempty(lines) + text = read(test_project, String) + occursin(r"^\[deps\]$"m, text) || (text = "[deps]\n\n" * text) + text = replace(text, r"^\[deps\]$"m => join(["[deps]"; lines], "\n"); count=1) + write(test_project, text) + end + + Pkg.status() +end + +# the ports that run for every kernel on every target and Julia version, and JuliaLICM, +# which runs for every loop (that test suites have) +const CORE_PORTS = ["jl-alloc-opt", "jl-late-lower-gc-frame", "jl-propagate-addrspaces", + "jl-remove-ni", "jl-demote-float16", "jl-remove-addrspaces", "jl-licm"] + +function check_ledger(file) + isfile(file) || error("No JuliaIR.jl ledger at $file: the ports did not run") + counts = Dict{String,Int}() + pids = Set{String}() + for (i, line) in enumerate(eachline(file)) + # PID, "pass", the pass, the Julia dialect and the LLVM version + fields = split(line, '\t') + if !(length(fields) == 5 && all(isdigit, fields[1]) && fields[2] == "pass" && + startswith(fields[3], "jl-") && startswith(fields[4], "julia=") && + startswith(fields[5], "llvm=")) + error("Malformed line $i in the ledger at $file: $(repr(line))") + end + counts[fields[3]] = get(counts, fields[3], 0) + 1 + push!(pids, fields[1]) + end + for (name, count) in sort!(collect(counts)) + println(rpad(name, 32), count) + end + println("in $(length(pids)) processes") + missing_ports = setdiff(CORE_PORTS, keys(counts)) + isempty(missing_ports) || + error("The ports $(join(missing_ports, ", ")) did not run; is GPUCompiler using the C++ passes?") +end + +function main(args) + isempty(args) && error("Usage: julia .ci/juliair.jl (test MODE | downstream PACKAGE... [--workspace] | check-ledger FILE)") + command = args[1] + if command == "test" + test(args[2]) + elseif command == "downstream" + downstream(filter(!=("--workspace"), args[2:end]), "--workspace" in args) + elseif command == "check-ledger" + check_ledger(args[2]) + else + error("Unknown command $command") + end +end + +main(ARGS) diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index ca1ac8e8c..fdb1191af 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -13,7 +13,7 @@ concurrency: jobs: release_test: - name: Julia ${{ matrix.version }} ${{ matrix.llvm_args }} - ${{ matrix.os }} + name: Julia ${{ matrix.version }} ${{ matrix.llvm_args }} - ${{ matrix.os }} - ${{ matrix.julia_passes }} runs-on: ${{ matrix.os }} timeout-minutes: 20 strategy: @@ -22,33 +22,43 @@ jobs: version: ['1.10', '1.11', '1.12', '1.13', 'nightly'] os: [ubuntu-24.04, ubuntu-24.04-arm, macOS-15, windows-2025] llvm_args: [''] + # the provider of Julia's passes (the `julia_passes` preference) + julia_passes: [cpp, ports] include: # starting with Julia 1.10, we can enable opaque pointers - # from Julia 1.12 on, this is the default. + # from Julia 1.12 on, this is the default. (the ports always use them.) - version: '1.10' os: 'ubuntu-24.04' llvm_args: '--opaque-pointers' + julia_passes: cpp - version: '1.10' os: 'ubuntu-24.04-arm' llvm_args: '--opaque-pointers' + julia_passes: cpp - version: '1.10' os: 'macOS-15' llvm_args: '--opaque-pointers' + julia_passes: cpp - version: '1.10' os: 'windows-2025' llvm_args: '--opaque-pointers' + julia_passes: cpp - version: '1.11' os: 'ubuntu-24.04' llvm_args: '--opaque-pointers' + julia_passes: cpp - version: '1.11' os: 'ubuntu-24.04-arm' llvm_args: '--opaque-pointers' + julia_passes: cpp - version: '1.11' os: 'macOS-15' llvm_args: '--opaque-pointers' + julia_passes: cpp - version: '1.11' os: 'windows-2025' llvm_args: '--opaque-pointers' + julia_passes: cpp steps: - uses: actions/checkout@v7 @@ -60,19 +70,20 @@ jobs: - name: Set-up cache uses: julia-actions/cache@v3 + # Run the suite in the environment with the selected pass-provider preference. + - name: Set up the test environment + run: julia --color=yes .ci/juliair.jl test ${{ matrix.julia_passes }} + - name: Run tests - uses: julia-actions/julia-runtest@v1 + run: julia --color=yes --project=test test/runtests.jl env: JULIA_LLVM_ARGS: ${{ matrix.llvm_args }} + JULIAIR_LEDGER: ${{ runner.temp }}/juliair-ledger.tsv - - name: Process coverage - # CoverageTools 1.4.1 uses a PkgLoadSpec field renamed in Julia 1.14. - if: matrix.version != 'nightly' - uses: julia-actions/julia-processcoverage@v1 - - name: Submit coverage - if: matrix.version != 'nightly' - uses: codecov/codecov-action@v7 - with: - token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: false - files: lcov.info + # the suite also runs the ports explicitly, so check the preference too + - name: Check that the ports were the default + if: matrix.julia_passes == 'ports' + run: julia --project=test -e 'using GPUCompiler; GPUCompiler.JULIA_PASSES === :ports || error("julia_passes preference not applied")' + - name: Check that the ports ran + if: matrix.julia_passes == 'ports' + run: julia .ci/juliair.jl check-ledger '${{ runner.temp }}/juliair-ledger.tsv' diff --git a/test/Project.toml b/test/Project.toml index 575672fcc..5e5fe8905 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -22,9 +22,7 @@ StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" demumble_jll = "1e29f10c-031c-5a83-9565-69cddfc27673" -[sources] -JuliaIR = {url = "https://github.com/JuliaLLVM/JuliaIR.jl", rev = "main"} - [compat] Aqua = "0.8" +JuliaIR = "0.1" ParallelTestRunner = "2"