Pourquoi exprimer des calculs sous forme de multiplications matricielles les rend-ils plus rapides?

18

Dans le didacticiel MNist de Google utilisant TensorFlow , un calcul est présenté dans lequel une étape équivaut à multiplier une matrice par un vecteur. Google montre d'abord une image dans laquelle chaque multiplication et addition numériques qui seraient nécessaires pour effectuer le calcul sont écrites en entier. Ensuite, ils montrent une image dans laquelle elle est plutôt exprimée comme une multiplication matricielle, affirmant que cette version du calcul est, ou du moins pourrait être, plus rapide:

Si nous écrivons cela sous forme d'équations, nous obtenons:

équation scalaire

Nous pouvons «vectoriser» cette procédure, la transformant en une multiplication matricielle et une addition vectorielle. Ceci est utile pour l'efficacité du calcul. (C'est aussi une façon utile de penser.)

équation vectorielle

Je sais que des équations comme celle-ci sont généralement écrites dans le format de multiplication matricielle par les praticiens de l'apprentissage automatique, et peuvent bien sûr voir les avantages de le faire du point de vue de la concision du code ou de la compréhension des mathématiques. Ce que je ne comprends pas, c'est l'affirmation de Google selon laquelle la conversion de la forme longue à la forme matricielle "est utile pour l'efficacité du calcul"

Quand, pourquoi et comment serait-il possible d'améliorer les performances des logiciels en exprimant les calculs sous forme de multiplications matricielles? Si je devais calculer moi-même la multiplication matricielle dans la deuxième image (basée sur une matrice), en tant qu'humain, je le ferais en effectuant chacun des calculs distincts montrés dans la première image (scalaire). Pour moi, ce ne sont que deux notations pour la même séquence de calculs. Pourquoi est-ce différent pour mon ordinateur? Pourquoi un ordinateur pourrait-il effectuer le calcul matriciel plus rapidement que le calcul scalaire?

Mark Amery
la source

Réponses:

19

Cela peut sembler évident, mais les ordinateurs n'exécutent pas de formules , ils exécutent du code , et la durée de cette exécution dépend directement du code qu'ils exécutent et seulement indirectement du concept que le code implémente. Deux morceaux de code logiquement identiques peuvent avoir des caractéristiques de performances très différentes. Certaines raisons susceptibles de se produire spécifiquement dans la multiplication matricielle:

  • Utilisation de plusieurs threads. Il n'y a presque pas de CPU moderne qui ne possède pas plusieurs cœurs, beaucoup en ont jusqu'à 8, et les machines spécialisées pour le calcul haute performance peuvent facilement en avoir 64 sur plusieurs sockets. L'écriture de code de manière évidente, dans un langage de programmation normal, n'en utilise qu'un seul . En d'autres termes, il peut utiliser moins de 2% des ressources informatiques disponibles de la machine sur laquelle il fonctionne.
  • Utilisation d'instructions SIMD (de manière confuse, cela est aussi appelé "vectorisation" mais dans un sens différent de celui des citations de texte dans la question). Essentiellement, au lieu de 4 ou 8 instructions arithmétiques scalaires, donnez au CPU une instruction qui exécute l'arithmétique sur 4 ou 8 registres en parallèle. Cela peut littéralement faire des calculs (lorsqu'ils sont parfaitement indépendants et adaptés au jeu d'instructions) 4 ou 8 fois plus rapidement.
  • Faire un usage plus intelligent du cache . Les accès en mémoire sont plus rapides s'ils sont cohérents dans le temps et dans l'espace , c'est-à-dire que les accès consécutifs se font vers des adresses proches et lorsque vous accédez deux fois à une adresse, vous y accédez deux fois de suite rapidement plutôt qu'avec une longue pause.
  • Utilisation d'accélérateurs tels que les GPU. Ces appareils sont des bêtes très différentes des processeurs et leur programmation efficace est une forme d'art à part entière. Par exemple, ils ont des centaines de cœurs, qui sont regroupés en groupes de quelques dizaines de cœurs, et ces groupes partagent des ressources - ils partagent quelques Kio de mémoire beaucoup plus rapide que la mémoire normale, et lorsqu'un cœur du groupe exécute un ifdéclaration tous les autres membres de ce groupe doivent l'attendre.
  • Répartissez le travail sur plusieurs machines (très important dans les superordinateurs!), Ce qui introduit un énorme ensemble de nouveaux maux de tête mais peut, bien sûr, donner accès à des ressources informatiques beaucoup plus importantes.
  • Des algorithmes plus intelligents. Pour la multiplication matricielle, l'algorithme simple O (n ^ 3), correctement optimisé avec les astuces ci-dessus, est souvent plus rapide que les sous-cubes pour des tailles de matrice raisonnables, mais parfois ils gagnent. Pour des cas particuliers tels que des matrices clairsemées, vous pouvez écrire des algorithmes spécialisés.

Beaucoup de gens intelligents ont écrit du code très efficace pour les opérations d'algèbre linéaire courantes , en utilisant les astuces ci-dessus et bien d'autres et généralement même avec des astuces stupides spécifiques à la plate-forme. Par conséquent, transformer votre formule en une multiplication matricielle puis implémenter ce calcul en appelant dans une bibliothèque d'algèbre linéaire mature bénéficie de cet effort d'optimisation. En revanche, si vous écrivez simplement la formule de manière évidente dans un langage de haut niveau, le code machine qui sera finalement généré n'utilisera pas toutes ces astuces et ne sera pas aussi rapide. Cela est également vrai si vous prenez la formulation matricielle et l'implémentez en appelant une routine de multiplication matricielle naïve que vous avez écrite vous-même (encore une fois, de manière évidente).

Faire du code rapidement demande du travail , et souvent beaucoup de travail si vous voulez cette dernière once de performance. Étant donné que de nombreux calculs importants peuvent être exprimés sous la forme d'une combinaison de deux opérations d'algèbre linéaire, il est économique de créer un code hautement optimisé pour ces opérations. Mais votre cas d'utilisation spécialisé unique? Personne ne se soucie de cela, sauf vous, donc l'optimisation de tout cela n'est pas économique.

Communauté
la source
4

(clairsemé) La multiplication matrice-vecteur est hautement parallélisable. Ce qui est très pratique si vos données sont volumineuses et que vous avez une batterie de serveurs à votre disposition.

Cela signifie que vous pouvez diviser la matrice et le vecteur en morceaux et laisser des machines séparées effectuer une partie du travail. Partagez ensuite certains de leurs résultats les uns avec les autres, puis obtenez le résultat final.

Dans votre exemple, les opérations seraient les suivantes

  1. configurer une grille de processeurs détenant chacun un Wx, y en fonction de leurs coordonnées dans la grille

  2. diffuser le vecteur source le long de chaque colonne (coût O(log height))

  3. avoir chaque processeur à la multiplication localement (coût O(width of submatrix * heightof submatrix))

  4. réduire le résultat le long de chaque ligne en utilisant une somme (coût O(log width))

Cette dernière opération est valide car la somme est associative.

Cela permet également de créer une redondance et vous évite d'avoir à mettre toutes les informations sur une seule machine.

Pour les petites matrices 4x4 comme vous le voyez dans les graphiques, c'est parce que le processeur a des instructions et des registres spéciaux pour gérer ces opérations.

monstre à cliquet
la source
-1

La chose la plus instructive serait de comparer les performances de votre code avec les performances de la multiplication matricielle implémentée.

Il y a toujours une optimisation de niveau inférieur à laquelle vous n'avez pas pensé, vous pouvez trouver ici un exemple:

https://simulationcorner.net/index.php?page=fastmatrixvector

ThePunisher
la source