-
Julia-ohjelmointikieli
Julia-ohjelmointikieli
Julia-ohjelmointikieli on suorituskykyinen, dynaaminen ohjelmointikieli. Julia soveltuu erinomaisesti tieteelliseen laskentaan, koska se pystyy kääntämään tehokasta natiivikoodia LLVM:n avulla ja sisältää standardikirjastossa matemaattisia funktioita, rinnakkaislaskentaominaisuuksia sekä paketinhallinnan. Lisäksi Julian syntaksi on intuitiivinen ja helppo oppia, multiple-dispatch-paradigma mahdollistaa koostettavan koodin kirjoittamisen, mikä lisää olemassa olevan koodin uudelleenkäytettävyyttä, ja ympäristöt mahdollistavat koodin suorittamisen toistettavalla tavalla.
Lisenssi
Julia-ohjelmointikieli on lisensoitu vapaalla ja avoimen lähdekoodin MIT-lisenssillä.
Viittaaminen
Jos käytät Juliaa tutkimuksessasi, viittaa seuraavaan artikkeliin:
Jeff Bezanson, Alan Edelman, Stefan Karpinski, and Viral B. Shah (2017). Julia: A Fresh Approach to Numerical Computing. SIAM Review, 59(1), 65–98. DOI: 10.1137/141000671
Jos haluat löytää oikean viittauksen tietylle Julia-paketille, voit käyttää komentoa pkg> status tai tutustua paketin dokumentaatioon ja repositorioon.
Saatavuus
Julia-ohjelmointikieli on käytettävissä Roihu-CPU:ssa, Roihu-GPU:ssa, Mahdissa ja LUMIssa komentorivikäyttöliittymästä moduulijärjestelmän kautta. Se on käytettävissä myös selainkäyttöliittymässä Jupyterin ja VSCoden kautta.
Jos kohtaat ongelmia Julian käytössä klusterissa, sinun kannattaa ottaa yhteyttä servicedeskiin.
Käyttö
Julia-moduulin käyttäminen
Julia-ohjelmointikieli on saatavilla julia-moduulista.
Julia-moduulin lataamisen jälkeen voimme käyttää Juliaa komennolla julia.
Ilman argumentteja se käynnistää interaktiivisen Julia-REPL:n.
Saatavilla olevat komentorivivalinnat näet komennolla julia --help tai lukemalla manuaalisivun man julia.
Jos sinulla on kysymyksiä Julia-ohjelmointikielen ominaisuuksista, suosittelemme virallista dokumentaatiota ja Discourse -kanavaa.
Paketinhallinnan käyttäminen
Vakiotapa asentaa Julia-paketteja on käyttää standardikirjaston paketinhallintaa Pkg.
Juliassa se voidaan tuoda käyttöön seuraavasti:
Yleisiä käyttämiämme toimintoja ovat Pkg.add pakettien lisäämiseen, Pkg.activate ympäristöjen aktivointiin ja Pkg.instantiate kaikkien aktiivisessa ympäristössä määriteltyjen pakettien asentamiseen.
Pdokumentaatio sisältää lisätietoa Julian paketinhallinnan käytöstä.
Julia depot -hakemiston sijoittaminen
Julia depot -polun ensimmäinen hakemisto määrittää, minne Julia tallentaa asennetut paketit, käännetyt tiedostot, lokitiedostot ja muut depotit.
Oletusarvoisesti se on $HOME/.julia.
Kotihakemistolla on suhteellisen pieni kiintiö Roihussa, Mahdissa ja LUMIssa.
Jos asennat suuria paketteja, suosittelemme sijoittamaan depot-hakemiston Projapplin alle, jotta kiintiö ei lopu kesken.
Voimme vaihtaa depot-hakemiston lisäämällä uuden hakemiston ympäristömuuttujan JULIA_DEPOT_PATH alkuun.
Voimme esimerkiksi käyttää seuraavaa korvaamalla <project> CSC-projektilla.
Tämän jälkeen voit turvallisesti poistaa oletus-depot-hakemiston komennolla rm -r $HOME/.julia.
Lisätietoa löytyy depot path -dokumentaatiosta.
Monisäikeisyys
Julia tarjoaa Threads-kirjaston monisäikeisyyteen.
Se sisältyy peruskirjastoon ja tuodaan oletuksena käyttöön Julia-istunnossa.
Voimme käynnistää Julian usealla säikeellä asettamalla ympäristömuuttujan JULIA_NUM_THREADS tai käynnistämällä Julian valinnalla --threads, joka ohittaa ympäristömuuttujan arvon.
Jos Julia-moduuli on ladattu Slurm-työssä eikä ympäristömuuttujaa ole asetettu, sen arvoksi asetetaan pyydettyjen CPU-ytimien määrä (--cpus-per-task).
Säikeiden oletusmäärä on yksi.
Suosittelemme lukemaan Julian manuaalin monisäikeisyys -osion lisätietoja varten.
Moniprosessointi ja hajautettu laskenta
Distributed ja SlurmClusterManager.jl
Moniprosessointiin ja hajautettuun laskentaan Julia tarjoaa Distributed-standardikirjaston.
Käytämme sitä moniprosessointiin paikallisella solmulla.
Voimme laajentaa Distributed-kirjastoa asentamalla SlurmClusterManager.jl-paketin, jonka avulla voimme lisätä työprosessit useille solmuille Slurmin kautta käyttäen SlurmManager-hallintaa.
Suosittelemme lukemaan Julian manuaalin moniprosessointi ja hajautettu laskenta -osion lisätietoja varten.
MPI.jl
Voimme käyttää MPI:tä hajautettuun laskentaan, erityisesti useiden solmujen yli, Juliassa Roihu-CPU:ssa, Roihu-GPU:ssa, Mahdissa ja LUMIssa MPI.jl-paketin avulla.
Voimme asentaa sen paketinhallinnalla seuraavasti:
Voimme ladata julia-mpi-moduulin, joka asettaa ympäristöön globaalit asetukset niin, että MPI.jl käyttää järjestelmän MPI-asennusta ja oikeaa komentoa MPI-prosessien käynnistämiseen.
Lisätietoja varten suosittelemme lukemaan MPI.jl-dokumentaation.
GPU-ohjelmointi
CUDA.jl
Roihu-GPU:n ja Mahdin GPU-solmut sisältävät NVidia-GPU:ita, joita voidaan ohjelmoida CUDA:n avulla.
Voimme asentaa CUDA.jl-paketin CUDA-ohjelmointia varten Juliassa paketinhallinnalla seuraavasti:
Voimme ladata julia-cuda-moduulin, joka asettaa ympäristöön globaalit asetukset niin, että CUDA.jl käyttää järjestelmän CUDA-asennusta.
Lisätietoja varten suosittelemme lukemaan CUDA.jl-dokumentaation.
AMDGPU.jl
LUMIn GPU-solmut sisältävät AMD-GPU:ita.
Voimme asentaa AMDGPU.jl-paketin AMD-GPU:iden ohjelmointia varten Juliassa paketinhallinnalla seuraavasti:
Voimme ladata julia-amdgpu-moduulin, joka asettaa ympäristöön globaalit asetukset niin, että AMDGPU.jl käyttää järjestelmän ROCm-asennusta.
Lisätietoja varten suosittelemme lukemaan AMDGPU.jl-dokumentaation.
Lisälukemista
Jos haluat lukea lisää rinnakkais- ja suorituskykylaskennasta Julian avulla, suosittelemme ENCCS:n materiaalia Julia for high-performance scientific computing sekä Kjartan Thor Wikfeldtin kirjoitusta A brief tour of Julia for high-performance computing. HLRS:n koulutusmateriaali kurssille Julia for High-Performance Computing tarjoaa syvällisen katsauksen suorituskykyisen koodin ohjelmointiin Julian avulla. Lopuksi Julia on HPC Clusters listaa yleisiä huomioita Julian käytöstä ja asentamisesta HPC-klusterissa.
Julia-eräajojen suorittaminen CSC:n klustereissa
Tämä osio sisältää esimerkkejä erilaisten Julia-eräajojen suorittamisesta Roihu-CPU-, Roihu-GPU-, Mahti- ja LUMI-klustereissa.
Ne havainnollistavat yllä kuvatun Julia-ympäristön käyttöä erilaisissa eräajoissa.
Ne on mukautettu yleisistä ohjeista töiden ajamiseen Roihussa ja Mahdissa sekä LUMIssa.
Huomaa, että emme käytä srun-komentoa prosessien käynnistämiseen eräajokomentosarjassa.
Sen sijaan käytämme Juliaa prosessien hallintaan tai kutsumme srun-komentoa Julia-koodin sisältä.
Ennen esimerkkien ajamista meidän täytyy instansioida Julia-projekti kirjautumissolmulla.
Toisin sanoen suorita seuraava komento siinä hakemistossa, jossa Julia-ympäristösi sijaitsee ja jossa Project.toml-tiedosto on.
Voit käyttää useita säikeitä --threads=10, mikä nopeuttaa esikäännöstä.
Sarjaohjelma
Käytämme seuraavaa hakemistorakennetta ja oletamme, että se on työhakemistomme.
.
├── Project.toml # Julia environment
├── batch.sh # Slurm batch script
└── script.jl # Julia script
Monisäikeisyys yhdellä solmulla
Käytämme seuraavaa hakemistorakennetta ja oletamme, että se on työhakemistomme.
.
├── Project.toml # Julia environment
├── batch.sh # Slurm batch script
└── script.jl # Julia script
# Number of threads
n = Threads.nthreads()
println(n)
# Lets fill the id of each thread to the ids array.
ids = zeros(Int, n)
Threads.@threads for i in eachindex(ids)
ids[i] = Threads.threadid()
end
println(ids)
# Alternatively, we can use the @spawn macro to run task on threads.
ids = zeros(Int, n)
@sync for i in eachindex(ids)
Threads.@spawn ids[i] = Threads.threadid()
end
println(ids)
Moniprosessointi yhdellä solmulla
Käytämme seuraavaa hakemistorakennetta ja oletamme, että se on työhakemistomme.
.
├── Project.toml # Julia environment
├── batch.sh # Slurm batch script
└── script.jl # Julia script
using Distributed
# We set one worker process per core.
proc_num = Sys.CPU_THREADS
# Environment variables that we pass to the worker processes.
# We set the thread count to one since each process uses one core.
proc_env = [
"JULIA_NUM_THREADS"=>"1",
"JULIA_CPU_THREADS"=>"1",
"OPENBLAS_NUM_THREADS"=>"1",
]
# We add worker processes to the local node using LocalManager.
addprocs(proc_num; env=proc_env, exeflags="--project=.")
# We use the `@everywhere` macro to include the task function in the worker processes.
# We must call `@everywhere` after adding worker processes; otherwise the code won't be included in the new processes.
@everywhere function task()
return (worker=myid(), hostname=gethostname(), pid=getpid())
end
# We run the task function in each worker process.
futures = [@spawnat worker task() for worker in workers()]
# Then, we fetch the output from the processes.
outputs = fetch.(futures)
# Remove processes after we are done.
rmprocs.(workers())
# Print the outputs of master and worker processes.
println(task())
println.(outputs)
Moniprosessointi useilla solmuilla
Käytämme seuraavaa hakemistorakennetta ja oletamme, että se on työhakemistomme.
.
├── Project.toml # Julia environment
├── batch.sh # Slurm batch script
└── script.jl # Julia script
[deps]
SlurmClusterManager = "c82cd089-7bf7-41d7-976b-6b5d413cbe0a"
Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b"
using Distributed
using SlurmClusterManager
# Environment variables that we pass to the worker processes.
# We set the thread count to one since each process uses one core.
n = get(ENV, "SLURM_CPUS_PER_TASK", "1")
proc_env = [
"JULIA_NUM_THREADS"=>"$n",
"JULIA_CPU_THREADS"=>"$n",
"OPENBLAS_NUM_THREADS"=>"$n",
]
# We add worker processes across the allocated nodes using SlurmManager
manager = SlurmManager(; launch_timeout=300)
addprocs(manager; env=proc_env, exeflags="--project=.")
# We use the `@everywhere` macro to include the task function in the worker processes.
# We must call `@everywhere` after adding worker processes; otherwise the code won't be included in the new processes.
@everywhere function task()
return (worker=myid(), hostname=gethostname(), pid=getpid())
end
# We run the task function in each worker process.
futures = [@spawnat worker task() for worker in workers()]
# Then, we fetch the output from the processes.
outputs = fetch.(futures)
# Remove processes after we are done.
rmprocs.(workers())
# Print the outputs of master and worker processes.
println(task())
println.(outputs)
MPI-ohjelma
Käynnistämme MPI-ohjelman Julian mpiexec-wrapper-funktion avulla.
Wrapper-funktio korvaa paikallisista asetuksista oikean komennon mpirun-muuttujaan MPI-ohjelman suorittamista varten.
Komento on srun Roihussa, Mahdissa ja LUMIssa.
Wrapperin avulla voimme kirjoittaa joustavampaa koodia, esimerkiksi yhdistää MPI- ja ei-MPI-koodia, sekä siirrettävämpää koodia, koska MPI-ohjelmien suorituskomento voi vaihdella eri alustoilla.
Huomaa, että laajamittaisissa Julia MPI -ajoissa, joissa on tuhansia rankeja, meidän täytyy jakaa depot-hakemisto paikallisen solmun tallennustilaan tai muistiin ja muokata depot-poluja vastaavasti.
Muuten pakettien lataamisesta tulee erittäin hidasta.
Käytämme seuraavaa hakemistorakennetta ja oletamme, että se on työhakemistomme.
.
├── Project.toml # Julia environment
├── batch.sh # Slurm batch script
├── prog.jl # Julia MPI program
└── script.jl # Julia script
using MPI
MPI.Init()
comm = MPI.COMM_WORLD
rank = MPI.Comm_rank(comm)
size = MPI.Comm_size(comm)
println("Hello from rank $(rank) out of $(size) from host $(gethostname()) and process $(getpid()).")
MPI.Barrier(comm)
#!/bin/bash
#SBATCH --account=<project>
#SBATCH --partition=standard
#SBATCH --time=00:15:00
#SBATCH --nodes=2
#SBATCH --ntasks-per-node=128
#SBATCH --cpus-per-task=1
#SBATCH --mem-per-cpu=0
module use /appl/local/csc/modulefiles
module load julia
module load julia-mpi
julia --project=. script.jl
Yksi GPU
Käytämme seuraavaa hakemistorakennetta ja oletamme, että se on työhakemistomme.
.
├── Project.toml # Julia environment
├── batch.sh # Slurm batch script
└── script.jl # Julia script
#!/bin/bash
#SBATCH --account=<project>
#SBATCH --partition=small-g
#SBATCH --time=00:15:00
#SBATCH --nodes=1
#SBATCH --gpus-per-node=1
#SBATCH --ntasks-per-node=1
#SBATCH --cpus-per-task=8
#SBATCH --mem-per-cpu=1750
module use /appl/local/csc/modulefiles
module load julia
module load julia-amdgpu
julia --project=. script.jl
GPU-aware MPI
Käytämme seuraavaa hakemistorakennetta ja oletamme, että se on työhakemistomme.
prog.jl-koodi on mukautettu tästä lähteestä.
.
├── Project.toml # Julia environment
├── batch.sh # Slurm batch script
├── prog.jl # Julia GPU-aware MPI program
└── script.jl # Julia script
[deps]
CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba"
MPI = "da04e1cc-30fd-572f-bb4f-1f8673147195"
using MPI
using CUDA
const gpu_devices = CUDA.devices()
const num_devices = length(gpu_devices)
MPI.Init()
comm = MPI.COMM_WORLD
rank = MPI.Comm_rank(comm)
# select device
comm_l = MPI.Comm_split_type(comm, MPI.COMM_TYPE_SHARED, rank)
rank_l = MPI.Comm_rank(comm_l)
device = CUDA.device!(mod(rank_l, num_devices))
gpu_id = CUDA.deviceid(CUDA.device())
# select device
size = MPI.Comm_size(comm)
dst = mod(rank+1, size)
src = mod(rank-1, size)
println("rank=$rank rank_loc=$rank_l (gpu_id=$gpu_id - $device), size=$size, dst=$dst, src=$src")
N = 2^16 # Minimum array size for gdrcopy to work.
send_mesg = CuArray{Float64}(undef, N)
recv_mesg = CuArray{Float64}(undef, N)
fill!(send_mesg, Float64(rank))
CUDA.synchronize()
rank==0 && println("start sending...")
MPI.Sendrecv!(send_mesg, dst, 0, recv_mesg, src, 0, comm)
println("sum(recv_mesg) on proc $rank: $(sum(recv_mesg))")
rank==0 && println("done.")
MPI.Finalize()
#!/bin/bash
#SBATCH --account=<project>
#SBATCH --partition=gpumedium
#SBATCH --time=00:15:00
#SBATCH --nodes=2
#SBATCH --ntasks-per-node=4
#SBATCH --cpus-per-task=72
#SBATCH --gres=gpu:gh200:4
module purge
module load julia
module load julia-mpi
module load julia-cuda
julia --project=. script.jl
[deps]
AMDGPU = "21141c5a-9bdb-4563-92ae-f87d6854732e"
MPI = "da04e1cc-30fd-572f-bb4f-1f8673147195"
using MPI
using AMDGPU
MPI.Init()
comm = MPI.COMM_WORLD
rank = MPI.Comm_rank(comm)
# select device
comm_l = MPI.Comm_split_type(comm, MPI.COMM_TYPE_SHARED, rank)
rank_l = MPI.Comm_rank(comm_l)
device = AMDGPU.device_id!(rank_l+1)
gpu_id = AMDGPU.device_id(AMDGPU.device())
# select device
size = MPI.Comm_size(comm)
dst = mod(rank+1, size)
src = mod(rank-1, size)
println("rank=$rank rank_loc=$rank_l (gpu_id=$gpu_id - $device), size=$size, dst=$dst, src=$src")
N = 4
send_mesg = ROCArray{Float64}(undef, N)
recv_mesg = ROCArray{Float64}(undef, N)
AMDGPU.synchronize()
rank==0 && println("start sending...")
MPI.Sendrecv!(send_mesg, dst, 0, recv_mesg, src, 0, comm)
println("recv_mesg on proc $rank: $recv_mesg")
rank==0 && println("done.")
#!/bin/bash
#SBATCH --account=<project>
#SBATCH --partition=small-g
#SBATCH --time=00:15:00
#SBATCH --nodes=2
#SBATCH --gpus-per-node=8
#SBATCH --ntasks-per-node=8
#SBATCH --cpus-per-task=8
#SBATCH --mem-per-cpu=0
module use /appl/local/csc/modulefiles
module load julia
module load julia-mpi
module load julia-amdgpu
julia --project=. script.jl
Huomioita
Monisäikeisyys lineaarialgebrassa
Julia käyttää OpenBLASia oletusarvoisena LinearAlgebra-taustajärjestelmänä.
Ulkoiset lineaarialgebran taustajärjestelmät, kuten OpenBLAS, käyttävät sisäistä säikeistystä.
Voimme asettaa niiden säiemäärät ympäristömuuttujilla.
julia-moduuli asettaa ne CPU-säikeiden määrään.
Meidän täytyy varoa ytimien ylivaraamista, kun käytämme BLAS-operaatioita Julia-säikeiden tai -prosessien sisällä.
Voimme muuttaa BLAS-säikeiden määrää ajon aikana funktiolla BLAS.set_num_threads.
using LinearAlgebra
# Number of threads
n = Threads.nthreads()
# Define a matrix
X = rand(1000, 1000)
# Set the number of threads to one before performing BLAS operations of multiple Julia threads.
BLAS.set_num_threads(1)
Y = zeros(n)
Threads.@threads for i in 1:n # uses n Julia threads
Y[i] = sum(X * X) # uses one BLAS thread
end
# Set the number of threads back to the default when performing BLAS operation on a single Julia Thread.
BLAS.set_num_threads(n)
Z = zeros(n)
for i in 1:n # uses one Julia thread
Z[i] = sum(X * X) # uses n BLAS threads
end
On olemassa huomioitavia rajoitteita, jos OpenBLASissa käytetään BLAS-säikeille jotain muuta määrää kuin yksi tai kaikki ytimet.