Кэширование компиляции

Начиная с Android 10, API нейронных сетей (NNAPI) предоставляет функции для поддержки кэширования артефактов компиляции, что сокращает время компиляции при запуске приложения. Используя эту функцию кэширования, драйверу не нужно управлять кэшированными файлами или очищать их. Это дополнительная функция, которую можно реализовать с помощью NN HAL 1.2. Дополнительные сведения об этой функции см. в разделе ANeuralNetworksCompilation_setCaching .

Драйвер также может реализовать кэширование компиляции независимо от NNAPI. Это можно реализовать независимо от того, используются ли функции кэширования NNAPI NDK и HAL. AOSP предоставляет низкоуровневую служебную библиотеку (механизм кэширования). Дополнительные сведения см. в разделе «Реализация механизма кэширования» .

Обзор рабочего процесса

В этом разделе описаны общие рабочие процессы с реализованной функцией кэширования компиляции.

Предоставленная информация о кэше и попадание в кэш

  1. Приложение передает каталог кэширования и уникальную для модели контрольную сумму.
  2. Среда выполнения NNAPI ищет файлы кэша на основе контрольной суммы, предпочтений выполнения и результата секционирования и находит файлы.
  3. NNAPI открывает файлы кеша и передает дескрипторы драйверу с помощью prepareModelFromCache .
  4. Драйвер подготавливает модель непосредственно из файлов кэша и возвращает подготовленную модель.

Предоставленная информация о кэше и промах в кэше

  1. Приложение передает контрольную сумму, уникальную для модели и каталога кэширования.
  2. Среда выполнения NNAPI ищет файлы кэширования на основе контрольной суммы, предпочтений выполнения и результата секционирования и не находит файлы кэша.
  3. NNAPI создает пустые файлы кэша на основе контрольной суммы, предпочтений выполнения и секционирования, открывает файлы кэша и передает дескрипторы и модель драйверу с prepareModel_1_2 .
  4. Драйвер компилирует модель, записывает информацию о кэшировании в файлы кэша и возвращает подготовленную модель.

Информация о кэше не предоставлена

  1. Приложение вызывает компиляцию без предоставления какой-либо информации о кэшировании.
  2. Приложение не передает ничего, связанного с кешированием.
  3. Среда выполнения NNAPI передает модель драйверу с помощью prepareModel_1_2 .
  4. Драйвер компилирует модель и возвращает подготовленную модель.

Кэшировать информацию

Информация о кэшировании, предоставляемая драйверу, состоит из токена и дескрипторов файлов кэша.

Токен

Токен представляет собой токен кэширования длиной 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, драйвер несет ответственность за освобождение кэшированных артефактов, когда они больше не нужны.