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

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