¿Por qué expresar cálculos como multiplicaciones matriciales los hace más rápidos?

18

En el tutorial MNist de Google con TensorFlow , se muestra un cálculo en el que un paso es equivalente a multiplicar una matriz por un vector. Google primero muestra una imagen en la que cada multiplicación y suma numérica que se utilizaría para realizar el cálculo se escribe por completo. A continuación, muestran una imagen en la que se expresa como una multiplicación de matriz, alegando que esta versión del cálculo es, o al menos podría ser, más rápida:

Si escribimos eso como ecuaciones, obtenemos:

ecuación escalar

Podemos "vectorizar" este procedimiento, convirtiéndolo en una matriz de multiplicación y suma de vectores. Esto es útil para la eficiencia computacional. (También es una forma útil de pensar).

ecuación vectorial

Sé que las ecuaciones como esta generalmente están escritas en el formato de multiplicación de matrices por profesionales de aprendizaje automático, y por supuesto puedo ver las ventajas de hacerlo desde el punto de vista del código o de la comprensión de las matemáticas. Lo que no entiendo es la afirmación de Google de que la conversión de la forma manual a la matriz "es útil para la eficiencia computacional"

¿Cuándo, por qué y cómo sería posible obtener mejoras de rendimiento en el software expresando los cálculos como multiplicaciones matriciales? Si tuviera que calcular la multiplicación de la matriz en la segunda imagen (basada en la matriz), como humano, lo haría haciendo secuencialmente cada uno de los cálculos distintos que se muestran en la primera imagen (escalar). Para mí, no son más que dos anotaciones para la misma secuencia de cálculos. ¿Por qué es diferente para mi computadora? ¿Por qué una computadora podría realizar el cálculo de la matriz más rápido que el escalar?

Mark Amery
fuente

Respuestas:

19

Esto puede sonar obvio, pero las computadoras no ejecutan fórmulas , ejecutan código , y cuánto tiempo lleva esa ejecución depende directamente del código que ejecutan y solo indirectamente del concepto que implemente el código. Dos partes de código lógicamente idénticas pueden tener características de rendimiento muy diferentes. Algunas razones que probablemente surjan específicamente en la multiplicación de matrices:

  • Usando múltiples hilos. Casi no hay una CPU moderna que no tenga múltiples núcleos, muchas tienen hasta 8, y las máquinas especializadas para computación de alto rendimiento pueden tener fácilmente 64 en varios zócalos. Escribir código de la manera obvia, en un lenguaje de programación normal, usa solo uno de esos. En otras palabras, puede usar menos del 2% de los recursos informáticos disponibles de la máquina en la que se está ejecutando.
  • Usando instrucciones SIMD (confusamente, esto también se llama "vectorización" pero en un sentido diferente al de las citas de texto en la pregunta). En esencia, en lugar de 4 u 8 más o menos instrucciones aritméticas escalares, dele a la CPU una instrucción que realice operaciones aritméticas en 4 u 8 o más registros en paralelo. Literalmente, esto puede hacer algunos cálculos (cuando son perfectamente independientes y aptos para el conjunto de instrucciones) 4 u 8 veces más rápido.
  • Hacer un uso más inteligente del caché . El acceso a la memoria es más rápido si son coherentes temporal y espacialmente , es decir, los accesos consecutivos son a direcciones cercanas y cuando accede a una dirección dos veces, accede a ella dos veces en rápida sucesión en lugar de con una pausa larga.
  • Usar aceleradores como las GPU. Estos dispositivos son bestias muy diferentes de las CPU y programarlos de manera eficiente es una forma de arte propia. Por ejemplo, tienen cientos de núcleos, que se agrupan en grupos de unas pocas docenas de núcleos, y estos grupos comparten recursos: comparten algunos KiB de memoria que es mucho más rápido que la memoria normal, y cuando cualquier núcleo del grupo ejecuta un ifdeclaración todos los demás en ese grupo tienen que esperarlo.
  • Distribuya el trabajo en varias máquinas (¡muy importante en las supercomputadoras!) Que introduce una gran cantidad de nuevos dolores de cabeza pero, por supuesto, puede dar acceso a recursos informáticos mucho mayores.
  • Algoritmos más inteligentes. Para la multiplicación de matrices, el algoritmo simple O (n ^ 3), correctamente optimizado con los trucos anteriores, suele ser más rápido que los subcúbicos para tamaños de matriz razonables, pero a veces ganan. Para casos especiales como matrices dispersas, puede escribir algoritmos especializados.

Muchas personas inteligentes han escrito código muy eficiente para operaciones comunes de álgebra lineal , utilizando los trucos anteriores y muchos más, y generalmente incluso con trucos estúpidos específicos de la plataforma. Por lo tanto, transformar su fórmula en una multiplicación de matriz y luego implementar ese cálculo llamando a una biblioteca de álgebra lineal madura se beneficia de ese esfuerzo de optimización. Por el contrario, si simplemente escribe la fórmula de la manera obvia en un lenguaje de alto nivel, el código de máquina que finalmente se genera no utilizará todos esos trucos y no será tan rápido. Esto también es cierto si toma la formulación de la matriz y la implementa llamando a una rutina de multiplicación de matriz ingenua que usted mismo escribió (nuevamente, de manera obvia).

Hacer que el código sea rápido requiere trabajo y, a menudo, mucho trabajo si quieres esa última onza de rendimiento. Debido a que muchos cálculos importantes pueden expresarse como una combinación de un par de operaciones de álgebra lineal, es económico crear un código altamente optimizado para estas operaciones. Sin embargo, ¿su caso de uso especializado único? A nadie le importa eso, excepto a ti, por lo que optimizarlo no es económico.

Comunidad
fuente
4

(disperso) La multiplicación de vectores de matriz es altamente paralelizable. Lo cual es muy útil si sus datos son grandes y tiene una granja de servidores a su disposición.

Esto significa que puede dividir la matriz y el vector en trozos y dejar que máquinas separadas hagan parte del trabajo. Luego, comparta algunos de sus resultados entre ellos y luego obtenga el resultado final.

En su ejemplo, las operaciones serían las siguientes

  1. configurar una cuadrícula de procesadores cada uno con un Wx, y de acuerdo con sus coordenadas en la cuadrícula

  2. difunde el vector fuente a lo largo de cada columna (costo O(log height))

  3. tener cada procesador a la multiplicación localmente (costo O(width of submatrix * heightof submatrix))

  4. contraer el resultado a lo largo de cada fila usando una suma (costo O(log width))

Esta última operación es válida porque la suma es asociativa.

Esto también permite generar redundancia y evitar tener que poner toda la información en una sola máquina.

Para matrices 4x4 pequeñas como las que se ven en los gráficos, es porque la CPU tiene instrucciones y registros especiales para manejar esas operaciones.

monstruo de trinquete
fuente
-1

Lo más instructivo sería comparar el rendimiento de su código con el rendimiento de la multiplicación matricial implementada por alredy.

Siempre hay alguna optimización de nivel inferior en la que no pensó, aquí puede encontrar un ejemplo:

https://simulationcorner.net/index.php?page=fastmatrixvector

El Castigador
fuente