Przyspieszenie NPU za pomocą LiteRT

LiteRT udostępnia ujednolicony interfejs do korzystania z jednostek przetwarzania neuronowego (NPU) bez konieczności korzystania z kompilatorów, środowisk wykonawczych ani zależności bibliotecznych konkretnych dostawców. Używanie LiteRT do przyspieszania NPU zwiększa wydajność wnioskowania w czasie rzeczywistym i w przypadku dużych modeli oraz minimalizuje kopiowanie pamięci dzięki wykorzystaniu bufora sprzętowego bez kopiowania.

Rozpocznij

Na początek zapoznaj się z przewodnikiem po NPU:

Przykłady implementacji LiteRT z obsługą NPU znajdziesz w tych aplikacjach demonstracyjnych:

Dostawcy NPU

LiteRT obsługuje akcelerację NPU w przypadku tych dostawców:

Qualcomm AI Engine Direct

  • Ścieżki wykonywania kompilacji AOT i na urządzeniu są obsługiwane przez interfejs Compiled Model API.
  • Szczegółowe informacje o konfiguracji znajdziesz w artykule Qualcomm AI Engine Direct.

MediaTek NeuroPilot

  • Ścieżki wykonania AOT i JIT są obsługiwane przez interfejs Compiled Model API.
  • Szczegółowe informacje o konfiguracji znajdziesz w artykule MediaTek NeuroPilot.

Konwertowanie i kompilowanie modeli na potrzeby NPU

Aby używać akceleracji NPU z LiteRT, modele muszą zostać przekonwertowane na format pliku LiteRT i skompilowane do użytku na urządzeniu z NPU. Możesz użyć kompilatora LiteRT AOT (ahead of time), aby skompilować modele do pakietu AI, który zawiera skompilowane modele z konfiguracjami kierowania na urządzenia. Weryfikuje to, czy modele są prawidłowo udostępniane na urządzeniach w zależności od tego, czy są wyposażone w określone układy SoC lub zoptymalizowane pod kątem tych układów.

Po przekonwertowaniu i skompilowaniu modeli możesz użyć Play for On-device AI (PODAI), aby przesłać modele do Google Play i dostarczać je na urządzenia za pomocą platformy On-Demand AI.

Skorzystaj z notatnika LiteRT AOT Compilation, aby uzyskać kompleksowy przewodnik po konwertowaniu i kompilowaniu modeli na potrzeby NPU.

[Tylko AOT] Wdrażanie za pomocą pakietu AI w Google Play

Po przekonwertowaniu modelu i skompilowaniu pakietu AI wykonaj te czynności, aby wdrożyć pakiet AI w Google Play.

Importowanie pakietów AI do projektu Gradle

Skopiuj pakiety AI do katalogu głównego projektu Gradle. Na przykład:

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

Dodaj każdy pakiet AI do konfiguracji kompilacji 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

Dodawanie bibliotek środowiska wykonawczego NPU do projektu

Pobierz litert_npu_runtime_libraries.zip w przypadku kompilacji AOT lub litert_npu_runtime_libraries_jit.zip w przypadku kompilacji JIT i rozpakuj go w katalogu głównym projektu:

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

Uruchom skrypt, aby pobrać biblioteki obsługi NPU. Na przykład w przypadku procesorów NPU firmy Qualcomm uruchom następujące polecenie:

$ ./litert_npu_runtime_libraries/fetch_qualcomm_library.sh

Dodawanie pakietów AI i bibliotek środowiska wykonawczego NPU do konfiguracji Gradle

Skopiuj device_targeting_configuration.xml z wygenerowanych pakietów AI do katalogu głównego modułu aplikacji. Następnie zaktualizuj settings.gradle.kts:

// my_app/setting.gradle.kts

...

// [AOT only]
// AI Packs
include(":ai_packs:my_model")
include(":ai_packs:my_model_mtk")

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

Zaktualizuj 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
      }
  }

  // [AOT Only]
  // AI Packs
  assetPacks.add(":ai_packs:my_model")
  assetPacks.add(":ai_packs:my_model_mtk")

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

[Tylko AOT] Używanie wdrożenia na żądanie

Po skonfigurowaniu funkcji Android AI Pack w pliku build.gradle.kts sprawdź możliwości urządzenia i używaj NPU na obsługujących je urządzeniach, a w razie potrzeby korzystaj z GPU i CPU:

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

Tworzenie klucza CompiledModel dla trybu JIT

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

val compiledModel = CompiledModel.create(
    "model/my_model.tflite",
    CompiledModel.Options(Accelerator.NPU),
    env,
)

Wnioskowanie na NPU za pomocą LiteRT w języku Kotlin

Aby zacząć korzystać z akceleratora NPU, podczas tworzenia skompilowanego modelu (CompiledModel) przekaż parametr NPU.

Poniższy fragment kodu pokazuje podstawową implementację całego procesu w języku Kotlin:

val inputBuffers = model.createInputBuffers()
val outputBuffers = model.createOutputBuffers()

inputBuffers[0].writeFloat(FloatArray(data_size) { data_value })
model.run(inputBuffers, outputBuffers)
val outputFloatArray = outputBuffers[0].readFloat()

inputBuffers.forEach { it.close() }
outputBuffers.forEach { it.close() }
model.close()

Wnioskowanie na NPU za pomocą LiteRT w C++

Zależności kompilacji

Użytkownicy C++ muszą skompilować zależności aplikacji z akceleracją LiteRT NPU. cc_binary reguła, która pakuje podstawową logikę aplikacji (np. main.cc) wymaga tych komponentów środowiska wykonawczego:

  • Biblioteka współdzielona LiteRT C API: atrybut data musi zawierać bibliotekę współdzieloną LiteRT C API (//litert/c:litert_runtime_c_api_shared_lib) i specyficzny dla dostawcy obiekt współdzielony wysyłania dla NPU (//litert/vendors/qualcomm/dispatch:dispatch_api_so).
  • Biblioteki backendu specyficzne dla NPU: np. biblioteki Qualcomm AI RT (QAIRT) dla hosta Androida (np. libQnnHtp.so, libQnnHtpPrepare.so) i odpowiednia biblioteka Hexagon DSP (libQnnHtpV79Skel.so). Dzięki temu środowisko wykonawcze LiteRT może przenosić obliczenia na NPU.
  • Zależności atrybutów: atrybut deps łączy się z niezbędnymi zależnościami w czasie kompilacji, takimi jak bufor tensora LiteRT (//litert/cc:litert_tensor_buffer) i interfejs API warstwy wysyłania NPU (//litert/vendors/qualcomm/dispatch:dispatch_api). Umożliwia to kodowi aplikacji interakcję z NPU za pomocą LiteRT.
  • Pliki modelu i inne komponenty: uwzględnione za pomocą atrybutu data.

Ta konfiguracja umożliwia skompilowanemu plikowi binarnemu dynamiczne wczytywanie i używanie NPU do przyspieszonego wnioskowania w uczeniu maszynowym.

Konfigurowanie środowiska NPU

Niektóre interfejsy NPU wymagają zależności lub bibliotek środowiska wykonawczego. Podczas korzystania ze skompilowanego interfejsu API modelu LiteRT organizuje te wymagania za pomocą obiektu Environment. Aby znaleźć odpowiednie biblioteki lub sterowniki NPU, użyj tego kodu:

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

Integracja w środowisku wykonawczym

Poniższy fragment kodu przedstawia podstawową implementację całego procesu w 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));

Kopiowanie zerowe z akceleracją NPU

Używanie zerowego kopiowania umożliwia NPU bezpośredni dostęp do danych w pamięci bez konieczności jawnego kopiowania tych danych przez procesor. Dzięki temu, że nie trzeba kopiować danych do pamięci procesora ani z niej, zero-copy może znacznie skrócić opóźnienie od początku do końca.

Poniższy kod to przykładowa implementacja NPU bez kopiowania z użyciem AHardwareBuffer, która przekazuje dane bezpośrednio do NPU. Ta implementacja pozwala uniknąć kosztownych podróży w obie strony do pamięci procesora, co znacznie zmniejsza obciążenie związane z wnioskowaniem.

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

Łączenie wielu wnioskowań NPU

W przypadku złożonych potoków możesz połączyć wiele wnioskowań NPU. Ponieważ każdy krok korzysta z bufora przyjaznego dla akceleratora, potok pozostaje w większości w pamięci zarządzanej przez 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);

Pamięć podręczna kompilacji Just-In-Time na potrzeby NPU

LiteRT obsługuje kompilację JIT (just-in-time) modeli .tflite na potrzeby NPU. Kompilacja JIT może być szczególnie przydatna w sytuacjach, w których wcześniejsza kompilacja modelu jest niemożliwa.

Kompilacja JIT może jednak wiązać się z pewnym opóźnieniem i dodatkowym obciążeniem pamięci, ponieważ model dostarczony przez użytkownika jest tłumaczony na instrukcje kodu bajtowego NPU na żądanie. Aby zminimalizować wpływ na wydajność, artefakty kompilacji NPU można przechowywać w pamięci podręcznej.

Gdy buforowanie jest włączone, LiteRT ponownie kompiluje model tylko w razie potrzeby, np.:

  • zmieniła się wersja wtyczki kompilatora NPU dostawcy;
  • zmieniono odcisk cyfrowy kompilacji Androida;
  • model dostarczony przez użytkownika uległ zmianie;
  • Opcje kompilacji zostały zmienione.

Aby włączyć buforowanie kompilacji NPU, w opcjach środowiska określ tag środowiska CompilerCacheDir. Wartość musi być ustawiona na istniejącą ścieżkę zapisu aplikacji.

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

Przykładowe oszczędności w zakresie opóźnień i pamięci:

Czas i pamięć wymagane do kompilacji NPU mogą się różnić w zależności od kilku czynników, takich jak bazowy układ NPU czy złożoność modelu wejściowego.

W tabeli poniżej porównano czas inicjowania środowiska wykonawczego i zużycie pamięci, gdy wymagana jest kompilacja NPU, z sytuacją, gdy można ją pominąć ze względu na buforowanie. Na jednym z urządzeń testowych uzyskujemy te dane:

Model TFLite inicjowanie modelu z kompilacją NPU, inicjowanie modelu za pomocą skompilowanej wersji z pamięci podręcznej, inicjowanie wykorzystania pamięci podczas kompilacji NPU inicjowanie pamięci za pomocą skompilowanego kodu z pamięci podręcznej,
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

Na innym urządzeniu uzyskujemy te informacje:

Model TFLite inicjowanie modelu z kompilacją NPU, inicjowanie modelu za pomocą skompilowanej wersji z pamięci podręcznej, inicjowanie wykorzystania pamięci podczas kompilacji NPU inicjowanie pamięci za pomocą skompilowanego kodu z pamięci podręcznej,
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