Aceleración de la NPU con LiteRT

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

Comenzar

Modelos de AA clásicos

Para los modelos de AA clásicos, consulta las siguientes aplicaciones de demostración.

Modelos de IA generativa

Para los modelos de IA generativa, consulta las siguientes demostraciones y guías:

Proveedores de NPU

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

Google Tensor

  • Admite la ejecución de AOT a través de la API de CompiledModel.
  • Consulta Google Tensor para obtener detalles de configuración.

Qualcomm AI Engine Direct

MediaTek NeuroPilot

Intel OpenVino

  • Admite la ejecución de compilación AOT e integrada en el dispositivo a través de la API de CompiledModel.
  • Consulta Intel OpenVino para obtener detalles de configuración.

Compilación AOT e integrada en el dispositivo

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

  • Compilación sin conexión (AOT): Es más adecuada para modelos grandes y complejos en los que se conoce el SoC de destino. La compilación ahead-of-time reduce significativamente los costos de inicialización y disminuye el uso de memoria cuando el usuario inicia tu app.
  • Compilación en línea (integrada en el dispositivo): También conocida como compilación JIT. 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, lo que no requiere un paso de preparación adicional, pero genera un costo más alto en la primera ejecución.

A continuación, se muestra cómo puedes implementar tu modelo con las opciones de compilación AOT o integrado en el dispositivo:

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

Puedes usar el compilador AOT (ahead of time) de LiteRT para compilar tu modelo .tflite en los SoCs compatibles. También puedes segmentar varios proveedores y versiones de SoC de forma simultánea dentro de 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 para reducir el tiempo de inicialización integrado en el dispositivo. Este paso no es obligatorio para la compilación integrada en el dispositivo.

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

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

  • Para los modelos de compilación integrado en el dispositivo: agrega el archivo de modelo .tflite original directamente al directorio assets/ de tu app.
  • Para los modelos de compilación AOT: usa LiteRT para exportar tus modelos compilados a un solo paquete de IA de Google Play. Luego, sube 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 entorno de ejecución de NPU, usa Play Feature Delivery para distribuir las bibliotecas de entorno de ejecución correctas a los dispositivos de los usuarios.

Consulta las siguientes secciones sobre cómo realizar la implementación con el paquete de IA de Play y Play Feature Delivery.

Implementa modelos AOT con el paquete de IA de Play

En los siguientes pasos, se te guiará para implementar tus modelos compilados AOT con los paquetes de IA de Play.

Agrega el paquete de IA al proyecto

Para importar paquetes de IA al proyecto de Gradle, copia los paquetes de IA en el directorio raíz del proyecto de Gradle. 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 paquetes de IA generados al 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")
}

Configura el paquete de IA para la entrega on demand

La entrega on demand te permite solicitar el modelo en el tiempo de ejecución, lo que es útil si el modelo solo es necesario para ciertos flujos de usuarios. El modelo se descargará en el espacio de almacenamiento interno de tu app. Con la función de paquete de IA de Android 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 cpuGpuModelProvider =
      ModelProvider.staticModel(
        ModelProvider.Type.ASSET,
        "model/my_model_cpu_gpu.tflite",
        if (accelerator != Accelerator.NPU) accelerator else Accelerator.CPU,
      )

val qualcommNpuModelProvider =
  AiPackModelProvider(context, "my_model", "model/my_model.tflite")
  {
    buildSet {
      if (
        accelerator == Accelerator.NPU && NpuCompatibilityChecker.Qualcomm.isDeviceSupported()
      )
        add(Accelerator.NPU)
    }
  }

val mtkNpuModelProvider =
  AiPackModelProvider(context, "my_model_mtk", "model/my_model.tflite")
  {
    buildSet {
      if (
        accelerator == Accelerator.NPU && NpuCompatibilityChecker.Mediatek.isDeviceSupported()
      )
        add(Accelerator.NPU)
    }
  }

val googleTensorTpuModelProvider =
  AiPackModelProvider(context, "my_model", "model/my_model.tflite")
  {
    buildSet {
      if (accelerator == Accelerator.NPU &&
          NpuCompatibilityChecker.GoogleTensor.isDeviceSupported()
      )
        add(Accelerator.NPU)
    }
  }

val aiPackModelProvider =
        ModelSelector(cpuGpuModelProvider, mtkNpuModelProvider, qualcommNpuModelProvider, googleTensorTpuModelProvider)
          .selectModel(env)

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

Implementa bibliotecas de entorno de ejecución de NPU con Play Feature Delivery

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

Agrega bibliotecas de entorno de ejecución de NPU al proyecto

Descarga litert_npu_runtime_libraries.zip de la versión más reciente para la compilación AOT o litert_npu_runtime_libraries_jit.zip de la versión más reciente para la compilación integrado en el dispositivo y descomprímela en el directorio raíz del proyecto:

my_app/
    ...
    litert_npu_runtime_libraries/
        google_tensor_runtime/...
        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 NPU. Por ejemplo, ejecuta lo siguiente para las NPU de Qualcomm:

$ ./litert_npu_runtime_libraries/fetch_qualcomm_library.sh

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

Copia device_targeting_configuration.xml de los paquetes de IA generados al 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:google_tensor_runtime")
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:google_tensor_runtime")
  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: Inferencia en NPU con el entorno de ejecución de LiteRT

LiteRT abstrae la complejidad del desarrollo en versiones específicas de 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 sólido y integrado: puedes especificar CPU, GPU o ambas como opciones, y LiteRT las usará automáticamente si la NPU no está disponible. De manera conveniente, la compilación AOT también admite el resguardo. Proporciona delegación parcial en la NPU, en la que los subgrafos no compatibles se ejecutan sin problemas en la CPU o la GPU según lo especificado.

Ejecuta en Kotlin

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

Agrega dependencias de Android

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

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

Integración del entorno 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()

Ejecuta en C++ multiplataforma

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 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 del entorno de ejecución:

  • Biblioteca compartida de la API de C de LiteRT: El atributo data debe incluir la biblioteca compartida de la API de C de LiteRT (//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 IA 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 entorno de ejecución de LiteRT pueda transferir los cálculos a la NPU.
  • Dependencias de atributos: el atributo deps se vincula con dependencias esenciales en el 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 modelo y otros recursos: Se incluyen a través del atributo data.

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

Configura un entorno de NPU

Algunos backends de NPU requieren dependencias o bibliotecas del entorno de ejecución. Cuando se usa la API de 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 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 del entorno 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 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. Si no se copian 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 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 NPU

Para canalizaciones complejas, puedes encadenar varias inferencias de 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 la compilación integrado en el dispositivo de NPU

LiteRT admite la compilación integrada en el dispositivo de NPU (conocida como JIT) de modelos .tflite. La compilación JIT puede ser especialmente útil en situaciones en las que no es posible 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 NPU on demand. Para minimizar el impacto en el rendimiento, se pueden almacenar en caché los artefactos de compilación de 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 NPU del proveedor.
  • Cambió la huella digital de la compilación de Android.
  • Cambió el modelo proporcionado por el usuario.
  • Cambiaron las opciones de compilación.

Para habilitar el almacenamiento en caché de la compilación de NPU, especifica la etiqueta de entorno CompilerCacheDir en las opciones de entorno. El valor debe establecerse en una ruta de acceso existente y grabable 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 latencia y ahorro de memoria:

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

En la siguiente tabla, se compara el tiempo de inicialización del entorno de ejecución y el consumo de memoria cuando se requiere la compilación de NPU en comparación con 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 NPU Inicialización del modelo con compilación almacenada en caché Inicializar el espacio en memoria con compilación de NPU Memoria inicial con 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 1592.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 NPU Inicialización del modelo con compilación almacenada en caché Inicializar el espacio en memoria con compilación de NPU Memoria inicial con 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