Aceleración de la NPU con LiteRT

LiteRT proporciona una interfaz unificada para usar las unidades de procesamiento neuronal (NPUs) sin solicitarte que navegues por compiladores, tiempos de ejecución o dependencias de bibliotecas específicos del proveedor. El uso de LiteRT para la aceleración de la NPU mejora el rendimiento de la inferencia en tiempo real y de modelos grandes, y minimiza las copias de memoria a través del uso de búferes de hardware de copia cero.

Comenzar

Proveedores de NPU

LiteRT admite la aceleración de la NPU con los siguientes proveedores:

Qualcomm AI Engine Direct

MediaTek NeuroPilot

Google Tensor

El SDK de Google Tensor está en acceso experimental. Regístrate aquí.

Compilación AOT y en el dispositivo

La NPU de LiteRT admite la compilación AOT y la compilación en el dispositivo para satisfacer tus requisitos de implementación específicos:

  • Compilación sin conexión (AOT): Es la mejor opción para modelos grandes y complejos en los que se conoce el SoC objetivo. La compilación anticipada reduce significativamente los costos de inicialización y disminuye el uso de memoria cuando el usuario inicia tu app.
  • Compilación en línea (en el dispositivo): También conocida como compilación JIT. Esto es ideal para la distribución de modelos pequeños independientes de la plataforma. El modelo se compila en el dispositivo del usuario durante la inicialización, por lo que no se requiere ningún paso de preparación adicional, pero se incurre en un costo de primera ejecución más alto.

En la siguiente guía, se muestra cómo realizar la implementación para la compilación AOT y la compilación en el dispositivo en tres pasos.

Paso 1: Compilación AOT para los SoCs de NPU objetivo

Puedes usar el compilador AOT (anticipado) de LiteRT para compilar tu modelo .tflite en los SoCs compatibles. También puedes segmentar tu compilación para varios proveedores y versiones de SoC de forma simultánea en un solo proceso de compilación. Consulta más detalles en este notebook de compilación AOT de LiteRT. Si bien es opcional, se recomienda la compilación AOT para modelos más grandes, ya que reduce el tiempo de inicialización en el dispositivo. Este paso no es obligatorio para la compilación en el dispositivo.

Paso 2: Realiza la implementación con Google Play si usas Android

En Android, usa Play para la IA en el dispositivo (PODAI) de Google para implementar el modelo y las bibliotecas de tiempo de ejecución de la NPU con tu app.

  • Para los modelos de compilación en el dispositivo: Agrega el archivo .tflite original del modelo directamente en el directorio assets/ de tu app.
  • Para los modelos de compilación AOT: Usa LiteRT para exportar tus modelos compilados en un solo Play AI Pack de Google. Luego, subes el paquete de IA a Google Play para entregar automáticamente los modelos compilados correctos a los dispositivos de los usuarios.
  • Para las bibliotecas de tiempo de ejecución de la NPU, usa Play Feature Delivery para distribuir las bibliotecas de tiempo de ejecución correctas a los dispositivos de los usuarios.

Consulta las siguientes secciones para obtener información sobre cómo realizar la implementación con Play AI Pack y Play Feature Delivery.

Implementa modelos AOT con Play AI Pack

Los siguientes pasos te guiarán para implementar tus modelos compilados con AOT usando los paquetes de IA de Play.

Agrega el paquete de IA al proyecto

Copia los paquetes de IA en el directorio raíz del proyecto de Gradle para importarlos. Por ejemplo:

my_app/
    ...
    ai_packs/
        my_model/...
        my_model_mtk/...

Agrega cada paquete de IA a la configuración de compilación de Gradle:

// my_app/ai_packs/my_model/build.gradle.kts

plugins { id("com.android.ai-pack") }

aiPack {
  packName = "my_model"  // ai pack dir name
  dynamicDelivery { deliveryType = "on-demand" }
}

// Add another build.gradle.kts for my_model_mtk/ as well

Agrega paquetes de IA a la configuración de Gradle

Copia device_targeting_configuration.xml de los AI Packs generados en el directorio del módulo principal de la app. Luego, actualiza settings.gradle.kts:

// my_app/setting.gradle.kts

...
// AI Packs
include(":ai_packs:my_model")
include(":ai_packs:my_model_mtk")

Actualiza build.gradle.kts:

// my_app/build.gradle.kts

android {
 ...

 defaultConfig {
    ...
    // API level 31+ is required for NPU support.
    minSdk = 31
  }

  // AI Packs
  assetPacks.add(":ai_packs:my_model")
  assetPacks.add(":ai_packs:my_model_mtk")
}

Cómo configurar el paquete de IA para la entrega a pedido

La entrega a pedido te permite solicitar el modelo en el tiempo de ejecución, lo que resulta útil si el modelo solo se requiere para ciertos flujos de usuarios. El modelo se descargará en el espacio de almacenamiento interno de tu app. Con la función Android AI Pack configurada en el archivo build.gradle.kts, verifica las capacidades del dispositivo. Consulta también las instrucciones para la entrega en el momento de la instalación y la entrega de seguimiento rápido de PODAI.

val env = Environment.create(BuiltinNpuAcceleratorProvider(context))

val modelProvider = AiPackModelProvider(
    context, "my_model", "model/my_model.tflite") {
    if (NpuCompatibilityChecker.Qualcomm.isDeviceSupported())
      setOf(Accelerator.NPU) else setOf(Accelerator.CPU, Accelerator.GPU)
}
val mtkModelProvider = AiPackModelProvider(
    context, "my_model_mtk", "model/my_model_mtk.tflite") {
    if (NpuCompatibilityChecker.Mediatek.isDeviceSupported())
      setOf(Accelerator.NPU) else setOf()
}
val modelSelector = ModelSelector(modelProvider, mtkModelProvider)
val model = modelSelector.selectModel(env)

val compiledModel = CompiledModel.create(
    model.getPath(),
    CompiledModel.Options(model.getCompatibleAccelerators()),
    env,
)

Cómo implementar bibliotecas de tiempo de ejecución de la NPU con Play Feature Delivery

Play Feature Delivery admite varias opciones de entrega para optimizar el tamaño de la descarga inicial, incluidas la entrega durante la instalación, la entrega a pedido, la entrega condicional y la entrega instantánea. Aquí, mostramos la guía básica de entrega durante la instalación.

Agrega bibliotecas de tiempo de ejecución de la NPU al proyecto

Descarga litert_npu_runtime_libraries.zip para la compilación AOT o litert_npu_runtime_libraries_jit.zip para la compilación en el dispositivo, y descomprímelo en el directorio raíz del proyecto:

my_app/
    ...
    litert_npu_runtime_libraries/
        mediatek_runtime/...
        qualcomm_runtime_v69/...
        qualcomm_runtime_v73/...
        qualcomm_runtime_v75/...
        qualcomm_runtime_v79/...
        qualcomm_runtime_v81/...
        fetch_qualcomm_library.sh

Ejecuta la secuencia de comandos para descargar las bibliotecas de compatibilidad con la NPU. Por ejemplo, ejecuta lo siguiente para las NPUs de Qualcomm:

$ ./litert_npu_runtime_libraries/fetch_qualcomm_library.sh

Agrega bibliotecas de tiempo de ejecución de la NPU a la configuración de Gradle

Copia device_targeting_configuration.xml de los AI Packs generados en el directorio del módulo principal de la app. Luego, actualiza settings.gradle.kts:

// my_app/setting.gradle.kts

...
// NPU runtime libraries
include(":litert_npu_runtime_libraries:runtime_strings")

include(":litert_npu_runtime_libraries:mediatek_runtime")
include(":litert_npu_runtime_libraries:qualcomm_runtime_v69")
include(":litert_npu_runtime_libraries:qualcomm_runtime_v73")
include(":litert_npu_runtime_libraries:qualcomm_runtime_v75")
include(":litert_npu_runtime_libraries:qualcomm_runtime_v79")
include(":litert_npu_runtime_libraries:qualcomm_runtime_v81")

Actualiza build.gradle.kts:

// my_app/build.gradle.kts

android {
 ...
 defaultConfig {
    ...
    // API level 31+ is required for NPU support.
    minSdk = 31

    // NPU only supports arm64-v8a
    ndk { abiFilters.add("arm64-v8a") }
    // Needed for Qualcomm NPU runtime libraries
    packaging { jniLibs { useLegacyPackaging = true } }
  }

  // Device targeting
  bundle {
      deviceTargetingConfig = file("device_targeting_configuration.xml")
      deviceGroup {
        enableSplit = true // split bundle by #group
        defaultGroup = "other" // group used for standalone APKs
      }
  }

  // NPU runtime libraries
  dynamicFeatures.add(":litert_npu_runtime_libraries:mediatek_runtime")
  dynamicFeatures.add(":litert_npu_runtime_libraries:qualcomm_runtime_v69")
  dynamicFeatures.add(":litert_npu_runtime_libraries:qualcomm_runtime_v73")
  dynamicFeatures.add(":litert_npu_runtime_libraries:qualcomm_runtime_v75")
  dynamicFeatures.add(":litert_npu_runtime_libraries:qualcomm_runtime_v79")
  dynamicFeatures.add(":litert_npu_runtime_libraries:qualcomm_runtime_v81")
}

dependencies {
  // Dependencies for strings used in the runtime library modules.
  implementation(project(":litert_npu_runtime_libraries:runtime_strings"))
  ...
}

Paso 3: Inferencias en la NPU con el tiempo de ejecución de LiteRT

LiteRT abstrae la complejidad del desarrollo en versiones específicas del SoC, lo que te permite ejecutar tu modelo en la NPU con solo unas pocas líneas de código. También proporciona un mecanismo de resguardo integrado y sólido: puedes especificar la CPU, la GPU o ambas como opciones, y LiteRT las usará automáticamente si la NPU no está disponible. Convenientemente, la compilación AOT también admite la resiliencia. Proporciona delegación parcial en la NPU, donde los subgrafos no admitidos se ejecutan sin problemas en la CPU o la GPU, según se especifique.

Ejecutar en Kotlin

Consulta un ejemplo de implementación en las siguientes apps de demostración:

Agrega dependencias de Android

Puedes agregar el paquete de Maven más reciente de LiteRT a tus dependencias de build.gradle:

dependencies {
  ...
  implementation("com.google.ai.edge.litert:litert:+")
}

Integración en el tiempo de ejecución

// 1. Load model and initialize runtime.
// If NPU is unavailable, inference will fallback to GPU.
val model =
    CompiledModel.create(
        context.assets,
        "model/mymodel.tflite",
        CompiledModel.Options(Accelerator.NPU, Accelerator.GPU)
    )

// 2. Pre-allocate input/output buffers
val inputBuffers = model.createInputBuffers()
val outputBuffers = model.createOutputBuffers()

// 3. Fill the first input
inputBuffers[0].writeFloat(...)

// 4. Invoke
model.run(inputBuffers, outputBuffers)

// 5. Read the output
val outputFloatArray = outputBuffers[0].readFloat()

Ejecución multiplataforma en C++

Consulta el ejemplo de implementación en la app de C++ de segmentación asíncrona.

Dependencias de compilación de Bazel

Los usuarios de C++ deben compilar las dependencias de la aplicación con la aceleración de la NPU de LiteRT. La regla cc_binary que empaqueta la lógica principal de la aplicación (p.ej., main.cc) requiere los siguientes componentes de tiempo de ejecución:

  • Biblioteca compartida de la API de LiteRT en C: El atributo data debe incluir la biblioteca compartida de la API de LiteRT en C (//litert/c:litert_runtime_c_api_shared_lib) y el objeto compartido de envío específico del proveedor para la NPU (//litert/vendors/qualcomm/dispatch:dispatch_api_so).
  • Bibliotecas de backend específicas de la NPU: Por ejemplo, las bibliotecas de Qualcomm AI RT (QAIRT) para el host de Android (como libQnnHtp.so, libQnnHtpPrepare.so) y la biblioteca de DSP de Hexagon correspondiente (libQnnHtpV79Skel.so). Esto garantiza que el tiempo de ejecución de LiteRT pueda descargar los cálculos en la NPU.
  • Dependencias de atributos: El atributo deps se vincula con dependencias esenciales en tiempo de compilación, como el búfer de tensor de LiteRT (//litert/cc:litert_tensor_buffer) y la API para la capa de envío de la NPU (//litert/vendors/qualcomm/dispatch:dispatch_api). Esto permite que el código de tu aplicación interactúe con la NPU a través de LiteRT.
  • Archivos de modelos y otros recursos: Se incluyen a través del atributo data.

Esta configuración permite que tu archivo binario compilado cargue y use de forma dinámica la NPU para la inferencia acelerada del aprendizaje automático.

Configura un entorno de NPU

Algunos backends de la NPU requieren dependencias o bibliotecas de tiempo de ejecución. Cuando se usa la API del modelo compilado, LiteRT organiza estos requisitos a través de un objeto Environment. Usa el siguiente código para encontrar las bibliotecas o los controladores de la NPU adecuados:

// Provide a dispatch library directory (following is a hypothetical path) for the NPU
std::vector<Environment::Option> environment_options = {
    {
      Environment::OptionTag::DispatchLibraryDir,
      "/usr/lib64/npu_dispatch/"
    }
};

LITERT_ASSIGN_OR_RETURN(auto env, Environment::Create(absl::MakeConstSpan(environment_options)));

Integración en el tiempo de ejecución

En el siguiente fragmento de código, se muestra una implementación básica de todo el proceso en C++:

// 1. Load the model that has NPU-compatible ops
LITERT_ASSIGN_OR_RETURN(auto model, Model::Load("mymodel_npu.tflite"));

// 2. Create a compiled model with NPU acceleration
//    See following section on how to set up NPU environment
LITERT_ASSIGN_OR_RETURN(auto compiled_model,
  CompiledModel::Create(env, model, kLiteRtHwAcceleratorNpu));

// 3. Allocate I/O buffers
LITERT_ASSIGN_OR_RETURN(auto input_buffers, compiled_model.CreateInputBuffers());
LITERT_ASSIGN_OR_RETURN(auto output_buffers, compiled_model.CreateOutputBuffers());

// 4. Fill model inputs (CPU array -> NPU buffers)
float input_data[] = { /* your input data */ };
input_buffers[0].Write<float>(absl::MakeConstSpan(input_data, /*size*/));

// 5. Run inference
compiled_model.Run(input_buffers, output_buffers);

// 6. Access model output
std::vector<float> data(output_data_size);
output_buffers[0].Read<float>(absl::MakeSpan(data));

Copia cero con aceleración de la NPU

El uso de la copia cero permite que una NPU acceda a los datos directamente en su propia memoria sin necesidad de que la CPU copie esos datos de forma explícita. Al no copiar datos hacia y desde la memoria de la CPU, la copia cero puede reducir significativamente la latencia de extremo a extremo.

El siguiente código es un ejemplo de implementación de la NPU de copia cero con AHardwareBuffer, que pasa datos directamente a la NPU. Esta implementación evita los viajes de ida y vuelta costosos a la memoria de la CPU, lo que reduce significativamente la sobrecarga de la inferencia.

// Suppose you have AHardwareBuffer* ahw_buffer

LITERT_ASSIGN_OR_RETURN(auto tensor_type, model.GetInputTensorType("input_tensor"));

LITERT_ASSIGN_OR_RETURN(auto npu_input_buffer, TensorBuffer::CreateFromAhwb(
    env,
    tensor_type,
    ahw_buffer,
    /* offset = */ 0
));

std::vector<TensorBuffer> input_buffers{npu_input_buffer};

LITERT_ASSIGN_OR_RETURN(auto output_buffers, compiled_model.CreateOutputBuffers());

// Execute the model
compiled_model.Run(input_buffers, output_buffers);

// Retrieve the output (possibly also an AHWB or other specialized buffer)
auto ahwb_output = output_buffers[0].GetAhwb();

Encadena varias inferencias de la NPU

En el caso de las canalizaciones complejas, puedes encadenar varias inferencias de la NPU. Dado que cada paso usa un búfer compatible con el acelerador, tu canalización permanece principalmente en la memoria administrada por la NPU:

// compiled_model1 outputs into an AHWB
compiled_model1.Run(input_buffers, intermediate_buffers);

// compiled_model2 consumes that same AHWB
compiled_model2.Run(intermediate_buffers, final_outputs);

Almacenamiento en caché de compilación en el dispositivo de la NPU

LiteRT admite la compilación en el dispositivo de la NPU (conocida como JIT) de modelos .tflite. La compilación JIT puede ser especialmente útil en situaciones en las que no es factible compilar el modelo con anticipación.

Sin embargo, la compilación JIT puede generar cierta latencia y sobrecarga de memoria para traducir el modelo proporcionado por el usuario en instrucciones de código de bytes de la NPU a pedido. Para minimizar el impacto en el rendimiento, se pueden almacenar en caché los artefactos de compilación de la NPU.

Cuando el almacenamiento en caché está habilitado, LiteRT solo activará la recompilación del modelo cuando sea necesario, por ejemplo:

  • Cambió la versión del complemento del compilador de la NPU del proveedor.
  • Cambió la huella digital de compilación de Android.
  • Se cambió el modelo proporcionado por el usuario.
  • Cambiaron las opciones de compilación.

Para habilitar el almacenamiento en caché de la compilación de la NPU, especifica la etiqueta de entorno CompilerCacheDir en las opciones del entorno. El valor debe establecerse en una ruta de acceso grabable existente de la aplicación.

   const std::array environment_options = {
        litert::Environment::Option{
            /*.tag=*/litert::Environment::OptionTag::CompilerPluginLibraryDir,
            /*.value=*/kCompilerPluginLibSearchPath,
        },
        litert::Environment::Option{
            litert::Environment::OptionTag::DispatchLibraryDir,
            kDispatchLibraryDir,
        },
        // 'kCompilerCacheDir' will be used to store NPU-compiled model
        // artifacts.
        litert::Environment::Option{
            litert::Environment::OptionTag::CompilerCacheDir,
            kCompilerCacheDir,
        },
    };

    // Create an environment.
    LITERT_ASSERT_OK_AND_ASSIGN(
        auto environment, litert::Environment::Create(environment_options));

    // Load a model.
    auto model_path = litert::testing::GetTestFilePath(kModelFileName);
    LITERT_ASSERT_OK_AND_ASSIGN(auto model,
                                litert::Model::CreateFromFile(model_path));

    // Create a compiled model, which only triggers NPU compilation if
    // required.
    LITERT_ASSERT_OK_AND_ASSIGN(
        auto compiled_model, litert::CompiledModel::Create(
                                 environment, model, kLiteRtHwAcceleratorNpu));

Ejemplo de ahorro de latencia y memoria:

El tiempo y la memoria necesarios para la compilación de la NPU pueden variar según varios factores, como el chip de la NPU subyacente, la complejidad del modelo de entrada, etcétera.

En la siguiente tabla, se comparan el tiempo de inicialización del tiempo de ejecución y el consumo de memoria cuando se requiere la compilación de la NPU y cuando se puede omitir la compilación debido al almacenamiento en caché. En un dispositivo de muestra, obtenemos lo siguiente:

Modelo de TFLite Inicialización del modelo con compilación de la NPU Inicialización del modelo con compilación almacenada en caché Inicializa el espacio en memoria con la compilación de la NPU Inicializa la memoria con la compilación almacenada en caché
torchvision_resnet152.tflite 7465.22 ms 198.34 ms 1525.24 MB 355.07 MB
torchvision_lraspp_mobilenet_v3_large.tflite 1,592.54 ms 166.47 ms 254.90 MB 33.78 MB

En otro dispositivo, obtenemos lo siguiente:

Modelo de TFLite Inicialización del modelo con compilación de la NPU Inicialización del modelo con compilación almacenada en caché Inicializa el espacio en memoria con la compilación de la NPU Inicializa la memoria con la compilación almacenada en caché
torchvision_resnet152.tflite 2766.44 ms 379.86 ms 653.54 MB 501.21 MB
torchvision_lraspp_mobilenet_v3_large.tflite 784.14 ms 231.76 ms 113.14 MB 67.49 MB