Back to blog
    llama.cpp en Android: guia de integracion con Kotlin
    llama.cppAndroidKotlinintegrationon-device AIVulkansegment:mobile-builder

    llama.cpp en Android: guia de integracion con Kotlin

    Guia paso a paso para integrar llama.cpp en una app Android con Kotlin. Bindings JNI, aceleracion GPU con Vulkan, carga de modelos y gestion de memoria en el espectro de dispositivos Android.

    EErtas Team·

    llama.cpp ejecuta modelos de lenguaje GGUF en dispositivos Android usando multi-threading de CPU y aceleracion GPU con Vulkan. El proyecto llama.android proporciona bindings Kotlin pre-construidos a traves de JNI, haciendo la integracion directa para desarrolladores Kotlin.

    Esta guia cubre el camino completo de integracion desde la configuracion del proyecto hasta el despliegue en produccion.

    Opciones de integracion

    Opcion 1: Libreria llama.android (Recomendado)

    El repositorio de llama.cpp incluye llama.android, una libreria Android pre-construida con bindings Kotlin. Este es el camino mas rapido a IA en el dispositivo funcional.

    Agregala a tu proyecto:

    1. Clona o descarga el modulo llama.android del repositorio de llama.cpp
    2. Incluyelo como modulo en tu proyecto Android
    3. Agrega la dependencia en el build.gradle.kts de tu app
    dependencies {
        implementation(project(":llama"))
    }
    

    Opcion 2: Compilar desde fuente

    Para mas control, compila llama.cpp como libreria nativa usando el Android NDK:

    mkdir build-android && cd build-android
    cmake .. \
        -DCMAKE_TOOLCHAIN_FILE=$NDK_PATH/build/cmake/android.toolchain.cmake \
        -DANDROID_ABI=arm64-v8a \
        -DANDROID_PLATFORM=android-26 \
        -DLLAMA_VULKAN=ON \
        -DBUILD_SHARED_LIBS=ON
    cmake --build . --config Release
    

    Esto produce libllama.so que incluyes en tu directorio jniLibs.

    Opcion 3: AAR pre-compilado

    Algunos proyectos de la comunidad publican llama.cpp como un AAR (Android Archive) que puedes incluir como dependencia Maven. Busca versiones recientes y mantenidas.

    Configuracion del proyecto

    Requisitos minimos

    • Android API 26+ (Android 8.0)
    • Arquitectura objetivo ARM64 (arm64-v8a)
    • NDK r25+ para compilar codigo nativo
    • 4GB+ RAM en dispositivo objetivo (para modelos 1B)

    Configuracion de build

    // app/build.gradle.kts
    android {
        defaultConfig {
            ndk {
                abiFilters += "arm64-v8a" // Solo ARM 64-bit
            }
        }
    }
    

    Permisos

    No se requieren permisos especiales para inferencia. Para descarga de modelos:

    <uses-permission android:name="android.permission.INTERNET" />
    

    Cargando un modelo

    class LlamaEngine(private val context: Context) {
        private var model: Long = 0 // Puntero nativo
        private var ctx: Long = 0   // Puntero de contexto nativo
    
        suspend fun loadModel(modelPath: String) = withContext(Dispatchers.Default) {
            // Cargar modelo con aceleracion GPU
            model = LlamaNative.loadModel(
                modelPath = modelPath,
                nGpuLayers = 99,    // Descargar todas las capas a Vulkan
            )
            require(model != 0L) { "Fallo al cargar modelo" }
    
            // Crear contexto de inferencia
            ctx = LlamaNative.createContext(
                model = model,
                nCtx = 2048,        // Ventana de contexto
                nThreads = 4,       // Hilos CPU
                nBatch = 512,       // Tamano de lote
            )
            require(ctx != 0L) { "Fallo al crear contexto" }
        }
    
        fun unload() {
            if (ctx != 0L) {
                LlamaNative.freeContext(ctx)
                ctx = 0
            }
            if (model != 0L) {
                LlamaNative.freeModel(model)
                model = 0
            }
        }
    }
    

    Aceleracion GPU con Vulkan

    Vulkan es la API de computo GPU de Android. llama.cpp la usa para acelerar operaciones matriciales durante la inferencia. Habilitala configurando nGpuLayers al conteo de capas del modelo.

    El soporte de Vulkan depende del dispositivo:

    • Snapdragon 8 Gen 2+: Soporte completo de computo Vulkan, mejor rendimiento
    • Tensor G3/G4: Buen soporte de Vulkan
    • Snapdragon 7 Gen 3+: Vulkan soportado, aceleracion moderada
    • Dispositivos antiguos/economicos: Pueden carecer de computo Vulkan. Recurre a CPU automaticamente.

    Verifica la disponibilidad de Vulkan en tiempo de ejecucion:

    fun isVulkanAvailable(): Boolean {
        return try {
            val vk = android.hardware.HardwareBuffer::class.java
            android.os.Build.VERSION.SDK_INT >= 26
            // Mas robusto: intentar crear una instancia Vulkan
        } catch (e: Exception) {
            false
        }
    }
    

    Generando texto

    class LlamaEngine(private val context: Context) {
        // ... loadModel y unload de arriba
    
        suspend fun generate(
            prompt: String,
            maxTokens: Int = 256,
            temperature: Float = 0.7f,
            onToken: (String) -> Unit = {}
        ): String = withContext(Dispatchers.Default) {
            val result = StringBuilder()
    
            LlamaNative.generate(
                context = ctx,
                prompt = prompt,
                maxTokens = maxTokens,
                temperature = temperature,
            ) { token ->
                result.append(token)
                // Despachar al hilo principal para actualizacion de UI
                withContext(Dispatchers.Main) {
                    onToken(token)
                }
            }
    
            result.toString()
        }
    }
    

    Integracion con ViewModel

    class AiViewModel(application: Application) : AndroidViewModel(application) {
        private val engine = LlamaEngine(application)
        private val _response = MutableStateFlow("")
        val response: StateFlow<String> = _response
        private val _isGenerating = MutableStateFlow(false)
        val isGenerating: StateFlow<Boolean> = _isGenerating
    
        fun loadModel(path: String) {
            viewModelScope.launch {
                engine.loadModel(path)
            }
        }
    
        fun generate(prompt: String) {
            viewModelScope.launch {
                _isGenerating.value = true
                _response.value = ""
    
                engine.generate(prompt, maxTokens = 256) { token ->
                    _response.value += token
                }
    
                _isGenerating.value = false
            }
        }
    
        override fun onCleared() {
            engine.unload()
            super.onCleared()
        }
    }
    

    UI con Jetpack Compose

    @Composable
    fun AiChatScreen(viewModel: AiViewModel = viewModel()) {
        val response by viewModel.response.collectAsState()
        val isGenerating by viewModel.isGenerating.collectAsState()
        var input by remember { mutableStateOf("") }
    
        Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
            // Area de respuesta
            Text(
                text = response,
                modifier = Modifier.weight(1f).verticalScroll(rememberScrollState())
            )
    
            // Area de entrada
            Row(modifier = Modifier.fillMaxWidth()) {
                TextField(
                    value = input,
                    onValueChange = { input = it },
                    modifier = Modifier.weight(1f),
                    enabled = !isGenerating
                )
                Button(
                    onClick = {
                        viewModel.generate(input)
                        input = ""
                    },
                    enabled = !isGenerating
                ) {
                    Text("Enviar")
                }
            }
        }
    }
    

    Gestion de memoria

    La gestion de memoria de Android es agresiva. El sistema elimina procesos en segundo plano para liberar RAM y puede terminar tu app bajo presion de memoria.

    Verificacion de memoria disponible

    fun getAvailableMemoryMb(): Long {
        val memInfo = ActivityManager.MemoryInfo()
        val activityManager = getSystemService(ACTIVITY_SERVICE) as ActivityManager
        activityManager.getMemoryInfo(memInfo)
        return memInfo.availMem / (1024 * 1024)
    }
    
    fun canLoadModel(modelSizeMb: Long): Boolean {
        val available = getAvailableMemoryMb()
        // Reservar 500MB para app y SO
        return available > modelSizeMb + 500
    }
    

    Gestion del ciclo de vida

    class AiService : LifecycleObserver {
        private var engine: LlamaEngine? = null
    
        @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
        fun onResume() {
            // Considerar cargar modelo si la pantalla de IA esta activa
        }
    
        @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
        fun onPause() {
            // Descargar modelo para liberar memoria
            engine?.unload()
        }
    }
    

    Manejador onTrimMemory

    override fun onTrimMemory(level: Int) {
        super.onTrimMemory(level)
        if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW) {
            engine?.unload()
        }
    }
    

    Entrega del modelo

    Asset Delivery (Play Feature Delivery)

    Para modelos mayores de 150MB, usa Play Asset Delivery para evitar el limite de tamano de APK:

    // build.gradle.kts
    assetPacks += ":model_pack"
    
    // model_pack/build.gradle.kts
    plugins {
        id("com.android.asset-pack")
    }
    assetPack {
        packName.set("model_pack")
        dynamicDelivery {
            deliveryType.set("install-time") // o "fast-follow" o "on-demand"
        }
    }
    

    Descarga post-instalacion

    Descarga el modelo despues de la instalacion:

    suspend fun downloadModel(url: String, destination: File): Boolean {
        return withContext(Dispatchers.IO) {
            val client = OkHttpClient()
            val request = Request.Builder().url(url).build()
            val response = client.newCall(request).execute()
    
            response.body?.let { body ->
                val totalBytes = body.contentLength()
                destination.outputStream().use { output ->
                    body.byteStream().use { input ->
                        val buffer = ByteArray(8192)
                        var bytesRead: Long = 0
                        var read: Int
    
                        while (input.read(buffer).also { read = it } != -1) {
                            output.write(buffer, 0, read)
                            bytesRead += read
                            val progress = bytesRead.toFloat() / totalBytes
                            // Actualizar UI de progreso
                        }
                    }
                }
                true
            } ?: false
        }
    }
    

    Usa WorkManager para descargas en segundo plano que sobrevivan reinicios de la app.

    Pruebas en diferentes dispositivos

    El espectro de dispositivos Android es amplio. Prueba en:

    1. Flagship (SD 8 Gen 3, 12GB): Verifica rendimiento en mejor caso
    2. Gama media (SD 7 Gen 3, 6-8GB): Verifica que el modelo 1B se ejecuta fluido
    3. Economico (SD 6 Gen 3, 4GB): Verifica respaldo gracioso si el modelo no puede cargar
    4. Flagship antiguo (SD 8 Gen 1, 8GB): Verifica que flagships de 2 anos funcionan

    Usa Firebase Test Lab o BrowserStack para pruebas de cobertura de dispositivos sin poseer cada dispositivo.

    Checklist de produccion

    1. El modelo carga y genera correctamente en el dispositivo de especificacion minima
    2. La aceleracion Vulkan se detecta y usa cuando esta disponible
    3. El respaldo a CPU funciona cuando Vulkan no esta disponible
    4. La verificacion de memoria previene la carga en dispositivos con poca RAM
    5. El modelo se descarga en onPause/onTrimMemory
    6. El progreso de descarga se muestra correctamente para entrega post-instalacion
    7. La integridad del modelo se verifica despues de la descarga (SHA256)
    8. La generacion puede ser cancelada por el usuario
    9. La app funciona normalmente cuando el modelo no esta disponible

    El modelo GGUF es lo que determina la calidad. Un modelo fine-tuned con tus datos de dominio (via Ertas o similar) producira respuestas especificas para el proposito de tu app. La integracion con llama.cpp es identica independientemente del modelo que uses.

    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