Retour
Simulation
Novembre 2022

Mise en place d'un cluster RoCE

Pour réaliser une tâche de calcul de grande envergure, comme la simulation complète de la dynamique des fluides autour d'une voiture de course, un seul ordinateur peut être insuffisant. Pour pallier ce problème, il est possible de regrouper plusieurs serveurs et de former un cluster de calcul.

Dans ce contexte, un facteur clé de succès est de disposer d'un bon réseau d'interconnexion entre les différents serveurs (que nous appellerons les nœuds de calcul). Si ce n'est pas le cas, le temps gagné en puissance de calcul supplémentaire sera perdu en temps de communication accru.

Dans cet article, nous verrons comment mettre en place un cluster haute performance à partir de zéro, en utilisant une technologie d'interconnexion appelée RoCE. L'objectif est d'exécuter des applications utilisant MPI.

Configuration physique et prérequis

Matériel

L'utilisation de RoCE nécessite l'emploi d'une carte réseau (NIC) compatible. Dans notre cas, il s'agit de cartes Mellanox ConnectX-4. L'ensemble de la procédure décrite dans cet article devrait être similaire pour les autres cartes fabriquées par Mellanox. Les serveurs utilisés sont des serveurs OCP Leopard, équipés de 2 processeurs Intel Xeon. Dans cet exemple, nous en avons 12.

Le commutateur (switch) doit également être compatible RoCE. Dans notre cas, le matériel provient également de la sphère OCP et est un wedge 100 GbE. Son interface en ligne de commande est similaire à celle des commutateurs Arista. Tous les serveurs sont connectés à ce commutateur. Leurs adresses IP vont de 10.1.4.1 à 10.1.4.12.

Si vous n'avez pas encore acheté vos serveurs, voici quelques lignes directrices. Les tâches HPC comme la CFD sont souvent limitées par la mémoire (memory bound), ce qui signifie que les opérations les plus longues sont l'attente des données provenant de la RAM. Par conséquent, vous devriez privilégier un processeur avec un nombre élevé de canaux de mémoire et une fréquence de RAM élevée. Le nombre de cœurs n'est pas très important, car les canaux de mémoire sont souvent saturés avec 3 cœurs par canal.

Logiciel

Tous les serveurs fonctionnent sous Linux et ont Ubuntu 20.04 installé.

Comprendre la technologie

Pour comprendre l'objectif de RoCE et pourquoi il offre d'excellentes performances, nous devons nous plonger un peu dans les technologies en jeu.

RDMA

Lors d'une communication "normale", un message envoyé, qui occupe initialement un espace mémoire, est transféré à travers différentes couches qui vont chacune copier le message dans leur mémoire et le traiter avant de le transmettre à la couche suivante. Cette architecture en couches présente de nombreux avantages du point de vue de la programmation, mais elle implique beaucoup de copies et une forte utilisation des ressources. Même avec un débit très élevé, un réseau utilisant cette architecture standard aura une latence élevée et ne pourra pas atteindre de bonnes performances en clustering.

Pour contourner cela, il est nécessaire de mettre en œuvre une technique appelée RDMA (Remote Direct Memory Access). Le RDMA est une technique où les données sont transférées directement de la mémoire principale d'un serveur à celle d'un autre, sans aucune autre copie ni tampon intermédiaire. Cela réduit considérablement la latence et signifie que le noyau n'a pas à se soucier de copier les données.

Infiniband et RoCE

Pour réaliser cette technique, de nouvelles technologies et protocoles ont dû être développés. L'un d'eux est l'Infiniband, qui est à la fois une spécification physique de bus et de connecteurs, ainsi qu'une spécification de protocole. L'Infiniband est très efficace et très répandu dans le TOP500, mais il est également assez coûteux et complexe à mettre en place.

RoCE (RDMA over Converged Ethernet) est la tentative de tirer le meilleur des deux mondes : la performance d'Infiniband et la popularité d'Ethernet. Son fonctionnement consiste à encapsuler le protocole Infiniband à l'intérieur de paquets Ethernet (RoCE v1) ou UDP/IP (RoCE v2). C'est cette technologie que nous allons découvrir aujourd'hui.

Mise en place d'un cluster standard

Avant de voir comment créer un cluster RoCE, nous allons configurer un cluster fonctionnant sur des connexions TCP standard. Voici les différentes étapes.

Configurer SSH sans mot de passe

La première chose à faire est de configurer la connexion SSH sans mot de passe entre tous les nœuds. SSH est un protocole et un programme qui permet de se connecter en toute sécurité à un serveur distant et d'y exécuter des commandes. Il sera utilisé par le nœud hôte pour démarrer des processus sur tous les autres nœuds. Cependant, nous devons le faire fonctionner sans demander de mot de passe.

Pour permettre une connexion sans mot de passe, le serveur de réception doit avoir la clé publique de l'expéditeur (qui est généralement ~/.ssh/id_rsa.pub) dans son fichier authorized_keys (l'emplacement standard est ~/.ssh/authorized_keys).

Deux options sont fondamentalement disponibles. La première est de copier toutes les clés dans les différents fichiers authorized_keys. La seconde est de générer une nouvelle paire de clés qui sera utilisée par tous les serveurs (il suffit de copier les fichiers id_rsa et id_rsa.pub dans chaque dossier ~/.ssh/). Ainsi, l'authorized_key de tous les nœuds sera le même et ne contiendra que la clé publique unique.

Une fois cela fait, vous devriez pouvoir vous connecter aux autres serveurs via SSH, sans que le terminal ne vous demande de mot de passe.

Créer un répertoire partagé NFS

Cette étape est facultative, mais elle simplifie grandement la tâche de mise en place d'un cas d'utilisation MPI. Pour fonctionner, le cluster MPI a besoin que les données du fichier exécutable se trouvent sur chaque serveur et au même emplacement. Par conséquent, il est possible soit de copier ces fichiers sur chaque serveur, soit de créer un répertoire partagé. La première solution offre de meilleures performances, mais la seconde est plus facile à mettre en place. Consultez ce tutoriel pour configurer un répertoire NFS.

Installer et tester MPI

Il est maintenant temps de mettre tout en pratique. Tout d'abord, nous allons installer une implémentation de MPI. J'ai choisi OpenMPI, qui peut être installé avec apt-get install openmpi-bin.

Pour exécuter OpenMPI sur plusieurs nœuds, nous devons indiquer à l'hôte où trouver les autres nœuds. Cela se fait dans un fichier d'hôtes qui contient une ligne par serveur, avec son IP et le nombre de slots sur la machine. Le nombre de slots serait généralement le nombre de cœurs sur la machine, mais ici nous pouvons commencer avec un seul slot par nœud, afin d'essayer le clustering.

Voici un petit script pour générer le fichier d'hôtes pour un nombre donné de slots par nœud :

rm -f hostfile
for ((i=1; i <= $NB_NODES; i++))
do
    echo “10.1.4.$i slots=$NB_SLOT_PER_NODE” >> hostfile
done

Le résultat sera :

$ NB_NODES=12 NB_SLOT_PER_NODE=1 ./generate-hostfile.sh
$ cat hostfile
10.1.4.1 slots=1
10.1.4.2 slots=1
10.1.4.3 slots=1
10.1.4.4 slots=1
10.1.4.5 slots=1
10.1.4.6 slots=1
10.1.4.7 slots=1
10.1.4.8 slots=1
10.1.4.9 slots=1
10.1.4.10 slots=1
10.1.4.11 slots=1
10.1.4.12 slots=1

Maintenant, essayons mpirun :

mpirun --np 12 --hostfile /path/to/hostfile \
     --mca plm_rsh_agent "ssh -q -o StrictHostKeyChecking=no" \
     hostname

oici la signification de chaque argument :

Mon résultat ressemble à :

1c34da7f9a3a
1c34da7f9a42
1c34da7f9ac2
0c42a1198eaa
1c34da7f9bb2
1c34da5c5cc4
1c34da5c5cac
1c34da7f99e2
1c34da5c5e54
1c34da7f9bba
1c34da7f9bc2
1c34da7f99ea

À ce stade, vous pouvez essayer d'exécuter votre programme HPC préféré. Vous devriez obtenir des résultats plutôt médiocres lors de l'utilisation de plusieurs nœuds, car notre interconnexion est lente. Il est temps de mettre RoCE en action !

Ajout du support RoCE

Maintenant que notre cluster de base fonctionne, ajoutons le support RoCE.

Installation du pilote

La première chose à faire est d'installer le pilote Mellanox pour la carte d'interface réseau. Vous pouvez l'obtenir ici. Ensuite, vous devrez dézipper l'archive et exécuter mlnxofedinstall sur chaque nœud du cluster.

Le code exact et les options à envoyer à mlnxofedinstall peuvent différer selon la distribution et la version du pilote, mais voici ce que j'ai exécuté :

VERSION=MLNX_OFED-5.5-1.0.3.2
DISTRO=ubuntu20.04-x86_64
wget “https://content.mellanox.com/ofed/$VERSION/$VERSION-$DISTRO.tgz” &&     
    tar xvf “$VERSION-$DISTRO.tgz” &&                 
    “./$VERSION-$DISTRO/mlnxofedinstall” --add-kernel-support

Une fois cela fait, vous pouvez vérifier la version installée du pilote avec :

root@b8-ce-f6-fc-40-12:~$ ofed_info -s
MLNX_OFED_LINUX-5.6-2.0.9.0:

Nous devons maintenant activer deux modules de noyau nécessaires aux échanges RDMA et RoCE. Pour ce faire, exécutez : modprobe rdma_cm ib_umad.

Ensuite, vous pouvez vérifier que les périphériques RoCE sont reconnus avec ibv_devinfo. Voici ce que j'obtiens :

root@b8-ce-f6-fc-40-12:~$ ibv_devinfo
hca_id:    mlx5_0
    transport:            InfiniBand (0)
    fw_ver:                14.32.1010
    node_guid:            b8ce:f603:00fc:4012
    sys_image_guid:            b8ce:f603:00fc:4012
    vendor_id:            0x02c9
    vendor_part_id:            4117
    hw_ver:                0x0
    board_id:            MT_2470112034
    phys_port_cnt:            1
        port:    1
            state:            PORT_ACTIVE (4)
            max_mtu:        4096 (5)
            active_mtu:        1024 (3)
            sm_lid:            0
            port_lid:        0
            port_lmc:        0x00
            link_layer:        Ethernet
hca_id:    mlx5_1
    transport:            InfiniBand (0)
    fw_ver:                14.32.1010
    node_guid:            b8ce:f603:00fc:4013
    sys_image_guid:            b8ce:f603:00fc:4012
    vendor_id:            0x02c9
    vendor_part_id:            4117
    hw_ver:                0x0
    board_id:            MT_2470112034
    phys_port_cnt:            1
        port:    1
            state:            PORT_DOWN (1)
            max_mtu:        4096 (5)
            active_mtu:        1024 (3)
            sm_lid:            0
            port_lid:        0
            port_lmc:        0x00
            link_layer:        Ethernet

La sortie montre qu'un seul des ports est connecté : la carte réseau a deux ports physiques, le premier, mlx5_0, est PORT_ACTIVE, ce qui signifie qu'il est connecté, tandis que le second, mlx5_1, est marqué PORT_DOWN. La couche de transport est reconnue comme Infiniband, ce qui est le signe que RoCE est activé. En effet, rappelez-vous que RoCE est réalisé en encapsulant Infiniband sur UDP/IP.

Définissez la version du protocole RoCE sur v2 (-d est le périphérique, -p le port et -m la version) :

root@b8-ce-f6-fc-40-12:~$ cma_roce_mode -d mlx5_0 -p 1 -m 2
RoCE v2

Enfin, vous pouvez voir comment les périphériques sont associés aux interfaces Internet avec :

root@b8-ce-f6-fc-40-12:~$ ibdev2netdev
mlx5_0 port 1 ==> eth0 (Up)
mlx5_1 port 1 ==> ens1f1np1 (Down)

Tester la connexion RoCE

Pour tester la connexion, exécutez ib_send_lat --disable_pcie_relaxed -F sur un nœud (le serveur) et ib_send_lat --disable_pcie_relaxed -F <ip-of-the-server> sur un autre. Sur mon cluster, j'ai des latences d'environ 2 microsecondes. Si les vôtres sont beaucoup plus élevées, cela pourrait signifier que RoCE ne fonctionne pas correctement.

Si vous voulez vous assurer que RoCE est utilisé (ce qui est souvent utile), vous pouvez utiliser un sniffer pour suivre tous les messages envoyés et reçus par un nœud. Cependant, capturer le trafic RoCE n'est pas si facile, car il ne passe pas par le noyau. La procédure à suivre est la suivante :

docker run -it --net=host --privileged 
    -v /dev/infiniband:/dev/infiniband -v /tmp/traces:/tmp/traces 
    mellanox/tcpdump-rdma bash

Le protocole RRoCE signifie Routable RoCE, qui est un autre nom pour RoCE v2.

MPI avec RoCE

Maintenant que RoCE est supporté par les nœuds, il est temps de l'utiliser pour accélérer certaines applications. Pour ce faire, nous devrons dire à MPI d'utiliser RoCE. La bonne nouvelle est que le pilote de Mellanox est livré avec une version optimisée d'OpenMPI, appelée HPC-X, conçue pour utiliser au mieux les cartes réseau Mellanox.

Mellanox installe HPC-X dans un emplacement non standard, donc pour l'utiliser, nous devons taper :

$ export PATH=/usr/mpi/gcc/openmpi-4.1.2rc2/bin:$PATH
$ export LD_LIBRARY_PATH=/usr/mpi/gcc/openmpi-4.1.2rc2/lib:$LD_LIBRARY_PATH

mpirun devrait maintenant faire référence à HPC-X tel que fourni avec le pilote Mellanox :

$ which mpirun
/usr/mpi/gcc/openmpi-4.0.2rc3/bin/mpirun

Il ne nous reste plus qu'à ajouter un petit flag à la commande MPI (UCX est la bibliothèque qui prend en charge l'interconnexion RoCE dans OpenMPI) :

mpirun --np 12 --hostfile /path/to/hostfile 
    --mca plm_rsh_agent "ssh -q -o StrictHostKeyChecking=no" 
    -x UCX_NET_DEVICES=mlx5_0:1 
    hostname

À ce stade, rien d'extraordinaire ne devrait se produire, car hostname n'implique pas l'envoi et la réception de messages. Pour tester l'interconnexion, nous allons utiliser osu-micro-benchmark. Essayez maintenant :

$ export OSU="/usr/mpi/gcc/openmpi-4.1.2rc2/tests/osu-micro-benchmarks-5.6.2”  
$ mpirun --np 12 --hostfile /path/to/hostfile 
      --mca plm_rsh_agent "ssh -q -o StrictHostKeyChecking=no" 
      -x UCX_NET_DEVICES=mlx5_0:1 
      $OSU/osu_alltoall

Vous pouvez utiliser la méthode précédente pour vous assurer que les paquets RoCE sont envoyés. Vous pouvez également essayer de voir la différence avec et sans RoCE. Cependant, le cluster n'est pas vraiment prêt, et le résultat pourrait être médiocre pour le moment.

C'est parce que le réseau a un comportement avec perte (lossy behavior) pour l'instant. Cela signifie que certains paquets peuvent être perdus en transit, et bien que cela puisse être acceptable dans certains cas, ce n'est pas du tout le cas dans un environnement HPC. Par conséquent, nous devons le configurer un peu plus.

Configurer le réseau pour un comportement sans perte

L'élément clé pour obtenir un comportement sans perte est d'activer un mécanisme appelé PFC (Priority Flow Control). L'idée de ce protocole est de différencier les niveaux de priorité et d'adapter le comportement en fonction de la priorité. L'option qui nous intéresse est de rendre cette priorité en mode sans perte (lossless). Ce qui se passe, c'est que lorsqu'un périphérique (le commutateur ou un nœud) est submergé, il demande au réseau de s'arrêter avant que son tampon ne soit entièrement plein et que certains paquets ne soient perdus.

Une solution possible serait d'activer ce mode pour chaque priorité, mais ce serait un peu sale. Ici, nous allons faire les choses correctement et activer le mode sans perte pour une seule priorité et indiquer à MPI de l'utiliser. Il y a deux étapes pour obtenir un comportement sans perte :

Ces deux étapes doivent être effectuées sur le commutateur et sur les serveurs. Dans cet exemple, nous utiliserons la priorité 3.

Configuration du commutateur pour le mode sans perte

La syntaxe pour configurer le commutateur peut être très différente d'un modèle à l'autre. Dans mon cas, le commutateur wedge a une syntaxe similaire aux commutateurs Arista.

Une fois connecté au commutateur, regardez d'abord l'état des interfaces pour connaître la plage à configurer :

wedge&gt;show interfaces status
Port       Name   Status        Vlan     Duplex Speed   Type         Flags Encapsulation
Et1/1             connected     trunk    full   25G    100GBASE-CR4
Et1/2             connected     trunk    full   25G    100GBASE-CR4
Et1/3             connected     trunk    full   25G    100GBASE-CR4
Et1/4             connected     trunk    full   25G    100GBASE-CR4
Et2/1             connected     trunk    full   25G    100GBASE-CR4
Et2/2             connected     trunk    full   25G    100GBASE-CR4
Et2/3             connected     trunk    full   25G    100GBASE-CR4
Et2/4             connected     trunk    full   25G    100GBASE-CR4
Et3/1             connected     trunk    full   25G    100GBASE-CR4 
Et3/2             connected     trunk    full   25G    100GBASE-CR4 
Et3/3             connected     trunk    full   25G    100GBASE-CR4 
Et3/4             connected     trunk    full   25G    100GBASE-CR4 
Et4/1             notconnect   trunk    full   25G    Not Present 
Et4/2             notconnect   trunk    full   25G    Not Present 
Et4/3             notconnect   trunk    full   25G    Not Present 
Et4/4             notconnect   trunk    full   25G    Not Present 
[...]

Ici, ma plage sera 1/1-3/4.

Maintenant, entrez en mode de configuration :

wedge>enable
wedge#configure
wedge(config)#interface ethernet 1/1-3/4
wedge(config-if-Et1/1-3/4)#

Nous allons d'abord définir le mécanisme de marquage de priorité sur L3, ce qui signifie que le champ DSCP dans l'en-tête IP sera utilisé. Ce mode est légèrement plus facile à configurer que le mécanisme L2, qui nécessite l'utilisation d'un VLAN.

wedge(config-if-Et1/1-3/4)#qos trust dscp

Nous activons maintenant le contrôle de flux prioritaire et définissons la priorité 3 en mode sans perte :

wedge(config-if-Et1/1-3/4)#priority-flow-control on
wedge(config-if-Et1/1-3/4)#priority-flow-control priority 3 no-drop

L'état du PFC peut être affiché pour s'assurer que tout est correctement configuré :

wedge(config-if-Et1/1-3/4)#show priority-flow-control
The hardware supports PFC on priorities 0 1 2 3 4 5 6 7
PFC receive processing is enabled on priorities 0 1 2 3 4 5 6 7
The PFC watchdog timeout is 0.0 second(s) (default)
The PFC watchdog recovery-time is 0.0 second(s) (auto) (default)
The PFC watchdog polling-interval is 0.0 second(s) (default)
The PFC watchdog action is errdisable
The PFC watchdog override action drop is false
The PFC watchdog non-disruptive priorities are not configured
The PFC watchdog non-disruptive action is not configured
The PFC watchdog port non-disruptive-only is false
The PFC watchdog hardware monitored priorities are not configured
Global PFC : Enabled

E: PFC Enabled, D: PFC Disabled, A: PFC Active, W: PFC Watchdog Enabled
Port  Status Priorities Action      Timeout  Recovery        Polling         Note
                                            Interval/Mode   Config/Oper
---------------------------------------------------------------------------------------
Et1/1      E A W      3          -           -        - / -           - / -           DCBX disabled 
Et1/2      E A W      3          -           -        - / -           - / -           DCBX disabled
Et1/3      E A W      3          -           -        - / -           - / -           DCBX disabled
Et1/4      E A W      3          -           -        - / -           - / -           DCBX disabled
Et2/1      E A W      3          -           -        - / -           - / -           DCBX disabled
Et2/2      E A W      3          -           -        - / -           - / -           DCBX disabled
Et2/3      E A W      3          -           -        - / -           - / -           DCBX disabled
Et2/4      E A W      3          -           -        - / -           - / -           DCBX disabled
Et3/1      E A W      3          -           -        - / -           - / -           DCBX disabled
Et3/2      E A W      3          -           -        - / -           - / -           DCBX disabled
Et3/3      E A W      3          -           -        - / -           - / -           DCBX disabled
Et3/4      E A W      3          -           -        - / -           - / -           DCBX disabled

Configurer le serveur pour le mode sans perte

Tout d'abord, nous configurons la carte réseau en définissant le mode de confiance (trust mode) (c'est-à-dire le mécanisme de marquage de priorité utilisé) sur L3 et en activant le mode sans chute de paquets sur la priorité 3 avec :

$ sudo mlnx_qos -i eth0 --trust=dscp --pfc 0,0,0,1,0,0,0,0

Les différentes options ont l'explication suivante :

Il ne nous reste plus qu'à dire à MPI d'utiliser la priorité 3. Pour ce faire, nous définissons le flag : UCX_IB_SL (pour Infiniband Service Level) sur 3 et le flag UCX_IB_TRAFFIC_CLASS sur 124. Ce deuxième flag sera utilisé pour remplir le champ DSCP. Par défaut, la priorité 3 va de 30 à 39, nous ciblons donc ici 31. Comme le champ DSCP est rempli avec UCX_IB_TRAFFIC_CLASS/4, nous le définissons sur 124 (car 124/4=31).

Voici un exemple pour le benchmark OSU :

$ mpirun --np 12 --hostfile /path/to/hostfile \
        --mca plm_rsh_agent "ssh -q -o StrictHostKeyChecking=no" \
        -x UCX_NET_DEVICES=mlx5_0:1 \
        -x UCX_IB_SL=3 \
        -x UCX_IB_TRAFFIC_CLASS=124 \
        $OSU/osu_alltoall

Conclusion

Pour conclure, examinons une comparaison des 3 configurations de cluster que nous avons démontrées dans cet article : cluster TCP, RoCE avec perte et RoCE sans perte. Ci-dessous se trouvent les sorties du benchmark osu_alltoall. Chaque fois, 4 nœuds utilisant chacun 28 cœurs ont été utilisés.

Quelques points sont notables dans cette comparaison :

Retour

Nos articles