Aceleração de NPU com LiteRT

O LiteRT oferece uma interface unificada para usar unidades de processamento neural (NPUs) sem precisar navegar por compiladores, tempos de execução ou dependências de biblioteca específicos do fornecedor. Usar o LiteRT para aceleração de NPU aumenta o desempenho da inferência em tempo real e de modelos grandes e minimiza as cópias de memória com o uso de buffers de hardware de cópia zero.

Começar

Fornecedores de NPU

O LiteRT oferece suporte à aceleração de NPU com os seguintes fornecedores:

Qualcomm AI Engine Direct

MediaTek NeuroPilot

Google Tensor

O SDK do Google Tensor está em acesso experimental. Inscreva-se neste link.

Compilação AOT e no dispositivo

A NPU do LiteRT oferece suporte à compilação AOT e no dispositivo para atender aos seus requisitos específicos de implantação:

  • Compilação off-line (AOT): é mais adequada para modelos grandes e complexos em que o SoC de destino é conhecido. A compilação antecipada reduz significativamente os custos de inicialização e diminui o uso de memória quando o usuário inicia o app.
  • Compilação on-line (no dispositivo): também conhecida como compilação JIT. Isso é ideal para distribuição de modelos pequenos independente da plataforma. O modelo é compilado no dispositivo do usuário durante a inicialização, sem exigir uma etapa de preparação extra, mas incorrendo em um custo mais alto na primeira execução.

O guia a seguir mostra como fazer a implantação para compilação AOT e no dispositivo em três etapas.

Etapa 1: compilação AOT para os SoCs de NPU de destino

Use o compilador AOT (antecipado) do LiteRT para compilar seu modelo .tflite para os SoCs compatíveis. Você também pode segmentar vários fornecedores e versões de SoC simultaneamente em um único processo de compilação. Confira mais detalhes neste notebook de compilação AOT do LiteRT. Embora seja opcional, a compilação AOT é altamente recomendada para modelos maiores, porque reduz o tempo de inicialização no dispositivo. Essa etapa não é necessária para a compilação no dispositivo.

Etapa 2: implante com o Google Play se estiver no Android

No Android, use o Play para IA no dispositivo (PODAI) para implantar o modelo e as bibliotecas de tempo de execução da NPU com seu app.

Consulte as seções a seguir sobre como fazer a implantação com o Pacote de IA do Google Play e o Play Feature Delivery.

Implantar modelos AOT com o Play AI Pack

As etapas a seguir orientam você na implantação dos modelos compilados com AOT usando os pacotes de IA do Play.

Adicionar o pacote de IA ao projeto

Importe pacotes de IA para o projeto do Gradle copiando-os para o diretório raiz do projeto. Exemplo:

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

Adicione cada pacote de IA à configuração de build do 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

Adicionar pacotes de IA à configuração do Gradle

Copie device_targeting_configuration.xml dos pacotes de IA gerados para o diretório do módulo principal do app. Em seguida, atualize settings.gradle.kts:

// my_app/setting.gradle.kts

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

Atualize 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")
}

Configurar o pacote de IA para entrega sob demanda

Com a entrega sob demanda, você pode solicitar o modelo no tempo de execução, o que é útil se ele for necessário apenas para determinados fluxos de usuários. O modelo será baixado para o espaço de armazenamento interno do app. Com o recurso Android AI Pack configurado no arquivo build.gradle.kts, verifique os recursos do dispositivo. Consulte também as instruções de entrega no momento da instalação e entrega rápida da 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,
)

Implantar bibliotecas de tempo de execução de NPU com a Play Feature Delivery

O Play Feature Delivery oferece várias opções de entrega para otimizar o tamanho do download inicial, incluindo entrega no momento da instalação, sob demanda, condicional e instantânea. Aqui, mostramos o guia básico de entrega no momento da instalação.

Adicionar bibliotecas de ambiente de execução da NPU ao projeto

Faça o download de litert_npu_runtime_libraries.zip para compilação AOT ou litert_npu_runtime_libraries_jit.zip para compilação no dispositivo e descompacte no diretório raiz do projeto:

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

Execute o script para fazer o download das bibliotecas de suporte à NPU. Por exemplo, execute o seguinte para NPUs da Qualcomm:

$ ./litert_npu_runtime_libraries/fetch_qualcomm_library.sh

Adicionar bibliotecas de tempo de execução da NPU à configuração do Gradle

Copie device_targeting_configuration.xml dos pacotes de IA gerados para o diretório do módulo principal do app. Em seguida, atualize 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")

Atualize 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"))
  ...
}

Etapa 3: inferência na NPU usando o ambiente de execução LiteRT

O LiteRT abstrai a complexidade do desenvolvimento em relação a versões específicas do SoC, permitindo que você execute seu modelo na NPU com apenas algumas linhas de código. Ele também fornece um mecanismo de fallback robusto e integrado: é possível especificar CPU, GPU ou ambas como opções, e o LiteRT as usará automaticamente se a NPU estiver indisponível. A compilação AOT também oferece suporte a fallback. Ele oferece delegação parcial na NPU, em que subgrafos não compatíveis são executados sem problemas na CPU ou GPU, conforme especificado.

Executar em Kotlin

Confira um exemplo de implementação nos seguintes apps de demonstração:

Adicionar dependências do Android

Adicione o pacote Maven mais recente do LiteRT às dependências do build.gradle:

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

Integração do ambiente de execução

// 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()

Executar em C++ multiplataforma

Consulte o exemplo de implementação no app C++ de segmentação assíncrona.

Dependências de build do Bazel

Os usuários de C++ precisam criar as dependências do aplicativo com aceleração de NPU do LiteRT. A regra cc_binary que empacota a lógica principal do aplicativo (por exemplo, main.cc) requer os seguintes componentes de tempo de execução:

  • Biblioteca compartilhada da API LiteRT C: o atributo data precisa incluir a biblioteca compartilhada da API LiteRT C (//litert/c:litert_runtime_c_api_shared_lib) e o objeto compartilhado de envio específico do fornecedor para a NPU (//litert/vendors/qualcomm/dispatch:dispatch_api_so).
  • Bibliotecas de back-end específicas da NPU: por exemplo, as bibliotecas Qualcomm AI RT (QAIRT) para o host Android (como libQnnHtp.so, libQnnHtpPrepare.so) e a biblioteca Hexagon DSP correspondente (libQnnHtpV79Skel.so). Isso garante que o tempo de execução do LiteRT possa descarregar computações para a NPU.
  • Dependências de atributos: o atributo deps vincula dependências essenciais de tempo de compilação, como o buffer de tensor do LiteRT (//litert/cc:litert_tensor_buffer) e a API para a camada de envio da NPU (//litert/vendors/qualcomm/dispatch:dispatch_api). Isso permite que o código do aplicativo interaja com a NPU usando o LiteRT.
  • Arquivos de modelo e outros recursos: incluídos pelo atributo data.

Essa configuração permite que o binário compilado carregue e use dinamicamente a NPU para inferência acelerada de machine learning.

Configurar um ambiente de NPU

Alguns back-ends de NPU exigem dependências ou bibliotecas de tempo de execução. Ao usar a API de modelo compilado, o LiteRT organiza esses requisitos em um objeto Environment. Use o código a seguir para encontrar as bibliotecas ou drivers de NPU adequados:

// 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)));

Integração do ambiente de execução

O snippet de código a seguir mostra uma implementação básica de todo o processo em 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));

Cópia zero com aceleração de NPU

Com o uso da cópia zero, uma NPU pode acessar dados diretamente na própria memória sem que a CPU precise copiar esses dados explicitamente. Ao não copiar dados para e da memória da CPU, a cópia zero pode reduzir significativamente a latência de ponta a ponta.

O código a seguir é um exemplo de implementação de NPU de cópia zero com AHardwareBuffer, transmitindo dados diretamente para a NPU. Essa implementação evita viagens de ida e volta caras para a memória da CPU, reduzindo significativamente a sobrecarga de inferência.

// 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();

Encadeie várias inferências de NPU

Para pipelines complexos, é possível encadear várias inferências de NPU. Como cada etapa usa um buffer compatível com acelerador, seu pipeline fica principalmente na memória gerenciada pela 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);

Armazenamento em cache de compilação no dispositivo da NPU

O LiteRT oferece suporte à compilação de modelos .tflite no dispositivo (conhecida como JIT) da NPU. A compilação JIT pode ser especialmente útil em situações em que não é possível compilar o modelo com antecedência.

No entanto, a compilação JIT pode ter alguma latência e sobrecarga de memória para traduzir o modelo fornecido pelo usuário em instruções de bytecode da NPU sob demanda. Para minimizar o impacto no desempenho, os artefatos de compilação da NPU podem ser armazenados em cache.

Quando o cache está ativado, o LiteRT só aciona a recompilação do modelo quando necessário, por exemplo:

  • A versão do plug-in do compilador de NPU do fornecedor mudou.
  • A impressão digital da versão do Android mudou.
  • O modelo fornecido pelo usuário mudou;
  • As opções de compilação mudaram.

Para ativar o cache de compilação da NPU, especifique a tag de ambiente CompilerCacheDir nas opções de ambiente. O valor precisa ser definido como um caminho gravável existente do aplicativo.

   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));

Exemplo de economia de latência e memória:

O tempo e a memória necessários para a compilação da NPU podem variar com base em vários fatores, como o chip da NPU, a complexidade do modelo de entrada etc.

A tabela a seguir compara o tempo de inicialização do ambiente de execução e o consumo de memória quando a compilação da NPU é necessária e quando ela pode ser ignorada devido ao armazenamento em cache. Em um dispositivo de amostra, obtemos o seguinte:

Modelo do TFLite Inicialização do modelo com compilação de NPU model init com compilação em cache consumo de memória de inicialização com compilação de NPU inicializar a memória com a compilação em cache
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

Em outro dispositivo, coletamos o seguinte:

Modelo do TFLite Inicialização do modelo com compilação de NPU model init com compilação em cache consumo de memória de inicialização com compilação de NPU inicializar a memória com a compilação em cache
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