Conseils et pièges de configuration de la bibliothèque de parallélisme des modèles SageMaker distribués - Amazon SageMaker

Les traductions sont fournies par des outils de traduction automatique. En cas de conflit entre le contenu d'une traduction et celui de la version originale en anglais, la version anglaise prévaudra.

Conseils et pièges de configuration de la bibliothèque de parallélisme des modèles SageMaker distribués

Consultez les conseils et astuces suivants avant d'utiliser la bibliothèque de parallélisme SageMaker de modèles d'Amazon. Cette liste contient des conseils qui s'appliquent à tous les cadres. Pour TensorFlow des conseils PyTorch spécifiques, voir Modifier un script TensorFlow d'entraînement etModifier un script PyTorch d'entraînement, respectivement.

Taille de lot et nombre de micro-lots

  • La bibliothèque est la plus efficace lorsque la taille du lot est augmentée. Dans les cas d'utilisation où le modèle tient dans un seul périphérique, mais ne peut être entraîné qu'avec un lot de petite taille, la taille du lot peut et doit être augmentée après l'intégration de la bibliothèque. Le parallélisme des modèles permet d'économiser de la mémoire pour les grands modèles, ce qui permet un entraînement avec des tailles de lots qui ne tenaient pas dans la mémoire auparavant.

  • Choisir un nombre de micro-lots trop petit ou trop grand peut baisser les performances. La bibliothèque exécute chaque micro-lot séquentiellement dans chaque périphérique, de sorte que la taille du micro-lot (taille du lot divisée par le nombre de micro-lots) doit être suffisamment grande pour utiliser pleinement chaque GPU. Dans le même temps, comme l'efficacité du pipeline augmente avec le nombre de micro-lots, il est important de trouver le bon équilibre. Normalement, un bon point de départ consiste à essayer 2 ou 4 micro-lots, en augmentant la taille du lot jusqu'à la limite de mémoire, puis à expérimenter avec des tailles de lot et un nombre de micro-lots supérieurs. L'augmentation du nombre de micro-lots permet d'envisager des tailles de lots supérieures, si un pipeline entrelacé est utilisé.

  • La taille de votre lot doit toujours être divisible par le nombre de micro-lots. Veuillez noter que, selon la taille du jeu de données, la taille du dernier lot de chaque époque peut parfois être inférieure au reste, mais ce petit lot doit également être divisible par le nombre de micro-lots. Si ce n'est pas le cas, vous pouvez définir drop_remainder=True l'tf.Dataset.batch()appel (in TensorFlow) ou le définir DataLoader (drop_last=Truein PyTorch), afin que ce dernier petit lot ne soit pas utilisé. Si vous utilisez une API différente pour le pipeline de données, vous devrez peut-être ignorer manuellement le dernier lot chaque fois qu'il n'est pas divisible par le nombre de micro-lots.

Partitionnement manuel

  • Si vous utilisez le partitionnement manuel, pensez toujours aux paramètres qui sont utilisés par plusieurs opérations et modules de votre modèle, tels que la table d'incorporation dans les architectures de transformateur. À des fins d'exactitude, les modules qui partagent le même paramètre doivent être placés dans le même périphérique. Lorsque vous utilisez le partitionnement automatique, la bibliothèque applique automatiquement cette contrainte.

Préparation des données

  • Si le modèle utilise plusieurs entrées, veillez à répartir les opérations aléatoires dans votre pipeline de données (remaniement, par exemple) avec smp.dp_rank(). Si le jeu de données est partitionné de manière déterministe entre des périphériques parallèles de données, assurez-vous que la partition est indexée par smp.dp_rank(). Ceci permet de garantir la cohérence de l'ordre des données affichées sur tous les rangs qui forment une partition de modèle.

Renvoyer les tenseurs à partir de smp.DistributedModel

  • Tout tenseur renvoyé par la fonction smp.DistributedModel.call (for TensorFlow) ou smp.DistributedModel.forward (for PyTorch) est diffusé vers tous les autres rangs, à partir du rang qui a calculé ce tenseur particulier. Par conséquent, tout tenseur qui n'est pas nécessaire en dehors des méthodes d'appel et de transmission (activations intermédiaires, par exemple) ne doit pas être renvoyé, car cela provoque un surdébit inutile de communication et de mémoire et nuit aux performances.

Le décorateur @smp.step

  • Si l'argument tenseur d'une fonction décorée smp.step n'a pas de dimension de lot, le nom de l'argument doit être fourni dans la liste non_split_inputs lors de l'appel smp.step. Cela empêche la bibliothèque d'essayer de diviser le tenseur en micro-lots. Pour de plus amples informations, consultez smp.step dans la documentation sur l'API.

Retarder l'initialisation des paramètres

Pour les très grands modèles comportant plus de 100 milliards de paramètres, l'initialisation du poids via la mémoire du processeur peut entraîner une out-of-memory erreur. Pour contourner ce problème, la bibliothèque propose un gestionnaire de contexte smp.delay_param_initialization. Cela retarde l'allocation physique des paramètres jusqu'à ce qu'ils se déplacent vers le GPU lors de la première exécution d'une fonction décorée smp.step. Cela évite l'utilisation inutile de la mémoire du processeur pendant l'initialisation de la formation. Utilisez le gestionnaire de contexte lorsque vous créez un objet de modèle comme illustré dans le code suivant.

with smp.delay_param_initialization(enabled=True): model = MyModel()

Parallélisme tensoriel pour PyTorch

  • Si vous utilisez une graine pour des résultats déterministes, définissez la graine en fonction de smp.dp_rank() (par exemple, torch.manual_seed(42 + smp.dp_rank())). Si vous ne le faites pas, différentes partitions d'un paramètre nn.Parameter sont initialisés de la même manière, ce qui a un impact sur la convergence.

  • SageMakerde la bibliothèque de parallélisme des modèles utilise NCCL pour implémenter les collectifs nécessaires à la distribution des modules. En particulier pour les modèles plus petits, si trop d'appels NCCL sont programmés simultanément sur le GPU, l'utilisation de la mémoire peut augmenter en raison de l'espace supplémentaire utilisé par NCCL. Pour contrer cela, smp limite les appels NCCL de sorte que le nombre d'opérations de la NCCL en cours à un moment donné soit inférieur ou égal à une limite donnée. La limite par défaut est 8, mais elle peut être ajustée à l'aide de la variable d'environnement SMP_NCCL_THROTTLE_LIMIT. Si vous constatez une utilisation de la mémoire plus importante que prévu lors de l'utilisation du parallélisme de tenseur, vous pouvez essayer de réduire cette limite. Toutefois, le choix d'une limite trop faible peut entraîner une perte de débit. Pour désactiver complètement la limitation, vous pouvez définir SMP_NCCL_THROTTLE_LIMIT=-1.

  • L'identité suivante, qui s'applique lorsque le degré de parallélisme de tenseur est de 1, ne tient pas lorsque le degré de parallélisme de tenseur est supérieur à 1 : smp.mp_size() * smp.dp_size() == smp.size(). En effet, le groupe de parallélisme de tenseur fait partie du groupe de parallélisme du modèle et du groupe de parallélisme des données. Si votre code contient déjà des références à mp_rank, mp_size, MP_GROUP, et ainsi de suite, et si vous souhaitez travailler uniquement avec le groupe parallèle de pipeline, vous devrez peut-être remplacer les références par smp.pp_size(). Les identités suivantes sont toujours vraies :

    • smp.mp_size() * smp.rdp_size() == smp.size()

    • smp.pp_size() * smp.dp_size() == smp.size()

    • smp.pp_size() * smp.tp_size() * smp.rdp_size() == smp.size()

  • Depuis la fonction wrapper smp.DistributedModel modifie les paramètres du modèle lorsque le parallélisme de tenseur est activé, l'optimiseur doit être créé après l'appel smp.DistributedModel, avec les paramètres distribués. Par exemple, les éléments suivants ne fonctionnent pas :

    ## WRONG model = MyModel() optimizer = SomeOptimizer(model.parameters()) model = smp.DistributedModel(model)  # optimizer now has outdated parameters! 

    Au lieu de cela, l'optimiseur doit être créé avec les paramètres du smp.DistributedModel comme suit :

    ## CORRECT model = smp.DistributedModel(MyModel()) optimizer = SomeOptimizer(model.optimizers())
  • Lorsqu'un module est remplacé par son homologue distribué par parallélisme de tenseur, le module distribué n'hérite pas de ses poids du module d'origine et initialise de nouveaux poids. Cela signifie que, par exemple, si les pondérations doivent être initialisées dans un appel particulier (par exemple, via un appel load_state_dict), cela doit se produire après l'appel smp.DistributedModel, une fois que la distribution du module a eu lieu.

  • Lorsque vous accédez directement aux paramètres des modules distribués, notez que le poids n'a pas la même forme que le module d'origine. Par exemple, 

    with smp.tensor_parallelism():     linear = nn.Linear(60, 60) # will pass assert tuple(linear.weight.shape) == (60, 60) distributed_linear = smp.DistributedModel(linear) # will fail. the number of input channels will have been divided by smp.tp_size() assert tuple(distributed_linear.module.weight.shape) == (60, 60)
  • A l'aide de torch.utils.data.distributed.DistributedSampler est fortement recommandé pour le parallélisme de tenseur. Cela garantit que chaque classement parallèle de données reçoit le même nombre d'échantillons de données, ce qui évite les blocages pouvant résulter de différents dp_rank prenant un certain nombre de mesures différentes.

  • Si vous utilisez l'joinAPI PyTorch de la DistributedDataParallel classe pour gérer les cas dans lesquels différents rangs parallèles de données comportent un nombre de lots différent, vous devez tout de même vous assurer que les rangs appartenant à la même classe TP_GROUP contiennent le même nombre de lots ; sinon, les collectifs de communication utilisés dans l'exécution distribuée des modules risquent de se bloquer. Les rangs qui sont dans des TP_GROUP différents peuvent avoir un nombre différent de lots, à condition que l'API join est utilisé.

  • Si vous souhaitez contrôler votre modèle et utiliser le parallélisme tenseur, tenez compte des points suivants :

    • Pour éviter les conditions de décrochage et de course lors de l'enregistrement et du chargement des modèles lorsque vous utilisez le parallélisme de tenseurs, assurez-vous d'appeler les fonctions appropriées à partir des états de modèle et d'optimiseur suivants dans un rang de parallélisme réduit des données.

    • Si vous faites la transition d'un script parallèle de pipeline existant et que vous activez le parallélisme de tenseur pour le script, veillez à modifier n'importe quel bloc if smp.dp_rank() == 0 utilisé pour enregistrer et charger avec les blocs if smp.rdp_rank() == 0. Sinon, cela pourrait entraîner le blocage de votre tâche d'entraînement.

    Pour en savoir plus sur les points de contrôle d'un modèle avec parallélisme de tenseur, consultez Point de contrôle d'un modèle distribué.