LiteRT를 사용한 NPU 가속

LiteRT는 공급업체별 컴파일러, 런타임 또는 라이브러리 종속 항목을 탐색하지 않고도 신경망 처리 장치 (NPU)를 사용할 수 있는 통합 인터페이스를 제공합니다. NPU 가속에 LiteRT를 사용하면 실시간 및 대규모 모델 추론의 성능이 향상되고 제로 복사 하드웨어 버퍼 사용을 통해 메모리 복사가 최소화됩니다.

시작하기

기존 ML 모델

기존 ML 모델은 다음 데모 애플리케이션을 참고하세요.

생성형 AI 모델

생성형 AI 모델은 다음 데모 및 가이드를 참고하세요.

NPU 공급업체

LiteRT는 다음 공급업체의 NPU 가속을 지원합니다.

Google Tensor

  • CompiledModel API를 통해 AOT 실행을 지원합니다.
  • 설정 세부정보는 Google Tensor를 참고하세요.

Qualcomm AI Engine Direct

MediaTek NeuroPilot

Intel OpenVino

  • CompiledModel API를 통해 AOT 및 온디바이스 컴파일 실행을 지원합니다.
  • 설정 세부정보는 Intel OpenVino를 참고하세요.

AOT 및 온디바이스 컴파일

LiteRT NPU는 특정 배포 요구사항을 충족하기 위해 AOT 및 온디바이스 컴파일을 모두 지원합니다.

  • 오프라인 (AOT) 컴파일: 타겟 SoC를 알고 있는 대규모의 복잡한 모델에 가장 적합합니다. 사전 컴파일을 사용하면 초기화 비용이 크게 절감되고 사용자가 앱을 실행할 때 메모리 사용량이 줄어듭니다.
  • 온라인 (온디바이스) 컴파일: JIT 컴파일이라고도 합니다. 소규모 모델의 플랫폼에 구애받지 않는 모델 배포에 적합합니다. 모델은 초기화 중에 사용자의 기기에서 컴파일되므로 추가 준비 단계가 필요하지 않지만 첫 실행 비용이 더 많이 발생합니다.

다음은 AOT 또는 온디바이스 컴파일 옵션을 모두 사용하여 모델을 배포하는 방법입니다.

1단계: 타겟 NPU SoC의 AOT 컴파일

LiteRT AOT (Ahead Of Time) 컴파일러를 사용하여 .tflite 모델을 지원되는 SoC로 컴파일할 수 있습니다. 단일 컴파일 프로세스 내에서 여러 SoC 공급업체와 버전을 동시에 타겟팅할 수도 있습니다. 자세한 내용은 이 LiteRT AOT 컴파일 노트북을 참고하세요. 선택사항이지만 대규모 모델의 경우 온디바이스 초기화 시간을 줄이기 위해 AOT 컴파일을 사용하는 것이 좋습니다. 온디바이스 컴파일에는 이 단계가 필요하지 않습니다.

2단계: Android인 경우 Google Play로 배포

Android에서는 Google Play for On-device AI (PODAI)를 사용하여 모델과 NPU 런타임 라이브러리를 앱과 함께 배포합니다.

  • 온디바이스 컴파일 모델의 경우: 원래 .tflite 모델 파일을 앱의 assets/ 디렉터리에 직접 추가합니다.
  • AOT 컴파일 모델의 경우: LiteRT를 사용하여 컴파일된 모델을 단일 Google Play AI 팩으로 내보냅니다. 그런 다음 AI 팩을 Google Play에 업로드하여 컴파일된 올바른 모델을 사용자 기기에 자동으로 제공합니다.
  • **NPU 런타임 라이브러리**의 경우 Play Feature Delivery를 사용하여 올바른 런타임 라이브러리를 사용자 기기에 배포합니다.

Play AI 팩 및 Play Feature Delivery로 배포하는 방법에 관한 다음 섹션을 참고하세요.

Play AI 팩으로 AOT 모델 배포

다음 단계에서는 Play AI 팩을 사용하여 AOT 컴파일된 모델을 배포하는 방법을 안내합니다.

프로젝트에 AI 팩 추가

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

Gradle 구성에 AI 팩 추가

생성된 AI 팩에서 device_targeting_configuration.xml을 기본 앱 모듈의 디렉터리에 복사합니다. 그런 다음 settings.gradle.kts를 업데이트합니다.

// my_app/setting.gradle.kts

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

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

주문형 전송을 위한 AI 팩 구성

주문형 전송 을 사용하면 런타임에 모델을 요청할 수 있습니다. 이는 특정 사용자 흐름에만 모델이 필요한 경우에 유용합니다. 모델이 앱의 내부 저장소 공간에 다운로드됩니다. build.gradle.kts 파일에 구성된 Android AI 팩 기능을 사용하여 기기 기능을 확인합니다. 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,
)

Play Feature Delivery로 NPU 런타임 라이브러리 배포

Play Feature Delivery 는 초기 다운로드 크기를 최적화하기 위한 여러 전송 옵션을 지원합니다. 설치 시 전송, 주문형 전송, 조건부 전송, 인스턴트 전송 등이 있습니다. 여기서는 기본적인 설치 시 전송 가이드를 보여줍니다.

프로젝트에 NPU 런타임 라이브러리 추가

AOT 컴파일의 최신 출시 버전에서 litert_npu_runtime_libraries.zip을 다운로드하거나 온디바이스 컴파일의 최신 출시 버전에서 litert_npu_runtime_libraries_jit.zip을 다운로드하고 프로젝트의 루트 디렉터리에서 압축을 풉니다.

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

스크립트를 실행하여 NPU 지원 라이브러리를 다운로드합니다. 예를 들어 Qualcomm NPU의 경우 다음을 실행합니다.

$ ./litert_npu_runtime_libraries/fetch_qualcomm_library.sh

Gradle 구성에 NPU 런타임 라이브러리 추가

생성된 AI 팩에서 device_targeting_configuration.xml을 기본 앱 모듈의 디렉터리에 복사합니다. 그런 다음 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")

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

3단계: LiteRT 런타임을 사용하여 NPU에서 추론

LiteRT는 특정 SoC 버전에 대해 개발하는 복잡성을 추상화하여 몇 줄의 코드로 NPU에서 모델을 실행할 수 있도록 합니다. 또한 강력한 기본 대체 메커니즘을 제공합니다. CPU, GPU 또는 둘 다를 옵션으로 지정할 수 있으며 NPU를 사용할 수 없는 경우 LiteRT에서 자동으로 사용합니다. 편리하게도 AOT 컴파일은 대체도 지원합니다. 지원되지 않는 하위 그래프가 지정된 대로 CPU 또는 GPU에서 원활하게 실행되는 NPU에 부분 위임을 제공합니다.

Kotlin에서 실행

구현 예는 다음 데모 앱을 참고하세요.

Android 종속 항목 추가

최신 LiteRT Maven 패키지를 build.gradle 종속 항목에 추가할 수 있습니다.

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

런타임 통합

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

C++ 크로스 플랫폼에서 실행

구현 예는 비동기 분할 C++ 앱을 참고하세요.

Bazel 빌드 종속 항목

C++ 사용자는 LiteRT NPU 가속을 사용하여 애플리케이션의 종속 항목을 빌드해야 합니다. 기본 애플리케이션 로직(예: main.cc)을 패키징하는 cc_binary 규칙에는 다음 런타임 구성요소가 필요합니다.

  • LiteRT C API 공유 라이브러리: data 속성에는 LiteRT C API 공유 라이브러리 (//litert/c:litert_runtime_c_api_shared_lib) 와 NPU의 공급업체별 디스패치 공유 객체 (//litert/vendors/qualcomm/dispatch:dispatch_api_so)가 포함되어야 합니다.
  • NPU별 백엔드 라이브러리: 예를 들어 Android 호스트용 Qualcomm AI RT (QAIRT) 라이브러리 (예: libQnnHtp.so, libQnnHtpPrepare.so) 및 상응하는 Hexagon DSP 라이브러리 (libQnnHtpV79Skel.so). 이렇게 하면 LiteRT 런타임이 계산을 NPU로 오프로드할 수 있습니다.
  • 속성 종속 항목: deps 속성은 LiteRT의 텐서 버퍼(//litert/cc:litert_tensor_buffer) 및 NPU 디스패치 레이어의 API(//litert/vendors/qualcomm/dispatch:dispatch_api)와 같은 필수 컴파일 시간 종속 항목에 연결됩니다. 이렇게 하면 애플리케이션 코드가 LiteRT를 통해 NPU와 상호작용할 수 있습니다.
  • 모델 파일 및 기타 애셋: 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 가속을 통한 제로 복사

제로 복사를 사용하면 CPU가 데이터를 명시적으로 복사할 필요 없이 NPU가 자체 메모리의 데이터에 직접 액세스할 수 있습니다. 제로 복사는 데이터를 CPU 메모리로 복사하지 않으므로 엔드 투 엔드 지연 시간을 크게 줄일 수 있습니다.

다음 코드는 데이터를 NPU에 직접 전달하는 AHardwareBuffer를 사용하는 제로 복사 NPU의 구현 예입니다. 이 구현은 CPU 메모리로의 비용이 많이 드는 왕복을 방지하여 추론 오버헤드를 크게 줄입니다.

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

NPU 온디바이스 컴파일 캐싱

LiteRT는 .tflite 모델의 NPU 온디바이스 (JIT라고 함) 컴파일을 지원합니다. 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 컴파일을 사용한 모델 초기화 캐시된 컴파일을 사용한 모델 초기화 NPU 컴파일을 사용한 초기화 메모리 사용량 캐시된 컴파일을 사용한 초기화 메모리
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

다른 기기에서 다음을 가져옵니다.

TFLite 모델 NPU 컴파일을 사용한 모델 초기화 캐시된 컴파일을 사용한 모델 초기화 NPU 컴파일을 사용한 초기화 메모리 사용량 캐시된 컴파일을 사용한 초기화 메모리
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