for - cuda reference
En CUDA, ¿qué es la unión de la memoria y cómo se logra? (4)
La fusión de memoria es una técnica que permite el uso óptimo del ancho de banda de la memoria global. Es decir, cuando las hebras paralelas ejecutan el mismo acceso de instrucción a ubicaciones consecutivas en la memoria global, se logra el patrón de acceso más favorable.
El ejemplo de la Figura anterior ayuda a explicar la disposición fusionada:
En la Fig. (A), n vectores de longitud m se almacenan de manera lineal. El elemento i del vector j se denota por v j i . Cada hilo en el núcleo de GPU se asigna a un vector de longitud m . Los subprocesos en CUDA se agrupan en una matriz de bloques y cada subproceso en GPU tiene una identificación única que se puede definir como indx=bd*bx+tx
, donde bd
representa la dimensión del bloque, bx
denota el índice del bloque y tx
es el índice del hilo en cada bloque
Las flechas verticales demuestran el caso de que los hilos paralelos acceden a los primeros componentes de cada vector, es decir, a las direcciones 0, m , 2m ... de la memoria. Como se muestra en la Fig. (A), en este caso el acceso a la memoria no es consecutivo. Al reducir a cero el espacio entre estas direcciones (las flechas rojas que se muestran en la figura anterior), el acceso a la memoria se fusiona.
Sin embargo, el problema se vuelve un poco complicado aquí, ya que el tamaño permitido de subprocesos residentes por bloque de GPU se limita a bd
. Por lo tanto, la disposición de datos combinados se puede realizar almacenando los primeros elementos de los primeros vectores bd
en orden consecutivo, seguido de los primeros elementos de los segundos vectores bd y así sucesivamente. El resto de elementos de vectores se almacenan de manera similar, como se muestra en la Fig. (B). Si n (número de vectores) no es un factor de bd
, es necesario rellenar los datos restantes en el último bloque con algún valor trivial, por ejemplo, 0.
En el almacenamiento de datos lineal en la Fig. (A), el componente i (0 ≤ i < m ) del vector indx (0 ≤ indx < n ) se aborda mediante m × indx +i
; el mismo componente en el patrón de almacenamiento coalescido en la Fig. (b) se trata como
(m × bd) ixC + bd × ixB + ixA
,
donde ixC = floor[(m.indx + j )/(m.bd)]= bx
, ixB = j
y ixA = mod(indx,bd) = tx
.
En resumen, en el ejemplo de almacenamiento de varios vectores con tamaño m , la indexación lineal se asigna a la indexación fusionada de acuerdo con:
m.indx +i −→ m.bd.bx +i .bd +tx
Este reordenamiento de datos puede llevar a un ancho de banda de memoria significativamente mayor en la memoria global de la GPU.
fuente: "Aceleración basada en GPU de cálculos en el análisis de deformación de elementos finitos no lineales". Revista internacional de métodos numéricos en ingeniería biomédica (2013).
¿Qué es "fusionado" en la transacción de memoria global de CUDA? No pude entender, incluso después de pasar por mi guía CUDA. ¿Cómo hacerlo? En el ejemplo de matriz de la guía de programación CUDA, el acceso a la matriz fila por fila se denomina "fusionado" o col .. por col .. se denomina fusionado? ¿Cuál es correcto y por qué?
Los criterios para la fusión están bien documentados en la Guía de programación CUDA 3.2 , Sección G.3.2. La versión corta es la siguiente: los hilos en la urdimbre deben estar accediendo a la memoria en secuencia, y las palabras a las que se accede deben> = 32 bits. Además, la dirección base a la que accede la deformación debe ser de 64, 128 o 256 bytes alineada para los accesos de 32, 64 y 128 bits, respectivamente.
El hardware Tesla2 y Fermi hace un buen trabajo al unir los accesos de 8 y 16 bits, pero es mejor evitarlos si quiere un ancho de banda máximo.
Tenga en cuenta que a pesar de las mejoras en el hardware Tesla2 y Fermi, la fusión es, de ninguna manera, obsoleta. Incluso en el hardware de clase Tesla2 o Fermi, el no poder fusionar las transacciones de memoria global puede dar como resultado un impacto de rendimiento 2x. (En el hardware de clase Fermi, esto parece ser cierto solo cuando ECC está habilitado. Las transacciones de memoria contiguas pero no unidas tienen aproximadamente un 20% de impacto en Fermi).
Si los subprocesos de un bloque acceden a ubicaciones de memoria global consecutivas, entonces todos los accesos se combinan en una sola solicitud (o combinada) por el hardware. En el ejemplo de la matriz, los elementos de la matriz en la fila se organizan linealmente, seguidos de la siguiente fila, y así sucesivamente. Para, por ejemplo, matriz 2x2 y 2 hilos en un bloque, las ubicaciones de memoria se organizan como:
(0,0) (0,1) (1,0) (1,1)
En el acceso de fila, subprocesos 1 accede (0,0) y (1,0) que no se pueden fusionar. En el acceso a la columna, el subproceso 1 accede (0,0) y (0,1) que se pueden unir porque son adyacentes.
Es probable que esta información solo se aplique a la capacidad de cálculo 1.x o cuda 2.0. Las arquitecturas más recientes y cuda 3.0 tienen un acceso de memoria global más sofisticado y, de hecho, las "cargas globales unidas" ni siquiera se perfilan para estos chips.
Además, esta lógica se puede aplicar a la memoria compartida para evitar conflictos bancarios.
Una transacción de memoria unida es aquella en la que todos los subprocesos en una memoria global de medio warp acceden al mismo tiempo. Esto es demasiado simple, pero la forma correcta de hacerlo es tener hilos consecutivos que accedan a direcciones de memoria consecutivas.
Por lo tanto, si las hebras 0, 1, 2 y 3 leen memoria global 0x0, 0x4, 0x8 y 0xc, debe ser una lectura unida.
En un ejemplo de matriz, tenga en cuenta que desea que su matriz resida linealmente en la memoria. Puede hacer lo que quiera, y su acceso a la memoria debe reflejar cómo se presenta su matriz. Por lo tanto, la matriz de 3x4 a continuación
0 1 2 3
4 5 6 7
8 9 a b
se podría hacer fila tras fila, así, de modo que (r, c) se asigne a la memoria (r * 4 + c)
0 1 2 3 4 5 6 7 8 9 a b
Supongamos que necesitas acceder al elemento una vez y dices que tienes cuatro hilos. ¿Qué hilos se utilizarán para qué elemento? Probablemente tampoco
thread 0: 0, 1, 2
thread 1: 3, 4, 5
thread 2: 6, 7, 8
thread 3: 9, a, b
o
thread 0: 0, 4, 8
thread 1: 1, 5, 9
thread 2: 2, 6, a
thread 3: 3, 7, b
¿Cual es mejor? ¿Qué resultará en lecturas unidas y cuáles no?
De cualquier manera, cada hilo hace tres accesos. Veamos el primer acceso y veamos si los hilos acceden a la memoria de forma consecutiva. En la primera opción, el primer acceso es 0, 3, 6, 9. No consecutivo, no fusionado. La segunda opción, es 0, 1, 2, 3. Consecutiva! Unida ¡Hurra!
Probablemente, la mejor manera es escribir su kernel y luego crear un perfil para ver si tiene cargas y tiendas globales no fusionadas.