Mi GPU Era Más Lenta Que la CPU a la Que Reemplazó

Una vez moví un cálculo pesado de la CPU a la GPU, esperando que volara. En cambio, se arrastró — dramáticamente más lento que el código corriente al que se suponía que debía reemplazar. Había comprado velocidad y de algún modo había instalado una ralentización.

El hardware no era el cuello de botella. Una GPU moderna es una de las cosas más rápidas que puedes poner en una computadora. El problema era que le había dado un trabajo con una forma en la que es casi singularmente mala, y luego me sorprendí cuando no pudo ejecutarlo.

La mejora que fue un retroceso

La expectativa era simple y, pensé, segura: las GPU son rápidas, mi tarea era lenta, así que mover la tarea a la GPU debería hacerla rápida. Esa es la versión popular de cómo funciona la aceleración, y es errónea de una manera específica e instructiva.

Cuando medí la versión “acelerada”, no era un poco más lenta. Era más lenta por un gran múltiplo — el tipo de resultado que te dice que el problema no es una optimización ausente sino un desajuste fundamental. Algo en la forma de lo que estaba haciendo luchaba activamente contra el hardware.

Qué significaba realmente “ponerlo en la GPU”

Esto es lo que en realidad había hecho. Tenía un bucle existente: tomar una unidad de trabajo, calcularla, pasar a la siguiente, miles de veces. Para “usar la GPU”, simplemente había hecho que cada unidad individual se ejecutara en la GPU — y había dejado el bucle exactamente donde estaba.

Así que miles de veces seguidas, mi programa se volvía hacia la GPU, le entregaba una pequeña tarea, esperaba la respuesta, y se volvía para la siguiente. El bucle seguía al mando. La GPU era solo un contratista muy rápido al que llamaba, individualmente, por cada clavo que quería que martillara.

El costo oculto: cuesta algo solo con pedir

Cada vez que entregas una pieza de trabajo a una GPU, hay un costo fijo para configurarla y despacharla — pequeño por sí solo, invisible si lo pagas una vez. Yo lo pagaba miles y miles de veces. Cuando miré de cerca, la mayor parte del tiempo de ejecución no era cálculo en absoluto. Era sobrecarga: la GPU sentada ociosa, esperando que le dijeran qué hacer, mientras mi programa pasaba su tiempo haciendo una pregunta diminuta tras otra.

Había construido un proceso que era casi enteramente el acto de pedir, con el trabajo real como un error de redondeo. La versión de CPU había sido lenta, pero al menos se mantenía ocupada. La versión de GPU era lenta porque pasaba la mayor parte de su vida esperando.

Una GPU es un autobús, no un taxi

El modelo mental que finalmente lo arregló: una GPU no es una forma más rápida de hacer una cosa. Es una forma de hacer una cantidad enorme de la misma cosa a la vez. Es un autobús de cincuenta asientos, no un auto deportivo.

Llamarla una vez por elemento es como hacer correr ese autobús de un lado a otro de la ciudad con un único pasajero en cada viaje. El autobús es genuinamente rápido. Tu rendimiento es lamentable — y es lamentable precisamente porque estás usando un vehículo construido para multitudes para mover gente de uno en uno. La culpa no está en el autobús. Está en el despacho.

La solución: deja de iterar, empieza a agrupar

La reparación no fue un kernel más rápido ni una mejor tarjeta. Fue reestructurar el bucle. En lugar de “por cada elemento, envíalo a la GPU”, reformé el trabajo para que la GPU pudiera tomar muchos elementos a la vez y procesarlos juntos — convirtiendo miles de despachos diminutos en un puñado de grandes. La sobrecarga que había estado pagando una y otra vez colapsó en algo que pagaba unas pocas veces en lugar de miles.

Esta es una forma común de lo que la gente quiere decir con “vectorizar” o agrupar (batching): expresar muchas operaciones similares juntas en lugar de como una larga secuencia de pequeños pasos. A menudo es la diferencia entre una GPU que avergüenza a tu CPU y una GPU avergonzada por ella, y la brecha entre ambas puede ser enorme.

Por qué este es el sentido completo del hardware

Me tomó un tiempo internalizar por qué la estructura importaba tanto, en vez de tratarla como un detalle de ajuste. La ventaja de una GPU aquí no venía de que cualquier operación individual fuera rápida. Venía de hacer una cantidad vasta de operaciones similares al mismo tiempo. Ese paralelismo es toda la propuesta de valor. Si tu código no puede expresar su trabajo como “haz todo esto al mismo tiempo”, no hay nada de lo que el paralelismo pueda agarrarse — y una GPU ejecutando trabajo serial, de uno en uno, es solo una CPU cara, torpe y mal formada.

Así que “ponlo en la GPU” nunca fue la verdadera instrucción. La verdadera instrucción era “reforma el trabajo para que pueda hacerse todo a la vez”. El hardware solo iba a ser tan rápido como la forma del problema lo permitiera.

La herramienta no puede salvar la forma equivocada

La lección se generaliza mucho más allá de las GPU. Recurrir a hardware más rápido sirve de poco si la estructura de tu cálculo lucha contra ese hardware. Para trabajo como este, el rendimiento vivía más en la forma del cálculo — cómo estaba organizado, agrupado y expresado — que en la velocidad nominal de lo que lo ejecutaba. Puedes poner un algoritmo lento en una máquina rápida y obtener una decepción rápida.

Mantengo esa ralentización en mente como recordatorio de que “lo hice más rápido” y “lo moví a hardware más rápido” no son la misma frase. La primera trata de entender el trabajo. La segunda trata de esperar que el silicio lo entienda por ti. No lo hará.

— Sin señales, sin retornos, esto no es asesoramiento de inversión.