Ускорение NPU с помощью LiteRT

LiteRT предоставляет унифицированный интерфейс для использования нейронных процессоров (NPU), избавляя вас от необходимости разбираться в компиляторах, средах выполнения и зависимостях библиотек разных производителей. Использование LiteRT для ускорения NPU повышает производительность вывода в реальном времени и для больших моделей, а также минимизирует копирование памяти благодаря нулевому использованию аппаратного буфера.

Начать

Для начала ознакомьтесь с обзорным руководством NPU:

Примеры реализаций LiteRT с поддержкой NPU см. в следующих демонстрационных приложениях:

Поставщики НПУ

LiteRT поддерживает ускорение NPU со следующими поставщиками:

Qualcomm AI Engine Direct

  • Пути выполнения компиляции AOT и On-Device поддерживаются через API скомпилированной модели.
  • Подробную информацию о настройке см. в разделе Qualcomm AI Engine Direct .

MediaTek NeuroPilot

  • Пути выполнения AOT и JIT поддерживаются через API скомпилированной модели.
  • Подробную информацию о настройке см. в разделе MediaTek NeuroPilot .

Конвертация и компиляция моделей для NPU

Для использования ускорения NPU с LiteRT модели необходимо преобразовать в формат файла LiteRT и скомпилировать для использования NPU на устройстве. Вы можете использовать компилятор LiteRT AOT (afore-time) для компиляции моделей в пакет AI Pack, который объединяет скомпилированные модели с конфигурацией для конкретного устройства. Это позволяет проверить корректность отображения моделей на устройствах в зависимости от того, оснащены ли они конкретными SoC или оптимизированы для них.

После преобразования и компиляции моделей вы можете использовать Play for On-device AI (PODAI) для загрузки моделей в Google Play и доставки моделей на устройства через фреймворк On-Demand AI.

Используйте блокнот компиляции LiteRT AOT для полного руководства по конвертации и компиляции моделей для NPU.

[Только AOT] Развертывание с помощью Play AI Pack

После преобразования модели и компиляции AI Pack выполните следующие шаги для развертывания AI Pack с помощью Google Play.

Импорт пакетов AI в проект Gradle

Скопируйте AI-пакет(ы) в корневой каталог проекта Gradle. Например:

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

Добавьте каждый пакет AI в конфигурацию сборки 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

Добавить библиотеки времени выполнения NPU в проект

Загрузите litert_npu_runtime_libraries.zip для AOT или litert_npu_runtime_libraries_jit.zip для JIT и распакуйте его в корневой каталог проекта:

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

Запустите скрипт для загрузки библиотек поддержки NPU. Например, для NPU Qualcomm выполните следующее:

$ ./litert_npu_runtime_libraries/fetch_qualcomm_library.sh

Добавьте пакеты AI и библиотеки времени выполнения NPU в конфигурацию Gradle.

Скопируйте файл device_targeting_configuration.xml из сгенерированных AI-пакетов в каталог основного модуля приложения. Затем обновите 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")

Обновление 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"))
  ...
}

[Только AOT] Использовать развертывание по требованию

С помощью функции Android AI Pack, настроенной в файле build.gradle.kts , проверьте возможности устройства и используйте NPU на совместимых устройствах, используя GPU и 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,
)

Создать CompiledModel для режима JIT

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

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

Вывод на NPU с использованием LiteRT в Kotlin

Чтобы начать использовать ускоритель NPU, передайте параметр NPU при создании скомпилированной модели ( CompiledModel ).

Следующий фрагмент кода показывает базовую реализацию всего процесса на 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()

Вывод на NPU с использованием LiteRT в C++

Зависимости сборки

Пользователи C++ должны собрать зависимости приложения с помощью ускорения NPU LiteRT. Правило cc_binary , которое упаковывает основную логику приложения (например, main.cc ), требует следующих компонентов среды выполнения:

  • Общая библиотека LiteRT C API : атрибут data должен включать общую библиотеку LiteRT C API ( //litert/c:litert_runtime_c_api_shared_lib ) и специфичный для поставщика общий объект диспетчеризации для NPU ( //litert/vendors/qualcomm/dispatch:dispatch_api_so ).
  • Специфичные для NPU бэкенд-библиотеки : например, библиотеки Qualcomm AI RT (QAIRT) для хоста Android (например, libQnnHtp.so , libQnnHtpPrepare.so ) и соответствующая библиотека Hexagon DSP ( libQnnHtpV79Skel.so ). Это гарантирует, что среда выполнения LiteRT сможет перенести вычисления на NPU.
  • Зависимости атрибутов : атрибуты deps связаны с важнейшими зависимостями времени компиляции, такими как тензорный буфер LiteRT ( //litert/cc:litert_tensor_buffer ) и API для уровня диспетчеризации NPU ( //litert/vendors/qualcomm/dispatch:dispatch_api ). Это позволяет коду вашего приложения взаимодействовать с NPU через LiteRT.
  • Файлы моделей и другие активы : включены через атрибут data .

Такая настройка позволяет скомпилированному двоичному файлу динамически загружать и использовать NPU для ускоренного вывода машинного обучения.

Настройка среды NPU

Некоторым бэкендам NPU требуются зависимости или библиотеки времени выполнения. При использовании API скомпилированной модели LiteRT организует эти требования через объект Environment . Используйте следующий код для поиска соответствующих библиотек или драйверов NPU:

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

Интеграция во время выполнения

Следующий фрагмент кода показывает базовую реализацию всего процесса на языке 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));

Нулевое копирование с ускорением NPU

Использование нулевого копирования позволяет нейронному процессору получать доступ к данным непосредственно в его собственной памяти, без необходимости явного копирования этих данных центральным процессором. Отказ от копирования данных в память процессора и обратно позволяет значительно снизить сквозную задержку.

Следующий код представляет собой пример реализации NPU с нулевым копированием и буфером AHardwareBuffer , передающим данные непосредственно в NPU. Такая реализация позволяет избежать дорогостоящих обращений к памяти ЦП, значительно снижая накладные расходы на вывод.

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

Объединение нескольких NPU-выводов в цепочку

Для сложных конвейеров можно объединить несколько выводов NPU. Поскольку каждый шаг использует удобный для ускорителя буфер, ваш конвейер в основном находится в памяти, управляемой 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);

Кэширование компиляции JIT-компиляции NPU

LiteRT поддерживает JIT-компиляцию моделей .tflite с помощью NPU. JIT-компиляция может быть особенно полезна в ситуациях, когда предварительная компиляция модели невозможна.

Однако JIT-компиляция может сопровождаться некоторой задержкой и накладными расходами памяти при преобразовании предоставленной пользователем модели в инструкции байт-кода NPU по запросу. Чтобы минимизировать влияние на производительность, артефакты компиляции NPU можно кэшировать.

Если кэширование включено, LiteRT будет запускать перекомпиляцию модели только при необходимости, например:

  • Изменена версия плагина компилятора NPU поставщика;
  • Изменился отпечаток сборки Android;
  • Изменилась предоставленная пользователем модель;
  • Изменились параметры компиляции.

Чтобы включить кэширование компиляции NPU, укажите тег среды CompilerCacheDir в параметрах среды. Значение должно соответствовать существующему пути записи приложения.

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

Пример задержки и экономии памяти:

Время и память, необходимые для компиляции NPU, могут различаться в зависимости от ряда факторов, таких как базовый чип NPU, сложность входной модели и т. д.

В следующей таблице сравниваются время инициализации и потребление памяти в режиме выполнения, когда требуется компиляция с использованием нейронного процессора (NPU), и когда компиляцию можно пропустить благодаря кэшированию. На одном тестовом устройстве мы получили следующее:

Модель TFLite инициализация модели с компиляцией NPU инициализация модели с кэшированной компиляцией init-отпечаток памяти с компиляцией NPU init-память с кэшированной компиляцией
torchvision_resnet152.tflite 7465,22 мс 198,34 мс 1525,24 МБ 355,07 МБ
torchvision_lraspp_mobilenet_v3_large.tflite 1592,54 мс 166,47 мс 254,90 МБ 33,78 МБ

На другом устройстве получаем следующее:

Модель TFLite инициализация модели с компиляцией NPU инициализация модели с кэшированной компиляцией init-отпечаток памяти с компиляцией NPU init-память с кэшированной компиляцией
torchvision_resnet152.tflite 2766,44 мс 379,86 мс 653,54 МБ 501,21 МБ
torchvision_lraspp_mobilenet_v3_large.tflite 784,14 мс 231,76 мс 113,14 МБ 67,49 МБ