
GGUF + llama.cpp: Enviando un Modelo Ajustado en Tu App Movil
Una guia practica para empaquetar modelos de AI ajustados como archivos GGUF y ejecutarlos en iOS y Android con llama.cpp. Incluye tamanos de archivo, benchmarks y patrones de integracion.
Tu modelo ajustado funciona genial en tu laptop. Las respuestas son rapidas, la calidad es exactamente lo que necesitas, y tus evaluaciones son solidas. Ahora necesitas que corra en 50,000 iPhones.
Aqui es donde la mayoria de los desarrolladores moviles se detienen. Incluir un modelo de AI en un binario de app suena intimidante: tamano del binario, restricciones de memoria, throttling termico, sistemas de compilacion especificos de cada plataforma. Pero las herramientas han madurado. Proyectos como PocketPal AI demuestran que una interfaz de chat completa impulsada por llama.cpp corre sin problemas en los telefonos insignia actuales. El camino desde pesos ajustados hasta una app movil en produccion esta definido, es repetible y mucho menos doloroso de lo que era incluso hace 18 meses.
Esta guia recorre cada paso: formato GGUF, opciones de cuantizacion, integracion en iOS y Android, expectativas de rendimiento y estrategias de entrega.
Que Es GGUF
GGUF (GGML Unified Format) es un formato de modelo de archivo unico mantenido por el proyecto llama.cpp. Antes de GGUF, distribuir un modelo significaba manejar archivos separados para pesos, configuracion, vocabulario del tokenizador y tokens especiales. GGUF empaqueta todo en un solo binario portable con una estructura de encabezado bien especificada.
Que contiene un archivo .gguf:
- Pesos del modelo (cuantizados o en precision completa)
- Metadatos de arquitectura (conteo de capas, attention heads, longitud de contexto, parametros rope)
- Vocabulario del tokenizador y reglas de fusion
- Definiciones de tokens especiales (BOS, EOS, tokens de relleno)
El diseno de archivo unico hace que GGUF sea ideal para despliegue movil. Referencias una ruta, cargas un archivo y la inferencia comienza. No se necesita configuracion de multiples archivos. llama.cpp lee GGUF de forma nativa, y Ollama usa GGUF como su formato de almacenamiento interno.
Niveles de Cuantizacion Explicados
La cuantizacion comprime los pesos del modelo de floats de 32 bits o 16 bits a enteros de menor precision. Esto reduce el tamano del archivo y la huella de memoria sustancialmente, con un costo modesto de calidad que frecuentemente es imperceptible para tareas especificas de dominio.
| Formato | Bits por peso | Calidad vs FP16 | Notas |
|---|---|---|---|
| Q4_K_M | ~4.5 bits | -2 a 4% en benchmarks | Mejor relacion tamano-calidad para moviles |
| Q5_K_M | ~5.5 bits | -1 a 2% en benchmarks | Coherencia notablemente mejor, ~20% mas grande |
| Q8_0 | 8 bits | Perdida negligible | Casi sin perdida, 2x el tamano de Q4_K_M |
| F16 | 16 bits | Linea base | Demasiado grande para la mayoria de objetivos moviles |
Tamanos de archivo concretos en Q4_K_M:
- Llama 3.2 1B: 808 MB
- Llama 3.2 3B: 2.02 GB
- Phi-3-mini 3.8B: aproximadamente 2.3 GB
- Llama 3.1 8B: aproximadamente 4.9 GB
Para moviles, Q4_K_M es la recomendacion por defecto. La diferencia de calidad versus Q5_K_M es pequena para modelos ajustados especificos de dominio (que ya han restringido la distribucion de salida), y el ahorro en tamano es significativo. Reserva Q8_0 para escritorio o tablets con capacidad offline donde el almacenamiento es menos restringido.
Un modelo de 1B en Q4_K_M (808 MB) cabe comodamente en cualquier telefono moderno. Un modelo de 3B (2.02 GB) funciona bien en dispositivos insignia. A 4.9 GB, un modelo de 8B solo es apropiado como recurso descargable en dispositivos con mas de 6 GB de RAM, no como binario incluido.
Como Funciona llama.cpp en Moviles
llama.cpp es un motor de inferencia puro en C/C++ para modelos GGUF. La biblioteca central no tiene dependencias externas mas alla de la biblioteca estandar de C++, que es por lo que compila en casi cualquier plataforma.
Para aceleracion, llama.cpp usa:
- Metal (iOS/macOS) para computo GPU via Metal Performance Shaders de Apple
- OpenCL o Vulkan (Android) para computo GPU en silicon Qualcomm, ARM y MediaTek
- Instrinsics NEON SIMD para operaciones de matrices en CPU ARM
- NNAPI (Android) como ruta delegada a NPUs de hardware en chipsets mas nuevos
El proyecto proporciona binarios de ejemplo, pero para integracion movil lo compilas como una biblioteca estatica y llamas a traves de bindings de plataforma. Tanto patrones de wrapper oficial en Swift como en Kotlin existen en la comunidad, y el proyecto PocketPal AI es una referencia open-source bien mantenida para ambas plataformas.
Integracion iOS
Configuracion de Build
llama.cpp usa CMake. Para iOS, compilas cruzadamente una biblioteca estatica apuntando a arm64-apple-ios:
cmake -B build-ios \
-DCMAKE_TOOLCHAIN_FILE=ios.toolchain.cmake \
-DPLATFORM=OS64 \
-DGGML_METAL=ON \
-DBUILD_SHARED_LIBS=OFF \
-DLLAMA_BUILD_TESTS=OFF \
.
cmake --build build-ios --config Release
La bandera GGML_METAL=ON habilita la aceleracion GPU por Metal. Las bibliotecas estaticas libllama.a y libggml.a resultantes se enlazan a tu proyecto Xcode.
Alternativamente, un XCFramework pre-compilado esta disponible desde el paquete Swift Package Manager de llama.cpp en github.com/ggml-org/llama.cpp, que maneja el paso de CMake por ti.
Bindings de Swift
El paquete SPM expone una clase Swift LlamaContext con un inicializador init(model: URL) y un metodo asincrono complete(prompt: String) -> AsyncStream<String> para streaming de tokens.
Una integracion minima se ve asi:
import llama
class ModelRunner {
private var context: LlamaContext?
func load(modelURL: URL) async throws {
context = try await LlamaContext.createContext(path: modelURL.path)
}
func stream(prompt: String) -> AsyncStream<String> {
guard let ctx = context else { return AsyncStream { $0.finish() } }
return AsyncStream { continuation in
Task {
for await token in ctx.completionStream(text: prompt) {
continuation.yield(token)
}
continuation.finish()
}
}
}
}
Gestion de Memoria en iOS
iOS mata procesos que exceden su presupuesto de memoria sin aviso. Reglas clave:
- Carga el modelo una vez al inicio de la app o en un hilo de fondo dedicado. No reinicialices por solicitud.
- Configura
n_ctxconservadoramente. La longitud de contexto determina directamente el tamano del cache KV. Un contexto de 2048 tokens usa significativamente menos memoria que uno de 8192 tokens. La mayoria de los casos de uso moviles necesitan menos de 2048 tokens. - Monitorea las advertencias de memoria. Implementa
applicationDidReceiveMemoryWarningy responde liberando el cache KV (llama allama_kv_cache_clear) en lugar de descargar todo el modelo. - Usa la bandera de carga
mmap. llama.cpp soporta carga de archivos mapeados en memoria (--mmap), que permite al OS paginar los pesos del modelo dentro y fuera. En iOS esto reduce el RSS maximo a costa de una latencia ligeramente mayor en el primer token.
Un modelo 1B Q4_K_M (808 MB) mas un cache KV de 2048 tokens corre comodamente dentro del presupuesto de memoria de iPhones desde la generacion 12 en adelante. Un modelo de 3B necesita dispositivos con al menos 6 GB de RAM (iPhone 15 Pro y posteriores).
Integracion Android
Build NDK
Android usa la cadena de herramientas NDK para codigo C/C++. Agrega llama.cpp como submodulo o copia su fuente en app/src/main/cpp/, luego configura CMakeLists.txt:
cmake_minimum_required(VERSION 3.22)
project(llama_android)
add_subdirectory(llama.cpp)
add_library(llama_jni SHARED jni_bridge.cpp)
target_link_libraries(llama_jni llama ggml android log)
En build.gradle (modulo app):
android {
defaultConfig {
externalNativeBuild {
cmake {
cppFlags "-std=c++17"
arguments "-DGGML_OPENMP=OFF", "-DLLAMA_BUILD_TESTS=OFF"
}
}
}
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
}
}
}
Para aceleracion GPU en Android, pasa -DGGML_OPENCL=ON (requiere headers de OpenCL) o -DGGML_VULKAN=ON (requiere el SDK de Vulkan). En dispositivos Snapdragon, el backend QNN de Qualcomm ofrece aceleracion NPU via -DGGML_QNN=ON, aunque esto requiere el SDK de AI de Qualcomm.
Wrapper JNI y Puente Kotlin
Crea un puente JNI delgado en jni_bridge.cpp que envuelve la API C de llama.cpp:
extern "C" JNIEXPORT jlong JNICALL
Java_com_yourapp_LlamaWrapper_loadModel(JNIEnv *env, jobject, jstring modelPath) {
const char *path = env->GetStringUTFChars(modelPath, nullptr);
llama_model_params mparams = llama_model_default_params();
llama_model *model = llama_load_model_from_file(path, mparams);
env->ReleaseStringUTFChars(modelPath, path);
return reinterpret_cast<jlong>(model);
}
En el lado de Kotlin, una clase wrapper delgada mantiene el puntero nativo y expone una API basada en coroutines:
class LlamaWrapper(private val modelPath: String) {
private var modelHandle: Long = 0
fun load() {
modelHandle = loadModel(modelPath)
}
fun complete(prompt: String, onToken: (String) -> Unit) {
generateTokens(modelHandle, prompt, onToken)
}
fun close() {
if (modelHandle != 0L) {
freeModel(modelHandle)
modelHandle = 0
}
}
private external fun loadModel(path: String): Long
private external fun generateTokens(handle: Long, prompt: String, cb: (String) -> Unit)
private external fun freeModel(handle: Long)
companion object {
init { System.loadLibrary("llama_jni") }
}
}
Manten el modelo cargado en un singleton vinculado al ciclo de vida de la Application, no a Activities individuales. Recrear el modelo en cada transicion de pantalla causara retrasos perceptibles y consumo excesivo de bateria.
Benchmarks de Rendimiento
Throughput del mundo real en hardware insignia actual, ejecutando modelos cuantizados en Q4 o Q8:
| Dispositivo | Modelo | Cuantizacion | Tokens/seg | Backend |
|---|---|---|---|---|
| iPhone 17 Pro | 1.5B | INT8 | 136 | NPU (Apple ANE) |
| Galaxy S25 Ultra | 2B | INT8 | 91 | NPU (Google Tensor) |
| iPhone 16 Pro | 1.5B | Q4 | 22 (sostenido) | Metal GPU |
| Snapdragon 8 Elite | 13B | Q4 | 20+ | Hexagon NPU |
| iPhone 15 Pro | 1B | Q4 | ~18 | Metal GPU |
| Android gama media (SD 7s Gen 3) | 1B | Q4 | 8-12 | CPU NEON |
Para contexto: la velocidad de lectura humana es aproximadamente 5-7 tokens por segundo. Cada dispositivo insignia listado arriba supera el ritmo comodo de lectura con un modelo de 1-3B. Incluso dispositivos de gama media a 8-12 tok/s se sienten responsivos para la mayoria de los casos de uso.
Los numeros de NPU (iPhone 17 Pro a 136 tok/s, Galaxy S25 Ultra a 91 tok/s) representan un cambio radical en capacidad. En rutas aceleradas por NPU, la latencia cae a aproximadamente 1-20% de la linea base de CPU, y la eficiencia energetica por billon de operaciones es significativamente mayor que la inferencia GPU o CPU.
Lo que estos numeros significan para la UX: A 22 tok/s en un iPhone 16 Pro, una respuesta de 200 tokens se renderiza en menos de 10 segundos. La latencia del primer token (tiempo antes de que el stream comience) es tipicamente 200-800ms dependiendo de la longitud del prompt. Ambos son aceptables para la mayoria de los patrones de asistente en app.
Gestion Termica y Bateria
La inferencia LLM sostenida es una de las cargas de trabajo mas intensivas termicamente que un procesador movil maneja. El iPhone 16 Pro pierde aproximadamente 44% de throughput bajo carga sostenida a medida que el SoC hace throttling para proteger la temperatura del hardware. Un benchmark que muestra 22 tok/s puede entregar mas cerca de 12-14 tok/s despues de 5-10 minutos de inferencia continua.
Estrategias practicas de mitigacion:
Limita la duracion de inferencia. Para la mayoria de los casos de uso, las respuestas se completan en menos de 30 segundos. Establece un conteo maximo de tokens (n_predict) apropiado para tu caso de uso. Esto limita el impacto termico por solicitud.
Agrega retrasos entre solicitudes. Para trabajos de procesamiento en segundo plano, inserta una pausa corta entre completaciones. Incluso una pausa breve permite que el SoC disipe calor antes del siguiente paso de inferencia.
Elige modelos mas pequenos para tareas continuas. Un modelo de 1B genera tokens a mayor throughput con sustancialmente menos calor que un modelo de 3B. Para tareas de clasificacion, extraccion o formateo, el modelo mas pequeno frecuentemente produce resultados equivalentes.
Monitorea la temperatura del dispositivo en Android. La API ThermalManager (Android 10+) expone el estado termico en una escala de 0-6. Registra un listener y reduce la frecuencia de inferencia o la longitud de contexto a medida que el dispositivo se calienta. No hay equivalente directo en iOS, pero puedes medir la degradacion del throughput como proxy.
Guia de bateria: Un modelo de 1B corriendo continuamente a maxima velocidad en un iPhone 16 Pro consume aproximadamente 2-3 watts de potencia de SoC por encima de la linea base. Una sesion de inferencia activa reduce la vida de la bateria aproximadamente un 20-30% relativo al uso tipico de app. Para la mayoria de los patrones de asistente en app (solicitudes cortas iniciadas por el usuario con pausas entre ellas), el impacto es mucho menor.
Estrategias de Entrega del Modelo
Como llevas el archivo GGUF al dispositivo importa tanto como como lo ejecutas.
Incluir en el Binario de la App
Pros: experiencia de primer uso con cero latencia, sin requisito de red, sin UX de descarga necesaria. Contras: Limites de tamano del App Store. Apple permite descargas over-the-air hasta 200 MB sin confirmacion del usuario; apps de mas de 4 GB requieren descarga por Wi-Fi. Google Play tiene restricciones similares.
Funciona para: Modelos 1B Q4_K_M (808 MB) como recurso descargable via iOS On-Demand Resources o paquetes de activos de Android App Bundle. El modelo se mantiene fuera del binario principal pero se descarga automaticamente en la instalacion.
Descarga en el Primer Lanzamiento
El patron mas comun para modelos mas grandes. La app se envia sin el modelo, y en el primer lanzamiento (o en una pantalla de "configurar funciones de AI" con opt-in) descarga el GGUF desde tu CDN.
Notas de implementacion:
- Usa tareas de descarga en segundo plano de
URLSessionen iOS para que la descarga continue si el usuario pone la app en segundo plano. - Usa
WorkManagercon restriccionesNETWORK_NOT_ROAMINGoNETWORK_UNMETEREDen Android. - Muestra progreso con una explicacion clara ("Descargando modelo de AI, 2 GB, se recomienda Wi-Fi").
- Cachea la descarga agresivamente. No re-descargues si el archivo ya esta presente y pasa una verificacion de checksum.
- Considera almacenar el GGUF en el directorio
Application Supportde la app (iOS) ofilesDir(Android) en lugar de una ubicacion compartida, para evitar limpieza a nivel de OS en almacenamiento bajo.
Actualizaciones Delta
Cuando lanzas una nueva version ajustada, probablemente no quieres enviar 2 GB a cada usuario en cada actualizacion. Patrones de actualizacion delta:
- Solo adaptador LoRA: Si tu actualizacion es un nuevo ajuste del mismo modelo base, envia solo el archivo del adaptador LoRA (tipicamente 20-200 MB) y cargalo sobre el modelo base congelado en tiempo de inferencia. llama.cpp soporta adaptadores LoRA via la bandera
--lorao la llamada API equivalente. Esto es mucho mas eficiente en ancho de banda que reemplazar el GGUF completo. - Diff de GGUF: Para cambios de arquitectura que requieren una nueva base, existen herramientas para computar diffs binarios entre versiones de GGUF. El parche es mucho mas pequeno que una re-descarga completa si solo un subconjunto de pesos cambio.
- Rutas CDN con version: Almacena modelos en rutas como
/models/v2/model.Q4_K_M.ggufy verifica un endpoint de version al inicio de la app. Actualiza la copia local solo cuando la version remota es mas nueva.
Alternativas de Plataforma a llama.cpp
Antes de comprometerte con la ruta de integracion de llama.cpp, evalua si una API gestionada por la plataforma satisface tus necesidades.
API Foundation Models de Apple (iOS 18.4+ / WWDC 2025)
Apple lanzo una API publica de Swift para inferencia en el dispositivo que apunta a su modelo en el dispositivo de aproximadamente 3B parametros. La API es de alto nivel: describes una tarea con un struct GenerationOptions y recibes texto, JSON estructurado o llamadas a herramientas.
Pros: Sin modelo que descargar o mantener, optimizado para hardware por Apple, API Swift trivialmente simple, sin dolores de cabeza de gestion de memoria.
Contras: No puedes cargar un modelo ajustado personalizado. Estas restringido al modelo base de Apple y sus capacidades. No disponible en Android. La calidad del modelo para dominios especializados puede ser insuficiente.
Usa la API Foundation Models si: tu tarea es lo suficientemente general para que el modelo base de Apple la maneje bien, y quieres enviar rapido sin gestionar archivos de modelo.
Usa llama.cpp con un GGUF personalizado si: necesitas calidad especifica de dominio, comportamiento multiplataforma o control sobre las salidas exactas del modelo.
Google Gemini Nano (Android, ML Kit GenAI APIs, Google I/O 2025)
ML Kit de Google ahora expone inferencia en el dispositivo de Gemini Nano via una API gestionada. Como la oferta de Apple, esto ejecuta un modelo fijo gestionado por el OS, no uno personalizado.
Pros: API simple, sin descarga requerida en dispositivos Pixel y socios soportados, se integra con patrones existentes de ML Kit.
Contras: Disponible solo en Pixel 9 y dispositivos selectos. Sin soporte de modelo personalizado. La consistencia entre dispositivos es limitada.
Para apps en produccion que apuntan a soporte amplio de dispositivos con un modelo ajustado personalizado, llama.cpp con GGUF sigue siendo el enfoque mas portable.
Checklist de Extremo a Extremo
Antes de enviar, verifica:
- GGUF exportado en la cuantizacion correcta para tu nivel de dispositivo objetivo (Q4_K_M para la mayoria de objetivos moviles)
- El tamano de archivo encaja dentro de tu estrategia de entrega (menos de 200 MB para OTA instantaneo, menos de 4 GB para descarga diferida)
- La longitud de contexto (
n_ctx) esta configurada al minimo requerido, no al maximo del modelo - El modelo se carga una vez al inicio de la app o en una cola de fondo dedicada, no por solicitud
- Las advertencias de memoria se manejan: limpia el cache KV antes de descargar el modelo completo
- Throttling termico probado: ejecuta inferencia por mas de 10 minutos y verifica la calidad de salida bajo carga sostenida
- Descarga en segundo plano implementada con retroalimentacion de progreso para entrega del modelo en primer lanzamiento
- Validacion de checksum en el GGUF descargado antes de intentar cargarlo
- Limite de tokens (
n_predict) configurado a un tope sensato para limitar la duracion de inferencia en el peor caso
Comenzando con Ertas
El trabajo de integracion anterior asume que ya tienes un GGUF ajustado. El paso de fine-tuning es donde entra Ertas.
Sube tus datos de dominio, configura parametros de entrenamiento visualmente y exporta el resultado como un GGUF en tu nivel de cuantizacion objetivo. Ertas maneja el computo GPU en la nube, el formateo del dataset y la exportacion cuantizada. Recibes un archivo .gguf listo para incorporar en la integracion iOS o Android descrita arriba.
La capa de inferencia movil es infraestructura open-source. El diferenciador es el modelo dentro de ella: uno que entiende tu dominio, el lenguaje de tus usuarios y los requisitos de salida especificos de tu producto. Eso es lo que el fine-tuning produce, y esa es la parte que tu posees.
Ship AI that runs on your users' devices.
Early bird pricing starts at $14.50/mo — locked in for life. Plans for builders and agencies.
Keep reading

Fine-Tuning for App Developers: A Non-ML-Engineer's Guide
A practical guide to fine-tuning AI models for mobile app developers. Learn LoRA, QLoRA, and GGUF export without needing an ML background.

GGUF Explained: The Open Format That Runs AI Anywhere
GGUF is the file format that made running AI models on consumer hardware practical. Here's what it is, how it works, and why every AI builder should understand it.

Shipping GGUF Models: App Store Bundling vs Post-Install Download
Two ways to get your GGUF model onto the user's device. Bundle it with the app for simplicity, or download post-install for flexibility. Architecture, size limits, and best practices for both.