Cualquier operación donde no todos los pasos dependen de los resultados de los pasos anteriores es un candidato para ser paralelizado.
Un pool es un grupo de dos o más procesos que ejecutan sus tareas de manera simultánea.
>>> from multiprocessing import Pool
>>> def f(x):
... return 2**x
...
>>> if __name__ == '__main__':
... with Pool(3) as p:
... print(p.map(f, range(1, 11)))
[2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]
El paquete multiprocessing
facilita la ejecución
paralela en Python; forma parte de la instalación básica por lo cual no es necesario instalarlo — se carga para su uso con la
instrucción import
. Es
importante incluir su uso dentro de main
por la forma en
que accede las variables.
Se puede determinar el número de núcleos disponibles en un sistema con la llamada
>>> from multiprocessing import cpu_count
>>> cpu_count()
8
>>> import psutil # instalar con pip
>>> psutil.cpu_count(logical = False)
4
>>> psutil.cpu_count(logical = True)
8
No conviene utilizar todos los núcleos en las tareas de Python,
ya que el sistema operativo y su interfaz de usuario suelen requerir
mantenerse responsivos durante los cálculos realizados, por lo
cual es recomendable crear un cluster con un número menor de
núcleos, por ejemplo
with Pool(cpu_count() - 1) as
p:
.
El pool únicamente existe dentro del bloque
de with
en el cual se ha creado y se libera
automáticamente al concluir el bloque.
Lo valores de las variables presentes en el
ambiente de trabajo de cargan al Pool
al momento de
crearlo; si cambian y ese cambio ocupa reflejarse en los procesos en
paralelo, hay que volver a crear el pool.
>>> desde = 3
>>> hasta = 8
>>> base = 2
>>> def f(x):
... return base**x
...
>>> from multiprocessing import Pool
>>> if __name__ == '__main__':
... with Pool(3) as p:
... print(p.map(f, range(desde, hasta + 1)))
... base = 3
... print(p.map(f, range(desde, hasta + 1)))
...
[8, 16, 32, 64, 128, 256]
[8, 16, 32, 64, 128, 256]
>>> base = 3
>>> if __name__ == '__main__':
... with Pool(3) as p:
... print(p.map(f, range(desde, hasta + 1)))
[27, 81, 243, 729, 2187, 6561]
Otra opción es crear funciones que toman más de un parámetro y usar la rutina starmap
:
>>> desde = 3
>>> hasta = 8
>>> def f(x, b):
... return b**x
...
>>> from multiprocessing import Pool
>>> if __name__ == '__main__':
... with Pool(3) as p:
... print(p.starmap(f, zip([2] * 10, range(desde, hasta + 1))))
... print(p.starmap(f, zip([3] * 10, range(desde, hasta + 1))))
... print(p.starmap(f, zip([4] * 10, range(desde, hasta + 1))))
...
[9, 16, 25, 36, 49, 64]
[27, 64, 125, 216, 343, 512]
[81, 256, 625, 1296, 2401, 4096]
>>> def f(x, b, c):
... return b**x - c
...
>>> c = [7 * i for i in range(10)]
>>> if __name__ == '__main__':
... with Pool(3) as p:
... for b in range(2, 5):
... base = [b] * 10
... vars = [j for j in range(desde, hasta + 1)]
... params = zip(base, vars, c)
... print(p.starmap(f, params))
[9, 9, 11, 15, 21, 29]
[27, 57, 111, 195, 315, 477]
[81, 249, 611, 1275, 2373, 4061]
Tarjetas gráficas contemporaneas contienen procesdaores
gráficos con bastante núcleos. El
paquete pyopencl
permite aprovechar esos núcleos
para cálculos. Su instalación requiere la
herramienta pybind11
con pip3
y la presencia
a las librerías de OpenCL (no todas las computadoras tienen chips o
drivers compatibles). Finalizando la instalación del paquete,
igual como al cargarlo al uso, se detectan GPUs presentes en la
computadora en cuestión.
Algunas versiones de pip
requieren un ligero hack en
/Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/site-packages/pip/req.py
(ajustar según su sistema) para que no marquen error al
agregar pybind11
:
class Hack: # agregado para compatibilidad def __init__(self, text): self.req = text def parse_requirements(filename, session=None): # otro hack para que haya session return [Hack(line) for line in (line.strip() for line in open(filename)) if line and not line.startswith("#")]
En una iMac con una GPU de Radeon dice
>>> import pyopencl
>>> from pyopencl.tools import get_test_platforms_and_devices
>>> get_test_platforms_and_devices()
[(<pyopencl.Platform 'Apple' at 0x7fff0000>, [<pyopencl.Device 'Intel(R) Core(TM) i7-6700K CPU @ 4.00GHz' on 'Apple' at 0xffffffff>, <pyopencl.Device 'ATI Radeon R9 M395X Compute Engine' on 'Apple' at 0x1021c00>])]
En una MacBook con una GPU de Intel dice
>>> import pyopencl
>>> from pyopencl.tools import get_test_platforms_and_devices
>>> get_test_platforms_and_devices()
[(<pyopencl.Platform 'Apple' at 0x7fff0000>, [<pyopencl.Device 'Intel(R) Core(TM) M-5Y71 CPU @ 1.20GHz' on 'Apple' at 0xffffffff>, <pyopencl.Device 'Intel(R) HD Graphics 5300' on 'Apple' at 0x1024500>])]
Por ejemplo, el producto de matrices que suele ser la prueba estándar para ver si la GPU está haciendo lo que debe (véase las notas de Jan Verschelde hace lo siguiente con el GPU de Radeon. Nota que el código para el GPU viene escrito en el lenguaje C.
# adaptado de http://homepages.math.uic.edu/~jan/mcs572/mcs572notes/lec29.html import pyopencl as cl import numpy as np import os os.environ['PYOPENCL_COMPILER_OUTPUT'] = '1' os.environ['PYOPENCL_CTX'] = '1' n = 7000 M1 = np.random.rand(n, n).astype(np.float32) M2 = np.random.rand(n, n).astype(np.float32) M3 = np.zeros((n, n), dtype=np.float32) platforms = cl.get_platforms() ctx = cl.Context(dev_type=cl.device_type.ALL, properties=[(cl.context_properties.PLATFORM, platforms[0])]) queue = cl.CommandQueue(ctx) mf = cl.mem_flags # memoria para procesar en GPU b1 = cl.Buffer(ctx, mf.READ_ONLY | mf.COPY_HOST_PTR, hostbuf=M1) b2 = cl.Buffer(ctx, mf.READ_ONLY | mf.COPY_HOST_PTR, hostbuf=M2) b3 = cl.Buffer(ctx, mf.WRITE_ONLY, M3.nbytes) # lo que se hace en GPU cCode = '''PONER AQUI LO QUE VIENE EN C ABAJO''' prg = cl.Program(ctx, cCode).build() from time import time antes = time() prg.multiply(queue, M3.shape, None, np.uint16(n), b1, b2, b3) cl.enqueue_copy(queue, np.empty_like(M3), b3) print(time() - antes) antes = time() np.matmul(M1, M2) print(time() - antes)Esto se pone en el string que debe contener la implementación en C:
__kernel void multiply(ushort n, __global float *a, __global float *b, __global float *c) { int gid = get_global_id(0); c[gid] = 0.0f; int rowC = gid / n; int colC = gid % n; __global float *pA = &a[rowC * n]; __global float *pB = &b[colC]; for (int k = 0; k < n; k++) { pB = &b[colC + k * n]; c[gid] += (*(pA++)) * (*pB); } }
Lo chistoso es que las rutinas de numpy
son tan
eficientes que no se logra ganarlos en este caso particular. Abajo
están las salidas con la iMac y la MacBook, respectivamente. En la
laptop que tiene un procesador más modesto, es casi lo mismo, pero
en la iMac que tiene un procesador potente, el GPU pierde por el
setup overhead.
$ python3 gpu.py
10.52643609046936
1.9072740077972412
$ python3 gpu.py
8.462943077087402
8.137696027755737
https://satuelisa.github.io/simulation/paralelo/Python.html