Introduction
Dans l'architecture hétérogène Ascend CANN, l'entraînement distribué constitue l'un des scénarios les plus critiques. Lorsqu’il s’agit d’entraîner de grands modèles comme BERT-Large ou LLaMA-7B, une seule carte NPU ne suffit ni à stocker le modèle entier, ni à exécuter efficacement le processus d'apprentissage. Il devient donc indispensable de répartir le modèle ou les données sur plusieurs cartes NPU afin qu’elles collaborent. Ce travail collaboratif repose essentiellement sur la communication inter-cartes : synchronisation des gradients, des paramètres et des états du modèle — toutes opérations gérées par HCCL.
HCCL (Huawei Collective Communication Library) est la bibliothèque de communication collective dédiée aux processeurs NPU Ascend, jouant un rôle équivalent à NCCL pour les GPU NVIDIA. Elle fournit un ensemble de primitives hautement optimisées — telles que AllReduce, Broadcast, AllGather et ReduceScatter — permettant un échange de données rapide entre plusieurs NPU avec une latence minimale et une bande passante maximisée. Bien que la communication puisse sembler simple au premier abord, son efficacité impacte directement la scalabilité du système d'entraînement. Même si une carte fonctionne à pleine vitesse, un goulot d’étranglement au niveau de la communication peut réduire drastiquement les gains attendus d’un cluster de 128 cartes.
Primitives fondamentales de communication dans HCCL
AllReduce : pilier de l'entraînement en parallèle de données
AllReduce est la primitive la plus utilisée dans les configurations de type data parallelism. Chaque processus local calcule ses gradients, puis tous participent à une opération de réduction (somme, moyenne, etc.), dont le résultat est ensuite diffusé uniformément à tous.
import torch
import cann
def execute_allreduce(tensor, operation="sum"):
"""
Applique AllReduce via HCCL.
Args:
tensor: tenseur à réduire (float16/float32)
operation: type de réduction ('sum', 'avg', 'max', etc.)
Returns:
Tenseur contenant le résultat agrégé
"""
communicator = cann.comm.get_world_comm()
reduced_tensor = cann.hccl.allreduce(
input=tensor,
comm=communicator,
reduction_type=operation
)
return reduced_tensor
Dans un contexte d'entraînement distribué :
- Chaque NPU traite un mini-lot différent.
- Les gradients locaux sont calculés indépendamment.
- Un appel à AllReduce agrège les gradients sur toutes les cartes.
- L’optimiseur met à jour les poids localement avec les gradients synchronisés.
L’algorithme sous-jacent utilisé par HCCL est souvent Ring AllReduce, qui divise la communication en deux phases : Reduce-Scatter suivie d’AllGather. Cette approche limite la charge réseau par lien physique, offrant une meilleure efficacité sur des topologies à large échelle.
Broadcast : diffusion hiérarchisée des paramètres
Broadcast permet à un processus source (appelé root) de propager une donnée à tous les autres processus du groupe. Cette opération est fréquente lors de l’initialisation ou de la mise à jour d’un modèle centralisé.
def diffuse_parametres(parametres, source_rank):
communicateur = cann.comm.get_world_comm()
cann.hccl.broadcast(
data=parametres,
root=source_rank,
comm=communicateur
)
Cette primitive est particulièrement utile dans les architectures de type parameter server, où un nœud maître maintient les derniers paramètres du modèle et les redistribue régulièrement aux workers.
AllGather : agrégation complète des sorties locales
AllGather concatène les contributions partielles de chaque processus pour former un résultat global. Contrairement à AllReduce, il ne réduit pas les données mais les assemble.
def collecte_globale(partiels):
taille_monde = cann.comm.get_world_comm().size()
dimension_sortie = list(partiels.shape)
dimension_sortie[0] *= taille_monde # Agrégation selon la première dimension
resultat_global = torch.empty(dimension_sortie, dtype=partiels.dtype, device=partiels.device)
cann.hccl.allgather(
input=partiels,
output=resultat_global,
comm=cann.comm.get_world_comm()
)
return resultat_global
Typiquement utilisé lors de l’évaluation distribuée ou dans les schémas de model parallelism, où chaque NPU gère une portion du modèle et doit partager sa sortie pour reconstruction globale.
ReduceScatter : division intelligente de la charge
ReduceScatter combine une opération de réduction (ex. somme) avec une distribution partielle du résultat. Chaque processus reçoit un fragment du tenseur agrégé, ce qui permet de réduire l’utilisation mémoire.
def reduce_et_repartir(tenseur_entier):
comm = cann.comm.get_world_comm()
nb_cartes = comm.size()
taille_par_fragments = tenseur_entier.shape[0] // nb_cartes
sortie_locale = torch.zeros(taille_par_fragments, device=tenseur_entier.device)
cann.hccl.reduce_scatter(
input=tenseur_entier,
output=sortie_locale,
reduction_type="sum",
comm=comm
)
return sortie_locale
Cette primitive est centrale dans les stratégies d’optimisation mémoire comme ZeRO-2 ou ZeRO-3, où les gradients ou les états d’optimiseur sont fragmentés entre les dispositifs.
Optimisation basée sur la topologie matérielle
HCCL exploite la structure physique du cluster pour choisir dynamiquement l’algorithme de communication optimal. Les types de connexion influencent fortement cette décision :
- HCCS : Interconnexion intra-puce, très haute bande passante.
- NVLink/PCIe : Liaison entre cartes au sein d’un même serveur.
- RoCE/IB : Réseau inter-nœuds, latence plus élevée.
En fonction de ces caractéristiques, HCCL sélectionne automatiquement :
- Ring AllReduce pour les communications internes (haute bande passante, faible contention).
- Tree AllReduce pour les clusters inter-nœuds (moins de connexions simultanées, latence logarithmique).
Exemple de logique de sélection :
def algorithme_optimal(bande_passante, taille_groupe, est_inter_noeud):
if not est_inter_noeud and bande_passante > 100:
return "ring"
elif est_inter_noeud and taille_groupe > 16:
return "tree"
else:
return "ring" # fallback
Stratégies d’optimisation des performances
Plusieurs paramètres influencent directement l’efficacité des opérations HCCL :
Configuration des tampons de communication
Une taille de buffer inadaptée peut nuire à la performance :
- Trop petit → fragmentation excessive, augmentation de la latence.
- Trop grand → occupation inutile de la mémoire HBM.
Recommandations pratiques :
| Taille des données | Taille de buffer conseillée |
|---|---|
| < 1 Mo | 64 Ko |
| 1–100 Mo | 4 Mo |
| > 100 Mo | 16 Mo |
Variables d’environnement clés
export HCCL_BUFF_SIZE=16777216 # 16 Mo
export HCCL_MAX_NCHANNELS=8 # Canaux max pour le transfert
export HCCL_NET_GDR_LEVEL=P2 # Activation GDR si disponible
export HCCL_NOCLROUTE_ENABLE=1 # Optimisation du routage
Mesure des performances
Il est crucial de valider empiriquement les performances dans votre environnement. Voici un script de benchmark simplifié :
def mesurer_performance_allreduce():
comm = cann.comm.get_world_comm()
tailles_test = [1024, 1024*1024, 16*1024*1024] # 1K, 1M, 16M octets
resultats = {}
for taille in tailles_test:
nb_elements = taille // 4 # float32
donnee = torch.randn(nb_elements, device='npu')
# Phase de rodage
for _ in range(10):
cann.hccl.allreduce(donnee, comm=comm, reduction_type="sum")
comm.barrier()
# Mesure
nb_iterations = 100 if taille < 1024*1024 else 20
torch.npu.synchronize()
debut = time.perf_counter()
for _ in range(nb_iterations):
cann.hccl.allreduce(donnee, comm=comm, reduction_type="sum")
comm.barrier()
torch.npu.synchronize()
fin = time.perf_counter()
latence_moyenne_ms = (fin - debut) / nb_iterations * 1000
debit_gb_s = (taille / 1e9) / (latence_moyenne_ms / 1000)
resultats[taille] = {
"latence_ms": latence_moyenne_ms,
"bande_passante_GB_s": debit_gb_s
}
if comm.rank == 0:
print(f"Taille={taille/1024:.1f}Ko | "
f"Latence={latence_moyenne_ms:.3f}ms | "
f"BW={debit_gb_s:.2f}GB/s")
return resultats
Performances typiques sur cluster Ascend 910
| Configuration | Latence AllReduce | Bande passante | Efficacité d’échelle |
|---|---|---|---|
| 4 cartes, même nœud (NVLink) | ~0.5 ms | ~100 GB/s | >90% |
| 8 cartes, même nœud | ~0.8 ms | ~150 GB/s | >85% |
| 32 cartes, inter-nœuds (RoCE) | ~2 ms | ~30 GB/s | >70% |
| 64 cartes, inter-nœuds | ~3 ms | ~25 GB/s | >60% |
On observe une baisse progressive de l’efficacité d’échelle avec l’augmentation du nombre de nœuds, principalement due à la proportion croissante de communication inter-nœuds, plus coûteuse.
Conclusion
HCCL constitue la colonne vertébrale des entraînements distribués sur plateforme Ascend. Grâce à ses primitives robustes et à son adaptation automatique à la topologie matérielle, il permet d’atteindre des niveaux élevés d’efficacité même à grande échelle. Maîtriser son fonctionnement — notamment le choix des algorithmes, la configuration des buffers et l’analyse des goulots d’étranglement — est fondamental pour optimiser les performances des applications IA hautes performances. Pour explorer les détails d’implémentation, le code source de HCCL est acessible publiquement :