Compile Stable Diffusion local en mi PC. Tres bugs distintos, ninguno donde lo esperaba.

Queria correr generacion de imagenes local en mi propia GPU. Termine peleando con el compilador, con mi propio script de background, y con la memoria de video. En ese orden, y ninguno tenia que ver con lo que yo pensaba.
El disparador fue evaluar open-generative-ai, un frontend que unifica mas de 400 modelos de imagen y video (Flux, Midjourney, Kling, Sora) detras de una sola UI. Se vende como "sin filtros de contenido, sin guardrails". Esa promesa es cierta solo para dos motores que trae embebidos y corren 100% local, sd.cpp para imagen y Wan2GP para video. Todo lo demas pasa por Muapi.ai, un gateway pago que reenvia a las mismas APIs comerciales de siempre, con sus mismos filtros de siempre.
Si queria algo sin intermediarios de verdad, tenia que ser local. Elegi sd.cpp primero: mismo autor y misma libreria base que llama.cpp (ggml), setup mas simple que Wan2GP, feedback en segundos en vez de minutos por prueba. Mi maquina: RTX 3060 de 12GB, i9-13900K, 32GB de RAM, CUDA 12.0 ya instalado.
Pense que con eso ya tenia lo dificil resuelto. Me equivoque tres veces distintas antes de ver la primera imagen.
El compilador se cae compilando CUDA
Clonar el repo con submodulos y configurar con soporte CUDA fue lo facil:
git clone --recursive https://github.com/leejet/stable-diffusion.cpp.git
cmake .. -DSD_CUDA=ON -DCMAKE_BUILD_TYPE=Release
Compilar, no. Con cmake --build . -j$(nproc) (32 cores en esta maquina), a los pocos minutos:
during RTL pass: reload
.../mmf.cuh: In function 'void mul_mat_f_switch_ids(...)':
internal compiler error: Segmentation fault
gcc-12: internal compiler error: Segmentation fault signal terminated program cc1plus
gcc-12 crasheandose a si mismo. Mi primer instinto fue pensar que habia algo roto en mi codigo o en mis flags de compilacion, pero no: es el compilador de host de nvcc reventando al procesar los archivos template-instance de ggml-cuda, los kernels de multiplicacion de matrices que ggml genera para cada combinacion de tipo de dato. Busque en dmesg algun rastro de un OOM-kill del sistema y no habia nada. El kernel no mato a nadie. Fue cc1plus tirandose abajo solo, bajo presion de memoria, con 32 jobs pesados compilando al mismo tiempo.
Baje el paralelismo y listo:
cmake --build . -j8
Build limpio de punta a punta. Lo que me queda de esto: en builds CUDA con archivos template-heavy, tirar -j$(nproc) a ciegas no sale gratis. Cada uno de esos jobs puede pesar mucho mas en RAM que un .cpp comun, y 32 en simultaneo alcanzan para tumbar al compilador antes de que el sistema operativo se entere de nada.
El proceso que mato a su propio hijo
Con los binarios listos, tocaba bajar un checkpoint SDXL de HuggingFace (~6.9GB). Lo lance como tarea en background con un patron que ya venia mal armado desde el principio:
curl -L -o modelo.safetensors "https://huggingface.co/.../sd_xl_base_1.0.safetensors" &
CURL_PID=$!
sleep 3
echo "descarga iniciada, pid $CURL_PID"
El bloque entero se mando como una sola tarea en background. El error esta en que ya era background por fuera, y yo le puse otro & manual al curl por adentro. El script "padre", el que tiene el sleep 3 y el echo, termina a los 3 segundos. En ese momento el orquestador de la tarea lo da por completado y limpia el arbol de procesos. curl, que seguia siendo hijo de ese script ya muerto, se va con el.
El archivo termino con 289MB en vez de 6.9GB, y durante un rato ni siquiera parecio corrupto. El header JSON de un
.safetensorsva al principio del archivo, y ese primer pedazo si se habia bajado entero.filelo reportaba como "data" sin quejarse, con un JSON perfectamente valido de metadata de tensores en los primeros bytes. La corrupcion recien se nota cuando intentas leer los pesos reales, mucho mas adelante en el archivo.
Nada de anidar backgrounds la proxima: lance el curl directo como la tarea en background, sin & interno ni ese sleep artificial que le dio al wrapper una señal falsa de "ya termine".
curl -L -o modelo.safetensors "https://huggingface.co/.../sd_xl_base_1.0.safetensors"
Corrido directo, sin nada anidado adentro. Descarga completa, 6.5GB reales esta vez.
La memoria que explota en el paso liviano
Con el modelo entero ya en disco, primera generacion real: SDXL a 1024x1024, 20 steps.
El sampling, el proceso iterativo de denoising que hace la mayor parte del trabajo, corrio sin drama: 35.7 segundos, unos 6.6GB de VRAM sobre los 12GB disponibles.
El lio aparecio en el decode del VAE, el paso final que convierte el latente en pixeles, y que corre una sola vez en vez de en loop:
[ERROR] ggml_backend_cuda_buffer_type_alloc_buffer: allocating 7680.25 MiB on device 0: cudaMalloc failed: out of memory
[ERROR] ggml_gallocr_reserve_n_impl: failed to allocate CUDA0 buffer of size 8053326080
[ERROR] vae: failed to allocate the compute buffer
[ERROR] decode_first_stage failed for latent 1
[ERROR] generate failed
Pedia 7.7GB de mas, de una sola vez. Esto si me sorprendio: el paso que en teoria es mas liviano, corre una vez y no veinte, fue el que revento la memoria. La explicacion es que el sampling de sd.cpp ya viene pensado para procesar en chunks, mientras que el decode del VAE arma por defecto el buffer de computo para la imagen entera de una sola pasada.
Bajar la resolucion hubiera sido resignarse. Hay un flag que resuelve esto sin perder calidad:
--vae-tiling
Con eso el decode deja de pedir el buffer completo y lo va troceando en tiles. El mismo paso que tiraba out of memory bajo a 6.43 segundos sin fallar una vez mas.
El comando final
./sd-cli -M img_gen -m sd_xl_base_1.0.safetensors -p "<prompt>" -o out.png --steps 20 -W 1024 -H 1024 --vae-tiling
43.59 segundos por imagen SDXL completa en una RTX 3060 de 12GB: 35.7s de sampling, 6.43s de decode con tiling.
Los tres bugs no se parecian entre si. Uno era el compilador, otro un patron de shell scripting mal armado, el tercero gestion de memoria de GPU. Pero los tres compartian algo que me quedo dando vueltas: ninguno apuntaba a su causa real desde el primer mensaje de error. El ICE de gcc se leia como un bug del compilador, no como un problema de paralelismo. El archivo truncado se leia como una descarga fallida, no como un proceso matado por su propio wrapper. El OOM del VAE se leia como falta de VRAM en general, cuando era un solo paso sin tiling. En infraestructura local, el primer error casi nunca señala la causa. Señala donde revento. Y eso no es lo mismo.