You are here

MPI usando Python

Si queremos programar una aplicación distribuida para un cluster o bien tenemos una aplicación en Python que queremos ejecutar en un cluster, veremos que disponemos de varias implementaciones MPI (interfaz de paso de mensajes) para el lenguaje Python.

Entre ellas destaca PyMPI que es una variante del interprete Python, este intérprete se encarga de que nuestra aplicación sea multinodo, en lugar del código que ejecuta el intérprete. Me pareció un sistema interesante para una aplicación ya lista pero no ofrece suficiente control sobre que fragmentos de código queremos ejecutar en "multinodos" y además no sabemos la evolución del proyecto y puede que en nuevas actualizaciones de Python se retrase esta variante del intérprete.

Otras opciones que se plantearon fueron mpi4py, PyPar, MYMPI, estos ya módulos de Python. Y al final nos quedamos con Parallel Python, por diversos motivos entre los cuales destacaría algunos que fueron decisivos: ligereza, instalación sencilla, fácil integración con otro software Python además de ofrecer una manera sencilla de paralelizar una aplicación existente sin necesidad de usar MPI. También hay que tener en cuenta que se distribuye bajo una licencia tipo BSD.

Hecha nuestra elección vamos a ver como instalar y configurar nuestro cluster MPI para ejecutar código Python. Los prerrequisitos de este tutorial es disponer de un Cluster LAM/MPI con Debian Etch. (No continuar, si no disponemos de un cluster MPI implementado)

Así que para ejecutar código Python en paralelo dentro de nuestro cluster, el primer paso va a ser instalar Python y Parallel Python en el frontend y todos los nodos computacionales de nuestro cluster:

apt-get install python2.5 python2.5-dev

cd /home/cluster/
tar -xzvf pp-1.5.4.tar.gz
cd pp-1.5.4
./setup.py install

Hecho esto, y con nuestro cluster LAM/MPI "lambooted" ejecutaremos ppserver.py en todos los nodos computacionales. Esto se puede hacer de forma sencilla desde el frontend:

rsh debian-node1 -n 'ppserver.py &'
rsh debian-node2 -n 'ppserver.py &'
rsh debian-node3 -n 'ppserver.py &'
rsh debian-node4 -n 'ppserver.py &'

Y listo! Supongo que estaréis de acuerdo conmigo en que es una instalación muy sencilla. Para probarla tenemos una serie de estupendos ejemplos en el sitio web.

Empezaremos con la suma de números primos hasta 100700 (sum_primes.py), lo ejecutaremos usando solo el frontend (un Pentium 3), que necesitará casi 59 segundos para ejecutar todo el proceso:

./sum_primes.py 1
Usage: python sum_primes.py [ncpus]
    [ncpus] - the number of workers to run in parallel,
    if omitted it will be set to the number of processors in the system

Starting pp with 1 workers
Sum of primes below 100 is 1060
Sum of primes below 100000 is 454396537
Sum of primes below 100100 is 454996777
Sum of primes below 100200 is 455898156
Sum of primes below 100300 is 456700218
Sum of primes below 100400 is 457603451
Sum of primes below 100500 is 458407033
Sum of primes below 100600 is 459412387
Sum of primes below 100700 is 460217613
Time elapsed:  58.4337880611 s
Job execution statistics:
 job count | % of all jobs | job time sum | time per job | job server
         9 |        100.00 |      51.1004 |     5.677823 | local
Time elapsed since server creation 58.4621379375

Ahora vamos a usar el cluster completo, el frontend más los cuatro nodos computacionales (estos nodos són Pentium 2), ahora le costará algo menos de 19 segundos ejecutar el proceso:

./sum_primes.py 1
Usage: python sum_primes.py [ncpus]
    [ncpus] - the number of workers to run in parallel,
    if omitted it will be set to the number of processors in the system

Starting pp with 1 workers
Sum of primes below 100 is 1060
Sum of primes below 100000 is 454396537
Sum of primes below 100100 is 454996777
Sum of primes below 100200 is 455898156
Sum of primes below 100300 is 456700218
Sum of primes below 100400 is 457603451
Sum of primes below 100500 is 458407033
Sum of primes below 100600 is 459412387
Sum of primes below 100700 is 460217613
Time elapsed:  18.7660019398 s
Job execution statistics:
 job count | % of all jobs | job time sum | time per job | job server
         2 |         22.22 |      18.3807 |     9.190361 | 10.11.12.2:60000
         2 |         22.22 |      18.5409 |     9.270460 | 10.11.12.4:60000
         1 |         11.11 |       9.1975 |     9.197520 | 10.11.12.5:60000
         3 |         33.33 |      14.6504 |     4.883458 | local
         1 |         11.11 |       9.2648 |     9.264778 | 10.11.12.3:60000
Time elapsed since server creation 18.7660019398

Vamos a ver que le cuesta al humilde cluster obtener un md5 reverso (por fuerza bruta) para ello le haremos calcular nada más y nada menos que 1.983.421 md5 para obtener el que le hemos indicado, a reverse_md5.py le costará aproximadamente 19 segundos (obteniendo unos 100.000 md5 por segundo):

./reverse_md5.py 1
Usage: python reverse_md5.py [ncpus]
    [ncpus] - the number of workers to run in parallel,
    if omitted it will be set to the number of processors in the system

Starting pp with 1 workers
hash = d88905653fc1ea8f9dfb21533f36cf9e
Reverse md5 for d88905653fc1ea8f9dfb21533f36cf9e is 1983421
Time elapsed:  19.352408886 s
Job execution statistics:
 job count | % of all jobs | job time sum | time per job | job server
        25 |         19.53 |      19.0860 |     0.763439 | 10.11.12.2:60000
        24 |         18.75 |      18.4000 |     0.766668 | 10.11.12.4:60000
        25 |         19.53 |      18.3720 |     0.734878 | 10.11.12.5:60000
        30 |         23.44 |      18.0695 |     0.602315 | local
        24 |         18.75 |      18.4427 |     0.768447 | 10.11.12.3:60000
Time elapsed since server creation 19.3536601067

Vamos a ver ahora la potencia de nuestro cluster con callback.py,que usando callbacks calculará la suma parcial 1-1/2+1/3-1/4+1/5-1/6+... (en el límite es ln(2)). Esto le costará al frontend unos 283 segundos:

./callback.py
Usage: python callback.py [ncpus]
    [ncpus] - the number of workers to run in parallel,
    if omitted it will be set to the number of processors in the system

Starting pp with 1 workers
Partial sum is 0.69314720556 | diff = -2.50000439239e-08
Job execution statistics:
 job count | % of all jobs | job time sum | time per job | job server
       128 |        100.00 |     283.1016 |     2.211731 | local
Time elapsed since server creation 283.22504282

Frente a los 15 segundos que tardará en hacer el cálculo completo usando todo los nodos del clúster:

./callback.py
Usage: python callback.py [ncpus]
    [ncpus] - the number of workers to run in parallel,
    if omitted it will be set to the number of processors in the system

Starting pp with 1 workers
Partial sum is 0.69314720556 | diff = -2.50000439239e-08
Job execution statistics:
 job count | % of all jobs | job time sum | time per job | job server
        26 |         20.31 |      14.5316 |     0.558909 | 10.11.12.2:60000
        27 |         21.09 |      15.0113 |     0.555973 | 10.11.12.4:60000
        26 |         20.31 |      14.5897 |     0.561143 | 10.11.12.5:60000
        23 |         17.97 |      14.2961 |     0.621569 | local
        26 |         20.31 |      14.5336 |     0.558986 | 10.11.12.3:60000
Time elapsed since server creation 15.4670109749

Qué te parece: 

Comments

Te has olvidado de comentar la particularidad que tiene el apt-get de debian al instalar la versión 2.5 de python. Y es que también te instala la 2.4 y por defecto utiliza el interprete de la 2.4 Tongue out

Tal y como recuerda Warwolf, un detalle importante es que si vamos a usar Python 2.5, en Etch nos instalará también la 2.4 que será el intérprete por defecto, para usar Python 2.5 deberíamos actualizar los enlaces simbólicos en /usr/bin:

cd /usr/bin/
rm python
ln -s python2.5 python