cd /news/computer-vision/wiring-android-s-vulkan-compute-to-a… Β· home β€Ί topics β€Ί computer-vision β€Ί article
[ARTICLE Β· art-158584] src=dev.to β†— pub= topic=computer-vision verified=true sentiment=↑ positive

Wiring Android's Vulkan Compute to a Quantized On-Device Diffusion Model for Real-Time Image Segmentation

A developer published a pattern for wiring raw Vulkan compute shaders directly to a quantized INT8 diffusion-based segmentation model on Android, targeting the Pixel 9's Adreno 750 GPU. The approach bypasses NNAPIβ€”which silently falls back to CPU on unsupported opsβ€”in favor of explicit descriptor set rings, push constants for dynamic tensor dimensions, and ping-pong buffer scheduling to overlap GPU work with CPU post-processing while staying within the device's VkMemoryHeap budget.

by read5 min views1 publishedOct 27, 2026
---
title: "Vulkan Compute Shaders for On-Device AI on Android"
published: true
description: "Wire Vulkan compute to a quantized diffusion model on Android. Bypass NNAPI, manage descriptor sets, and stay within Pixel 9's memory ceiling on Adreno 750."
tags: android, mobile, architecture, performance
canonical_url: https://mvpfactory.co/blog/vulkan-compute-android-on-device-ai
---

## What we are building

Let me show you a pattern I use when NNAPI stops being your friend. We are going to wire raw Vulkan compute directly to a quantized INT8 segmentation model running on Pixel 9 β€” covering descriptor set management, push constants for dynamic tensor dimensions, ping-pong buffer scheduling, and the `VkMemoryHeap` budget that keeps you from hitting OOM on Adreno 750.

## Prerequisites

- NDK r26+ and Vulkan headers in your Android project
- A quantized INT8 model with custom attention-like ops (where NNAPI silent fallback hurts most)
- Familiarity with `VkCommandBuffer` and `VkQueue` basics
- Android GPU Inspector installed for verification

---

## Why not NNAPI?

NNAPI was designed for portability across a heterogeneous device ecosystem. That portability costs you: driver-level graph compilation, opaque buffer staging, and an inference runtime that hides scheduling primitives you need to overlap GPU work with CPU post-processing.

Here is the gotcha that will save you hours: NNAPI does not crash on unsupported ops β€” it falls back silently to CPU execution. Your profiling shows wall-clock latency but not *why*. Here is how the three paths compare on quantized models and Adreno-class GPUs:

| Execution path | Dispatch overhead | Memory visibility | Op coverage |
|---|---|---|---|
| NNAPI (HAL delegated) | High β€” compiled graph, opaque staging | None | Partial β€” silent CPU fallback |
| TFLite GPU Delegate | Medium β€” OpenCL/Vulkan backend | Limited | Good for standard ops |
| Raw Vulkan Compute | Minimal β€” direct VkQueue submit | Full | Complete β€” you write every shader |

Raw Vulkan wins on control. You pay in engineering complexity. That is the right trade-off when targeting a single known GPU family.

---

## Step 1 β€” Descriptor set management

Here is the minimal setup to get this working. Allocate a single `VkDescriptorPool` sized to your worst-case pipeline depth. Bind tensor buffers as `VK_DESCRIPTOR_TYPE_STORAGE_BUFFER`. Do not re-create descriptor sets per frame β€” maintain a small ring of pre-allocated sets and rotate them in sync with your command buffer submissions.

glsl

// INT8 depthwise conv compute shader bindings

layout(set = 0, binding = 0) readonly buffer InputTensor { int8_t input[]; };

layout(set = 0, binding = 1) readonly buffer Weights { int8_t weights[]; };

layout(set = 0, binding = 2) writeonly buffer Output { int32_t output[]; };

For a multi-stage pipeline β€” encoder, bottleneck, decoder β€” each shader stage gets its own `VkDescriptorSetLayout`. Pooling these correctly is the difference between a stable production build and a driver crash under memory pressure.

---

## Step 2 β€” Push constants for dynamic tensor dimensions

The docs do not mention this clearly, but hard-coding tensor dimensions in shader specialization constants forces recompilation on resize. Push constants β€” limited to 128 bytes on most Adreno drivers β€” are how you pass dynamic width, height, stride, and quantization scale at dispatch time with zero overhead.

kotlin

// Kotlin-side push constant upload before vkCmdDispatch

val pushData = ByteBuffer.allocateDirect(16).order(ByteOrder.nativeOrder()).apply {

putInt(inputWidth); putInt(inputHeight)

putFloat(quantScale); putFloat(quantZeroPoint)

}

vkCmdPushConstants(cmdBuffer, pipelineLayout,

VK_SHADER_STAGE_COMPUTE_BIT, 0, 16, pushData)
---

## Step 3 β€” Ping-pong buffer scheduling

A single dispatch-and-wait loop leaves the CPU idle during GPU execution. The fix: maintain two complete sets of input/output buffers. Submit frame N to the GPU, then let the CPU process the mask output from frame N-1 while the GPU works.

This requires explicit `VkSemaphore` synchronization between submissions β€” one semaphore signaled when the GPU finishes writing output, consumed by the CPU-side mask compositor. The scheduling complexity is real, but latency-to-display drops because you eliminate the synchronous stall on every frame.

---

## Step 4 β€” Respect the VkMemoryHeap ceiling on Adreno 750

The Pixel 9's Adreno 750 surfaces multiple `VkMemoryHeap` entries. The device-local heap is the fast path β€” but it is bounded. Allocate your full model weight set, activations, and ping-pong buffers there simultaneously and you will breach the budget and trigger driver-level eviction or OOM.

The safe approach: place read-only weights in the device-local heap once. Activations and ping-pong buffers go into a host-visible heap allocation pool. Query `vkGetPhysicalDeviceMemoryProperties` at init time and gate your allocation strategy on the reported heap sizes β€” never assume a fixed budget.

---

## Gotchas

**Silent NNAPI fallback is invisible in wall-clock profiling.** Use Android GPU Inspector to verify which ops actually dispatch to the accelerator before committing to NNAPI for any custom op layer.

**Descriptor set recreation per frame kills performance.** Pre-allocate your ring and rotate.

**Specialization constants on resize trigger recompilation.** Push constants exist for exactly this reason β€” use them for any per-inference parameter that changes.

**Guessing heap budget constants is always wrong.** Query at runtime, every time, on every device.

---

## Conclusion

NNAPI is a comfortable abstraction until it silently degrades on a custom op. Dropping to raw Vulkan compute with managed descriptor sets, push constants for dynamic tensor dims, and ping-pong buffer scheduling cut end-to-end inference latency on this pipeline and kept memory safely under the Adreno 750's usable `VkMemoryHeap` budget. The engineering overhead is real β€” but for a known GPU family at production cadence, it is the right call.

**Further reading:**
- [Vulkan Compute β€” Khronos spec](https://registry.khronos.org/vulkan/)
- [Android GPU Inspector](https://developer.android.com/agi)
- [VkMemoryHeap docs](https://registry.khronos.org/vulkan/specs/latest/man/html/VkMemoryHeap.html)
── more in #computer-vision 4 stories Β· sorted by recency
── more on @android 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/wiring-android-s-vul…] indexed:0 read:5min 2026-10-27 Β· β€”