On-Device LLM Inference with MediaPipe

AIMobileOn-DeviceMediaPipe
Share on LinkedIn

Google's MediaPipe LLM Inference API gave us a working Android prototype in an afternoon — load Gemma 2B, stream tokens to a Compose Text, GPU acceleration enabled with one flag. The same task took three days with raw llama.cpp JNI, though we eventually switched for a custom Qwen quant llama.cpp didn't support yet. MediaPipe sits in the middle: opinionated, Google-maintained, optimized for Gemma and a short list of models, with less plumbing than rolling your own C++ bridge.

MediaPipe LLM Inference stack

┌─────────────────────────────────────────┐
│  Your app (Kotlin / Swift)              │
├─────────────────────────────────────────┤
│  MediaPipe Tasks GenAI API              │
│  LlmInference · LlmInferenceSession     │
├─────────────────────────────────────────┤
│  GPU delegate (OpenCL / Metal) or CPU   │
├─────────────────────────────────────────┤
│  Converted model (.bin / task bundle)   │
└─────────────────────────────────────────┘

Part of Google AI Edge — same family as MediaPipe vision/audio tasks, unified delegate system.

Setup (Android Gradle)

// libs.versions.toml / build.gradle.kts
dependencies {
    implementation("com.google.mediapipe:tasks-genai:0.10.24")
}

Copy converted model to device storage or assets:

class LocalLlmRepository(private val context: Context) {
    private var llm: LlmInference? = null

    suspend fun initialize(modelPath: String) = withContext(Dispatchers.IO) {
        val options = LlmInference.LlmInferenceOptions.builder()
            .setModelPath(modelPath)
            .setMaxTokens(512)
            .setPreferredBackend(LlmInference.Backend.GPU)
            .build()
        llm = LlmInference.createFromOptions(context, options)
    }
}

Fallback to CPU when GPU delegate fails (common on emulators):

try {
    options.setPreferredBackend(LlmInference.Backend.GPU)
} catch (e: MediaPipeException) {
    options.setPreferredBackend(LlmInference.Backend.CPU)
}

Streaming generation.

fun summarize(text: String): Flow<String> = callbackFlow {
    val session = LlmInferenceSession.createFromOptions(
        llm!!,
        LlmInferenceSession.LlmInferenceSessionOptions.builder()
            .setTopK(40)
            .setTemperature(0.8f)
            .build()
    )

    session.addQueryChunk("""
        Summarize in 3 bullet points:
        $text
    """.trimIndent())

    session.generateResponseAsync { partial, done ->
        trySend(partial)
        if (done) close()
    }

    awaitClose { session.close() }
}

Collect in ViewModel with stateIn — update UI per partial token for responsive feel.

iOS integration.

import MediaPipeTasksGenAI

var llm: LlmInference?

func loadModel(path: String) throws {
    var options = LlmInferenceOptions()
    options.modelPath = path
    options.maxTokens = 512
    options.preferredBackend = .gpu
    llm = try LlmInference(options: options)
}

func generate(prompt: String) async throws -> AsyncStream<String> {
    var sessionOptions = LlmInferenceSessionOptions()
    sessionOptions.topK = 40
    sessionOptions.temperature = 0.8
    let session = try LlmInferenceSession(llm: llm!, options: sessionOptions)
    try session.addQueryChunk(inputText: prompt)
    return session.generateResponseAsync()
}

Metal delegate on Apple Silicon and recent A-series chips.

Model conversion.

MediaPipe models aren't raw GGUF — convert via Google-provided scripts or download preconverted:

# Example pattern — check current ai-edge-torch docs for exact commands
python -m mediapipe_model_maker.convert \
  --model_path gemma-2b-it \
  --output_path gemma-2b-it.task

Preconverted models often hosted on Kaggle Models or Google's model zoo. Verify:

Configuration tuning.

Parameter Effect Mobile recommendation
maxTokens Output cap 256–512 for summaries
topK Sampling diversity 20–40
temperature Randomness 0.3 factual, 0.8 creative
preferredBackend GPU vs CPU GPU first, CPU fallback

Long input contexts consume RAM linearly — truncate input text before addQueryChunk. For long documents, map-reduce: chunk → summarize each → merge summaries.

Model delivery.

  1. Bundled in APK/IPA — instant, increases store download size
  2. Play Asset Delivery / ODR — large models on-demand
  3. First-run download — ProgressBar + Wi-Fi-only checkbox
suspend fun ensureModel(): String {
    val dest = File(context.filesDir, "gemma-2b.task")
    if (!dest.exists()) {
        downloadWithChecksum(MODEL_URL, dest, expectedSha256 = HASH)
    }
    return dest.absolutePath
}

Error handling and lifecycle.

MediaPipe vs llama.cpp.

Criterion MediaPipe llama.cpp
Time to first demo Hours Days
Model catalog Google-curated Any GGUF
GPU on Android Built-in delegate Vulkan manual
iOS Metal Supported Supported
Community models Limited Extensive
Binary size tasks-genai AAR libllama.so per ABI

Hybrid approach: MediaPipe for MVP with Gemma; migrate to llama.cpp if you need Qwen/Llama custom quants.

Production checklist

We shipped MediaPipe Gemma 1B for email subject-line suggestions — 40 tok/s on Pixel 7a, 1.1GB model via PAD. Quality sufficient; 2B was marginal gain for 2× RAM.

Lock tasks-genai version in Gradle and CocoaPods — Google updates can change delegate behavior between patch releases. Test emulator CPU-only path explicitly; GPU delegates fail silently on CI emulators while working on hardware. Provide user-visible "downloading model" and "preparing AI" states; first load takes seconds and feels broken without feedback. Compare MediaPipe latency against llama.cpp on the same device before committing — for unsupported models, hybrid fallback may be cheaper than waiting for official conversions. Document minimum SDK and chip requirements in store listings to reduce one-star "doesn't work" reviews from incompatible devices. Re-run load tests after each tasks-genai bump; delegate selection logic changes more often than release notes suggest. Treat first-token latency as a product metric, not just an engineering benchmark.

Common production mistakes

Teams get on device llm mediapipe llm inference wrong in predictable ways:

Production implementations of on device llm mediapipe llm inference fail when staging mirrors production topology poorly, rollback is untested, and on-call runbooks describe the happy path only.

Resources

Frequently asked questions

What models does MediaPipe LLM Inference support?

Primarily Gemma (1B, 2B), Phi-2, Falcon, and StableLM variants converted to MediaPipe's flatbuffer format (.task or bundled weights). The supported model list evolves — check Google's AI Edge documentation for current conversions and hardware requirements.

How does MediaPipe compare to llama.cpp on mobile?

MediaPipe offers a higher-level Android/iOS API with GPU delegation (OpenCL/Vulkan/Metal) and Google-maintained integration — less JNI boilerplate. llama.cpp offers broader GGUF model support and finer control. Choose MediaPipe for faster integration with supported models; llama.cpp for custom models and quant flexibility.

Can MediaPipe LLM run fully offline?

Yes. Models ship in-app or download once to device storage. Inference requires no network after model load. Verify model checksum on download and store in app-private storage.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →