Android 10에서는 NNAPI(Neural Networks API)가 컴파일 아티팩트의 캐싱을 지원하기 위한 기능을 제공하여 앱 시작 시 컴파일에 사용되는 시간을 줄여줍니다. 캐싱 기능을 사용하면 드라이버에서 캐시된 파일을 관리하거나 정리할 필요가 없습니다. 이는 NN HAL 1.2로 구현 가능한 선택적 기능입니다. 이 함수에 관한 자세한 내용은 ANeuralNetworksCompilation_setCaching
을 참고하세요.
또한 드라이버는 NNAPI와 상관없는 컴파일 캐싱을 구현할 수 있습니다. 이는 NNAPI NDK 및 HAL 캐싱 기능의 사용 여부와 상관없이 구현할 수 있습니다. AOSP는 낮은 수준의 유틸리티 라이브러리(캐싱 엔진)를 제공합니다. 자세한 내용은 캐싱 엔진 구현을 참조하세요.
워크플로 개요
이 섹션에서는 구현된 컴파일 캐싱 기능에 관련된 일반적인 워크플로를 설명합니다.
제공된 캐시 정보 및 캐시 적중
- 앱에서는 모델의 고유한 캐싱 디렉터리와 체크섬을 전달합니다.
- NNAPI 런타임은 체크섬, 실행 환경설정, 파티션 분할 결과를 기준으로 캐시 파일을 찾고 파일을 찾습니다.
- NNAPI가 캐시 파일을 열고
prepareModelFromCache
가 포함된 드라이버에 핸들을 전달합니다. - 드라이버가 캐시 파일에서 바로 모델을 준비하고 준비된 모델을 반환합니다.
제공된 캐시 정보 및 캐시 부적중
- 앱에서 모델의 고유한 체크섬과 캐싱 디렉터리를 전달합니다.
- NNAPI 런타임은 체크섬, 실행 환경설정, 파티션 분할 결과를 기준으로 캐싱 파일을 찾고 캐시 파일을 찾지 않습니다.
- NNAPI에서 체크섬, 실행 환경설정, 파티션 분할을 기준으로 빈 캐시 파일을 생성하고 캐시 파일을 연 다음
prepareModel_1_2
를 포함하는 드라이버에 핸들과 모델을 전달합니다. - 드라이버가 모델을 컴파일하고 캐시 파일에 캐싱 정보를 작성하고 준비된 모델을 반환합니다.
캐시 정보가 제공되지 않음
- 어떠한 캐싱 정보도 제공되지 않은 상태에서 앱이 컴파일을 호출합니다.
- 앱이 캐싱과 관련된 어떤 것도 전달하지 않습니다.
- NNAPI 런타임이
prepareModel_1_2
가 포함된 드라이버에 모델을 전달합니다. - 드라이버가 모델을 컴파일하고 준비된 모델을 반환합니다.
캐시 정보
드라이버에 제공되는 캐싱 정보는 토큰과 캐시 파일 핸들로 구성됩니다.
토큰
토큰은 준비된 모델을 식별하는 길이 Constant::BYTE_SIZE_OF_CACHE_TOKEN
의 캐싱 토큰입니다. prepareModel_1_2
로 캐시 파일을 저장하고 prepareModelFromCache
로 준비된 모델을 검색하면 동일한 토큰이 제공됩니다. 드라이버의 클라이언트는 충돌 비율이 낮은 토큰을 선택해야 합니다. 드라이버는 토큰 충돌을 감지할 수 없습니다. 충돌 시 실행이 실패하거나 잘못된 출력 값을 생산하는 정상 실행으로 이어집니다.
캐시 파일 핸들(두 가지 유형의 캐시 파일)
캐시 파일에는 데이터 캐시 및 모델 캐시라는 두 가지 유형이 있습니다.
- 데이터 캐시: 사전 처리 및 변환된 텐서 버퍼를 포함하는 상수 데이터를 캐시하는 데 사용됩니다. 데이터 캐시를 수정할 경우 발생할 수 있는 최악의 결과는 실행 시간에 잘못된 출력 값이 생성되는 것입니다.
- 모델 캐시: 컴파일된 실행 가능한 기계어 코드(기기의 네이티브 바이너리 형식)와 같이 보안에 민감한 데이터를 캐시하는 데 사용됩니다. 모델 캐시를 수정하면 드라이버의 실행 동작에 영향을 미칠 수 있으며 악의적인 클라이언트에서 이를 악용하여 부여된 권한을 초과하는 작업을 실행할 수 있습니다. 따라서 드라이버는 캐시의 모델을 준비하기 전에 모델 캐시가 손상되었는지 확인해야 합니다. 자세한 내용은 보안을 참조하세요.
드라이버는 캐시 정보가 두 유형의 캐시 파일 간에 분산되는 방식을 결정하고 getNumberOfCacheFilesNeeded
를 포함하는 각 유형에 몇 개의 캐시 파일이 필요한지 보고해야 합니다.
NNAPI 런타임은 항상 읽기 및 쓰기 권한을 둘 다 사용하여 캐시 파일 핸들을 엽니다.
보안
컴파일 캐싱에서는 모델 캐시에 컴파일된 실행 가능한 기계어 코드(기기의 네이티브 바이너리 형식)와 같이 보안에 민감한 데이터가 포함될 수 있습니다. 제대로 보호되지 않은 경우 모델 캐시 수정이 드라이버의 실행 동작에 영향을 미칠 수 있습니다. 캐시 콘텐츠는 앱 디렉터리에 저장되므로 클라이언트에서 캐시 파일을 수정할 수 있습니다. 버그가 있는 클라이언트는 실수로 캐시를 손상시킬 수 있으며 악의적인 클라이언트에서 이를 악용하여 기기에서 인증되지 않은 코드를 실행할 수 있습니다. 이는 기기 특성에 따라 보안 문제일 수도 있습니다. 따라서 드라이버는 캐시의 모델을 준비하기 전에 잠재적인 모델 캐시 손상을 감지할 수 있어야 합니다.
이를 위한 하나의 방법은 드라이버가 모델 캐시의 암호화 해시 대상 토큰의 맵을 유지하는 것입니다. 드라이버는 컴파일을 캐시에 저장할 때 모델 캐시의 토큰과 해시를 저장할 수 있습니다. 드라이버는 캐시에서 컴파일을 가져올 때 기록된 토큰과 해시 쌍으로 모델 캐시의 새 해시를 확인합니다. 이 매핑은 시스템 재부팅 내내 지속되어야 합니다. 드라이버는 Android 키 저장소 서비스, framework/ml/nn/driver/cache
의 유틸리티 라이브러리 또는 기타 모든 적절한 메커니즘을 사용하여 매핑 관리자를 구현할 수 있습니다. 드라이버가 업데이트되면 캐시 파일이 이전 버전에서 준비되지 않도록 이 매핑 관리자가 다시 초기화되어야 합니다.
TOCTOU(time-of-check to time-of-use) 공격을 방지하려면 드라이버가 파일에 저장하기 전에 기록된 해시를 계산해야 하며 파일 콘텐츠를 내부 버퍼에 복사한 후에는 새 해시를 계산해야 합니다.
다음의 샘플 코드는 이 논리를 구현하는 방법을 보여줍니다.
bool saveToCache(const sp<V1_2::IPreparedModel> preparedModel,
const hidl_vec<hidl_handle>& modelFds, const hidl_vec<hidl_handle>& dataFds,
const HidlToken& token) {
// Serialize the prepared model to internal buffers.
auto buffers = serialize(preparedModel);
// This implementation detail is important: the cache hash must be computed from internal
// buffers instead of cache files to prevent time-of-check to time-of-use (TOCTOU) attacks.
auto hash = computeHash(buffers);
// Store the {token, hash} pair to a mapping manager that is persistent across reboots.
CacheManager::get()->store(token, hash);
// Write the cache contents from internal buffers to cache files.
return writeToFds(buffers, modelFds, dataFds);
}
sp<V1_2::IPreparedModel> prepareFromCache(const hidl_vec<hidl_handle>& modelFds,
const hidl_vec<hidl_handle>& dataFds,
const HidlToken& token) {
// Copy the cache contents from cache files to internal buffers.
auto buffers = readFromFds(modelFds, dataFds);
// This implementation detail is important: the cache hash must be computed from internal
// buffers instead of cache files to prevent time-of-check to time-of-use (TOCTOU) attacks.
auto hash = computeHash(buffers);
// Validate the {token, hash} pair by a mapping manager that is persistent across reboots.
if (CacheManager::get()->validate(token, hash)) {
// Retrieve the prepared model from internal buffers.
return deserialize<V1_2::IPreparedModel>(buffers);
} else {
return nullptr;
}
}
고급 사용 사례
특정 고급 사용 사례에서는 컴파일 호출 이후에 드라이버에서 캐시 콘텐츠에 대한 액세스 권한(읽기 또는 쓰기)을 요구합니다. 사용 사례의 예는 다음과 같습니다.
- Just-in-time 컴파일: 최초 실행까지 컴파일이 지연됩니다.
- 다단계 컴파일: 처음에는 빠른 컴파일이 실행되며 나중에는 사용 빈도에 따라 최적화된 컴파일이 선택적으로 실행됩니다.
컴파일 호출 후에 캐시 콘텐츠에 액세스(읽기 또는 쓰기)하려면 드라이버가 다음을 실행해야 합니다.
prepareModel_1_2
또는prepareModelFromCache
호출 도중 파일 핸들을 복제하고 나중에는 캐시 콘텐츠를 읽고 업데이트합니다.- 일반 컴파일 호출 외의 파일 잠금 논리를 구현하여 쓰기가 읽기 또는 다른 쓰기와 동시에 발생하지 않도록 합니다.
캐싱 엔진 구현
NN HAL 1.2 컴파일 캐싱 인터페이스 외에 frameworks/ml/nn/driver/cache
디렉터리에서 캐싱 유틸리티 라이브러리도 찾을 수 있습니다. nnCache
하위 디렉터리에는 드라이버에서 NNAPI 캐싱 기능을 사용하지 않고 컴파일 캐싱을 구현하도록 하기 위한 영구 저장소 코드가 포함되어 있습니다. 이러한 형식의 컴파일 캐싱은 NN HAL의 모든 버전으로 구현할 수 있습니다. 드라이버에서 HAL 인터페이스에서 연결 해제된 캐싱을 구현하기로 한 경우 드라이버는 더 이상 필요하지 않은 캐시된 아티팩트를 해제해야 합니다.