Dégradation de la performance de la multiplication matricielle des matrices de précision simple et double sur une machine à plusieurs cœurs

UPDATE

malheureusement, par inadvertance, J'avais une version plus ancienne de MKL (11.1) liée à numpy. La nouvelle version de MKL (11.3.1) donne la même performance en C et lorsqu'elle est appelée à partir de python.

ce qui obscurcissait les choses, était même si relier explicitement les bibliothèques partagées compilées avec les nouvelles MKL, et pointer à travers les variables LD_* vers elles, et puis en python faire importation numpy, était en quelque sorte faire appel à python anciennes bibliothèques MKL. Seulement en remplaçant dans le dossier python lib tout libmkl_*.ainsi, avec MKL plus récent, j'ai pu faire correspondre les performances en Python et en C.

Fond d'écran / de la bibliothèque d'informations.

la multiplication matricielle a été effectuée via les appels de bibliothèque sgemm (simple-précision) et dgemm (double-précision) Intel MKL, via numpy.dot de la fonction. L'appel réel des fonctions de la bibliothèque peut être vérifié avec par exemple oprof.

en utilisant ici 2x18 core CPU E5-2699 v3, donc un total de 36 physique core. KMP_AFFINITY=scatter. Fonctionnant sous linux.

TL;DR

1) Pourquoi c'est nul.dot, même s'il appelle les mêmes fonctions de bibliothèque MKL, deux fois plus lent au mieux par rapport au code compilé C?

2) Pourquoi via numpy.dot vous obtenez des performances décroissantes avec le nombre croissant de cœurs, alors que le même effet n'est pas observé dans le code C (appelant les mêmes fonctions de bibliothèque).

Le problème

j'ai observé que faire la multiplication de matrice des flotteurs de précision simple/double dans numpy.dot, ainsi que l'appel cblas_sgemm / dgemm directement à partir D'un C compilé bibliothèque partagée donner des performances bien pires que d'appeler les mêmes fonctions MKL cblas_sgemm/dgemm à partir du code C pur.

import numpy as np
import mkl
n = 10000
A = np.random.randn(n,n).astype('float32')
B = np.random.randn(n,n).astype('float32')
C = np.zeros((n,n)).astype('float32')

mkl.set_num_threads(3); %time np.dot(A, B, out=C)
11.5 seconds
mkl.set_num_threads(6); %time np.dot(A, B, out=C)
6 seconds
mkl.set_num_threads(12); %time np.dot(A, B, out=C)
3 seconds
mkl.set_num_threads(18); %time np.dot(A, B, out=C)
2.4 seconds
mkl.set_num_threads(24); %time np.dot(A, B, out=C)
3.6 seconds
mkl.set_num_threads(30); %time np.dot(A, B, out=C)
5 seconds
mkl.set_num_threads(36); %time np.dot(A, B, out=C)
5.5 seconds

faire exactement la même chose que ci-dessus, mais avec une double précision A, B et C, vous obtenez: 3 cœurs: 20s, 6 cœurs: 10s, 12 cœurs: 5s, 18 cœurs: 4.3 s, 24 cœurs: 3s, 30 cœurs: 2.8 s, 36 cœurs: 2.8 s.

l'augmentation de la vitesse pour les points flottants de précision semble être associée à des erreurs de cache. Pour 28 core run, voici la sortie de perf. Pour une seule précision:

perf stat -e task-clock,cycles,instructions,cache-references,cache-misses ./ptestf.py
631,301,854 cache-misses # 31.478 % of all cache refs

Et double précision:

93,087,703 cache-misses # 5.164 % of all cache refs

C bibliothèque partagée, compilée avec

/opt/intel/bin/icc -o comp_sgemm_mkl.so -openmp -mkl sgem_lib.c -lm -lirc -O3 -fPIC -shared -std=c99 -vec-report1 -xhost -I/opt/intel/composer/mkl/include

#include <stdio.h>
#include <stdlib.h>
#include "mkl.h"

void comp_sgemm_mkl(int m, int n, int k, float *A, float *B, float *C);

void comp_sgemm_mkl(int m, int n, int k, float *A, float *B, float *C)
{
    int i, j;
    float alpha, beta;
    alpha = 1.0; beta = 0.0;

    cblas_sgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans,
                m, n, k, alpha, A, k, B, n, beta, C, n);
}

fonction Python wrapper, appelant la bibliothèque compilée ci-dessus:

def comp_sgemm_mkl(A, B, out=None):
    lib = CDLL(omplib)
    lib.cblas_sgemm_mkl.argtypes = [c_int, c_int, c_int, 
                                 np.ctypeslib.ndpointer(dtype=np.float32, ndim=2), 
                                 np.ctypeslib.ndpointer(dtype=np.float32, ndim=2),
                                 np.ctypeslib.ndpointer(dtype=np.float32, ndim=2)]
    lib.comp_sgemm_mkl.restype = c_void_p
    m = A.shape[0]
    n = B.shape[0]
    k = B.shape[1]
    if np.isfortran(A):
        raise ValueError('Fortran array')
    if m != n:
        raise ValueError('Wrong matrix dimensions')
    if out is None:
        out = np.empty((m,k), np.float32)
    lib.comp_sgemm_mkl(m, n, k, A, B, out)

cependant, les appels explicites d'un appel binaire C compilé par MKL cblas_sgemm / cblas_dgemm, avec des tableaux alloués par malloc en C, donne des performances presque 2 fois meilleures par rapport au code python, c'est-à-dire le numpy.point d'appel. De plus, L'effet de la dégradation de la performance avec l'augmentation du nombre de carottes n'est pas observé. La meilleure performance a été de 900 ms pour la simple précision de la multiplication de matrice et a été atteint en utilisant les 36 cœurs physiques via mkl_set_num_cores et en exécutant le code C avec numactl --interleave=all.

peut-être des outils de fantaisie ou conseils pour l'établissement de profils, l'inspection et la compréhension de cette situation? Toute lecture est très apprécié.

UPDATE Suivant le Conseil de @Hristo Iliev, exécuter numactl -- interleave = all ./ipython n'a pas modifié les timings (dans le bruit), mais améliore les durées d'exécution binaires pures de C.

28
demandé sur Fermion Portal 2016-01-27 18:35:57

1 réponses

je soupçonne que cela est dû à la mauvaise programmation du fil. J'ai pu reproduire un effet similaire au vôtre. Python fonctionnait à ~ 2.2 s, alors que la version C montrait d'énormes variations de 1.4-2.2 S.

Application: KMP_AFFINITY=scatter,granularity=thread Cela garantit que les 28 threads tournent toujours sur le même thread du processeur.

réduit les deux temps d'exécution à ~1.24 s pour C et ~1.26 s pour python.

Ceci est sur une double socket 28 core Xeon E5-2680 v3 système.

fait intéressant, sur un système Haswell 24 core dual socket, python et C exécutent presque identiques même sans affinité de thread / pinning.

Pourquoi python affectent la planification? Je suppose qu'il y a plus d'environnement runtime autour. En fin de compte, sans épingler vos résultats de rendement seront non déterministes.

vous devez également tenir compte du fait que L'exécution Intel OpenMP génère un thread de gestion supplémentaire qui peut embrouille l'ordonnanceur. Il y a plus de choix pour épingler, par exemple KMP_AFFINITY=compact - mais, pour une raison qui est totalement foiré sur mon système. Vous pouvez ajouter ,verbose à la variable pour voir comment l'exécution épingle vos threads.

likwid broches est une alternative utile offrant un contrôle plus commode.

en général, la précision simple devrait être au moins aussi rapide que la double précision. La double précision peut être plus lente parce que:

  • Vous besoin de plus de bande passante mémoire/cache pour une double précision.
  • vous pouvez construire des Alu à haut rendement pour une précision simple, mais cela ne s'applique généralement pas aux CPU mais plutôt aux GPU.

je pense qu'une fois que vous vous débarrasserez de l'anomalie de performance, cela se reflétera dans vos chiffres.

quand vous augmentez le nombre de threads pour MKL / * gemm, considérez

  • mémoire / cache partagé la bande passante peut devenir un goulot d'étranglement, limiter l'évolutivité
  • le mode Turbo diminuera efficacement la fréquence centrale lors de l'augmentation de l'utilisation. Cela s'applique même lorsque vous exécutez à la fréquence nominale: sur les processeurs Haswell-EP, les instructions AVX imposeront une "fréquence de base AVX" plus basse - mais le processeur est autorisé à dépasser cela lorsque moins de cœurs sont utilisés / headroom thermique est disponible et en général encore plus pour un court laps de temps. Si vous voulez des résultats parfaitement neutres, vous devez utiliser la base AVX fréquence, qui est de 1,9 GHz pour vous. Il est documenté ici, et expliqué dans une image.

Je ne pense pas qu'il y ait une façon très simple de mesurer comment votre application est affectée par une mauvaise planification. Vous pouvez exposer ce perf trace -e sched:sched_switch et il y a certains logiciels pour visualiser ceci, mais cela viendra avec une grande courbe d'apprentissage. Et encore une fois - pour l'analyse des performances en parallèle, vous devriez avoir les threads épinglés de toute façon.

7
répondu Zulan 2016-12-23 21:17:40