<jupyter_start><jupyter_text>**Notas para contenedor de docker:** Comando de docker para ejecución de la nota de forma local:nota: cambiar `` por la ruta de directorio que se desea mapear a `/datos` dentro del contenedor de docker.```docker run --rm -v :/datos --cap-add SYS_ADMIN --privileged --name jupyterlab-numerical -p 8888:8888 -d palmoreck/jupyterlab_numerical:1.1.0```password para jupyterlab: `<PASSWORD>`Detener el contenedor de docker:```docker stop jupyterlab_numerical``` Documentación de la imagen de docker `palmoreck/jupyterlab_numerical:1.1.0` en [liga](https://github.com/palmoreck/dockerfiles/tree/master/jupyterlab/numerical). **Para ejecutar el comando de linux [perf](https://github.com/torvalds/linux/tree/master/tools/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](https://www.dropbox.com/s/fyqwiqasqaa3wlt/3.1.1.Multiplicacion_de_matrices_y_estructura_de_datos.pdf?dl=0), [liga2](https://www.dropbox.com/s/l4hq45rj860ql9f/3.1.2.Localidad_y_vectorizacion.Analisis_del_error_en_computos_matriciales_basicos.pdf?dl=0)  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](http://www.netlib.org/lapack/explore-html/dir_fa94b7b114d387a7a8beb2e3e22bf78d.html) son utilizados para denotar operaciones con escalares, vectores o matrices. Ver [ Reference-LAPACK / lapack](https://github.com/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:**<jupyter_code>c=0
n=5
x=[-1]*n
y=[1.5]*n

for i in range(n):
    c += x[i]*y[i]
c<jupyter_output><empty_output><jupyter_text>**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](https://en.wikipedia.org/wiki/FLOPS) (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](http://www.netlib.org/lapack/explore-html/d0/d16/sdot_8f.html), [ddot](http://www.netlib.org/lapack/explore-html/d5/df6/ddot_8f.html), [cdotu](http://www.netlib.org/lapack/explore-html/d7/d7b/cdotu_8f.html) y [zdotu](http://www.netlib.org/lapack/explore-html/db/d2d/zdotu_8f.html) para descripción de las funciones/subrutinas escritas en [Fortran](https://en.wikipedia.org/wiki/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](http://www.netlib.org/lapack/explore-html/d8/daf/saxpy_8f.html) 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](http://www.netlib.org/lapack/explore-html/de/da2/caxpy_8f.html) o [daxpy](http://www.netlib.org/lapack/explore-html/d9/dcd/daxpy_8f.html) 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:**<jupyter_code>alpha=2
n=5
x=[-2]*n
y=[0]*n

for i in range(n):
    y[i] += alpha*x[i]
y<jupyter_output><empty_output><jupyter_text>o en una forma *update*:<jupyter_code>alpha=2
n=5
x=[-2]*n
y=[3,4,-1,0,1]

for i in range(n):
    y[i] += alpha*x[i]
y<jupyter_output><empty_output><jupyter_text>**Comentario:** La operación de producto punto y *saxpy* son algoritmos catalogados como de **nivel BLAS 1**, ver [BLAS: Basic Linear Algebra Subprograms](https://en.wikipedia.org/wiki/Basic_Linear_Algebra_Subprograms). Éstos algoritmos se caracterizan por involucrar una cantidad de trabajo lineal sobre una cantidad lineal de datos. Ver [level 1](http://www.netlib.org/blas/_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](http://www.netlib.org/lapack/explore-html/db/d58/sgemv_8f.html), [dgemv](http://www.netlib.org/lapack/explore-html/dc/da8/dgemv_8f.html), [cgemv](http://www.netlib.org/lapack/explore-html/d4/d8a/cgemv_8f.html) o [zgemv](http://www.netlib.org/lapack/explore-html/db/d40/zgemv_8f.html) 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:<jupyter_code>m=2
n=5
A=[[1.2]*n if i%2==0 else [1]*n for i in range(m)]
A
A[0][0]
A[1][n-1]<jupyter_output><empty_output><jupyter_text>se tiene:  -) Algoritmo gaxpy *row oriented* **Ejemplo**<jupyter_code>x=[2]*n
y=[0]*m
for i in range(m):
    for j in range(n):
        y[i]+=A[i][j]*x[j]

y<jupyter_output><empty_output><jupyter_text>Si $y$ tiene valores distintos de $0$, se realiza un *update*:<jupyter_code>x=[2]*n
y=[-1]*m
for i in range(m):
    for j in range(n):
        y[i]+=A[i][j]*x[j]

y<jupyter_output><empty_output><jupyter_text>**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**<jupyter_code>x=[2]*n
y=[0]*m
for j in range(n):
    for i in range(m):
        y[i]+=A[i][j]*x[j]
y<jupyter_output><empty_output><jupyter_text>**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](http://www.netlib.org/blas/_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:<jupyter_code>x=[2]*n
y=[0]*m
for j in range(n):
    y+=A[:,j]*x[j]<jupyter_output><empty_output><jupyter_text>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](https://en.wikipedia.org/wiki/Bytecode) de Python no está optimizado para vectorización. Un ejemplo de un paquete que permite realizar operaciones de forma vectorizada es [numpy](https://numpy.org/):<jupyter_code>import numpy as np
x = 2*np.ones(n)
y = np.zeros(m)
x
y
A=np.array([[1.2,1.2,1.2,1.2,1.2],[1,1,1,1,1]])
A
for j in range(n):
    y+=A[:,j]*x[j]
y<jupyter_output><empty_output><jupyter_text>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:<jupyter_code>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]])
for i in range(m):
    y[i]+=A[i,:].dot(x)
y<jupyter_output><empty_output><jupyter_text>en donde se utilizó la función [numpy.dot](https://docs.scipy.org/doc/numpy/reference/generated/numpy.dot.html) de *numpy*. **Comentarios:*** La vectorización como se describe en [2.1.Un_poco_de_historia_y_generalidades](https://github.com/ITAM-DS/analisis-numerico-computo-cientifico/blob/master/temas/II.computo_paralelo/2.1.Un_poco_de_historia_y_generalidades.ipynb), 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](https://en.wikipedia.org/wiki/Instructions_per_cycle) para procesadores que soportan el Single Instruction Multiple Data (SIMD) que encontramos en la taxonomía de Flynn (ver [liga](https://en.wikipedia.org/wiki/Flynn%27s_taxonomy)). Como ejemplo de tales procesadores están los procesadores vectoriales o en arreglo, ver [liga](https://en.wikipedia.org/wiki/Vector_processor). Obsérvese en [liga](https://github.com/numpy/numpy/blob/master/numpy/core/src/umath/simd.inc.src) 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](https://en.wikipedia.org/wiki/Instruction_cycle).  `Perf` En Linux existe la herramienta [perf](https://github.com/torvalds/linux/tree/master/tools/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**<jupyter_code>%%file norm_square.py
n=10**5
vector=list(range(n))
norm=0
for v in vector:
    norm+=v*v
%%bash
sudo perf stat -S -e cycles,instructions,cache-references,cache-misses -r 20 python3 norm_square.py<jupyter_output>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% )<jupyter_text>**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](http://man7.org/linux/man-pages/man2/sync.2.html) 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](https://github.com/ITAM-DS/analisis-numerico-computo-cientifico/blob/master/temas/II.computo_paralelo/2.1.Un_poco_de_historia_y_generalidades.ipynb) 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](https://en.wikipedia.org/wiki/Fragmentation_(computing)). 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](https://github.com/ITAM-DS/analisis-numerico-computo-cientifico/blob/master/temas/II.computo_paralelo/2.1.Un_poco_de_historia_y_generalidades.ipynb)). 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](https://en.wikipedia.org/wiki/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](http://www.brendangregg.com/perf.html). `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:<jupyter_code>%%bash
sudo perf stat -S -a --per-core -e cycles,instructions,cache-references,cache-misses -r 20 python3 norm_square.py<jupyter_output>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          [...]<jupyter_text>Otras métricas pueden ser obtenidas si ejecutamos `perf` sólo con la *flag* `-r`:<jupyter_code>%%bash
sudo perf stat -S -r 20 python3 norm_square.py<jupyter_output>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% )<jupyter_text>**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](https://en.wikipedia.org/wiki/Page_fault).* Ver [2.1.Un_poco_de_historia_y_generalidades](https://github.com/ITAM-DS/analisis-numerico-computo-cientifico/blob/master/temas/II.computo_paralelo/2.1.Un_poco_de_historia_y_generalidades.ipynb) para el *branching*. Esencialmente interesa que el número de *branch misses* sea pequeño. Podemos generar la salida anterior por *core*:<jupyter_code>%%bash
sudo perf stat -S -a --per-core -r 20 python3 norm_square.py<jupyter_output>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          [...]<jupyter_text>Para contrastar las salidas anteriores con un mejor uso del caché y de los *cores* se utiliza a continuación el paquete de `numpy`:<jupyter_code>%%file norm_square_numpy.py
import numpy as np
n=10**5
vector=np.arange(n)
vector.dot(vector)
%%bash
sudo perf stat -S -e cycles,instructions,cache-references,cache-misses -r 20 python3 norm_square_numpy.py<jupyter_output>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% )<jupyter_text>**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:<jupyter_code>%%bash
sudo perf stat -S -a --per-core -e cycles,instructions,cache-references,cache-misses -r 20 python3 norm_square_numpy.py<jupyter_output>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          [...]<jupyter_text>**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`:<jupyter_code>%%bash
sudo perf stat -S -r 20 python3 norm_square_numpy.py
%%bash
sudo perf stat -S -a --per-core -r 20 python3 norm_square_numpy.py<jupyter_output>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          [...]<jupyter_text>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**:<jupyter_code>np.random.seed(2020)
m=10**4
n=10**4
A=np.random.rand(m,n)
file='A.txt'
np.savetxt(file,A)<jupyter_output><empty_output><jupyter_text>Arhivo sin vectorizar y utilizando listas para construir a la matriz y a los vectores:<jupyter_code>%%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]<jupyter_output>Writing mult_matrix_vector.py<jupyter_text>Archivo vectorizando con numpy:<jupyter_code>%%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]<jupyter_output>Writing mult_matrix_vector_numpy.py<jupyter_text>Caso no vectorizado<jupyter_code>%%bash
sudo perf stat -S -e cycles,instructions,cache-references,cache-misses -r 2 python3 mult_matrix_vector.py<jupyter_output>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% )<jupyter_text>Métricas por core:<jupyter_code>%%bash
sudo perf stat -S -a --per-core -e cycles,instructions,cache-references,cache-misses -r 2 python3 mult_matrix_vector.py<jupyter_output>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           [...]<jupyter_text>Caso vectorizando<jupyter_code>%%bash
sudo perf stat -S -e cycles,instructions,cache-references,cache-misses -r 2 python3 mult_matrix_vector_numpy.py
%%bash
sudo perf stat -S -a --per-core -e cycles,instructions,cache-references,cache-misses -r 2 python3 mult_matrix_vector_numpy.py<jupyter_output>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           [...]<jupyter_text>**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](http://www.netlib.org/blas/_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*:<jupyter_code>%%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
%%bash
sudo perf stat -S -e cycles,instructions,cache-references,cache-misses -r 2 python3 mult_matrix_vector_numpy_2.py
%%bash
sudo perf stat -S -a --per-core -e cycles,instructions,cache-references,cache-misses -r 2 python3 mult_matrix_vector_numpy_2.py<jupyter_output>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           [...]<jupyter_text>**O con una versión *gaxpy row oriented***<jupyter_code>%%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)
%%bash
sudo perf stat -S -e cycles,instructions,cache-references,cache-misses -r 2 python3 mult_matrix_vector_numpy_row_oriented.py
%%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<jupyter_output>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           [...]<jupyter_text>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](http://www.netlib.org/blas/_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](http://www.netlib.org/lapack/explore-html/d4/de2/sgemm_8f.html), [dgemm](http://www.netlib.org/lapack/explore-html/d7/d2b/dgemm_8f.html), [cgemm](http://www.netlib.org/lapack/explore-html/d6/d5b/cgemm_8f.html) y [zgemm](http://www.netlib.org/lapack/explore-html/d7/d76/zgemm_8f.html) 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**<jupyter_code>np.random.seed(2020)
m=10**4
r=10**4

A=np.random.rand(m,r)
fileA='A.txt'
np.savetxt(fileA,A)
np.random.seed(2021)
r=10**4
n=10**4

B=np.random.rand(r,n)
fileB='B.txt'
np.savetxt(fileB,B)
%%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]

%%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
%%bash
sudo perf stat -S -a --per-core -e cycles,instructions,cache-references,cache-misses -r 1 python3 mult_matrix_matrix.py<jupyter_output>Process is terminated.<jupyter_text>... 36 min y todavía no terminaba... cambiar a r 1<jupyter_code>%%bash
sudo perf stat -S -a -e cycles,instructions,cache-references,cache-misses -r 1 python3 mult_matrix_matrix.py
%%bash
sudo perf stat -S -a -e cycles,instructions,cache-references,cache-misses -r 2 python3 mult_matrix_matrix_numpy.py
%%bash
sudo perf stat -S -a --per-core -e cycles,instructions,cache-references,cache-misses -r 2 python3 mult_matrix_matrix_numpy.py<jupyter_output>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           [...]<jupyter_text>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:<jupyter_code>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)
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)
%%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])
%%bash
sudo perf stat -S -a -e cycles,instructions,cache-references,cache-misses -r 5 python3 mult_matrix_matrix_numpy_dot_product.py
%%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
%%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
%%bash
sudo perf stat -S -a -e cycles,instructions,cache-references,cache-misses -r 5 python3 mult_matrix_matrix_numpy_dot_product_gaxpy.py
%%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<jupyter_output>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           [...]<jupyter_text>**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](https://github.com/ITAM-DS/analisis-numerico-computo-cientifico/blob/master/temas/II.computo_paralelo/2.1.Un_poco_de_historia_y_generalidades.ipynb) conocido con el nombre de *bottleneck* de <NAME>. 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.<jupyter_code>import os
os.remove(fileA)
os.remove(fileB)
os.remove(fileA_10_3)
os.remove(fileB_10_3)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.10326882080006689, 'fraction_numerical': 0.04669146846131338, 'mean_word_length': 2.442973303590703, 'pattern_counts': {'":': 0, '<': 102, '<?xml version=': 0, '>': 102, 'https://': 25, 'lorem ipsum': 0, 'www.': 23, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 89, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '29', 'dir': 'jupyter-structured-clean-dedup', 'id': '290532', 'license': 'Apache-2.0', 'n_tokens_mistral': 16948, 'n_tokens_neox': 15895, 'n_words': 4889, 'path': 'temas/III.computo_matricial/3.1.Vectorizacion_BLAS_y_el_uso_del_cache_eficientemente.ipynb', 'repo_name': 'Danahirmt/analisis-numerico-computo-cientifico', 'size': '47847'}