Skip to content

Add opt-in support for Intel's LTS driver stack (ONEAPI_LTS) - #574

Merged
michel2323 merged 12 commits into
mainfrom
aurora-lts-2523.40
Aug 3, 2026
Merged

Add opt-in support for Intel's LTS driver stack (ONEAPI_LTS)#574
michel2323 merged 12 commits into
mainfrom
aurora-lts-2523.40

Conversation

@michel2323

@michel2323 michel2323 commented May 11, 2026

Copy link
Copy Markdown
Member

Intel's LTS driver releases, as deployed on Aurora (NEO 25.18 / 2523.40) and other data center deployments, differ enough from the rolling stack that oneAPI.jl currently fails to compile kernels, silently corrupts reductions, and gets contexts banned on them. This PR adds the workarounds needed to run there, each gated behind a master switch.

The switch

ONEAPI_LTS (oneL0.LTS[]), resolved from the environment at the top of oneL0.__init__, defaults to off. With it off every code path below is skipped and the package behaves exactly as on main, so this PR is a no-op for anyone on the rolling stack. An LTS deployment opts in with ONEAPI_LTS=1.

A second, independent switch, ONEAPI_SYNC_EACH_SUBMISSION (also default off), covers one workaround with a large throughput cost; see below.

Both are parsed by a shared parse_env_bool accepting 1/true/yes/on and 0/false/no/off, which warns rather than silently falling back to the default on an unrecognized value.

What is gated on ONEAPI_LTS

Area Problem on the LTS stack Workaround
SPIR-V codegen (src/compiler/compilation.jl) NEO/IGC only accepts SPIR-V from the Khronos translator, and needs extensions declared explicitly select backend = :khronos and declare SPV_EXT_relaxed_printf_string_address_space / SPV_EXT_shader_atomic_float_add; the rolling stack keeps :llvm
BFloat16 the translator cannot codegen native bfloat in generic kernels (clamp!InvalidIRError), and declaring SPV_KHR_bfloat16 segfaults NEO force supports_bfloat16 = false; the test suite and examples/bfloat16.jl skip to match
Reductions (src/mapreduce.jl) IGC miscompiles strided (non-coalesced) global reads in the reduction kernel — silently wrong results for sum(A; dims=2), a == transpose(b), ishermitian materialize strided inputs to dense, and route dim-1-kept reductions to a new coalesced one-work-item-per-slice kernel
Buffer free (src/pool.jl, src/context.jl) NEO advertises ZE_extension_memory_free_policies but ignores BLOCKING_FREE, so a GC-driven free of a buffer with work in flight pagefaults and bans the context a queue registry plus synchronize_all_queues before free
Queue destruction (lib/level-zero/cmdqueue.jl) zeCommandQueueDestroy does not wait for in-flight work; the same fault-and-ban follows drain in the finalizer with a 10 s bound, then leak the queue rather than hang GC and process exit

ONEAPI_SYNC_EACH_SUBMISSION

Under heavy multi-process oversubscription of a single tile, a whole-queue zeCommandQueueSynchronize does not reliably retire the tail of an earlier, separately submitted command list: the last work-items of a kernel or the last elements of a copy silently go missing. Synchronizing after every submission eliminates it at roughly 3× throughput cost, so it is a separate opt-in rather than folded into ONEAPI_LTS. oneL0.sync_each_submission(f, enable) scopes it temporarily, which is needed for submit-then-signal patterns that would otherwise deadlock.

Not gated (applies to both stacks)

  • src/oneAPI.jl: dlopen the NEO driver by full path at init, so that libsycl's bundled Level Zero loader resolves it by soname later. Required when no system NEO is installed. The adjacent LD_LIBRARY_PATH extension only ever affected child processes.
  • src/compiler/precompile.jl: the precompile workload mirrors the runtime backend choice, so precompilation warms the pipeline that will actually be used.
  • SPIRV_LLVM_Translator_jll becomes a dependency — GPUCompiler resolves the tool from Base.loaded_modules, so both back-ends must be loaded for either path to work, and is reported by versioninfo.

CI

The self-hosted Aurora runner sets ONEAPI_LTS=1 and ONEAPI_SYNC_EACH_SUBMISSION=1, builds liboneapi_support.so from deps/src (and verifies the preference actually took effect, rather than silently testing the registered JLL), and disables AVX512-FP16 host codegen via -C native,-avx512fp16: under concurrent oneMKL load the native FP16 path miscomputes host Float16 reference values on these Sapphire Rapids nodes, failing tests whose GPU result is correct.

The buildkite runner is unchanged and gets the defaults, so it keeps exercising the non-LTS :llvm back-end path.

Tests

test/array.jl gains a "strided mixed reductions" regression test using exact Int32 sums (immune to Float32 accumulation-order rounding) across several shapes and dims combinations, covering both the reductions routed to the coalesced kernel and the mixed ones that deliberately stay on the workgroup-per-slice kernel.

@github-actions

github-actions Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

Your PR requires formatting changes to meet the project's style guidelines.
Please consider running Runic (git runic main) to apply these changes.

Click here to view the suggested changes.
diff --git a/test/level-zero.jl b/test/level-zero.jl
index bbf8f46..101b7c6 100644
--- a/test/level-zero.jl
+++ b/test/level-zero.jl
@@ -210,21 +210,21 @@ pool = ZeEventPool(ctx, 2)
 signal_event = pool[1]
 wait_event = pool[2]
 
-# This is a submit-then-signal pattern: the kernel is gated on `wait_event`, which is
-# only signaled *after* submission. The ONEAPI_SYNC_EACH_SUBMISSION=1 workaround (Aurora
-# LTS) makes `execute!` block in zeCommandQueueSynchronize right after submitting, which
-# would deadlock here since the kernel cannot retire before `wait_event` is signaled. No
-# production code path submits event-gated work, so disable the workaround just here.
-oneL0.sync_each_submission(false) do
-    execute!(queue) do list
-        append_launch!(list, kernel, 1, signal_event, wait_event)
-    end
-    @test !Base.isdone(signal_event)
-
-    signal(wait_event)
-    synchronize(queue)
-    @test Base.isdone(signal_event)
-end
+        # This is a submit-then-signal pattern: the kernel is gated on `wait_event`, which is
+        # only signaled *after* submission. The ONEAPI_SYNC_EACH_SUBMISSION=1 workaround (Aurora
+        # LTS) makes `execute!` block in zeCommandQueueSynchronize right after submitting, which
+        # would deadlock here since the kernel cannot retire before `wait_event` is signaled. No
+        # production code path submits event-gated work, so disable the workaround just here.
+        oneL0.sync_each_submission(false) do
+            execute!(queue) do list
+                append_launch!(list, kernel, 1, signal_event, wait_event)
+            end
+            @test !Base.isdone(signal_event)
+
+            signal(wait_event)
+            synchronize(queue)
+            @test Base.isdone(signal_event)
+        end
 
 end
 

@michel2323
michel2323 force-pushed the aurora-lts-2523.40 branch from eec2785 to 991d29e Compare June 12, 2026 14:28
@codecov

codecov Bot commented Jun 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.38462% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.24%. Comparing base (0a71d96) to head (45f89ca).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/mapreduce.jl 75.00% 4 Missing ⚠️
lib/level-zero/oneL0.jl 70.00% 3 Missing ⚠️
lib/level-zero/cmdqueue.jl 66.66% 2 Missing ⚠️
src/compiler/compilation.jl 87.50% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #574      +/-   ##
==========================================
+ Coverage   79.76%   80.24%   +0.47%     
==========================================
  Files          50       50              
  Lines        3391     3487      +96     
==========================================
+ Hits         2705     2798      +93     
- Misses        686      689       +3     

☔ 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.

@michel2323
michel2323 force-pushed the aurora-lts-2523.40 branch from 32d11d7 to b13cc3e Compare June 24, 2026 13:26
@michel2323
michel2323 force-pushed the aurora-lts-2523.40 branch 2 times, most recently from 434bd6c to f158ea6 Compare July 7, 2026 13:41
@michel2323
michel2323 force-pushed the aurora-lts-2523.40 branch 2 times, most recently from d7f0d35 to ddaa418 Compare July 22, 2026 18:13
@michel2323
michel2323 force-pushed the aurora-lts-2523.40 branch 7 times, most recently from 7973eca to f9435c1 Compare August 1, 2026 02:12
@michel2323 michel2323 changed the title Aurora LTS 2523.40 Add opt-in support for Intel's LTS driver stack (ONEAPI_LTS) Aug 1, 2026
Aurora ships the Intel "LTS" GPU software stack rather than the rolling
release upstream targets, and needs a set of driver/IGC workarounds that
must not affect anyone else. Introduce oneL0.LTS[] as the single switch
those workarounds gate on, so with it off the package behaves exactly
like upstream.

Default OFF, matching upstream: an LTS deployment opts in with
ONEAPI_LTS=1. Resolved at the top of oneL0.__init__, before the
driver-availability early returns, since it gates codegen and must be set
even on a host without a functional GPU.

Route it through a parse_env_bool helper that accepts on/off/yes/no/1/0/
true/false and warns on an unrecognized value, so a typo does not silently
select the wrong stack.
The LTS NEO/IGC runtime does not accept the output of the LLVM SPIR-V
back-end (#491) and needs SPIR-V extensions declared explicitly, which the
back-end handles itself. Select the codegen path from oneL0.LTS[]:
:khronos (translator) on the LTS stack, :llvm on the rolling stack.

GPUCompiler resolves the tool from the target's `backend` field through a
LazyModule that looks the JLL up in Base.loaded_modules, so both JLLs are
listed as deps and loaded, and the choice is made at compile time. Keep
SPIRV_LLVM_Backend_jll: with it neither a dep nor loaded, the first
@oneapi launch on the rolling stack fails in mcgen and no kernel compiles
at all.

Preserve the rolling-stack behavior exactly: supports_bfloat16 stays
_device_supports_bfloat16(), SPV_KHR_bfloat16 is declared when the runtime
advertises it, and finish_ir! otherwise lowers bfloat->i16. Force bf16 off
on the LTS branch, whose SPIR-V stack cannot codegen native bfloat in
generic kernels (clamp! fails with InvalidIRError, and declaring
SPV_KHR_bfloat16 crashes the LTS runtime).

The precompile workload mirrors the same choice, resolving ONEAPI_LTS
directly from the environment since oneL0.__init__ has not run yet.
The LTS IGC silently miscompiles strided (non-coalesced) global reads
inside the reduction kernels, producing wrong results with no error --
e.g. `a == transpose(b)`, `sum(transpose(x))`, `ishermitian`. Add two
complementary guards, both gated on oneL0.LTS[]:

  - A coalesced one-work-item-per-slice kernel for when the contiguous
    leading dimension is not reduced (size(Rreduce, 1) == 1, e.g.
    sum(A; dims=2)), whose lanes read consecutive memory. Its group size
    comes from launch_configuration(kernel) rather than a hardcoded 256,
    which can exceed the kernel's max work-group size and fail the launch.
  - Materialize strided / non-dense inputs (Transpose, Adjoint,
    PermutedDimsArray, SubArray, ...) to a dense array before reducing,
    via _dense_reduce_input. Only recurse when materialization actually
    produced a dense oneArray: a plain host AbstractArray materializes to
    another host array that still fails the predicate, which would recurse
    forever (StackOverflowError) instead of raising the usual
    kernel-conversion error.

Reductions that also reduce dim 1 (e.g. dims=(1,3)) keep a contiguous
innermost axis and need no workaround; the miscompile bites only when
adjacent lanes land a full stride apart on every lane. Covered by an Int32
regression test -- exact and associative, so it is immune to the Float32
accumulation-order rounding that makes large GPU/CPU sums differ for
unrelated reasons.
On the LTS NEO stack (25.18) freeing a buffer does not drain queues that
still have work in flight referencing it: the in-flight kernel then faults
and the context is banned, surfacing later as a ZE_RESULT_ERROR_UNKNOWN at
an unrelated op. global_queue is task-local, so a test file's task can also
die with work still queued.

  - Register every queue in a per-(context,device) registry that holds it
    strongly, keyed by a weak reference to the owning task. A WeakRef to
    the queue would be cleared in the same GC cycle that queues its
    finalizer, hiding it from release exactly when its in-flight work
    still references buffers being freed.
  - Register from both global_queue and KA.priority!, via a shared
    register_queue! helper. priority! replaces the task-local queue with a
    directly constructed one; unregistered, all work after
    priority!(backend, :high) ran on a queue release() never synchronizes
    -- the exact use-after-free the registry exists to prevent.
  - Before any BLOCKING_FREE, synchronize all queues that could reference
    the buffer. Synchronize outside the registry lock with finalizers
    disabled, skip already-finalized queues, and retire queues of dead
    tasks.
  - In the queue finalizer, drain then destroy, and null the handle so a
    concurrent synchronize_all_queues skips it. Bound the drain rather
    than waiting forever, so a task that dies mid-submission cannot hang
    the finalizer indefinitely.

All gated on oneL0.LTS[]; the rolling stack keeps upstream behavior.
The Aurora LTS NEO stack intermittently drops the tail of a command list,
silently corrupting results. Add an off-by-default workaround
(ONEAPI_SYNC_EACH_SUBMISSION=1) that synchronizes the queue after every
command-list submission, exposed as a sync_each_submission() getter, a
sync_each_submission!(enable) setter, and a scoped
sync_each_submission(f, enable) do-block form.

Use the scoped form to disable it around the submit-then-signal block in
test/level-zero.jl: that block launches a kernel gated on a wait_event only
signaled after submission, so synchronizing on submission deadlocks waiting
for a kernel that cannot retire yet. No production path submits event-gated
work, so the workaround stays safe everywhere it matters.

The flag shares parse_env_bool with ONEAPI_LTS, so a deployment writing
ONEAPI_SYNC_EACH_SUBMISSION=on gets the workaround rather than silently
running without it.
Setting ENV["LD_LIBRARY_PATH"] inside __init__ cannot affect the running
process's dlopen search paths: glibc captures LD_LIBRARY_PATH once at
process startup, so the stated purpose -- letting libsycl's bundled ze_lib
find NEO's libze_intel_gpu in-process -- was never served for the current
process, only for child processes that inherit the environment.

dlopen the NEO driver by full path in __init__ so it is resident before
libsycl loads; a later dlopen("libze_intel_gpu.so.1") by libsycl's bundled
loader then resolves to it by soname without a path search. Keep extending
LD_LIBRARY_PATH, but correct the comment to say it only covers spawned
worker processes. Required when no system NEO is installed.
The LTS SPIR-V stack (Khronos translator + NEO/IGC) cannot codegen native
bfloat in generic kernels: any kernel that keeps a bfloat value fails with
InvalidIRError, and declaring SPV_KHR_bfloat16 crashes the LTS runtime.
_device_supports_bfloat16() is a hardware check and does not capture that.

  - Gate the bf16 eltypes push in the testsuite on !oneL0.LTS[], so
    BFloat16 is exercised as a generic element type only off the LTS
    stack. Storage, conversions and oneMKL bf16 are unaffected.
  - Skip examples/bfloat16.jl on the LTS stack, alongside the existing
    old-Julia and unsupported-device guards. The example uses BFloat16 as
    storage only, but even a bfloat load/store forces the bfloat LLVM type
    into the module, which the LTS translator cannot emit -- so it fails
    with "Failed to translate LLVM code to SPIR-V" regardless of device
    support.
michel2323 and others added 4 commits August 3, 2026 08:14
Set up the GitHub Actions job for the Aurora LTS self-hosted runner:

  - Opt into the LTS code paths (ONEAPI_LTS=1), per-worker GPU spreading
    (ONEAPI_TEST_SPREAD_GPUS=1), and the per-submission synchronize
    workaround (ONEAPI_SYNC_EACH_SUBMISSION=1).
  - Build liboneapi_support.so from deps/src before julia-buildpkg, since
    the registered oneAPI_Support_jll artifact lags behind the wrappers on
    this branch, and verify oneAPI_Support_jll actually resolves to it --
    a failed build would otherwise silently fall back to the JLL and
    report green.
  - Run the tests through `julia -C native,-avx512fp16`. Under concurrent
    oneMKL load the Sapphire Rapids native AVX512-FP16 path silently
    miscomputes *host* Float16 (e.g. the GPUArrays broadcast reference),
    failing tests even though the GPU result is correct. julia-runtest
    cannot pass a cpu-target, so invoke Pkg.test() directly.
Add a dedicated page covering the ONEAPI_LTS switch: the Khronos SPIR-V
translator replacing the LLVM SPIR-V back-end, the BFloat16 limitation,
the strided-reduction and free-synchronization workarounds, and the
separate ONEAPI_SYNC_EACH_SUBMISSION option. Link it from the README,
index, installation and troubleshooting pages.

Also fix a comment pointing at ISSUE_dropped_tail.md, which is not in
the repository, to reference the new page instead.
The PR checks box showed "self-runner (ubuntu-latest, 1, x64)", which says
nothing about which stack ran and misreports the runner. Name it explicitly
so it reads as the LTS counterpart to buildkite's rolling-stack build.
The self-hosted job ran no coverage instrumentation and uploaded nothing, so
Codecov saw only buildkite's rolling-stack run. Every LTS-gated branch is
unreachable there, which is why this branch reports 38% patch coverage
against 65 missing lines that are almost entirely `if oneL0.LTS[]` bodies.
Codecov merges uploads per commit, so reporting from both stacks covers both
sides of those branches.
@michel2323
michel2323 enabled auto-merge (squash) August 3, 2026 16:14
@michel2323
michel2323 merged commit 8fd0eff into main Aug 3, 2026
5 checks passed
@michel2323
michel2323 deleted the aurora-lts-2523.40 branch August 3, 2026 17:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant