Regardez [ce tweet] de M. Robota (https://twitter.com/kaityo256/status/1049669976745861121) concernant MPI, et comment chaque processus fonctionne-t-il ensemble (principalement en ce qui concerne la compilation de la sortie standard)? J'étais curieux de connaître le contrôle du tampon et le point, alors j'ai fait une enquête approximative.
Cette fois, je n'ai pas écrit en détail sur l'environnement, alors assurez-vous de prendre le dos et de l'utiliser par vous-même sans le prendre. De plus, veuillez noter que les termes utilisés sont assez attachés au texte.
De plus, j'ai essayé l'environnement Linux et il existe trois types de MPI: Intel, SGI-MPT et OpenMPI. De plus, il n'y a aucune mention de Fortran. Pour C / C ++.
Tout d'abord, à propos de la "mise en mémoire tampon".
Lors de la sortie de données à partir de la sortie standard, cela se fait via stdout pour C et std :: cout pour C ++, mais même si vous appelez printf ou std :: ostream :: operator <<, la sortie sera immédiatement reflétée. Ce n'est pas toujours fait. En effet, l'appel des API OS (écriture et envoi) qui sont réellement en charge de la sortie en petits morceaux est généralement désavantageux en termes de performances, et dans la bibliothèque standard, il est stocké dans une mémoire tampon dans une certaine mesure et regroupé. C'est parce que j'essaye de tout cracher en même temps. Ce comportement est appelé ** mise en mémoire tampon **.
Il existe trois modes de mise en mémoire tampon:
Pour C stdio, le contrôle se fait avec setbuf / setvbuf. Le contrôle par défaut dépend du fichier / périphérique auquel la sortie standard est connectée, de la ligne tamponnée pour TTY / PTY, et entièrement tamponnée dans le cas contraire. L'opération de rinçage est effectuée avec la fonction fflush.
Pour iostream C ++, le contrôle est effectué en définissant l'indicateur std :: ios_base :: unitbuf sur std :: cout (std :: cout.setf ou unsetf). S'il est défini, il n'est pas mis en tampon, s'il n'est pas défini, il est entièrement mis en mémoire tampon et il n'y a pas de ligne en mémoire tampon. La valeur par défaut est pleine mémoire tampon. L'opération de vidage est effectuée avec le manipulateur d'E / S std :: flush ou std :: endl.
Comme vous pouvez le voir, les contrôles sont séparés pour C et C ++, mais dans le cas d'un code mixte C / C ++, c'est généralement un problème si l'ordre de sortie est perturbé, donc par défaut les deux sont synchronisés. Je vais. En d'autres termes, même s'il est entièrement mis en mémoire tampon du côté C ++, si le côté C ne l'est pas, il sera tiré du côté C. Cependant, ce comportement peut être remplacé par std :: ios_base :: sync_with_stdio (false).
En gros, ce que MPI est, c'est que si vous spécifiez les nœuds qui peuvent être mobilisés et le nombre de processus à exécuter, plusieurs nœuds peuvent démarrer le même programme (voire hétérogène) et effectuer des calculs coopératifs. C'est une bibliothèque et un groupe d'outils.
Par conséquent, il existe divers réseaux à haut débit tels qu'InfiniBand en tant que coopération entre les programmes démarrés, mais cette fois je pense au "flux de sortie standard". Par conséquent, il suffit de considérer les trois facteurs indiqués dans la figure suivante.
** Ceci est un terme arbitraire **,
Il sera classé comme. Dans la figure ci-dessus, l'avant et les autres sont dessinés comme s'ils s'exécutaient sur des nœuds différents, mais ils peuvent être identiques.
Intel MPI Le premier est Intel MPI. Cela ressemble à la figure suivante.
Les rôles et les réponses sont les suivants.
Après le démarrage, le gestionnaire établit une connexion TCP / IP vers l'avant, agrège la sortie acheminée par le worker et l'envoie vers l'avant.
SGI MPT Vient ensuite SGI MPT.
Les rôles et les réponses sont les suivants.
La division des rôles est similaire à l'Intel MPI mentionné précédemment, mais le démarrage du gestionnaire par l'avant nécessite un démon appelé arrayd (même s'il s'agit d'un nœud local) fourni avec le SGI MPT.
OpenMPI Enfin, OpenMPI.
Les rôles et les réponses sont les suivants.
La grande différence avec les deux MPI ci-dessus est que la communication entre les gestionnaires et les travailleurs devient PTY, comme c'est le cas avec la gestion des nœuds locaux.
Si vous effectuez une sortie avec un programme MPI, nous examinerons ce qui se passe lorsqu'il est finalement agrégé et sorti vers l'avant.
Comme organisé ci-dessus, trois types de programmes, front manager et worker, travaillent ensemble lors de l'exécution de MPI. Et ** la sortie des travailleurs est agrégée au début via le gestionnaire **. Par conséquent, il est également nécessaire d'organiser la mise en mémoire tampon pour chaque route. C'est,
Il y a trois endroits.
Intel MPI Dans le cas d'Intel MPI, la sortie de chaque worker est mélangée même au milieu de la ligne. Il semble donc que la mise en mémoire tampon est désactivée.
Cela est dû au fait que la bibliothèque MPI appelle en interne setbuf / setvbuf pendant MPI_Init pour mettre le worker dans un état sans tampon **. En d'autres termes, la partie worker-> manager est désactivée pour la mise en mémoire tampon, et la destination de sortie manager-> front, front-> final est renversée telle quelle sans aucun contrôle particulier, il semble donc que la mise en mémoire tampon est désactivée dans son ensemble.
Par conséquent, après MPI_Init, vous pouvez activer la mise en mémoire tampon en appelant setbuf / setvbuf et en reconfigurant la mise en mémoire tampon. De plus, il semble que l'indicateur de std :: cout ne soit pas modifié dans MPI_Init ou MPI: Init, donc s'il s'agit d'une application C ++ pure, la mise en mémoire tampon sera activée en désactivant la synchronisation C, C ++. ..
-ordered-output
au moment de mpiexec. Alors setbuf / setvbuf ne serait pas nécessaire. Cependant, il y avait une note dans le manuel pour se souvenir du dernier saut de ligne dans la sortie. Les erreurs standard semblent également être affectées. Ce qui suit est un extrait du chapitre 2.3.1 de Intel MPI 2019 Developer Reference.-ordered-output Use this option to avoid intermingling of data output from the MPI processes. This option affects both the standard output and the standard error streams. NOTE When using this option, end the last output line of each process with the end-of-line '\n' character. Otherwise the application may stop responding.
SGI MPT Dans le cas de SGI MPT, la sortie est organisée ligne par ligne, ce qui équivaut au comportement de mise en mémoire tampon de ligne.
Le mécanisme derrière cela est un peu compliqué.
En d'autres termes, il est tamponné par les efforts de la réception. À l'inverse, il se peut que vous ne souhaitiez pas que l'application MPI (worker) contrôle le tampon sans autorisation.
OpenMPI Comme SGI MPT, OpenMPI se comporte comme une ligne tamponnée.
Ce mécanisme est très simple, car la sortie entre le travailleur et le gestionnaire est PTY, et le contrôle stdout correspondant est mis en mémoire tampon de lignes par défaut. Autres Manager-> Front, Front-> Destination de sortie finale ne semble pas contrôler quelque chose de spécial. En d'autres termes, OpenMPI lui-même ne traite pas spécifiquement du contrôle de la mise en mémoire tampon, il est laissé à la bibliothèque standard.
Donc, j'ai vu la différence de contrôle dans chaque MPI.
Bien qu'il existe des différences dans chaque MPI, si vous voulez vous assurer que la mise en mémoire tampon est efficace, je pense qu'il est préférable d'appeler setbuf / setvbuf immédiatement après MPI_Init.
-ordered-output
, donc dans le cas des trois MPI discutés cette fois, il semble qu'il n'y ait pas besoin de toucher le côté programme. ..Ci-dessous, la source et le journal des opérations lors de la tentative de comportement avec Intel MPI sont répertoriés à titre de référence.
Journal des opérations
$ cat /etc/centos-release
CentOS Linux release 7.4.1708 (Core)
$ mpirun --version
Intel(R) MPI Library for Linux* OS, Version 2018 Update 1 Build 20171011 (id: 17941)
Copyright (C) 2003-2017, Intel Corporation. All rights reserved.
$ icpc --version
icpc (ICC) 18.0.1 20171018
Copyright (C) 1985-2017 Intel Corporation. All rights reserved.
$ mpiicpc -std=gnu++11 -o test test.cpp
$ mpirun -np 2 ./test
abababababababababbababababababababababa
abababababababababababababababababababab
a
bbabaababababababababababababababababab
a
bababbaababababbaababababababababababab
babaabababababababababababababababababab
$ mpirun -np 2 ./test --nosync
aaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbb
aaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbb
aaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbb
aaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbb
aaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbb
$ mpirun -np 2 ./test --setvbuf
aaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbb
aaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbb
aaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbb
aaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbb
aaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbb
$ mpirun -np 2 ./test --nosync --unitbuf
abababbaabababababababababababababababab
a
bababababababababababababababababababab
babababababababababababababababababababa
ababababbabababababababababababababababa
abababababababababababababababababababab
$ mpiicpc -std=gnu++11 -o test2 test2.cpp
$ mpirun -np 2 ./test2
abababababbaababababababbaababababababab
babaabababbaabababababababababababababab
babababababababababababababababababababa
ababababbaabababababbabaabababababababab
a
bababababababababababababababababababab
$ mpirun -np 2 ./test2 -f
aaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbb
$ mpirun -np 2 ./test2 -l
aaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbb
aaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbb
aaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbb
aaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbb
aaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbb
$
test.cpp
#include <mpi.h>
#include <iostream>
#include <thread>
#include <string>
#include <cstdio>
static char stdoutbuf[8192];
int main(int argc, char **argv) {
MPI::Init(argc,argv);
MPI::COMM_WORLD.Set_errhandler(MPI::ERRORS_THROW_EXCEPTIONS);
int rank = MPI::COMM_WORLD.Get_rank();
for ( int i=1; i<argc; i++ ) {
std::string opt(argv[i]);
if ( opt == "--nosync" ) {
// detach C++-iostream from C-stdio
std::ios_base::sync_with_stdio(false);
}
else if ( opt == "--setvbuf" ) {
// re-setvbuf for C-stdio
std::setvbuf(stdout,stdoutbuf,_IOFBF,sizeof(stdoutbuf));
}
else if ( opt == "--unitbuf" ) {
// disable buffering on C++-iostream
std::cout.setf(std::ios_base::unitbuf);
}
else if ( rank == 0 ) {
std::cerr << "invalid option: " << opt << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
char c='a'+rank;
for ( int i=0; i<5; i++ ) {
MPI::COMM_WORLD.Barrier();
for ( int j=0; j<20; j++ ) {
std::cout << c;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
std::cout << std::endl;
}
MPI::Finalize();
}
test2.cpp
#include <mpi.h>
#include <iostream>
#include <thread>
#include <string>
#include <cstdio>
static char stdoutbuf[8192];
int main(int argc, char **argv) {
MPI::Init(argc,argv);
MPI::COMM_WORLD.Set_errhandler(MPI::ERRORS_THROW_EXCEPTIONS);
int rank = MPI::COMM_WORLD.Get_rank();
if ( argc > 1 ) {
std::string opt(argv[1]);
if ( opt == "-f" ) {
// full buffered
std::setvbuf(stdout,stdoutbuf,_IOFBF,sizeof(stdoutbuf));
}
else if ( opt == "-l" ) {
// line buffered
std::setvbuf(stdout,stdoutbuf,_IOLBF,sizeof(stdoutbuf));
}
}
char c='a'+rank;
for ( int i=0; i<5; i++ ) {
MPI::COMM_WORLD.Barrier();
for ( int j=0; j<20; j++ ) {
std::cout << c;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
std::cout << '\n';
}
std::cout << std::flush;
MPI::Finalize();
}
Recommended Posts