Skoči na vsebino

Vzporejanje nalog

V tem razdelku poženemo program count-words.py na gruči – najprej kot en posel, ki ga nato s pomočjo lupine razbijemo na naloge, ki jih izvajamo vzporedno. Več informacij o Slurmu najdemo v navodilih in Osnovah superračunalništva, tu pa si za ogrevanje ogledamo nekaj primerov.

Izvajanje na gruči

Kot običajno lahko program za štetje besed na gruči poženemo neposredno s srun. Če imamo na voljo rezervacijo, jo uporabimo s parametrom --reservation=<ID>. Preštejmo besede v prvih sto recenzijah s seznama pos.list:

$ head -100 pos.list > pos.100
$ srun python3 count-words.py $(cat pos.100) > pos100.count
srun: job 814213 queued and waiting for resources
srun: job 814213 has been allocated resources
head -100 pos.list > pos.100
srun python3 count-words.py $(cat pos.100) > pos100.count

Spomnimo se, da z > pa preusmerimo izpis v datoteko, z $(…) pa izpis ukaza v oklepajih uporabimo kot argumente drugemu ukazu. Izpišimo najpogostejših pet besed:

$ cat pos100.count | sort -nr -k 2 | head -5
the 1444
be 1016
a 943
and 749
of 727
cat pos100.count | sort -nr -k 2 | head -5

Rezultat ni presenetljiv. Pri analizi naravnega jezika pogosto izpustimo t.i. stop words, kot so a, the in be. Podrobnejšo analizo recenzij prepuščamo bralcu za vajo, tu pa napišimo še skripto sbatch, ki prešteje besede v vseh datotekah na danem seznamu.

1
2
3
4
5
#!/bin/sh
#SBATCH --job-name=count-simple
#SBATCH --output=count-simple.out

python3 count-words.py $(cat $1)
Izvorna datoteka: count-simple.sh

Poleg vrstice #! smo skripto opremili še z argumenti za Slurm. Lupina bo te vrstice med izvajanjem skripte ignorirala, saj se začnejo z #, ki označuje komentar. Prva vrstica je tako v resnici namenjena operacijskemu sistemu, drugi dve pa Slurmu.

V skripti spremenljivka $1 hrani vrednost prvega argumenta, s katerim bomo podali datoteko s seznamom recenzij, torej npr. pos.list ali neg.list. Seznam izpišemo s cat in izpis uporabimo za argumente programu count-words.py.

Namesto, da bi izpis programa count-words.py preusmerili v datoteko z >, smo tokrat z --output naročili Slurmu, naj izpis shrani v count-simple.out. Skripto pošljemo v izvajanje s programom sbatch:

$ sbatch count-simple.sh pos100.list
srun: job 916271 queued and waiting for resources
srun: job 916271 has been allocated resources
sbatch count-simple.sh pos100.list

Stanje svojih poslov preverimo s squeue --me --long.

Vzporejanje

Štetje besed je primer nerodno paralelnega problema (angl. embarrassingly parallel), saj je število besed v eni datoteki popolnoma neodvisno od števila besed v ostalih. Zato lahko poljubne podmnožice datotek obdelamo vzporedno in nato seštejemo dobljene frekvence. Najprej pa si oglejmo nekaj načinov, kako posel razdelimo na naloge.

Z lupino

Za vzporedno izvajanje nalog v skriptah sbatch lahko uporabimo kar orodja, ki nam jih za ta namen ponuja lupina. Proces zaženemo v ozadju tako, da na konec ukaza dodamo &. Z wait počakamo, da se končajo vsi procesi v ozadju. Naslednja skripta pošlje v obdelavo pet nalog hkrati, pri čemer N‐ta naloga traja N sekund, in počaka, da se vse končajo. Ukazu srun moramo dodati stikalo --exclusive in s tem prisilimo Slurm, da vsaki nalogi dodeli svoje procesorsko jedro.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#!/bin/sh
#SBATCH --job-name=multitask
#SBATCH --ntasks=5

# zaženi $SLURM_NTASKS nalog
for task_id in $(seq $SLURM_NTASKS) ; do
    # počakaj $task_id sekund
    srun --exclusive --ntasks=1 sleep $task_id &
done
wait  # počakaj, da se končajo vse naloge
Izvorna datoteka: job-for.sh

Okoljsko spremenljivko $SLURM_NTASKS nastavi Slurm, ko zažene našo skripto. V zanki ustvarimo po eno nalogo za vsako od števil med 1 in 5 , ki jih izpiše ukaz seq $SLURM_NTASKS. Vsaka naloga pokliče sleep, ki počaka dano število sekund.

Z nizi poslov

Druga in pogosto enostavnejša možnost za vzporejanje so Slurmovi nizi poslov (angl. array jobs). Spodnja skripta zažene enake naloge kot zgornja, le da večino administracije prevzame Slurm, mi pa definiramo samo ukaz za posamezno nalogo.

1
2
3
4
5
6
#!/bin/sh
#SBATCH --job-name=arraytask
#SBATCH --array=1-5
#SBATCH --output=arraytask_%A-%a.out

sleep $SLURM_ARRAY_TASK_ID
Izvorna datoteka: job-array.sh

Argument --array=1-5 pove Slurmu, naj skripto zažene za vsako od števil med 1 in 5. Slurm za vsako nalogo nastavi okoljsko spremenljivko $SLURM_ARRAY_TASK_ID na ustrezno število. Ta spremenljivka ima torej isto vrednost kot prej $task_id, le da smo slednji morali vrednosti nastavljati sami s pomočjo zanke in seq.

Slurm definira še vrsto drugih spremenljivk, npr. $SLURM_ARRAY_TASK_COUNT s številom vseh nalog. Te spremenljivke lahko uporabljamo kjerkoli v skripti, razen v vrsticah #SBATCH, ki jih tolmači Slurm in ne lupina. Zato moramo v argumentu --output namesto $SLURM_ARRAY_JOB_ID in $SLURM_ARRAY_TASK_ID uporabiti simbola %A in %a z istima vrednostima. Seznam vseh okoljskih spremenljivk in simbolov, ki jih definira Slurm, najdemo v sbatch(1).

Z GNU parallel

Za konec si oglejmo še parallel(1), ki vzporeja naloge podobno kot nizi poslov, a ni odvisen od Slurma. Recimo, da želimo stisniti vsako datoteko v imeniku files in pri tem zagnati do deset procesov hkrati:

$ parallel --jobs 10 "gzip {}" ::: files/*
parallel --jobs 10 "gzip {}" ::: files/*

V narekovajih smo podali ukaz, ki ga želimo vzporediti. Pri tem bo parallel simbol {} zamenjal z imenom posamezne datoteke. Sledi :::, ki zaznamuje konec ukaza, in seznam datotek. Podobno kot pri nizih poslov lahko v ukazu uporabimo simbol {#} oziroma spremenljivko $PARALLEL_SEQ, da dobimo zaporedno številko trenutne naloge.

Poročilo o opravljenih nalogah lahko shranimo v datoteko z argumentom --joblog status.log. Če dodamo še --resume, bo parallel ob ponovnem zagonu obdelavo nadaljeval tam, kjer je bila prekinjena (s Ctrl+C ali kako drugače). Ko želimo cel posel zagnati znova, moramo datoteko status.log najprej izbrisati.

Splošna skripta sbatch z uporabo programa parallel izgleda tako:

1
2
3
4
5
6
7
#!/bin/sh
#SBATCH --job-name=paratask
#SBATCH --ntasks=5
#SBATCH --cpus-per-task=4

parallel --jobs $SLURM_NTASKS --joblog paratask.log --resume \
    srun --ntasks=1 -c$SLURM_CPUS_PER_TASK "gzip {1}" ::: "$@"
Izvorna datoteka: job-parallel.sh

V tem primeru zahtevamo pet nalog, pri čemer dobi vsaka štiri procesorska jedra. Prvo vrednost kot $SLURM_NTASKS podamo parallel, ki zažene posamezne naloge s pomočjo srun. Ta poskrbi, da ima vsaka naloga na voljo $SLURM_CPUS_PER_TASK jeder.

Poročilo o poslu

O nekem poslu, ki smo ga zagnali na gruči nas pogosto zanima tudi poročilo o izvajanju. To si lahko ogledamo s pomočjo ukaza sacct, ki nam omogoča da prikažemo statistike o poslu, kot so število rezerviranih jeder, porabljen CPE čas, pretečen čas, količina pomnilnika, ki ga je posel potreboval in podobno.

Primer izpisa statistike za posel z identifikatorjem 918966:

$ sacct --format JobID,State,AllocCPUS,TotalCPU,Elapsed,MaxRSS --job 918996
JobID        State      AllocCPUS  TotalCPU   Elapsed    MaxRSS
------------ ---------- ---------- ---------- ---------- ----------
918966_1      COMPLETED          1  00:00.045   00:00:01
918966_1.ba+  COMPLETED          1  00:00.042   00:00:01      1812K
918966_1.ex+  COMPLETED          1  00:00.002   00:00:01      1544K
918966_2      COMPLETED          1  00:00.043   00:00:02
918966_2.ba+  COMPLETED          1  00:00.041   00:00:02      1808K
918966_2.ex+  COMPLETED          1  00:00.002   00:00:02      1512K 
sacct --format JobID,State,AllocCPUS,TotalCPU,Elapsed,MaxRSS -j 918996

Z zastavico --format nastavimo katere informacije o poslu naj se izpišejo. Celoten seznam lahko najdete na povezavi. V zgornjem primeru smo izpisali: identifikator posla (JobID), njegovo stanje (State), število uporabljenih jeder (AllocCPUS), porabljen CPE čas (TotalCPU), porabljen čas (Elapsed) in najvišjo porabo pomnilnika (MaxRSS). Opazite lahko, da je statistika o poslu razdeljena po več vrsticah. To je zato, ker je bil omenjeni posel v bistvu niz (angl. array job) dveh poslov. Informacije se izpišejo za vsakega v nizu. Končnici ba+ in ex+ sta okrajšavi za batch in extern. Prva označuje del posla, ki je uporabljal vire v okviru Slurma (ta nas zanima) druga pa dele posla, ki so uporabljali vire izven nadzora Slurma (npr. seje ssh).

Optimizacija

Če posel razdelimo na n neodvisnih nalog, bi načeloma lahko pričakovali n‐kratno pohitritev. V praksi to običajno ne drži, saj Slurm tudi za razporejanje nalog potrebuje nekaj časa. Če bi hkrati poslali v obdelavo več deset ali sto tisoč nalog, bi za razporejanje lahko porabili več časa kot za samo štetje. Zato se pri velikem naboru vhodnih datotek splača vzporejati malo pametneje.

Pri nizih poslov lahko argument --array=1-1000 zamenjamo z --array=1-1000%20, da zaganjamo po največ dvajset nalog hkrati. S tem zmanjšamo obremenitev razporejevalnika, a podaljšamo čas izvajanja. Program parallel to naredi samodejno glede na argument --jobs. Tudi skripto iz prvega primera bi lahko razširili na podoben način, a si bomo v naslednji vaji raje ogledali, kako v eni nalogi obdelamo več datotek. Tako bomo lahko za naš posel uporabili poljubno število nalog.

Ogledali si bomo tudi, kako združimo rezultate nalog v končni rezultat in celoten postopek zapisali v skripto. Pri tem nam bo prišla prav možnost --dependency za srun/sbatch. Z njo poskrbimo, da se posel zažene šele, ko je izpolnjen določen pogoj. Na primer:

  • srun --dependency after:42+10 …: zaženi posel deset minut po začetku posla 42,
  • srun --dependency afterok:42 …: zaženi posel, ko se je posel 42 zaključil uspešno,
  • srun --dependency afternotok:42 …: zaženi posel, ko se je posel 42 zaključil neuspešno.

Številko posla v človeku prijazni obliki izpiše sbatch, ko posel pošlje razporejevalniku. Če sbatch zaženemo z argumentom --parsable, bo izpisal samo številko brez dodatnega besedila, kar nam pride prav pri pisanju skript.

Preveri svoje znanje

Vaja

Vaja 03: skripto za štetje besed razdeli na več nalog s pomočjo lupine in Slurma. Posel zaženi in združi rezultate vseh nalog. Skripto predelaj tako, da bo vsaka naloga obdelala določeno število datotek.