Estado del experimento. Esta es una crónica de laboratorio, no una recomendación universal ni una guía de producción. Los números proceden de una prueba corta con una versión y un modelo concretos. No hubo soak térmico prolongado, evaluación de calidad ni una matriz amplia de modelos.
La pregunta parecía sencilla: en una APU AMD Strix Halo, ¿me convenía ejecutar llama.cpp con Vulkan o invertir en una compilación HIP/ROCm?
La respuesta no salió de una tabla genérica. Salió de varios fallos, de acotar mejor cada prueba y de decidir por adelantado qué tendría que mejorar HIP para sustituir a una base Vulkan que ya funcionaba.
El equipo y el presupuesto de memoria real
El laboratorio usa un GMKtec EVO-X2 con Ryzen AI Max+ 395, Radeon 8060S (gfx1151), 128 GiB de memoria física y CachyOS. En una APU con memoria unificada, esa cifra comercial no equivale a 128 GiB disponibles para cualquier carga.
La primera configuración de BIOS, con UMA en automático, dejaba a Linux unos 62 GiB de RAM visible y al driver aproximadamente 31,2 GiB de GTT. La evidencia apuntaba a un carve-out de firmware, no a una reserva CMA normal del kernel.
Hice un cambio deliberadamente reversible: UMA_SPECIFIED con un frame buffer fijo de 2 GiB. Tras reiniciar y repetir el inventario:
| Configuración | RAM visible | VRAM fija | Límite GTT aproximado |
|---|---|---|---|
| UMA Auto | 62 GiB | 64 GiB observados por AMDGPU | 31,2 GiB |
| UMA especificada | 123,5 GiB | 2 GiB | 61,7 GiB |
Esto no “creó” memoria. Recuperó para el sistema operativo memoria que el firmware había apartado de forma fija y dejó una ventana GTT dinámica mayor. También fijó el techo práctico del experimento: memoria física, RAM visible, VRAM fija y GTT son magnitudes relacionadas, pero no intercambiables.
Antes de compilar: definir el criterio de promoción
Quería evitar una comparación en la que cada backend usara parámetros distintos. Congelé:
- el modelo
Qwen3-1.7B-Q8_0; - la misma cuantización y el mismo offload;
PP512para procesado de prompt;TG128para generación;- tres repeticiones;
- comprobaciones separadas de dispositivo, carga, generación, journal y limpieza.
El criterio también quedó escrito antes del resultado: HIP debía mejorar al menos un 5 % la métrica objetivo sin introducir una regresión inaceptable en generación. No es un umbral científico general; es una regla operativa de este laboratorio.
Separar PP y TG resultó importante. Un backend puede preparar el contexto más rápido y, a la vez, producir tokens más despacio. Un único promedio habría ocultado ese intercambio.
Vulkan: llegar a una base que realmente se pueda medir
El primer obstáculo fue corriente: faltaban cabeceras de desarrollo de Vulkan. Instalarlas permitió configurar la compilación, pero no convirtió el primer smoke en una prueba fiable.
Ese intento se descontroló. La generación superó diez minutos y produjo un log de 6,4 GiB. En las muestras del principio y el final se mezclaban salida del modelo y líneas del controlador. La hipótesis principal fue contaminación por stdin o un límite de tokens que no estaba actuando como se esperaba, pero no quedó demostrada una causa raíz única.
Después aparecieron fallos menos espectaculares y muy instructivos:
- ambigüedad al decidir qué proceso era seguro terminar;
- un error de sintaxis Bash en el script de preparación;
- una validación demasiado frágil del proceso monitor;
- intentos detenidos antes de ejecutar el modelo porque el propio arnés no superaba sus gates.
La mejora no fue “volver a ejecutar hasta que funcionase”, sino limitar cada dimensión de daño. Este es el patrón reducido —con rutas genéricas— que quedó como referencia:
timeout --foreground --signal=TERM --kill-after=10s 120s \
bash -c 'ulimit -f 131072; exec "$1" -m "$2" \
-p "Reply with exactly: VULKAN_SMOKE_OK" \
-ngl 999 -c 2048 -n 64 --temp 0 \
--simple-io --no-display-prompt --no-warmup --log-disable' \
_ "<LLAMA_CLI>" "<MODEL_PATH>" \
</dev/null >"<LOG_DIR>/smoke.stdout" 2>"<LOG_DIR>/smoke.stderr"
Aquí hay tres límites independientes: 120 segundos, 64 tokens y un límite de tamaño por fichero. El prompt viaja como argumento y la redirección desde /dev/null evita heredar la entrada interactiva del controlador. Las unidades de ulimit -f, los valores concretos y los flags deben contrastarse con el shell y la ayuda de la build bajo prueba; no son contratos eternos.
También separé dos preguntas que antes estaban mezcladas:
- ¿Carga el modelo y se descarga en la GPU? Una ejecución sin generación confirmó
29/29 layersen Radeon. - ¿Genera una salida corta, acotada y verificable? Solo después de ese gate se habilitó el benchmark.
El baseline Vulkan resultante fue:
| Backend | PP512 | TG128 |
|---|---|---|
| Vulkan/RADV | 5263,79 ± 10,83 tok/s | 114,39 ± 0,24 tok/s |
Las lecturas térmicas y del journal de esa ventana corta no mostraron errores relevantes, pero eso no sustituye una prueba sostenida de temperatura, potencia o estabilidad.
HIP/ROCm: varios “fallos” estaban en el procedimiento
La ruta HIP avanzó por gates independientes:
- Primero no pude confirmar correctamente la ruta del compilador.
- CMake detectó HIP, pero faltaba el metadato de desarrollo de
hipBLAS. - El preflight de paquetes bloqueó la instalación por un falso positivo: encontró la palabra “linux” dentro de una URL del repositorio, no en el nombre de un paquete del kernel.
- Tras corregir el parser e instalar un conjunto ROCm coherente, la compilación terminó.
- Los primeros gates de dispositivo y enlazado rechazaron salida válida porque sus predicados eran demasiado estrictos.
- Una reclasificación basada en la evidencia permitió completar carga, smoke y benchmark.
La lección aquí no es relajar cualquier comprobación que moleste. Es diseñar gates que validen la propiedad que importa. Buscar una palabra en texto arbitrario no equivale a inspeccionar nombres de paquetes; exigir una forma exacta de salida tampoco equivale a comprobar que la cadena HIP está realmente cargada.
El resultado medido
Con el mismo modelo y las mismas dimensiones:
| Backend | PP512 | TG128 | Cambio frente a Vulkan |
|---|---|---|---|
| Vulkan/RADV | 5263,79 ± 10,83 tok/s | 114,39 ± 0,24 tok/s | base |
| HIP/ROCm | 5448,84 ± 173,67 tok/s | 102,51 ± 0,17 tok/s | PP ≈ +3,5 %; TG ≈ −10,4 % |
HIP procesó el prompt algo más rápido, pero no alcanzó el 5 % de mejora definido para la promoción y empeoró claramente la generación. Para este modelo pequeño y este perfil de uso, mantuve Vulkan como baseline y conservé HIP como build experimental.
Eso no demuestra que Vulkan sea siempre más rápido en AMD, ni siquiera en este equipo. Modelos mayores, otras cuantizaciones, tamaños de lote o revisiones posteriores de llama.cpp, Mesa y ROCm pueden cambiar el resultado. La decisión es local, fechada y reversible.
El workflow que me llevo
Mi secuencia para el siguiente backend será:
- Inventariar hardware, memoria visible, GTT y versiones.
- Definir modelo, parámetros y umbral de promoción antes de medir.
- Validar configuración y compilación.
- Confirmar dispositivo y enlazado con predicados específicos.
- Ejecutar carga sin generación.
- Ejecutar un smoke con límites de tiempo, tokens y salida.
- Medir PP y TG por separado.
- Revisar sensores, journal y procesos restantes.
- Promover, conservar como experimental o revertir.
No todo fallo de un gate es un fallo del backend. En este recorrido hubo dependencias ausentes, un parser defectuoso y comprobaciones demasiado rígidas. Precisamente por eso conviene que los gates sean pequeños: permiten distinguir un problema de infraestructura de un resultado de rendimiento.
Qué quedó establecido —y qué no
Establecido por esta prueba:
- la configuración UMA cambió de forma medible el presupuesto de RAM/GTT;
- ambas builds descargaron las 29 capas del modelo de prueba en la GPU;
- Vulkan e HIP completaron el mismo benchmark corto;
- con este workload, HIP ganó algo en PP y perdió más en TG;
- según el criterio previo, Vulkan debía seguir como baseline.
No establecido:
- estabilidad durante horas;
- calidad de respuesta;
- eficiencia energética comparada;
- comportamiento con modelos grandes o varias solicitudes;
- superioridad general de Vulkan sobre ROCm;
- preparación para producción.
Una última limitación documental: el repositorio de HalostrixLab todavía no tiene historial de commits. Las fechas y la secuencia proceden de sus notas de laboratorio y configuraciones versionadas por referencia, no de una cronología Git verificable. Por eso presento estos datos como el cierre provisional de un experimento, no como una verdad permanente.