{"schemaVersion":1,"id":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute","url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute","title":"Programación GPU: CUDA vs ROCm vs Vulkan Compute, Warp Scheduling, Shared Memory y Multiplicación de Matrices","description":"Más allá de los tutoriales, warp divergence, bank conflicts de shared memory, tuning de occupancy, y un kernel de multiplicación de matrices que realmente rinde. Con benchmarks.","language":"es","datePublished":"2026-03-06","dateModified":null,"contentFormat":"text/markdown","content":"GPU Computing\n\n# Programación GPU: CUDA vs ROCm vs Vulkan Compute, Warp Scheduling, Shared Memory y Multiplicación de Matrices\n\n2026-03-069 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/gpu-programming-cuda-vs-rocm-vulkan-compute>)\n\n## El Modelo de Ejecución: Warps, Wavefronts y Subgroups\n\nA continuación se describe qué ocurre cuando el código se ejecuta en la GPU.\n\n### NVIDIA (CUDA): Warps de 32\n\nLa unidad de ejecución fundamental en NVIDIA ([CUDA](<https://developer.nvidia.com/cuda-toolkit>)) es el **warp**, 32 threads que ejecutan la misma instrucción en lockstep (SIMT: Single Instruction, Multiple Threads). Un Streaming Multiprocessor (SM) puede schedular múltiples warps, pero dentro de un solo warp, los 32 threads están en el mismo program counter.\n\n```cuda\n// Esto parece inocente pero asesina el rendimiento\n__global__ void bad_branch(float* data, int n) {\n int tid = threadIdx.x + blockIdx.x * blockDim.x;\n if (tid < n) {\n if (data[tid] > 0.0f) {\n data[tid] = sqrtf(data[tid]); // Camino A\n } else {\n data[tid] = -sqrtf(-data[tid]); // Camino B\n }\n }\n}\n// Si threads 0-15 toman Camino A y threads 16-31 toman Camino B,\n// el warp ejecuta AMBOS caminos secuencialmente. 50% utilización.\n```\n\n### AMD (ROCm/HIP): Wavefronts de 64\n\nEl equivalente de AMD es el **wavefront**, 64 threads en arquitecturas CDNA/RDNA. Esto no es solo una diferencia de nombre; cambia fundamentalmente cómo considerars sobre occupancy y primitise va a nivel warp.\n\n```cpp\n// Código HIP, se ve idéntico a CUDA pero wavefront = 64\n__global__ void hip_reduce(float* data, float* result, int n) {\n __shared__ float sdata[256];\n int tid = threadIdx.x;\n\n sdata[tid] = (tid < n) ? data[tid] : 0.0f;\n __syncthreads();\n\n // PELIGRO: esta reducción asume warp size de 32\n // En AMD, las últimas iteraciones 'unrolled' necesitan cubrir 64 threads\n for (int s = blockDim.x / 2; s > 32; s >>= 1) {\n if (tid < s) sdata[tid] += sdata[tid + s];\n __syncthreads();\n }\n\n // Este unrolling a nivel warp está MAL en AMD\n if (tid < 32) { // Debería ser < 64 en AMD!\n volatile float* vsmem = sdata;\n vsmem[tid] += vsmem[tid + 32];\n vsmem[tid] += vsmem[tid + 16];\n vsmem[tid] += vsmem[tid + 8];\n vsmem[tid] += vsmem[tid + 4];\n vsmem[tid] += vsmem[tid + 2];\n vsmem[tid] += vsmem[tid + 1];\n }\n}\n```\n\nEste bug exacto aparece al portar codebases CUDA a ROCm para clusters MI250X. ` hipify-perl ` traduce la sintaxis perfecto pero no marca la suposición de warp size. Los resultados pueden estar mal por ~3%, lo suficientemente cerca como para pasar tests unitarios con tolerancias flojas pero completamente incorrecto para simulaciones de física.\n\n### Vulkan Compute: Subgroups\n\nVulkan no especifica un tamaño de subgroup. Puede ser 4, 8, 16, 32, 64 o incluso 128 dependiendo del hardware y driver. Lo se consulta en runtime:\n\n```glsl\n#version 450\n#extension GL_KHR_shader_subgroup_basic : enable\n#extension GL_KHR_shader_subgroup_arithmetic : enable\n\nlayout(local_size_x = 256) in;\nlayout(set = 0, binding = 0) buffer Data { float data[]; };\nlayout(set = 0, binding = 1) buffer Result { float result; };\n\nvoid main() {\n uint tid = gl_LocalInvocationID.x;\n\n // subgroupSize es un built-in, puede ser 32, 64, etc.\n float val = data[tid];\n float subgroup_sum = subgroupAdd(val);\n\n if (subgroupElect()) {\n atomicAdd(result, subgroup_sum);\n }\n}\n```\n\n## Shared Memory: El Recurso que Hace o Deshace\n\nLa shared memory (CUDA/HIP) es la SRAM rápida on-chip que hace la computación GPU práctica. En GPUs NVIDIA modernas, está organizada en 32 banks, cada uno de 4 bytes de ancho.\n\n### Bank Conflicts: El Asesino Silencioso\n\n```cuda\n// Matriz almacenada en shared memory: 32 x 32 floats\n__shared__ float tile[32][32];\n\n// Thread (tx, ty) lee por columna:\n// Thread 0 lee tile[0][0] -> bank 0\n// Thread 1 lee tile[1][0] -> bank 0 (!!!)\n// Thread 2 lee tile[2][0] -> bank 0 (!!!)\n// ... los 32 threads pegan en bank 0 -> conflicto de 32 vías!\n\n// Fix: paddear cada fila con 1 elemento\n__shared__ float tile[32][33]; // <-- el número mágico\n\n// Ahora:\n// Thread 0 lee tile[0][0] -> bank 0\n// Thread 1 lee tile[1][0] -> bank 1 (offset por 33 % 32 = 1)\n// Thread 2 lee tile[2][0] -> bank 2\n// ... los 32 threads pegan en banks distintos -> sin conflicto!\n```\n\nEse ` [32][33] ` en vez de ` [32][32] ` es el tipo de cosa que convierte un kernel al 40% de pico en uno al 80%. Un elemento extra por fila. Este solo cambio puede dar speedups de 2x en kernels memory-bound.\n\n## Multiplicación de Matrices: SGEMM Optimizado\n\nEl triple loop naive es insuficiente para uso en producción. El siguiente kernel SGEMM tileado fue optimizado iterativamente. Esta versión consigue ~75% del rendimiento de cuBLAS en una A100:\n\n```cuda\n#define BM 128\n#define BN 128\n#define BK 8\n#define TM 8\n#define TN 8\n\n__global__ void sgemm_optimized(\n const float* __restrict__ A,\n const float* __restrict__ B,\n float* __restrict__ C,\n int M, int N, int K\n) {\n const int bx = blockIdx.x;\n const int by = blockIdx.y;\n const int tx = threadIdx.x;\n const int ty = threadIdx.y;\n\n __shared__ float As[BK][BM + 1]; // Transpuesta para coalescing\n __shared__ float Bs[BK][BN + 1];\n\n float accum[TM][TN] = {0.0f};\n float a_reg[TM];\n float b_reg[TN];\n\n const int row_base = by * BM;\n const int col_base = bx * BN;\n\n for (int k = 0; k < K; k += BK) {\n // Carga colaborativa de tile A en shared memory\n #pragma unroll\n for (int i = 0; i < BM; i += blockDim.y) {\n int row = row_base + ty + i;\n int col = k + tx;\n if (row < M && col < K) {\n As[tx][ty + i] = A[row * K + col];\n } else {\n As[tx][ty + i] = 0.0f;\n }\n }\n\n #pragma unroll\n for (int i = 0; i < BN; i += blockDim.y) {\n int row = k + ty;\n int col = col_base + tx + i;\n if (row < K && col < N) {\n Bs[ty][tx + i] = B[row * N + col];\n } else {\n Bs[ty][tx + i] = 0.0f;\n }\n }\n\n __syncthreads();\n\n #pragma unroll\n for (int kk = 0; kk < BK; kk++) {\n #pragma unroll\n for (int m = 0; m < TM; m++) {\n a_reg[m] = As[kk][ty * TM + m];\n }\n #pragma unroll\n for (int n = 0; n < TN; n++) {\n b_reg[n] = Bs[kk][tx * TN + n];\n }\n #pragma unroll\n for (int m = 0; m < TM; m++) {\n #pragma unroll\n for (int n = 0; n < TN; n++) {\n accum[m][n] += a_reg[m] * b_reg[n];\n }\n }\n }\n\n __syncthreads();\n }\n\n #pragma unroll\n for (int m = 0; m < TM; m++) {\n #pragma unroll\n for (int n = 0; n < TN; n++) {\n int row = row_base + ty * TM + m;\n int col = col_base + tx * TN + n;\n if (row < M && col < N) {\n C[row * N + col] = accum[m][n];\n }\n }\n }\n}\n```\n\nDecisiones clave de optimización:\n\n1. **Store transpuesto en shared memory para A**, Cuando los threads leen una columna de ` As `, acceden a filas consecutivas que mapean a banks distintos.\n2. **Register tiling (TM x TN)**, Cada thread computa un sub-tile de 8x8, amortizando loads de shared memory sobre 64 operaciones FMA por paso de K.\n3. **Punteros ` __restrict__ `**, Le dice al compilador que A, B, C no se solapan en memoria.\n4. **` #pragma unroll `**, Crítico para los inner loops.\n\n## El Port a ROCm: No es Solo Find-and-Replace\n\nAl portar esto a HIP para MI250X, surgen varios problemas no obvios. La shared memory de AMD es 64 KB por CU vs los 164 KB configurables de NVIDIA en A100. Si tu estrategia de tiling necesita más de 64 KB, se necesita tiles más pequeños en AMD, lo que cambia toda tu estrategia de optimización.\n\n## Benchmarks: Números Reales en Hardware Real\n\nLos siguientes benchmarks muestran SGEMM 4096x4096 en tres setups:\n\n| Implementación | Hardware | TFLOPS | % del Pico |\n| --- | --- | --- | --- |\n| cuBLAS | A100 80GB | 17.8 | 91% |\n| Kernel CUDA custom | A100 80GB | 14.7 | 75% |\n| rocBLAS | MI250X | 15.2 | 88% |\n| Kernel HIP custom | MI250X | 11.9 | 69% |\n| Vulkan Compute (GLSL) | RTX 4090 | 9.3 | 56% |\n| Vulkan Compute (GLSL) | RX 7900 XTX | 7.1 | 52% |\n\nLas bibliotecas del vendor (cuBLAS, rocBLAS) hacen trucos a nivel assembly que son impracticables de replicar en código de alto nivel, cosas como software pipelining de loads de shared memory y asignación explícita de registros.\n\n## Profiling: Distribución del Tiempo de Ejecución\n\nLa habilidad más valiosa en programación GPU es leer un output de profiler. Para el kernel CUDA:\n\n```\nKernel: sgemm_optimized\nDuration: 2.31 ms\nTheoretical Occupancy: 75%\nAchieved Occupancy: 71.2%\nSM Throughput: 87.3%\nMemory Throughput: 62.1% <-- No es memory bound\nCompute Throughput: 87.3% <-- Es compute bound\n\nL1/TEX Hit Rate: 94.7%\nShared Memory Bank Conflicts: 0 <-- El padding funcionó\n\nWarp Stall Reasons:\n stall_mio_throttle: 23.1% <-- Ancho de banda de shared memory\n stall_math_pipe_throttle: 18.7% <-- Bien! La ALU está saturada\n stall_short_scoreboard: 15.2% <-- Esperando resultados MIO\n stall_not_selected: 12.4% <-- Scheduler no pudo elegir este warp\n```\n\nEse ` stall_mio_throttle ` al 23.1% indica que el ancho de banda de shared memory es el próximo bottleneck, el kernel carga de shared memory más rápido de lo que el hardware puede servir. El fix sería aumentar el register tile size (TM/TN) para hacer más computación por load de shared memory, al costo de más presión de registros y potencialmente menor occupancy.\n\nEste es el juego. Cada optimización crea un nuevo bottleneck. Terminaste cuando el profiler te muestra que se está limitado por algo fundamental, throughput de ALU, ancho de banda de memoria, o límites de occupancy por presión de registros.\n\n## Framework de Decisión\n\nUna evaluación honesta de los tres ecosistemas tras uso extensivo:\n\n**CUDA** tiene el mejor tooling por lejos. Si se está en hardware NVIDIA, no hay razón para usar otra cosa para compute.\n\n**ROCm** mejoró dramáticamente pero todavía se siente 2 años atrás de CUDA en tooling. El lenguaje HIP en sí es 98% source-compatible con CUDA, lo que hace el porting directo. Vale la pena si teners hardware AMD.\n\n**Vulkan Compute** es la opción correcta cuando se necesita una codebase corriendo en Intel, NVIDIA, AMD y hasta GPUs mobile. El trade-off es menos control sobre específicos del hardware y una API más verbosa.\n\nElegir basándote en tus restricciones, no en tribalismo. Y por favor, profileá antes de optimizar.\n\n## Preguntas frecuentes\n\n### ¿Cuándo debería usar CUDA vs ROCm vs Vulkan Compute?\n\nCUDA si se es solo NVIDIA y se desea el mejor tooling y ecosistema. ROCm si apuntás a GPUs AMD (especialmente MI250/MI300 para workloads de ML). Vulkan Compute si se necesita portabilidad cross-vendor o se está haciendo compute GPU en un contexto que ya usa Vulkan para gráficos.\n\n### ¿Qué es warp divergence y por qué destruye el rendimiento?\n\nUn warp (32 threads en NVIDIA, 64 en AMD como 'wavefront') ejecuta en lockstep. Cuando threads del mismo warp toman ramas diferentes en un if/else, el hardware serializa ambos paths, threads que no están en el path activo quedan enmascarados pero siguen consumiendo ciclos.\n\n### ¿Cómo afectan los bank conflicts de shared memory al rendimiento?\n\nLa shared memory se divide en 32 banks. Cuando múltiples threads de un warp acceden a direcciones diferentes que mapean al mismo banco simultáneamente, los accese es se serializan. Un bank conflict de 32 vías significa acceso 32x más lento. Paddear los arrays de shared memory con 1 elemento por fila frecuentemente elimina estos conflictos.\n\n## Tips\n\n1. **Tip:** Perfilar con ` ncu --set full ` (Nsight Compute) antes de tocar el código de tu kernel. Es común ver equipos pasar días 'optimizando' un kernel que era memory-bound, agregando instrucciones aritméticas que el scheduler escondía detrás de la latencia de memoria de todas formas. Primero conocé tu bottleneck.\n2. **Tip:** En GPUs AMD, el wavefront size es 64 (no 32 como los warps de CUDA). Si se porta código CUDA a ROCm con ` hipify-perl `, auditá cada ` __shfl_sync ` y primitiva a nivel warp manualmente, la traducción automática clava la sintaxis pero se equivoca en la semántica para algoritmos que dependen del warp size.\n3. **Tip:** Para Vulkan Compute, siempre configurar ` VK_KHR_shader_non_semantic_info ` en las extensiones de device y compilar GLSL con el flag ` -g `. Sin eso, RenderDoc muestra el compute shader como un blob opaco y la depuración se vuelve conjetural.\n\n- cuda\n- rocm\n- vulkan-compute\n- programación-gpu\n- multiplicación-matrices\n- warp-scheduling\n- shared-memory\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)","headings":[{"level":1,"text":"Programación GPU: CUDA vs ROCm vs Vulkan Compute, Warp Scheduling, Shared Memory y Multiplicación de Matrices","url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#title"},{"level":2,"text":"El Modelo de Ejecución: Warps, Wavefronts y Subgroups","url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#el-modelo-de-ejecucin-warps-wavefronts-y-subgroups"},{"level":3,"text":"NVIDIA (CUDA): Warps de 32","url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#nvidia-cuda-warps-de-32"},{"level":3,"text":"AMD (ROCm/HIP): Wavefronts de 64","url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#amd-rocmhip-wavefronts-de-64"},{"level":3,"text":"Vulkan Compute: Subgroups","url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#vulkan-compute-subgroups"},{"level":2,"text":"Shared Memory: El Recurso que Hace o Deshace","url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#shared-memory-el-recurso-que-hace-o-deshace"},{"level":3,"text":"Bank Conflicts: El Asesino Silencioso","url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#bank-conflicts-el-asesino-silencioso"},{"level":2,"text":"Multiplicación de Matrices: SGEMM Optimizado","url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#multiplicacin-de-matrices-sgemm-optimizado"},{"level":2,"text":"El Port a ROCm: No es Solo Find-and-Replace","url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#el-port-a-rocm-no-es-solo-find-and-replace"},{"level":2,"text":"Benchmarks: Números Reales en Hardware Real","url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#benchmarks-nmeros-reales-en-hardware-real"},{"level":2,"text":"Profiling: Distribución del Tiempo de Ejecución","url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#profiling-distribucin-del-tiempo-de-ejecucin"},{"level":2,"text":"Framework de Decisión","url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#framework-de-decisin"},{"level":2,"text":"Preguntas frecuentes","url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#questions"},{"level":3,"text":"¿Cuándo debería usar CUDA vs ROCm vs Vulkan Compute?","url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#question-1"},{"level":3,"text":"¿Qué es warp divergence y por qué destruye el rendimiento?","url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#question-2"},{"level":3,"text":"¿Cómo afectan los bank conflicts de shared memory al rendimiento?","url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#article-tips"}],"sections":[{"heading":"Programación GPU: CUDA vs ROCm vs Vulkan Compute, Warp Scheduling, Shared Memory y Multiplicación de Matrices","level":1,"url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute","content":"GPU Computing"},{"heading":"Programación GPU: CUDA vs ROCm vs Vulkan Compute, Warp Scheduling, Shared Memory y Multiplicación de Matrices","level":1,"url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#title","content":"2026-03-069 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/gpu-programming-cuda-vs-rocm-vulkan-compute>)"},{"heading":"El Modelo de Ejecución: Warps, Wavefronts y Subgroups","level":2,"url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#el-modelo-de-ejecucin-warps-wavefronts-y-subgroups","content":"A continuación se describe qué ocurre cuando el código se ejecuta en la GPU."},{"heading":"NVIDIA (CUDA): Warps de 32","level":3,"url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#nvidia-cuda-warps-de-32","content":"La unidad de ejecución fundamental en NVIDIA ([CUDA](<https://developer.nvidia.com/cuda-toolkit>)) es el **warp**, 32 threads que ejecutan la misma instrucción en lockstep (SIMT: Single Instruction, Multiple Threads). Un Streaming Multiprocessor (SM) puede schedular múltiples warps, pero dentro de un solo warp, los 32 threads están en el mismo program counter.\n\n```cuda\n// Esto parece inocente pero asesina el rendimiento\n__global__ void bad_branch(float* data, int n) {\n int tid = threadIdx.x + blockIdx.x * blockDim.x;\n if (tid < n) {\n if (data[tid] > 0.0f) {\n data[tid] = sqrtf(data[tid]); // Camino A\n } else {\n data[tid] = -sqrtf(-data[tid]); // Camino B\n }\n }\n}\n// Si threads 0-15 toman Camino A y threads 16-31 toman Camino B,\n// el warp ejecuta AMBOS caminos secuencialmente. 50% utilización.\n```"},{"heading":"AMD (ROCm/HIP): Wavefronts de 64","level":3,"url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#amd-rocmhip-wavefronts-de-64","content":"El equivalente de AMD es el **wavefront**, 64 threads en arquitecturas CDNA/RDNA. Esto no es solo una diferencia de nombre; cambia fundamentalmente cómo considerars sobre occupancy y primitise va a nivel warp.\n\n```cpp\n// Código HIP, se ve idéntico a CUDA pero wavefront = 64\n__global__ void hip_reduce(float* data, float* result, int n) {\n __shared__ float sdata[256];\n int tid = threadIdx.x;\n\n sdata[tid] = (tid < n) ? data[tid] : 0.0f;\n __syncthreads();\n\n // PELIGRO: esta reducción asume warp size de 32\n // En AMD, las últimas iteraciones 'unrolled' necesitan cubrir 64 threads\n for (int s = blockDim.x / 2; s > 32; s >>= 1) {\n if (tid < s) sdata[tid] += sdata[tid + s];\n __syncthreads();\n }\n\n // Este unrolling a nivel warp está MAL en AMD\n if (tid < 32) { // Debería ser < 64 en AMD!\n volatile float* vsmem = sdata;\n vsmem[tid] += vsmem[tid + 32];\n vsmem[tid] += vsmem[tid + 16];\n vsmem[tid] += vsmem[tid + 8];\n vsmem[tid] += vsmem[tid + 4];\n vsmem[tid] += vsmem[tid + 2];\n vsmem[tid] += vsmem[tid + 1];\n }\n}\n```\n\nEste bug exacto aparece al portar codebases CUDA a ROCm para clusters MI250X. ` hipify-perl ` traduce la sintaxis perfecto pero no marca la suposición de warp size. Los resultados pueden estar mal por ~3%, lo suficientemente cerca como para pasar tests unitarios con tolerancias flojas pero completamente incorrecto para simulaciones de física."},{"heading":"Vulkan Compute: Subgroups","level":3,"url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#vulkan-compute-subgroups","content":"Vulkan no especifica un tamaño de subgroup. Puede ser 4, 8, 16, 32, 64 o incluso 128 dependiendo del hardware y driver. Lo se consulta en runtime:\n\n```glsl\n#version 450\n#extension GL_KHR_shader_subgroup_basic : enable\n#extension GL_KHR_shader_subgroup_arithmetic : enable\n\nlayout(local_size_x = 256) in;\nlayout(set = 0, binding = 0) buffer Data { float data[]; };\nlayout(set = 0, binding = 1) buffer Result { float result; };\n\nvoid main() {\n uint tid = gl_LocalInvocationID.x;\n\n // subgroupSize es un built-in, puede ser 32, 64, etc.\n float val = data[tid];\n float subgroup_sum = subgroupAdd(val);\n\n if (subgroupElect()) {\n atomicAdd(result, subgroup_sum);\n }\n}\n```"},{"heading":"Shared Memory: El Recurso que Hace o Deshace","level":2,"url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#shared-memory-el-recurso-que-hace-o-deshace","content":"La shared memory (CUDA/HIP) es la SRAM rápida on-chip que hace la computación GPU práctica. En GPUs NVIDIA modernas, está organizada en 32 banks, cada uno de 4 bytes de ancho."},{"heading":"Bank Conflicts: El Asesino Silencioso","level":3,"url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#bank-conflicts-el-asesino-silencioso","content":"```cuda\n// Matriz almacenada en shared memory: 32 x 32 floats\n__shared__ float tile[32][32];\n\n// Thread (tx, ty) lee por columna:\n// Thread 0 lee tile[0][0] -> bank 0\n// Thread 1 lee tile[1][0] -> bank 0 (!!!)\n// Thread 2 lee tile[2][0] -> bank 0 (!!!)\n// ... los 32 threads pegan en bank 0 -> conflicto de 32 vías!\n\n// Fix: paddear cada fila con 1 elemento\n__shared__ float tile[32][33]; // <-- el número mágico\n\n// Ahora:\n// Thread 0 lee tile[0][0] -> bank 0\n// Thread 1 lee tile[1][0] -> bank 1 (offset por 33 % 32 = 1)\n// Thread 2 lee tile[2][0] -> bank 2\n// ... los 32 threads pegan en banks distintos -> sin conflicto!\n```\n\nEse ` [32][33] ` en vez de ` [32][32] ` es el tipo de cosa que convierte un kernel al 40% de pico en uno al 80%. Un elemento extra por fila. Este solo cambio puede dar speedups de 2x en kernels memory-bound."},{"heading":"Multiplicación de Matrices: SGEMM Optimizado","level":2,"url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#multiplicacin-de-matrices-sgemm-optimizado","content":"El triple loop naive es insuficiente para uso en producción. El siguiente kernel SGEMM tileado fue optimizado iterativamente. Esta versión consigue ~75% del rendimiento de cuBLAS en una A100:\n\n```cuda\n#define BM 128\n#define BN 128\n#define BK 8\n#define TM 8\n#define TN 8\n\n__global__ void sgemm_optimized(\n const float* __restrict__ A,\n const float* __restrict__ B,\n float* __restrict__ C,\n int M, int N, int K\n) {\n const int bx = blockIdx.x;\n const int by = blockIdx.y;\n const int tx = threadIdx.x;\n const int ty = threadIdx.y;\n\n __shared__ float As[BK][BM + 1]; // Transpuesta para coalescing\n __shared__ float Bs[BK][BN + 1];\n\n float accum[TM][TN] = {0.0f};\n float a_reg[TM];\n float b_reg[TN];\n\n const int row_base = by * BM;\n const int col_base = bx * BN;\n\n for (int k = 0; k < K; k += BK) {\n // Carga colaborativa de tile A en shared memory\n #pragma unroll\n for (int i = 0; i < BM; i += blockDim.y) {\n int row = row_base + ty + i;\n int col = k + tx;\n if (row < M && col < K) {\n As[tx][ty + i] = A[row * K + col];\n } else {\n As[tx][ty + i] = 0.0f;\n }\n }\n\n #pragma unroll\n for (int i = 0; i < BN; i += blockDim.y) {\n int row = k + ty;\n int col = col_base + tx + i;\n if (row < K && col < N) {\n Bs[ty][tx + i] = B[row * N + col];\n } else {\n Bs[ty][tx + i] = 0.0f;\n }\n }\n\n __syncthreads();\n\n #pragma unroll\n for (int kk = 0; kk < BK; kk++) {\n #pragma unroll\n for (int m = 0; m < TM; m++) {\n a_reg[m] = As[kk][ty * TM + m];\n }\n #pragma unroll\n for (int n = 0; n < TN; n++) {\n b_reg[n] = Bs[kk][tx * TN + n];\n }\n #pragma unroll\n for (int m = 0; m < TM; m++) {\n #pragma unroll\n for (int n = 0; n < TN; n++) {\n accum[m][n] += a_reg[m] * b_reg[n];\n }\n }\n }\n\n __syncthreads();\n }\n\n #pragma unroll\n for (int m = 0; m < TM; m++) {\n #pragma unroll\n for (int n = 0; n < TN; n++) {\n int row = row_base + ty * TM + m;\n int col = col_base + tx * TN + n;\n if (row < M && col < N) {\n C[row * N + col] = accum[m][n];\n }\n }\n }\n}\n```\n\nDecisiones clave de optimización:\n\n1. **Store transpuesto en shared memory para A**, Cuando los threads leen una columna de ` As `, acceden a filas consecutivas que mapean a banks distintos.\n2. **Register tiling (TM x TN)**, Cada thread computa un sub-tile de 8x8, amortizando loads de shared memory sobre 64 operaciones FMA por paso de K.\n3. **Punteros ` __restrict__ `**, Le dice al compilador que A, B, C no se solapan en memoria.\n4. **` #pragma unroll `**, Crítico para los inner loops."},{"heading":"El Port a ROCm: No es Solo Find-and-Replace","level":2,"url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#el-port-a-rocm-no-es-solo-find-and-replace","content":"Al portar esto a HIP para MI250X, surgen varios problemas no obvios. La shared memory de AMD es 64 KB por CU vs los 164 KB configurables de NVIDIA en A100. Si tu estrategia de tiling necesita más de 64 KB, se necesita tiles más pequeños en AMD, lo que cambia toda tu estrategia de optimización."},{"heading":"Benchmarks: Números Reales en Hardware Real","level":2,"url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#benchmarks-nmeros-reales-en-hardware-real","content":"Los siguientes benchmarks muestran SGEMM 4096x4096 en tres setups:\n\n| Implementación | Hardware | TFLOPS | % del Pico |\n| --- | --- | --- | --- |\n| cuBLAS | A100 80GB | 17.8 | 91% |\n| Kernel CUDA custom | A100 80GB | 14.7 | 75% |\n| rocBLAS | MI250X | 15.2 | 88% |\n| Kernel HIP custom | MI250X | 11.9 | 69% |\n| Vulkan Compute (GLSL) | RTX 4090 | 9.3 | 56% |\n| Vulkan Compute (GLSL) | RX 7900 XTX | 7.1 | 52% |\n\nLas bibliotecas del vendor (cuBLAS, rocBLAS) hacen trucos a nivel assembly que son impracticables de replicar en código de alto nivel, cosas como software pipelining de loads de shared memory y asignación explícita de registros."},{"heading":"Profiling: Distribución del Tiempo de Ejecución","level":2,"url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#profiling-distribucin-del-tiempo-de-ejecucin","content":"La habilidad más valiosa en programación GPU es leer un output de profiler. Para el kernel CUDA:\n\n```\nKernel: sgemm_optimized\nDuration: 2.31 ms\nTheoretical Occupancy: 75%\nAchieved Occupancy: 71.2%\nSM Throughput: 87.3%\nMemory Throughput: 62.1% <-- No es memory bound\nCompute Throughput: 87.3% <-- Es compute bound\n\nL1/TEX Hit Rate: 94.7%\nShared Memory Bank Conflicts: 0 <-- El padding funcionó\n\nWarp Stall Reasons:\n stall_mio_throttle: 23.1% <-- Ancho de banda de shared memory\n stall_math_pipe_throttle: 18.7% <-- Bien! La ALU está saturada\n stall_short_scoreboard: 15.2% <-- Esperando resultados MIO\n stall_not_selected: 12.4% <-- Scheduler no pudo elegir este warp\n```\n\nEse ` stall_mio_throttle ` al 23.1% indica que el ancho de banda de shared memory es el próximo bottleneck, el kernel carga de shared memory más rápido de lo que el hardware puede servir. El fix sería aumentar el register tile size (TM/TN) para hacer más computación por load de shared memory, al costo de más presión de registros y potencialmente menor occupancy.\n\nEste es el juego. Cada optimización crea un nuevo bottleneck. Terminaste cuando el profiler te muestra que se está limitado por algo fundamental, throughput de ALU, ancho de banda de memoria, o límites de occupancy por presión de registros."},{"heading":"Framework de Decisión","level":2,"url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#framework-de-decisin","content":"Una evaluación honesta de los tres ecosistemas tras uso extensivo:\n\n**CUDA** tiene el mejor tooling por lejos. Si se está en hardware NVIDIA, no hay razón para usar otra cosa para compute.\n\n**ROCm** mejoró dramáticamente pero todavía se siente 2 años atrás de CUDA en tooling. El lenguaje HIP en sí es 98% source-compatible con CUDA, lo que hace el porting directo. Vale la pena si teners hardware AMD.\n\n**Vulkan Compute** es la opción correcta cuando se necesita una codebase corriendo en Intel, NVIDIA, AMD y hasta GPUs mobile. El trade-off es menos control sobre específicos del hardware y una API más verbosa.\n\nElegir basándote en tus restricciones, no en tribalismo. Y por favor, profileá antes de optimizar."},{"heading":"Preguntas frecuentes","level":2,"url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#questions","content":""},{"heading":"¿Cuándo debería usar CUDA vs ROCm vs Vulkan Compute?","level":3,"url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#question-1","content":"CUDA si se es solo NVIDIA y se desea el mejor tooling y ecosistema. ROCm si apuntás a GPUs AMD (especialmente MI250/MI300 para workloads de ML). Vulkan Compute si se necesita portabilidad cross-vendor o se está haciendo compute GPU en un contexto que ya usa Vulkan para gráficos."},{"heading":"¿Qué es warp divergence y por qué destruye el rendimiento?","level":3,"url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#question-2","content":"Un warp (32 threads en NVIDIA, 64 en AMD como 'wavefront') ejecuta en lockstep. Cuando threads del mismo warp toman ramas diferentes en un if/else, el hardware serializa ambos paths, threads que no están en el path activo quedan enmascarados pero siguen consumiendo ciclos."},{"heading":"¿Cómo afectan los bank conflicts de shared memory al rendimiento?","level":3,"url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#question-3","content":"La shared memory se divide en 32 banks. Cuando múltiples threads de un warp acceden a direcciones diferentes que mapean al mismo banco simultáneamente, los accese es se serializan. Un bank conflict de 32 vías significa acceso 32x más lento. Paddear los arrays de shared memory con 1 elemento por fila frecuentemente elimina estos conflictos."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#article-tips","content":"1. **Tip:** Perfilar con ` ncu --set full ` (Nsight Compute) antes de tocar el código de tu kernel. Es común ver equipos pasar días 'optimizando' un kernel que era memory-bound, agregando instrucciones aritméticas que el scheduler escondía detrás de la latencia de memoria de todas formas. Primero conocé tu bottleneck.\n2. **Tip:** En GPUs AMD, el wavefront size es 64 (no 32 como los warps de CUDA). Si se porta código CUDA a ROCm con ` hipify-perl `, auditá cada ` __shfl_sync ` y primitiva a nivel warp manualmente, la traducción automática clava la sintaxis pero se equivoca en la semántica para algoritmos que dependen del warp size.\n3. **Tip:** Para Vulkan Compute, siempre configurar ` VK_KHR_shader_non_semantic_info ` en las extensiones de device y compilar GLSL con el flag ` -g `. Sin eso, RenderDoc muestra el compute shader como un blob opaco y la depuración se vuelve conjetural.\n\n- cuda\n- rocm\n- vulkan-compute\n- programación-gpu\n- multiplicación-matrices\n- warp-scheduling\n- shared-memory\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)"}],"links":[{"text":"English","url":"https://lucioduran.com/blog/gpu-programming-cuda-vs-rocm-vulkan-compute"},{"text":"CUDA","url":"https://developer.nvidia.com/cuda-toolkit"},{"text":"Volver al Blog","url":"https://lucioduran.com/es/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute"},"headline":"Programación GPU: CUDA vs ROCm vs Vulkan Compute, Warp Scheduling, Shared Memory y Multiplicación de Matrices","description":"Más allá de los tutoriales, warp divergence, bank conflicts de shared memory, tuning de occupancy, y un kernel de multiplicación de matrices que realmente rinde. Con benchmarks.","datePublished":"2026-03-06","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://lucioduran.com/favicons/og-default.png","url":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute","inLanguage":"es","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"GPU Computing","keywords":"cuda, rocm, vulkan-compute, programación-gpu, multiplicación-matrices, warp-scheduling, shared-memory","wordCount":1647,"timeRequired":"PT9M","workTranslation":[{"@type":"BlogPosting","inLanguage":"en","url":"https://lucioduran.com/blog/gpu-programming-cuda-vs-rocm-vulkan-compute"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Inicio","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/es/blog"},{"@type":"ListItem","position":3,"name":"Programación GPU: CUDA vs ROCm vs Vulkan Compute, Warp Scheduling, Shared Memory y Multiplicación de Matrices","item":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"¿Cuándo debería usar CUDA vs ROCm vs Vulkan Compute?","acceptedAnswer":{"@type":"Answer","text":"CUDA si se es solo NVIDIA y se desea el mejor tooling y ecosistema. ROCm si apuntás a GPUs AMD (especialmente MI250/MI300 para workloads de ML). Vulkan Compute si se necesita portabilidad cross-vendor o se está haciendo compute GPU en un contexto que ya usa Vulkan para gráficos."}},{"@type":"Question","name":"¿Qué es warp divergence y por qué destruye el rendimiento?","acceptedAnswer":{"@type":"Answer","text":"Un warp (32 threads en NVIDIA, 64 en AMD como 'wavefront') ejecuta en lockstep. Cuando threads del mismo warp toman ramas diferentes en un if/else, el hardware serializa ambos paths, threads que no están en el path activo quedan enmascarados pero siguen consumiendo ciclos."}},{"@type":"Question","name":"¿Cómo afectan los bank conflicts de shared memory al rendimiento?","acceptedAnswer":{"@type":"Answer","text":"La shared memory se divide en 32 banks. Cuando múltiples threads de un warp acceden a direcciones diferentes que mapean al mismo banco simultáneamente, los accese es se serializan. Un bank conflict de 32 vías significa acceso 32x más lento. Paddear los arrays de shared memory con 1 elemento por fila frecuentemente elimina estos conflictos."}}]}]}],"contentHash":"c304ebb7acb01007a02b99f0261f5f4ca42cc439f5512983dc050378c531d1e6","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/es/blog/gpu-programming-cuda-vs-rocm-vulkan-compute","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":64680,"contentBytes":12357}}
