버스트 실행 및 FMQ

Neural Networks HAL 1.2에는 버스트 실행의 개념이 도입됩니다. 버스트 실행은 카메라 캡처 프레임 또는 연속적인 오디오 샘플에서 작동하는 실행과 같이 빠르게 연속해서 발생하는 준비된 모델의 실행 시퀀스입니다. 버스트 객체는 버스트 실행 집합을 제어하고 실행 간의 리소스를 보존하는 데 사용되어 실행의 오버헤드를 낮춰줍니다. 버스트 객체는 세 가지 최적화를 지원합니다.

  1. 버스트 객체는 실행 시퀀스 이전에 생성되며 시퀀스가 종료되면 해제됩니다. 따라서 버스트 객체의 전체 기간은 고성능 상태를 얼마나 오랫동안 유지해야 하는지를 드라이버에 알립니다.
  2. 버스트 객체는 실행 간의 리소스를 보존할 수 있습니다. 예를 들어 드라이버는 첫 실행 시 메모리 객체를 매핑하고 매핑을 버스트 객체에 캐시하여 후속 실행에서 다시 사용하도록 할 수 있습니다. 모든 캐시된 리소스는 버스트 객체가 소멸되거나 NNAPI 런타임이 리소스가 더 이상 필요하지 않다고 버스트 객체에 알리면 해제할 수 있습니다.
  3. 버스트 객체는 fast message queues(FMQ)를 사용하여 앱과 드라이버 프로세스 간에 통신합니다. 그러면 FMQ가 HIDL을 우회하고 공유 메모리의 원자적 원형 FIFO를 통해 데이터를 다른 프로세스에 직접적으로 전달하기 때문에 지연 시간이 감소할 수 있습니다. 소비자 프로세스는 알아서 항목을 대기열에서 제외하고 FIFO에서 요소 수를 폴링하거나 생산자에 의해 신호를 받는 FMQ의 이벤트 플래그를 기다리는 방식으로 처리 과정을 시작합니다. 이 이벤트 플래그는 futex(fast userspace mutex)입니다.

FMQ는 프로세스 전체에 걸쳐 전체 기간 보증을 제공하지 않는 하위 수준의 데이터 구조이며, FMQ의 반대쪽 끝의 프로세스가 예상대로 실행되고 있는지를 파악하기 위한 메커니즘이 내장되어 있지 않습니다. 따라서 FMQ의 생산자가 죽으면 소비자가 도착하지 않을 데이터를 계속해서 기다려야 할 수도 있습니다. 이 문제의 한 가지 해결책은 드라이버가 FMQ를 더 높은 수준의 버스트 객체와 연결하여 버스트 실행이 종료된 시점을 감지하는 것입니다.

버스트 실행은 동일한 인수에서 작동하고 다른 실행 경로와 동일한 결과를 반환합니다. 따라서 기본 FMQ와 NNAPI 서비스 드라이버 간에 동일한 데이터가 오가야 합니다. 하지만 FMQ는 POD(plain-old-data) 유형만 전송할 수 있습니다. 복잡한 데이터를 전송하려면 중첩된 버퍼(벡터 유형)를 FMQ에서 바로 직렬화하고 역직렬화해야 하며, 요청이 있을 경우 HIDL 콜백 객체를 사용하여 메모리 풀 핸들을 전송해야 합니다. FMQ의 생산자 측에서는 대기열이 차단되는 경우 MessageQueue::writeBlocking을 사용하여, 대기열이 차단되지 않는 경우 MessageQueue::write를 사용하여 요청 또는 결과 메시지를 소비자에게 원자적으로 전송해야 합니다.

버스트 인터페이스

Neural Networks HAL의 버스트 인터페이스는 hardware/interfaces/neuralnetworks/1.2/에 있으며 아래에 설명되어 있습니다. NDK 레이어의 버스트 인터페이스에 관한 자세한 내용은 frameworks/ml/nn/runtime/include/NeuralNetworks.h를 참고하세요.

types.hal

types.hal은 FMQ 전체에 걸쳐 전송되는 데이터 유형을 정의합니다.

  • FmqRequestDatum: 실행 Request 객체 및 MeasureTiming 값의 직렬화된 표현의 단일 요소로, fast message queue를 통해 전송됩니다.
  • FmqResultDatum: 실행(ErrorStatus, OutputShapes, Timing)에서 반환된 값의 직렬화된 표현의 단일 요소로, fast message queue를 통해 반환됩니다.

IBurstContext.hal

IBurstContext.hal은 Neural Networks 서비스에 있는 HIDL 인터페이스 객체를 정의합니다.

  • IBurstContext: 버스트의 리소스를 관리하는 컨텍스트 객체입니다.

IBurstCallback.hal

IBurstCallback.hal은 Neural Networks 런타임에서 생성된 콜백의 HIDL 인터페이스 객체를 정의하고 Neural Networks 서비스에서 슬롯 식별자에 상응하는 hidl_memory 객체를 검색하는 데 사용됩니다.

  • IBurstCallback: 서비스에서 메모리 객체를 검색하기 위해 사용하는 콜백 객체입니다.

IPreparedModel.hal

IPreparedModel.hal은 준비된 모델에서 IBurstContext 객체를 만드는 메서드를 통해 HAL 1.2에서 확장됩니다.

  • configureExecutionBurst: 준비된 모델에서 빠르게 연속으로 여러 추론을 실행하는 데 사용되는 버스트 객체를 구성합니다.

드라이버에서 버스트 실행 지원

HIDL NNAPI 서비스에서 버스트 객체를 지원하는 가장 간단한 방법은 버스트 유틸리티 함수 ::android::nn::ExecutionBurstServer::create를 사용하는 것으로, 이 함수는 ExecutionBurstServer.h에 있고 libneuralnetworks_commonlibneuralnetworks_util 정적 라이브러리에 패키징됩니다. 이 팩토리 함수에는 두 개의 오버로드가 있습니다.

  • 한 오버로드는 IPreparedModel 객체 포인터를 허용합니다. 이 유틸리티 함수는 IPreparedModel 객체의 executeSynchronously 메서드를 사용하여 모델을 실행합니다.
  • 나머지 오버로드는 맞춤설정 가능한 IBurstExecutorWithCache 객체를 허용하고 이 객체는 여러 실행에서 지속되는 리소스(예: hidl_memory 매핑)를 캐시하는 데 사용될 수 있습니다.

각 오버로드는 자체 전용 리스너 스레드를 포함하고 관리하는 IBurstContext 객체(버스트 객체를 표현)를 반환합니다. 이 스레드는 requestChannel FMQ에서 요청을 수신하고 추론을 실행한 후 resultChannel FMQ를 통해 결과를 반환합니다. 이 스레드와 IBurstContext 객체에 포함된 다른 모든 리소스는 버스트의 클라이언트가 IBurstContext 참조를 상실할 때 자동으로 해제됩니다.

또는 IPreparedModel::configureExecutionBurst에 전달된 requestChannelresultChannel FMQ를 통해 메시지를 주고받는 방법을 이해하는 자체 IBurstContext 구현을 만들 수 있습니다.

버스트 유틸리티 함수는 ExecutionBurstServer.h에 있습니다.

/**
 * Create automated context to manage FMQ-based executions.
 *
 * This function is intended to be used by a service to automatically:
 * 1) Receive data from a provided FMQ
 * 2) Execute a model with the given information
 * 3) Send the result to the created FMQ
 *
 * @param callback Callback used to retrieve memories corresponding to
 *     unrecognized slots.
 * @param requestChannel Input FMQ channel through which the client passes the
 *     request to the service.
 * @param resultChannel Output FMQ channel from which the client can retrieve
 *     the result of the execution.
 * @param executorWithCache Object which maintains a local cache of the
 *     memory pools and executes using the cached memory pools.
 * @result IBurstContext Handle to the burst context.
 */
static sp<ExecutionBurstServer> create(
        const sp<IBurstCallback>& callback, const FmqRequestDescriptor& requestChannel,
        const FmqResultDescriptor& resultChannel,
        std::shared_ptr<IBurstExecutorWithCache> executorWithCache);

/**
 * Create automated context to manage FMQ-based executions.
 *
 * This function is intended to be used by a service to automatically:
 * 1) Receive data from a provided FMQ
 * 2) Execute a model with the given information
 * 3) Send the result to the created FMQ
 *
 * @param callback Callback used to retrieve memories corresponding to
 *     unrecognized slots.
 * @param requestChannel Input FMQ channel through which the client passes the
 *     request to the service.
 * @param resultChannel Output FMQ channel from which the client can retrieve
 *     the result of the execution.
 * @param preparedModel PreparedModel that the burst object was created from.
 *     IPreparedModel::executeSynchronously will be used to perform the
 *     execution.
 * @result IBurstContext Handle to the burst context.
 */
  static sp<ExecutionBurstServer> create(const sp<IBurstCallback>& callback,
                                         const FmqRequestDescriptor& requestChannel,
                                         const FmqResultDescriptor& resultChannel,
                                         IPreparedModel* preparedModel);

다음은 frameworks/ml/nn/driver/sample/SampleDriver.cpp의 Neural Networks 샘플 드라이버에 있는 버스트 인터페이스의 참조 구현입니다.

Return<void> SamplePreparedModel::configureExecutionBurst(
        const sp<V1_2::IBurstCallback>& callback,
        const MQDescriptorSync<V1_2::FmqRequestDatum>& requestChannel,
        const MQDescriptorSync<V1_2::FmqResultDatum>& resultChannel,
        configureExecutionBurst_cb cb) {
    NNTRACE_FULL(NNTRACE_LAYER_DRIVER, NNTRACE_PHASE_EXECUTION,
                 "SampleDriver::configureExecutionBurst");
    // Alternatively, the burst could be configured via:
    // const sp<V1_2::IBurstContext> burst =
    //         ExecutionBurstServer::create(callback, requestChannel,
    //                                      resultChannel, this);
    //
    // However, this alternative representation does not include a memory map
    // caching optimization, and adds overhead.
    const std::shared_ptr<BurstExecutorWithCache> executorWithCache =
            std::make_shared<BurstExecutorWithCache>(mModel, mDriver, mPoolInfos);
    const sp<V1_2::IBurstContext> burst = ExecutionBurstServer::create(
            callback, requestChannel, resultChannel, executorWithCache);
    if (burst == nullptr) {
        cb(ErrorStatus::GENERAL_FAILURE, {});
    } else {
        cb(ErrorStatus::NONE, burst);
    }
    return Void();
}