Saltar al contenido

Multiplicación de matrices enteras multiproceso en NumPy / SciPy

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
¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *