從 Android 10 開始,神經網絡 API (NNAPI) 提供了支持編譯工件緩存的函數,從而減少了應用啟動時用於編譯的時間。使用此緩存功能,驅動程序無需管理或清理緩存文件。這是一個可選功能,可以使用 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 保存緩存文件並使用prepareModel_1_2
檢索準備好的模型時提供相同的prepareModelFromCache
。驅動程序的客戶端應該選擇碰撞率低的令牌。驅動程序無法檢測到令牌衝突。衝突會導致執行失敗或成功執行產生不正確的輸出值。
緩存文件句柄(兩種類型的緩存文件)
兩種類型的緩存文件是數據緩存和模型緩存。
- 數據緩存:用於緩存常量數據,包括預處理和轉換的張量緩衝區。對數據緩存的修改不應該導致比在執行時生成錯誤的輸出值更糟糕的效果。
- 模型緩存:用於緩存安全敏感數據,例如以設備的本機二進制格式編譯的可執行機器代碼。對模型緩存的修改可能會影響驅動程序的執行行為,惡意客戶端可能會利用它來執行超出授予權限的操作。因此,驅動程序必須在從緩存中準備模型之前檢查模型緩存是否已損壞。有關詳細信息,請參閱安全性。
驅動程序必須決定如何在兩種類型的緩存文件之間分配緩存信息,並使用getNumberOfCacheFilesNeeded
報告每種類型需要多少個緩存文件。
NNAPI 運行時始終以讀取和寫入權限打開緩存文件句柄。
安全
在編譯緩存中,模型緩存可能包含安全敏感數據,例如設備本機二進制格式的已編譯可執行機器代碼。如果沒有得到適當的保護,對模型緩存的修改可能會影響驅動程序的執行行為。因為緩存內容存放在app目錄下,所以緩存文件是客戶端可以修改的。有缺陷的客戶端可能會意外損壞緩存,惡意客戶端可能會故意利用它在設備上執行未經驗證的代碼。根據設備的特性,這可能是一個安全問題。因此,驅動程序必須能夠在從緩存中準備模型之前檢測到潛在的模型緩存損壞。
一種方法是讓驅動程序維護從令牌到模型緩存的加密哈希的映射。將編譯保存到緩存時,驅動程序可以存儲其模型緩存的令牌和哈希。當從緩存中檢索編譯時,驅動程序會使用記錄的令牌和哈希對檢查模型緩存的新哈希。此映射應該在系統重新啟動後保持不變。驅動程序可以使用Android 密鑰庫服務、 framework/ml/nn/driver/cache
中的實用程序庫或任何其他合適的機制來實現映射管理器。驅動程序更新後,應重新初始化此映射管理器,以防止從早期版本準備緩存文件。
為了防止檢查時間到使用時間(TOCTOU) 攻擊,驅動程序必須在保存到文件之前計算記錄的哈希值,並在將文件內容複製到內部緩衝區後計算新的哈希值。
此示例代碼演示瞭如何實現此邏輯。
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;
}
}
高級用例
在某些高級用例中,驅動程序需要在編譯調用後訪問緩存內容(讀取或寫入)。示例用例包括:
- 即時編譯:編譯延遲到第一次執行。
- 多階段編譯:最初執行快速編譯,稍後根據使用頻率執行可選的優化編譯。
要在編譯調用後訪問緩存內容(讀取或寫入),請確保驅動程序:
- 在調用
prepareModel_1_2
或prepareModelFromCache
期間複製文件句柄,並在以後讀取/更新緩存內容。 - 在普通編譯調用之外實現文件鎖定邏輯,以防止寫入與讀取或其他寫入同時發生。
實現緩存引擎
除了 NN HAL 1.2 編譯緩存接口之外,您還可以在frameworks/ml/nn/driver/cache
目錄中找到緩存實用程序庫。 nnCache
子目錄包含驅動程序的持久存儲代碼,以在不使用 NNAPI 緩存功能的情況下實現編譯緩存。這種形式的編譯緩存可以使用任何版本的 NN HAL 來實現。如果驅動程序選擇實現與 HAL 接口斷開連接的緩存,則驅動程序負責在不再需要緩存的工件時釋放它們。