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 dostawcy. 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

Klasyczne modele ML

W przypadku klasycznych modeli ML zapoznaj się z tymi aplikacjami demonstracyjnymi.

Modele generatywnej AI

W przypadku modeli generatywnej AI zapoznaj się z tymi demonstracjami i przewodnikiem:

Dostawcy NPU

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

Google Tensor

  • Obsługa wykonywania AOT za pomocą interfejsu CompiledModel API.
  • Szczegółowe informacje o konfiguracji znajdziesz w artykule Google Tensor.

Qualcomm AI Engine Direct

MediaTek NeuroPilot

Intel OpenVino

  • Obsługa kompilacji AOT i wykonywania kompilacji na urządzeniu za pomocą interfejsu CompiledModel API.
  • Szczegółowe informacje o konfiguracji znajdziesz w artykule Intel OpenVino.

Kompilacja AOT i na urządzeniu

Procesor NPU LiteRT obsługuje kompilację AOT i 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 kroku przygotowawczego, ale wiąże się z wyższym kosztem pierwszego uruchomienia.

Model możesz wdrożyć, korzystając z kompilacji AOT lub kompilacji na urządzeniu:

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 AI na urządzeniu (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/ w 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 Play AI

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

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 żądanie modelu w czasie działania aplikacji, co jest 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 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,
)

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ą. W tym miejscu pokazujemy podstawowy przewodnik dostawy w momencie instalacji.

Dodawanie bibliotek środowiska wykonawczego NPU do projektu

Pobierz litert_npu_runtime_libraries.zipnajnowszej wersji na potrzeby kompilacji AOT lub litert_npu_runtime_libraries_jit.zipnajnowszej wersji na potrzeby kompilacji na urządzeniu i rozpakuj go w katalogu głównym projektu:

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

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

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

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

LiteRT upraszcza proces tworzenia aplikacji pod kątem konkretnych wersji SoC, umożliwiając uruchamianie modelu na NPU za pomocą zaledwie kilku wierszy kodu. Zapewnia też niezawodny, wbudowany mechanizm rezerwowy: możesz określić CPU, GPU 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 na 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 wielu platformach

Przykładową implementację 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 zawiera 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 obiekt współdzielony wysyłania specyficzny dla dostawcy w przypadku 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 odciążyć obliczenia do NPU.
  • Zależności atrybutów: atrybut deps łączy się z niezbędnymi zależnościami czasu 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 back-endy 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));

Bez kopiowania 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, kopiowanie zerowe 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 kompilacja modelu z wyprzedzeniem nie jest możliwa.

Kompilacja JIT może jednak wiązać się z pewnym opóźnieniem i dodatkowym obciążeniem pamięci, ponieważ na żądanie tłumaczy model dostarczony przez użytkownika na instrukcje kodu bajtowego NPU. 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;
  • zmienił się odcisk cyfrowy kompilacji Androida;
  • model dostarczony przez użytkownika uległ zmianie;
  • Opcje kompilacji uległy zmianie.

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, złożoność modelu wejściowego itp.

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