Julia

From CC Doc
Jump to navigation Jump to search
Other languages:

Julia is a programming language that was designed for performance, ease of use and portability. It is is available as a module on Compute Canada clusters.

Compiling packages

The first time you add a package to a Julia project (using Pkg.add or the package mode), the package will be downloaded, installed in ~/.julia, and pre-compiled. The same package can be added to different projects, in which case the data in ~/.julia will be reused. Different versions of the same package can be added to different projects; the required package versions will coexist in ~/.julia. (Compared to Python, Julia projects replace “virtual environments” while avoiding code duplication.)

From Julia 1.6 onwards, Julia packages include their binary dependencies (such as libraries). There is therefore no need to load any software module, and we recommend not to.

With Julia 1.5 and earlier, you may run into problems if a package depends on system-provided binaries. For instance, JLD depends on a system-provided HDF5 library. On a personal computer, Julia attempts to install such a dependency using yum or apt with sudo. This will not work on a Compute Canada cluster; instead, some extra information must be provided to allow Julia's package manager (Pkg) to find the HDF5 library.

$ module load gcc/7.3.0 hdf5 julia/1.4.1
$ julia
julia> using Libdl
julia> push!(Libdl.DL_LOAD_PATH, ENV["HDF5_DIR"] * "/lib")
julia> using Pkg
julia> Pkg.add("JLD")
julia> using JLD

If we were to omit the Libdl.DL_LOAD_PATH line from the above example, it would happen to work on Graham because Graham has HDF5 installed system-wide. It would fail on Cedar because Cedar does not. The best practice on any Compute Canada system, though, is that shown above: Load the appropriate module first, and use the environment variable defined by the module (HDF5_DIR in this example) to extend Libdl.DL_LOAD_PATH. This will work uniformly on all systems.

Package files and storage quotas

In the example above, installing just the JLD package creates a ~/.julia tree with 18673 files and directories and using 236M of space, almost 5% of a standard user's quota for /home. It's worth remembering that installing a lot of packages will consume a lot of space.

Available versions

We have removed earlier versions of Julia (< 1.0) because the old package manager was creating vast numbers of small files which in turn caused performance issues on the parallel file systems. Please start using Julia 1.4, or newer versions.

Question.png
[name@server ~]$ module spider julia
--------------------------------------------------------
  julia: julia/1.4.1
--------------------------------------------------------
[...]
    You will need to load all module(s) on any one of the lines below before the "julia/1.4.1" module is available to load.

      nixpkgs/16.09  gcc/7.3.0
[...]
Question.png
[name@server ~]$ module load gcc/7.3.0 julia/1.4.1

Porting code from Julia 0.x to 1.x

In the summer of 2018 the Julia developers released version 1.0, in which they stabilized the language API and removed deprecated (outdated) functionality. To help updating Julia programs for version 1.0, the developers also released version 0.7.0. Julia 0.7.0 contains all the new functionality of 1.0 as well as the outdated functionalities from 0.x versions, which will give deprecation warnings when used. Code that runs in Julia 0.7 without warnings should be compatible with Julia 1.0.

Running Julia with multiple processes on clusters

The following is an example of running a parallel Julia code computing pi using 100 cores across nodes on a cluster


File : run_julia_pi.sh

#!/bin/bash
#SBATCH --ntasks=100
#SBATCH --cpus-per-task=1
#SBATCH --mem-per-cpu=1024M
#SBATCH --time=0-00:10

srun hostname -s > hostfile
sleep 5
julia --machine-file ./hostfile ./pi_p.jl 1000000000000


In this example, the command

srun hostname -s > hostfile

generates a list of names of the nodes allocated and writes it to the text file hostfile. Then the command

julia --machine-file ./hostfile ./pi_p.jl 1000000000000

starts one main Julia process and 100 worker processes on the nodes specified in the hostfile and runs the program pi_p.jl in parallel.

Running Julia with MPI

You must make sure Julia's MPI is configured to use our MPI libraries. To install correctly, run the following:

module load StdEnv/2020  julia/1.5.2
export JULIA_MPI_BINARY=system
export JULIA_MPI_PATH=$EBROOTOPENMPI
export JULIA_MPI_LIBRARY=$EBROOTOPENMPI/lib64/libmpi.so
export JULIA_MPI_ABI=OpenMPI
export JULIA_MPIEXEC=$EBROOTOPENMPI/bin/mpiexec

Then start Julia and inside it run:

import Pkg;
Pkg.add("MPI")
using MPI

To use afterwards, run (with two processes in this example):

module load StdEnv/2020  julia/1.5.2
mpirun -np 2 julia hello.jl

The hello.jl code here is:

using MPI
MPI.Init()
comm = MPI.COMM_WORLD
print("Hello world, I am rank $(MPI.Comm_rank(comm)) of $(MPI.Comm_size(comm))\n")
MPI.Barrier(comm)

Configuring Julia's threading behavior

You can restrict the number of threads Julia can use by setting JULIA_NUM_THREADS=k, for example a single process on a 12 cpus-per-task job could use k=12. Setting the number of threads to the number of processors is a typical choice (although see Scalability for a discussion). In addition, one can 'pin' threads to cores, by setting JULIA_EXCLUSIVE to anything non-zero. As per the documentation this takes control of thread scheduling away from the OS, and pins threads to cores (sometimes referred to 'green' threads with affinity). Depending on the computation that threads execute, this can improve performance when one has precise information on cache access patterns or otherwise unwelcome scheduling patterns used by the OS. Setting JULIA_EXCLUSIVE works only if your job has exclusive access to the compute nodes (all available CPU cores were allocated to your job). Since SLURM already pins processes and threads to CPU cores, asking Julia to re-pin threads may not lead to any performance improvement.

Related is the variable JULIA_THREAD_SLEEP_THRESHOLD, controlling the number of nanoseconds after which a spinning thread is scheduled to sleep. A value of infinite (as string) indicates no sleeping on spinning. Changing this variable can be of use if many threads are contending frequently for a shared resource, where it can be preferred to schedule out spinning threads more quickly. Under heavy contention, spinning would only increase CPU load. Conversely, in a situation where a resource is only very infrequently contended, lower latency can result from prohibiting threads to sleep, that is, setting the threshold to infinity.

It goes without saying that configuring these values should only be done when one has accurately profiled any contention issues. Given the high pace at which Julia, and especially its threading sub-system Base.Threads, evolves, one should always consult the documentation to ensure changing the default configuration will have only the expected behaviour as a result.

Videos

A series of online seminars produced by SHARCNET: