OpenMP

OpenMP (Open Multi-Processing) es una un conjunto de comandos y rutinas, portables y escalables, que permite la paralelización de tareas dentro de un programa. Trabaja con paralelización multi-threaded y shared memory (de memoria compartida). Sus objetivo es ser estandarizado , portable , discreto, eficiente y fácil de usar. Tiene soporte para Fortran y C/C++.

El hecho de que openMP use un modelo de memoria compartida trae un problema inherente conocido como race-conditions. Esto es cuando distintos procesos acceden a una misma porción de memoria y la leen o escriben haciendo que el resultado del cálculo cambie arbtitrariamente dependiendo de cual proceso accedio primero. La forma de evitar estos conflictos es atraves de la sincronización de threads. La sincronización puede afectar significativamente la performance del programa por lo que hay que intentar minimizarla.

Modelo de programación Fork-Join

OpenMP sigue un modelo conocido como fork-join donde se distinguen regiones seriales (con un único thread) y regiones paralelas (multiple threads)

Regiones paralelas:

Para indicar que comienza una porción del código a ser corrido en paralelo se indica:

!$omp parallel
  !(Acá va la region paralela)
!$omp end parallel

Lo que queda encerrado entre ese bloque se paraleliza. En esta región del código se asigna un thread master y varios slave threads.

Ejemplo simple: Hola mundo

OpenMP ya está incluido en la mayoría de los compiladores comunmente utilizados, por lo que no requiere ninguna compilación ni instalación.

Un programa “Hola mundo” en fortran sería:

program hola
  use omp_lib
  !$omp parallel 
     print '("Hello world! ",I0,"/",I0)',omp_get_thread_num(),omp_get_num_threads()
  !$omp end parallel
end program

En C sería:

#include <stdio.h>

int main(){
   #pragma omp parallel
   {
        //printf("Hola mundo!\n");
        printf("Hola mundo! %d/%d\n",omp_get_thread_num(),omp_get_num_threads());
   }
}

Al compilar, para que el script soporte openMP hay que agregar un comando para habilitarlo, para GNU es:

$> gfortran -fopenmp hola_omp.f90

Para correr un programa compilado con openMP primero hay que indicar el numero de threads y luego ejecutarlo

export OMP_NUM_THREADS=4
./a.out

Sincronización

La sincronización se usa para imponer orden entre procesos y proteger el acceso a la memoria compartida.

OpenMP tiene los siguientes comandos de alto nivel para la sincronización:

También hay otros comandos de bajo nivel:

Interacción entre threads

Almacenamiento default

Atributos de compartición de datos:

Se puede definir por default que usar: default(private/shared/none)

Comandos de trabajo compartido

Las regiones paralelas crean un programa simple de datos multiples donde cada thread ejecuta el mismo codigo.

Para dividir el trabajo entre threads en una region paralela pueden utilizarse:

  1. Loops
  2. Secciones
  3. Taks
  4. Workshare

Loop

Para hacer que se comparta el trabajo de un loop.

El trabajo compartido puede ser controlado utilizando las clausulas de schedule() (esquema)

Reducción

Race conditions: Ocurren cuando multiples threads leen y escriben una misma variable simultaneamente. Esto produce resultados aleatorios dependiendo del orden de acceso de los threads.

Ejemplo:

asum=0.0
!$OMP PARALLEL DO SHARED(x,y,n,asum) PRIVATE(i)
do i=1, n
      asum= asum + x(i)*y(i)
end do
!$OMP END PARALLEL DO 

Por lo tanto se necesita algún mecanísmo para controlar el acceso. reduction(operador:lista)

Ejemplo:

fortran !$OMP PARALLEL DO SHARED(x,y,n,asum) PRIVATE(i) REDUCTION(+:asum) do i=1, n asum= asum + x(i)*y(i) end do !$OMP END PARALLEL DO

(!) Dado que la suma y el producto con punto flotante no es asociativa, es posible que se obtenga distintos valores cada vez que se ejecuta el programa.

Buenas prácticas:

Secciones

Ejecución paralela de regiones de codigo independientes, cada una con un thread: !$OMP SECTIONS !$OMP SECTION

Comunmente usado para asignar diferentes llamadas a subrutinas a diferentes threads.

Dificil de cargar balancce

!$OMP PARALLEL
  !$OMP SECTIONS
      !$omp section
        call do_a()
      !$omp section
        call do_b()
      !$omp section
        call do_c()
  !$OMP END SECTIONS
!$OMP END PARALLEL

Tasks

Asignar tasks (tareas) a cada thread !$OMP TASK Cada task tiene:

La cola de taks es manejada por ompenMP runtime

integer :: a

subroutine foo()
    integer :: b, a
    !$OMP PARALLEL firstprivate(b)
    !$OMP TASK SHARED(c)
      call bar(b,c)
    !$OMP END TASK
    !$OMP END PARALLEL
end subroutine foo

subroutine bar(b,c)
  integer :: b,c,d
    ! scope of a: shared
    ! scope of b: firstprivate
    ! scope of c: shared
    ! scope of d: private
subroutine bar
integer x

!$OMP PARALLEL
  !$OMP SINGLE
    x= fib(n)
  !$OMP END SINGLE

!$OMP END PARALLEL
  contains
  recursive function fib(n) result(fn)
    integer :: j,n,fnm, fn
    if (n<2) then
    fn=n
    return 
    else if (n<10)
      fn fibo_secuencial(n) !Una funcion secuencial de fibonacci
      return
    end if

    !$OMP TASK SHARED(fn)
      fn= fib(n-1)
    !$OMP END TASK
    !$OMP TASK shared(fnm)
    fnm= fib(n-2)
    !$OMP END TASK
    !$OMP TASKWAIT
    fn= fn+fnm
  end function

Workshare construct

!$OMP WORKSHARE Restricción: el bloque de código solo puede contenter:

real A(100,100), B(100,100)
call random_number(A)
!$OMP PARALLEL_SHARED(A,B)
!$OMP WORKSHARE
  where (A<0.5)
    B=0.0
  elsewhere
    B=1.0
  end where
!$OMP END WORKSHARE
!$OMP END PARALLEL

Sincronización

Aveces parte de la region paralela debe ser ejecutada sólo por el master thread ó por un solo thread a la vez. (I/O inicialización, actualización de valores globales, etc.)

OpenMP provée de clausulas para controlar la ejecución de bloques de código.

Variables de entorno

OpenMP provée de varias formas para interactuar con el entorno de ejecución, estas operaciones incluyen:

Operanciones en run-time:

Control de ejecución: LOCKS y paralelismo anidado

Hay dos tipos de locks: simple y anidado

integer (kind=omp_lock_kind) svar
integer (kind=omp_nest_lock_kind) svar

Los locks habilitan:

Los locks proveen funcionalidades análogas a semáforos

Sintaxis: su subroutine OMP_(init/set/destroy)_LOCK(svar) subroutine OMP_(init/set/destroy)_nest_LOCK(svar)

Workflow:

  1. Definir una variable lock
  2. Inicializar con omp_init_lock
  3. setear con omp_set_lock ó omp_test_lock
  4. Liberar con omp_unset_lock
  5. Destruir con omp_destroy_lock

Ejemplo:

integer (kind=omp_lock_kind) lock
  call omp_init_lock(lock)
!$OMP PARALLEL
  do while (.not. omp_test_lock(lock))
    !Hacer algo
  end do
!call omp_unset_lock(lock)
!$OMP END PARALLEL
call omp_destroy_lock(lock)

Hardware

Jerarquía de memoria, afinidad

Las computadoras de memoria compartida (shared memory) pueden ser de dos tipos:

Afinidad Los S.O asignan threads y procesos a determinados nucleos. Por ejemplo, en linux por default se utiliza soft affinity (el SO intenta evitar mover threads de un nucleo a otro.) Para la mayor eficiencia computacional es util asociar (pin) threads a nucleos especificos. Para configurar afinidad en GNU se utiliza: GOMP_CPU_AFFINITY ="0-6"

Coherencia del cache (false sharing)

Los caches de las nuevas CPUs son complejos. Los datos son leidos/escritos como lineas de cache enteras (generalmente 64bits) Los modelos de programación requiren que la información en la memoria sea consistente (un address solo puede tener un valor).

Cuando diferentes threads modifican ubicación en memoria sucesivamente, la coeherencia del cache forza estas actualizaciones a ser transferidas entre todas las copias de cache. Si esto ocurre en una rapida sucesión hay una penalidad muy grande en la performance debido a perdidas de caches. Para evitarlo, reorganizar el acceso a los datos de forma tal que cada thread modifique valores dentro de un bloque más grande, ó usar variables privadas. (Esto no ocurre cuando los datos son solo leídos).


Edit this page.

Licensed MIT.