Skip to content

Software Float64 emulation using declarative device libraries - #926

Draft
maleadt wants to merge 3 commits into
tb/device-librariesfrom
tb/softfloat
Draft

maleadt wants to merge 3 commits into
tb/device-librariesfrom
tb/softfloat

Conversation

@maleadt

@maleadt maleadt commented Sep 9, 2026 •

Copy link
Copy Markdown
Member

Add declarative device libraries and late intrinsic lowering

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 of DeviceLibrarys. 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:

  • Before optimization, where 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.
  • After optimization, once the new 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 to finish_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 be optimized 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's nvvm_reflect! now folds __CUDA_PREC_SQRT/__CUDA_PREC_DIV to !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:

  • Dependency tracking sat on the launch path. cufunction lookup went from 201 ns to 4.85 µs, and an AMDGPU @roc launch=false from 113 ns to 4.2 µs. Kernel cache lookup is now identical to main. 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.
  • Linking only after optimization means library code (including the runtime) is never optimized with the kernel. It also forced AMDGPU to copy attributes onto declarations up front, and that copy blocked LICM: one F64 benchmark went from 465 to 2058 ms.
  • Julia libraries imported after GC lowering could not allocate.

Removed compared to the first version: device_library_bitcode, load_device_library, runtime_imports, digest tracking and uses_legacy_linker.

Known regression for back-ends that lower math late

LLVM's cost model treats an intrinsic as a single instruction (isLoweredToCall returns false), even when the target will lower it to a library call. Loops over math intrinsics are therefore unrolled as if they were cheap, before lower_intrinsics! turns each intrinsic into a call. In CUDA.jl, an F32 sum of sin grows 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.

  • GPUCompiler:
    • The full suite passes on Julia 1.12.
    • device_library, ptx, metal, native and utils pass on 1.10 and nightly.
    • device_library passes on 1.13.
  • CUDA.jl#3433, on 1.12 and 1.13:
    • Tests: the core group passes, plus the new test that exercises raw-intrinsic lowering.
    • Launch overhead: unchanged from main. cufunction takes about 200 ns, with no extra allocations.
    • Generated code: SASS is identical to main, apart from improvements: Float16 exp loses a call, and vector math compiles where main fails.
    • Runtime: kernels are within ±1% of main.
    • First compile: a first @cuda is 4% slower on 1.12 (1.60 vs 1.53 s) and 8% slower on 1.13 (1.09 vs 1.01 s).
  • AMDGPU.jl#1177, on 1.13 and 1.10:
    • Tests: pass.
    • Launch overhead and generated code: match main; @roc launch=false takes about 110 ns.
    • sincos: now inlined. Float32 sincos runs twice as fast.
    • LICM: the regression from the first version of this PR is gone (F64: 465 ms, against 2058 ms before).
  • Software Float64 emulation using declarative device libraries #926 (softfloat), rebased onto this PR:
    • Its tests pass on 1.10, 1.12 and 1.13.
    • On PoCL, all 23 operations are bit-exact against the CPU.

Downstream: #926 (softfloat, stacked on this PR), JuliaGPU/CUDA.jl#3433 and JuliaGPU/AMDGPU.jl#1177.

@codecov

codecov Bot commented Sep 9, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.45087% with 48 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.34%. Comparing base (dbe5000) to head (bc82f75).

Files with missing lines Patch % Lines
src/softfloat/legalize.jl 90.98% 41 Missing ⚠️
src/softfloat/binary64/types.jl 87.80% 5 Missing ⚠️
src/softfloat/uint128.jl 93.93% 2 Missing ⚠️
Additional details and impacted files
@@                   Coverage Diff                   @@
##           tb/device-libraries     #926      +/-   ##
=======================================================
+ Coverage                90.92%   91.34%   +0.42%     
=======================================================
  Files                       32       42      +10     
  Lines                     7469     8334     +865     
=======================================================
+ Hits                      6791     7613     +822     
- Misses                     678      721      +43     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

maleadt added a commit that referenced this pull request Sep 9, 2026
#904 demoted Julia's `unordered` heap-reference accesses, but codegen also
stores the type tag of every heap-allocated object with `release` ordering,
through the generic pointer the device allocator returns. Apple's back-end
cannot legalize that either: on macOS 26 the pipeline compile aborts with
XPC_ERROR_CONNECTION_INTERRUPTED, and metal-tt reports "unable to legalize
instruction: store release (p0)". It surfaced with Float64 emulation (#926),
where DomainError(::Float64, ...) in log1p's throw path is not inlined and
its allocation survives to the back-end.

The same applies to SPIR-V: OpAtomicLoad/OpAtomicStore only take scalars, so
a release-ordered pointer store is just as invalid there as an unordered one.
Since device-side atomics go through target intrinsics rather than these
instructions, every LLVM atomic load or store that reaches these back-ends is
Julia GC bookkeeping, and `demote_atomics!` now strips all of them.
@maleadt

maleadt commented Sep 9, 2026

Copy link
Copy Markdown
Member Author

Depends on llvm/llvm-project#208026, which I'll add in JuliaPackaging/Yggdrasil#14727

@maleadt
maleadt force-pushed the tb/softfloat branch 3 times, most recently from 9b4ff44 to 3452534 Compare September 11, 2026 11:32
@maleadt maleadt added metal Stuff about the Apple metal back-end. spirv Stuff about the SPIR-V back-end. labels Oct 19, 2026
@maleadt maleadt changed the title Software Float64 emulation, and job-scoped device libraries Software Float64 emulation using declarative device libraries Oct 20, 2026
@maleadt
maleadt changed the base branch from main to tb/device-libraries October 20, 2026 11:47
maleadt added a commit that referenced this pull request Oct 20, 2026
Replace the provider hooks from PR #926 with the ordered device-library interface. Legalize callers and Julia-library artifacts at the same late boundary so imported runtime signatures remain ABI-compatible; keep expensive arithmetic helpers out of line.
maleadt added a commit that referenced this pull request Oct 20, 2026
Replace the provider hooks from PR #926 with the ordered device-library interface. Legalize callers and Julia-library artifacts at the same late boundary so imported runtime signatures remain ABI-compatible; keep expensive arithmetic helpers out of line.
maleadt added a commit that referenced this pull request Oct 20, 2026
Replace the provider hooks from PR #926 with the ordered device-library interface. Legalize callers and Julia-library artifacts at the same late boundary so imported runtime signatures remain ABI-compatible; keep expensive arithmetic helpers out of line.
maleadt added a commit that referenced this pull request Oct 20, 2026
Replace the provider hooks from PR #926 with the ordered device-library interface. Legalize callers and Julia-library artifacts at the same late boundary so imported runtime signatures remain ABI-compatible; keep expensive arithmetic helpers out of line.
@maleadt
maleadt force-pushed the tb/device-libraries branch from 4f70a38 to 004f464 Compare October 20, 2026 19:22
maleadt added a commit that referenced this pull request Oct 20, 2026
Replace the provider hooks from PR #926 with the device-library interface: back-ends
select `softfloat_library()` in `device_libraries` and call `SoftFloat.legalize!(job, mod)`
from `lower_intrinsics!`. Legalization rebuilds functions whose type changes under their
original names, so callers and Julia libraries imported after optimization agree on the
integer ABI.
@maleadt
maleadt force-pushed the tb/device-libraries branch from 004f464 to dbe5000 Compare October 23, 2026 13:30
maleadt added a commit that referenced this pull request Oct 23, 2026
Replace the provider hooks from PR #926 with the device-library interface: back-ends
select `softfloat_library()` in `device_libraries` and call `SoftFloat.legalize!(job, mod)`
from `lower_intrinsics!`. Legalization rebuilds functions whose type changes under their
original names, so callers and Julia libraries imported after optimization agree on the
integer ABI.
Replace the provider hooks from PR #926 with the device-library interface: back-ends
select `softfloat_library()` in `device_libraries` and call `SoftFloat.legalize!(job, mod)`
from `lower_intrinsics!`. Legalization rebuilds functions whose type changes under their
original names, so callers and Julia libraries imported after optimization agree on the
integer ABI.
Before LLVM 19, `icmp` could be a constant expression, as found in Base's `sin`. The
legalizer rejected every constant expression it did not know how to rebuild, even when
neither its type nor its operands changed; return those as they are.
GPUCompiler has no cost-based inliner, so the emulation routines all stayed out of line:
a kernel calling `sin` called `gpu_softfloat_neg64`, a single xor, 85 times. Split the
routines over two libraries: arithmetic and narrowing conversions (79 to 444 instructions
once optimized) stay out of line, the rest (at most 43 instructions) are forced inline.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

metal Stuff about the Apple metal back-end. spirv Stuff about the SPIR-V back-end.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant