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 specyficznych dla danego dostawcy. Korzystanie z LiteRT do przyspieszania obliczeń na 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

Dostawcy NPU

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

Qualcomm AI Engine Direct

MediaTek NeuroPilot

Google Tensor

Pakiet Google Tensor SDK jest dostępny w ramach dostępu eksperymentalnego. Zarejestruj się tutaj.

Kompilacja AOT i na urządzeniu

Procesor NPU LiteRT obsługuje kompilację AOT i kompilację na urządzeniu, aby spełniać Twoje konkretne wymagania dotyczące wdrożenia:

  • Kompilacja offline (AOT): najlepiej sprawdza się w przypadku dużych, złożonych modeli, w których znany jest docelowy układ SoC. Kompilacja z wyprzedzeniem znacznie zmniejsza koszty inicjowania i wykorzystanie pamięci, gdy użytkownik uruchamia aplikację.
  • Kompilacja online (na urządzeniu): znana też jako kompilacja JIT. Jest to idealne rozwiązanie do dystrybucji małych modeli niezależnych od platformy. Model jest kompilowany na urządzeniu użytkownika podczas inicjowania, co nie wymaga dodatkowego przygotowania, ale wiąże się z wyższym kosztem pierwszego uruchomienia.

W tym przewodniku pokazujemy, jak wdrożyć kompilację AOT i kompilację na urządzeniu w 3 krokach.

Krok 1. Kompilacja AOT dla docelowych układów SoC NPU

Możesz użyć kompilatora LiteRT AOT (ahead of time) do skompilowania modelu .tflite na obsługiwane układy SoC. W ramach jednego procesu kompilacji możesz też kierować reklamy jednocześnie na wielu dostawców układów SoC i wersji. Więcej informacji znajdziesz w tym notatniku dotyczącym kompilacji AOT LiteRT. Kompilacja AOT jest opcjonalna, ale w przypadku większych modeli jest wysoce zalecana, ponieważ skraca czas inicjowania na urządzeniu. Ten krok nie jest wymagany w przypadku kompilacji na urządzeniu.

Krok 2. Wdróż aplikację w Google Play (jeśli korzystasz z Androida)

Na Androidzie używaj Google Play do wdrażania na urządzeniu AI (PODAI), aby wdrażać model i biblioteki środowiska wykonawczego NPU w aplikacji.

  • W przypadku modeli kompilowanych na urządzeniu: dodaj oryginalny plik modelu .tflite bezpośrednio do katalogu assets/ aplikacji.
  • W przypadku modeli kompilacji AOT: użyj LiteRT, aby wyeksportować skompilowane modele do jednego pakietu AI w Google Play. Następnie przesyłasz pakiet AI do Google Play, aby automatycznie dostarczać prawidłowe skompilowane modele na urządzenia użytkowników.
  • W przypadku bibliotek środowiska wykonawczego NPU użyj Play Feature Delivery, aby rozpowszechniać odpowiednie biblioteki środowiska wykonawczego na urządzeniach użytkowników.

W sekcjach poniżej znajdziesz informacje o wdrażaniu za pomocą Play AI PackPlay Feature Delivery.

Wdrażanie modeli AOT za pomocą pakietu AI Play

Z podanych niżej instrukcji dowiesz się, jak wdrożyć skompilowane z wyprzedzeniem modele za pomocą pakietów AI Play.

Dodawanie pakietu AI Pack do projektu

Zaimportuj pakiety AI do projektu Gradle, kopiując je 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 pakietów AI 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

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

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

Konfigurowanie pakietu AI do dostarczania na żądanie

Przesyłanie na żądanie umożliwia wysyłanie żądań dotyczących modelu w czasie działania aplikacji. Jest to przydatne, jeśli model jest wymagany tylko w przypadku określonych ścieżek użytkownika. Model zostanie pobrany do pamięci wewnętrznej aplikacji. Po skonfigurowaniu funkcji Android AI Pack w pliku build.gradle.kts sprawdź możliwości urządzenia. Zobacz też instrukcje dotyczące przesyłania w momencie instalacjiprzesyłania w trybie fast-follow z 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,
)

Wdrażanie bibliotek środowiska wykonawczego NPU za pomocą Play Feature Delivery

Play Feature Delivery obsługuje wiele opcji dostawy, które optymalizują rozmiar pobierania początkowego, w tym dostawę podczas instalacji, dostawę na żądanie, dostawę warunkową i dostawę natychmiastową. Poniżej przedstawiamy podstawowy przewodnik dostawy w momencie instalacji.

Dodawanie bibliotek środowiska wykonawczego NPU do projektu

Pobierz plik litert_npu_runtime_libraries.zip w przypadku kompilacji AOT lub litert_npu_runtime_libraries_jit.zip w przypadku kompilacji na urządzeniu 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 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

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

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

Krok 3. Wnioskowanie na NPU za pomocą środowiska wykonawczego LiteRT

LiteRT upraszcza proces tworzenia aplikacji na konkretne wersje SoC, dzięki czemu możesz uruchomić model na NPU za pomocą zaledwie kilku wierszy kodu. Zapewnia też solidny, wbudowany mechanizm rezerwowy: możesz określić procesor, procesor graficzny lub oba te elementy jako opcje, a LiteRT automatycznie ich użyje, jeśli NPU będzie niedostępny. Wygodnie jest też to, że kompilacja AOT obsługuje też powracanie do poprzedniej wersji. Zapewnia częściowe przekazywanie uprawnień do NPU, gdzie nieobsługiwane podgrafy działają bezproblemowo na procesorze lub GPU zgodnie z ustawieniami.

Uruchamianie w języku Kotlin

Przykładową implementację znajdziesz w tych aplikacjach demonstracyjnych:

Dodawanie zależności Androida

Najnowszy pakiet LiteRT Maven możesz dodać do zależności build.gradle:

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

Integracja w środowisku wykonawczym

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

Uruchamianie w C++ na różnych platformach

Przykład implementacji znajdziesz w asynchronicznej aplikacji do segmentacji w C++.

Zależności kompilacji Bazel

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 do NPU.
  • Zależności atrybutów: atrybut deps łączy się z niezbędnymi zależnościami w czasie kompilacji, takimi jak bufor tensorów 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 na urządzeniu w przypadku NPU

LiteRT obsługuje kompilację modeli .tflite na urządzeniu z procesorem NPU (znaną jako JIT). Kompilacja JIT może być szczególnie przydatna w sytuacjach, w których nie można skompilować modelu z wyprzedzeniem.

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óźnienia 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