Programing

numpy의 einsum이 numpy의 내장 함수보다 빠른 이유는 무엇입니까?

lottogame 2020. 11. 9. 07:39
반응형

numpy의 einsum이 numpy의 내장 함수보다 빠른 이유는 무엇입니까?


3 개의 dtype=np.double. 타이밍은 인텔의 .NET Framework로 컴파일 icc되고 연결된 numpy 1.7.1을 사용하여 인텔 CPU에서 수행됩니다 mkl. numpy 1.6.1 gcc없이 컴파일 된 AMD CPU mkl도 타이밍을 확인하는 데 사용되었습니다. 타이밍은 시스템 크기에 따라 거의 선형 적으로 확장되며 numpy 함수 if명령문 에서 발생하는 작은 오버 헤드로 인해 이러한 차이가 밀리 초가 아닌 마이크로 초로 표시됩니다.

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)

먼저 np.sum함수를 살펴 보겠습니다 .

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

힘 :

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

외부 제품 :

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

위의 모든 것은 np.einsum. 모든 것이 구체적이기 때문에 이것들은 사과 대 사과 비교 여야합니다 dtype=np.double. 다음과 같은 작업에서 속도가 향상 될 것으로 예상합니다.

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는 두 배 이상 빠른 속도를위한 것 같다 np.inner, np.outer, np.kron,와 np.sum상관없이 axes보세요. 주요 예외 np.dot는 BLAS 라이브러리에서 DGEMM을 호출하는 것입니다. 그렇다면 np.einsum동등한 다른 numpy 함수보다 더 빠른 이유는 무엇입니까?

완전성을위한 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

주요 이론은 SSE2를np.einsum 사용할 수있는 @sebergs 주석에서 나왔지만 numpy의 ufuncs는 numpy 1.8 ( 변경 로그 참조)까지는 그렇지 않습니다 . 나는 이것이 정답이라고 생각하지만 확인 하지 못했습니다 . 입력 배열의 dtype을 변경하고 속도 차이를 관찰하고 모든 사람이 타이밍에서 동일한 경향을 관찰하지 않는다는 사실을 관찰하면 제한된 증거를 찾을 수 있습니다.


우선, numpy 목록에서 이것에 대해 과거에 많은 논의가있었습니다. 예를 들어 다음을 참조하십시오. 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

일부 einsum는 새로운 사실로 귀결되며 캐시 정렬 및 기타 메모리 액세스 문제에 대해 더 나은 방법을 시도하고있는 반면, 이전의 numpy 기능 중 상당수는 고도로 최적화 된 것보다 쉽게 ​​이식 가능한 구현에 중점을 둡니다. 그래도 추측하고 있습니다.


그러나 당신이하는 일 중 일부는 "사과 간"비교가 아닙니다.

@Jamie가 이미 말한 것 외에도 sum배열에 더 적합한 누산기를 사용합니다.

예를 들어, sum입력 유형을 확인하고 적절한 누산기를 사용하는 데 더주의해야합니다. 예를 들어 다음을 고려하십시오.

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)

(가) 있습니다 sum올바른 :

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

einsum잘못된 결과를 제공하는 동안 :

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

그러나 덜 제한된을 사용하는 경우 dtype에도 예상 한 결과를 얻을 수 있습니다.

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

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

문서에 따르면 모든 ufunc가 SSE2를 사용해야하는 numpy 1.8이 출시되었으므로 SSE2에 대한 Seberg의 의견이 유효한지 다시 확인하고 싶었습니다.

테스트를 수행하기 위해 새로운 python 2.7 설치가 생성되었습니다 icc. Ubuntu를 실행하는 AMD opteron 코어에서 표준 옵션을 사용하여 numpy 1.7 및 1.8이 컴파일되었습니다 .

이것은 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

나는 이것이 SSE가 타이밍 차이에서 큰 역할을한다는 상당히 결정적이라고 생각합니다. 이러한 테스트를 반복하면 타이밍이 ~ 0.003 초에 불과하다는 점에 유의해야합니다. 나머지 차이점은이 질문에 대한 다른 답변에서 다루어야합니다.


이 타이밍이 무슨 일이 일어나고 있는지 설명한다고 생각합니다.

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

따라서 기본적으로 np.sumover를 호출 할 때 거의 일정한 3us 오버 헤드가 np.einsum있으므로 기본적으로 빠르게 실행되지만 진행하는 데 조금 더 오래 걸립니다. 왜 그럴 수 있습니까? 내 돈은 다음과 같습니다.

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

확실하지 무슨 일이 정확히 벌어지고 있지만, 그 보인다 np.einsum곱셈 및 추가 할 추출 유형의 특정 기능에 몇 가지 검사를 생략하고, 직접 것입니다 *+표준 C 타입 만.


다차원 사례는 다르지 않습니다.

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

따라서 대부분의 지속적인 오버 헤드가 발생합니다.


numpy 1.16.4 업데이트 : Numpy의 기본 함수는 거의 모든 경우에 einsums보다 빠릅니다. einsum의 외부 변형과 sum23만이 비 einsum 버전보다 빠르게 테스트됩니다.

numpy의 기본 함수를 사용할 수 있다면 그렇게하십시오.

( 내 프로젝트 인 perfplot으로 만든 이미지 .)

여기에 이미지 설명 입력

여기에 이미지 설명 입력

여기에 이미지 설명 입력

여기에 이미지 설명 입력

여기에 이미지 설명 입력

여기에 이미지 설명 입력


플롯을 재현하는 코드 :

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",
)

참고 URL : https://stackoverflow.com/questions/18365073/why-is-numpys-einsum-faster-than-numpys-built-in-functions

반응형