NUMA práctica

Presenter Notes

Plan de clase

  • Autoparalelización.
  • numactl, taskset.
  • Ejemplo que anda mal en OpenMP si no asignamos bien la memoria inicialmente.
    • Porque si funciona en MPI bajo shmem.
  • Todo en mendieta.
  • Tratar de recuperar ese caso de estudio raro que planteó el chico de Rosario. Había links interesantes.

Faltó

  • GOMP_CPU_AFFINITY (GNU), MP_BIND y MP_BLIST (PGI), PSC_OMP_AFFINITY_MAP (EKOPath), KMP_AFFINITY (Intel).

Nicolás Wolovick 20160505

Presenter Notes

Autoparalelización

 1 PROGRAM EASYPARALLEL
 2 
 3 PARAMETER (N = 2**26)
 4 REAL A(N)
 5 REAL B(N)
 6 DO I = 1,N
 7     B(I) = I*3.14159 + N - I
 8 ENDDO
 9 
10 DO I = 1,N
11     A(I) = B(I) * 3.14159
12 ENDDO
13 
14 END

Probamos

1 $ gfortran -O3 -floop-parallelize-all -ftree-parallelize-loops=2 hpc_p123.f && perf stat -r 5 -e task-clock ./a.out
2  Performance counter stats for './a.out' (5 runs):
3         325.545034      task-clock (msec)         #    1.785 CPUs utilized            ( +-  0.51% )
4        0.182329860 seconds time elapsed                                          ( +-  1.27% )

Presenter Notes

Autoparalelización

Con -O1 no funciona

1 $ gfortran -O1 -floop-parallelize-all -ftree-parallelize-loops=2 hpc_p123.f && perf stat -r 5 -e task-clock ./a.out
2  Performance counter stats for './a.out' (5 runs):
3         322.723723      task-clock (msec)         #    0.988 CPUs utilized            ( +-  1.63% )
4        0.326674686 seconds time elapsed                                          ( +-  1.46% )

Con gfortran-4.9 se queja la comadreja por Graphite

1 $ gfortran-4.9 -O3 -floop-parallelize-all -ftree-parallelize-loops=2 hpc_p123.f && perf stat -r 5 -e task-clock ./a.out
2 f951: sorry, unimplemented: Graphite loop optimizations cannot be used (-fgraphite, -fgraphite-identity, -floop-block, -floop-interchange, -floop-strip-mine, -floop-parallelize-all, and -ftree-loop-linear)

Por dentro

 1 main:
 2 .LFB1:
 3     .cfi_startproc
 4     subq   $24, %rsp
 5     .cfi_def_cfa_offset 32
 6     call   _gfortran_set_args
 7     movl   $options.0.3397, %esi
 8     movl   $9, %edi
 9     call   _gfortran_set_options
10     movq   %rsp, %rsi
11     xorl   %ecx, %ecx
12     movl   $2, %edx
13     movl   $main._loopfn.0, %edi
14     movq   $b.3384, (%rsp)
15     call   GOMP_parallel
16     ...

Presenter Notes

Autoparalelización por dentro (cont'd)

 1 main._loopfn.0:
 2     pushq   %rbp
 3     pushq   %rbx
 4     subq    $8, %rsp
 5     movq    (%rdi), %rbx
 6     call    omp_get_num_threads
 7     movslq  %eax, %rbp
 8     call    omp_get_thread_num
 9     xorl    %edx, %edx
10     movslq  %eax, %rcx
11     movl    $67108863, %eax
12     divq    %rbp
13     cmpq    %rdx, %rcx
14     ...

Pero no es vectorial

 1 .L9:
 2     pxor    %xmm1, %xmm1
 3     leal    1(%rdx), %eax
 4     cvtsi2ss    %eax, %xmm1
 5     movaps  %xmm1, %xmm0
 6     mulss   %xmm3, %xmm0
 7     addss   %xmm2, %xmm0
 8     subss   %xmm1, %xmm0
 9     movss   %xmm0, (%rbx,%rdx,4)
10     addq    $1, %rdx
11     cmpq    %rdx, %rcx
12     jne .L9
13     ...

Presenter Notes

Otro caso de autopar

 1 float a[N][N], b[N], c[N];
 2 
 3 int main(void) {
 4     size_t i = 0, j = 0;
 5     double start = 0.0;
 6 
 7     start = omp_get_wtime();
 8     for (i=0; i<N; ++i)
 9         for (j=0; j<N; ++j)
10             c[i] += a[i][j]*b[j];
11     printf("%f ", ((long)N*N*3*sizeof(float))/((1<<30)*(omp_get_wtime()-start)));
12 
13     return 0;
14 }
  • -ffast-math: para poder reordenar.
  • -ftree-vectorize para vectorizar.
  • -ftree-parallelize-loops=n, donde n es la cantidad de hilos.

¡¡¡Tengo autovectorización y autoparalelización!!!!

(esto en gcc-4.9 no andaba, o tenía uno o tenía el otro)

Presenter Notes

Mediciones en mendieta

1 $ gcc -O3 -ftree-parallelize-loops=16 sgemv.c && perf stat -r 5 -e task-clock ./a.out
2 61.421345 61.059372 60.920014 60.466478 61.158156 
3  Performance counter stats for './a.out' (5 runs):
4 
5 767,697361 task-clock                #   14,689 CPUs utilized            ( +-  0,76% )
6 
7 0,052262059 seconds time elapsed                                          ( +-  0,31% )

En OpenMP era un one-liner, que lo pude evitar sin problema.

Notar que llega a 60 GiB/s, casi el tope del ancho de banda de estas máquinas.
(bueno en realidad no, son dos pastillas E5-2680, y cada una tiene 51.2 GiB/s)

Nota

Mirar el reporte de autoparalelización con -fdump-tree-parloops-all.

Presenter Notes

Autoparalelizador y autovectorizador

  • For Data Dependence :
    gcc -fdump-tree-all -fcheck-data-deps -fdump-tree-ckdd-all -O3 filename.c
  • For Vectorization :
    gcc -fdump-tree-all -fdump-tree-vect-all -msse4 -O3 filename.c
  • For Parallelization :
    gcc -fdump-tree-all -ftree-parallelize-loops=x -fdump-tree-parloops-all -O3 filename.c
  • Graphite Parallelization :
    gcc -fdump-tree-all -ftree-parallelize-loops=x -fdump-tree-parloops-all -floop-parallelize-all -O2 filename.c
  • For Loop Interchange :
    gcc -fdump-tree-all -floop-interchange -fdump-tree-graphite-all -O3 filename.c

Sacado de PARALLELIZATION AND VECTORIZATION IN GCC.

Presenter Notes

NUMA

Presenter Notes

NUMA

Presenter Notes

NUMA en hardware

Se puede deshabilitar el interleave.

http://frankdenneman.nl/2010/12/node-interleaving-enable-or-disable/

Presenter Notes

UMA en hardware (con máquina NUMA)

Se puede habilitar el interleave.

http://frankdenneman.nl/2010/12/node-interleaving-enable-or-disable/

  • Transforma una máquina NUMA en UMA.
  • Promedio del comportamiento (es más determinística).
  • Le mete presión al interconnect.
  • Nunca activen el interleave.

Presenter Notes

NUMA en Mendieta 1

lstopo -p --no-io --no-bridges --no-caches --of png > mendieta-lstopo.png

lstopo -p --no-io --no-bridges --no-caches --of png > mendieta-lstopo.png

Presenter Notes

NUMA en Mendieta 1

 1 $ numactl --hardware
 2 available: 2 nodes (0-1)
 3 node 0 cpus: 0 1 2 3 4 5 6 7
 4 node 0 size: 16355 MB
 5 node 0 free: 15254 MB
 6 node 1 cpus: 8 9 10 11 12 13 14 15
 7 node 1 size: 16384 MB
 8 node 1 free: 15805 MB
 9 node distances:
10 node   0   1 
11   0:  10  21 
12   1:  21  10

Notar como se informa de la memoria disponible por nodo.

Presenter Notes

NUMA en PSG16

Presenter Notes

NUMA en PSG16

 1 $ numactl --hardware
 2 available: 8 nodes (0-7)
 3 node 0 cpus: 0 1 2 3 4 5 6 7
 4 node 0 size: 32765 MB
 5 node 0 free: 2833 MB
 6 node 1 cpus: 8 9 10 11 12 13 14 15
 7 node 1 size: 32768 MB
 8 node 1 free: 20462 MB
 9 node 2 cpus: 16 17 18 19 20 21 22 23
10 node 2 size: 32768 MB
11 node 2 free: 25181 MB
12 node 3 cpus: 24 25 26 27 28 29 30 31
13 node 3 size: 32768 MB
14 node 3 free: 22888 MB
15 node 4 cpus: 32 33 34 35 36 37 38 39
16 node 4 size: 32768 MB
17 node 4 free: 24988 MB
18 node 5 cpus: 40 41 42 43 44 45 46 47
19 node 5 size: 32768 MB
20 node 5 free: 25006 MB
21 node 6 cpus: 48 49 50 51 52 53 54 55
22 node 6 size: 32768 MB
23 node 6 free: 24859 MB
24 node 7 cpus: 56 57 58 59 60 61 62 63
25 node 7 size: 32752 MB
26 node 7 free: 25099 MB
27 node distances:
28 node   0   1   2   3   4   5   6   7·
29   0:  10  16  16  22  16  22  16  22·
30   1:  16  10  22  16  22  16  22  16·
31   2:  16  22  10  16  16  22  16  22·
32   3:  22  16  16  10  22  16  22  16·
33   4:  16  22  16  22  10  16  16  22·
34   5:  22  16  22  16  16  10  22  16·
35   6:  16  22  16  22  16  22  10  16·
36   7:  22  16  22  16  22  16  16  10·

Presenter Notes

Experimentos con numactl

Puedo controlar cómo quiero que use la memoria respecto a nodos.

Esto es usando el sgemv.c autoparalelizado.

 1 $numactl --cpunodebind=1 --membind=1 ./a.out
 2 37.390317
 3 $ numactl --cpunodebind=0 --membind=1 ./a.out
 4 19.060755
 5 $ numactl --cpunodebind=1 --membind=0 ./a.out
 6 21.900698
 7 $ numactl --cpunodebind=0,1 --membind=0,1 ./a.out
 8 69.289990
 9 $ numactl --cpunodebind=0 --membind=0,1 ./a.out
10 33.960225
11 $ numactl --cpunodebind=0,1 --membind=0 ./a.out
12 42.920589

Presenter Notes

numactl como taskset

¡Atar procesos a cores!

sgemv.c para N=1L<<16.
Ojo, hay que compilar con -mcmodel=large. Las cuentas dicen que son ~16 GiB
(((1<<16)*(1<<16) + 2*(1<<16)) * 4.0) / (1<<30) = 16.00048828125

1 $ numactl --physcpubind=0 ./a.out
2 6.915849
3 $ numactl --physcpubind=0-1 ./a.out
4 13.575607
5 $ numactl --physcpubind=0-15 ./a.out
6 68.431601

El último es idem a:

1 $ taskset 0x0000FFFF ./a.out
2 69.925633

Presenter Notes

sgemv paralelo

 1 #define N (1L<<16)
 2 float a[N][N], b[N], c[N];
 3 
 4 int main(void) {
 5     int i = 0, j = 0;
 6     double start = 0.0;
 7 
 8     start = omp_get_wtime();
 9     #pragma omp parallel for shared(a,b,c,start) private(i,j)
10     for (i=0; i<N; ++i)
11     for (j=0; j<N; ++j)
12         c[i] += a[i][j]*b[j];
13     printf("%f ", ((long)N*N*3*sizeof(float))/((1<<30)*(omp_get_wtime()-start)));
14 
15     return 0;
16 }

Resultado en GiB/s:

1 $ gcc -O3 -mcmodel=large -fopenmp parallel_sgemv.c && ./a.out 
2 73.599593

Presenter Notes

sgemv paralelo

En medio de la ejecución (puse un pause 0 antes de salir) tenemos:

 1 $ numactl --hardware
 2 available: 2 nodes (0-1)
 3 node 0 cpus: 0 1 2 3 4 5 6 7
 4 node 0 size: 16355 MB
 5 node 0 free: 6861 MB
 6 node 1 cpus: 8 9 10 11 12 13 14 15
 7 node 1 size: 16384 MB
 8 node 1 free: 6965 MB
 9 node distances:
10 node   0   1 
11   0:  10  21 
12   1:  21  10

Ambos nodos NUMA ocupados.

Notar: que hay ~16 GiB ocupados distribuidos en los dos nodos.

Presenter Notes

Otra forma de leer consumo de memoria

Para evitar poner un getchar(), se puede usar /usr/bin/time.

Entre otras cosas muestra el consumo máximo de memoria residente

1 $ gcc -O3 -mcmodel=large -fopenmp parallel_sgemv_nonnuma.c && /usr/bin/time -f '%MkB' ./a.out
2 75.722791 
3 67114112kB

Ojo con /usr/bin/time versión 1.7, hay que dividir el resultado por 4.
O sea da bien: ~16 GiB.

Presenter Notes

sgemv init 0

 1 #define N (1L<<16)
 2 
 3 float a[N][N], b[N], c[N];
 4 
 5 int main(void) {
 6     int i = 0, j = 0;
 7     double start = 0.0;
 8 
 9     memset(a, 0, N*N*sizeof(float));
10     memset(b, 0, N*sizeof(float));
11     memset(c, 0, N*sizeof(float));
12 
13     start = omp_get_wtime();
14     #pragma omp parallel for shared(a,b,c,start) private(i,j)
15     for (i=0; i<N; ++i)
16     for (j=0; j<N; ++j)
17         c[i] += a[i][j]*b[j];
18     printf("%f ", ((long)3*N*N*sizeof(float))/((1<<30)*(omp_get_wtime()-start)));
19 
20     return 0;
21 }

Resultado en GiB/s:

1 $ gcc -O3 -mcmodel=large -fopenmp parallel_sgemv_nonnuma.c && ./a.out 
2 82.269709

Presenter Notes

sgemv init 0

En medio de la ejecución tenemos:

 1 $ numactl --hardware
 2 available: 2 nodes (0-1)
 3 node 0 cpus: 0 1 2 3 4 5 6 7
 4 node 0 size: 16355 MB
 5 node 0 free: 2245 MB
 6 node 1 cpus: 8 9 10 11 12 13 14 15
 7 node 1 size: 16384 MB
 8 node 1 free: 12172 MB
 9 node distances:
10 node   0   1 
11   0:  10  21 
12   1:  21  10

Pero tenemos lo que queremos, casi toda la memoria en el nodo 0.

¿Porqué funciona rápido?
¿Porqué?
¿Ah?

Presenter Notes

Medición de comunicación NUMA

 1 [nwolovick@mendieta Clase13_20140513]$ numastat && gcc -O3 -mcmodel=large -fopenmp parallel_sgemv.c && ./a.out && numastat
 2                            node0           node1
 3 numa_hit                89139513        91739553
 4 numa_miss                   4240           24102
 5 numa_foreign               24102            4240
 6 interleave_hit             78788           78786
 7 local_node              89080660        91631851
 8 other_node                 63093          131804
 9 67.330149
10                            node0           node1
11 numa_hit                89151580        91747783
12 numa_miss                   4240           24102
13 numa_foreign               24102            4240
14 interleave_hit             78788           78786
15 local_node              89092727        91640081
16 other_node                 63093          131804

El valor de other_node es la que nos interesa.

No hubo cambio.

Presenter Notes

Medición de comunicación NUMA

La versión non-numa-aware si registra comunicación.

 1 $ numastat && gcc -O3 -mcmodel=large -fopenmp parallel_sgemv_nonnuma.c && ./a.out && numastat
 2                            node0           node1
 3 numa_hit                89096545        91700335
 4 numa_miss                   3230           24102
 5 numa_foreign               24102            3230
 6 interleave_hit             78788           78786
 7 local_node              89037692        91592633
 8 other_node                 62083          131804
 9 81.223156
10                            node0           node1
11 numa_hit                89115401        91717115
12 numa_miss                   4240           24102
13 numa_foreign               24102            4240
14 interleave_hit             78788           78786
15 local_node              89056548        91609413
16 other_node                 63093          131804

Más indicios de que efectivamente se está sobrecargando el QPI.

Presenter Notes

¿Auto migración de páginas?

No, solo está disponible a partir de Linux-3.8.

Se puede controlar desde Linux-3.14 (commit).

Ayuda sobre el tema en RHEL7 (¡aun no salió!).

Auto NUMA Balancing.

  • No está habilitado en zx81 ni jupiterace.

Presenter Notes

Bibliografía

Presenter Notes

La clase que viene

"De la Playstation a la Computación Científica",
ó
"¿Porqué los gamers son nuestro máximo aliado?"

Presenter Notes