Začínáme s OpenMPI (nejen) na Fedoře Silverblue

V dnešním článku bych rád rychle prolétl openMPI – knihovnu nejen pro jazyk C, která umožňuje psát multivláknové aplikace. A pro ty, kteří studují FIT VUT, budete ji potřebovat i na projekty, třeba do PRL ;).

Úvod

OpenMPI je jedna z populárních implementací něčeho, čemu se říká MPI – message passing interface. Velmi jednoduše řečeno, MPI je jeden ze způsobů, jak vytvořit paralelní program. Další možnosti jsou třeba OpenMP (na konci skutečně chybí „i“). Na rozdíl od OpenMPI (tady to „i“ zase je) má OpenMP jinou filozofii – umožní nám paralelizovat určité části kódu, hlavně rozsáhlejší for cykly, pomocí direktivy #pragma. Ale zpět k OpenMPI. Samotné jméno už asi hodně napoví; procesy spolu budou komunikovat pomocí zpráv.

Instalace

Jste-li na Silverblue, vytvořte si nový toolbox a vlezte do něj (jméno si zvolte jakékoli, PRL je pouze název předmětu, do kterého jsem OpenMPI potřeboval):

toolbox create PRL
toolbox enter PRL

A pak si stačí nainstalovat openmpi a g++, kterým se – alespoň za mě, v roce 2020/2021 – kompiloval projekt:

sudo dnf install -y openmpi openmpi-devel g++

Následně musíme do PATH přidat cestu „/usr/lib64/openmpi/bin“ (případně jinou, podle toho, kam se vám openMPI nainstaluje). Pro lidi z FIT: merlin toto nemá a proto na něm musíte spuštět openmpi příkazy s flagem „–prefix /usr/local/share/OpenMPI“.

export PATH=$PATH:/usr/lib64/openmpi/bin

A jak se s tím pracuje?

Jednoduše! Nebudu tady do detailu vysvětlovat, jak s OpenMPI pracovat, takže sem rovnou hodím dokumentovaný OpenMPI Hello world, ze kterého to pochopíte:

/* Header file for OpenMPI*/
#include <mpi.h> 
#include <stdio.h>

using namespace std;

/* Function for first processor */ 
void master(int num_procs)
{
    bool can_start = true;
    MPI_Bcast(&can_start, 1, MPI_C_BOOL, 0, MPI_COMM_WORLD);
    MPI_Status stat;
    /* Just a place to put a value into */
    int buffer = 0;
    /* TODO */ 
    for(int i = 1; i < num_procs; i++)
    {   
        MPI_Recv(&buffer,1,MPI_INT,i, MPI_ANY_TAG, MPI_COMM_WORLD, &stat);
        printf("Processor number %d sent a message\n", buffer);
    }   
}

/* Function for regular processor */
void standard(int my_id)
{
    bool can_start;
    MPI_Bcast(&can_start, 1, MPI_C_BOOL, 0, MPI_COMM_WORLD);
    if(can_start)
    {   
        MPI_Send(&my_id, 1, MPI_INT, 0, 0, MPI_COMM_WORLD);
    }   
}

int main(int argc, char** argv) 
{
    int num_procs;
    int my_id;

    /* Initializes MPI. It just needs to be there :) */
    MPI_Init(&argc, &argv);
    
    /* Get the number of processors */
    MPI_Comm_size(MPI_COMM_WORLD, &num_procs);

    /* Get the rank of me AKA my id*/
    MPI_Comm_rank(MPI_COMM_WORLD, &my_id);
    
    if(my_id == 0)  
    {   
        /* Very often, we need some special 
           job for the first processor */
        master(num_procs);
    }   
    else
    {   
        standard(my_id);
    }   
    
    
   /* Last MPI function to be called in the program. 
    * Just before return is a great place to live for it. */
    MPI_Finalize();
    return 0;
}


Překlad a spuštění

Na Fedoře musíme nejprve načíst modul s OpenMPI:

module load mpi/openmpi-x86_64

A potom už jen ve složce, kam jsme si stáhli hello world shora, spustíme příkaz níže, čímž kód přeložíme:

mpic++  -o hello-ompi hello-ompi.cpp -std=c++0x

Pro spuštění kódu provedeme tento příkaz:

mpirun -np X hello-ompi

Na merlinovi přitom nesmíme zapomenout prefix. v případě spuštění je za flagem -np velké X – místo něj dejte počet procesorů ( = procesů), kolik chcete vytvořit. A pokud budete chtít vytvořit více procesů, než máte HW vláken, přidejte ještě před flag -np flag –oversubscribe.

A kdyby náhodou spuštění nefungovalo, vyzkoušejte místo hello-ompi zapsat název binárky jako ./hello-ompi, občas to pomůže.

Krátký cheatsheet pro komunikaci

V této části dám základní OpenMPI funkce a jak s nimi pracovat. Ani zdaleka se nejedná o všechny a z větší části to jsou hlavně funkce, se kterými jsem sám pracoval v projektu.

int MPI_Send(const void *buff, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm);

MPI_Send() je funkce, která odesílá zprávu jinému procesu. I když se může zdát, že má hromadu parametr, je to docela jednoduché:

  • buff je buffer s daty. V případě, kdy budeme chtít poslat třeba int, uložený v proměnné x, do parametru napíšeme &x.
  • count určuje, kolikrát se zpráva pošle. V 99% případů to bude 1.
  • datatype určuje, co za typ dat posíláme. Nejedná se přitom o datatypy C++, ale o speciální MPI datatypy. Jejich seznam se dá dohledat na internetu. Příkladem je třeba MPI_INT.
  • dest určuje číslo (rank) procesu, pro který je zpráva určená.
  • tag je volitelná značka zprávy – dá se použít například na určení, do které fronty daná hodnota patří atp. většinou si je budete definovat sami, třeba takto: #define MPI_TAG_QUEUE_1 1. Název je samozřejmě jakýkoli, ale je dobré mít přehled o tom, co to vlastně je.
  • comm potom určuje komunikátor – jakýsi balík procesů. Já nikdy nepoužil nic jiného než MPI_COMM_WORLD – hlavní komunikátor.
int MPI_Recv(void *buff, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Status *status);

MPI_Recv() slouží pro příjem zpráv. Logika je podobná jako u MPI_Send(), takže tu okomentuju jen rozdíly:

  • buff je tentokrát buffer, kam se bude hodnota ukládat.
  • source je číslo procesu, od kterého očekáváte data.
  • tag může tentokrát nabývat i hodnotu MPI_ANY_TAG, který říká, že chceme přijímat všechny zprávy od daného procesu, ale nezajímá nás TAG. Použití je například v situaci, kdy jeden proces posílá jinému procesu data do dvou front. Příjemce potom nemusí laborovat s tím, jestli mumá přijít zpráva s daty pro první nebo druhou frontu, ale prostě přijme zprávu a pak se už lokálně podívá, který tag přijatá zpráva skutečně obsahuje (přes políčko ve struktuře status)
  • status je speciální datová struktura, která vám dává informace o obdržené zprávě. Já se přiznám, že jsem ji v životě nepoužil jinak než pro zjišťování tagu. Je to asi chyba a mělo by se přes ní po každém přijetí kontrolovat, zda data dorazila v pořádku.

Kromě standardních MPI_Recv() a MPI_Send() existují i syncronní verze (Ssend a Srecv) i neblokující asyncchronní verze (Isend, Irecv). Standardní Send a Recv je přitom asynchronní, ale blokující – dokud není zpráva přijatá/odeslaná, nejde se dál.

MPI_Bcast(void *buff, int count, MPI_Datatype datatype, int root, MPI_Comm comm);

Bcast je společně s Reduce funkcí pro tzv. kolektivní komunikaci. To znamená, že spolu „mluví“ všechny procesy. Bcast se hodí třeba v situaci, kdy chceme něco sdělit všem procesorům (velikost vstupu…).

Reduce je samostatná speciální funkce – nějakou danou operací redukuje celou kolekci (třeba pole) do jedné proměnné. Redukce MAX nad polem [1,-5,3,12,-30] tedy vrátí 12. Na začátku má přitom každý procesor právě jednu hodnotu z kolekce (potebovali bychom tedy 5 procesů pro předchozí příklad). Důležité je také vědět, že redukci a broadcast musí provést všechny procesory v komunikátoru – ve všech procesorech musí dojít k zavolání funkce MPI_Bcast nebo MPI_Reduce.

A co se týče parametrů:

  • buff, count, datatype a comm známe. Jen je rozdíl v tom, že v případě master procesoru (rank/id = root) je buff odesílající (data se vždy berou z master procesoru) a pro všechny ostatní procesory je buff přijímací buffer.
  • root je jednoduše procesor, ze kterého se vezmou data. Často to bude procesor číslo 0.
int MPI_Reduce(const void *sendbuff, void *recvbuff, int count, MPI_Datatype datatype, MPI_Op op, int root, MPI_Comm comm);  
  • datatype, count a comm mají stejný význam jako všude jinde.
  • root je číslo procesoru, který po skončení redukce obdrží výslednou hodnotu (dotane ji jen a pouze on!).
  • sendbuff – vstupní buffer (odtud se berou jednotlivá data)
  • recvbuff – výstupní buffer (zde bude mít procesor root výsledek).
  • op je asi nejdůležitější parametr a říká nám, jakou operací se budou prvky redukovat. Operace to musí být binární a asociativní, takže třeba sčítání, násobení nebo maximum.

Perla na závěr: vlastní redukce

Občas můžete dojít do situace, kdy vám vestavěné redukční operace nestačí – třeba chcete minimum z kladných čísel v poli (předpokládáme, že v posloupnosti je vžy aspoň jedno kladné číslo). Není problém, vytvoříme si vlastní redukční funkci:

void positive_min(int *invect, int *inoutvect, int *len, MPI_Datatype *type)
{
    for(int i = 0; i < *len; i++)
    {
        int iv = invect[i];
        int iov = inoutvect[i];
        /* If both numbers are > 0, we want min*/
        if(iv > 0 && iov > 0)
           inoutvect[i] = min(iov, iv);
        else if(iv > 0) /* if only iv > 0, we want it in iov*/
           inoutvect[i] = iv;
        /* else both numbers are negative and we will handle it in the next iteration */  
    }
    return;
}

Za zmínku stojí pár věcí:

  • invect je vektor/pole vstupních hodnot
  • inoutvect je vektor/pole vstupních hodnot, kam se rovnou ukládá výsledek
  • Signatura funkce bude až na jméno a datový typ vstupních a výstupních vektorů pořád stejná.

Následně ještě musíme MPI říct, že tahle funkce bude naší novou redukční funkcí:

MPI_Op MPI_POSMIN;
MPI_Op_create((MPI_User_function*) positive_min, 0, &MPI_POSMIN); 

Dohromady z toho může vyjít třeba takovýhle kód. Je to přitom skutečně jen ukázka, tak její kvalitu berte s rezervou.


#include <mpi.h>
using namespace std;
void positive_min(int *invect, int *inoutvect, int *len, MPI_Datatype *type)
{
    for(int i = 0; i < *len; i++)
    {
        int iv = invect[i];
        int iov = inoutvect[i];
        /* If both numbers are > 0, we want min*/
        if(iv > 0 && iov > 0)
           inoutvect[i] = min(iov, iv);
        else if(iv > 0) /* if only iv > 0, we want it in iov*/
           inoutvect[i] = iv;
        /* else both numbers are negative and we will handle it in the next iteration */  
    }
    return;
}
    
int main(int argc, char *argv[])
{
int num_procs;
int rank;

MPI_Init(&argc,&argv);
MPI_Comm_size(MPI_COMM_WORLD, &num_procs);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);

MPI_Op MPI_POSMIN;
MPI_Op_create((MPI_User_function*) positive_min, 0, &MPI_POSMIN); 

/* This just makes the numbers in the processors "pseudorandom" */
int num = rank%2 ? rank*num_procs%17  : -rank*num_procs%17 ;
num = num==0 ? num_procs : num;

int posmin;
printf("I have rank %d and have num %d\n", rank, num);

MPI_Reduce(&num, &posmin,1, MPI_INT, MPI_POSMIN, 0, MPI_COMM_WORLD);

if(rank==0) /* master has the result*/
    printf("PosMin is: %d\n",posmin );

MPI_Finalize();
return 0;

}//main

Závěr

No a to je extrémně rychlý úvod do OpenMPI, primárně zaměřený na studenty, kteří budou muset projít kurzem PRL, snad ale pomůže i někomu jinému. Jako vždy, máte-li dotazy, klidně pište do komentářů, i když dost možná nebudu stran OpenMPI umět odpovědět – viděl jsem to letos poprvé a dost možná naposled :D.

Leave a Reply

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *