Annex VI: Binding: tasks on specific cores
En las arquitecturas multinúcleo actuales para obtener un rendimiento óptimo es recomendable siempre usar opciones de “*binding*” de procesos a CPUs (cores) físicos no permitiendo al sistema la migración de procesos entre los distintos cores y asegurando la mayor proximidad entre los datos en memoria y el core que los lee/escribe. Por defecto en la configuración del FT2 cada una de las tareas solicitadas están asociadas a un cgroup (Linux Control Groups) que no permitirá que los procesos de la tarea se ejecuten fuera de los recursos físicos asociados a esa tarea.
Definiciones:
Topología del nodo por defecto (128GB RAM y 24 cores):
Cada socket dispone de 64GB RAM y 12 cores. Destacar que el disco local reside en el socket 0 y las conexiones de infiniband (lustre y comunicaciones MPI) en el socket 1.
Procesador lógico: Procesador a nivel de sistema operativo (software). Por defecto en el FT2 los procesadores lógicos corresponden a cores físicos. En algún momento puede haber nodos con el “*Hyperthreading*” activado (2 o más threads por core), en ese caso el procesador lógico corresponde a un thread de uno de los cores.
Afinidad: Asignación de un proceso a un determinado procesador lógico.
Máscara de afinidad: Máscara de bits donde los índices corresponden a procesadores lógicos. ‘1’ implica posibilidad de ejecución de un proceso en el procesador lógico asociado a la correspondiente posición en la marca.
Con slurm tenemos las siguientes posibilidades:
Afinidad a nivel de tarea (Puro MPI). Siempre es recomendable especificar el número de tareas por nodo “*–ntasks-per-node*” o el sistema de colas asignará un reparto en modelo bloque donde se intenta llenar el nodo antes de usar el siguiente asegurando siempre por lo menos una tarea por nodo. Con la opción: ***–cpu_bind=[{quiet,verbose},]type ***es posible especificar la afinidad de cada una de las tareas/procesos a procesador lógico. A continuación se describe las opciones de type recomendadas. type: rank: Afinidad automática según el rango (rank) MPI del proceso. La tarea con rango más bajo en cada nodo es asignada al procesador lógico 0. Ejemplo: ***srun -t 00:10:00 -N2 -n8 –ntasks-per-node=4 –cpu_bind=verbose,rank ./pi3 cpu_bind=RANK - c7202, task 4 0 [16381]: mask 0x1 set cpu_bind=RANK - c7202, task 5 1 [16382]: mask 0x2 set cpu_bind=RANK - c7202, task 6 2 [16383]: mask 0x4 set cpu_bind=RANK - c7202, task 7 3 [16384]: mask 0x8 set cpu_bind=RANK - c7201, task 1 1 [17456]: mask 0x2 set cpu_bind=RANK - c7201, task 2 2 [17457]: mask 0x4 set cpu_bind=RANK - c7201, task 3 3 [17458]: mask 0x8 set cpu_bind=RANK - c7201, task 0 0 [17455]: mask 0x1 set … [0] MPI startup(): RankPid Node name Pin cpu [0] MPI startup(): 0 17455c7201 +1 [0] MPI startup(): 1 17456c7201 +1 [0] MPI startup(): 2 17457c7201 +1 [0] MPI startup(): 3 17458c7201 +1 [0] MPI startup(): 4 16381c7202 +1 [0] MPI startup(): 5 16382c7202 +1 [0] MPI startup(): 6 16383c7202 +1 [0] MPI startup(): 7 16384c7202 +1 … ***Los 8 procesos MPI son asignados a los procesadores lógicos 0,1,2 y 3 (socket 0) de cada uno de los nodos. Esto no es una situación recomendable teniendo en cuenta que la infiniband (comunicaciones MPI) reside en el socket 1. *map_cpu:<list>*****: **afinidad por nodo a la lista de procesadores lógicos especificados. El ejemplo anterior debería ejecutarse mejor especificando está afinidad: ***$ srun -t 00:10:00 -N2 -n8 –ntasks-per-node=4 –cpu_bind=verbose,map_cpu:12,13,14,15 ./pi3 cpu_bind=MAP - c7201, task 1 1 [18684]: mask 0x2000 set cpu_bind=MAP - c7201, task 0 0 [18683]: mask 0x1000 set cpu_bind=MAP - c7201, task 2 2 [18685]: mask 0x4000 set cpu_bind=MAP - c7201, task 3 3 [18686]: mask 0x8000 set cpu_bind=MAP - c7202, task 4 0 [17234]: mask 0x1000 set cpu_bind=MAP - c7202, task 5 1 [17235]: mask 0x2000 set cpu_bind=MAP - c7202, task 6 2 [17236]: mask 0x4000 set cpu_bind=MAP - c7202, task 7 3 [17237]: mask 0x8000 set …* Nótese en el cambio de la máscara de afinidad (hexadecimal). Se recomienda el uso de esta opción ya que es la que da un control y una versatilidad mayor. Existen dentro de “*****srun”* opciones extra dedicadas a la especificación de la distribución de tareas:
*–distribution=arbitrary|<block|cyclic|plane=<options>[:block|cyclic|fcyclic]> *
El primer argumento especifica la distribución de las tareas en los nodos y el segundo el “*binding*” de los procesos.
Es recomendable consultar la página del man de “*****srun”* respecto a esta opción por si pudiese facilitar la ejecución de un caso concreto pero debido a la situación de los buses de infiniband en el socket 1 no se recomienda su uso por defecto ya que todas las políticas comienzan en el socket 0.
Afinidad a nivel de thread (trabajos OpenMP y MPI/OpenMP). La afinidad de los threads derivados de procesos paralelizados en memoria compartida (OpenMP en general) se especifica a través del uso de máscaras de afinidad (nº en hexadecimal que convertido en binario la posición de los “1” habilita el uso del procesador lógico). Como el caso de la ejecución de los trabajos MPI o serie al sistema de colas se debe solicitar el número de tareas a ejecutar así como el número de procesadores lógicos (cores en el FT2) asignados a esa tarea mediante la opción: *-c, –cpus-per-task=<ncpus>* También con la opción: *–cpu_bind=[{quiet,verbose},]type* es posible especificar la afinidad de cada una de las tareas/procesos a procesador lógico pero en este caso en type debe especificarse opciones de máscara. *type*****: *mask_cpu:<list>* Con esta opción se especifica la máscara de cada una de las tareas a ejecutar por nodo. Se aconseja usar una máscara en hexadecimal compuesta de 6 dígitos (cada dígito especifica la máscara de ejecución de 4 cores, 24 cores en total, 12 primeros corresponden al socket 0 y 12 segundos al socket 1) y ella especificar exclusivamente 0 o “f” habilitando o no el uso de los correspondientes 4 cores. Ejemplos de máscaras: f00f00: Los threads se ejecutarán en los 4 primeros cores del socket 0 y en los 4 primeros del socket 1 000fff (ó fff): Los threads se ejecutarán en los 12 cores del socket 0 fff000: Los threads se ejecutarán en los 12 cores del socket 1 Toda la combinatoria de máscaras posibles para los nodos de 24 cores se puede generar con esta aplicación javascript: *https://vis.cesga.es/mask_affinity/mask_affinity.html* Ejemplos de ejecuciones con “*****srun”* (se usa la utilidad cpuinfo de Intel mpi como programa ejemplo a ejecutar ya que da información de los procesadores lógicos asignados): *srun -t 00:10:00 -p thinnodes -n1 -c8 –cpu_bin=verbose,mask_cpu:f0000f cpuinfo -d srun -t 00:10:00 -p thinnodes -n2 -c8 –cpu_bin=verbose,mask_cpu:f0000f,00ff00 cpuinfo -d srun -t 00:10:00 -p thinnodes -n4 –tasks-per-node 2 -c4 –cpu_bin=verbose,mask_cpu:f0000f,00ff00 cpuinfo -d*
Se recomienda exclusivamente usar las opciones de binding disponibles en slurm. No se recomienda mezclar las opciones de binding de los distintos paquetes: usar opciones de binding con slurm y a su vez usar las proporcionadas por las librerías MPI o de paralelización en memoria compartida (OpenMP).