Repository navigation
Conversation
maleadt
force-pushed
the
tb/device-libraries
branch
from
October 20, 2026 19:22
4f70a38 to
004f464
Compare
maleadt
marked this pull request as draft
October 20, 2026 19:25
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1044 +/- ##
==========================================
- Coverage 92.25% 90.92% -1.34%
==========================================
Files 31 32 +1
Lines 7254 7469 +215
==========================================
+ Hits 6692 6791 +99
- Misses 562 678 +116 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Other back-ends need to rewrite calls to LLVM intrinsics the same way Metal does, e.g., to lower math intrinsics to library calls. Generalize the vector min/max scalarization into `scalarize_vector_calls!`, which handles any elementwise intrinsic.
Back-ends now describe the libraries that implement device functions with `device_libraries(job)`: Julia methods (`DeviceLibraryMethod`s, as used for the GPU runtime) or LLVM bitcode. Definitions are imported on demand, searching the libraries in order until no library provides a remaining reference, both before optimization (so library code is optimized along with the kernel) and after GC lowering (which introduces runtime calls). This replaces the separate runtime linking, and lets back-ends drop fixed-point linking loops over vendor libraries. `link_libraries!` keeps working. Julia libraries can be optimized on their own, for standalone numerical code, and bitcode libraries can be force-inlined, as AMDGPU does with the ROCm device libraries.
Some LLVM intrinsics have no lowering on GPU targets, e.g., math functions on vectors (#679) or any double-precision operation on hardware without it. Keeping them as intrinsics during optimization lets LLVM reason about them, so lower them afterwards: the new `lower_intrinsics!` hook runs after optimization, before GC lowering, and its library calls are resolved like any other reference. Julia libraries imported at that point are lowered by the same hook, so their ABI matches the lowered caller. `finish_imports!` lets back-ends prepare late imports, which are simplified once inlined.
NVVMReflect folds `__CUDA_PREC_SQRT` and `__CUDA_PREC_DIV` to 0, so libdevice used its approximate square roots and divisions (e.g. in `__nv_hypotf`) even without fast math. Fold them to `!fastmath` instead, matching nvcc's `-prec-sqrt`/`-prec-div` defaults. Also accept a bare global as the reflect argument on LLVM < 17: with opaque pointers the zero-index GEP folds away there too.
maleadt
force-pushed
the
tb/device-libraries
branch
from
October 23, 2026 13:30
004f464 to
dbe5000
Compare
maleadt
added a commit
to JuliaGPU/CUDA.jl
that referenced
this pull request
Oct 23, 2026
Pin GPUCompiler to the tb/device-libraries branch rather than a commit, so the source follows JuliaGPU/GPUCompiler.jl#1044 as it is rebased.
maleadt
added a commit
to JuliaGPU/AMDGPU.jl
that referenced
this pull request
Oct 23, 2026
Pin GPUCompiler to the tb/device-libraries branch rather than a commit, so the source follows JuliaGPU/GPUCompiler.jl#1044 as it is rebased.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Back-ends currently link their device libraries by hand in
link_libraries!, often with fixed-point loops over vendor bitcode, and the GPU runtime has its own linking path. Some operations also can't be selected on some targets (math intrinsics on vectors, #679, or any binary64 operation on hardware without it) and have to be lowered to library calls. Doing that before optimization hides the operations from LLVM.This PR adds
device_libraries(job), which returns an ordered tuple ofDeviceLibrarys. Each library either exports Julia methods (DeviceLibraryMethod, the mechanism the runtime already uses) or contains LLVM bitcode. Definitions are imported on demand: GPUCompiler searches the libraries in order, and repeats until no library provides any remaining reference. Unresolved references are left for IR validation, and an ABI mismatch between a declaration and its definition is an error. Linkonce definitions shared by several libraries are coalesced by LLVM's linker.Imports happen in two phases:
link_libraries!and the runtime were linked before, so library code is optimized together with the kernel. This is what Clang does with-mlink-builtin-bitcode, and what NVVM, MLIR, Triton and XLA do too.lower_intrinsics!(job, mod)hook has rewritten operations the target can't select into library calls, and again after GC lowering, which introduces runtime calls. Julia libraries imported at this point are lowered by the same hook, so their ABI matches the caller. Late imports are internalized and passed tofinish_imports!(job, mod, imported). CPU-feature and sync-scope lowering are applied to them, then they are force-inlined where requested, and a small cleanup pipeline runs.A library can be marked
always_inline, as AMDGPU needs for the ROCm device libraries. A Julia library can also beoptimized on its own, which suits standalone numerical code such as #926.link_libraries!overrides keep working at both call sites.The intrinsic-rewriting helpers move out of the Metal back-end into
utils.jl(call_declared!,lower_intrinsic_calls!,scalarize_vector_calls!). PTX'snvvm_reflect!now folds__CUDA_PREC_SQRT/__CUDA_PREC_DIVto!fastmath, matching nvcc's defaults. Before, libdevice used its approximate implementations even without fast math.Differences from the first version of this PR
The first version linked all libraries once, late, and tracked library methods and bitcode digests in the kernel cache. I reworked it because:
cufunctionlookup went from 201 ns to 4.85 µs, and an AMDGPU@roc launch=falsefrom 113 ns to 4.2 µs. Kernel cache lookup is now identical tomain. Device libraries are exempt from kernel invalidation for now: library selection must only depend on the job's configuration, and bitcode is read once per session. Plain method-instance edges from GPU inference frames would integrate this with invalidation at no launch cost, without changing this API.Removed compared to the first version:
device_library_bitcode,load_device_library,runtime_imports, digest tracking anduses_legacy_linker.Known regression for back-ends that lower math late
LLVM's cost model treats an intrinsic as a single instruction (
isLoweredToCallreturns false), even when the target will lower it to a library call. Loops over math intrinsics are therefore unrolled as if they were cheap, beforelower_intrinsics!turns each intrinsic into a call. In CUDA.jl, an F32 sum ofsingrows from 123 to 566 SASS instructions. CUDA.jl#3433 keeps the libdevice overlays for the affected functions for now, marked as a workaround, and tests the late lowering path so those overlays can be dropped once LLVM's cost model is fixed.Validation
Tested on an RTX 5080 (sm_120) and an AMD gfx1036 iGPU.
device_library,ptx,metal,nativeandutilspass on 1.10 and nightly.device_librarypasses on 1.13.coregroup passes, plus the new test that exercises raw-intrinsic lowering.main.cufunctiontakes about 200 ns, with no extra allocations.main, apart from improvements: Float16exploses a call, and vector math compiles wheremainfails.main.@cudais 4% slower on 1.12 (1.60 vs 1.53 s) and 8% slower on 1.13 (1.09 vs 1.01 s).main;@roc launch=falsetakes about 110 ns.sincosruns twice as fast.Downstream: #926 (softfloat, stacked on this PR), JuliaGPU/CUDA.jl#3433 and JuliaGPU/AMDGPU.jl#1177.