LiteRT 컴파일러 플러그인

컴파일러 플러그인은 언제 만들어야 하나요?

LiteRT 컴파일러 플러그인은 컴파일러 종속 항목이 있는 특정 하드웨어 가속기를 LiteRT 프레임워크에 통합해야 하는 경우에 필요합니다.

다음과 같은 경우 컴파일러 플러그인을 만들어야 합니다.

  1. 지원되지 않는 새 하드웨어 백엔드를 타겟팅하고 있습니다.
  2. 성능 또는 전력 효율성을 위해 해당 하드웨어 가속기로 특정 모델 작업을 오프로드하려고 합니다.
  3. 워크스테이션에서 AOT 컴파일 또는 기기 내 컴파일을 지원해야 합니다.

이 플러그인은 머신러닝 모델의 일부를 가져와 백엔드 컴파일러 호출을 사용하여 타겟 하드웨어에서 실행할 수 있는 형식으로 변환하는 브리지 역할을 합니다. LiteRT는 플러그인에서 생성된 맞춤 바이트 코드를 .tflite 모델에 번들로 묶어 LiteRT 런타임을 사용하여 실행할 수 있도록 합니다.

컴파일러 플러그인은 어떻게 작동하나요?

LiteRT 프레임워크는 모델 로드 또는 오프라인 사전 처리 단계에서 컴파일러 플러그인을 사용하여 타겟 하드웨어에서 실행할 모델 하위 그래프를 식별하고 준비합니다.

이 프로세스에는 플러그인의 내보낸 함수를 사용하여 프레임워크에서 오케스트레이션하는 두 가지 주요 단계가 포함됩니다.

  1. 파티셔닝: 플러그인은 전체 모델 그래프를 검사하고 지원하며 타겟 하드웨어에서 효율적으로 가속화할 수 있는 작업의 하위 집합을 식별합니다. 지원되는 이러한 하위 그래프는 컴파일을 위해 '파티셔닝' (표시)되고 윤곽이 표시됩니다.
  2. 컴파일: LiteRT 프레임워크는 파티션을 나눈 하위 그래프를 플러그인에 다시 전달합니다. 그러면 플러그인이 내부 로직과 외부 도구 체인 (컴파일러)을 사용하여 파티션을 구현하는 하나 이상의 하드웨어별 바이트코드 모듈을 생성합니다. 이 바이트 코드는 타겟 하드웨어의 런타임 (HAL/드라이버)이 최종적으로 로드하고 실행하는 것입니다.

프레임워크는 하드웨어 드라이버를 호출하는 맞춤 작업으로 원래 하위 그래프를 대체하여 플러그인에서 생성된 컴파일된 바이트 코드를 전달합니다.

LiteRT 디스패치는 컴파일러 플러그인의 런타임 아날로그입니다. 컴파일러 출력이 주어지면 HAL을 호출하는 수단을 제공합니다. 자세한 내용은 디스패치 문서를 참고하세요.

AOT와 온디바이스 비교

LiteRT는 컴파일러 플러그인을 사용하여 도구를 통한 AOT 컴파일과 기기 내 컴파일을 지원할 수 있습니다. 기기 내 컴파일은 더 유연하고 LiteRT 런타임 API 내에서 완전히 내부화되며 단일 모델 관리만 필요합니다. AOT 흐름은 리소스 집약도가 너무 높아 기기에서 실행할 수 없는 경우 컴파일을 차단 해제할 수 있습니다. 이는 많은 최신 대형 모델의 경우에 해당할 수 있습니다.

대체

LiteRT는 이기종 그래프를 지원하도록 빌드됩니다. 플러그인에서 선택하지 않은 작업은 CPU에 남겨지거나 다른 백엔드에서 가속할 수 있습니다.

컴파일러 플러그인 구현

LiteRT 컴파일러 플러그인은 LiteRT C API에 정의된 특정 C 함수 집합을 내보내는 공유 라이브러리로 구현됩니다.

필수 인터페이스 기능

핵심 기능은 두 가지 주요 컴파일 단계인 LiteRtCompilerPluginPartitionLiteRtCompilerPluginCompile를 중심으로 이루어집니다.

함수 목적
LiteRtCompilerPluginPartition 주어진 모델 하위 그래프 (파티션 단계) 내에서 지원되는 모든 작업을 선택하고 표시합니다.
LiteRtCompilerPluginCompile$ 미리 선택된 파티션의 하드웨어 관련 바이트 코드를 생성합니다 (컴파일 단계).

C API 스니펫

// Name associated with the manufacturer this plugin relates to.
LITERT_CAPI_EXPORT const char* LiteRtGetCompilerPluginSocManufacturer();

// Create and initialize the plugin instance.
LITERT_CAPI_EXPORT LiteRtStatus
LiteRtCreateCompilerPlugin(LiteRtCompilerPlugin* compiler_plugin,
                           LiteRtEnvironmentOptions env, LiteRtOptions options);

// Choose ops for compilation.
// This is the PARTITION step.
LITERT_CAPI_EXPORT LiteRtStatus LiteRtCompilerPluginPartition(
    LiteRtCompilerPlugin compiler_plugin, const char* soc_model,
    LiteRtSubgraph subgraph, LiteRtOpList selected_ops);

// Prepare result to pass to the runtime for given model containing partitioned
// subgraphs. This is the COMPILE step.
LITERT_CAPI_EXPORT LiteRtStatus LiteRtCompilerPluginCompile(
    LiteRtCompilerPlugin compiler_plugin, const char* soc_model,
    LiteRtModel partitions, LiteRtCompiledResult* compiled_result);

1. 파티션 함수

함수 서명은 다음과 같습니다.

LITERT_CAPI_EXPORT LiteRtStatus LiteRtCompilerPluginPartition(
    LiteRtCompilerPlugin compiler_plugin, const char* soc_model,
    LiteRtSubgraph subgraph, LiteRtOpList selected_ops);

partition 함수의 역할: 선택 단계입니다. 플러그인은 입력 LiteRtSubgraph의 작업을 반복합니다. 타겟 하드웨어가 지원하고 가속화할 수 있는 모든 작업에 대해 플러그인은 selected_ops 매개변수에 제공된 LiteRtOpList$에 해당 작업을 추가합니다. LiteRt 프레임워크는 이 목록을 사용하여 최종 컴파일 단계에 전송될 파티션의 경계를 정의합니다.

기본적으로 LiteRT는 선택된 모든 작업을 가능한 가장 큰 하위 DAG로 그룹화합니다. 더 세부적인 파티셔닝을 위해 이러한 하위 그래프를 추가로 분할하는 역할을 하는 작업을 선택할 때 색인을 연결할 수 있습니다.

2. 컴파일 함수

함수 서명은 다음과 같습니다.

LITERT_CAPI_EXPORT LiteRtStatus LiteRtCompilerPluginCompile(
    LiteRtCompilerPlugin compiler_plugin, const char* soc_model,
    LiteRtModel partitions, LiteRtCompiledResult* compiled_result);

compile 함수의 기능: 생성 단계입니다. 입력 partitions은 선택된 모든 하위 그래프가 격리된 모델을 나타냅니다. 플러그인은 이러한 파티션을 처리하여 특정 도구 모음을 호출하여 타겟 하드웨어의 바이트 코드를 생성합니다. 플러그인의 출력은 컴파일을 위해 전달된 각 하위 그래프의 진입점을 제공해야 합니다. 대부분의 경우 이는 각 입력 하위 그래프의 개별 바이트 코드 모듈이거나 진입점이 여러 개인 단일 바이트 코드 모듈입니다.

compile에서 반환된 데이터 유형: LiteRtCompilerPluginCompile 함수는 out-parameter LiteRtCompiledResult를 사용하여 출력을 반환합니다.

LiteRtCompiledResult는 플러그인에서 관리하는 구조에 대한 불투명 (LiteRT 관련) 핸들입니다. 이는 컴파일의 출력을 나타내며 다음과 같은 두 가지 주요 정보를 포함합니다.

  1. 바이트 코드 모듈: 하드웨어별 실행 가능 바이트 코드 (즉, 컴파일된 명령어)를 포함하는 하나 이상의 원시 메모리 버퍼입니다.
  2. 통화 정보: 각 파티션의 메타데이터입니다. 이는 i번째 입력 하위 그래프에서 결과 바이트 코드 모듈과 해당 모듈의 진입점 식별자로의 매핑을 제공합니다.

구현 예시

다음 스니펫은 기본 플러그인이 핵심 기능을 구현하는 방법을 보여줍니다. 이 예는 litert/vendors/examples/의 완전한 기능 예에서 가져온 것입니다.

플러그인 식별 및 설정

이러한 함수는 플러그인과 하드웨어에 관한 기본 정보를 프레임워크에 제공합니다.

// Define the plugin's internal state structure
struct LiteRtCompilerPluginT {};

// Identify the manufacturer
const char* LiteRtGetCompilerPluginSocManufacturer() {
  return "AcmeCorp"; // Example manufacturer name
}

// Specify the supported hardware (in this example, it supports kLiteRtHwAcceleratorNpu)
LiteRtStatus LiteRtGetCompilerPluginSupportedHardware(
    LiteRtCompilerPlugin compiler_plugin,
    LiteRtHwAccelerators* supported_hardware) {
  // ... argument checking ...
  *supported_hardware = kLiteRtHwAcceleratorNpu;
  return kLiteRtStatusOk;
}

파티셔닝 로직 (LiteRtCompilerPluginPartition)

이 예에서는 모든 입력과 출력이 32비트 부동 소수점인 경우에만 제한된 작업 집합 (mul, sub, 특정 복합 작업)을 선택하는 플러그인을 보여줍니다. 일반적으로 작업이 선택되어야 하는지 여부를 결정하는 데 백엔드 컴파일러 도구 모음의 유효성 검사 후크 호출이 포함됩니다.

LiteRtStatus LiteRtCompilerPluginPartition(LiteRtCompilerPlugin compiler_plugin,
                                          const char* soc_model,
                                          LiteRtSubgraph subgraph,
                                          LiteRtOpList selected_ops) {

  // Iterate over ops and check criteria for selection
  // (using a C++ wrapper namespace '::litert' for convenience).
  // `subgraph` is a single subgraph from the original model, as such
  // this function will be called for each subgraph in the original model.

  ::litert::Subgraph main_subgraph(subgraph);
  for (const auto& op : main_subgraph.Ops()) {
    // 1. Check a constraint: require all tensors to be Float32
    bool only_f32 = true;
    // ... logic to check input/output types ...
    if (!only_f32) {
      continue;
    }

    // 2. Check op codes and push to selected_ops list
    if (op.Code() == kLiteRtOpCodeTflMul) {
      LITERT_RETURN_IF_ERROR(LiteRtPushOp(selected_ops, op.Get(), 0));
    } else if (op.Code() == kLiteRtOpCodeTflSub) {
      LITERT_RETURN_IF_ERROR(LiteRtPushOp(selected_ops, op.Get(), 0));
    } else if (op.Code() == kLiteRtOpCodeShloComposite) {
      // Example of checking composite op options
      // ... logic to check for "odml.rms_norm" name ...
      LITERT_RETURN_IF_ERROR(LiteRtPushOp(selected_ops, op.Get(), 0));
    }
  }
  return kLiteRtStatusOk;
}

컴파일을 호출하기 전에 LiteRT는 선택된 모든 작업을 새 중간 모델의 새 하위 그래프로 검증하고 '개요'를 작성합니다. 이 중간 모델이 컴파일에 전달됩니다.

컴파일 로직 (LiteRtCompilerPluginCompile)

이 함수는 파티셔닝된 하위 그래프를 가져와 맞춤 LiteRtCompiledResult를 생성합니다. 이 예시에서는 컴파일할 각 파티션의 독립형 바이트코드 모듈을 생성합니다. 실제 사례에서는 일반적으로 LiteRT 작업을 백엔드 컴파일러 라이브러리의 유형으로 변환하는 작업이 포함됩니다. 기능 예시 플러그인의 '컴파일'은 그래프를 인코딩하는 사람이 읽을 수 있는 문자열을 만듭니다.

// Internal structure defining the compiled output
struct LiteRtCompiledResultT {
  std::vector<std::string> byte_code;   // The hardware bytecode buffers
  std::vector<std::string> per_op_data; // Per-call metadata (CallInfo)
};

LiteRtStatus LiteRtCompilerPluginCompile(
    LiteRtCompilerPlugin compiler_plugin, const char* soc_model,
    LiteRtModel partitions, LiteRtCompiledResult* compiled_result) {

  // 1. Create the internal result structure
  auto model = litert::Model::CreateFromNonOwnedHandle(partitions);
  const auto num_partitions = model.NumSubgraphs();
  auto result = std::make_unique<LiteRtCompiledResultT>();
  result->byte_code.resize(num_partitions);
  result->per_op_data.resize(num_partitions);

  // 2. Iterate and compile each partition
  for (auto i = 0; i < num_partitions; ++i) {
    // CompileSinglePartition is an internal helper that converts the subgraph
    // into the target hardware's format and stores it in result->byte_code.
    // In the case of the example this is just a stringification of the graph.

    // ... internal call to CompileSinglePartition ...
    // Example: result.byte_code[i] = generated_hw_code;
    // Example: result.per_op_data[i] = absl::StrFormat("Partition_%d", i);

    // The "per_op_data" is a unique identifier associated to the `ith` partition.
    // This is analogous to the name of a function in a library.
    // This is only meaningful when the plugin is preparing single modules with multiple entry points.
  }

  // 3. Pass ownership of the result back to the framework
  *compiled_result = result.release();

  return kLiteRtStatusOk;
}

// Functions to expose the compiled result data to the framework
LiteRtStatus LiteRtGetCompiledResultByteCode(
    LiteRtCompiledResult compiled_result, LiteRtParamIndex byte_code_idx,
    const void** byte_code, size_t* byte_code_size) {
  // ... implementation reads from compiled_result->byte_code ...
}
// ... other LiteRtGetCompiledResult* functions ...

사용 및 검증

LiteRT는 컴파일러 플러그인을 모델 파일에 적용하고, 결과를 실행하고, 검증/벤치마킹하기 위한 다양한 도구를 제공합니다. 액셀러레이터 테스트 도구 모음 문서벤치마킹 및 프로파일링 문서를 참고하세요.