Julia¶
Experimental support
Support for the Julia programming language on Aurora is currently experimental. This guide provides a set of best practices, but you may encounter unexpected issues.
Introduction¶
Julia is a high-level, high-performance programming language designed for technical and scientific computing. It combines the ease of use of dynamic languages with the performance of compiled languages, making it well-suited for large-scale simulations and data analysis.
This guide details how to configure and run Julia on the Aurora supercomputer, focusing on leveraging the system's key architectural features for large-scale parallel and GPU-accelerated computing.
Contributing
This guide is a first draft of the Julia documentation for Aurora. If you have suggestions or find errors, please open a pull request or contact us by opening a ticket at the ALCF Helpdesk.
All the source files used in this documentation are located at https://github.com/anlsys/julia_alcf. Feel free to open PRs!
Julia Setup¶
Julia is available on Aurora as a module.
We recommend setting your environment variable $JULIA_DEPOT_PATH to a project directory PROJECT on Flare or Gila for faster file access and to avoid filling up your home directory.
Two things about where and when you set this matter more than they look:
Set it in your job script, not only in your shell configuration
A PBS job runs bash, whatever your interactive login shell is. If you set JULIA_DEPOT_PATH only in ~/.zshrc (or ~/.tcshrc, or any other non-bash file), your interactive sessions will be configured correctly and your batch jobs will silently fall back to ~/.julia — where none of your packages are installed. Every rank then fails with:
Even in bash this is easy to get wrong: a login shell reads ~/.bash_profile (or ~/.profile), not ~/.bashrc. The reliable fix is to export it in the submission script itself, as the job script below does. Setting it in both places is fine.
Set it before loading the module
The module reads JULIA_DEPOT_PATH at load time to derive other settings, so exporting it afterwards leaves those pointing at the wrong place. Always export first, then module load.
If $JULIA_DEPOT_PATH is not set, it defaults to ~/.julia and the module prints a warning when you load it. Make sure the filesystem holding your depot is also listed in the job's #PBS -l filesystems= directive, or the compute nodes will not be able to reach it.
What the module sets for you¶
Loading the Julia module configures more than PATH. The settings you are most likely to notice:
| Variable | Value | Why |
|---|---|---|
TMPDIR | $JULIA_DEPOT_PATH/tmp | Julia needs more scratch space than Aurora's /tmp provides. Derived from the depot at load time |
JULIA_LOAD_PATH | includes the system environment | Supplies the system LocalPreferences.toml described in Configuring the Programming Environment |
ONEAPI_LTS | 1 | Enables the LTS driver workarounds |
ZE_FLAT_DEVICE_HIERARCHY | FLAT | Exposes each GPU's two tiles as separate devices, giving 12 per node |
JULIA_MPI_HAS_ONEAPI | 1 | Enables oneAPI-aware MPI |
Loading Julia¶
Load the Julia module:
By default, this loads the latest stable version of Julia. To load a specific version:
module load libraries/julia/1.12 # Latest version
module load libraries/julia/1.11 # Previous version
module load libraries/julia/1.10 # LTS (Long Term Support)
Pick one spelling and stay with it
/soft/modulefiles/libraries/julia is a symlink into the Julia installation, so the same modulefile can be reached two ways — as libraries/julia (via module use /soft/modulefiles, shown above) or as julia (via module use /soft/modulefiles/libraries). They are the same module, but Lmod treats them as different names, and the modulefile declares a conflict with itself. Loading the second on top of the first fails:
Lmod has detected the following error: Cannot load module
"libraries/julia/1.12" because these module(s) are loaded:
julia
If you already load Julia from your shell configuration, either use the same spelling in your job scripts or module unload julia first.
Version Policy¶
Aurora maintains three Julia versions:
- Latest stable release (currently 1.12): The most recent stable version with the newest features and performance improvements
- Previous version (currently 1.11): The previous stable release for compatibility with recent projects
- LTS (Long Term Support) (currently 1.10): Provides long-term stability with bug fixes but no new features, ideal for production workloads requiring consistency
When new versions are released, the oldest non-LTS version is retired (removed from the system and no longer available), and the LTS version is updated according to the Julia LTS release schedule.
Configuring the Programming Environment¶
To leverage Aurora's architecture, you must configure Julia to use the system's optimized libraries for MPI.jl, oneAPI.jl, and HDF5.jl. For a modern, interactive development experience, we recommend using Visual Studio Code with the official Julia and Remote - SSH extensions.
The Julia module on Aurora is pre-configured with system-specific preferences (via LocalPreferences.toml in the system load path) so that these packages use the correct system libraries: the MPICH provided by the mpich module, the Intel Level Zero loader, Compute Runtime (NEO) and Graphics Compiler (IGC) installed under /usr/lib64, and the system parallel HDF5 built against that same MPICH.
Install the required packages in your Julia environment with the following commands:
Intel LTS Driver Stack¶
Aurora runs Intel's long-term-servicing (LTS) branch of the Compute Runtime (NEO 25.18.33578 / IGC 2.11.29 / Level Zero 1.24), not the rolling releases that oneAPI.jl targets by default. That branch predates a number of driver and compiler fixes, several of which corrupt results silently rather than raise an error.
oneAPI.jl v2.8 added workarounds for these behind a single opt-in switch, ONEAPI_LTS, which the Julia module sets for you. No action is required on your part beyond using a recent enough oneAPI.jl.
oneAPI.jl v2.8 or newer is required
Older versions ignore ONEAPI_LTS entirely, without a warning, leaving the workarounds disabled. The resulting failures are silent: wrong numerical results from reductions over strided arrays, or a banned Level Zero context that surfaces much later as a ZE_RESULT_ERROR_UNKNOWN at an unrelated call.
If you are reusing an environment whose Manifest.toml predates this, run Pkg.update("oneAPI") and confirm the resolved version with Pkg.status("oneAPI").
Recompile required
Julia does not invalidate precompilation caches when an environment variable changes. If you precompiled oneAPI.jl in a depot created before the LTS switch was enabled, run Pkg.precompile() once. This affects first-call latency only, never correctness.
Confirm that the LTS path is active (on a compute node — see Verify Configuration below):
oneAPI.versioninfo() does not report this, so the check above is the only way to confirm it.
What changes on the LTS path¶
| Change | What it means for you |
|---|---|
| Kernels are compiled with the Khronos SPIR-V translator instead of LLVM's SPIR-V back-end | Transparent — the LTS runtime does not accept the back-end's output. @device_code_spirv reports which tool was used in its Generator: line |
BFloat16 is unavailable | Forced off regardless of what the hardware reports, because the LTS SPIR-V stack cannot translate the LLVM bfloat type. Other floating-point types are unaffected |
| Reductions over strided inputs are materialized into a dense array first | sum(transpose(x)), a == transpose(b) and ishermitian(x) cost an extra allocation and copy. Without this the LTS compiler miscompiles them and silently returns wrong numbers |
| Reductions that keep the contiguous leading dimension use a coalesced kernel | sum(A; dims=2) and friends stay correct, but get less parallelism when there are few output slices |
| Buffers are freed only after draining the work that may reference them | A garbage collection can block until outstanding GPU work completes |
See Intel LTS driver stack in the oneAPI.jl documentation for the full details.
Oversubscribing a tile¶
The module sets ZE_FLAT_DEVICE_HIERARCHY=FLAT, so each of a node's 6 GPUs is exposed as 2 separate tiles, for 12 devices in total. The job submission script below uses 12 ranks per node, giving one rank per tile.
If you place more ranks than tiles on a node, the LTS stack can silently drop the tail of a kernel or a copy — the last work-items simply never land, with no error reported. Setting
eliminates it, at roughly a 3x throughput cost. Leave it unset for the one-rank-per-tile mapping used below.
oneAPI-Aware MPI¶
oneAPI-aware MPI is enabled by default on Aurora. You can pass oneArray objects directly to MPI.jl functions without explicit host-device transfers, enabling efficient GPU-to-GPU communication across nodes:
using oneAPI, MPI
MPI.Init()
# Create a oneArray and pass it directly to MPI operations
data = oneAPI.rand(Float64, 100)
MPI.Allreduce!(data, +, MPI.COMM_WORLD) # GPU-to-GPU communication
Do not configure MPIPreferences yourself
MPIPreferences is a dependency of MPI.jl and does not need to be added explicitly. Do not run MPIPreferences.use_system_binary(), and do not keep an [MPIPreferences] section in your own project's LocalPreferences.toml. Either one shadows the system configuration and pins MPI.jl to a specific MPICH installation path, which breaks outright once that Aurora programming environment release is retired.
The system configuration instead tracks whichever MPICH the mpich module provides at deployment time, so it follows Aurora's programming environment updates. If you have an [MPIPreferences] section of your own, delete it and restart Julia.
Parallel HDF5¶
HDF5.jl is pre-configured against Aurora's system HDF5, which is built with MPI support against the same MPICH that MPI.jl uses. Collective parallel I/O therefore works without any additional setup — you do not need to build or configure HDF5 yourself.
Unlike the GPU checks, this one works on a login node:
If has_parallel() returns false, HDF5.jl has fallen back to its own binary artifact, which is serial. The usual cause is a [HDF5] section in your own project's LocalPreferences.toml shadowing the system one; remove it and restart Julia.
To write collectively, open the file with a communicator and pass dxpl_mpio=:collective. Each rank then writes its own slice of a single shared dataset:
See the Parallel HDF5 section of the HDF5.jl documentation for the full API.
Serial writes remain valid
The π example below opens the file without a communicator and writes from rank 0 only. That is still correct, and is often the better choice for small outputs — parallel HDF5 pays off when every rank contributes a large slice of the data.
The HDF5 version tracks Aurora's programming environment and changes when the mpich default moves, so treat the version reported above as indicative rather than fixed.
Verify Configuration on a Compute Node¶
The Aurora login nodes do not have GPU access. You must request an interactive job to test your GPU configuration.
Two further checks are worth running once per environment. The first confirms that the LTS workarounds are active, the second that MPI.jl picked up the system MPICH:
If oneAPI.oneL0.LTS[] is false, your oneAPI.jl predates v2.8 or was precompiled before the switch was set — see the warnings under Intel LTS Driver Stack.
Example Julia Code for Approximating Pi¶
Job Submission Script¶
This PBS script requests resources and launches the Julia application using mpiexec:
Package X is required but does not seem to be installed
If every rank fails this way, the job is almost certainly running against the wrong depot. Look for this line in the module's output:
Exporting JULIA_DEPOT_PATH in the submission script, as above, is the fix. Setting it only in ~/.bashrc is not enough, because a batch job does not necessarily source it.