Skoči na vsebino

Računajmo na več vozliščih

Na prvih računalniških gručah so imela vozlišča eno ali največ nekaj procesorskih jeder, za hitrejše računanje mnogih problemov je bilo potrebno usklajeno delovanje več vozlišč. Vozlišča si ne delijo skupnega pomnilniškega prostora, vsako vozlišče vidi samo svoj del porazdeljenega pomnilnika. Računanje na več vozliščih poteka tako, da se na vsakem vozlišču zažene vsaj en proces, ki sodeluje v izvajanju posla. Procesi si preko omrežja izmenjujejo sporočila s potrebnimi podatki. Da so stroški komunikacije čim manjši, so računska vozlišča med seboj povezana z nizkolatenčnimi širokopasovnimi omrežji, priljubljeno je omrežje Infiniband. Na superračunalniških sistemih se za tovrstno izvajanje poslov najpogosteje uporablja standard MPI (angl. Message Passing Interface). Standard MPI vključuje množico funkcij za porazdeljeno računanje: podpira komunikacijo med dvema procesoma, izmenjevanje sporočil med skupinami procesov, skupinsko računanje in še marsikaj.

Obstaja več knjižnic, ki podpirajo standard, zelo priljubljena je knjižnica OpenMPI, saj podpira najnovejšo komunikacijsko opremo in protokole, poleg tega pa se lepo povezuje z razvrščevalnikom poslov Slurm. Knjižnice običajno podpirajo programska jezika C/C++ in Fortran, za večino programskih jezikov pa so pripravljene ovojnice (angl. wrapper). V nadaljevanju bomo uporabili paket mpi4py z ovojnnico za programski jezik python.

Porazdeljeno računanje Mandelbrotove množica z MPI

Programu za izris Mandelbrotove množice dodajmo še podporo za porazdeljeno računanje po standardu MPI.

Koda programa mandelbrot-mpi.py

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
#!/usr/bin/env python3


####################################################################################
#                                                                                  #
#  MPI implementation of the Mandelbrot set                                 #
#                                                                                  #
####################################################################################


# import required libraries
import time, os
import argparse, configparser
import numpy as np
from PIL import Image
from numba import jit, prange
from mpi4py import MPI

# mandelbrot_colors computes pixel colors in the image
#   real_min, real_max, imag_min, imag_max: bounds of the complex plane
#   iters_max: the maximum number of iterations
#   image: a reference to a memory location of the image
@jit(nopython = True, parallel = False, cache = True)
def mandelbrot_colors(real_min, real_max, imag_min, imag_max, iters_max, image):

    # image size in pixels
    width = image.shape[1]
    height = image.shape[0]

    # pixel size in complex plane
    real_step = (real_max - real_min) / width
    imag_step = (imag_max - imag_min) / height

    # check convergence of each pixel in the image
    for y in prange(0, height):
        for x in prange(0, width):              

            # a point in a complex plane corrsponding to the pixel (x, y)
            real = real_min + real_step * x
            imag = imag_min + imag_step * y
            c = complex(real, imag)

            # check for convergence
            z = complex(0, 0)
            iters = 0
            while abs(z) <= 2 and iters < iters_max:
                z = z*z + c
                iters += 1

            # color pixel in HSV scheme
            image[y, x] = (iters % 256, 255, 255 * (iters < iters_max))

# end mandelbrot_colors


# mandelbrot_mpi creates an image array of the Mandelbrot set
#   real_min, real_max, imag_min, imag_max: bounds of the complex plane
#   iters_max: the maximum number of iterations
#   image: a reference to the image
def mandelbrot_mpi(real_min, real_max, imag_min, imag_max, iters_max, width, height):

    # MPI: init
    mpi_procs = MPI.COMM_WORLD.Get_size() 
    my_id = MPI.COMM_WORLD.Get_rank()
    my_name = MPI.Get_processor_name()

    # print MPI info
    print('Process', my_id, 'of', mpi_procs, 'runs on node', my_name)

    # split work
    # area
    my_imag_min = imag_min + (imag_max - imag_min) * my_id / mpi_procs
    my_imag_max = imag_min + (imag_max - imag_min) * (my_id + 1) / mpi_procs
    # image
    my_height = height * (my_id + 1) // mpi_procs - height * my_id // mpi_procs
    my_image = np.zeros((my_height, width, 3), dtype = np.uint8)

    # process a part of the image
    mandelbrot_colors(real_min, real_max, my_imag_min, my_imag_max, iters_max, my_image)

    # gather image arrays
    image = None
    images_gathered = MPI.COMM_WORLD.gather(my_image, root = 0)
    if my_id == 0:
        image = np.zeros((height, width, 3), dtype = np.uint8)
        for p in range(mpi_procs):
            p_low = height * p // mpi_procs
            p_high = height * (p + 1) // mpi_procs
            image[p_low: p_high] = images_gathered[p]

    # return image array
    return image

# end mandelbrot_mpi


# main routine
def main():

    # parse arguments
    ap = argparse.ArgumentParser()
    ap.add_argument('--config', type = str, default = '', help = 'config file')
    args = vars(ap.parse_args())
    config_file = args['config']

    # parse config file
    config = configparser.ConfigParser()
    if os.path.isfile(config_file):
        config.read(config_file)
    real_min = config.getfloat('AREA', 'real_min', fallback = -2.5)
    real_max = config.getfloat('AREA', 'real_max', fallback = +1.5)
    imag_min = config.getfloat('AREA', 'imag_min', fallback = -1.125)
    imag_max = config.getfloat('AREA', 'imag_max', fallback = +1.125)
    iters_max = config.getint('ITERATIONS', 'max', fallback = 256)
    width = config.getint('IMAGE', 'width', fallback = 3840)
    height = config.getint('IMAGE', 'height', fallback = 2160)
    name = config.get('IMAGE', 'name', fallback = 'mandelbrot.jpg')

    # main processing
    t = time.time()
    image = mandelbrot_mpi(real_min, real_max, imag_min, imag_max, iters_max, width, height)
    t = time.time() - t

    if MPI.COMM_WORLD.Get_rank() == 0:
        # save image
        Image.fromarray(image, mode='HSV').convert('RGB').save(name)    

        # printout
        print('MPI: size:', (width, height), 'iterations:', iters_max, 'time:', round(t, 3), "s")    

# end main


# invoke the main routine, when this is the main script
if __name__ == "__main__":
   main()
Izvorna datoteka: mandelbrot-mpi.py

V osnovnem programu mandelbrot-seq.py smo tokrat spremenili funkcijo mandelbrot (zdaj mandelbrot_mpi). V funkciji mandelbrot_mpi najprej razdelimo delo med procese, tako da vsak proces poračuna svoj del slike. Na koncu osnovni proces (rang 0) dele slik s funkcijo gather združi v eno. V funkciji main smo dopisali, da samo osnovni proces shrani sliko in izpiše podatke o poteku računanja.

Vsebnik s podporo za OpenMPI

Podobno, kot pri grafičnih procesnih enotah, mora ogrodje Apptainer tudi pri delu s knjižnicami MPI poskrbeti, da vsebniku dovoli dostop do potrebne strojne opreme. Pri izbiranju knjižnic moramo paziti, da je knjižnica v vsebniku združljiva s knjižnico na gostitelju.

Na superračunalniški gruči Arnes je nameščena knjižnica OpenMPI verzija 4.1.1, zato isto verzijo knjižnice namestimo tudi v vsebnik. Knjižnico OpenMPI najdemo med namestitvenimi paketi, vendar tam ni prevedena z ustreznimi stikali in na gruči deluje bolj za silo. Za pravilno namestitev moramo knjižnico prenesti v vsebnik in jo v njem prevesti. Za nameščanje sledimo postopku, pri čemer vrstico s klicem programa ./configure dopolnimo s stikali --with-slurm in --with-pmix. S prvim stikalom zahtevamo podporo za Slurm. Z drugim stikalom zahtevamo, da MPI za komunikacijo z ostalimi sistemi in strojno opremo uporablja programsko plast PMIx (angl. Process Management Interface eXascale).

Poleg jezika python, namestitvenega paketa pip in ostalih standardnih paketov, namestimo še paket mpi4py z ovojnico za MPI.

Poskrbimo, da v vsebnik prenesemo program mandelbrot-mpi.py in konfiguracijsko datoteko default.conf.

Vsebnik mb-mpi.def

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
Bootstrap: localimage
From: ../04-python-scilib/python-scilib.sif

%files
    ./prg/mandelbrot-mpi.py /exec/
    ./prg/default.conf /exec/

%environment
    # general
    export LC_ALL=C
    export PATH=/exec:${PATH}

    # UCX
    export LD_LIBRARY_PATH=/opt/ucx-1.10.0/lib:${LD_LIBRARY_PATH}
    export PATH=/opt/ucx-1.10.0/bin:${PATH}

    # OpenMPI
    export LD_LIBRARY_PATH=/opt/openmpi/lib:${LD_LIBRARY_PATH}
    export PATH=/opt/openmpi/bin:${PATH}
    export OMPI_DIR=/opt/openmpi/build

%post
    export LC_ALL=C
    export DEBIAN_FRONTEND=noninteractive

    # install tools
    apt update -y
    apt install -y wget
    apt install -y openssh-client
    apt install -y libpmix-dev
    apt install -y libnuma-dev
    apt install -y rdma-core    

    # download and build UCX
    cd /opt
    wget https://github.com/openucx/ucx/releases/download/v1.10.0/ucx-1.10.0.tar.gz
    tar xvf ucx-1.10.0.tar.gz
    cd ucx-1.10.0
    ./configure --prefix=/opt/ucx-1.10.0 --enable-mt
    make -j 10
    make install

    # download and build OpenMPI
    export MPI_VER_MAIN=4.1
    export MPI_VER_SUB=1
    export MPI_VER=${MPI_VER_MAIN}.${MPI_VER_SUB}
    cd /opt
    wget https://download.open-mpi.org/release/open-mpi/v${MPI_VER_MAIN}/openmpi-${MPI_VER}.tar.gz
    tar xvzf openmpi-${MPI_VER}.tar.gz
    rm -rf /opt/openmpi-${MPI_VER}.tar.gz
    mv openmpi-${MPI_VER} openmpi
    cd openmpi
    ./configure --prefix=/opt/openmpi --with-slurm --with-pmix --with-ucx=/opt/ucx-1.10.0
    make all install
    cd ..
    export LD_LIBRARY_PATH=/opt/openmpi/lib:${LD_LIBRARY_PATH}
    export PATH=/opt/openmpi/bin:${PATH}

    # install mpi4py
    pip3 install mpi4py

    # set file permissions
    chmod a+rx /exec/*.py
    chmod a+r /exec/*.conf

%runscript
    echo "Usage: "
    echo "  singularity exec mb-mpi.sif mandelbrot-mpi.py"
    echo "  singularity exec mb-mpi.sif mandelbrot-mpi.py --config <config file>"
    echo
    echo "  <config file> example:"
    echo "--------------------------"
    cat /exec/default.conf
    echo "--------------------------"

%test
    echo "Mandelbrot set python scripts and config file:" 
    ls -lr /exec/*

%labels
    Author      uros (dot) lotric (at) fri (dot) uni (dash) lj (dot) si
                barbara (dot) krasovec (at) ijs (dot) si
    Container   Mandelbrot set with MPI
    Version     1.1
    Description Workshop advanced supercomputing (Superračunalištvo bolj zares)

%help
    For details run:
        ./mpi-mb.sif
Izvorna datoteka: mb-mpi.def

Tako zgrajen vsebnik lahko zaženemo na dva načina:

  • Če želimo program izvajati na enem samem vozlišču, lahko v vsebniku zaženemo razvrščevalnik mpirun, ki mu s stikalom --n povemo, koliko procesov naj zažene. Privzeto se bo vsak proces izvajal na svojem procesorskem jedru.

    Na računalniku, kjer smo vsebnik zgradili, zaženemo štiri procese:

    $ apptainer exec mb-mpi.sif mpirun --n 4 mandelbrot-mpi.py
    Process 3 of 4 runs on node hpc-login1.arnes.si
    Process 0 of 4 runs on node hpc-login1.arnes.si
    Process 1 of 4 runs on node hpc-login1.arnes.si
    Process 2 of 4 runs on node hpc-login1.arnes.si
    MPI: size: (3840, 2160) iterations: 256 time: 3.394 s
    
    apptainer exec mb-mpi.sif mpirun --n 4 mandelbrot-mpi.py
    

    Na računskem vozlišču gre podobno, vendar moramo ustrezno nastaviti razvrščevalnik Slurm. V razvrščevalniku Slurm za eno nalogo rezerviramo štiri procesorska jedra, nato pa v vsebniku zaženemo razvrščevalnik mpirun, ki poskrbi za ostalo. Razvrščevalniku s stikalom --n določimo, koliko procesov naj zažene, pri tem pazimo, da navedemo enako številko kot pri stikalu --cpus-per-task. Razvrščevalnik mpirun bo privzeto zagnal toliko procesov, kot je navedeno s stikalom --ntasks. Da sprostimo to omejitev in izkoristimo vsa rezervirana procesorska jedra, dodamo stikalo --oversubscribe.

    $ srun --ntasks=1 --cpus-per-task=4 apptainer exec mb-mpi.sif \
      mpirun --n 4 --oversubscribe mandelbrot-mpi.py
    Process 3 of 4 runs on node wn120.arnes.si
    Process 2 of 4 runs on node wn120.arnes.si
    Process 1 of 4 runs on node wn120.arnes.si
    Process 0 of 4 runs on node wn120.arnes.si
    MPI: size: (3840, 2160) iterations: 256 time: 4.294 s
    
    srun --ntasks=1 --cpus-per-task=4 apptainer exec mb-mpi.sif mpirun --n 4 --oversubscribe  mandelbrot-mpi.py
    
  • Pri zaganjanju na več vozliščih, najprej zaženemo mpirun, ta pa potem zažene vsebnik. Pripravimo skripto bash

    ===

    Skripta mandelbrot-mpi.sh

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    #!/bin/bash
    
    #SBATCH --job-name=mandelbrot-mpi
    #SBATCH --ntasks=4
    #SBATCH --nodes=2
    #SBATCH --output=mandelbrot-mpi.log
    #SBATCH --reservation=fri
    
    module load OpenMPI/4.1.1-GCC-10.3.0
    
    mpirun --n $SLURM_NTASKS apptainer exec mb-mpi.sif mandelbrot-mpi.py
    
    Izvorna datoteka: mandelbrot-mpi.sh

    V skripti zahtevamo, da se štiri naloge izvedejo na dveh vozliščih. Med izvajanjem skripta najprej na računskih vozliščih namesti modul OpenMPI. Paziti moramo, da sta verziji knjižnic v modulu in vsebniku enaki. Za mpi, ki uporablja isto različino knjižnic MPI. Nato zažene še ukaz mpirun, ki na dodeljenih procesorskih jedrih preko programa apptainer zažene vsebnik mb-mpi.sif in v njem program mandelbrot-mpi.py.

    Skripto zaženemo, počakamo, da se posel konča, in pogledamo izpis v datoteki mandelbrot-mpi.log.

    $ sbatch mandelbrot-mpi.sh       
    Submitted batch job 1198176
    $ cat mandelbrot-mpi.log
    Process 1 of 4 runs on node wn113.arnes.si
    Process 3 of 4 runs on node wn120.arnes.si
    Process 2 of 4 runs on node wn120.arnes.si
    Process 0 of 4 runs on node wn113.arnes.si
    MPI: size: (3840, 2160) iterations: 256 time: 3.668 s
    
    sbatch mandelbrot-mpi.sh
    squeue --me
    cat mandelbrot-mpi.log
    

Preveri svoje znanje

Vaja

Za pripravo recepta in gradnjo vsebnika sledimo navodilom: Vaja 07.