Backends d'accélérateur

Il est assez simple de décrire un calcul Tensor , mais le moment et la manière dont ce calcul est effectué dépendront du backend utilisé pour les Tensor et du moment où les résultats sont nécessaires sur le processeur hôte.

En coulisses, les opérations sur Tensor sont réparties vers des accélérateurs tels que les GPU ou les TPU , ou exécutées sur le CPU lorsqu'aucun accélérateur n'est disponible. Cela se produit automatiquement pour vous et facilite l'exécution de calculs parallèles complexes à l'aide d'une interface de haut niveau. Cependant, il peut être utile de comprendre comment se produit cette répartition et de pouvoir la personnaliser pour des performances optimales.

Swift pour TensorFlow dispose de deux backends pour effectuer des calculs accélérés : le mode impatient TensorFlow et X10. Le backend par défaut est le mode TensorFlow impatient, mais cela peut être remplacé. Un tutoriel interactif est disponible qui vous guide dans l'utilisation de ces différents backends.

Mode impatient de TensorFlow

Le backend en mode impatient de TensorFlow exploite l'API TensorFlow C pour envoyer chaque opération Tensor à un GPU ou à un CPU au fur et à mesure qu'elle est rencontrée. Le résultat de cette opération est ensuite récupéré et transmis à l’opération suivante.

Cette répartition opération par opération est simple à comprendre et ne nécessite aucune configuration explicite dans votre code. Cependant, dans de nombreux cas, cela n'aboutit pas à des performances optimales en raison de la surcharge liée à l'envoi de nombreuses petites opérations, combinée au manque de fusion et d'optimisation des opérations qui peut survenir lorsque des graphiques d'opérations sont présents. Enfin, le mode impatient de TensorFlow est incompatible avec les TPU et ne peut être utilisé qu'avec les CPU et les GPU.

X10 (traçage basé sur XLA)

X10 est le nom du backend Swift pour TensorFlow qui utilise le traçage tensoriel paresseux et le compilateur d'optimisation XLA pour, dans de nombreux cas, améliorer considérablement les performances par rapport à la répartition opération par opération. De plus, il ajoute la compatibilité pour les TPU , des accélérateurs spécifiquement optimisés pour les types de calculs trouvés dans les modèles d'apprentissage automatique.

L'utilisation de X10 pour les calculs Tensor n'est pas la valeur par défaut, vous devez donc vous inscrire à ce backend. Cela se fait en spécifiant qu'un Tensor est placé sur un appareil XLA :

let tensor1 = Tensor<Float>([0.0, 1.0, 2.0], on: Device.defaultXLA)
let tensor2 = Tensor<Float>([1.5, 2.5, 3.5], on: Device.defaultXLA)

Après ce point, la description d'un calcul est exactement la même que pour le mode impatient de TensorFlow :

let tensor3 = tensor1 + tensor2

Des détails supplémentaires peuvent être fournis lors de la création d'un Tensor , comme le type d'accélérateur à utiliser et même lequel, si plusieurs sont disponibles. Par exemple, un Tensor peut être créé sur le deuxième périphérique TPU (en supposant qu'il soit visible par l'hôte sur lequel le programme s'exécute) en utilisant les éléments suivants :

let tpuTensor = Tensor<Float>([0.0, 1.0, 2.0], on: Device(kind: .TPU, ordinal: 1, backend: .XLA))

Aucun mouvement implicite des Tensor entre les appareils n'est effectué, donc si deux Tensor sur des appareils différents sont utilisés ensemble dans une opération, une erreur d'exécution se produira. Pour copier manuellement le contenu d'un Tensor sur un nouvel appareil, vous pouvez utiliser l'initialiseur Tensor(copying:to:) . Certaines structures à plus grande échelle contenant des Tensor , comme les modèles et les optimiseurs, disposent de fonctions d'assistance pour déplacer tous leurs Tensor intérieurs vers un nouvel appareil en une seule étape.

Contrairement au mode impatient de TensorFlow, les opérations utilisant le backend X10 ne sont pas distribuées individuellement au fur et à mesure qu'elles sont rencontrées. Au lieu de cela, la distribution vers un accélérateur n'est déclenchée que par la lecture des valeurs calculées à l'hôte ou par la mise en place d'une barrière explicite. La façon dont cela fonctionne est que le moteur d'exécution démarre à partir de la valeur lue sur l'hôte (ou du dernier calcul avant une barrière manuelle) et trace le graphique des calculs qui aboutissent à cette valeur.

Ce graphique tracé est ensuite converti en représentation intermédiaire XLA HLO et transmis au compilateur XLA pour être optimisé et compilé pour exécution sur l'accélérateur. De là, l’intégralité du calcul est envoyée à l’accélérateur et le résultat final est obtenu.

Le calcul est un processus qui prend du temps, c'est pourquoi X10 est mieux utilisé avec des calculs massivement parallèles exprimés via un graphique et effectués plusieurs fois. Les valeurs de hachage et la mise en cache sont utilisées afin que des graphiques identiques ne soient compilés qu'une seule fois pour chaque configuration unique.

Pour les modèles d’apprentissage automatique, le processus de formation implique souvent une boucle dans laquelle le modèle est soumis encore et encore à la même série de calculs. Vous souhaiterez que chacune de ces passes soit considérée comme une répétition de la même trace, plutôt que comme un long graphique contenant des unités répétées. Ceci est activé par l'insertion manuelle d'un appel à la fonction LazyTensorBarrier() aux emplacements de votre code où vous souhaitez qu'une trace se termine.

Prise en charge de précision mixte dans X10

La formation avec une précision mixte via X10 est prise en charge et des API de bas niveau et de haut niveau sont fournies pour la contrôler. L' API de bas niveau propose deux propriétés calculées : toReducedPrecision et toFullPrecision qui convertissent entre la précision complète et réduite, ainsi que isReducedPrecision pour interroger la précision. Outre les Tensor , les modèles et les optimiseurs peuvent être convertis entre une précision totale et réduite à l'aide de cette API.

Notez que la conversion en précision réduite ne modifie pas le type logique d'un Tensor . Si t est un Tensor<Float> , t.toReducedPrecision est également un Tensor<Float> avec une représentation sous-jacente de précision réduite.

Comme pour les appareils, les opérations entre tenseurs de précisions différentes ne sont pas autorisées. Cela évite une promotion silencieuse et indésirable vers des flottants 32 bits, qui seraient difficiles à détecter par l'utilisateur.