# Anticipation des communications réseau grâce à la connaissance du futur dans le parallélisme à tâche

Tanguy Chatelain

<figure><img src="image_1.png" /></figure>

## To cite this version:

Tanguy Chatelain. Anticipation des communications réseau grâce à la connaissance du futur dans le parallélisme à tâche. Informatique [cs]. 2025. hal-05281620

HAL Id: hal-05281620

https://inria.hal.science/hal-05281620v1

Submitted on 24 Sep 2025

HAL is a multi-disciplinary open access
archive for the deposit and dissemination of sci-
entific research documents, whether they are pub-
lished or not. The documents may come from
teaching and research institutions in France or
abroad, or from public or private research centers.

L'archive ouverte pluridisciplinaire HAL, est
destinée au dépôt et à la diffusion de documents
scientifiques de niveau recherche, publiés ou non,
émanant des établissements d'enseignement et de
recherche français ou étrangers, des laboratoires
publics ou privés.

<figure><img src="image_2.png" /></figure>

Distributed under a Creative Commons Attribution 4.0 International License
# Anticipation des communications réseau grâce à la connaissance du futur dans le parallélisme à tâche

## Stage de fin d'études

Auteur :

Tanguy Chatelain

Encadrants :

Alexandre Denis, Samuel Thibault, Philippe Swartvagher

BORDEAUX
INP

Enseirb-Matmeca

<figure><img src="image_3.png" /></figure>
Table des matières

<table><tr><td>1 Introduction</td><td>3</td></tr><tr><td>1.1 Contexte du stage</td><td>3</td></tr><tr><td>1.2 Objectif</td><td>3</td></tr><tr><td>2 Interactions entre runtime à tâches et communications réseau</td><td>4</td></tr><tr><td>2.1 Support d'exécution à tâches</td><td>4</td></tr><tr><td>2.2 Communications réseau</td><td>4</td></tr><tr><td>2.3 Interactions entre ordonnancement et communications réseau</td><td>4</td></tr><tr><td>3 Allocation mémoire des buffers de réception</td><td>6</td></tr><tr><td>4 Enregistrement mémoire</td><td>8</td></tr><tr><td>4.1 L'enregistrement mémoire sur les réseaux InfiniBand</td><td>8</td></tr><tr><td>4.2 Cache d'enregistrement</td><td>9</td></tr><tr><td>4.3 Coût d'un enregistrement mémoire</td><td>10</td></tr><tr><td>4.4 Enregistrements mémoire en parallèle</td><td>11</td></tr><tr><td>4.5 Enregistrement mémoire avec StarPU et NewMadeleine</td><td>12</td></tr><tr><td>5 Évolution des performances</td><td>13</td></tr><tr><td>6 Conclusion</td><td>15</td></tr></table>
# Résumé

Les architectures actuelles des calculateurs haute performance présentent des nœuds multicœurs ou GPU reliés par un réseau haute performance. Ce type d'architecture pousse à l'utilisation de modèles de programmation parallèle multi-thread et distribués. On retrouve ainsi des supports d'exécution comme StarPU qui utilise un parallélisme à base de tâches. Dans sa version distribuée, StarPU s'appuie sur une bibliothèque MPI, ou bien sur NewMadeleine, et exécute les communications inter-nœuds de manière implicite en les inférant depuis le graphe de tâches. Ainsi, pour chaque tâche postée par l'application, StarPU vérifie si les données nécessitées par la tâche requièrent une communication réseau. Lorsqu'une communication est nécessaire, StarPU prépare immédiatement la requête en allouant un buffer de réception. Cependant, cette allocation mémoire immédiate entraîne un surcoût mémoire et limite donc la soumission de tâches à StarPU. Notre premier objectif est donc de déplacer cette allocation au plus tard sans ralentir les communications.

Par ailleurs, sur certains de ces réseaux haute-performance, une étape préalable à l'échange de données est l'enregistrement mémoire qui permet à la carte réseau d'accéder directement aux adresses mémoire physiques enregistrées sans passer par l'OS. Or cette étape allonge la durée des communications car elle est réalisée juste avant l'envoi ou réception de données. Par conséquent, une pratique est de réaliser un cache d'enregistrement appelé *rcache*. L'idée du *rcache* est de réaliser un seul enregistrement mémoire pour plusieurs communications utilisant un même buffer. Cependant, certaines applications utilisent des buffers de communications systématiquement différents et n'en bénéficient donc pas. Ainsi, notre objectif est de réaliser l'enregistrement mémoire en avance, ce qui est possible car cette étape n'utilise que les adresses mémoire et pas leur contenu. Elle peut donc être réalisée avant que les contenus des buffers d'envoi ne soient prêts ou alors pendant qu'on utilise ailleurs un buffer qui servira plus tard à la réception.

Comme l'allocation mémoire et l'enregistrement mémoire sont deux étapes intrinsèquement liées, nous pouvons y répondre simultanément en utilisant la connaissance du futur fournie par le graphe de tâche maintenu par StarPU. En effet, il est possible d'être prévenu lorsque la dernière tâche de calcul utilisant un buffer d'envoi commence. De cette manière, nous pouvons réaliser l'enregistrement mémoire et l'allocation du buffer de réception pendant l'exécution de cette tâche, et éviter la latence induite par ces étapes. En les retirant du chemin critique de l'exécution des communications nous avons pu réduire la durée des communications réseau, ce qui a conduit à une augmentation des performances de 15% sur l'application potrf. Aussi, nous avons grandement diminué l'utilisation mémoire de ces applications, permettant ainsi un meilleur passage à l'échelle de ces applications.
# 1 Introduction

## 1.1 Contexte du stage

Mon stage de fin d'études s'est déroulé au centre Inria de l'Université de Bordeaux, durant 6 mois. L'Institut national de recherche en sciences et technologies du numérique (Inria), fondé en 1967; est un centre de recherche français dont les travaux sont focalisés sur les sciences mathématiques et numériques. Un vaste panel de thématiques sont donc abordées, dont par exemple l'intelligence artificielle, le calcul haute performance, ou les systèmes embarqués. L'institut a pour mission le développement de la recherche et de la valorisation en sciences et techniques de l'information et de la communication. Ce centre est financé par des subventions nationales et des projets européens ou nationaux, mais également par des partenariats industriels ou consortiums.

Le sujet de mon stage reliant à la fois l'ordonnancement de tâches et les communications réseau, j'ai pu travailler avec 2 équipes de l'Inria : *Storm* et *TADaaM*. L'équipe *TADaaM* a pour thématique les interactions entrées/sorties bas niveau entre les applications et le système dans le cadre du calcul haute performance, tandis que l'équipe *Storm* a pour objectif d'améliorer l'accès à la puissance de calcul disponible dans une architecture. En collaboration avec ces équipes, j'ai pu travailler sur les interactions des bibliothèques de communications réseaus avec les systèmes d'exécution à base de tâches. Enfin, mes travaux ont été encadrés par Alexandre Denis, mon maître de stage, ainsi que par Samuel Thibault et Philippe Swartvagher, qui m'auront orienté dans mes travaux.

## 1.2 Objectif

Avec l'émergence du numérique, de nombreuses disciplines scientifiques ont commencé l'utilisation de simulations numériques et d'applications de calculs afin d'améliorer leur développement. On y retrouve de nombreux domaines comme l'astrophysique, la chimie quantique, la géophysique ou bien l'intelligence artificielle. Cependant, avec le développement de jeux de données de plus en plus grands et la complexité grandissante des problèmes, ces applications et simulations demandent toujours plus de ressources. Ainsi, on retrouve aujourd'hui des machines de calculs répondant à ces besoins, avec des architectures complexes, notamment avec un très grand nombre de cœurs, inter-connectées par un réseau haute-performance permettant des débits de données de plusieurs dizaines de gigaoctets par secondes. Ainsi, savoir comment utiliser efficacement ces ressources est devenue une discipline elle-même, appelée *High Performance Computing* (HPC).

Pour utiliser efficacement une machine de calcul, il est nécessaire d'utiliser chaque cœur en permanence. Pour cela, il est d'usage de paralléliser les applications en les découplant en morceaux pouvant s'exécuter en même temps. De plus, certaines applications utilisent des supports d'exécution à tâches permettant de répartir équitablement le travail entre les cœurs. Aussi, certaines applications utilisent plusieurs machines. Dès lors, ces machines communiquent via un réseau haute-performance, et il est préférable de profiter d'une couche d'abstraction via une bibliothèque de communications réseau pour les communications.

Cependant, lorsqu'une application utilise un support d'exécution et une bibliothèque de communications réseau en même temps, les communications peuvent alors déséquilibrer l'ordonnancement à cause de la bande passante limitée du réseau. Ainsi, les équipes *Storm* et *TADaaM* travaillent sur l'interaction entre le support d'exécution à tâches, StarPU, et la bibliothèque de communications réseau, NewMadeleine, qui y sont développés, afin d'influer les décisions prises au sein de ces deux bibliothèques pour en obtenir de meilleures performances.

L'objectif de mon stage a donc été d'améliorer les interactions entre StarPU et NewMadeleine. Pour cela, nous verrons tout d'abord comment une bibliothèque de communication réseau et un support d'exécution peuvent interagir, puis nous étudierons comment cette interaction peut permettre à StarPU une meilleure gestion de la mémoire attribuée au buffers de réception. Ensuite, nous verrons que cette interaction peut également être utilisée pour réaliser des étapes préliminaires à la communication en avance sur certains réseaux. Enfin, nous observerons les différences de performances apportées par ces travaux.
# 2 Interactions entre runtime à tâches et communications réseau

## 2.1 Support d'exécution à tâches

Lors de la conception d'une application haute performance, l'un des objectifs recherché est l'utilisation optimisée des ressources à disposition dans les calculateurs. Ces ressources sont notamment les nombreux cœurs de la machine. Pour alimenter en permanence chaque cœur en calcul à effectuer, une pratique est de découper une application en tâche. Par exemple, en algèbre linéaire, le code est découpé en tâches dont la durée est de l'ordre de la millisecorde. Aussi, certaines tâches auront besoin du résultat d'autres tâches pour s'exécuter, ce qui implique l'existence de dépendances entre les tâches. Ainsi, une application peut être découpée pour être représentée sous la forme d'un ensemble de tâches avec des dépendances entre elles, ce qui, in fine, représente un graphe orienté acyclique. Il convient d'utiliser un support d'exécution à tâches, qui, à l'aide du graphe de tâche, choisit le moment et le cœur où une tâche s'exécutera, de manière à maximiser l'utilisation des cœurs disponibles.

De plus, les machines récentes présentent des topologies de plus en plus complexes, avec un nombre de cœurs croissant, la présence additionnelle de GPU(s), ou bien un partitionnement de la mémoire particulier (système NUMA). Dans le but d'implémenter un outil d'ordonnancement performant pour ces architectures complexes, l'équipe *Storm* de l'Inria de Bordeaux a développé le support d'exécution StarPU [2]. Il permet la création du graphe de tâches pendant l'exécution, et une exécution optimisée pour les architectures hétérogènes complexes. De plus, StarPU permet une cohérence des données en attachant à chaque tâche les données qui seront utilisées par celle-ci. Cette gestion des données lui permet donc de piloter et de prédire les mouvements mémoires. Pour ces raisons, nous utiliserons StarPU dans nos travaux.

## 2.2 Communications réseau

Lancer une application sur une seule machine présente des limites. La première limite est la mémoire disponible de la machine. Les machines de calculs actuelles possèdent quelques centaines de gigaoctets de mémoire vive, ce qui est une barrière pour les applications nécessitant d'avantage de mémoire. La seconde limite est la puissance de calcul, qui est nécessaire pour exécuter une application plus rapidement.

Afin de répondre à ces deux limites, il est possible d'exécuter une application sur plusieurs machines. On bénéficie alors de la mémoire et de la puissance de calcul de ces machines en même temps. Les machines sont alors inter-connectées par un réseau haute-performance, qui permet le transit des données de l'application d'une machine à l'autre.

Cependant, cet ajout soulève de nouvelles difficultés. Par exemple, le réseau a un débit limité qui est bien inférieur au bus mémoire d'une machine. Ainsi, chaque communication a un coût, ce qui implique d'en réaliser aussi peu que possible. Aussi, il existe différents types de réseaux haute-performances, qui diffèrent tant dans leur débit, latence ou dans les étapes à effectuer dans l'envoi des données. Effectivement, certains réseaux nécessitent une étape préalable à l'envoi des données. Sur InfiniBand, on retrouve par exemple une étape nommée *enregistrement mémoire*, dont l'exécution est préalable à chaque envoi ou réception sur le réseau.

Pour pouvoir répondre aux difficultés d'usage d'un réseau, les applications distribuées utilisent une bibliothèque de communications réseau, afin de ne pas se soucier des spécificités du réseau. L'équipe *TADaaM* de l'Inria a implémenté sa propre bibliothèque de communications réseau, NewMadeleine [3], qui est utilisée pour la recherche dans ce domaine. NewMadeleine permet une bonne scalabilité en nombre de requêtes [5]. De plus, bien que la bibliothèque implémente le standard MPI, elle inclut également une interface native, qui permet une gestion plus fine des requêtes réseau. Nous choisisons donc NewMadeleine comme bibliothèque de communications réseau pour nos travaux.

## 2.3 Interactions entre ordonnancement et communications réseau

Les bibliothèques de communications réseau et les supports d'exécution sont souvent deux outils à part, qui ne communiquent pas entre eux. Cependant, les communications ont souvent un impact sur l'ordonnancement des tâches, car des tâches peuvent dépendre des résultats de tâches s'exécutant sur une autre machine. Dès lors, la
communication réseau du résultat va retarder l'exécution de la tâche sur la machine locale.

<figure><img src="image_4.png" /><figcaption>FIGURE 1 – Graphe de tâches nécessitant une communication réseau</figcaption></figure>

Afin de pouvoir prendre en compte les communications réseaux, StarPU dans sa version distribuée [1] les infère depuis le graphe de tâche de manière implicite. De cette manière, si une tâche devant s'exécuter sur une machine ne possède pas les données nécessaires, alors une communication réseau sera implicitement générée afin de récupérer les données nécessaires à la tâche. Par exemple, en Figure 1, la dépendance de la tâche C vers D nécessite implicitement une communication réseau du processus 1 vers le processus 0.

Plus concrètement, pour chaque communication nécessaire, StarPU allouera le buffer de réception dès la soumission de la tâche au runtime, et enverra les données dès que celles-ci seront écrites dans le buffer d'envoi. Allouer le buffer de réception aussi tôt a pour avantage de ne pas ajouter de latence à la communication, mais a pour désavantage un surcoût mémoire conséquent, car on alloue un buffer par communication à effectuer dans le futur. Par conséquent, le passage à l'échelle des applications distribuées se retrouve limité.

Ainsi, pour effectuer ces communications, StarPU peut se baser soit sur une bibliothèque MPI, soit sur New-Madeleine et son interface native [4] [7]. En effet, pouvoir utiliser directement l'interface native de NewMadeleine présente l'avantage de pouvoir utiliser des fonctionnalités non disponibles dans l'interface MPI, comme par exemple l'ajout de priorité dans les communications. Par ailleurs, comme NewMadeleine et StarPU sont deux bibliothèques développées à proximité (Inria de Bordeaux), il est plus simple d'y implémenter des modifications permettant une meilleure interaction entre elles.

**Problématique :**

Enfin, la problématique de ce stage fut d'abord de trouver **comment réaliser l'allocation des buffers de réception le plus tard possible sans ralentir les communications** à cause de buffers non alloués à temps. Ensuite, comme nous souhaitons allouer les buffers de réception le plus tard possible, nous pouvons également réserver un peu de temps pour réaliser par la même occasion l'étape d'enregistrement mémoire nécessitée par certains réseaux haute-performance. Le second objectif de ce stage fut donc de chercher comment utiliser la connaissance du futur de StarPU afin de **réaliser l'enregistrement mémoire des buffers d'envoi et de réception en avance sans perturber ou ralentir les communications**.
## 3 Allocation mémoire des buffers de réception

<figure><img src="image_5.png" /><figcaption>FIGURE 2 – Allocation mémoire dès le début de l'application</figcaption></figure>

Comme expliqué en section 2.3, les communications réseau à réaliser sont directement inférées via le graphe de tâche par StarPU. Ainsi, dans l'ancienne implémentation de StarPU, dès qu'une tâche nécessitant une communication réseau était soumise au graphe de tâche, l'allocation du buffer de réception était réalisée au même moment. Par conséquent, comme les applications soumettent généralement toutes les tâches au runtime dès le début de leur exécution, tous les buffers de réception étaient alloués au début de l'application, comme montré en Figure 2. Ainsi, à partir d'un certain nombre de communications requises, l'application était tuée par un *Out Of Memory Kill* car les buffers de réception occupaient toute la mémoire disponible.

Pour éviter d'allouer tous les buffers de réception en même temps, et dès le début de l'application, notre objectif est de réaliser l'allocation juste avant que ceux-ci soient requis. Cependant, ceci n'est pas trivial, car seul l'émetteur contrôle la communication, comme c'est lui qui décide de quand débute la communication, c'est à dire lorsque les données à envoyer sont disponibles.

Ainsi, nous avons ajouté l'envoi d'une notification par l'émetteur vers le récepteur de la communication. Le but de cette notification est d'avertir le récepteur que la communication réseau va bientôt être demandée, c'est à dire postée chez l'émetteur.

Or, comment savoir que la communication réseau va bientôt être postée côté émetteur ? Pour cela, nous avons utilisé StarPU, qui possède la connaissance du futur grâce au graphe de tâche. En effet StarPU est capable de
<figure><img src="image_6.png" /><figcaption>FIGURE 3 – Allocation mémoire en avance grâce à une notification en avance de l'émetteur</figcaption></figure>

savoir en avance quand les données vont être envoyées. Comme le montre la figure 3, lorsque la dernière tâche de calcul produisant les données à envoyer démarre, nous pouvons notifier le destinataire de la communication via un petit message que la communication réseau va bientôt être soumise. De cette manière, le destinataire a le temps de réaliser l'allocation du buffer de réception avant que l'émetteur ne soumette la requête d'envoi. Par conséquent, nous avons éliminé le surcoût mémoire tout en laissant du temps au destinataire pour allouer son buffer de réception.

Un problème de sémantique se pose cependant. En effet, pour réaliser le matching d'une requête d'envoi avec une requête de réception, NewMadeleine permet à l'application d'attribuer un tag à la requête. Une requête d'envoi sera matchée avec la requête de réception ayant le même tag et la même quantité de donnée, ce qui nous permet de créer la bonne paire envoi/réception. Cependant, lorsque l'on a 2 paires de requêtes ayant la même quantité de données et le même tag, le matching est réalisé en considérant l'ordre séquentiel dans lequel les requêtes d'envoi et de réception ont été postée. Ainsi, la première requête d'envoi sera associée à la première requête de réception postée, et ainsi de suite.

Toutefois, le mécanisme de notification présenté précédemment utilise le même tag que la requête d'envoi associée. Cela permet chez le destinataire de savoir à quelle requête de réception est destinée la notification. Cependant, comme la notification est envoyée avant que la requête d'envoi soit postée, l'ordre séquentiel n'est alors pas encore défini pour la requête correspondante. Ainsi, cela signifie que toute application utilisant ce mécanisme d'envoi de notification devra systématiquement utiliser un tag différent pour chaque requête. Si cela n'est pas fait, alors il est impossible d'associer la notification à la requête de réception correspondante. D'ailleurs, ce cas de figure n'apparaît pas avec StarPU car celui-ci utilise dans ses requêtes un tag différent pour chaque donnée différente, et ne poste
qu'une seule requête à la fois pour une donnée.

# 4 Enregistrement mémoire

Chaque type de réseau haute-performance présente ses propres caractéristiques. Pour certains réseaux comme InfiniBand, une étape d'enregistrement mémoire est nécessaire juste avant de réaliser une communication.

## 4.1 L'enregistrement mémoire sur les réseaux InfiniBand

<figure><img src="image_7.png" /><figcaption>FIGURE 4 – Topologie de 2 machines multi-cœurs reliées par un réseau InfiniBand</figcaption></figure>

Dire que 2 machines sont reliées par un réseau InfiniBand revient à dire que chacune de ces machines possède une carte InfiniBand, comme montré en Figure 4. Ces 2 cartes InfiniBand sont reliées entre elles, et peuvent accéder à la mémoire de la machine distante en réalisant un **Remote Direct Memory Access (RDMA)**. Aussi, un RDMA consiste en réalité en un Direct Memory Access (DMA) réalisé par la carte réseau locale, sur demande de la carte réseau distante. Le principe d'un DMA est de pouvoir lire et écrire dans la mémoire sans passer par l'OS, ni consommer du temps CPU. En effet, cela permet aux communications d'être le plus rapide possible avec un débit maximal.

En revanche, l'utilisation du DMA implique que la carte réseau connaisse l'adresse physique de la page mémoire à laquelle accéder. Cependant, une application est exécutée en espace utilisateur et utilise un adressage virtuel. En effet, avec InfiniBand, on contrôle la carte réseau en faisant de l'OS bypass, en envoyant des requêtes sur le bus PCI vers la carte réseau directement depuis l'espace utilisateur. Ainsi, l'adressage en espace utilisateur rend l'application incapable de fournir à la carte InfiniBand l'adresse physique d'un buffer qu'elle souhaite utiliser pour de l'envoi ou de la réception.

Ainsi, comment fait-on pour fournir l'adresse physique de notre buffer à la carte ? Pour cela, il faut réaliser une étape préalable, comme le montre la Figure 5. L'application va devoir, avant d'envoyer ses données, réaliser un appel système dont le but sera de réaliser la traduction des adresses virtuelles en adresses physiques. Aussi, cet appel système réalisera un pinning des pages mémoires afin que leurs adresses physiques ne puissent pas changer (par exemple avec un swap). Cette étape est appelée **enregistrement mémoire**, et doit donc être réalisée systématiquement avant chaque utilisation d'un buffer pour une réception ou un envoi avec InfiniBand. L'enregistrement mémoire est donc une étape préalable qui est coûteuse car elle réalise un appel système qui est lent vis à vis du débit du réseau InfiniBand.

Aussi, il existe une étape appelée désenregistrement mémoire. En effet, il s'agit de l'appel système permettant de retirer le pinning de nos pages mémoires, et d'oublier la traduction d'adresse réalisée par l'enregistrement mémoire. Ainsi, un envoi classique avec InfiniBand consiste en 3 étapes :

1. Enregistrement mémoire
<figure><img src="image_8.png" /><figcaption>FIGURE 5 – Principe de l'enregistrement mémoire</figcaption></figure>

2. Envoi / réception des données

3. Désenregistrement mémoire

Par ailleurs ; le standard MPI ne propose pas de routine permettant d'indiquer quand réaliser l'enregistrement mémoire d'une requête MPI. Par conséquent, avec MPI, l'enregistrement mémoire est réalisé au dernier moment, c'est à dire au moment d'appeler MPI Send() ou MPI Recv(). L'enregistrement mémoire fait donc partie intégrante du temps de communication !

## 4.2 Cache d'enregistrement

L'enregistrement mémoire étant coûteux, nous voulons l'éviter autant que possible. En effet, pour envoyer 2 fois le même buffer, nous pouvons réaliser un seul enregistrement mémoire pour le premier envoi, puis réutiliser ce premier enregistrement pour envoyer à nouveau le buffer. Ainsi, tant que le nous n'avons pas réalisé un désenregistrement mémoire, nous pouvons éviter toutes les étapes d'enregistrement suivantes pour un même buffer.

De cette manière, l'état de l'art propose l'implémentation d'un cache des enregistrements réalisés, aussi appelé **rcache** (registration cache) [8]. Nous gardons en cache les enregistrements mémoires les plus récents dans l'éventualité où ceux-ci pourraient être réutilisés dans un avenir proche.

Cependant, comme cette implémentation repose sur un cache, elle reste insuffisante pour des applications ne réutilisant que très peu les mêmes buffers d'envoi ou de réception. Par exemple, des applications comme la factorisation de Cholesky (potrf) devront réaliser de nombreux enregistrements mémoire car elles manipulent systématiquement des données différentes, donc des buffers d'envoi et de réception différents. Le coût de l'enregistrement mémoire ne pourra donc pas être évité avec le rcache.
<figure><img src="image_9.png" /><figcaption>FIGURE 6 – Temps d'un enregistrement mémoire selon la taille du buffer à enregistrer</figcaption></figure>

## 4.3 Coût d'un enregistrement mémoire

Nous avons vu que certaines applications allaient devoir réaliser de nombreux enregistrements mémoires sans bénéficier du rcache. Il convient donc d'étudier l'enregistrement mémoire afin de déterminer le temps perdu par enregistrement mémoire.

Le temps passé à réaliser un enregistrement mémoire dépend en grande partie du nombre de pages mémoires à enregistrer. La Figure 6 présente le temps passé dans l'enregistrement mémoire en fonction de la taille du buffer à enregistrer, réalisé sur un réseau InfiniBand HDR à 200 Gbit/s. On remarque que plus la zone mémoire à enregistrer est grande, plus l'enregistrement mémoire est lent. De plus, si l'on utilise des pages mémoire plus grandes (ici de 2 mégaoctets avec des huge pages), l'enregistrement sera plus rapide car il y aura moins de pages à enregistrer et que les pages plus grandes se situent plus haut dans la table des pages et sont donc plus faciles à trouver. De manière générale, on observe 3 phases sur la Figure 6.

- Pour les buffers de tailles inférieure à une page mémoire, le temps d'un enregistrement est constant car il n'y a toujours qu'une seule page à enregistrer

- Pour des buffers plus grands qu'une page mémoire, et environ inférieurs à 10 mégaoctets, le temps de l'enregistrement augmente progressivement car le nombre de pages à enregistrer augmente

- Pour des buffers plus grands que 10 mégaoctets, la pente de la courbe change. D'après une lecture du code noyau Linux, on peut supposer que cela vient du fait que l'appel système commence à réaliser des yield et passe la main à d'autres threads.

En prenant l'application potrf (factorisation de Cholesky) comme référence, la taille des messages envoyés est équivalente à la taille d'une tuile de calcul. Pour de la double précision, avec des tuiles en 320x320, on obtient des tuiles de 820 kilooctets. On pourra donc estimer que le temps d'un enregistrement mémoire est d'environ 25 µs. Cependant, pour un réseau InfiniBand HDR à 200 Gbit/s, le temps d'envoi d'un tel buffer est de 50 µs. Par conséquent, l'enregistrement mémoire représente un tiers du temps de communication réseau dans ce contexte. Cela n'est pas négligeable, d'autant plus que les communications MPI font souvent partie du chemin critique d'un graphe de tâches, ce qui signifie que l'enregistrement mémoire est inclus dans le chemin critique de l'application.

Aussi, lorsque l'on s'intéresse au débit de l'enregistrement mémoire, nous observons en pratique que celui-ci est du même ordre de grandeur que le débit réseau. En effet, la Figure 7 présente le débit d'un enregistrement mémoire en Gbit/s en fonction de la taille de la zone mémoire à envoyer. On y remarque que le débit de l'enregistrement est proche du débit réseau, à un facteur 2 à 4. Cependant, on observe, pendant l'exécution d'applications comme potrf ou gemm que le débit de l'enregistrement mémoire peut être divisé par 2 voire 3. Cela provient probablement de l'augmentation de la charge imposée sur le bus mémoire par l'application
<figure><img src="image_10.png" /><figcaption>FIGURE 7 – Débit d'un enregistrement mémoire selon la taille du buffer à enregistrer</figcaption></figure>

Par ailleurs, en Figure 6 et 7, nous pouvons remarquer l'avantage des huge pages sur le débit de l'enregistrement mémoire. Ainsi, utiliser des pages mémoire plus grandes pourraient s'avérer utile si nous avons besoin d'accélérer cette étape d'enregistrement mémoire. Aussi, l'utilisation volontaire de huge pages, sans utiliser de transparant huge page, pourrait permettre aux applications utilisant ces pages pour des calculs de bénéficier à la fois d'un enregistrement mémoire plus rapide, mais également une réduction des TLB miss et de la fragmentation en mémoire physique des données. Leur utilisation pourrait donc permettre en théorie une accélération des calculs.

## 4.4 Enregistrements mémoire en parallèle

Afin d'augmenter le débit d'enregistrement mémoire, il pourrait être envisageable de paralléliser les enregistrements mémoire. Nous pourrions par exemple utiliser les CPUs non utilisés par StarPU lorsqu'il n'y a pas assez de travail pour y lancer des enregistrements mémoire qui permettront de prendre de l'avance dans les enregistrements à réaliser.

Cependant, l'enregistrement mémoire étant un appel système, il n'est donc pas évident que celui-ci puisse être accéléré en le parallélisant. Il est donc nécessaire de le vérifier via des benchmarks. Pour cela, nous avons tracé en Figure 8 l'accélération de l'enregistrement mémoire en fonction du nombre de CPUs utilisés, en plaçant 1 thread par CPU. Ces tests ont été réalisés sur un réseau InfiniBand HDR à 200 Gbit/s en utilisant des buffers à enregistrer de 100 kilooctets. Nous observons ainsi une accélération presque optimale pour un nombre de CPUs inférieur à 4. En revanche, en augmentant le nombre de CPUs, on observe une asymptote autour d'une accélération de 6. Ainsi, il est bien possible d'accélérer l'enregistrement mémoire en utilisant des CPUs non utilisés par StarPU.

Ensuite, nous pouvons également espérer accélérer encore l'enregistrement en plaçant plusieurs threads réalisant un appel d'enregistrement mémoire sur le même CPU. Nous avons donc réalisé un second benchmark, en Figure 9 et 10, afin de déterminer si l'oversouscription de threads peut accélérer l'appel d'enregistrement mémoire. Ces 2 figures ont été réalisées en désactivant l'hyperthreading, et en répartissant *m* threads sur *n* CPUs ($n \le m$). Ainsi, on observe en Figure 9 que sur 1 seul CPU, l'oversouscription de 2 threads permet une accélération de l'ordre de 1.35. De plus, nous pouvons remarquer qu'utiliser l'oversouscription avec d'avantages de threads et de CPUs permet également d'augmenter le débit de l'enregistrement mémoire. Aussi, nous pouvons voir qu'augmenter le nombre de threads par CPUs n'est pas nécessaire. En effet, le passage de 2 à 4 threads par CPU montre peu d'accélération supplémentaire, indépendamment du nombre de CPUs utilisés.
<figure><img src="image_11.png" /><figcaption>FIGURE 8 – Speedup des enregistrements mémoire en parallèle</figcaption></figure>

<figure><img src="image_12.png" /><figcaption>FIGURE 9 – Débit d'enregistrements mémoire en parallèle avec oversouscription</figcaption></figure>

<figure><img src="image_13.png" /><figcaption>FIGURE 10 – Durée d'enregistrements mémoire en parallèle avec oversouscription</figcaption></figure>

De plus, utiliser l'oversouscription a un coût. En effet, bien que celle-ci permet d'augmenter le débit d'enregistrement moyen, la Figure 10 montre qu'elle augmente aussi le temps moyen nécessaire pour réaliser un enregistrement mémoire. Effectivement, comme plusieurs threads travaillent sur un même CPU, les ressources de calculs se retrouvent partagées et le temps moyen d'un enregistrement avec oversouscription sera légèrement inférieur au temps d'enregistrement sans oversouscription multiplié par le nombre de threads par CPU. Ainsi, pour maximiser l'accélération, il vaut mieux lancer les enregistrements en parallèle avec oversouscription. Cependant, si l'objectif est de terminer un enregistrement le plus rapidement possible, il est préférable de lancer cet enregistrement seul sur son CPU.

## 4.5 Enregistrement mémoire avec StarPU et NewMadeleine

Si le standard MPI ne permet pas à l'utilisateur d'avoir le contrôle sur l'enregistrement mémoire, cette fonctionnalité peut être ajoutée dans l'interface native de NewMadeleine. Par conséquent, la question subsistante est de savoir quand réaliser l'enregistrement de manière à ce que celui-ci soit terminé au moment de soumettre la communication au réseau. Autrement dit, sur un réseau InfiniBand, nous souhaitons terminer l'enregistrement mémoire avant que les données du buffer ne soient prêtes. En effet, comme nous l'avons vu en section 4.1,
l'enregistrement mémoire est une étape qui n'agit pas sur les données du buffer car il ne fait qu'utiliser les adresses.

Ainsi, nous avons réutilisé l'idée proposée en section 3. En effet, côté émetteur, StarPU connaît le futur et peut avertir NewMadeleine de l'envoi temporellement proche d'un buffer. De cette manière, lorsque la dernière tâche écrivant dans un buffer d'émission débute, nous commençons l'enregistrement mémoire par la même occasion. Ensuite, côté récepteur, l'étape d'enregistrement est réalisée immédiatement à la suite de l'allocation mémoire car nous savons déjà à ce moment là que la requête d'émission sera soumise sous peu. De cette manière, nous avons pu retirer le temps de l'enregistrement mémoire du temps de la communication, et par conséquent, du chemin critique de notre graphe de tâche, ce qui devrait aboutir en une amélioration des performances des applications distribuées.

Cependant, l'intégration de cette fonctionnalité dans StarPU pose un problème. En effet, comme tout est réalisé en avance, nous demandons *en avance* à StarPU où sera située la donnée à envoyer. Or, entre le moment où l'on démarre l'enregistrement mémoire et le moment où l'on envoie les données, StarPU peut encore changer d'avis et donc placer les données dans une autre zone mémoire que celle indiquée à l'enregistrement mémoire. Il est donc nécessaire de pouvoir modifier la requête d'envoi et d'annuler l'enregistrement mémoire de la première zone, pour finalement envoyer la bonne zone mémoire.

Dans un premier temps, nous avions décidé d'ajouter une fonction permettant d'annuler une requête d'envoi. Cependant, annuler une requête d'envoi dans notre cas n'est pas simple. En effet, lorsque StarPU demande l'enregistrement en avance d'une requête d'envoi à NewMadeleine, il demande par la même occasion l'envoi d'une notification au récepteur (voir section 3). Or, dans NewMadeleine, la notification et le corps du message sont indissociables et forment ensemble la requête d'envoi. En effet, nous utilisons une fonctionnalité de NewMadeleine permettant de définir la notification comme un header du message. Cela implique que pour annuler une requête d'envoi avec notification, il faut annuler la requête d'envoi alors que le récepteur a déjà reçu la notification (donc les headers) mais pas le corps du message. Ainsi, pour annuler une requête d'envoi, il aurait également fallu l'annuler côté récepteur, c'est à dire que l'émetteur aurait dû envoyer un message informant le destinataire que la requête d'envoi avait été annulée. Or, réaliser tout cela représenterait beaucoup de travail et de changements dans le code source de NewMadeleine, ce n'est donc pas la solution qui a été retenue.

L'annulation d'une requête d'envoi étant complexe à implémenter, nous avons plutôt opté pour l'ajout d'une fonctionnalité permettant d'éditer le corps d'une requête d'envoi tant que nous sommes toujours dans la phase d'enregistrement en avance. De cette manière, la modification d'une requête d'envoi n'impacte pas le destinataire.

Nous avons vu comment réaliser l'enregistrement mémoire en amont d'une communication pour des communications simples point à point. Cependant, il faut aussi prendre en charge les communications collectives. Dans NewMadeleine, les communications collectives sont réalisées en diffusant les données dans le réseau à l'aide d'un ensemble de communications point à point réalisées sous la forme d'un arbre, où chaque nœud représente un processus, et chaque branche est une communication [6]. Pour s'assurer que l'enregistrement est réalisé à temps pour les communications collectives, nous envoyons une notification à travers l'arbre de diffusion sous la forme d'un petit message. Ainsi, chaque processus aura le temps d'allouer son buffer de réception et de réaliser son enregistrement. Finalement, comme dans une collective, un destinataire devient souvent un émetteur, le rcache permet donc à un processus participant à une communication collective de réutiliser lors de l'émission l'enregistrement mémoire réalisé pour la réception.

# 5 Évolution des performances

Pour tester l'amélioration des performances mémoires et de calculs, nous avons réalisé des benchmarks sur le calculateur national irene. La topologie des nœuds utilisés est présentée en Figure 11. Il s'agit d'une architecture ayant 2 processeurs AMD EPYC 7H12, pour un total de 128 cœurs physiques, 248 Go de mémoire répartis équitablement dans 8 nœud NUMA, et un réseau InfiniBand HDR de 200 Gbit/s. L'ensemble des tests qui suivront sont tous réalisés en double précision, avec des tuiles de taille 320 par 320, et en plaçant 1 processus MPI par nœud NUMA, soit 8 processus par nœud. Nous avons choisi l'application *potrf* (factorisation de Cholesky) car elle fait partie des applications pour lesquelles le rcache n'est pas efficace, et donc où le temps d'enregistrement mémoire fait systématiquement partie du temps de communication.
<figure><img src="image_14.png" /><figcaption>FIGURE 11 – Topologie d'un nœud du calculateur irène</figcaption></figure>

Tout d'abord, on va s'intéresser à la consommation mémoire de l'application sur l'un de ses processus. La Figure 3 présente le maximum de la consommation mémoire allouée entre le début de l'application et un instant *t1*, en fonction de cet instant *t1*. On y compare l'ancienne implémentation, où l'allocation des buffers de réception est réalisée dès le début de l'application avec la nouvelle où l'allocation est réalisée un peu avant la réception effective. On remarque un gain significatif en mémoire. En effet, pour des matrices de taille 180 000 par 180 000, l'ancienne implémentation n'avait pas suffisamment de mémoire pour stocker tous les buffers de réception. Pour la nouvelle implémentation, la mémoire utilisée ne dépasse pas 7 Go.

Ensuite, toujours pour potrf, nous allons nous intéresser aux performances globales de l'application. Nous devons nous assurer que les performances n'ont pas chuté, ce qui signifierait que l'allocation mémoire serait réalisée trop tardivement, et ralentirait alors les communications. En Figure 13, nous comparons 2 exécutions sans enregistrement, où l'allocation mémoire des buffers de réception est réalisée soit au début de l'application, soit un peu avant la réception effective des données. Nous remarquons ici que les performances n'ont pas changé, ce qui signifie que l'allocation mémoire est bien en dehors du chemin critique de la communication.

Enfin, nous souhaitons savoir si retirer le temps de l'enregistrement mémoire du temps de la communication réseau a amélioré les performances de l'application. Pour cela, la Figure 14 présente les performances de l'application potrf en fonction de la taille de matrice utilisée. Les 2 versions comparées réalisent l'allocation mémoire des buffers de réception grâce à une notification de l'émetteur, cependant l'une réalise l'enregistrement mémoire en avance alors que l'autre le réalise juste avant l'envoi ou réception. On remarque que dans la pente, on obtiens une augmentation des performances de l'ordre de 15%. Pour des matrices plus grandes, cette différence diminue jusqu'à être négligeable. L'augmentation des performances uniquement dans la pente s'explique principalement par le fait que pour des matrices de taille moyenne, StarPU a des difficultés à obtenir du parallélisme sur tous les cœurs. Ainsi, des communications plus rapides permettent de débloquer des tâches plus rapidement, ce qui permet une augmentation des performances.
<figure><img src="image_15.png" /><figcaption>FIGURE 12 – Utilisation mémoire - Cholesky 179 840 x 179 840 double précision - tuiles 320x320 - 8 nœuds - 64 processus MPI</figcaption></figure>

# 6 Conclusion

Dans sa version distribuée, StarPU allouait un buffer de réception aussitôt la tâche soumise au runtime. Ce qui résultait en un manque de scalabilité dû à une sur-utilisation mémoire venant d'un trop grand nombre de buffers de réception alloués.

Pour résoudre ce problème, nous avons déplacé l'allocation des buffers de réception avant la réception des données. Pour ce faire, nous avons utilisé la connaissance du futur détenue par StarPU qui possède le graphe de tâche. En effet, cette connaissance permet à l'émetteur d'envoyer en avance une notification au destinataire pour l'avertir que la requête d'envoi va bientôt être envoyée. Le destinataire a obtenu ainsi une fenêtre de temps avant la réception effective des données pendant laquelle il peut allouer les buffers de réception, mais aussi réaliser d'autres étapes préalables, dans notre cas l'enregistrement mémoire.

En effet, certains réseaux demandent une étape d'enregistrement avant d'envoyer les données. Avant, cette étape d'enregistrement était réalisée juste avant la communication et rallongeait par conséquent sa durée. Nous avons pu encore une fois profiter du graphe de tâches détenu par StarPU pour réaliser l'étape d'enregistrement en amont de la communication, sans pour autant ralentir celle-ci. Ainsi nous avons pu retirer la durée de l'enregistrement du chemin critique d'une communication.

En déplaçant l'allocation des buffers mémoire, nous avons pu obtenir un gain en mémoire conséquent, permettant désormais aux applications d'obtenir un meilleur scaling. De plus cette amélioration mémoire est accompagnée d'une hausse des performances de calcul de l'ordre de 15 % pour l'application potrf, avec des matrices de taille moyenne, ce qui signifie que nous avons bien réussi à retirer l'enregistrement du chemin critique de l'application.

# Travaux futurs

Dans NewMadeleine, l'ajout d'une fonction d'enregistrement mémoire avec envoi de notification au destinataire casse la séquentialité des communications. L'ordre séquentiel pour des requêtes ayant le même tag (voir section 3) est désormais défini par l'enregistrement ce qui n'est pas souhaitable.

Nous avons observé que le débit réseau d'une application est 2 à 3 fois inférieur au débit réseau des benchmarks. Bien qu'on peut supposer que cette réduction provienne d'une surcharge du bus mémoire, il serait préférable d'étudier la raison de cette diminution des performances pour potentiellement la prendre en compte dans les
<figure><img src="image_16.png" /><figcaption>FIGURE 13 – Comparaison selon le moment d’allocation du buffer de réception - Cholesky double précision - tuiles 320x320 - 16 nœuds- 128 processus MPI - communications point à point</figcaption></figure>

communications.

L’utilisation de huge pages permet une augmentation significative du débit réseau et devrait augmenter les performances des applications. Il serait intéressant de les utiliser dans StarPU sur demande de l’utilisateur.

Nous avons pu améliorer l’interaction entre StarPU et l’interface native de NewMadeleine, cependant l’al-
location mémoire côté récepteur pourrait être elle aussi améliorée pour un cas d’utilisation StarPU + standard MPI.

De la même manière que l'interface native de NewMadeleine propose l'utilisation de priorités pour des
communications réseau. Celles-ci permettent aux communications sur le chemin critique du graphe de tâche d'être
envoyées en priorité. Cette interface pourrait également proposer une gestion des enregistrements mémoire à l'aide
de priorités. En effet, cet ajout permettrait de s'assurer qu'un maximum d'enregistrements soient réalisés à temps
dans des cas de congestion réseau.
<figure><img src="image_17.png" /><figcaption>FIGURE 14 – Cholesky double précision - tuiles 320x320 - 16 nœuds- 128 processus MPI - communications point à point - Comparaison avec et sans enregistrement mémoire</figcaption></figure>

Références

[1] Emmanuel AGULLO et al. « Achieving High Performance on Supercomputers with a Sequential Task-based Programming Model ». In : 2017.

[2] Cédric AUGONNET et al. « StarPU : A Unified Platform for Task Scheduling on Heterogeneous Multicore Architectures ». In : *Concurrency and Computation : Practice and Experience* 23.2 (fév. 2011). Special Issue : Euro-Par 2009, p. 187-198.

[3] Olivier AUMAGE et al. « NewMadeleine : a Fast Communication Scheduling Engine for High Performance Networks ». In : *Proceedings of the Workshop on Communication Architecture for Clusters (CAC 2007)*. Long Beach, California, United States, mars 2007.

[4] Guillaume BEAUCHAMP. « Portage de StarPU sur la bibliothèque de communication NewMadeleine ». In : (2017). URL : https://inria.hal.science/hal-01587584v1.

[5] Alexandre DENIS. « Scalability of the NewMadeleine Communication Library for Large Numbers of MPI Point-to-Point Requests ». In : (avr. 2019). URL : https://inria.hal.science/hal-02103700v1.

[6] Alexandre DENIS et al. « Using Dynamic Broadcasts to improve Task-Based Runtime Performances ». In : (2020). URL : https://inria.hal.science/hal-02872765v1.

[7] Philippe SWARTVAGHER. « On the Interactions between HPC Task-based Runtime Systems and Communication Libraries ». In : (2023). URL : https://inria.hal.science/tel-03989856v1.

[8] Hiroshi TEZUKA et al. « Pin-down cache : a virtual memory management technique for zero-copy communication ». In : *Proceedings of the First Merged International Parallel Processing Symposium and Symposium on Parallel and Distributed Processing (IPPS/SPDP)*. Avr. 1998, p. 308-314.