Notas para contenedor de docker:

Comando de docker para ejecución de la nota de forma local:

nota: cambiar <ruta a mi directorio> por la ruta de directorio que se desea mapear a /datos dentro del contenedor de docker.

docker run --rm -v <ruta a mi directorio>:/datos --cap-add SYS_ADMIN --privileged --name jupyterlab-numerical -p 8888:8888 -d palmoreck/jupyterlab_numerical:1.1.0

password para jupyterlab: qwerty

Detener el contenedor de docker:

docker stop jupyterlab_numerical

Documentación de la imagen de docker palmoreck/jupyterlab_numerical:1.1.0 en liga.

Para ejecutar el comando de linux perf, que se revisará en esta nota, obsérvese que es necesario correr el contenedor de docker descrito antes con las flags --cap-add SYS_ADMIN y --privileged. Además, sólo es posible obtener las métricas descritas en esta nota vía perf con un host que tenga un sistema operativo ubuntu


Nota generada a partir de liga, liga2

3.1 El cómputo matricial y el álgebra lineal. Vectorización, BLAS y el uso del caché eficientemente.

El cómputo matricial está construído sobre una jerarquía de operaciones del álgebra lineal:

  • Productos punto involucran operaciones escalares de suma y multiplicación (nivel BLAS 1).

  • La multiplicación matriz-vector está hecha de productos punto (nivel BLAS 2).

  • La multiplicación matriz-matriz utiliza colecciones de productos matriz-vector (nivel BLAS 3).

Las operaciones anteriores se describen en el álgebra lineal con la teoría de espacios vectoriales pero también es posible describirlas en una forma algorítmica. Ambas descripciones se complementan una a la otra.

Manejaremos nombres que en el Linear Algebra Package: LAPACK son utilizados para denotar operaciones con escalares, vectores o matrices. Ver Reference-LAPACK / lapack para su github.

Nivel 1 de BLAS

Operación del producto interno estándar o producto punto

Consideramos $x,y \in \mathbb{R}^n$. El producto punto entre $x$ y $y$ es $c = x^Ty = \displaystyle \sum_{i=1}^n x_iy_i$.

Ejemplo y algoritmo del producto punto:


In [1]:
c=0
n=5
x=[-1]*n
y=[1.5]*n

for i in range(n):
    c += x[i]*y[i]

In [2]:
c


Out[2]:
-7.5

Obs:

  • El producto punto de dos $n$-vectores involucran $n$ multiplicaciones y $n$ sumas para un total de $2n$ operaciones o floating point operations per second (flops)*. Usamos la notación $\mathcal{O}(\cdot)$ para escribir que el producto punto es $\mathcal{O}(n)$ y se lee "de orden $n$ o proporcional a $n$" para indicar que la cantidad de trabajo tiene un comportamiento lineal con la dimensión $n$. También tal cantidad de trabajo opera sobre una cantidad lineal de datos.

*Los flops que realiza un algoritmo es una forma de cuantificar el volumen de trabajo asociado con un cálculo. Un flop es una operación de punto flotante: suma, multiplicación o división. Por ejemplo, en la línea:

C[i][j] = C[i][j] + A[i][k]*B[k][j]

se realizan $2$ flops. Los flops sólo representan una componente para categorizar a los algoritmos de acuerdo al trabajo que realizan, otras componentes son la transferencia o movimientos de datos, data movement/motion, ejecución secuencial o en paralelo y el data locality y data reuse que realizan.

  • En LAPACK encontramos sdot, ddot, cdotu y zdotu para descripción de las funciones/subrutinas escritas en Fortran del producto punto en los casos de precisión simple, doble o números complejos respectivamente.

Operación saxpy

Consideramos $\alpha \in \mathbb{R}, x,y \in \mathbb{R}^n$. El nombre lo recibe por scalar alpha x plus y. En LAPACK saxpy se escribe en forma update:

$$y=\alpha x + y \therefore y_i = \alpha x_i + y_i \forall i=1,...,n$$

Obs:

  • El símbolo $=$ no se utiliza como igualdad de expresiones sino para denotar asignación (como en computación al escribir un algoritmo).

  • También encontramos en LAPACK caxpy o daxpy para el caso complejo y para números en doble precisión respectivamente.

  • Ésta operación realiza un trabajo de $\mathcal{O}(n)$ sobre una cantidad de datos $\mathcal{O}(n)$.

Ejemplo y algoritmo de saxpy:


In [3]:
alpha=2
n=5
x=[-2]*n
y=[0]*n

for i in range(n):
    y[i] += alpha*x[i]

In [4]:
y


Out[4]:
[-4, -4, -4, -4, -4]

o en una forma update:


In [5]:
alpha=2
n=5
x=[-2]*n
y=[3,4,-1,0,1]

for i in range(n):
    y[i] += alpha*x[i]

In [6]:
y


Out[6]:
[-1, 0, -5, -4, -3]

Comentario: La operación de producto punto y saxpy son algoritmos catalogados como de nivel BLAS 1, ver BLAS: Basic Linear Algebra Subprograms. Éstos algoritmos se caracterizan por involucrar una cantidad de trabajo lineal sobre una cantidad lineal de datos. Ver level 1 para más ejemplos de este tipo de algoritmos.

Nivel 2 de BLAS

Multiplicación matriz-vector

Consideramos $A \in \mathbb{R}^{m \times n}, x \in \mathbb{R}^n, y \in \mathbb{R}^m$. La operación $y = y + Ax$ es una operación generalizada saxpy, por ello se denomina gaxpy pero en LAPACK podemos encontrarla con nombres como sgemv, dgemv, cgemv o zgemv para los casos de precisión simple, doble o números complejos respectivamente. Hay diferentes formas de visualizar y escribir el algoritmo de multiplicación matriz-vector. Por ejemplo para una matriz $A$ con entradas:


In [7]:
m=2
n=5
A=[[1.2]*n if i%2==0 else [1]*n for i in range(m)]

In [8]:
A


Out[8]:
[[1.2, 1.2, 1.2, 1.2, 1.2], [1, 1, 1, 1, 1]]

In [9]:
A[0][0]


Out[9]:
1.2

In [10]:
A[1][n-1]


Out[10]:
1

se tiene:

 -) Algoritmo gaxpy row oriented

Ejemplo


In [11]:
x=[2]*n
y=[0]*m
for i in range(m):
    for j in range(n):
        y[i]+=A[i][j]*x[j]

In [12]:
y


Out[12]:
[12.0, 10]

Si $y$ tiene valores distintos de $0$, se realiza un update:


In [13]:
x=[2]*n
y=[-1]*m
for i in range(m):
    for j in range(n):
        y[i]+=A[i][j]*x[j]

In [14]:
y


Out[14]:
[11.0, 9]

Comentarios:

  • En la versión row oriented del algoritmo gaxpy, el inner loop realiza productos punto entre el $i$-ésimo renglón de $A$ y el vector $x$. Se realizan $m$ productos punto $A[i,:]^Tx$:
for i in range(m)
    y[i]+=A[i,:]*x #producto punto

donde: $A[i,:]$ es el $i$-ésimo renglón de $A$. Así podemos reescribir de forma más compacta este algoritmo.

  • Obsérvese que el acceso a la matriz $A$ del algoritmo gaxpy row oriented es por renglón.

También puede escribirse al algoritmo gaxpy en una forma orientada por columnas:

-) Algoritmo gaxpy column oriented

Este algoritmo ayuda a visualizar al producto matriz-vector como una combinación lineal de las columnas de $A$:

$$Ax = \displaystyle \sum_{j=1}^n A_jx_j$$

con $A_j$ la $j$-ésima columna de $A$.

Ejemplo


In [15]:
x=[2]*n
y=[0]*m
for j in range(n):
    for i in range(m):
        y[i]+=A[i][j]*x[j]

In [16]:
y


Out[16]:
[12.0, 10]

Comentarios:

  • El algoritmo de multiplicación matriz-vector (versión row o column oriented) involucra $\mathcal{O}(mn)$ operaciones o una cantidad cuadrática de trabajo, que podemos entender como "si duplicamos cada dimensión de $A$ entonces la cantidad de trabajo se incrementa por un factor de $4$". Tal número de operaciones trabajan sobre una matriz o sobre una cantidad cuadrática de datos. A los algoritmos que realizan una cantidad cuadrática de trabajo sobre una cantidad cuadrática de datos se les cataloga de nivel BLAS 2. Ver level 2 para más ejemplos de algoritmos en el álgebra lineal en esta categoría.

  • En el algoritmo gaxpy column oriented el acceso a la matriz $A$ es por columna.

  • La versión column oriented se puede analizar desde el punto de vista puramente algorítmico como un intercambio entre las líneas con los índices $i$ y $j$ de cada loop y un acceso a los datos de la matriz por columna. O bien, se puede analizar desde el álgebra lineal indicando que el vector $y$ está en el espacio generado por las columnas de $A$ y cuyas coordenadas son dadas por las entradas del vector $x$:

Una ejemplo de visualización del espacio generado por las columnas de $A \in \mathbb{R}^{3 \times 2}$, llamado rango o imagen de $A$, $Im(A)$, es el siguiente:

En este dibujo los vectores $b, r(x) \in \mathbb{R}^3$ no están en $Im(A) \subset \mathbb{R}^3$ pero $Ax$ en azul sí. $Im(A)$ en el dibujo es un plano en el espacio $\mathbb{R}^3$.

  • Obsérvese que el inner loop de la versión column oriented en gaxpy es un saxpy en la que el escalar está dado por una entrada de $x$. Esto lo podemos escribir de forma explícita definiendo $A[:,j]$ a la $j$-ésima columna de $A$ por lo que $A = [A[:,1] | A[:,2] | \dots | A[:,n]]$, entonces:
for j in range(n)
    y+=A[:,j] * x[j]

sin embargo como hemos visto en Python con su implementación más común CPython, no es posible realizar tal indexado:


In [17]:
x=[2]*n
y=[0]*m
for j in range(n):
    y+=A[:,j]*x[j]


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-17-93f449af9194> in <module>
      2 y=[0]*m
      3 for j in range(n):
----> 4     y+=A[:,j]*x[j]

TypeError: list indices must be integers or slices, not tuple

a menos que incorporemos alguna paquetería que permita la vectorización y el uso de índices para extracción de columnas (o renglones) de $A$. Entre las razones que encontramos en Python el no soporte para la vectorización está que el bytecode de Python no está optimizado para vectorización. Un ejemplo de un paquete que permite realizar operaciones de forma vectorizada es numpy:


In [18]:
import numpy as np

In [19]:
x = 2*np.ones(n)
y = np.zeros(m)

In [20]:
x


Out[20]:
array([2., 2., 2., 2., 2.])

In [21]:
y


Out[21]:
array([0., 0.])

In [23]:
A=np.array([[1.2,1.2,1.2,1.2,1.2],[1,1,1,1,1]])

In [24]:
A


Out[24]:
array([[1.2, 1.2, 1.2, 1.2, 1.2],
       [1. , 1. , 1. , 1. , 1. ]])

In [25]:
for j in range(n):
    y+=A[:,j]*x[j]

In [26]:
y


Out[26]:
array([12., 10.])

Asimismo, el algoritmo gaxpy row oriented puede escribirse de forma más compacta haciendo uso de la definición de producto punto estándar: $x^Ty$ para dos vectores columna $x$ y $y$. En el caso de una matriz $A$ se tiene:

for i=1:m
    y[i]+=A[i,:]^T*x

donde: $A[i,:]$ es el $i$-ésimo renglón de $A$. En Python:


In [76]:
x = 2*np.ones(n)
y = np.zeros(m)
A=np.array([[1.2,1.2,1.2,1.2,1.2],[1,1,1,1,1]])

In [81]:
for i in range(m):
    y[i]+=A[i,:].dot(x)

In [82]:
y


Out[82]:
array([12., 10.])

en donde se utilizó la función numpy.dot de numpy.

Comentarios:

  • La vectorización como se describe en 2.1.Un_poco_de_historia_y_generalidades, es una herramienta que tenemos a nuestra disposición para escribir programas de alto rendimiento. La vectorización incrementa* el número de instrucciones por ciclo IPC para procesadores que soportan el Single Instruction Multiple Data (SIMD) que encontramos en la taxonomía de Flynn (ver liga). Como ejemplo de tales procesadores están los procesadores vectoriales o en arreglo, ver liga. Obsérvese en liga que numpy soporta tales operaciones.

*El incremento en IPC vía la vectorización se enfoca a que las instrucciones tiendan a completar trabajo útil por ciclo pues podría darse la situación en la que se tiene: a high rate of instructions, but a low rate of actual work completed. Recuérdese que un ciclo involucra los pasos de leer una instrucción, determinar acciones a realizar por tal instrucción y ejecutar las acciones, ver liga.

Perf

En Linux existe la herramienta perf que nos ayuda a calcular métricas de desempeño de la CPU o de los cores. Para la ejecución de los siguientes ejemplos es indispensable correr el contenedor de docker descrito al inicio de la nota en un sistema ubuntu.

Ejemplo de cálculo de la norma $2$ al cuadrado de un vector


In [27]:
%%file norm_square.py
n=10**5
vector=list(range(n))
norm=0
for v in vector:
    norm+=v*v


Overwriting norm_square.py

In [28]:
%%bash
sudo perf stat -S -e cycles,instructions,cache-references,cache-misses -r 20 python3 norm_square.py


 Performance counter stats for 'python3 norm_square.py' (20 runs):

         112969642      cycles                                                        ( +-  0.36% )
         217301216      instructions              #    1.92  insn per cycle           ( +-  0.73% )
           1137719      cache-references                                              ( +-  0.92% )
             58733      cache-misses              #    5.162 % of all cache refs      ( +-  2.07% )

       0.030025947 seconds time elapsed                                          ( +-  1.22% )

Comentarios:

  • Con perf se repiten las mediciones utilizando la flag -r. Con la flag -e se enlistan las métricas a calcular. -S llama a sync antes de iniciar la ejecución del programa.

  • En el ejemplo anterior además de los ciclos e instrucciones se calculan los cache references y los cache misses. Ver 2.1.Un_poco_de_historia_y_generalidades para definiciones de cache references y misses. Esencialmente el sistema de memoria buscó el número que aparece en cache-references por datos o instrucciones en el caché y de éstas búsquedas, en cache-misses, se repora el número (y porcentaje) de búsquedas fallidas que no estuvieron en memoria.

  • Un factor que incrementa el número de cache-misses es la fragmentación de datos, ver Fragmentation). La fragmentación incrementa el número de transferencias de memoria hacia la CPU y además imposibilita la vectorización porque el caché no está lleno. El caché puede llenarse sólo al tener bloques contiguos de memoria alojados para los datos (recuérdese que la interconexión o bus sólo puede mover chunks de memoria contigua, ver 2.1.Un_poco_de_historia_y_generalidades). Python en su implementación común CPython, tiene data fragmentation por ejemplo al usar listas. Las listas de Python alojan locaciones donde se pueden encontrar los datos y no los datos en sí. Al utilizar tales estructuras para operaciones matriciales el runtime de Python debe realizar lookups por índices y al no encontrarse de forma contigua los datos, el overhead en la transferencia de datos es mayor lo que causa que se tengan mayor número de cache-misses y que los cores tengan que esperar hasta que los datos estén disponibles en el caché.

  • Obsérvese que se reporta el IPC al lado de instructions con nombre insn per cycle. Un alto IPC indica una alta transferencia de instrucciones de la unidad de memoria hacia la unidad de cómputo y un menor IPC indica más stall cycles, ver pipeline stall. Sin embargo como se mencionó antes, se debe evaluar si a high rate of instructions indica un high rate of actual work completed (p.ej. un loop tiene una alta tasa de IPC pero no siempre se realiza trabajo útil). Ver sección CPU statistics en la liga. perf también tiene una métrica para medir el número de ciclos que se utilizaron para esperar a ejecutar instrucciones (los cores están stalled). Las métricas son: stalled-cycles-frontend y stalled-cycles-backend.

También podemos obtener las estadísticas por core. Obsérvese que la salida anterior de perf calculó el IPC a partir del número de instrucciones y ciclos ahí reportados. El mismo cálculo se realiza a continuación:


In [29]:
%%bash
sudo perf stat -S -a --per-core -e cycles,instructions,cache-references,cache-misses -r 20 python3 norm_square.py


 Performance counter stats for 'system wide' (20 runs):

S0-C0           2          111124270      cycles                                                      
S0-C0           2          214054021      instructions              #    1.93  insn per cycle         
S0-C0           2            1196096      cache-references                                            
S0-C0           2              60653      cache-misses              #    5.071 % of all cache refs    
S0-C1           2          111521501      cycles                                                      
S0-C1           2           26249177      instructions              #    0.24  insn per cycle         
S0-C1           2               2337      cache-references                                            
S0-C1           2               1379      cache-misses              #   59.007 % of all cache refs    
S0-C2           2            1184308      cycles                                                      
S0-C2           2             313203      instructions              #    0.26  insn per cycle         
S0-C2           2               9650      cache-references                                            
S0-C2           2               4572      cache-misses              #   47.378 % of all cache refs    
S0-C3           2            1500849      cycles                                                      
S0-C3           2             482866      instructions              #    0.32  insn per cycle         
S0-C3           2              29157      cache-references                                            
S0-C3           2               6797      cache-misses              #   23.312 % of all cache refs    

       0.030082184 seconds time elapsed                                          ( +-  0.58% )

Otras métricas pueden ser obtenidas si ejecutamos perf sólo con la flag -r:


In [30]:
%%bash
sudo perf stat -S -r 20 python3 norm_square.py


 Performance counter stats for 'python3 norm_square.py' (20 runs):

         30.059631      task-clock (msec)         #    0.994 CPUs utilized            ( +-  1.01% )
                 0      context-switches          #    0.008 K/sec                    ( +- 39.74% )
                 0      cpu-migrations            #    0.000 K/sec                  
              2076      page-faults               #    0.069 M/sec                    ( +-  0.02% )
         111057933      cycles                    #    3.695 GHz                      ( +-  0.23% )
         214464593      instructions              #    1.93  insn per cycle           ( +-  0.45% )
          45107448      branches                  # 1500.599 M/sec                    ( +-  0.52% )
            669749      branch-misses             #    1.48% of all branches          ( +-  0.11% )

       0.030241277 seconds time elapsed                                          ( +-  1.02% )

Comentarios:

  • La métrica de task-clock indica cuántos clock cycles tomó nuestra tarea y se reporta en milisegundos. Obsérvese que también se indica cuántos CPU's fueron utilizados. En el output anterior no es exactamente 1 pues el programa no sólo involucra trabajo de CPU sino también de alojamiento de memoria.

  • context-switches y cpu-migrations indican cuánto se detuvo el programa para realizar:

    • operaciones relacionadas con el kernel del sistema (por ejemplo I/O).
    • ejecución de otras aplicaciones.
    • alojamiento de la ejecución en un core distinto (lo que ocasiona que haya movimiento de datos o instrucciones hacia el caché de otro core).

la idea es que nuestro programa tenga un número pequeño de éstas métricas.

  • page-faults se relaciona con el alojamiento de memoria a partir del kernel del sistema operativo. Ocasiona que se detenga la ejecución del programa y por tanto deseamos que ésta métrica no sea muy grande. Ver Page fault.

  • Ver 2.1.Un_poco_de_historia_y_generalidades para el branching. Esencialmente interesa que el número de branch misses sea pequeño.

Podemos generar la salida anterior por core:


In [31]:
%%bash
sudo perf stat -S -a --per-core -r 20 python3 norm_square.py


 Performance counter stats for 'system wide' (20 runs):

S0-C0           2          59.699698      cpu-clock (msec)          #    1.911 CPUs utilized          
S0-C0           2                  5      context-switches          #    0.084 K/sec                  
S0-C0           2                  0      cpu-migrations            #    0.000 K/sec                  
S0-C0           2               2074      page-faults               #    0.035 M/sec                  
S0-C0           2          114151283      cycles                    #    1.912 GHz                    
S0-C0           2          226146639      instructions              #    1.98  insn per cycle         
S0-C0           2           47873603      branches                  #  801.907 M/sec                  
S0-C0           2             667536      branch-misses             #    1.39% of all branches        
S0-C1           2          59.700993      cpu-clock (msec)          #    1.911 CPUs utilized          
S0-C1           2                  4      context-switches          #    0.067 K/sec                  
S0-C1           2                  0      cpu-migrations            #    0.000 K/sec                  
S0-C1           2                  0      page-faults               #    0.000 K/sec                  
S0-C1           2             276727      cycles                    #    0.005 GHz                    
S0-C1           2              75162      instructions              #    0.27  insn per cycle         
S0-C1           2              15164      branches                  #    0.254 M/sec                  
S0-C1           2                628      branch-misses             #    4.14% of all branches        
S0-C2           2          59.700878      cpu-clock (msec)          #    1.911 CPUs utilized          
S0-C2           2                  6      context-switches          #    0.101 K/sec                  
S0-C2           2                  0      cpu-migrations            #    0.000 K/sec                  
S0-C2           2                  0      page-faults               #    0.000 K/sec                  
S0-C2           2            1610430      cycles                    #    0.027 GHz                    
S0-C2           2             406556      instructions              #    0.25  insn per cycle         
S0-C2           2              87378      branches                  #    1.464 M/sec                  
S0-C2           2               2075      branch-misses             #    2.37% of all branches        
S0-C3           2          59.722514      cpu-clock (msec)          #    1.912 CPUs utilized          
S0-C3           2                 14      context-switches          #    0.234 K/sec                  
S0-C3           2                  1      cpu-migrations            #    0.017 K/sec                  
S0-C3           2                  0      page-faults               #    0.000 K/sec                  
S0-C3           2            1381439      cycles                    #    0.023 GHz                    
S0-C3           2             370386      instructions              #    0.27  insn per cycle         
S0-C3           2              66333      branches                  #    1.111 M/sec                  
S0-C3           2               3825      branch-misses             #    5.77% of all branches        

       0.031236297 seconds time elapsed                                          ( +-  2.81% )

Para contrastar las salidas anteriores con un mejor uso del caché y de los cores se utiliza a continuación el paquete de numpy:


In [33]:
%%file norm_square_numpy.py
import numpy as np
n=10**5
vector=np.arange(n)
vector.dot(vector)


Writing norm_square_numpy.py

In [32]:
%%bash
sudo perf stat -S -e cycles,instructions,cache-references,cache-misses -r 20 python3 norm_square_numpy.py


 Performance counter stats for 'python3 norm_square_numpy.py' (20 runs):

        2517939029      cycles                                                        ( +-  0.07% )
        1287233993      instructions              #    0.51  insn per cycle           ( +-  0.07% )
           7472851      cache-references                                              ( +-  0.22% )
            403479      cache-misses              #    5.399 % of all cache refs      ( +-  1.80% )

       0.139254850 seconds time elapsed                                          ( +-  0.36% )

Comentarios:

  • Obsérvese que es más tardado este programa para el número $n$ que se está utilizando en comparación con el programa anterior sin vectorizar.

  • El número de instrucciones es menor al reportado anteriormente y parece tener un mayor número $%$ de cache-misses. Obsérvese que para este ejemplo es más ilustrativo observar las estadísticas por core y realizar conclusiones:


In [33]:
%%bash
sudo perf stat -S -a --per-core -e cycles,instructions,cache-references,cache-misses -r 20 python3 norm_square_numpy.py


 Performance counter stats for 'system wide' (20 runs):

S0-C0           2          569110059      cycles                                                      
S0-C0           2          222173905      instructions              #    0.39  insn per cycle         
S0-C0           2              47498      cache-references                                            
S0-C0           2              12696      cache-misses              #   26.730 % of all cache refs    
S0-C1           2          569620862      cycles                                                      
S0-C1           2          223486329      instructions              #    0.39  insn per cycle         
S0-C1           2              51512      cache-references                                            
S0-C1           2              12984      cache-misses              #   25.206 % of all cache refs    
S0-C2           2          569812750      cycles                                                      
S0-C2           2          223109158      instructions              #    0.39  insn per cycle         
S0-C2           2              44136      cache-references                                            
S0-C2           2               9939      cache-misses              #   22.519 % of all cache refs    
S0-C3           2          818225184      cycles                                                      
S0-C3           2          622681657      instructions              #    0.76  insn per cycle         
S0-C3           2            7436355      cache-references                                            
S0-C3           2             377443      cache-misses              #    5.076 % of all cache refs    

       0.139100270 seconds time elapsed                                          ( +-  0.16% )

Comentarios:

  • Se observa que la métrica IPC está más uniforme a través de los cores lo que indica un mejor load balancing.

  • Además, el $\%$ de cache-misses es menor que en el caso no vectorizado a diferencia del output anterior que indicaba un mayor $\%$ de cache-misses.

También podemos obtener las salidas anteriores de perf al usar numpy:


In [34]:
%%bash
sudo perf stat -S -r 20 python3 norm_square_numpy.py


 Performance counter stats for 'python3 norm_square_numpy.py' (20 runs):

        658.846611      task-clock (msec)         #    4.733 CPUs utilized            ( +-  0.28% )
              1309      context-switches          #    0.002 M/sec                    ( +- 97.65% )
                 1      cpu-migrations            #    0.002 K/sec                    ( +- 14.60% )
              4755      page-faults               #    0.007 M/sec                    ( +-  0.04% )
        2509194053      cycles                    #    3.808 GHz                      ( +-  0.28% )
        1288129520      instructions              #    0.51  insn per cycle           ( +-  0.14% )
         256581259      branches                  #  389.440 M/sec                    ( +-  0.15% )
           6512464      branch-misses             #    2.54% of all branches          ( +-  0.09% )

       0.139189866 seconds time elapsed                                          ( +-  0.27% )


In [35]:
%%bash
sudo perf stat -S -a --per-core -r 20 python3 norm_square_numpy.py


 Performance counter stats for 'system wide' (20 runs):

S0-C0           2         280.327176      cpu-clock (msec)          #    1.997 CPUs utilized          
S0-C0           2                 48      context-switches          #    0.171 K/sec                  
S0-C0           2                  5      cpu-migrations            #    0.018 K/sec                  
S0-C0           2                  4      page-faults               #    0.014 K/sec                  
S0-C0           2          569732162      cycles                    #    2.032 GHz                    
S0-C0           2          222998243      instructions              #    0.39  insn per cycle         
S0-C0           2           41820920      branches                  #  149.186 M/sec                  
S0-C0           2             761701      branch-misses             #    1.82% of all branches        
S0-C1           2         280.362219      cpu-clock (msec)          #    1.997 CPUs utilized          
S0-C1           2                 46      context-switches          #    0.164 K/sec                  
S0-C1           2                  5      cpu-migrations            #    0.018 K/sec                  
S0-C1           2                  0      page-faults               #    0.000 K/sec                  
S0-C1           2          595485979      cycles                    #    2.124 GHz                    
S0-C1           2          252609982      instructions              #    0.42  insn per cycle         
S0-C1           2           47805375      branches                  #  170.513 M/sec                  
S0-C1           2             850921      branch-misses             #    1.78% of all branches        
S0-C2           2         280.362523      cpu-clock (msec)          #    1.997 CPUs utilized          
S0-C2           2                 82      context-switches          #    0.292 K/sec                  
S0-C2           2                  2      cpu-migrations            #    0.007 K/sec                  
S0-C2           2                  1      page-faults               #    0.004 K/sec                  
S0-C2           2          570048514      cycles                    #    2.033 GHz                    
S0-C2           2          221858305      instructions              #    0.39  insn per cycle         
S0-C2           2           41692943      branches                  #  148.711 M/sec                  
S0-C2           2             763207      branch-misses             #    1.83% of all branches        
S0-C3           2         280.375634      cpu-clock (msec)          #    1.997 CPUs utilized          
S0-C3           2                 59      context-switches          #    0.210 K/sec                  
S0-C3           2                  5      cpu-migrations            #    0.018 K/sec                  
S0-C3           2               4744      page-faults               #    0.017 M/sec                  
S0-C3           2          832271740      cycles                    #    2.968 GHz                    
S0-C3           2          625657253      instructions              #    0.75  insn per cycle         
S0-C3           2          132103906      branches                  #  471.168 M/sec                  
S0-C3           2            4270632      branch-misses             #    3.23% of all branches        

       0.140398443 seconds time elapsed                                          ( +-  0.23% )

Regresando al algoritmo gaxpy column oriented...

Utilizaremos perf en lo que sigue para evaluar las métricas de IPC, cache-references, cache-misses y medir el tiempo de ejecución. Además, esto permitirá comparar los tiempos de ejecución del algoritmo gaxpy (nivel BLAS 2), con vectorización y sin vectorización.

Consideramos una matriz $A \in \mathbb{R}^{10^4 \times 10^4}$ con entradas pseudoaleatorias. No se sugiere ejecutar los siguientes ejemplos para máquinas que tengan menos de 8gb de memoria:


In [22]:
np.random.seed(2020)
m=10**4
n=10**4
A=np.random.rand(m,n)
file='A.txt'
np.savetxt(file,A)

Arhivo sin vectorizar y utilizando listas para construir a la matriz y a los vectores:


In [8]:
%%file mult_matrix_vector.py
m=10**4
n=10**4
x=[2.5]*n
y=[0]*m
A = []
file='A.txt'
with open(file,'r') as f:
    for l in f:
        A.append([float(k) for k in l.replace('\n','').replace(' ',',').split(',')])      
for j in range(n):
    for i in range(m):
        y[i]+=A[i][j]*x[j]


Writing mult_matrix_vector.py

Archivo vectorizando con numpy:


In [9]:
%%file mult_matrix_vector_numpy.py
import numpy as np
m=10**4
n=10**4
x = 2.5*np.ones(n)
y = np.zeros(m)
file='A.txt'
A = np.loadtxt(file)
for j in np.arange(n):
    y+=A[:,j]*x[j]


Writing mult_matrix_vector_numpy.py

Caso no vectorizado


In [10]:
%%bash
sudo perf stat -S -e cycles,instructions,cache-references,cache-misses -r 2 python3 mult_matrix_vector.py


 Performance counter stats for 'python3 mult_matrix_vector.py' (2 runs):

      253636293168      cycles                                                        ( +-  2.57% )
      612211438956      instructions              #    2.41  insn per cycle           ( +-  0.80% )
        1021919811      cache-references                                              ( +-  0.04% )
         127117602      cache-misses              #   12.439 % of all cache refs      ( +-  0.56% )

      63.613271875 seconds time elapsed                                          ( +-  2.51% )

Métricas por core:


In [11]:
%%bash
sudo perf stat -S -a --per-core -e cycles,instructions,cache-references,cache-misses -r 2 python3 mult_matrix_vector.py


 Performance counter stats for 'system wide' (2 runs):

S0-C0           2          343119167      cycles                                                      
S0-C0           2           98370360      instructions              #    0.29  insn per cycle         
S0-C0           2            5840329      cache-references                                            
S0-C0           2            1414327      cache-misses              #   24.217 % of all cache refs    
S0-C1           2          528971112      cycles                                                      
S0-C1           2          258058789      instructions              #    0.49  insn per cycle         
S0-C1           2            8051433      cache-references                                            
S0-C1           2            2969543      cache-misses              #   36.882 % of all cache refs    
S0-C2           2        69421643702      cycles                                                      
S0-C2           2       146665430983      instructions              #    2.11  insn per cycle         
S0-C2           2          510280653      cache-references                                            
S0-C2           2           75086567      cache-misses              #   14.715 % of all cache refs    
S0-C3           2       182606290800      cycles                                                      
S0-C3           2       459364489796      instructions              #    2.52  insn per cycle         
S0-C3           2          530605786      cache-references                                            
S0-C3           2           60288944      cache-misses              #   11.362 % of all cache refs    

      61.854651367 seconds time elapsed                                          ( +-  0.61% )

Caso vectorizando


In [12]:
%%bash
sudo perf stat -S -e cycles,instructions,cache-references,cache-misses -r 2 python3 mult_matrix_vector_numpy.py


 Performance counter stats for 'python3 mult_matrix_vector_numpy.py' (2 runs):

      243217588216      cycles                                                        ( +-  1.27% )
      598729874148      instructions              #    2.46  insn per cycle           ( +-  0.42% )
         662503038      cache-references                                              ( +-  1.39% )
          95202746      cache-misses              #   14.370 % of all cache refs      ( +-  0.97% )

      60.488697485 seconds time elapsed                                          ( +-  1.26% )


In [13]:
%%bash
sudo perf stat -S -a --per-core -e cycles,instructions,cache-references,cache-misses -r 2 python3 mult_matrix_vector_numpy.py


 Performance counter stats for 'system wide' (2 runs):

S0-C0           2         2664298731      cycles                                                      
S0-C0           2         1964872569      instructions              #    0.74  insn per cycle         
S0-C0           2           10837071      cache-references                                            
S0-C0           2            4435770      cache-misses              #   40.931 % of all cache refs    
S0-C1           2          964636936      cycles                                                      
S0-C1           2          320553130      instructions              #    0.33  insn per cycle         
S0-C1           2            6043601      cache-references                                            
S0-C1           2            2799592      cache-misses              #   46.323 % of all cache refs    
S0-C2           2         1339127149      cycles                                                      
S0-C2           2          935833922      instructions              #    0.70  insn per cycle         
S0-C2           2            8451552      cache-references                                            
S0-C2           2            2653359      cache-misses              #   31.395 % of all cache refs    
S0-C3           2       305074104555      cycles                                                      
S0-C3           2       606515058789      instructions              #    1.99  insn per cycle         
S0-C3           2          661941985      cache-references                                            
S0-C3           2           94553002      cache-misses              #   14.284 % of all cache refs    

      61.588377630 seconds time elapsed                                          ( +-  2.84% )

Comentarios:

  • No se observa una gran diferencia entre el caso vectorizado y no vectorizado salvo que en el caso vectorizando se tiene un $\%$ mayor de cache-misses. Sería conveniente realizar más repeticiones de esta misma operación o con diferentes operaciones de nivel 2 BLAS para realizar conclusiones más sólidas. Ver level 2 para más ejemplos de algoritmos en el álgebra lineal en esta categoría.

  • Otra opción que se tiene es utilizar la siguiente implementación ya que numpy soporta operaciones del tipo matriz-vector sin necesidad de utilizar un for loop:


In [14]:
%%file mult_matrix_vector_numpy_2.py
import numpy as np
m=10**4
n=10**4
x = 2.5*np.ones(n)
y = np.zeros(m)
file='A.txt'
A = np.loadtxt(file)
y=A@x


Writing mult_matrix_vector_numpy_2.py

In [15]:
%%bash
sudo perf stat -S -e cycles,instructions,cache-references,cache-misses -r 2 python3 mult_matrix_vector_numpy_2.py


 Performance counter stats for 'python3 mult_matrix_vector_numpy_2.py' (2 runs):

      243195411750      cycles                                                        ( +-  1.00% )
      602205576232      instructions              #    2.48  insn per cycle           ( +-  0.23% )
         553013970      cache-references                                              ( +-  0.65% )
          87666482      cache-misses              #   15.852 % of all cache refs      ( +-  0.47% )

      60.022411249 seconds time elapsed                                          ( +-  1.06% )


In [16]:
%%bash
sudo perf stat -S -a --per-core -e cycles,instructions,cache-references,cache-misses -r 2 python3 mult_matrix_vector_numpy_2.py


 Performance counter stats for 'system wide' (2 runs):

S0-C0           2         5768543148      cycles                                                      
S0-C0           2         2652993320      instructions              #    0.46  insn per cycle         
S0-C0           2           13669781      cache-references                                            
S0-C0           2            5700218      cache-misses              #   41.699 % of all cache refs    
S0-C1           2         1341384457      cycles                                                      
S0-C1           2          397736414      instructions              #    0.30  insn per cycle         
S0-C1           2            7366652      cache-references                                            
S0-C1           2            3922098      cache-misses              #   53.241 % of all cache refs    
S0-C2           2         1434196155      cycles                                                      
S0-C2           2          399070532      instructions              #    0.28  insn per cycle         
S0-C2           2            8991081      cache-references                                            
S0-C2           2            4214025      cache-misses              #   46.869 % of all cache refs    
S0-C3           2       250722975414      cycles                                                      
S0-C3           2       598321598562      instructions              #    2.39  insn per cycle         
S0-C3           2          553229600      cache-references                                            
S0-C3           2           83582036      cache-misses              #   15.108 % of all cache refs    

      59.291536937 seconds time elapsed                                          ( +-  0.88% )

O con una versión gaxpy row oriented


In [5]:
%%file mult_matrix_vector_numpy_row_oriented.py
import numpy as np
m=10**4
n=10**4
x = 2.5*np.ones(n)
y = np.zeros(m)
file='A.txt'
A = np.loadtxt(file)
for i in np.arange(m):
    y[i]+=A[i,:].dot(x)


Overwriting mult_matrix_vector_numpy_row_oriented.py

In [6]:
%%bash
sudo perf stat -S -e cycles,instructions,cache-references,cache-misses -r 2 python3 mult_matrix_vector_numpy_row_oriented.py


 Performance counter stats for 'python3 mult_matrix_vector_numpy_row_oriented.py' (2 runs):

      240065888790      cycles                                                        ( +-  0.31% )
      596676621081      instructions              #    2.49  insn per cycle           ( +-  0.28% )
         549617732      cache-references                                              ( +-  0.84% )
          87432788      cache-misses              #   15.908 % of all cache refs      ( +-  0.29% )

      59.657694617 seconds time elapsed                                          ( +-  0.32% )


In [7]:
%%bash
sudo perf stat -S -a --per-core -e cycles,instructions,cache-references,cache-misses -r 2 python3 mult_matrix_vector_numpy_row_oriented.py


 Performance counter stats for 'system wide' (2 runs):

S0-C0           2         2022241353      cycles                                                      
S0-C0           2          749567422      instructions              #    0.37  insn per cycle         
S0-C0           2            5565741      cache-references                                            
S0-C0           2            1418960      cache-misses              #   25.495 % of all cache refs    
S0-C1           2          978643966      cycles                                                      
S0-C1           2          468796681      instructions              #    0.48  insn per cycle         
S0-C1           2            5124516      cache-references                                            
S0-C1           2            2519200      cache-misses              #   49.160 % of all cache refs    
S0-C2           2         1827829093      cycles                                                      
S0-C2           2         1157668329      instructions              #    0.63  insn per cycle         
S0-C2           2            7050536      cache-references                                            
S0-C2           2            4433714      cache-misses              #   62.885 % of all cache refs    
S0-C3           2       253080374948      cycles                                                      
S0-C3           2       606973949249      instructions              #    2.40  insn per cycle         
S0-C3           2          559644223      cache-references                                            
S0-C3           2           87449250      cache-misses              #   15.626 % of all cache refs    

      60.039748269 seconds time elapsed                                          ( +-  1.13% )

Nivel 3 de BLAS

Otras comparaciones que podemos realizar entre los tiempos de ejecución, cache-references y cache-misses utilizando la versión vectorizada y no vectorizada, es con el algoritmo de multiplicación de matrices $C = C + AB$ con $A \in \mathbb{R}^{m \times r}, B \in \mathbb{R}^{r \times n}, C \in \mathbb{R}^{m \times n}$. Este algoritmo se cataloga como de nivel 3 de BLAS pues realiza una cantidad de trabajo cúbica sobre una cantidad cuadrática de datos. Ver level 3 para más ejemplos de algoritmos en el álgebra lineal en esta categoría.

Multiplicación de matrices

En LAPACK encontramos al algoritmo de multiplicación matricial con nombres como sgemm, dgemm, cgemm y zgemm para los casos de precisión simple, doble o números complejos respectivamente.

El algoritmo de multiplicación de matrices puede escribirse en diferentes versiones. Por ejemplo la versión i,j,k es:

for i in range(m):
    for j in range(n):
        for k in range(r):
            C[i][j]+=A[i][k]*B[k][j]

y la versión j,k,i es:

for j in range(n):
    for k in range(r):
        for i in range(m):
            C[i][j]+=A[i][k]*B[k][j]

Comentarios:

  • Cualquiera de las versiones (hay $3!$ de ellas) involucra una cantidad de trabajo del orden $\mathcal{O}(mnr)$, la cual es cúbica. Es posible interpretar ésta cantidad de trabajo con una frase del tipo "si duplicamos cada dimensión de $A$ entonces la cantidad de trabajo se incrementa por un factor de $8$".

  • Distintas versiones tienen distinto patrón de acceso a los datos de $A, B$ y $C$. Por ejemplo, para la variante i,j,k el inner loop realiza un producto punto que requiere acceso a renglones de $A$ y columnas de $B$ en una forma izquierda-derecha y abajo-arriba respectivamente. La variante j,k,i involucra una operación saxpy en el inner loop y acceso por columnas a la matriz $C$ y a $A$. Un resúmen de lo anterior se presenta en el siguiente cuadro:

Comparación entre versiones vectorizadas y no vectorizadas

Consideramos matrices $A, B \in \mathbb{R}^{10^4 \times 10^4}$ con entradas pseudoaleatorias. No se sugiere ejecutar los siguientes ejemplos para máquinas que tengan menos de 8gb de memoria. Si se ejecutó el ejemplo de gaxpy de nivel 2 de BLAS, no es necesario volver a ejecutar la siguiente celda para crear a la matriz A


In [21]:
np.random.seed(2020)
m=10**4
r=10**4

A=np.random.rand(m,r)
fileA='A.txt'
np.savetxt(fileA,A)

In [44]:
np.random.seed(2021)
r=10**4
n=10**4

B=np.random.rand(r,n)
fileB='B.txt'
np.savetxt(fileB,B)

In [45]:
%%file mult_matrix_matrix.py
m=10**4
r=10**4
n=10**4

A = []
B = []
fileA='A.txt'
fileB='B.txt'

with open(fileA,'r') as f:
    for l in f:
        A.append([float(k) for k in l.replace('\n','').replace(' ',',').split(',')]) 
        
with open(fileB,'r') as f:
    for l in f:
        B.append([float(k) for k in l.replace('\n','').replace(' ',',').split(',')])  

C=[[0]*n for i in range(m)]

for i in range(m):
    for j in range(n):
        for k in range(r):
            C[i][j]+=A[i][k]*B[k][j]


Writing mult_matrix_matrix.py

In [46]:
%%file mult_matrix_matrix_numpy.py
import numpy as np
m=10**4
r=10**4
n=10**4

fileA='A.txt'
fileB='B.txt'
A = np.loadtxt(fileA)
B = np.loadtxt(fileB)
C = A@B


Writing mult_matrix_matrix_numpy.py

In [47]:
%%bash
sudo perf stat -S -a --per-core -e cycles,instructions,cache-references,cache-misses -r 1 python3 mult_matrix_matrix.py


Process is terminated.

... 36 min y todavía no terminaba... cambiar a r 1


In [1]:
%%bash
sudo perf stat -S -a -e cycles,instructions,cache-references,cache-misses -r 1 python3 mult_matrix_matrix.py


Process is terminated.

In [3]:
%%bash
sudo perf stat -S -a -e cycles,instructions,cache-references,cache-misses -r 2 python3 mult_matrix_matrix_numpy.py


 Performance counter stats for 'system wide' (2 runs):

      836044127523      cycles                                                        ( +-  0.57% )
     1686463020251      instructions              #    2.02  insn per cycle           ( +-  0.43% )
        4086628234      cache-references                                              ( +-  1.72% )
         882062956      cache-misses              #   21.584 % of all cache refs      ( +-  4.13% )

     130.609360500 seconds time elapsed                                          ( +-  0.66% )


In [2]:
%%bash
sudo perf stat -S -a --per-core -e cycles,instructions,cache-references,cache-misses -r 2 python3 mult_matrix_matrix_numpy.py


 Performance counter stats for 'system wide' (2 runs):

S0-C0           2        89616044167      cycles                                                      
S0-C0           2       121446222500      instructions              #    1.36  insn per cycle         
S0-C0           2          811435068      cache-references                                            
S0-C0           2          194368638      cache-misses              #   23.954 % of all cache refs    
S0-C1           2        88898510819      cycles                                                      
S0-C1           2       121389910135      instructions              #    1.37  insn per cycle         
S0-C1           2          722839867      cache-references                                            
S0-C1           2          162558289      cache-misses              #   22.489 % of all cache refs    
S0-C2           2       563600069053      cycles                                                      
S0-C2           2      1280855473637      instructions              #    2.27  insn per cycle         
S0-C2           2         1782304769      cache-references                                            
S0-C2           2          317385129      cache-misses              #   17.808 % of all cache refs    
S0-C3           2       103452663605      cycles                                                      
S0-C3           2       155023368415      instructions              #    1.50  insn per cycle         
S0-C3           2          805500520      cache-references                                            
S0-C3           2          192465138      cache-misses              #   23.894 % of all cache refs    

     131.489034132 seconds time elapsed                                          ( +-  0.71% )

Otras versiones de la multiplicación de matrices apoyándose de las formas (i,j,k), (j,k,i),...

Si utilizamos la versión i,j,k podemos reescribir la multiplicación de matrices en una forma:

for i in range(m):
    for j in range(n)
        C[i][j]+= A[i,:].dot(B[:,j])

y hemos vectorizado el inner loop para usar productos punto que es una operación nivel 1 de BLAS.

Además numpy nos provee de funcionalidad para reescribir ésta forma de productos punto en una como sigue:

for i in range(m):
    C[i,:]+= A[i,:]@B

Obs: obsérvese que en esta reescritura en el loop se realizan $m$ operaciones gaxpy de nivel 2. Esta versión como se verá a continuación es más rápida que la que utiliza operaciones de nivel 1 de BLAS:

Comparación de versiones de multiplicación de matrices utilizando nivel 1 vs nivel 2 de BLAS

Ejemplo

Utilizamos matrices $A, B \in \mathbb{R}^{10^3 \times 10^3}$ pseudoaleatorias:


In [19]:
np.random.seed(2020)
m=10**3
r=10**3

A=np.random.rand(m,r)
fileA_10_3='A_10_3.txt'
np.savetxt(fileA_10_3,A)

In [20]:
np.random.seed(2021)
m=10**3
r=10**3

B=np.random.rand(m,r)
fileB_10_3='B_10_3.txt'
np.savetxt(fileB_10_3,B)

In [23]:
%%file mult_matrix_matrix_numpy_dot_product.py
import numpy as np
m=10**3
n=10**3

fileA_10_3='A_10_3.txt'
fileB_10_3='B_10_3.txt'
A = np.loadtxt(fileA_10_3)
B = np.loadtxt(fileB_10_3)
C = np.zeros((m,n))
for i in np.arange(m):
        for j in np.arange(n):
                C[i][j]+= A[i,:].dot(B[:,j])


Writing mult_matrix_matrix_numpy_dot_product.py

In [26]:
%%bash
sudo perf stat -S -a -e cycles,instructions,cache-references,cache-misses -r 5 python3 mult_matrix_matrix_numpy_dot_product.py


 Performance counter stats for 'system wide' (5 runs):

       17727880724      cycles                                                        ( +-  2.67% )
       31196800089      instructions              #    1.76  insn per cycle           ( +-  0.18% )
         174120561      cache-references                                              ( +-  1.57% )
          14645042      cache-misses              #    8.411 % of all cache refs      ( +-  8.98% )

       3.817488998 seconds time elapsed                                          ( +-  0.55% )


In [27]:
%%bash
sudo perf stat -S -a --per-core -e cycles,instructions,cache-references,cache-misses -r 5 python3 mult_matrix_matrix_numpy_dot_product.py


 Performance counter stats for 'system wide' (5 runs):

S0-C0           2          582438322      cycles                                                      
S0-C0           2          225885082      instructions              #    0.39  insn per cycle         
S0-C0           2             274724      cache-references                                            
S0-C0           2             130031      cache-misses              #   47.332 % of all cache refs    
S0-C1           2          642407408      cycles                                                      
S0-C1           2          272043994      instructions              #    0.42  insn per cycle         
S0-C1           2             546749      cache-references                                            
S0-C1           2             324188      cache-misses              #   59.294 % of all cache refs    
S0-C2           2          596216226      cycles                                                      
S0-C2           2          226892804      instructions              #    0.38  insn per cycle         
S0-C2           2             446956      cache-references                                            
S0-C2           2             184440      cache-misses              #   41.266 % of all cache refs    
S0-C3           2        17956845889      cycles                                                      
S0-C3           2        30574523485      instructions              #    1.70  insn per cycle         
S0-C3           2          185655560      cache-references                                            
S0-C3           2           15107439      cache-misses              #    8.137 % of all cache refs    

       3.861269417 seconds time elapsed                                          ( +-  0.93% )


In [30]:
%%file mult_matrix_matrix_numpy_dot_product_gaxpy.py
import numpy as np
m=10**3
n=10**3
fileA_10_3='A_10_3.txt'
fileB_10_3='B_10_3.txt'
A = np.loadtxt(fileA_10_3)
B = np.loadtxt(fileB_10_3)
C = np.zeros((m,n))
for i in np.arange(m):
    C[i,:] = A[i,:]@B


Overwriting mult_matrix_matrix_numpy_dot_product_gaxpy.py

In [31]:
%%bash
sudo perf stat -S -a -e cycles,instructions,cache-references,cache-misses -r 5 python3 mult_matrix_matrix_numpy_dot_product_gaxpy.py


 Performance counter stats for 'system wide' (5 runs):

       11010075589      cycles                                                        ( +-  0.66% )
       14922627515      instructions              #    1.36  insn per cycle           ( +-  0.26% )
         142059490      cache-references                                              ( +-  1.24% )
           8572872      cache-misses              #    6.035 % of all cache refs      ( +- 11.05% )

       1.455285799 seconds time elapsed                                          ( +-  0.44% )


In [32]:
%%bash
sudo perf stat -S -a --per-core -e cycles,instructions,cache-references,cache-misses -r 5 python3 mult_matrix_matrix_numpy_dot_product_gaxpy.py


 Performance counter stats for 'system wide' (5 runs):

S0-C0           2         1448177294      cycles                                                      
S0-C0           2          623468047      instructions              #    0.43  insn per cycle         
S0-C0           2           29581790      cache-references                                            
S0-C0           2             575180      cache-misses              #    1.944 % of all cache refs    
S0-C1           2         1477272782      cycles                                                      
S0-C1           2          656096587      instructions              #    0.44  insn per cycle         
S0-C1           2           29881953      cache-references                                            
S0-C1           2             608210      cache-misses              #    2.035 % of all cache refs    
S0-C2           2         1457641798      cycles                                                      
S0-C2           2          633875919      instructions              #    0.43  insn per cycle         
S0-C2           2           24959423      cache-references                                            
S0-C2           2            1281191      cache-misses              #    5.133 % of all cache refs    
S0-C3           2         6518413194      cycles                                                      
S0-C3           2        12982975813      instructions              #    1.99  insn per cycle         
S0-C3           2           53648618      cache-references                                            
S0-C3           2            2954550      cache-misses              #    5.507 % of all cache refs    

       1.449412595 seconds time elapsed                                          ( +-  0.43% )

Comentarios:

  • Obsérvese que en las implementaciones anteriores ambas versiones utilizan la vectorización mediante numpy.

  • A continuación se muestra una tabla que presenta el número de flops realizados por distintas operaciones del álgebra lineal:

  • Como se observa en la tabla anterior, gaxpy (nivel 2 de BLAS) realiza más flops que un producto punto. Aún así, los tiempos medidos con perf indican que la versión de gaxpy es más rápida que la versión con productos punto (nivel 1 de BLAS) para la multiplicación de matrices ya que aprovecha mejor el data reuse y data locality. Esto se logra pues gaxpy disminuye el número de cache misses y por tanto el tráfico hacia y desde el caché, el llamado data movement/motion.

Blocking algorithms para multiplicación de matrices

El data reuse y el data locality como se vio en el ejemplo pasado se incrementa al utilizar operaciones de nivel BLAS mayores. También disminuye el data motion hacia y desde el caché lo que disminuye el tráfico de datos o data movement/motion pues se atiende uno de los cuellos de botella que se revisó en 2.1.Un_poco_de_historia_y_generalidades conocido con el nombre de bottleneck de Von Neumann.

Por lo anterior hay algoritmos que se diseñan con el objetivo de aprovechar lo más posible el data reuse y el data locality y se nombran blocking algorithms. Entre los blocking algorithms encontramos a los que trabajan con matrices por bloques pues son más eficientes al utilizar un nivel $3$ de BLAS. Una matriz $A \in \mathbb{R}^{m \times n}$ por bloques se puede escribir como:

donde: $m_1 + \dots + m_q = m$, $n_1 + \dots + n_r = n$. Con esta definición se llama a la matriz $A$, una matriz por bloques de tamaño $q \times r$.

Comentarios:

  • Hay que tener en cuenta que trabajar con matrices por bloques utiliza mayor cantidad de memoria que un algoritmo que opera a un nivel escalar esto puede ser benéfico o no dependiendo del problema, máquina o arquitectura en la que se esté trabajando.

  • Trabajar con blocking algorithms permite simplificar notación matemática. Su escritura ayuda a pensar en una implementación con cómputo en paralelo.

Algunas operaciones en matrices por bloques

Entre las operaciones que son posibles realizar a un nivel por bloques se encuentran:

Multiplicación por escalares

$$\begin{array}{l} \mu \left[ \begin{array}{cc} A_{11} & A_{12}\\ A_{21} & A_{22}\\ A_{31} & A_{32} \end{array} \right] = \left[\begin{array}{cc} \mu A_{11} & \mu A_{12}\\ \mu A_{21} & \mu A_{22}\\ \mu A_{31} & \mu A_{32} \end{array} \right] \end{array} $$

con $\mu \in \mathbb{R}$.

Transposición

$$\begin{array}{l} \left[ \begin{array}{cc} A_{11} & A_{12}\\ A_{21} & A_{22}\\ A_{31} & A_{32} \end{array} \right]^T = \left[\begin{array}{ccc} A_{11}^T & A_{21}^T & A_{31}^T\\ A_{12}^T & A_{22}^T & A_{32}^T\\ \end{array} \right] \end{array} $$

Suma

$$\begin{array}{l} \left[ \begin{array}{cc} A_{11} & A_{12}\\ A_{21} & A_{22}\\ A_{31} & A_{32} \end{array} \right] + \left[\begin{array}{cc} B_{11} & B_{12}\\ B_{21} & B_{22}\\ B_{31} & B_{32} \end{array} \right] = \left[\begin{array}{cc} A_{11} + B_{11} & A_{12} + B_{12}\\ A_{21} + B_{21} & A_{22} + B_{22}\\ A_{31} + B_{31} & A_{32} + B_{32} \end{array} \right] \end{array} $$

Multiplicación

$$\begin{array}{l} \left[ \begin{array}{cc} A_{11} & A_{12}\\ A_{21} & A_{22}\\ A_{31} & A_{32} \end{array} \right] \cdot \left[\begin{array}{cc} B_{11} & B_{12}\\ B_{21} & B_{22}\\ \end{array} \right] = \left[\begin{array}{cc} A_{11}B_{11} + A_{12}B_{21} & A_{11}B_{12} + A_{12}B_{22}\\ A_{21}B_{11} + A_{22}B_{21} & A_{21}B_{12} + A_{22}B_{22}\\ A_{31}B_{11} + A_{32}B_{21} & A_{31}B_{12} + A_{32}B_{22} \end{array} \right] \end{array} $$

Obs: el número de columnas de los bloques $A_{11}, A_{21}, A_{31}$ deben coincidir con el número de renglones de $B_{11}, B_{12}$. Así como con los bloques $A_{12}, A_{22}, A_{32}$ y $B_{21}, B_{22}$.

Considérese que se particionan a las matrices $A, B, C$ como sigue:

Esto es, $A,B,C$ son $N \times N$ matrices por bloques con $\ell \times \ell$ bloques. Entonces, para índices $\alpha = 1,2,\dots, N$ y $\beta = 1,2,\dots,N$ se tiene que el bloque $\alpha \beta$ de $C$ se obtiene: $C_{\alpha \beta} = \displaystyle \sum_{\gamma=1}^N A_{\alpha \gamma} B_{\gamma \beta}$.

El algoritmo es:

for alpha in np.arange(N)
    i = np.arange((alpha - 1)*l + 1,alpha*l + 1) #hay que revisar si están bien definidos los índices
    for beta in np.arange(N)
        j = np.arange((beta - 1)*l + 1, beta*l + 1) #hay que revisar si están bien definidos los índices
        for gamma in np.arange(N)
            k = np.arange((gamma - 1)*l + 1, gamma*l + 1) #hay que revisar si están bien los índices
            C[i][:,j]+= A[i][:,k]*B[k][:,j]

Ejercicio:

Implementar multiplicación por bloques en Python y R. Perfilar tal implementación.


In [34]:
import os

In [37]:
os.remove(fileA)
os.remove(fileB)
os.remove(fileA_10_3)
os.remove(fileB_10_3)

Referencias:

  • E. Anderson, Z. Bai, C. Bischof, L. S. Blackford, J. Demmel, J. Dongarra, J. Du Croz, A. Greenbaum, S. Hammarling, A. Mckenney and D. Sorensen, LAPACK Users Guide, Society for Industrial and Applied Mathematics, Philadelphia, PA, third ed., 1999.

  • G. H. Golub, C. F. Van Loan, Matrix Computations, John Hopkins University Press, 2013.

  • 2.1.Un_poco_de_historia_y_generalidades

Otras referencias útiles para perf son: