Solución:
Tenga en cuenta que, si bien esta respuesta envejece, numpy podría obtener soporte optimizado para enteros. Verifique si esta respuesta aún funciona más rápido en su configuración.
-
Opción 5: lanzar una solución personalizada: Divida el producto de la matriz en algunos subproductos y realice estos en paralelo. Esto puede implementarse relativamente fácil con módulos estándar de Python. Los subproductos se calculan con
numpy.dot
, que libera el bloqueo de intérprete global. Por lo tanto, es posible utilizar subprocesos que son relativamente ligeros y pueden acceder a las matrices desde el subproceso principal para la eficiencia de la memoria.
Implementación:
import numpy as np
from numpy.testing import assert_array_equal
import threading
from time import time
def blockshaped(arr, nrows, ncols):
"""
Return an array of shape (nrows, ncols, n, m) where
n * nrows, m * ncols = arr.shape.
This should be a view of the original array.
"""
h, w = arr.shape
n, m = h // nrows, w // ncols
return arr.reshape(nrows, n, ncols, m).swapaxes(1, 2)
def do_dot(a, b, out):
#np.dot(a, b, out) # does not work. maybe because out is not C-contiguous?
out[:] = np.dot(a, b) # less efficient because the output is stored in a temporary array?
def pardot(a, b, nblocks, mblocks, dot_func=do_dot):
"""
Return the matrix product a * b.
The product is split into nblocks * mblocks partitions that are performed
in parallel threads.
"""
n_jobs = nblocks * mblocks
print('running {} jobs in parallel'.format(n_jobs))
out = np.empty((a.shape[0], b.shape[1]), dtype=a.dtype)
out_blocks = blockshaped(out, nblocks, mblocks)
a_blocks = blockshaped(a, nblocks, 1)
b_blocks = blockshaped(b, 1, mblocks)
threads = []
for i in range(nblocks):
for j in range(mblocks):
th = threading.Thread(target=dot_func,
args=(a_blocks[i, 0, :, :],
b_blocks[0, j, :, :],
out_blocks[i, j, :, :]))
th.start()
threads.append(th)
for th in threads:
th.join()
return out
if __name__ == '__main__':
a = np.ones((4, 3), dtype=int)
b = np.arange(18, dtype=int).reshape(3, 6)
assert_array_equal(pardot(a, b, 2, 2), np.dot(a, b))
a = np.random.randn(1500, 1500).astype(int)
start = time()
pardot(a, a, 2, 4)
time_par = time() - start
print('pardot: {:.2f} seconds taken'.format(time_par))
start = time()
np.dot(a, a)
time_dot = time() - start
print('np.dot: {:.2f} seconds taken'.format(time_dot))
Con esta implementación obtengo una aceleración de aproximadamente x4, que es la cantidad física de núcleos en mi máquina:
running 8 jobs in parallel
pardot: 5.45 seconds taken
np.dot: 22.30 seconds taken
“¿Por qué es más rápido realizar la multiplicación de matrices flotante por flotante en comparación con int por int?” explica por qué los enteros son muy lentos: primero, las CPU tienen canalizaciones de punto flotante de alto rendimiento. En segundo lugar, BLAS no tiene ningún tipo de número entero.
Solución alterna: Convertir las matrices a float32
los valores obtienen grandes aceleraciones. ¿Cómo es la aceleración 90x en una MacBook Pro 2015? (Utilizando float64
es la mitad de bueno.)
import numpy as np
import time
def timeit(callable):
start = time.time()
callable()
end = time.time()
return end - start
a = np.random.random_integers(0, 9, size=(1000, 1000)).astype(np.int8)
timeit(lambda: a.dot(a)) # ≈0.9 sec
timeit(lambda: a.astype(np.float32).dot(a.astype(np.float32)).astype(np.int8) ) # ≈0.01 sec