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

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