[PYTHON] Multiplication matricielle sur GPU Raspberry Pi (partie 2)

Cet article devait être publié sur le Calendrier de l'Avent Raspberry Pi 2015 il y a une semaine. Il s'agit d'une continuation de la multiplication de matrice avec le GPU de Raspberry Pi (1). Je suis vraiment désolé d'avoir dépassé le calendrier.

La dernière fois, j'ai fait une partie pour calculer la matrice 16x64 dans la figure ci-dessous en parallèle avec SIMD, donc je vais faire d'autres parties pour compléter le code de multiplication matricielle.

# Implémentation de i-loop et j-loop

i-loop et j-loop sont exécutés très peu de fois par rapport à k-loop, c'est donc facile à faire.

À propos de j-loop

for(j = 0; j < r; j+=64) {
   body
}

Est traduit en un style d'assemblage

j = 0

j_loop_begin:

if(j >= r) goto j_loop_exit

body

j += 64
goto j_loop_begin

j_loop_exit:

Cependant, il y a un saut inutile en l'état. Le saut prend du temps et si l'entrée du cache d'instructions est expulsée, les performances des autres parties seront affectées. Vous pouvez le réécrire pour qu'il n'y ait qu'un seul saut dans la boucle comme indiqué ci-dessous.

j = 0
if(j >= r) goto j_loop_exit

j_loop_begin:

body

j += 64
if(j < r) goto j_loop_begin
j_loop_exit:

Le tout premier j> = r échoue toujours et peut être omis.

j = 0

j_loop_begin:

body

j += 64
if(j < r) goto j_loop_begin

j_loop_exit:

En d'autres termes, ce sera sous la forme d'une boucle de type do-while comme indiqué ci-dessous.

j = 0
do {
    body

    j += 64
} while (j < r);

Aussi, que la forme ci-dessus

j = r
do {
    body

    j -= 64
} while(j > 0);

Je pense que la forme est meilleure. Le fait est que ce dernier réduit le nombre d'accès à «r». (En prime, le nombre d'opérations est réduit de un. Le premier était deux fois «j + 64» et «j --r», mais le second est une fois «j --64».)

j = 0
do {
    body

    j += 64
} while (j < r);  <-Lisez r ici à chaque fois


j = r <-Une seule fois ici
do {
    body

    j -= 64
} while(j > 0);

Les variables à faible fréquence d'accès ont peu d'effet même si elles sont placées dans un endroit lent, elles peuvent donc être sauvegardées en mémoire ou par la méthode introduite la dernière fois. Ensuite, les registres et accumulateurs enregistrés peuvent être utilisés efficacement dans le corps de la boucle, ce qui conduit indirectement à une amélioration des performances. Écrivez i-loop de la même manière.

Revêtement de tuyau logiciel

J'ai pensé à faire le revêtement du pipeline logiciel que j'ai mentionné la dernière fois, mais je ne l'ai pas fait parce que j'ai épuisé le contrôle exclusif autour duquel il sera décrit plus tard.

Je vais expliquer ce que j'ai essayé de faire. Si vous écrivez ce programme en pseudo-code, ce sera comme suit. Comme je l'ai expliqué la dernière fois, j'ajoute la matrice 16x16 en 4 étapes, mais il est temps de ne faire aucun calcul au point où le 4ème bloc est transféré vers l'hôte.

for (j) {

Calcul d'adresse et initialisation du cache

    k-La partie qui dépasse de la boucle
    for (k) {
Calculer le produit des vecteurs de A et B
    }
Lors du chargement du premier bloc, k-Calculez la quantité qui dépasse derrière la boucle
Chargez le deuxième bloc et calculez le premier bloc
Calculez le deuxième bloc tout en stockant le premier bloc et en chargeant le troisième bloc
Calculez le 3e bloc en stockant le 2e bloc et en chargeant le 4e bloc
Calculez le 4ème bloc en stockant le 3ème bloc
Stocker le 4ème bloc<-Je n'ai rien calculé!

Branche conditionnelle(Revenir au début ou sortir de la boucle)
}

Afin de combler ce temps d'attente (latence), l'opération consistant à introduire les instructions de l'itération suivante est appelée pipeline logiciel. Dans certains cas, vous pouvez l'apporter des itérations suivantes et suivantes. VideoCore IV n'effectue pas (probablement) une exécution dans le désordre d'instructions telles que des GPU coûteux, donc je pense que ce genre d'effort est important.

path17806.png

Bien sûr, la latence peut également être masquée en la rendant multithread. C'est une distorsion dans le GPU de NVIDIA. VideoCore IV QPU peut exécuter 2 threads en même temps.

Référence de filetage unique

Maintenant que je peux courir avec un QPU, j'ai pris un benchmark. J'ai utilisé le Raspberry Pi Zero que j'ai acheté l'autre jour. Le processeur et le GPU installés sont les mêmes que Raspberry Pi 1, et seule l'horloge du processeur est améliorée à 1Ghz.

Le code source utilisé est ci-dessous.

https://github.com/nineties/py-videocore/blob/master/examples/sgemm_1thread.py

Voici les résultats. Ici, le type de A est 96x363 et le type de B est 363x3072 selon le pi-gemm.

la mise en oeuvre Le nombre de fils Temps d'exécution Performance mesurée
numpy(BLAS) Processeur 1 fil 3.05 secondes 0.070 Gflops
pi-gemm Fils QPU 12 0.21 secondes 1.02 Gflops
ma Filetage QPU 1 0.23 secondes 0.95 Gflops

J'ai pu le rendre assez rapide pour rattraper pi-gemm avec un seul QPU. Cependant, comme la performance théorique d'un QPU est de 2Gflops, il est dommage que la performance ne soit que d'environ 50% de cela. Après diverses mesures, Uniforms Cache a été plus lent que prévu et il a fallu environ 2 instructions pour charger à 1 QPU. Ensuite, la longueur du corps de la boucle k est à peu près doublée, ce qui est d'environ 50%. L'augmentation du nombre de QPU augmentera les échecs de cache, réduisant encore plus l'efficacité. Je m'y attendais dans une certaine mesure car une charge ne fait que 4 octets et la distance est courte avec SoC.

    L.k_loop

    fadd(ra1,  ra1,  r0).fmul(r0, r4, uniform)  #Ces gars ont environ 7 ans~Comme environ 8 horloges(2 instructions)
    fadd(rb1,  rb1,  r0).fmul(r0, r4, uniform)

    ...

    fadd(rb31, rb31, r0, sig='load tmu0').mov(uniforms_address, r2)
    iadd(r2, r2, r3).mov(tmu0_s, r1)
    jzc(L.k_loop)
    iadd(r1, r1, 4).fmul(r0, r4, uniform)      # delay slot #Ce mec ressemble à 30 horloges
    fadd(ra0,  ra0,  r0).fmul(r0, r4, uniform) # delay slot
    fadd(rb0,  rb0,  r0).fmul(r0, r4, uniform) # delay slot

Je me demande comment améliorer cela.

Le TMU a un cache L1 et semble être plus rapide que le cache d'uniformes, mais il consomme de l'ALU pour le calcul d'adresse, il n'est donc pas adapté à la lecture de différents vecteurs les uns après les autres. Il semble qu'il existe un moyen de lire d'abord deux vecteurs avec TMU et de calculer le produit direct tout en répétant la rotation et la diffusion pour un. Cela consomme également de l'ALU avec la rotation, mais comme le nombre de fois où atteindre le cache est réduit, les performances lors de l'augmentation du QPU peuvent être meilleures.

En fait, ce n'était pas si lent entre QPU et VPM, et quand il n'y avait qu'un seul QPU, les deux lectures / écritures pouvaient se faire sans blocage. Cependant, VPM est partagé par tous les QPU, donc augmenter les QPU finira par ralentir. De plus, si vous lisez les matrices A et B avec VPM, il y a aussi le problème que le nombre de DMA augmentera considérablement.

Pour le moment, je pense que la première étape consiste à mesurer les performances de chaque cache et à examiner la structure interne. Ce qui précède est une tâche future et nous continuerons.

[Ajout] Je pensais que Uniforms Cache ne pouvait pas être prérécupéré par logiciel en raison du mécanisme, mais comme le cache L2 est partagé avec TMU, il semble que les Uniforms peuvent être extraits à l'avance jusqu'à L2 en utilisant TMU.

VideoCoreArch.png

Extrait du Guide de référence sur l'architecture 3D de VideoCore IV

Parallélisation entre QPU

VideoCore dispose de 12 QPU. Cette fois, chaque QPU exécutera un thread, pour un total de 12 threads. Par conséquent, comme le montre la figure ci-dessous, la matrice $ A $ est divisée en plusieurs parties horizontalement, et $ B $ est divisé en plusieurs parties verticalement, et le produit de celles-ci est attribué à chaque QPU.

Contrôle synchrone / contrôle exclusif

Comme un programme multithread standard, il nécessite un contrôle exclusif sur l'accès aux ressources partagées. Pour cette raison, VideoCore IV a un mutex et 16 sémaphos 4 bits.

J'écrirai le contrôle de synchronisation en utilisant ces derniers, mais cette fois j'ai omis de joindre la partie qui a besoin de synchronisation avec un mutex entier. Bien sûr, cela devrait avoir un impact sur les performances ...

Principalement les ressources partagées par QPU

Etc., mais

Seules deux configurations de lecture VPM peuvent être émises à la fois et seront ignorées lorsque la file d'attente est pleine. (Guide de référence P56)

Et

Le chargement dans DMA ne peut pas être émis tant que le précédent n'est pas terminé. La même chose s'applique au magasin. (Identique à P56)

Il y a des restrictions comme. Aucun d'eux n'est du genre "caler jusqu'à la fin du précédent", et la requête est ignorée ou Raspi lui-même s'arrête. En outre, il semble y avoir des restrictions non mentionnées dans le guide de référence (en particulier concernant le registre de configuration de foulée supplémentaire). C'était une vraie pénitence parce que le bug de mon propre assembleur chevauchait cela.

Semafo est utilisé pour gérer des situations où le nombre de ressources est limité, comme le premier "seulement jusqu'à deux". Conservez l'un de tous les threads en tant que thread principal, et d'abord il élève le semapho de 2. Les threads qui veulent lire VPM réduiront ce sémapho de un et le relèveront de un lorsqu'ils auront fini de l'utiliser. Les threads qui tentent de le réduire en dessous de 0 seront dans un état d'attente, vous pouvez donc en utiliser jusqu'à deux en même temps. Le verrouillage peut également être effectué avec un sémapho.

Je n'ai pas utilisé le semapho dans cette partie car je l'ai entouré de mutex cette fois, mais je l'utilise à la place pour synchroniser la fin du fil. Le thread maître doit attendre que tous les threads terminent leurs calculs avant d'émettre une interruption vers l'hôte. S'il y a 12 threads, la procédure de synchronisation est la suivante.

  1. Chaque thread élève le sémapho de 1 lorsque le calcul dont il est chargé est terminé.
  2. Une fois que le thread maître a abaissé le sémapho 12 fois, il émet une interruption vers l'hôte et se termine.

C'est très simple. Vous trouverez ci-dessous le code de cette partie.

    sema_up(COMPLETED)  # Notify completion to the thread 0

    ...

    mov(null, uniform, set_flags=True)  # thread index

    jzc(L.skip_fin)
    nop(); nop(); nop()

    # Only thread 0 enters here.
    for i in range(n_threads):
        sema_down(COMPLETED)    # Wait completion of all threads.

    interrupt()

    L.skip_fin

    exit(interrupt=False)

En fait, je pensais faire le traitement de quatre blocs dans un pipeline comme le montre la figure ci-dessous. Je peux l'essayer quand j'en ai le temps.

path21629.png

référence

Le code est ci-dessous.

https://github.com/nineties/py-videocore/blob/master/examples/sgemm.py

Comme précédemment, la taille de la matrice est de 96x363 pour A et de 363x3072 pour B. Avec 12 threads, A a été divisé en deux et B a été divisé en six, ce qui était le plus rapide. J'imagine que c'est là que le cache pour A (TMU) et le cache pour B (Uniforms) sont bien équilibrés. Je ferai bientôt des recherches détaillées.

la mise en oeuvre Le nombre de fils Nombre de divisions de A Nombre de divisions de B Temps d'exécution Performance mesurée
numpy(BLAS) Filetage CPU1 - - 3.05 secondes 0.070 Gflops
pi-gemm Fils QPU 12 - - 0.21 secondes 1.02 Gflops
ma Filetage QPU 1 1 1 0.23 secondes 0.95 Gflops
ma Fils QPU 12 2 6 0.026 secondes 8.32 Gflops

Résumé

Le calcul (avec une double précision) avec numpy + BLAS sur mon ordinateur portable (core i7-5600U 2,6 GHz) est à peu près la même vitesse. En d'autres termes, même si vous essayez d'utiliser le GPU de Raspberry Pi, il est malheureusement plus rapide de calculer avec un ordinateur normal. Pi-Zero est de 600 yens, il peut donc être rentable s'il s'agit d'un rapport qualité / prix.

IMG_1610.JPG

Recommended Posts

Multiplication matricielle sur GPU Raspberry Pi (partie 2)
Raspberry Pi "Lampe de notification Honwaka" Partie 1
Raspberry Pi "Lampe de notification Honwaka" Partie 3
pigpio sur Raspberry pi
Cython sur Raspberry Pi
Introduction de pyenv sur Raspberry Pi
Utilisez NeoPixel avec la tarte aux framboises
Installez OpenCV4 sur Raspberry Pi 3
Installez TensorFlow 1.15.0 sur Raspberry Pi
Test de la communication UART avec Raspberry Pi
Raspberry pi 1 modèle b, partie rouge noeud 17
MQTT sur Raspberry Pi et Mac
raspberry pi 4 centos7 installer sur docker
Installez ghoto2 sur Raspberry Pi (Remarque)
Essayez d'utiliser ArUco avec Raspberry Pi
Procédure d'installation d'OpenCV sur Raspberry Pi
Allumer / éteindre le Raspberry pi avec Arduino
Détecter l'état du commutateur avec Raspberry Pi 3
Installez OpenMedia Vault 5 sur Raspberry Pi 4
L Chika avec Raspberry Pi C #
Construisez wxPython sur Ubuntu 20.04 sur Raspberry Pi 4
Produit matriciel
Produit matriciel en python numpy
[Python] Temps de traitement de la multiplication de la matrice avec NumPy
Multiplication matricielle sur GPU Raspberry Pi (partie 2)
À propos de Confusion Matrix
[Python] Opération de matrice
Démarrage USB sur Raspberry Pi 4 modèle B
Activer la communication série UART + avec Raspberry Pi
Adafruit Python BluefruitLE fonctionne sur Raspeye.
Accélérez l'apprentissage en profondeur avec le processeur Rasperry Pi 4
Définir l'espace d'échange sur Ubuntu sur Raspberry Pi
Programmation normale avec la programmation Node-RED avec Raspberry Pi 3
Installez la version 64 bits du système d'exploitation (bate) sur Raspberry Pi
Installez docker-compose sur le système d'exploitation Raspberry Pi 64 bits
Exécutez un servomoteur en utilisant python sur Raspberry Pi 3
Travailler avec des capteurs dans Mathematica sur Raspberry Pi
Construire un environnement OpenCV-Python sur Raspberry Pi B +
Détectez la température à l'aide de python sur Raspberry Pi 3!
Comment installer NumPy sur Raspeye
Travailler avec le GPS en Python pour Raspberry Pi 3
Débutant du développement d'applications GUI Raspberry Pi facile, partie 1
Pourquoi detectMultiScale () est lent sur Raspberry Pi B +
Détectez les interrupteurs à glissière à l'aide de python sur Raspberry Pi 3!
Construire un environnement Django sur Raspai (MySQL)
Essayez d'utiliser le code QR avec Raspberry Pi
Détectez les commutateurs magnétiques à l'aide de python sur Raspberry Pi 3!
Profitez du travail électronique avec GPIO de Raspberry Pi
Allumez / éteignez votre PC avec Raspberry Pi
Débutant du développement d'applications GUI Raspberry Pi facile, partie 2
Grove - Capteur de température et d'humidité (DHT11) avec Raspberry Pi
Rendre DHT11 disponible avec Raspeye + python (Remarque)
Démarrage de la compilation croisée pour Raspberry Pi Zero sur Ubuntu
Sonnez le buzzer en utilisant python sur Raspberry Pi 3!
Afficher la température du processeur toutes les 5 secondes sur Raspberry Pi 4
Introduction de Ceph avec Kubernetes sur Raspberry Pi 4B (ARM64)
Connectez-vous à MySQL avec Python sur Raspberry Pi
Construire un environnement de développement Python sur Raspberry Pi
Créer un environnement Arch Linux sur Raspai
Enregistrez la température et l'humidité avec systemd sur Raspberry Pi
Créer un environnement OpenCV4 sur Raspberry Pi à l'aide de Poetry
Exécutez la matrice LED de manière interactive avec Raspberry Pi 3B + sur Slackbot
Visualisons la pièce avec tarte aux râpes, partie 1