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 > 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 si oglejmo še skripto za 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. Skripto pošljemo v izvajanje s programom sbatch:

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

Stanje svojih poslov lahko preverimo s squeue --me --long, podrobnejše poročilo o izvajanju posameznega posla pa si lahko ogledamo s pomočjo programa sacct. Primer poročila za posel z identifikatorjem 916271:

$ sacct --format JobID,State,Start,Elapsed,AllocCPUS,TotalCPU,MaxRSS --job 916271
       JobID      State               Start    Elapsed  AllocCPUS   TotalCPU     MaxRSS 
------------ ---------- ------------------- ---------- ---------- ---------- ---------- 
 916271       COMPLETED 2021-09-18T19:05:40   00:00:13          1  00:04.193            
 916271.bat+  COMPLETED 2021-09-18T19:05:40   00:00:13          1  00:04.191      1772K 
 916271.ext+  COMPLETED 2021-09-18T19:05:40   00:00:13          1  00:00.001      1392K
sacct --format JobID,State,Start,Elapsed,AllocCPUS,TotalCPU,MaxRSS --job 916271

Zastavica --format nastavi stolpce, ki jih želimo prikazati; celoten seznam najdemo v priročniku. V zgornjem primeru smo izpisali identifikator posla JobID, njegovo stanje State, začetek Start in trajanje Elapsed, število uporabljenih jeder AllocCPUS in količino porabljenega procesorskega časa TotalCPU ter pomnilnika MaxRSS. Vrstica bat+ oziroma batch označuje del posla, ki je uporabljal vire v okviru Slurma (ta nas zanima), medtem ko vrstica ext+ oziroma external označuje dele posla, ki so uporabljali vire izven nadzora Slurma (npr. seje ssh).

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 si oglejmo nekaj načinov, kako v poslu SLURM vzporedno zaženemo več nalog. Za demonstracijo uporabimo preprost program, ki počaka dano število sekund in konča:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/bin/sh

# V spremenljivko $trajanje shranimo vrednost prvega argumenta ($1).
# Če argument ni podan, uporabimo privzeto vrednost 12.
trajanje=${1:-12}

# Spremenljivka $$ hrani številko procesa, ki izvaja skripto.
proces=$$

# Spremenljivka $SLURM_STEPID hrani številko naloge v poslu
# (samo, če skripto izvaja sbatch).
naloga=${SLURM_STEPID:-N/A}

echo "Čakam $trajanje sekund (proces=$proces naloga=$naloga)"
sleep $trajanje
echo "Dočakal $trajanje sekund (proces=$proces, naloga=$naloga)"
Izvorna datoteka: task.sh. Ne pozabimo je označiti za izvršljivo s chmod +x task.sh.

S spodnjo skripto za sbatch zaženemo posel s petimi nalogami. Naloge poženemo eno za drugo, vsaka naloga pa traja eno sekundo dlje.

1
2
3
4
5
6
7
8
9
#!/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 task.sh $task_id
done
Izvorna datoteka: job-seq.sh

Posel damo v vrsto s sbatch in s sacct preverjamo stanje:

$ sbatch job-seq.sh
Submitted batch job 1193660
$ sacct -j 1193660 --format=jobid,jobname,partition,ncpus,start,end,state
       JobID    JobName  Partition      NCPUS               Start                 End      State 
------------ ---------- ---------- ---------- ------------------- ------------------- ---------- 
1193660       multitask   gridlong          5 2021-09-18T11:49:20 2021-09-18T11:49:36  COMPLETED 
1193660.bat+      batch                     5 2021-09-18T11:49:20 2021-09-18T11:49:36  COMPLETED 
1193660.ext+     extern                     5 2021-09-18T11:49:20 2021-09-18T11:49:36  COMPLETED 
1193660.0         sleep                     1 2021-09-18T11:49:20 2021-09-18T11:49:22  COMPLETED 
1193660.1         sleep                     1 2021-09-18T11:49:22 2021-09-18T11:49:24  COMPLETED 
1193660.2         sleep                     1 2021-09-18T11:49:24 2021-09-18T11:49:27  COMPLETED 
1193660.3         sleep                     1 2021-09-18T11:49:27 2021-09-18T11:49:31  COMPLETED 
1193660.4         sleep                     1 2021-09-18T11:49:31 2021-09-18T11:49:36  COMPLETED 
sbatch job-seq.sh
sacct -j 1193660 --format=jobid,jobname,partition,ncpus,start,end,state

Posamezna naloga v poslu ima identifikator $SLURM_JOB_ID.$SLURM_STEP_ID. Vidimo, da se je vsaka naloga res začela šele po tem, ko se je prejšnja končala. V nadaljevanju si ogledamo tri načine, kako poslati v obdelavo več nalog hkrati.

Z lupino

Za vzporedno izvajanje nalog v skriptah sbatch lahko uporabimo kar orodja, ki nam jih za ta namen ponuja lupina. Ukaz zaženemo v ozadju tako, da na konec dodamo &; z wait pa 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 v skripti 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 task.sh $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.

Skripto poženemo s sbatch in s sacct preverimo, da so naloge res bile zagnane hkrati.

$ sbatch job-for.sh
Submitted batch job 1193660
$ sacct -j 1193660 --format=jobid,jobname,partition,ncpus,start,end,state
       JobID    JobName  Partition      NCPUS               Start                 End      State
------------ ---------- ---------- ---------- ------------------- ------------------- ---------- 
1193660       multitask   gridlong          5 2021-09-20T15:09:12 2021-09-20T15:09:18  COMPLETED 
1193660.bat+      batch                     5 2021-09-20T15:09:12 2021-09-20T15:09:18  COMPLETED 
1193660.ext+     extern                     5 2021-09-20T15:09:12 2021-09-20T15:09:18  COMPLETED 
1193660.0       task.sh                     1 2021-09-20T15:09:13 2021-09-20T15:09:18  COMPLETED 
1193660.1       task.sh                     1 2021-09-20T15:09:13 2021-09-20T15:09:15  COMPLETED 
1193660.2       task.sh                     1 2021-09-20T15:09:13 2021-09-20T15:09:14  COMPLETED 
1193660.3       task.sh                     1 2021-09-20T15:09:13 2021-09-20T15:09:17  COMPLETED 
1193660.4       task.sh                     1 2021-09-20T15:09:13 2021-09-20T15:09:16  COMPLETED 
sbatch job-for.sh
sacct -j 1193660 --format=jobid,jobname,partition,ncpus,start,end,state

Z nizi poslov

Druga in pogosto enostavnejša možnost za vzporejanje so 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

./task.sh $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).

$ sbatch job-array.sh
Submitted batch job 1193724
$ sacct -j 1193724 --format=jobid,jobname,partition,ncpus,start,end,state 
       JobID    JobName  Partition      NCPUS               Start                 End      State 
------------ ---------- ---------- ---------- ------------------- ------------------- ---------- 
1193724_1     arraytask   gridlong          1 2021-09-18T19:25:43 2021-09-18T19:25:44  COMPLETED 
1193724_1.b+      batch                     1 2021-09-18T19:25:43 2021-09-18T19:25:44  COMPLETED 
1193724_1.e+     extern                     1 2021-09-18T19:25:43 2021-09-18T19:25:44  COMPLETED 
1193724_2     arraytask   gridlong          1 2021-09-18T19:25:43 2021-09-18T19:25:45  COMPLETED 
1193724_2.b+      batch                     1 2021-09-18T19:25:43 2021-09-18T19:25:45  COMPLETED 
1193724_2.e+     extern                     1 2021-09-18T19:25:43 2021-09-18T19:25:45  COMPLETED 
1193724_3     arraytask   gridlong          1 2021-09-18T19:25:43 2021-09-18T19:25:46  COMPLETED 
1193724_3.b+      batch                     1 2021-09-18T19:25:43 2021-09-18T19:25:46  COMPLETED 
1193724_3.e+     extern                     1 2021-09-18T19:25:43 2021-09-18T19:25:46  COMPLETED 
1193724_4     arraytask   gridlong          1 2021-09-18T19:25:43 2021-09-18T19:25:47  COMPLETED 
1193724_4.b+      batch                     1 2021-09-18T19:25:43 2021-09-18T19:25:47  COMPLETED 
1193724_4.e+     extern                     1 2021-09-18T19:25:43 2021-09-18T19:25:47  COMPLETED 
1193724_5     arraytask   gridlong          1 2021-09-18T19:25:43 2021-09-18T19:25:48  COMPLETED 
1193724_5.b+      batch                     1 2021-09-18T19:25:43 2021-09-18T19:25:48  COMPLETED 
1193724_5.e+     extern                     1 2021-09-18T19:25:43 2021-09-18T19:25:48  COMPLETED 
sbatch job-array.sh
sacct -j 1193724 --format=jobid,jobname,partition,ncpus,start,end,state 

Vidimo, da je Slurm tokrat namesto enega posla s petimi nalogami ustvaril pet neodvisnih poslov. Za vsakega sacct izpiše postavki batch in external.

Z GNU parallel

Za konec si oglejmo še program parallel, ki vzporeja naloge na podoben način kot nizi poslov, a ni odvisen od Slurma. Recimo, da želimo s programom gzip 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.

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.

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.