python - numba guvectorize target=''parallel'' más lento que target=''cpu''
parallel-processing numexpr (1)
He estado tratando de optimizar una pieza de código python que implica grandes cálculos de matriz multidimensional. Estoy obteniendo resultados contraintuitivos con numba. Me estoy ejecutando en un MBP, mediados de 2015, 2.5 GHz i7 quadcore, OS 10.10.5, python 2.7.11. Considera lo siguiente:
import numpy as np
from numba import jit, vectorize, guvectorize
import numexpr as ne
import timeit
def add_two_2ds_naive(A,B,res):
for i in range(A.shape[0]):
for j in range(B.shape[1]):
res[i,j] = A[i,j]+B[i,j]
@jit
def add_two_2ds_jit(A,B,res):
for i in range(A.shape[0]):
for j in range(B.shape[1]):
res[i,j] = A[i,j]+B[i,j]
@guvectorize([''float64[:,:],float64[:,:],float64[:,:]''],
''(n,m),(n,m)->(n,m)'',target=''cpu'')
def add_two_2ds_cpu(A,B,res):
for i in range(A.shape[0]):
for j in range(B.shape[1]):
res[i,j] = A[i,j]+B[i,j]
@guvectorize([''(float64[:,:],float64[:,:],float64[:,:])''],
''(n,m),(n,m)->(n,m)'',target=''parallel'')
def add_two_2ds_parallel(A,B,res):
for i in range(A.shape[0]):
for j in range(B.shape[1]):
res[i,j] = A[i,j]+B[i,j]
def add_two_2ds_numexpr(A,B,res):
res = ne.evaluate(''A+B'')
if __name__=="__main__":
np.random.seed(69)
A = np.random.rand(10000,100)
B = np.random.rand(10000,100)
res = np.zeros((10000,100))
Ahora puedo ejecutar timeit en las diversas funciones:
%timeit add_two_2ds_jit(A,B,res)
1000 loops, best of 3: 1.16 ms per loop
%timeit add_two_2ds_cpu(A,B,res)
1000 loops, best of 3: 1.19 ms per loop
%timeit add_two_2ds_parallel(A,B,res)
100 loops, best of 3: 6.9 ms per loop
%timeit add_two_2ds_numexpr(A,B,res)
1000 loops, best of 3: 1.62 ms per loop
Parece que ''paralelo'' no está tomando incluso la mayoría de un solo núcleo, ya que su uso en la top
muestra que python está alcanzando ~ 40% de cpu para ''paralelo'', ~ 100% para ''cpu'' y hits de numexpr ~ 300 %
Hay dos problemas con las implementaciones de @guvectorize. La primera es que estás haciendo todos los bucles dentro de tu núcleo @guvectorize, por lo que no hay nada para que el objetivo paralelo de Numba se paralelice. Tanto @vectorize como @guvectorize se paralelizan en las dimensiones de difusión en ufunc / gufunc. Como la firma de su gufunc es 2D, y sus entradas son 2D, solo hay una sola llamada a la función interna, lo que explica el único uso de CPU al 100% que ha visto.
La mejor forma de escribir la función que tienes arriba es usar un ufunc regular:
@vectorize(''(float64, float64)'', target=''parallel'')
def add_ufunc(a, b):
return a + b
Luego en mi sistema, veo estas velocidades:
%timeit add_two_2ds_jit(A,B,res)
1000 loops, best of 3: 1.87 ms per loop
%timeit add_two_2ds_cpu(A,B,res)
1000 loops, best of 3: 1.81 ms per loop
%timeit add_two_2ds_parallel(A,B,res)
The slowest run took 11.82 times longer than the fastest. This could mean that an intermediate result is being cached
100 loops, best of 3: 2.43 ms per loop
%timeit add_two_2ds_numexpr(A,B,res)
100 loops, best of 3: 2.79 ms per loop
%timeit add_ufunc(A, B, res)
The slowest run took 9.24 times longer than the fastest. This could mean that an intermediate result is being cached
1000 loops, best of 3: 2.03 ms per loop
(Este es un sistema OS X muy similar al suyo, pero con OS X 10.11).
Aunque ufunc paralelo de Numba ahora supera a numexpr (y veo add_ufunc
usando aproximadamente 280% de CPU), no supera al caso simple de CPU de subproceso único. Sospecho que el cuello de botella se debe al ancho de banda de la memoria (o caché), pero no he hecho las mediciones para verificarlo.
En general, verá mucho más beneficio del objetivo ufunc paralelo si está realizando más operaciones matemáticas por elemento de memoria (como, por ejemplo, un coseno).