Analyse approfondie de la bibliothèque HCCL dans CANN Ascend : primitives de communication parallèle multi-cartes et guide technique complet d’optimisation des performances

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 :

https://atomgit.com/cann/hccl

Étiquettes: HCCL cann Ascend NPU communication_collective

Publié le 24 octobre à 11h44