web-dev-qa-db-fra.com

Pourquoi l'einsum de numpy est-il plus rapide que les fonctions intégrées de numpy?

Commençons par trois tableaux de dtype=np.double. Les temporisations sont effectuées sur un processeur Intel à l'aide de numpy 1.7.1 compilé avec icc et lié au mkl d'Intel. Un processeur AMD avec numpy 1.6.1 compilé avec gcc sans mkl a également été utilisé pour vérifier les timings. Veuillez noter que les timings évoluent de manière presque linéaire avec la taille du système et ne sont pas dus au petit surcoût encouru dans les fonctions numpy if, cette différence apparaîtra en microsecondes et non en millisecondes:

arr_1D=np.arange(500,dtype=np.double)
large_arr_1D=np.arange(100000,dtype=np.double)
arr_2D=np.arange(500**2,dtype=np.double).reshape(500,500)
arr_3D=np.arange(500**3,dtype=np.double).reshape(500,500,500)

Voyons d'abord le np.sum fonction:

np.all(np.sum(arr_3D)==np.einsum('ijk->',arr_3D))
True

%timeit np.sum(arr_3D)
10 loops, best of 3: 142 ms per loop

%timeit np.einsum('ijk->', arr_3D)
10 loops, best of 3: 70.2 ms per loop

Pouvoirs:

np.allclose(arr_3D*arr_3D*arr_3D,np.einsum('ijk,ijk,ijk->ijk',arr_3D,arr_3D,arr_3D))
True

%timeit arr_3D*arr_3D*arr_3D
1 loops, best of 3: 1.32 s per loop

%timeit np.einsum('ijk,ijk,ijk->ijk', arr_3D, arr_3D, arr_3D)
1 loops, best of 3: 694 ms per loop

Produit extérieur:

np.all(np.outer(arr_1D,arr_1D)==np.einsum('i,k->ik',arr_1D,arr_1D))
True

%timeit np.outer(arr_1D, arr_1D)
1000 loops, best of 3: 411 us per loop

%timeit np.einsum('i,k->ik', arr_1D, arr_1D)
1000 loops, best of 3: 245 us per loop

Tous ces éléments sont deux fois plus rapides avec np.einsum. Il devrait s'agir de comparaisons entre pommes et pommes, car tout est spécifiquement de dtype=np.double. Je m'attendrais à une accélération dans une opération comme celle-ci:

np.allclose(np.sum(arr_2D*arr_3D),np.einsum('ij,oij->',arr_2D,arr_3D))
True

%timeit np.sum(arr_2D*arr_3D)
1 loops, best of 3: 813 ms per loop

%timeit np.einsum('ij,oij->', arr_2D, arr_3D)
10 loops, best of 3: 85.1 ms per loop

Einsum semble être au moins deux fois plus rapide pour np.inner, np.outer, np.kron, et np.sum quelle que soit la sélection de axes. La principale exception étant np.dot car il appelle DGEMM à partir d'une bibliothèque BLAS. Alors pourquoi np.einsum plus rapide que d'autres fonctions numpy équivalentes?

Le cas de l'exhaustivité de la DGEMM:

np.allclose(np.dot(arr_2D,arr_2D),np.einsum('ij,jk',arr_2D,arr_2D))
True

%timeit np.einsum('ij,jk',arr_2D,arr_2D)
10 loops, best of 3: 56.1 ms per loop

%timeit np.dot(arr_2D,arr_2D)
100 loops, best of 3: 5.17 ms per loop

La théorie principale est du commentaire de @sebergs que np.einsum peut utiliser SSE2 , mais les ufuncs de numpy ne le seront que jusqu'à numpy 1.8 (voir journal des modifications ). Je pense que c'est la bonne réponse, mais pas pu le confirmer. Une preuve limitée peut être trouvée en changeant le type de tableau d'entrée et en observant la différence de vitesse et le fait que tout le monde n'observe pas les mêmes tendances dans les synchronisations.

70
Daniel

Maintenant que numpy 1.8 est sorti, où, selon la documentation, tous les ufunc devraient utiliser SSE2, je voulais vérifier que le commentaire de Seberg sur SSE2 était valide.

Pour effectuer le test, une nouvelle installation python 2.7 a été créée - numpy 1.7 et 1.8 ont été compilées avec icc en utilisant des options standard sur un noyau opteron AMD exécutant Ubuntu.

Il s'agit du test effectué avant et après la mise à niveau 1.8:

import numpy as np
import timeit

arr_1D=np.arange(5000,dtype=np.double)
arr_2D=np.arange(500**2,dtype=np.double).reshape(500,500)
arr_3D=np.arange(500**3,dtype=np.double).reshape(500,500,500)

print 'Summation test:'
print timeit.timeit('np.sum(arr_3D)',
                      'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D',
                      number=5)/5
print timeit.timeit('np.einsum("ijk->", arr_3D)',
                      'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D',
                      number=5)/5
print '----------------------\n'


print 'Power test:'
print timeit.timeit('arr_3D*arr_3D*arr_3D',
                      'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D',
                      number=5)/5
print timeit.timeit('np.einsum("ijk,ijk,ijk->ijk", arr_3D, arr_3D, arr_3D)',
                      'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D',
                      number=5)/5
print '----------------------\n'


print 'Outer test:'
print timeit.timeit('np.outer(arr_1D, arr_1D)',
                      'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D',
                      number=5)/5
print timeit.timeit('np.einsum("i,k->ik", arr_1D, arr_1D)',
                      'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D',
                      number=5)/5
print '----------------------\n'


print 'Einsum test:'
print timeit.timeit('np.sum(arr_2D*arr_3D)',
                      'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D',
                      number=5)/5
print timeit.timeit('np.einsum("ij,oij->", arr_2D, arr_3D)',
                      'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D',
                      number=5)/5
print '----------------------\n'

Numpy 1.7.1:

Summation test:
0.172988510132
0.0934836149216
----------------------

Power test:
1.93524689674
0.839519000053
----------------------

Outer test:
0.130380821228
0.121401786804
----------------------

Einsum test:
0.979052495956
0.126066613197

Numpy 1.8:

Summation test:
0.116551589966
0.0920487880707
----------------------

Power test:
1.23683619499
0.815982818604
----------------------

Outer test:
0.131808176041
0.127472200394
----------------------

Einsum test:
0.781750011444
0.129271841049

Je pense que cela est assez concluant que SSE joue un grand rôle dans les différences de synchronisation, il convient de noter que la répétition de ces tests les synchronisations très par seulement ~ 0,003 s. La différence restante doit être couverte dans les autres réponses à cette question.

21
Daniel

Tout d'abord, il y a eu beaucoup de discussions passées à ce sujet sur la liste numpy. Par exemple, voir: http://numpy-discussion.10968.n7.nabble.com/poor-performance-of-sum-with-sub-machine-Word-integer-types-td41.html = http://numpy-discussion.10968.n7.nabble.com/odd-performance-of-sum-td3332.html

Certains se résument au fait que einsum est nouveau, et essaie probablement d'être meilleur sur l'alignement du cache et d'autres problèmes d'accès à la mémoire, tandis que la plupart des anciennes fonctions numpy se concentrent sur une implémentation facilement portable sur une interface fortement optimisée une. Je ne fais que spéculer, cependant.


Cependant, une partie de ce que vous faites n'est pas tout à fait une comparaison "pommes à pommes".

En plus de ce que @Jamie a déjà dit, sum utilise un accumulateur plus approprié pour les tableaux

Par exemple, sum fait plus attention à vérifier le type de l'entrée et à utiliser un accumulateur approprié. Par exemple, tenez compte des éléments suivants:

In [1]: x = 255 * np.ones(100, dtype=np.uint8)

In [2]: x
Out[2]:
array([255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
       255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
       255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
       255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
       255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
       255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
       255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
       255, 255, 255, 255, 255, 255, 255, 255, 255], dtype=uint8)

Notez que sum est correct:

In [3]: x.sum()
Out[3]: 25500

Alors que einsum donnera le mauvais résultat:

In [4]: np.einsum('i->', x)
Out[4]: 156

Mais si nous utilisons un dtype moins limité, nous obtiendrons toujours le résultat que vous attendez:

In [5]: y = 255 * np.ones(100)

In [6]: np.einsum('i->', y)
Out[6]: 25500.0
30
Joe Kington

Je pense que ces horaires expliquent ce qui se passe:

a = np.arange(1000, dtype=np.double)
%timeit np.einsum('i->', a)
100000 loops, best of 3: 3.32 us per loop
%timeit np.sum(a)
100000 loops, best of 3: 6.84 us per loop

a = np.arange(10000, dtype=np.double)
%timeit np.einsum('i->', a)
100000 loops, best of 3: 12.6 us per loop
%timeit np.sum(a)
100000 loops, best of 3: 16.5 us per loop

a = np.arange(100000, dtype=np.double)
%timeit np.einsum('i->', a)
10000 loops, best of 3: 103 us per loop
%timeit np.sum(a)
10000 loops, best of 3: 109 us per loop

Donc, vous avez essentiellement une surcharge de 3us presque constante lorsque vous appelez np.sum plus de np.einsum, ils fonctionnent donc aussi vite, mais il faut un peu plus de temps pour démarrer. Pourquoi est-ce possible? Mon argent est sur les éléments suivants:

a = np.arange(1000, dtype=object)
%timeit np.einsum('i->', a)
Traceback (most recent call last):
...
TypeError: invalid data type for einsum
%timeit np.sum(a)
10000 loops, best of 3: 20.3 us per loop

Je ne sais pas exactement ce qui se passe, mais il semble que np.einsum ignore certaines vérifications pour extraire des fonctions spécifiques au type pour effectuer les multiplications et les ajouts, et va directement avec * et + pour les types C standard uniquement.


Les cas multidimensionnels ne sont pas différents:

n = 10; a = np.arange(n**3, dtype=np.double).reshape(n, n, n)
%timeit np.einsum('ijk->', a)
100000 loops, best of 3: 3.79 us per loop
%timeit np.sum(a)
100000 loops, best of 3: 7.33 us per loop

n = 100; a = np.arange(n**3, dtype=np.double).reshape(n, n, n)
%timeit np.einsum('ijk->', a)
1000 loops, best of 3: 1.2 ms per loop
%timeit np.sum(a)
1000 loops, best of 3: 1.23 ms per loop

Donc, une surcharge généralement constante, pas une course plus rapide une fois qu'ils s'y sont mis.

18
Jaime

Une mise à jour pour numpy 1.16.4: les fonctions natives de Numpy sont plus rapides que les einsums dans presque tous les cas. Seules la variante externe d'einsum et le test sum23 sont plus rapides que les versions non einsum.

Si vous pouvez utiliser les fonctions natives de numpy, faites-le.

(Images créées avec perfplot , un de mes projets.)

enter image description here

enter image description here

enter image description here

enter image description here

enter image description here

enter image description here


Code pour reproduire les parcelles:

import numpy
import perfplot


def setup1(n):
    return numpy.arange(n, dtype=numpy.double)


def setup2(n):
    return numpy.arange(n ** 2, dtype=numpy.double).reshape(n, n)


def setup3(n):
    return numpy.arange(n ** 3, dtype=numpy.double).reshape(n, n, n)

def setup23(n):
    return (
        numpy.arange(n ** 2, dtype=numpy.double).reshape(n, n),
        numpy.arange(n ** 3, dtype=numpy.double).reshape(n, n, n)
    )


def numpy_sum(a):
    return numpy.sum(a)


def einsum_sum(a):
    return numpy.einsum("ijk->", a)


perfplot.save(
    "sum.png",
    setup=setup3,
    kernels=[numpy_sum, einsum_sum],
    n_range=[2 ** k for k in range(10)],
    logx=True,
    logy=True,
    title="sum",
)


def numpy_power(a):
    return a * a * a


def einsum_power(a):
    return numpy.einsum("ijk,ijk,ijk->ijk", a, a, a)


perfplot.save(
    "power.png",
    setup=setup3,
    kernels=[numpy_power, einsum_power],
    n_range=[2 ** k for k in range(9)],
    logx=True,
    logy=True,
)


def numpy_outer(a):
    return numpy.outer(a, a)


def einsum_outer(a):
    return numpy.einsum("i,k->ik", a, a)


perfplot.save(
    "outer.png",
    setup=setup1,
    kernels=[numpy_outer, einsum_outer],
    n_range=[2 ** k for k in range(13)],
    logx=True,
    logy=True,
    title="outer",
)



def dgemm_numpy(a):
    return numpy.dot(a, a)


def dgemm_einsum(a):
    return numpy.einsum("ij,jk", a, a)


def dgemm_einsum_optimize(a):
    return numpy.einsum("ij,jk", a, a, optimize=True)


perfplot.save(
    "dgemm.png",
    setup=setup2,
    kernels=[dgemm_numpy, dgemm_einsum],
    n_range=[2 ** k for k in range(13)],
    logx=True,
    logy=True,
    title="dgemm",
)



def dot_numpy(a):
    return numpy.dot(a, a)


def dot_einsum(a):
    return numpy.einsum("i,i->", a, a)


perfplot.save(
    "dot.png",
    setup=setup1,
    kernels=[dot_numpy, dot_einsum],
    n_range=[2 ** k for k in range(20)],
    logx=True,
    logy=True,
    title="dot",
)

def sum23_numpy(data):
    a, b = data
    return numpy.sum(a * b)


def sum23_einsum(data):
    a, b = data
    return numpy.einsum('ij,oij->', a, b)


perfplot.save(
    "sum23.png",
    setup=setup23,
    kernels=[sum23_numpy, sum23_einsum],
    n_range=[2 ** k for k in range(10)],
    logx=True,
    logy=True,
    title="sum23",
)
0
Nico Schlömer