python - transpuesta - Degradación del rendimiento de la multiplicación de matrices de matrices de precisión simple vs doble en máquinas con varios núcleos
python matrices tutorial (1)
Sospecho que esto se debe a la desafortunada programación de subprocesos. Pude reproducir un efecto similar al tuyo. Python se ejecutaba a ~ 2.2 s, mientras que la versión C mostraba grandes variaciones desde 1.4-2.2 s.
Aplicación: KMP_AFFINITY=scatter,granularity=thread
Esto garantiza que los 28 subprocesos siempre se estén ejecutando en el mismo subproceso del procesador.
Reduce ambos tiempos de ejecución a ~ 1,24 s más estables para C y ~ 1,26 s para python.
Esto está en un sistema Xeon E5-2680 v3 de doble socket de 28 núcleos.
Curiosamente, en un sistema Haswell de doble socket de 24 núcleos muy similar, tanto Python como C tienen un rendimiento casi idéntico, incluso sin afinidad / fijación de hilos.
¿Por qué afecta Python a la programación? Bueno, supongo que hay más entorno de ejecución a su alrededor. La conclusión es que, sin fijar los resultados de rendimiento, no será determinista.
También debe tener en cuenta que el tiempo de ejecución de Intel OpenMP genera un hilo de administración adicional que puede confundir al programador. Hay más opciones de KMP_AFFINITY=compact
, por ejemplo, KMP_AFFINITY=compact
, pero por alguna razón eso está totalmente desordenado en mi sistema. Puede agregar ,verbose
a la variable para ver cómo el tiempo de ejecución está fijando sus hilos.
likwid-pin es una alternativa útil que proporciona un control más conveniente.
En general, la precisión simple debe ser al menos tan rápida como la precisión doble. La precisión doble puede ser más lenta porque:
- Necesita más memoria / ancho de banda de caché para una doble precisión.
- Puede crear ALU que tengan un rendimiento más alto para una precisión única, pero eso generalmente no se aplica a las CPU, sino a las GPU.
Creo que una vez que se deshaga de la anomalía de rendimiento, esto se reflejará en sus números.
Cuando aumente el número de subprocesos para MKL / * gemm, considere
- El ancho de banda de la memoria / caché compartida puede convertirse en un cuello de botella, lo que limita la escalabilidad
- El modo turbo disminuirá efectivamente la frecuencia central al aumentar la utilización. Esto se aplica incluso cuando se ejecuta a una frecuencia nominal: en los procesadores Haswell-EP, las instrucciones AVX impondrán una "frecuencia base AVX" más baja, pero se permite que el procesador exceda eso cuando se utilizan menos núcleos / se dispone de margen térmico y, en general, incluso más por un corto tiempo. Si desea resultados perfectamente neutrales, tendría que usar la frecuencia base AVX, que es de 1.9 GHz para usted. Se documenta here , y se explica en una imagen .
No creo que haya una manera realmente sencilla de medir cómo su aplicación se ve afectada por una mala programación. Puede exponer esto con perf trace -e sched:sched_switch
y hay algún software para visualizarlo, pero esto tendrá una curva de aprendizaje alta. Y luego otra vez: para el análisis de rendimiento paralelo, debes tener los hilos anclados de todos modos.
ACTUALIZAR
Desafortunadamente, debido a mi descuido, tenía una versión anterior de MKL (11.1) vinculada a numpy. La versión más reciente de MKL (11.3.1) ofrece el mismo rendimiento en C y cuando se llama desde python.
Lo que estaba oscureciendo las cosas, fue incluso si vinculaba las bibliotecas compartidas compiladas explícitamente con la MKL más nueva, y señalaba a través de las variables LD_ * hacia ellas, y luego en python hacía un número de importación, de alguna manera hacía que las bibliotecas MKL de Python se llamaran antiguas. Solo al reemplazar en la carpeta lib de python todos los archivos libmkl _ *. Así que con el MKL más reciente pude igualar el rendimiento en las llamadas a python y C.
Información de fondo / biblioteca.
La multiplicación de matrices se realizó a través de sgemm (precisión simple) y dgemm (precisión doble) llamadas a la biblioteca MKL de Intel, mediante la función numpy.dot. La llamada real de las funciones de la biblioteca se puede verificar, por ejemplo, con oprof.
Usando aquí 2x18 CPU central E5-2699 v3, por lo tanto un total de 36 núcleos físicos. KMP_AFFINITY = Dispersión. Corriendo en linux.
TL; DR
1) ¿Por qué es numpy.dot, aunque está llamando a las mismas funciones de la biblioteca MKL, dos veces más lento en comparación con el código compilado en C?
2) Por qué a través de numpy.dot se obtiene una disminución en el rendimiento al aumentar el número de núcleos, mientras que el mismo efecto no se observa en el código C (llamar a las mismas funciones de la biblioteca).
El problema
He observado que al realizar la multiplicación de matrices de flotadores de precisión simple / doble en numpy.dot, y al llamar a cblas_sgemm / dgemm directamente desde una biblioteca compilada de C, se obtiene un rendimiento notablemente peor en comparación con llamar a las mismas funciones de cblas_sgemm / dgemm de MKL desde dentro de C puro código.
import numpy as np
import mkl
n = 10000
A = np.random.randn(n,n).astype(''float32'')
B = np.random.randn(n,n).astype(''float32'')
C = np.zeros((n,n)).astype(''float32'')
mkl.set_num_threads(3); %time np.dot(A, B, out=C)
11.5 seconds
mkl.set_num_threads(6); %time np.dot(A, B, out=C)
6 seconds
mkl.set_num_threads(12); %time np.dot(A, B, out=C)
3 seconds
mkl.set_num_threads(18); %time np.dot(A, B, out=C)
2.4 seconds
mkl.set_num_threads(24); %time np.dot(A, B, out=C)
3.6 seconds
mkl.set_num_threads(30); %time np.dot(A, B, out=C)
5 seconds
mkl.set_num_threads(36); %time np.dot(A, B, out=C)
5.5 seconds
Haciendo exactamente lo mismo que arriba, pero con doble precisión A, B y C, obtienes: 3 núcleos: 20s, 6 núcleos: 10s, 12 núcleos: 5s, 18 núcleos: 4.3s, 24 núcleos: 3s, 30 núcleos: 2.8 s, 36 núcleos: 2.8s.
El aumento de la velocidad para los puntos flotantes de precisión simple parece estar asociado con fallas de caché. Para 28 core run, aquí está la salida de perf. Para precisión simple:
perf stat -e task-clock,cycles,instructions,cache-references,cache-misses ./ptestf.py
631,301,854 cache-misses # 31.478 % of all cache refs
Y doble precisión:
93,087,703 cache-misses # 5.164 % of all cache refs
C biblioteca compartida, compilada con
/opt/intel/bin/icc -o comp_sgemm_mkl.so -openmp -mkl sgem_lib.c -lm -lirc -O3 -fPIC -shared -std=c99 -vec-report1 -xhost -I/opt/intel/composer/mkl/include
#include <stdio.h>
#include <stdlib.h>
#include "mkl.h"
void comp_sgemm_mkl(int m, int n, int k, float *A, float *B, float *C);
void comp_sgemm_mkl(int m, int n, int k, float *A, float *B, float *C)
{
int i, j;
float alpha, beta;
alpha = 1.0; beta = 0.0;
cblas_sgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans,
m, n, k, alpha, A, k, B, n, beta, C, n);
}
Función de envoltura de Python, que llama a la biblioteca compilada anterior:
def comp_sgemm_mkl(A, B, out=None):
lib = CDLL(omplib)
lib.cblas_sgemm_mkl.argtypes = [c_int, c_int, c_int,
np.ctypeslib.ndpointer(dtype=np.float32, ndim=2),
np.ctypeslib.ndpointer(dtype=np.float32, ndim=2),
np.ctypeslib.ndpointer(dtype=np.float32, ndim=2)]
lib.comp_sgemm_mkl.restype = c_void_p
m = A.shape[0]
n = B.shape[0]
k = B.shape[1]
if np.isfortran(A):
raise ValueError(''Fortran array'')
if m != n:
raise ValueError(''Wrong matrix dimensions'')
if out is None:
out = np.empty((m,k), np.float32)
lib.comp_sgemm_mkl(m, n, k, A, B, out)
Sin embargo, las llamadas explícitas de un binario compilado en C que llama a cblas_sgemm / cblas_dgemm de MKL, con arrays asignados a través de malloc en C, dan un rendimiento casi 2 veces mejor en comparación con el código python, es decir, la llamada numpy.dot. Además, NO se observa el efecto de la degradación del rendimiento al aumentar el número de núcleos. El mejor rendimiento fue 900 ms para la multiplicación de matrices de precisión simple y se logró al usar los 36 núcleos físicos a través de mkl_set_num_cores y ejecutar el código C con numactl --interleave = all.
¿Quizás alguna herramienta o consejo sofisticado para perfilar / inspeccionar / comprender esta situación aún más? Cualquier material de lectura es muy apreciado también.
ACTUALIZACIÓN Siguiendo el consejo de @Hristo Iliev, ejecutar numactl --interleave = all ./ipython no cambió los tiempos (dentro del ruido), pero mejora los tiempos de ejecución binarios de C puro.