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

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