AI for Offensive Security · Agentic Systems

Probando Qwen3.8-27B en local con OpenCode y Hack The Box

Qwen3.8-27B en una RTX 5070 Ti de 16 GB, ajustes de inferencia y una prueba agentic real desde Kali con OpenCode.

Cuando apareció Qwen3.8-27B empecé a verlo bastante en conversaciones y comparativas sobre modelos locales. Es un modelo de 27B con reasoning, tool use y un contexto nativo de 262K tokens. Sobre el papel tenía justo el tamaño que me interesaba probar: suficientemente grande como para esperar capacidades útiles de coding y razonamiento, pero todavía dentro del terreno donde una buena cuantización podía hacerlo viable en una GPU doméstica.

Mi GPU es una RTX 5070 Ti con 16 GB de VRAM. Como referencia, 27.000 millones de parámetros almacenados a 16 bits necesitan del orden de 54 GB solo para los pesos. La primera parte del experimento era bastante práctica: ver qué combinación de cuantización, GPU offload y contexto podía ejecutar con el hardware que ya tenía.

Las primeras pruebas fueron con el modelo en LM Studio. Cuando conseguí una configuración rápida y con suficiente contexto, el siguiente paso fue usarla en algo más cercano a mi trabajo: servir el modelo por API, conectar OpenCode desde una Kali y probar una sesión larga de pentesting sobre una máquina Medium de Hack The Box.

El equipo y el laboratorio

Todo se hizo en mi PC habitual:

ComponenteConfiguración
GPUNVIDIA GeForce RTX 5070 Ti · 16 GB VRAM
CPUIntel Core i9-9900K
RAM32 GB DDR4
HostWindows
RuntimeLM Studio / llama.cpp
Entorno de pentestingKali Linux en VMware

Para iterar con las distintas configuraciones utilicé LM Studio. En esta prueba me interesaba poder descargar un GGUF, cambiar el contexto, mover el GPU offload, tocar la KV cache y volver a cargar el modelo en segundos. Para un servicio con otros requisitos de concurrencia o despliegue habría valorado una herramienta más orientada a serving, como vLLM.

LM Studio también me permitía mantener el mismo runtime cuando pasase de las pruebas manuales al agente. Puede exponer los modelos cargados mediante una API compatible con OpenAI, así que OpenCode podía consumir el modelo local sin necesitar una integración específica.

Diagrama del host Windows con Qwen y LM Studio conectado por LAN a una Kali con OpenCode y la VPN de Hack The Box

Arquitectura del laboratorio: la inferencia queda en Windows y OpenCode trabaja desde la Kali a través de la LAN.

Primeras pruebas con Q4

El primer modelo que descargué fue el Q4_K_M de LM Studio Community, de unos 16,8 GB. Los modelos se distribuían como GGUF, el formato que utiliza llama.cpp para almacenar los pesos cuantizados y los metadatos necesarios para cargarlos.

La cuantización es lo que hace posible plantearse un modelo así en 16 GB. La forma más sencilla que tengo de visualizarla es pensar en redondear números. Si no necesitas conservar 3.14159265, puedes utilizar una representación menos precisa y gastar menos espacio. En un LLM el proceso es bastante más complejo, pero la idea útil para este caso es esa: utilizar menos bits para representar los pesos reduce mucho la memoria necesaria.

La otra opción para hacer entrar modelos grandes es repartirlos entre GPU y CPU. llama.cpp permite decidir cuántas capas se mantienen en VRAM mediante el GPU offload. Las capas que no entran se quedan en RAM y se procesan en CPU.

Qwen3.8-27B tiene 64 capas. llama.cpp también puede descargar a GPU la capa de salida, por eso LM Studio mostraba un máximo de 65 unidades en su control de offload. Con el Q4, 32K de contexto y KV cache en Q8, mi configuración se quedaba en 54 de 65.

Ya esperaba que mezclar CPU y GPU no fuese lo ideal, pero la diferencia práctica fue mayor de lo que esperaba. La generación rondaba los 9 tokens/s. Cambiar de seis a ocho CPU threads apenas movió el resultado y algunas pruebas con speculative decoding tampoco mejoraron el problema principal: una parte de la inferencia seguía fuera de la GPU.

Configuración completa de LM Studio utilizada para la prueba Q4 con 32768 tokens de contexto y GPU offload 54

Q4 a 32K: 54 unidades de GPU offload, KV cache Q8 y Flash Attention activado.

OpenCode junto a los logs de LM Studio durante la prueba Q4, mostrando una generación cercana a 9 tokens por segundo

Los logs de LM Studio se estabilizaban alrededor de 9 tokens/s con el Q4 y offload parcial.

A 9 tokens/s se puede conversar con el modelo sin demasiado problema. Con un agente la espera se acumula mucho más rápido. Cada paso puede generar reasoning, producir una tool call, esperar al comando, incorporar su salida y volver a generar para decidir qué hacer después. En una tarea larga ese ciclo se repite continuamente.

El salto a UD-Q3_K_XL

Después probé UD-Q3_K_XL, una de las cuantizaciones dinámicas publicadas por Unsloth. LM Studio mostraba unos 13,4 GB en disco, aproximadamente tres gigabytes menos que el Q4 que había probado primero.

Ese margen permitió llevar el GPU offload al máximo. La carga del modelo podía quedarse en la 5070 Ti y las generaciones cortas pasaron a moverse alrededor de 40-45 tokens/s.

Estos resultados no permiten decir que Q3 sea cinco veces más rápido que Q4. Entre las dos pruebas también cambió dónde se ejecutaba el modelo: en la primera había capas en CPU y en la segunda la carga quedaba en GPU. Para el uso que buscaba, resolver ese cuello de botella tenía más impacto que intentar comparar pequeñas diferencias de calidad entre las dos cuantizaciones.

Gráfica con aproximadamente 9.2 tokens por segundo para Q4 con offload parcial, 43.2 para una request corta del Q3 y alrededor de 20.9 con más de 20K tokens de contexto

Tres referencias de throughput observadas en el mismo equipo.

Aumentar el contexto antes de conectar OpenCode

Las primeras pruebas se hicieron con ventanas de 32K o inferiores. Para cargar el modelo, probar prompts y medir la velocidad era suficiente. Con el Q3 completamente en GPU todavía quedaba margen de VRAM, y quería saber hasta dónde podía aumentarlo sin volver a una experiencia incómoda.

Además, la siguiente prueba ya iba a ser agentic. Una sesión de OpenCode no acumula únicamente mensajes: también entran instrucciones, schemas de tools, resultados de comandos, contenido de archivos, búsquedas y el historial necesario para mantener una tarea durante bastante tiempo. Para algo pequeño 32K puede bastar. Para una prueba de pentesting que podía durar horas prefería trabajar alrededor de 64K.

Antes de pasar a OpenCode preparé un benchmark sencillo de long context. El script PowerShell enviaba un documento grande junto con diez preguntas de recuperación a /v1/chat/completions, sin permitir conocimiento externo, y guardaba por separado la respuesta, el reasoning y las métricas de uso. Me servía para comprobar que una ventana grande seguía siendo utilizable en mi configuración, no para medir la calidad general de Qwen.

En una de esas ejecuciones el prompt llegó a 50.525 tokens. Con UD-Q3_K_XL, KV cache Q4_0 y el modelo completamente en GPU, generó 2.821 tokens en 176,63 segundos, alrededor de 20,9 tokens/s, y respondió correctamente las diez preguntas del documento. Era bastante más lento que una request corta, pero seguía dentro de un rango con el que podía trabajar.

KV cache y Flash Attention

Al aumentar el contexto volvió a aparecer la presión sobre la VRAM.

Durante la generación, el runtime mantiene una KV cache con información calculada sobre los tokens anteriores. Esto permite reutilizar parte del estado de attention en vez de recalcularlo desde cero con cada token nuevo. La consecuencia práctica para esta prueba es sencilla: cuanto más contexto quieres mantener, más memoria necesita esa cache.

LM Studio permite cuantizar por separado las caches K y V. Igual que con los pesos, reducir su precisión recorta consumo de memoria a cambio de un posible impacto en calidad. En este caso pasé ambas a Q4_0. No hice una evaluación específica de degradación de la KV cache, pero era una prueba razonable porque permitía recuperar bastante VRAM sin cambiar otra vez los pesos del modelo.

También mantuve Flash Attention activado. LM Studio lo expone como una optimización que puede reducir el consumo de memoria y mejorar la velocidad de generación. En mi configuración era además necesario para cuantizar la V cache.

Reparto de VRAM entre pesos del modelo, KV cache y overhead del runtime al aumentar el contexto

Al crecer el contexto, la KV cache consume una parte mayor del mismo presupuesto de VRAM que utilizan los pesos y el runtime.

Con K y V en Q4_0 pude mantener una ventana grande sin perder el full GPU offload. En una de las ejecuciones la memoria dedicada llegó a aproximadamente 15,4 de 16 GB. El setup quedaba bastante ajustado, pero Windows seguía funcionando con normalidad y no necesitaba volver a mover capas a CPU.

Configuración final en LM Studio

La configuración que terminé utilizando combinaba los ajustes de memoria anteriores con los parámetros de sampling recomendados por Unsloth para el modelo.

Unsloth recomienda esos valores de sampling para el modo de reasoning. El contexto, el offload y la KV cache de la tabla son específicos de esta GPU y de este experimento.

Configuración completa de LM Studio para Qwen3.8-27B UD-Q3_K_XL con 81920 tokens de contexto y GPU offload 65

Configuración del Q3 en LM Studio con full GPU offload y margen adicional de contexto para el runtime.

Ventanas de contexto en OpenCode y LM Studio

Para la parte agentic trabajé con OpenCode, porque ya me daba el loop y las tools que quería probar: shell, filesystem, TODOs, sesiones largas y soporte para providers OpenAI-compatible. En el directorio de trabajo configuré el provider local mediante opencode.json, indicando la dirección de LM Studio y los límites que OpenCode debía utilizar para gestionar el modelo.

El fragmento relevante era equivalente a este:

{
  "model": "lmstudio/unsloth/qwen3.8-27b",
  "provider": {
    "lmstudio": {
      "npm": "@ai-sdk/openai-compatible",
      "options": {
        "baseURL": "http://<windows-host>:1234/v1"
      },
      "models": {
        "unsloth/qwen3.8-27b": {
          "limit": {
            "context": 65536,
            "output": 12288
          }
        }
      }
    }
  },
  "compaction": {
    "auto": true,
    "prune": true,
    "reserved": 8192
  }
}

OpenCode utiliza los límites declarados en el provider para gestionar cuánto contexto queda disponible y puede compactar automáticamente una sesión cuando se acerca al límite. En mi caso lo dejé en 65.536 tokens.

Durante las sesiones largas aparecieron algunas peticiones que superaban el límite cargado en LM Studio antes de que la compaction actuase como esperaba. El servidor respondía con un HTTP 400 por context overflow y el run quedaba interrumpido.

Como la configuración Q3 con KV Q4_0 todavía permitía algo más de contexto, acabé cargando LM Studio con una ventana física de aproximadamente 81K. OpenCode seguía gestionando la sesión como 64K. Los tokens adicionales quedaban como margen frente a esos pequeños overflows y evitaban trabajar exactamente contra el mismo límite en las dos capas.

LM Studio como backend local

Con la inferencia ya estable levanté el servidor de LM Studio en el host Windows y habilité el acceso desde la red local. Desde Kali, OpenCode apuntaba a la IP privada del host en el puerto 1234. La VPN de Hack The Box y las herramientas de pentesting seguían dentro de la VM, mientras la inferencia utilizaba directamente la GPU de Windows.

Esta separación también hacía más fácil pensar en cada pieza por separado. Qwen ejecutado por LM Studio quedaba como backend de inferencia. OpenCode añadía el loop agentic, las tools, shell, filesystem y la gestión de la sesión.

Los logs de LM Studio dejaban bastante claro qué significaba eso. Las requests de OpenCode incluían, además del historial de mensajes, los schemas de herramientas como bash, lectura y escritura de archivos, búsquedas o TODOs. El modelo podía proponer una tool call y OpenCode ejecutaba la acción antes de devolverle el resultado.

Prueba de hacking en DevHub

Para probar el conjunto utilicé DevHub, una máquina Linux Medium de Hack The Box que seguía activa durante la preparación de este artículo. Por ese motivo no voy a describir su ruta de explotación ni los detalles concretos que permitan reconstruirla.

El run empezó como una prueba normal: enumeración, análisis de la aplicación y actualización del estado a medida que aparecía información nueva. El modelo terminó investigando una aplicación que exponía una API y utilizó los recursos disponibles para entender cómo se comunicaban frontend y backend.

A partir de ahí fue construyendo y descartando hipótesis hasta encontrar una primitive de ejecución de comandos. La vulnerabilidad ya era pública en el componente afectado, pero el agente no llegó a ella leyendo un walkthrough o una advisory. Las instrucciones de la prueba prohibían buscar soluciones de Hack The Box y, en la práctica, acabó trabajando esa parte sin búsquedas externas.

Lo interesante de esa fase fue el camino hasta la explotación. El agente partió del comportamiento de la aplicación, relacionó parámetros de la API con las respuestas del servidor y terminó reproduciendo por su cuenta un problema que ya existía, sin partir del identificador de una CVE ni de un exploit preparado.

Después del primer acceso tuvo que mantener lo que había conseguido mientras seguía enumerando para escalar privilegios. En uno de los pivots terminó interactuando con otro componente interno mediante pequeños scripts. Algunos fallaron, revisó sus propios payloads y corrigió un error que estaba provocando que el servidor descartase los mensajes.

OpenCode durante la sesión de DevHub, mostrando el razonamiento y la obtención del primer acceso

OpenCode durante la obtención de la primera reverse shell, con más de 36K tokens de contexto en la sesión.

Esa secuencia muestra mejor el trabajo que quería observar que una captura de la flag. Para seguir avanzando tenía que conservar accesos anteriores, relacionar componentes descubiertos en momentos distintos y corregir intentos cuando algo no funcionaba.

El proceso fue más lento y menos eficiente que con un buen modelo frontier. También dedicó tiempo a caminos que no aportaron nada. Aun así, mantuvo el run durante una sesión larga, utilizó sus herramientas y terminó la máquina.

Qué aportaba el harness

OpenCode ya resolvía buena parte del harness: mantenía la conversación, exponía tools, ejecutaba shell y operaciones de filesystem, gestionaba TODOs y se encargaba de la compaction.

Después repetí parte del experimento con un pequeño harness propio para HTB. El tiempo y el resultado fueron muy parecidos a la primera ejecución, así que lo trato como una variación del mismo experimento.

ComponenteFunción
Instrucciones / skillsDar metodología y restricciones más explícitas al agente
Estado persistente en MarkdownConservar información importante fuera del contexto activo
Scripts deterministasResolver tareas repetitivas sin volver a gastar reasoning en ellas

Esta parte me interesa como línea de trabajo por separado, pero el experimento de DevHub no necesita una arquitectura de agentes más compleja para explicar lo que ocurrió.

Limitaciones

Los números de rendimiento son mediciones del setup que utilicé, no un benchmark formal entre cuantizaciones. Entre Q4 y Q3 cambiaron a la vez el formato de los pesos y el reparto CPU/GPU, así que no se puede atribuir toda la diferencia de tokens/s únicamente a la cuantización.

Tampoco medí de forma sistemática la pérdida de calidad entre Q4, Q3 o las distintas precisiones de KV cache. Es una parte que tendría sentido estudiar de forma separada si el objetivo fuese escoger la mejor configuración general para esta GPU.

Las mediciones con contexto grande proceden del benchmark de long context y de requests agentic reales. Sirven para saber cómo se comportó el sistema durante este trabajo, pero no para construir una curva precisa de throughput frente a longitud de contexto.

DevHub sigue siendo una única máquina. El resultado demuestra que el conjunto pudo sostener una tarea de hacking bastante más compleja que un prompt aislado. No sirve para medir de forma general la capacidad de pentesting del modelo.

Conclusiones

El primer Q4 podía ejecutarse en 16 GB, pero dejar varias capas en CPU reducía el rendimiento hasta aproximadamente 9 tokens/s. UD-Q3_K_XL dejaba suficiente espacio para mantener la carga en GPU y las requests cortas pasaban a la zona de 40-45 tokens/s.

Ese margen permitió aumentar el contexto antes de conectar OpenCode. Con 64K, la KV cache empezó a tener un impacto visible sobre la VRAM. Pasar K y V a Q4_0 permitió conservar una ventana grande y el full GPU offload. En el benchmark de unas 50K tokens de entrada, la generación seguía rondando los 21 tokens/s.

En las sesiones agentic también aparecieron problemas que no veía usando el modelo como chat: compactions, peticiones que rozaban el límite y errores 400 por context overflow. Mantener 64K como límite lógico en OpenCode y dejar más margen en LM Studio resultó una solución práctica para ese setup.

La prueba de DevHub fue la parte que más cambió mi percepción de los modelos locales. Hace unos meses me había costado obtener un comportamiento parecido incluso con modelos frontier. Aquí un 27B ejecutado en mi propia GPU pudo mantener una sesión larga de pentesting, usar herramientas, desarrollar una explotación a partir de la aplicación que tenía delante, pivotar entre componentes y corregir sus propios intentos hasta terminar una máquina Medium.

Sigue habiendo diferencias claras frente a los mejores modelos hosted, tanto en velocidad como en consistencia. Aun así, el nivel actual de los modelos locales ya permite utilizar hardware doméstico para experimentos agentic bastante más serios que una demo de chat o un pequeño ejercicio de coding.