Procesador vectorial

Ajustar Compartir Imprimir Citar

En informática, un procesador vectorial o procesador de matriz es una unidad central de procesamiento (CPU) que implementa un conjunto de instrucciones donde sus instrucciones están diseñadas para operar de manera eficiente y efectiva en grandes arreglos unidimensionales de datos llamados vectores. Esto contrasta con los procesadores escalares, cuyas instrucciones operan solo en elementos de datos únicos, y en contraste con algunos de esos mismos procesadores escalares que tienen unidades aritméticas SWAR o de instrucción única adicional, datos múltiples (SIMD). Los procesadores vectoriales pueden mejorar en gran medida el rendimiento en ciertas cargas de trabajo, en particular, la simulación numérica y tareas similares. Las técnicas de procesamiento de vectores también funcionan en el hardware de las consolas de videojuegos y en los aceleradores de gráficos.

Las máquinas vectoriales aparecieron a principios de la década de 1970 y dominaron el diseño de las supercomputadoras durante la década de 1970 hasta la década de 1990, en particular las diversas plataformas Cray. La rápida caída en la relación precio-rendimiento de los diseños de microprocesadores convencionales condujo a una disminución de las supercomputadoras vectoriales durante la década de 1990.

Historia

Trabajo temprano

El desarrollo del procesamiento vectorial comenzó a principios de la década de 1960 en Westinghouse en su "Solomon" proyecto. El objetivo de Solomon era aumentar drásticamente el rendimiento matemático mediante el uso de una gran cantidad de coprocesadores matemáticos simples bajo el control de una sola CPU maestra. La CPU alimentaba una sola instrucción común a todas las unidades aritméticas lógicas (ALU), una por ciclo, pero con un punto de datos diferente para que cada una trabajara. Esto permitió que la máquina de Solomon aplicara un solo algoritmo a un gran conjunto de datos, alimentado en forma de matriz.

En 1962, Westinghouse canceló el proyecto, pero el esfuerzo se reinició en la Universidad de Illinois como ILLIAC IV. Su versión del diseño originalmente requería una máquina de 1 GFLOPS con 256 ALU, pero, cuando finalmente se entregó en 1972, solo tenía 64 ALU y solo podía alcanzar de 100 a 150 MFLOPS. No obstante, demostró que el concepto básico era sólido y, cuando se usaba en aplicaciones de uso intensivo de datos, como la dinámica de fluidos computacional, ILLIAC era la máquina más rápida del mundo. El enfoque de ILLIAC de usar ALU separadas para cada elemento de datos no es común a los diseños posteriores y, a menudo, se lo denomina en una categoría separada, computación paralela masiva. Alrededor de este tiempo, Flynn clasificó este tipo de procesamiento como una forma temprana de SIMT.

Kartsev presentó y desarrolló una computadora para operaciones con funciones en 1967.

Superordenadores

Las primeras supercomputadoras vectoriales son Control Data Corporation STAR-100 y Texas Instruments Advanced Scientific Computer (ASC), que se introdujeron en 1974 y 1972, respectivamente.

La ALU básica de ASC (es decir, "one pipe") usaba una arquitectura de tubería que admitía cálculos tanto escalares como vectoriales, con un rendimiento máximo que alcanzaba aproximadamente 20 MFLOPS, que se alcanza fácilmente cuando se procesan vectores largos. Las configuraciones ALU ampliadas admiten "dos tuberías" o "cuatro tubos" con una ganancia de rendimiento correspondiente de 2X o 4X. El ancho de banda de la memoria fue suficiente para soportar estos modos expandidos.

Por lo demás, la STAR-100 era más lenta que las propias supercomputadoras de los CDC, como la CDC 7600, pero en las tareas relacionadas con los datos podían mantener el ritmo siendo mucho más pequeñas y menos costosas. Sin embargo, la máquina también tardó un tiempo considerable en decodificar las instrucciones vectoriales y prepararse para ejecutar el proceso, por lo que requirió conjuntos de datos muy específicos para trabajar antes de que realmente acelerara algo.

La técnica del vector fue explotada completamente por primera vez en 1976 por el famoso Cray-1. En lugar de dejar los datos en la memoria como el STAR-100 y el ASC, el diseño de Cray tenía ocho registros vectoriales, que contenían sesenta y cuatro palabras de 64 bits cada uno. Las instrucciones vectoriales se aplicaron entre registros, lo que es mucho más rápido que hablar con la memoria principal. Mientras que el STAR-100 aplicaría una sola operación a través de un vector largo en la memoria y luego pasaría a la siguiente operación, el diseño de Cray cargaría una sección más pequeña del vector en los registros y luego aplicaría tantas operaciones como pudiera a esos datos., evitando así muchas de las operaciones de acceso a la memoria mucho más lentas.

El diseño de Cray utilizó el paralelismo de canalización para implementar instrucciones vectoriales en lugar de varias ALU. Además, el diseño tenía canalizaciones completamente separadas para diferentes instrucciones, por ejemplo, la suma y la resta se implementaron en un hardware diferente al de la multiplicación. Esto permitió canalizar un lote de instrucciones vectoriales en cada una de las subunidades ALU, una técnica que llamaron encadenamiento vectorial. El Cray-1 normalmente tenía un rendimiento de alrededor de 80 MFLOPS, pero con hasta tres cadenas en funcionamiento podía alcanzar un máximo de 240 MFLOPS y un promedio de alrededor de 150, mucho más rápido que cualquier máquina de la época.

Módulo de procesador Cray J90 con cuatro procesadores de escalar/vector

Otros ejemplos siguieron. Control Data Corporation intentó volver a entrar en el mercado de gama alta con su máquina ETA-10, pero se vendió mal y lo aprovecharon como una oportunidad para abandonar por completo el campo de la supercomputación. A principios y mediados de la década de 1980, las empresas japonesas (Fujitsu, Hitachi y Nippon Electric Corporation (NEC) introdujeron máquinas vectoriales basadas en registros similares a la Cray-1, que suelen ser un poco más rápidas y mucho más pequeñas. Sistemas de punto flotante (FPS) basados en Oregón construyeron procesadores de matriz adicionales para minicomputadoras y luego construyeron sus propias minisupercomputadoras.

En todo momento, Cray siguió siendo el líder en rendimiento, superando continuamente a la competencia con una serie de máquinas que dieron lugar a Cray-2, Cray X-MP y Cray Y-MP. Desde entonces, el mercado de las supercomputadoras se ha centrado mucho más en el procesamiento masivo en paralelo que en mejores implementaciones de procesadores vectoriales. Sin embargo, al reconocer los beneficios del procesamiento de vectores, IBM desarrolló la arquitectura de vector virtual para su uso en supercomputadoras que acoplan varios procesadores escalares para actuar como un procesador de vector.

Aunque las supercomputadoras vectoriales que se asemejan a la Cray-1 son menos populares en estos días, NEC ha seguido fabricando este tipo de computadora hasta el día de hoy con su serie de computadoras SX. Más recientemente, la SX-Aurora TSUBASA coloca el procesador y 24 o 48 gigabytes de memoria en un módulo HBM 2 dentro de una tarjeta que se parece físicamente a un coprocesador de gráficos, pero en lugar de servir como coprocesador, es la computadora principal con la computadora compatible con PC en la que está conectado cumple funciones de soporte.

GPU

Las unidades de procesamiento de gráficos (GPU) modernas incluyen una matriz de canalizaciones de sombreado que pueden ser impulsadas por núcleos de cómputo y pueden considerarse procesadores vectoriales (usando una estrategia similar para ocultar latencias de memoria). Como se muestra en el artículo de Flynn de 1972, el factor distintivo clave de las GPU basadas en SIMT es que tiene un decodificador-emisor de una sola instrucción, pero los núcleos que reciben y ejecutan esa misma instrucción son, por lo demás, razonablemente normales: sus propias ALU, sus propias registrar archivos, sus propias unidades de carga/almacenamiento y sus propios cachés de datos L1 independientes. Por lo tanto, aunque todos los núcleos ejecutan simultáneamente exactamente la misma instrucción al mismo tiempo, lo hacen con datos completamente diferentes de ubicaciones de memoria completamente diferentes. Esto es significativamente más complejo y complicado que "SIMD empaquetado", que se limita estrictamente a la ejecución de operaciones aritméticas canalizadas paralelas únicamente. Aunque los detalles internos exactos de las GPU comerciales actuales son secretos patentados, el equipo de MIAOW pudo reunir información anecdótica suficiente para implementar un subconjunto de la arquitectura AMDGPU.

Desarrollo reciente

Varias arquitecturas modernas de CPU se están diseñando como procesadores vectoriales. La extensión vectorial RISC-V sigue principios similares a los primeros procesadores vectoriales y se está implementando en productos comerciales como Andes Technology AX45MPV. También se están desarrollando varias arquitecturas de procesadores vectoriales de código abierto, incluidas ForwardCom y Libre-SOC.

Comparación con arquitecturas modernas

A partir de 2016, la mayoría de las CPU básicas implementan arquitecturas que cuentan con instrucciones SIMD de longitud fija. A primera vista, estos pueden considerarse una forma de procesamiento vectorial porque operan en conjuntos de datos múltiples (vectorizados, de longitud explícita) y toman prestadas características de los procesadores vectoriales. Sin embargo, por definición, la adición de SIMD no puede calificar por sí sola a un procesador como un Procesador de vectores real porque SIMD tiene una longitud fija y los vectores son variables. La diferencia se ilustra a continuación con ejemplos que muestran y comparan las tres categorías: Pure SIMD, Predicated SIMD y Pure Vector Processing.

Otros diseños de CPU incluyen algunas instrucciones múltiples para el procesamiento de vectores en conjuntos de datos múltiples (vectorizados), típicamente conocidos como MIMD (Múltima Instruction, Mmúltiples Datas) y realizado con VLIW (Muymuy Llargo Instruction W palabra). El Fujitsu FR-V VLIW/procesador vectorial combina ambas tecnologías.

Diferencia entre SIMD y procesador vectorial

Los conjuntos de instrucciones SIMD carecen de características cruciales en comparación con los conjuntos de instrucciones del procesador vectorial. El más importante de ellos es que los procesadores vectoriales, inherentemente por definición y diseño, siempre han sido de longitud variable desde su creación.

Donde los SIMD puros (de ancho fijo, sin predicación) comúnmente se afirman erróneamente que son "vectores" (debido a que SIMD procesa datos que sucede que son vectores), a través de un análisis minucioso y una comparación de los ISA históricos y modernos, se puede observar que los procesadores vectoriales reales tienen las siguientes características que ningún SIMD ISA tiene:

SIMD predicado (parte de la taxonomía de Flynn) que es máscaras de predicado de nivel de elemento individual integral en cada instrucción de vector como ahora está disponible en ARM SVE2. y AVX-512, casi califica como un procesador vectorial. La SIMD predicada utiliza ALU de SIMD de ancho fijo, pero permite la activación de unidades controlada localmente (predicada) para proporcionar la apariencia de vectores de longitud variable. Los ejemplos a continuación ayudan a explicar estas distinciones categóricas.

SIMD, debido a que es un procesamiento por lotes de ancho fijo, es incapaz por diseño de hacer frente a la iteración y la reducción. Esto se ilustra más adelante con ejemplos.

Simd vs vector.png

Además, los procesadores vectoriales pueden ser más eficientes en el uso de recursos (usar hardware más lento, ahorrar energía, pero aun así alcanzar el rendimiento) y tener menos latencia que SIMD, a través del encadenamiento de vectores.

Considere un procesador SIMD y un procesador vectorial trabajando en 4 elementos de 64 bits, realizando una secuencia LOAD, ADD, MULTIPLY y STORE. Si el ancho de SIMD es 4, entonces el procesador SIMD debe CARGAR cuatro elementos por completo antes de poder pasar a los ADD, debe completar todos los ADD antes de poder pasar a los MULTIPLY y, de la misma manera, debe completar todos los MULTIPLY antes de que pueda poner en marcha las TIENDAS. Esto es por definición y por diseño.

Tener que realizar 4 LOADs de 64 bits y STORE de 64 bits simultáneos en todo el ancho es muy costoso en hardware (rutas de datos de 256 bits a la memoria). Tener 4 ALU de 64 bits, especialmente MULTIPLY, también. Para evitar estos altos costos, un procesador SIMD tendría que tener 1 LOAD de 64 bits de ancho, 1 ALMACENAMIENTO de 64 bits de ancho y solo 2 ALU de 64 bits de ancho. Como se muestra en el diagrama, que asume un modelo de ejecución de varios problemas, las consecuencias son que ahora las operaciones tardan más en completarse. Si la emisión múltiple no es posible, entonces las operaciones demoran aún más porque es posible que el LD no se emita (inicie) al mismo tiempo que los primeros ADD, y así sucesivamente. Si solo hay 4 ALU SIMD de 64 bits de ancho, el tiempo de finalización es aún peor: solo cuando los cuatro LOAD se hayan completado, pueden comenzar las operaciones SIMD, y solo cuando se hayan completado todas las operaciones ALU, puede comenzar la operación. Comienzan las TIENDAS.

Por el contrario, un procesador vectorial, incluso si es de un solo problema y no utiliza SIMD ALU, solo tiene 1 LOAD de 64 bits de ancho, 1 ALMACENAMIENTO de 64 bits de ancho (y, como en el Cray-1, la capacidad de ejecutar MULTIPLY simultáneamente con ADD), puede completar las cuatro operaciones más rápido que un procesador SIMD con CARGA de 1 ancho, ALMACENAMIENTO de 1 ancho y SIMD de 2 anchos. Esta utilización de recursos más eficiente, debido al encadenamiento de vectores, es una ventaja clave y una diferencia en comparación con SIMD. SIMD por diseño y definición no puede realizar encadenamiento excepto para todo el grupo de resultados.

Descripción

En términos generales, las CPU pueden manipular uno o dos datos a la vez. Por ejemplo, la mayoría de las CPU tienen una instrucción que esencialmente dice "agregue A a B y coloque el resultado en C". Los datos para A, B y C podrían ser, al menos en teoría, codificados directamente en la instrucción. Sin embargo, en una implementación eficiente, las cosas rara vez son tan simples. Los datos rara vez se envían sin procesar y, en cambio, se "apuntan a" pasando una dirección a una ubicación de memoria que contiene los datos. Decodificar esta dirección y obtener los datos de la memoria lleva algún tiempo, durante el cual la CPU tradicionalmente permanecería inactiva esperando que aparecieran los datos solicitados. A medida que aumentaron las velocidades de la CPU, esta latencia de la memoria se ha convertido históricamente en un gran impedimento para el rendimiento; ver Muro de la memoria.

Para reducir la cantidad de tiempo consumido por estos pasos, la mayoría de las CPU modernas utilizan una técnica conocida como canalización de instrucciones en la que las instrucciones pasan a través de varias subunidades a la vez. La primera subunidad lee la dirección y la decodifica, la siguiente "busca" los valores en esas direcciones, y el siguiente hace los cálculos por sí mismo. Con canalizar el "truco" es comenzar a decodificar la siguiente instrucción incluso antes de que la primera haya salido de la CPU, a la manera de una línea de ensamblaje, por lo que el decodificador de direcciones está constantemente en uso. Cualquier instrucción en particular tarda la misma cantidad de tiempo en completarse, un tiempo conocido como latencia, pero la CPU puede procesar un lote completo de operaciones, de manera superpuesta, mucho más rápido y más eficientemente que si lo hiciera. lo hizo de uno en uno.

Los procesadores vectoriales llevan este concepto un paso más allá. En lugar de canalizar solo las instrucciones, también canalizan los datos en sí. El procesador recibe instrucciones que dicen no solo que sume A a B, sino que sume todos los números 'de aquí a aquí'. a todos los números "de allí a allí". En lugar de tener que decodificar constantemente las instrucciones y luego obtener los datos necesarios para completarlas, el procesador lee una sola instrucción de la memoria, y simplemente está implícito en la definición de la instrucción en sí misma que la instrucción operará. de nuevo en otro elemento de datos, en una dirección un incremento mayor que el anterior. Esto permite un ahorro significativo en el tiempo de decodificación.

Para ilustrar la diferencia que esto puede hacer, considere la simple tarea de sumar dos grupos de 10 números. En un lenguaje de programación normal, uno escribiría un "bucle" que recogió cada uno de los pares de números a su vez, y luego los sumó. Para la CPU, esto se vería así:

; Máquina RISC hipotética; asumir a, b y c son lugares de memoria en sus respectivos registros; añadir 10 números en a a 10 números en b, almacenar resultados en c Muévanse. 10 dólares, Cuenta ; contar:= 10Loop: carga r1, a carga r2, b añadir r3, r1, r2 ; r3:= r1 + r2 tienda r3, c añadir a, a, $4 ; seguir adelante añadir b, b, $4 añadir c, c, $4 dec Cuenta ; decremento Jnez Cuenta, bucle ; volver atrás si el conteo todavía no es 0 Ret

Pero para un procesador vectorial, esta tarea se ve considerablemente diferente:

; asumir que tenemos registros vectoriales v1-v3 ; con tamaño igual o superior a 10 Muévanse. 10 dólares, Cuenta ; cuenta = 10 vload v1, a, Cuenta vload v2, b, Cuenta ¡Vamos! v3, v1, v2 vstore v3, c, Cuenta Ret

Observe la falta total de bucles en las instrucciones, porque es el hardware el que ha realizado 10 operaciones secuenciales: efectivamente, el recuento de bucles está en un por instrucción explícito base.

Los ISA de vector estilo Cray van un paso más allá y proporcionan un "recuento" registro, llamado longitud del vector (VL):

; asumir nuevamente que tenemos registros vectoriales v1-v3; con tamaño mayor o igual a 10 setvli 10 dólares # Longitud vectorial VL=10 vload v1, a # 10 cargas de una vload v2, b # 10 cargas de b ¡Vamos! v3, v1, v2 # 10 adds vstore v3, c # 10 tiendas en c Ret

Hay varios ahorros inherentes a este enfoque.

  1. sólo se necesitan tres traducciones de direcciones. Dependiendo de la arquitectura, esto puede representar un ahorro significativo por sí mismo.
  2. Otro ahorro es buscar y decodificar la instrucción misma, que tiene que hacerse sólo una vez en lugar de diez.
  3. El código en sí también es más pequeño, lo que puede dar lugar a un uso de memoria más eficiente, reducción del tamaño de caché de instrucción L1, reducción del consumo de energía.
  4. Con el tamaño del programa que se reduce la predicción de rama tiene un trabajo más fácil.
  5. Con la longitud (equivalente a ancho SIMD) que no se codifica duro en la instrucción, no sólo es la codificación más compacta, también es "a prueba de futuro" y permite incluso diseños de procesadores integrados para considerar el uso de vectores puramente para ganar todas las otras ventajas, en lugar de ir para un alto rendimiento.

Además, en los ISA de procesador vectorial más modernos, "Fail on First" o "Error primero" se ha introducido (ver más abajo) que trae aún más ventajas.

Pero más que eso, un procesador vectorial de alto rendimiento puede tener múltiples unidades funcionales sumando esos números en paralelo. No se requiere verificar las dependencias entre esos números, ya que una instrucción de vector especifica varias operaciones independientes. Esto simplifica la lógica de control requerida y puede mejorar aún más el rendimiento al evitar paradas. Por lo tanto, las operaciones matemáticas se completaron mucho más rápido en general, siendo el factor limitante el tiempo requerido para obtener los datos de la memoria.

No todos los problemas se pueden atacar con este tipo de solución. Incluir este tipo de instrucciones necesariamente agrega complejidad a la CPU central. Esa complejidad generalmente hace que otras instrucciones se ejecuten más lentamente, es decir, siempre que no suman muchos números seguidos. Las instrucciones más complejas también se suman a la complejidad de los decodificadores, lo que podría ralentizar la decodificación de las instrucciones más comunes, como la adición normal. (Esto se puede mitigar un poco manteniendo todo el ISA según los principios RISC: RVV solo agrega alrededor de 190 instrucciones vectoriales, incluso con las funciones avanzadas.)

Los procesadores vectoriales se diseñaron tradicionalmente para funcionar mejor solo cuando hay una gran cantidad de datos en los que trabajar. Por esta razón, este tipo de CPU se encontraban principalmente en supercomputadoras, ya que las propias supercomputadoras, en general, se encontraban en lugares como centros de predicción meteorológica y laboratorios de física, donde se procesan enormes cantidades de datos. Sin embargo, como se muestra arriba y lo demuestra RISC-V RVV, la eficiencia de los vectores ISA brinda otros beneficios que son convincentes incluso para los casos de uso integrados.

Instrucciones vectoriales

El ejemplo de pseudocódigo vectorial anterior viene con la gran suposición de que la computadora vectorial puede procesar más de diez números en un lote. Para una mayor cantidad de números en el registro vectorial, se hace inviable que la computadora tenga un registro tan grande. Como resultado, el procesador vectorial obtiene la capacidad de realizar bucles por sí mismo o expone algún tipo de registro de control vectorial (estado) al programador, generalmente conocido como longitud vectorial.

Las instrucciones autorrepetitivas se encuentran en las primeras computadoras vectoriales como la STAR-100, donde la acción anterior se describiría en una sola instrucción (algo así como vadd c, a, b, $10). También se encuentran en la arquitectura x86 como REP prefijo. Sin embargo, solo se pueden realizar cálculos muy simples de manera efectiva en hardware de esta manera sin un aumento de costo muy grande. Dado que todos los operandos deben estar en la memoria para la arquitectura STAR-100, la latencia causada por el acceso también se volvió enorme.

Curiosamente, sin embargo, Broadcom incluyó espacio en todas las operaciones vectoriales de Videocore IV ISA para un REP, pero a diferencia del STAR-100 que usa memoria para sus repeticiones, las repeticiones de Videocore IV están en todas las operaciones, incluidas las operaciones aritméticas vectoriales. La longitud de repetición puede ser un rango pequeño de potencia de dos o provenir de uno de los registros escalares.

El Cray-1 introdujo la idea de utilizar registros de procesador para almacenar datos vectoriales en lotes. Las longitudes de los lotes (longitud del vector, VL) se pueden configurar dinámicamente con una instrucción especial, la importancia en comparación con Videocore IV (y, de manera crucial, como se mostrará a continuación, también con SIMD) es que la longitud de repetición no tiene que ser parte de la codificación de instrucciones. De esta manera, se puede hacer mucho más trabajo en cada lote; la codificación de instrucciones es mucho más elegante y compacta también. El único inconveniente es que para aprovechar al máximo esta capacidad adicional de procesamiento por lotes, la carga de memoria y la velocidad de almacenamiento también tuvieron que aumentar correspondientemente. A veces se afirma que esto es una desventaja de los procesadores vectoriales de estilo Cray: en realidad, es parte de lograr un rendimiento de alto rendimiento, como se ve en las GPU, que enfrentan exactamente el mismo problema.

Las computadoras SIMD modernas afirman mejorar las primeras Cray mediante el uso directo de múltiples ALU, para un mayor grado de paralelismo en comparación con el uso exclusivo de la canalización escalar normal. Los procesadores vectoriales modernos (como el SX-Aurora TSUBASA) combinan ambos, mediante la emisión de múltiples datos a múltiples ALU SIMD canalizadas internas, y el programa vectorial elige dinámicamente el número emitido en tiempo de ejecución. Las máscaras se pueden usar para cargar y almacenar datos de manera selectiva en ubicaciones de memoria, y usar esas mismas máscaras para deshabilitar selectivamente el elemento de procesamiento de las SIMD ALU. Algunos procesadores con SIMD (AVX-512, ARM SVE2) son capaces de este tipo de procesamiento selectivo por elemento ("predicado"), y son estos los que merecen la nomenclatura de "procesador vectorial& #34; o al menos merecen la afirmación de ser capaces de "procesamiento de vectores". Procesadores SIMD sin predicación por elemento (MMX, SSE, AltiVec) categóricamente no.

Las GPU modernas, que tienen muchas unidades de cómputo pequeñas, cada una con sus propias SIMD ALU independientes, usan subprocesos múltiples de instrucción única (SIMT). Las unidades SIMT se ejecutan desde una unidad de instrucción sincronizada de transmisión única compartida. Los "registros vectoriales" son muy anchas y las tuberías tienden a ser largas. El "enhebrado" parte de SIMT implica la forma en que los datos se manejan de forma independiente en cada una de las unidades de cómputo.

Además, las GPU como Broadcom Videocore IV y otros procesadores vectoriales externos como NEC SX-Aurora TSUBASA pueden usar menos unidades vectoriales de lo que implica el ancho: en lugar de tener 64 unidades para un registro de 64 números de ancho, el en cambio, el hardware podría hacer un bucle canalizado en 16 unidades para un enfoque híbrido. El Broadcom Videocore IV también es capaz de este enfoque híbrido: indica nominalmente que su motor SIMD QPU admite operaciones de matriz FP de 16 largos en sus instrucciones, en realidad las hace 4 a la vez, como (otra) forma de "hilos& #34;.

Ejemplo de instrucción vectorial

Este ejemplo comienza con un algoritmo ("IAXPY"), primero muéstrelo en instrucciones escalares, luego SIMD, luego SIMD predicado y finalmente instrucciones vectoriales. Esto ayuda a ilustrar de forma incremental la diferencia entre un procesador vectorial tradicional y uno SIMD moderno. El ejemplo comienza con una variante entera de 32 bits del "DAXPY" función, en C:

vacío iaxpy()size_t n, int a, const int x[], int Sí.[]) {} para ()size_t i = 0; i . n; i++) Sí.[i] = a * x[i] + Sí.[i];}

En cada iteración, a cada elemento de y se le suma un elemento de x multiplicado por a. El programa se expresa en forma lineal escalar para mejorar la legibilidad.

Ensambladora escalar

(feminine)

La versión escalar de esto cargaría uno de cada uno de x e y, procesaría un cálculo, almacenaría un resultado y repetiría:

Loop: load32 r1, x ; cargar un dato de 32 bits load32 r2, Sí. mul32 r1, a, r1 ; r1:= r1 * a add32 r3, r1, r2 ; r3:= r1 + r2 tienda32 r3, Sí. addl x, x, $4 ; x:= x + 4 addl Sí., Sí., $4 subl n, n, 1 dólar ; n:= n - 1 jgz n, bucle ; volver atrás si no 0Fuera: Ret

El código similar a STAR sigue siendo conciso, pero debido a que la vectorización del STAR-100 se basó por diseño en los accesos a la memoria, ahora se requiere una ranura adicional de memoria para procesar la información. También se necesita el doble de latencia debido al requisito adicional de acceso a la memoria.

 ; Assume tmp is pre-allocated vmul tmp, a, x, n ; tmp[i] = a * x[i] ¡Vamos! Sí., Sí., tmp, n ; y[i] = y[i] + tmp[i] Ret

SIMD pura (no predicada, empaquetada)

(feminine)

Una arquitectura SIMD empaquetada moderna, conocida por muchos nombres (enumerados en la taxonomía de Flynn), puede realizar la mayor parte de la operación en lotes. El código es en su mayoría similar a la versión escalar. Se supone que tanto x como y están correctamente alineados aquí (solo comienzan en un múltiplo de 16) y que n es un múltiplo de 4, ya que, de lo contrario, se necesitaría algún código de configuración para calcular una máscara o ejecutar una versión escalar. También se puede suponer, por simplicidad, que las instrucciones SIMD tienen una opción para repetir automáticamente los operandos escalares, como puede hacerlo ARM NEON. Si no es así, un "splat" (broadcast) debe usarse para copiar el argumento escalar a través de un registro SIMD:

 splatx4 v4, a ; v4 = a,a,a

El tiempo necesario sería básicamente el mismo que el de una implementación vectorial de y = mx + c descrito anteriormente.

Vloop: load32x4 v1, x load32x4 v2, Sí. mul32x4 v1, a, v1 ; v1:= v1 * a add32x4 v3, v1, v2 ; v3:= v1 + v2 store32x4 v3, Sí. addl x, x, 16 dólares ; x:= x + 16 addl Sí., Sí., 16 dólares subl n, n, $4 ; n:= n - 4 jgz n, vloop ; volver si no 0Fuera: Ret

Tenga en cuenta que los punteros x e y se incrementan en 16, porque esa es la longitud (en bytes) de cuatro números enteros de 32 bits. Se tomó la decisión de que el algoritmo deberá solo hacer frente a SIMD de 4 anchos, por lo tanto, la constante está codificada en el programa.

Desafortunadamente para SIMD, la clave estaba en la suposición anterior, "que n es un múltiplo de 4" así como "acceso alineado", que, claramente, es un caso de uso especializado limitado.

Siendo realistas, para bucles de uso general, como en bibliotecas portátiles, donde n no se puede limitar de esta manera, la sobrecarga de configuración y limpieza de SIMD para hacer frente a los no múltiplos del ancho de SIMD puede superar con creces la recuento de instrucciones dentro del bucle mismo. Suponiendo en el peor de los casos que el hardware no pueda realizar accesos a la memoria SIMD desalineados, un algoritmo del mundo real:

El SIMD de ocho anchos requiere repetir el algoritmo de bucle interno primero con elementos SIMD de cuatro anchos, luego SIMD de dos anchos, luego uno (escalar), con una prueba y una bifurcación entre cada uno, para cubrir el primero y últimos elementos SIMD restantes (0 <= n <= 7).

Esto más que triplica el tamaño del código, de hecho, en casos extremos resulta en un aumento de orden de magnitud en el número de instrucciones. Esto se puede demostrar fácilmente compilando el ejemplo iaxpy para AVX-512, usando las opciones "-O3 -march=knl" a gcc.

Con el tiempo, a medida que ISA evoluciona para seguir aumentando el rendimiento, los arquitectos de ISA agregan SIMD de 2 anchos, luego SIMD de 4 anchos, luego 8 de ancho y más. Por lo tanto, se puede ver por qué AVX-512 existe en x86.

Sin lugar a dudas, cuanto mayor sea el ancho de SIMD, peores serán los problemas, lo que provocará una proliferación masiva de códigos de operación, un rendimiento degradado, un consumo de energía adicional y una complejidad de software innecesaria.

Los procesadores vectoriales, por otro lado, están diseñados para realizar cálculos de longitud variable para un conteo arbitrario, n, y por lo tanto requieren muy poca configuración y ninguna limpieza. Incluso en comparación con los SIMD ISA que tienen máscaras (pero no setvl instrucción), los procesadores vectoriales producen un código mucho más compacto porque no necesitan realizar un cálculo de máscara explícito para cubrir los últimos elementos (ilustrado a continuación).

SIMD predicado

Suponiendo un SIMD ISA predicado hipotético (capaz de máscara), y suponiendo nuevamente que las instrucciones SIMD pueden hacer frente a datos desalineados, el bucle de instrucciones se vería así:

Vloop: # Prepara máscara. pocos ISA tienen min min t0, n, $4 ; t0 = min(n, 4) cambio m, 1 dólar, t0 ; m = 1 sub m, m, 1 dólar ; m = (1 0)-1 # Ahora haz la operación, enmascarada por m bits load32x4 v1, x, m load32x4 v2, Sí., m mul32x4 v1, a, v1, m ; v1:= v1 * a add32x4 v3, v1, v2, m ; v3:= v1 + v2 store32x4 v3, Sí., m # update x, y n for next loop addl x, t0*4 ; x:= x + t0*4 addl Sí., t0*4 subl n, n, t0 ; n:= n - t0 # Loop? jgz n, vloop ; volver si no 0Fuera: Ret

Aquí se puede ver que el código es mucho más limpio pero un poco complejo: al menos, sin embargo, no hay configuración ni limpieza: en la última iteración del bucle, la máscara de predicado se establecerá en 0b0000, 0b0001, 0b0011, 0b0111 o 0b1111, lo que da como resultado que se realicen entre 0 y 4 operaciones de elementos SIMD, respectivamente. Una complicación potencial adicional: algunos RISC ISA no tienen un "min" instrucción, necesitando en su lugar usar una rama o una comparación predicada escalar.

Está claro cómo el SIMD predicado al menos merece el término "capaz de vector", porque puede hacer frente a vectores de longitud variable mediante el uso de máscaras de predicado. El último paso evolutivo hacia un "verdadero" vector ISA, sin embargo, es no tener ninguna evidencia en el ISA en absoluto de un ancho SIMD, dejando eso completamente en manos del hardware.

Vector puro (verdadero) ISA

Para ISA de vector estilo Cray como RVV, una instrucción llamada "setvl" (establecer la longitud del vector) se utiliza. El hardware primero define cuántos valores de datos puede procesar en un 'vector': esto podría ser registros reales o podría ser un bucle interno (el enfoque híbrido, mencionado anteriormente). Esta cantidad máxima (la cantidad de "carriles") de hardware se denomina "MVL" (Longitud Máxima del Vector). Tenga en cuenta que, como se ve en SX-Aurora y Videocore IV, MVL puede ser una cantidad de carril de hardware real o uno virtual. (Nota: como se mencionó en el Tutorial de ARM SVE2, los programadores deben no cometer el error de suponer un ancho de vector fijo: en consecuencia, MVL no es una cantidad que el programador necesita saber. Esto puede ser un poco desconcertante después de años de mentalidad SIMD).

Al llamar a setvl con el número de elementos de datos pendientes para ser procesados, "setvl" está permitido (esencialmente requerido) para limitar eso a la longitud máxima del vector (MVL) y, por lo tanto, devuelve el número real que puede ser procesado por el hardware en instrucciones de vector posteriores, y establece el registro especial interno, & #34;VL", a esa misma cantidad. ARM se refiere a esta técnica como "independiente de la longitud del vector" programación en sus tutoriales sobre SVE2.

A continuación se muestra el ensamblador vectorial estilo Cray para el mismo bucle de estilo SIMD anterior. Tenga en cuenta que t0 (que, al contener una copia conveniente de VL, puede variar) se usa en lugar de constantes codificadas:

Vloop: setvl t0, n # VL=t0=min(MVL, n) vld32 v0, x vector de carga vld32 v1, Sí. # Carga vectorial y vmadd32 v1, v0, a # v1 += v0 * a vst32 v1, Sí. # store Y añadir Sí., t0*4 # advance y by VL*4 añadir x, t0*4 # advance x by VL*4 sub n, t0 # N -= VL (t0) Bnez n, vloop # Repetir si n!= 0

Esencialmente, esto no es muy diferente de la versión SIMD (procesa 4 elementos de datos por bucle), o de la versión Scalar inicial (procesa solo uno). n aún contiene el número de elementos de datos que quedan por procesar, pero t0 contiene la copia de VL: el número que se va a procesar en cada iteración. t0 se resta de n después de cada iteración, y si n es cero, entonces se han procesado todos los elementos.

Varias cosas a tener en cuenta, al comparar con la variante de ensamblaje Predicated SIMD:

  1. El setvl la instrucción ha incrustado dentro de ella una min instrucción
  2. Donde la variante SIMD codificaba tanto el ancho (4) en la creación de la máscara y en el ancho SIMD (load32x4 etc.) los equivalentes vector ISA no tienen tal límite. Esto hace que los programas vectoriales sean portátiles, independientes del vendedor y a prueba de futuro.
  3. Configuración de VL eficazmente crea una máscara oculta que se aplica automáticamente a los vectores
  4. Dónde con SIMD predicado el bitlength máscara se limita a lo que se puede mantener en un registro de escalar (o máscara especial), los registros de máscaras de vector ISA no tienen tal limitación. Cray... Los vectores podrían ser más de 1.000 elementos (en 1977).

Así se puede ver, muy claramente, cómo las ISA vectoriales reducen el número de instrucciones.

También tenga en cuenta que, al igual que la variante SIMD predicada, los punteros a xey avanzan t0 veces cuatro porque ambos apuntan a datos de 32 bits, pero esa n se reduce en t0 directo. En comparación con el ensamblador SIMD de tamaño fijo, hay muy poca diferencia aparente: x e y avanzan con la constante codificada 16, n se reduce con un 4 codificado, por lo que inicialmente es difícil apreciar el significado. La diferencia viene al darse cuenta de que el hardware vectorial podría ser capaz de realizar 4 operaciones simultáneas, o 64, o 10 000, sería exactamente el mismo ensamblador vectorial para todas y aún no habría un código de limpieza SIMD. Incluso en comparación con el SIMD con capacidad de predicado, sigue siendo más compacto, más claro, más elegante y utiliza menos recursos.

No solo es un programa mucho más compacto (ahorra en el tamaño de la memoria caché L1), sino que, como se mencionó anteriormente, la versión vectorial puede enviar mucho más procesamiento de datos a las ALU, lo que nuevamente ahorra energía porque la decodificación y emisión de instrucciones pueden permanecer inactivas.

Además, la cantidad de elementos que ingresan a la función puede comenzar en cero. Esto establece la longitud del vector en cero, lo que deshabilita efectivamente todas las instrucciones del vector, convirtiéndolas en no-ops, en tiempo de ejecución. Por lo tanto, a diferencia de SIMD no predicado, incluso cuando no hay elementos para procesar, no hay código de limpieza desperdiciado.

Ejemplo de reducción de vectores

Este ejemplo comienza con un algoritmo que implica reducción. Al igual que en el ejemplo anterior, se mostrará primero en instrucciones escalares, luego en SIMD y finalmente en instrucciones vectoriales, comenzando en c:

vacío ()size_t n, int a, const int x[]) {} int Sí. = 0; para ()size_t i = 0; i . n; i++) Sí. += x[i]; retorno Sí.;}

Aquí, se usa un acumulador (y) para sumar todos los valores en la matriz, x.

Ensambladora escalar

(feminine)

La versión escalar de esto cargaría cada uno de x, lo agregaría a y y repetiría:

 set Sí., 0 ; y inicializado a ceroLoop: load32 r1, x ; cargar un dato de 32 bits add32 Sí., Sí., r1 ; y:= y + r1 addl x, x, $4 ; x:= x + 4 subl n, n, 1 dólar ; n:= n - 1 jgz n, bucle ; volver atrás si no 0Fuera: Ret Sí. ; resultados de retorno, y

Esto es muy sencillo. "y" comienza en cero, los enteros de 32 bits se cargan de uno en uno en r1, se suman a y y la dirección de la matriz "x" pasó al siguiente elemento de la matriz.

Reducción SIMD

Aquí es donde empiezan los problemas. SIMD por diseño es incapaz de realizar operaciones aritméticas "entre elementos". El elemento 0 de un registro SIMD puede agregarse al elemento 0 de otro registro, pero el elemento 0 no puede agregarse a nada que no sea otro elemento 0. Esto impone algunas limitaciones severas sobre implementaciones potenciales. Por simplicidad se puede suponer que n es exactamente 8:

 addl r3, x, 16 dólares ; para el segundo 4 de x load32x4 v1, x ; primero 4 de x load32x4 v2, r3 ; 2o 4 de x add32x4 v1, v2, v1 ; añadir 2 grupos

En este punto se han realizado cuatro adiciones:

  • x[0]+x[4] - Primer ADD SIMD: elemento 0 del primer grupo añadido al elemento 0 del segundo grupo
  • x[1]+x[5] - Segundo ADD SIMD: elemento 1 del primer grupo añadido al elemento 1 del segundo grupo
  • x[2]+x[6] - Tercer ADD SIMD: elemento 2 del primer grupo añadido al elemento 2 del segundo grupo
  • x[3]+x[7] - Cuarto ADD SIMD: elemento 3 del primer grupo añadido al elemento 2 del segundo grupo

pero con SIMD de 4 anchos siendo incapaz por diseño de agregar x[0]+x[1] por ejemplo, las cosas van cuesta abajo rápidamente tal como sucedió con el caso general de usar SIMD para bucles IAXPY de propósito general. Para sumar los cuatro resultados parciales, se pueden usar SIMD de dos anchos, seguidos de una sola suma escalar, para finalmente producir la respuesta, pero, con frecuencia, los datos deben transferirse fuera de los registros SIMD dedicados antes de que se pueda realizar el último cálculo escalar..

Incluso con un bucle general (n no fijo), la única forma de usar SIMD de 4 anchos es asumir cuatro "flujos" separados, cada uno compensado por cuatro elementos. Finalmente, se deben sumar los cuatro resultados parciales. Otras técnicas implican barajar: se pueden encontrar ejemplos en línea para AVX-512 de cómo hacer "Suma horizontal"

Además del tamaño del programa y la complejidad, surge un problema potencial adicional si se involucra el cálculo de coma flotante: el hecho de que los valores no se suman en orden estricto (cuatro resultados parciales) podría generar errores de redondeo.

Reducción del vector ISA

Los conjuntos de instrucciones vectoriales tienen operaciones de reducción aritmética integradas en ISA. Si se supone que n es menor o igual a la longitud máxima del vector, solo se requieren tres instrucciones:

 setvl t0, n # VL=t0=min(MVL, n) vld32 v0, x vector de carga vredadd32 Sí., v0 # reduce-add into y

El código cuando n es mayor que la longitud máxima del vector no es mucho más complejo y es un patrón similar al del primer ejemplo ("IAXPY").

 set Sí., 0Vloop: setvl t0, n # VL=t0=min(MVL, n) vld32 v0, x vector de carga vredadd32 Sí., Sí., v0 # add all x into y añadir x, t0*4 # advance x by VL*4 sub n, t0 # N -= VL (t0) Bnez n, vloop # Repetir si n!= 0 Ret Sí.

La simplicidad del algoritmo es marcada en comparación con SIMD. Nuevamente, al igual que con el ejemplo IAXPY, el algoritmo es independiente de la longitud (incluso en implementaciones integradas donde la longitud máxima del vector podría ser solo una).

Las implementaciones en hardware pueden, si están seguras de que se producirá la respuesta correcta, realizar la reducción en paralelo. Algunas ISA de vector ofrecen un modo de reducción paralelo como una opción explícita, para cuando el programador sabe que los posibles errores de redondeo no importan y que la baja latencia es fundamental.

Este ejemplo vuelve a resaltar una diferencia fundamental clave entre los procesadores vectoriales reales y los procesadores SIMD, incluida la mayoría de las GPU comerciales, que se inspiran en las características de los procesadores vectoriales.

Perspectivas a partir de ejemplos

En comparación con cualquier procesador SIMD que afirme ser un procesador vectorial, la reducción del orden de magnitud en el tamaño del programa es casi impactante. Sin embargo, este nivel de elegancia a nivel ISA tiene un precio bastante alto a nivel de hardware:

  1. Desde el ejemplo IAXPY, se puede ver que a diferencia de los procesadores SIMD, que pueden simplificar su hardware interno evitando tratar con el acceso a la memoria desalineado, un procesador vectorial no puede salirse con tal simplificación: se escriben algoritmos que dependen inherentemente de Vector Load y Store siendo exitoso, independientemente de la alineación del inicio del vector.
  2. Aunque desde el ejemplo de reducción se puede ver que, aparte de las instrucciones de permute, SIMD por definición evita las operaciones inter-lane por completo (elemento 0 sólo se puede añadir a otro elemento 0), los procesadores vectoriales abordan este encabezado. Lo que los programadores se ven obligados a hacer en el software (utilizando shuffle y otros trucos, para intercambiar datos en los procesadores vectoriales "lane") correctos deben hacer en hardware, automáticamente.

En general, existe la opción de tener

  1. software complejo y hardware simplificado (SIMD)
  2. software simplificado y hardware complejo (procesadores de vehículos)

Estas marcadas diferencias son lo que distingue a un procesador vectorial de uno que tiene SIMD.

Características del procesador vectorial

Donde muchos SIMD ISA toman prestado o están inspirados en la lista a continuación, las características típicas que tendrá un procesador vectorial son:

Características de procesamiento de vectores GPU

Con muchas aplicaciones de sombreado 3D que necesitan operaciones trigonométricas, así como vectores cortos para operaciones comunes (RGB, ARGB, XYZ, XYZW), la compatibilidad con lo siguiente suele estar presente en las GPU modernas, además de las que se encuentran en los procesadores vectoriales:

Falla (o falla) primero

Introducido en ARM SVE2 y RISC-V RVV está el concepto de cargas vectoriales secuenciales especulativas. ARM SVE2 tiene un registro especial llamado "Registro de primera falla", donde RVV modifica (trunca) la longitud del vector (VL).

El principio básico de ffirst es intentar una gran carga vectorial secuencial, pero permitir que el hardware trunque arbitrariamente la cantidad real cargada a la cantidad que tendría éxito sin generar una falla de memoria o simplemente a una cantidad (mayor que cero) que sea más conveniente. El factor importante es que las instrucciones subsiguientes se notifican o pueden determinar exactamente cuántas cargas realmente se realizaron correctamente, utilizando esa cantidad para llevar a cabo solo el trabajo en los datos que se han cargado realmente.

Compare esta situación con SIMD, que es un ancho de carga fijo (inflexible) y un ancho de procesamiento de datos fijo, incapaz de hacer frente a las cargas que cruzan los límites de la página, e incluso si lo fueran, no pueden adaptarse a lo que realmente tuvo éxito, sin embargo., paradójicamente, si el programa SIMD incluso intentara descubrir por adelantado (en cada ciclo interno, cada vez) qué podría tener un éxito óptimo, esas instrucciones solo servirían para obstaculizar el rendimiento porque, por necesidad, serían parte del crítico interno. bucle.

Esto comienza a insinuar la razón por la que ffirst es tan innovador, y se ilustra mejor con memcpy o strcpy cuando se implementa con SIMD estándar de 128 bits no predicado que no es ffirst. Para IBM POWER9, la cantidad de instrucciones optimizadas a mano para implementar strncpy supera las 240. Por el contrario, la misma rutina estricta en el ensamblador RVV optimizado a mano es de solo 22 instrucciones.

El ejemplo anterior de SIMD podría fallar y fallar al final de la memoria, debido a los intentos de leer demasiados valores: también podría causar un número significativo de fallas de página o desalineadas al cruzar los límites de manera similar. Por el contrario, al permitir que la arquitectura vectorial tenga la libertad de decidir cuántos elementos cargar, la primera parte de un strncpy, si comienza inicialmente en un límite de memoria subóptimo, puede devolver suficientes cargas de modo que en subsecuente iteraciones del ciclo, los lotes de lecturas de memoria vectorizada se alinean de manera óptima con los cachés subyacentes y los arreglos de memoria virtual. Además, el hardware puede optar por aprovechar la oportunidad para finalizar las lecturas de memoria de cualquier iteración de ciclo dada exactamente en un límite de página (evitando una costosa segunda búsqueda de TLB), con una ejecución especulativa preparando el siguiente virtual página de memoria mientras los datos aún se están procesando en el ciclo actual. Todo esto está determinado por el hardware, no por el programa en sí.

Rendimiento y aceleración

Sea r la razón de velocidad del vector y f la razón de vectorización. Si el tiempo que le toma a la unidad vectorial agregar una matriz de 64 números es 10 veces más rápido que su contraparte escalar equivalente, r = 10. Además, si el número total de operaciones en un programa es 100, de las cuales solo 10 son escalares (después de la vectorización), entonces f = 0.9, es decir, el 90% del trabajo lo realiza la unidad vectorial. Sigue la velocidad alcanzable de:

Así, incluso si el rendimiento de la unidad vectorial es muy alto (Hay una velocidad menos que , lo que sugiere que la relación f es crucial para el rendimiento. Esta relación depende de la eficiencia de la compilación como la adyacencia de los elementos en la memoria.