從 Android 10 開始,Neural Networks 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
儲存快取檔案,以及使用 prepareModelFromCache
擷取準備好的模型時,系統會提供相同的權杖。駕駛人的用戶端應選擇碰撞率較低的權杖。驅動程式無法偵測到權杖衝突。發生碰撞時,執行作業會失敗,或成功執行但產生錯誤的輸出值。
快取檔案控制代碼 (兩種快取檔案)
快取檔案分為兩種:資料快取和模型快取。
- 資料快取:用於快取常數資料,包括預先處理和轉換的張量緩衝區。修改資料快取時,不應導致任何比執行階段產生錯誤輸出值更糟的影響。
- 模型快取:用於快取安全性敏感資料,例如裝置原生二進位格式的已編譯可執行機器碼。修改模型快取可能會影響驅動程式的執行行為,惡意用戶端可能會利用這點,在授權範圍外執行作業。因此,驅動程式必須先檢查模型快取是否已損毀,再從快取準備模型。詳情請參閱「安全性」。
驅動程式必須決定如何在兩種快取檔案類型之間分配快取資訊,並使用 getNumberOfCacheFilesNeeded
回報每種快取檔案類型所需的檔案數量。
NNAPI 執行階段一律會開啟快取檔案控制代碼,並同時具備讀取和寫入權限。
安全性
在編譯快取中,模型快取可能包含安全性敏感資料,例如裝置原生二進位格式的已編譯可執行機器碼。如果未受到妥善保護,模型快取遭到修改可能會影響驅動程式的執行行為。由於快取內容儲存在應用程式目錄中,因此用戶端可以修改快取檔案。有錯誤的用戶端可能會意外損毀快取,惡意用戶端則可能刻意利用這點,在裝置上執行未經驗證的程式碼。視裝置特性而定,這可能是安全性問題。因此,驅動程式必須能夠在從快取準備模型之前,偵測到潛在的模型快取損毀。
其中一種做法是讓驅動程式維護從權杖到模型快取密碼編譯雜湊的路徑。將編譯內容儲存至快取時,驅動程式可以儲存權杖和模型快取的雜湊。從快取擷取編譯內容時,驅動程式會檢查模型快取的新雜湊值,以及記錄的權杖和雜湊值配對。這項對應應會在系統重新啟動後持續存在。駕駛人可以使用 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 介面中斷連線的快取,驅動程式必須在不再需要快取構件時釋放這些構件。