Skoči na vsebino

Vsebniki za grafične procesne enote

Računanje na GPE

Grafična procesna enota (angl. Graphics Processing Unit, GPU) ali krajše GPE je poseben procesor, v osnovi namenjen izrisovanju slike na zaslon. Grafične procesne enote so zato optimizirane za hitro računanje podatkovno paralelnih poslov, na primer za obdelavo slik ali video posnetkov. Čeprav niso primerne za reševanje splošnih problemov, se izkaže, da znajo hitro izvesti mnoge operacije, uporabne tudi v drugih domenah. Pri globokem učenju programski paketi, na primer PyTorch in Tensorflow, izkoriščajo grafične procesne enote za hitro računanje operacij nad matrikami in vektorji. Podporo za grafične procesne enote najdemo tudi v orodjih za molekulsko dinamiko, na primer GROMACS in v orodjih za simulacijo tekočinskega toka, na primer RapidCFD.

Mandelbrotova množica na GPE

Programu za izris Mandelbrotove množice dodajmo podporo za računanje na grafičnih procesnih enotah. Izračun konvergence izvajamo za vsako točke v kompleksnem prostoru, neodvisno od ostalih točk. Gre torej za izjemno podatkovno paralelen problem, ki se nam ga splača izvajati na grafični procesni enoti.

Tudi za računanje na grafičnih procesnih enotah uporabimo knjižnico numba, le da moramo tokrat zaradi drugačnega programskega modela precej spremeniti funkciji mandelbrot in mandelbrot_color, ki smo jih zato tudi preimenovali.

Celoten program za izdelavo slike Mandelbrotove množice na grafični procesni enoti:

Koda programa mandelbrot-gpu.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
#!/usr/bin/env python3


####################################################################################
#                                                                                  #
#  GPU 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 cuda


# mandelbrot_color_kernel computes pixel colors in the image in GPU
# the describes the work which is performed by each GPU thread
#   real_min, real_max, imag_min, imag_max: bounds of the complex plane
#   iters_max: the maximum number of iterations
#   image_device: a reference to a memory location of the image in GPU
@cuda.jit
def mandelbrot_color_kernel(real_min, real_max, imag_min, imag_max, iters_max, image_device):

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

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

    # work alloted to a thread
    (x_start, y_start) = cuda.grid(2)
    x_grid = cuda.gridDim.x * cuda.blockDim.x
    y_grid = cuda.gridDim.y * cuda.blockDim.y

    # check convergence of each pixel in the image
    for y in range(x_start, height, x_grid):
        for x in range(y_start, width, y_grid):

            # 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_device[y, x] = (iters % 256, 255, 255 * (iters < iters_max))

# end mandelbrot_color_kernel


# mandelbrot_gpu creates an image of the Mandelbrot set
#   real_min, real_max, imag_min, imag_max: bounds of the complex plane
#   iters_max: the maximum number of iterations
#   width, height: size of the final image
def mandelbrot_gpu(real_min, real_max, imag_min, imag_max, iters_max, width, height):

    # allocate image array
    image = np.zeros((height, width, 3), dtype = np.uint8)

    # copy image array to a GPU
    image_device = cuda.to_device(image)

    # invoke the computation on a GPU
    block_size = (16, 16)
    grid_size = ((width - 1) // block_size[0] + 1, (height - 1) // block_size[1] + 1)
    mandelbrot_color_kernel[grid_size, block_size](real_min, real_max, imag_min, imag_max, iters_max, image_device)

    # copy imag array from GPU
    image = image_device.copy_to_host()

    # return image¸array
    return image

# end mandelbrot_gpu


# 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_gpu(real_min, real_max, imag_min, imag_max, iters_max, width, height)
    t = time.time() - t

    # save image
    Image.fromarray(image, mode='HSV').convert('RGB').save(name)    

    # printout
    print('GPU: 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-gpu.py

Če malo poenostavimo, funkcija mandelbrot_gpu (vrstice 62 do 85) na grafični procesni enoti zažene toliko niti, kot je točk na sliki, nato pa vsaka nit v funkciji mandelbrot_color_kernel (vrstice 19 do 59), ki teče na grafični procesni enoti, preveri konvergenco in določi barvo slikovne točke.

Obvestilo

Mnogo več o programiranju grafičnih procesnih enot izveste na delavnici Programiranje grafičnih procesnih enot.

Vsebnik s podporo za GPE

Delo s posebno strojno opremo, kot so grafične procesne enote, moramo ob zagonu vsebnika posebej zahtevati. Poleg tega moramo poskrbeti, da so na gostitelju nameščeni ustrezni gonilniki. Ogrodje apptainer podpira grafične procesne enote Nvidia (CUDA) in AMD (ROCm). Pri klicu vsebnika dodamo stikalo --nv za grafične procesne enote Nvidia in --rocm za grafične procesne enote AMD. Zdaj ogrodje Apptainer na gostitelju poišče ustrezne gonilnike in jih poveže z vsebnikom.

Stikalo --nv:

  • Lahko uporabljamo z ukazi apptainer shell, apptainer run in apptainer exec.
  • Zagotovi, da so zapisi /dev/nvidia* o grafičnih procesnih enotah na gostitelju vidni tudi v vsebniku.
  • Poišče in poveže osnovne knjižnice CUDA z gostitelja v vsebnik. Tako so knjižnice na voljo v vsebniku in ustrezajo gonilnikom grafičnih procesnih enot na gostitelju.
  • Nastavi spremenljivko okolja LD_LIBRARY_PATH v vsebniku, da programi v vsebniku kličejo knjižnice na gostitelju.

Za uporabo grafičnih procesnih enot Nvidia moramo v vsebnik namestiti knjižnice CUDA. To lahko namestimo sami, tako kot smo delali do sedaj. Veliko lažje pa je, če naše aplikacije dodamo v vsebnik, ki ima knjižnice že nameščene. Na repozitoriju dockerhub Nvidia objavlja raznorazne vsebnike. Najdemo jih na povezavi https://hub.docker.com/r/nvidia/cuda. Izberemo vsebnik 12.0.0-base-ubuntu20.04, ki vključuje le najosnovnejše knjižnice.

Do sedaj smo knjižnice za python nameščali s programom pip3 (angl. Package Installer for Python3). Zelo priljubljeno pa je nameščanje programskih paketov z ogrodjem conda, saj nam poleg namestitve omogoča tudi nastavljanje okolij (spremenljivk okolja). Ker gre za obsežno ogrodje, se mu v vsebniku raje izognemo. Uporabimo raje manjši različico Miniconda; za namestitev sledimo dokumentaciji.

Namestimo še pakete numpy, pillow, numba in za računanje na grafičnih procesnih enotah še ustrezen paket cudatoolkit.

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

Vsebnik mb-gpu.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
Bootstrap: docker
From: nvidia/cuda:12.0.0-base-ubuntu20.04

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

%environment
    export LC_ALL=C    
    export PATH=/opt/conda/bin:${PATH}
    export PATH=/exec:${PATH}

%post
    export LC_ALL=C    
    export DEBIAN_FRONTEND=noninteractive

    apt update -y 
    apt install -y wget
    apt install -y sudo

    # install Miniconda
    wget https://repo.anaconda.com/miniconda/Miniconda3-4.6.14-Linux-x86_64.sh -O Miniconda.sh
    /bin/bash Miniconda.sh -b -p /opt/conda
    rm Miniconda.sh
    export PATH=/opt/conda/bin:${PATH}

    conda install -y numpy 
    conda install -y Pillow 
    conda install -y numba 
    conda install -y cudatoolkit

    chmod a+rx /exec/*.py
    chmod a+r /exec/*.conf
    export PATH=/exec:${PATH}

%runscript
    echo "Usage: "
    echo "  apptainer exec mb-gpu.sif mandelbrot-gpu.py"
    echo "  apptainer exec mb-gpu.sif mandelbrot-gpu.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
    Container   Mandelbrot set on GPU
    Version     1.0
    Description Workshop advanced supercomputing (Superračunalištvo bolj zares)

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

Zgrajen vsebnik zaženemo z ukazom

$ srun --gpus=1 --partition=gpu apptainer exec --nv mb-gpu.sif mandelbrot-gpu.py
srun: job 48543178 queued and waiting for resources
srun: job 48543178 has been allocated resources
INFO:    Setting 'NVIDIA_VISIBLE_DEVICES=all' to emulate legacy GPU binding.
INFO:    Setting --writable-tmpfs (required by nvidia-container-cli)
GPU: size: (3840, 2160) iterations: 256 time: 1.662 s
srun --gpus=1 --partition=gpu apptainer exec --nv mb-gpu.sif mandelbrot-gpu.py

S stikalom --gpus=1 smo od razvrščevalnika Slurma zahtevali, da nam dodeli en procesor in eno grafično procesno enoto. S stikalom --partition=gpu smo zahtevali, da se naš posel izvaja na particiji gpu. Na koncu pa smo s stikalom --nv zahtevali, da ogrodje Apptainer poišče gonilnike na gostitelju in jih poveže z vsebnikom.

Preveri svoje znanje

Vaja

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