Saltar al contenido

Cómo paralelizar este bucle for de Python cuando se usa Numba

Solución:

Numba se ha actualizado y prange() funciona ahora! (Estoy respondiendo a mi propia pregunta).

Las mejoras a las capacidades de computación paralela de Numba se analizan en esta publicación de blog, con fecha del 12 de diciembre de 2017. Aquí hay un fragmento relevante del blog:

Hace mucho tiempo (¡más de 20 lanzamientos!), Numba solía tener soporte para un modismo para escribir paralelos para bucles llamado prange(). Después de una importante refactorización del código base en 2014, esta característica tuvo que ser eliminada, pero ha sido una de las características de Numba más solicitadas desde ese momento. Después de que los desarrolladores de Intel paralelizaran las expresiones de matriz, se dieron cuenta de que traer de vuelta prange sería bastante fácil

Usando Numba versión 0.36.1, puedo paralelizar mi vergonzosamente paralelo for-loop usando el siguiente código simple:

@numba.jit(nopython=True, parallel=True)
def csrMult_parallel(x,Adata,Aindices,Aindptr,Ashape): 

    numRowsA = Ashape[0]    
    Ax = np.zeros(numRowsA)

    for i in numba.prange(numRowsA):
        Ax_i = 0.0        
        for dataIdx in range(Aindptr[i],Aindptr[i+1]):

            j = Aindices[dataIdx]
            Ax_i += Adata[dataIdx]*x[j]

        Ax[i] = Ax_i            

    return Ax

En mis experimentos, paralelizar el for-loop hizo que la función se ejecutara unas ocho veces más rápido que la versión que publiqué al principio de mi pregunta, que ya estaba usando Numba, pero que no estaba en paralelo. Además, en mis experimentos, la versión paralelizada es aproximadamente 5 veces más rápida que el comando Ax = A.dot(x) que utiliza la función de multiplicación escasa matriz-vector de scipy. Numba ha aplastado a scipy y finalmente tengo una rutina de multiplicación de matriz-vector dispersa en Python que es tan rápido como MATLAB.

Gracias por tus actualizaciones cuantitativas, Daniel.
Las siguientes líneas pueden ser difíciles de tragar, pero créanme amablemente, hay más cosas a tener en cuenta. He trabajado en problemas de hpc / procesamiento paralelo / paralelismo-amdahl
tener matrices en las escalas ~ N [TB]; N > 10 y sus escasos acompañamientos, por lo que algunas piezas de experiencia pueden ser útiles para sus futuras vistas.

ADVERTENCIA: No espere que se sirva ninguna cena gratis

El deseo de paralelizar un fragmento de código suena como un maná rearticulado cada vez más contemporáneo. El problema es no el código, pero el costo de dicha mudanza.

La economía es el problema número uno. La Ley de Amdahl, tal como la formuló originalmente Gene Amdahl, no tuvo en cuenta los costos mismos de [PAR]-procesos-configuraciones + [PAR]-procesos-finalizaciones y terminaciones, que de hecho deben pagarse en cada implementación del mundo real.

El Ley de Amdahl estricta por encima de la cabeza describe la escala de estos efectos adversos inevitables y ayuda a comprender algunos aspectos nuevos que deben evaluarse antes de optar por introducir la paralelización (a un costo aceptable de hacerlo, ya que es muy, de hecho MUY FÁCIL pagar MUCHO más que uno puede beneficiarse, donde una decepción ingenua por un rendimiento de procesamiento degradado es la parte más fácil de la historia).

Siéntase libre de leer más publicaciones sobre la reformulación estricta de la Ley de Amdahl, si está dispuesto a comprender mejor este tema y a precalcular el real mínimo“-subProblem-“Talla, por lo que el la suma de-[PAR]-los gastos generales se justificarán al menos de herramientas del mundo real para introducir la división en paralelo del subproblema en N_trully_[PAR]_processes (no cualquier “solo” –[CONCURRENT], Pero cierto-[PARALLEL] – estos no son iguales).


Python puede recibir una dosis de esteroides para un mayor rendimiento:

Python es un gran ecosistema de creación de prototipos, mientras que numba, numpy y otras extensiones compiladas ayudan mucho a aumentar el rendimiento mucho más de lo que normalmente ofrece un procesamiento (co -) de python con pasos GIL nativos.

Aquí, intentas hacer cumplir numba.jit() para arreglar el trabajo casi-gratis, solo por su automatizado jit()-time lexical-analyzer (en el que arroja su código), que debe “comprender” su objetivo global ( Qué hacer), y también proponer algunos trucos de vectorización ( Como mejor ensamblar un montón de instrucciones de CPU para la máxima eficiencia de tal ejecución de código).

Suena fácil, pero no lo es.

El equipo de Travis Oliphant ha hecho inmenso progreso en numba herramientas, pero seamos realistas y justos para no esperar que se implemente ninguna forma de hechicería automatizada dentro de un .jit()-lexer + análisis de código, al intentar transformar un código y ensamblar un flujo más eficiente de instrucciones de la máquina para implementar el objetivo de la tarea de alto nivel.

@guvectorize? ¿Aquí? ¿Seriamente?

Debido a [PSPACE] tallaje, puede olvidarse inmediatamente de preguntar numba para “rellenar” de alguna manera eficientemente el motor de la GPU con datos, una huella de memoria que está muy por detrás de los tamaños de GPU-GDDR (sin hablar en absoluto de tamaños de kernel de GPU demasiado “superficiales” para un procesamiento matemático tan “diminuto” simplemente multiplicar, potencialmente en [PAR], pero para luego resumir [SEQ] ).

(Re -): cargar la GPU con datos lleva mucho tiempo. Si habiendo pagado eso, las latencias de memoria en GPU tampoco son muy amigables para la economía “pequeña” de los núcleos de GPU, su ejecución de código GPU-SMX lo hará tengo que pagar ~ 350-700 [ns] solo para buscar un número (lo más probable es que no se vuelva a alinear automáticamente para la mejor reutilización combinada compatible con el caché SM en los siguientes pasos y es posible que note que nunca, déjeme repetirlo, NUNCA reutilice una sola celda de matriz en absoluto, por lo que el almacenamiento en caché per se no entregará nada debajo de esos 350~700 [ns] por celda de matriz), mientras un inteligente puro numpy-código vectorizado puede procesar producto matriz-vector en menos de 1 [ns] por celda incluso en el más grande [PSPACE]huellas de pisadas.

Ese es un criterio con el que comparar.

(La elaboración de perfiles mostraría mejor aquí los hechos concretos, pero el principio es bien conocido de antemano, sin probar cómo mover algunos TB de datos en la estructura de la GPU solo para darse cuenta de esto por su cuenta. )


La peor de las malas noticias:

Dadas las escalas de memoria de la matriz A, el peor efecto que se puede esperar es que la escasa organización del almacenamiento de la representación de la matriz probablemente devastará la mayoría, si no todas, las posibles ganancias de rendimiento que se pueden lograr mediante numbatrucos vectorizados en representaciones de matrices densas, ya que es probable que haya casi cero posibilidades de reutilizaciones eficientes de la línea de caché obtenida de la memoria y la escasez también romperá cualquier forma fácil de lograr un mapeo compacto de operaciones vectorizadas y estas difícilmente seguirán siendo capaces de obtener se traduce fácilmente en recursos avanzados de procesamiento de vectores de hardware de CPU.


Inventario de problemas solucionables:

  • siempre mejor preasignar el vector Ax = np.zeros_like( A[:,0] ) y pasarlo como otro parámetro a la numba.jit()– partes compiladas del código, para evitar pagos repetitivos adicionales [PTIME,PSPACE]-costos para crear (nuevamente) nuevas asignaciones de memoria (más si el vector es sospechoso de ser utilizado dentro de un proceso de optimización iterativo orquestado externamente)
  • siempre es mejor especificar (para reducir la universalidad, en aras del rendimiento del código resultante)
    al menos el numba.jit( "f8[:]( f4[:], f4[:,:], ... )" )-llamando directivas de interfaz
  • siempre revisa todo numba.jit()-opciones disponibles y sus respectivos valores predeterminados (puede cambiar de versión a versión) para su situación específica (deshabilitar GIL y alinear mejor los objetivos con numba + las capacidades de hardware siempre ayudarán en partes numéricamente intensivas del código)

@jit(   signature = [    numba.float32( numba.float32, numba.int32 ),                                   #          # [_v41] @decorator with a list of calling-signatures
                         numba.float64( numba.float64, numba.int64 )                                    #
                         ],    #__________________ a list of signatures for prepared alternative code-paths, to avoid a deferred lazy-compilation if undefined
        nopython = False,      #__________________ forces the function to be compiled in nopython mode. If not possible, compilation will raise an error.
        nogil    = False,      #__________________ tries to release the global interpreter lock inside the compiled function. The GIL will only be released if Numba can compile the function in nopython mode, otherwise a compilation warning will be printed.
        cache    = False,      #__________________ enables a file-based cache to shorten compilation times when the function was already compiled in a previous invocation. The cache is maintained in the __pycache__ subdirectory of the directory containing the source file.
        forceobj = False,      #__________________ forces the function to be compiled in object mode. Since object mode is slower than nopython mode, this is mostly useful for testing purposes.
        locals   = {}          #__________________ a mapping of local variable names to Numba Types.
        ) #____________________# [_v41] ZERO <____ TEST *ALL* CALLED sub-func()-s to @.jit() too >>>>>>>>>>>>>>>>>>>>> [DONE]
 def r...(...):
      ...
¡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 *