A partir de Android 10, la API de Neural Networks (NNAPI) proporciona funciones para admitir el almacenamiento en caché de artefactos de compilación, lo que reduce el tiempo necesario para la compilación cuando se inicia una app. Con esta función de almacenamiento en caché, el controlador no necesita administrar ni limpiar los archivos almacenados en caché. Esta es una función opcional que se puede implementar con NN HAL 1.2. Para obtener más información sobre esta función, consulta ANeuralNetworksCompilation_setCaching
.
El controlador también puede implementar el almacenamiento en caché de compilación independientemente de la NNAPI. Esto se puede implementar independientemente de si se usan o no las funciones de almacenamiento en caché de la HAL y el NDK de la NNAPI. AOSP proporciona una biblioteca de utilidades de bajo nivel (un motor de almacenamiento en caché). Para obtener más información, consulta Cómo implementar un motor de almacenamiento en caché.
Descripción general del flujo de trabajo
En esta sección, se describen los flujos de trabajo generales con la función de almacenamiento en caché de compilación implementada.
Información de caché proporcionada y acierto de caché
- La app pasa un directorio de almacenamiento en caché y una suma de comprobación únicas para el modelo.
- El tiempo de ejecución de la NNAPI busca los archivos de caché según la suma de comprobación, la preferencia de ejecución y el resultado de la partición, y los encuentra.
- La NNAPI abre los archivos de caché y pasa los controladores al controlador con
prepareModelFromCache
. - El controlador prepara el modelo directamente desde los archivos de caché y muestra el modelo preparado.
Información de caché proporcionada y error de caché
- La app pasa una suma de comprobación única para el modelo y un directorio de almacenamiento en caché.
- El tiempo de ejecución de la NNAPI busca los archivos de almacenamiento en caché en función de la suma de verificación, la preferencia de ejecución y el resultado de la partición, pero no encuentra los archivos de caché.
- La NNAPI crea archivos de caché vacíos basados en la suma de comprobación, la preferencia de ejecución y la partición, abre los archivos de caché y pasa los controladores y el modelo al controlador con
prepareModel_1_2
. - El controlador compila el modelo, escribe información de almacenamiento en caché en los archivos de caché y muestra el modelo preparado.
No se proporcionó información de la caché
- La app invoca la compilación sin proporcionar información de almacenamiento en caché.
- La app no pasa nada relacionado con el almacenamiento en caché.
- El entorno de ejecución de NNAPI pasa el modelo al controlador con
prepareModel_1_2
. - El controlador compila el modelo y muestra el modelo preparado.
Información de caché
La información de almacenamiento en caché que se proporciona a un controlador consta de un token y controladores de archivos de caché.
Token
El token es un token de almacenamiento en caché de longitud Constant::BYTE_SIZE_OF_CACHE_TOKEN
que identifica el modelo preparado. Se proporciona el mismo token cuando se guardan los archivos de caché con prepareModel_1_2
y se recupera el modelo preparado con prepareModelFromCache
. El cliente del controlador debe elegir un token con una tasa de colisión baja. El controlador no puede detectar una colisión de tokens. Una colisión genera una ejecución fallida o una ejecución correcta que produce valores de salida incorrectos.
Identificadores de archivos de caché (dos tipos de archivos de caché)
Los dos tipos de archivos de caché son la caché de datos y la caché del modelo.
- Caché de datos: Úsala para almacenar en caché datos constantes, incluidos los búferes de tensores procesados previamente y transformados. Una modificación de la caché de datos no debería causar ningún efecto peor que generar valores de salida incorrectos en el momento de la ejecución.
- Caché del modelo: Se usa para almacenar en caché datos sensibles a la seguridad, como el código máquina ejecutable compilado en el formato binario nativo del dispositivo. Una modificación en la caché del modelo podría afectar el comportamiento de ejecución del controlador, y un cliente malicioso podría hacer uso de esto para ejecutar más allá del permiso otorgado. Por lo tanto, el controlador debe verificar si la caché del modelo está dañada antes de preparar el modelo desde la caché. Para obtener más información, consulta Seguridad.
El controlador debe decidir cómo se distribuye la información de caché entre los dos tipos de archivos de caché y, luego, informar cuántos archivos de caché necesita para cada tipo con getNumberOfCacheFilesNeeded
.
El entorno de ejecución de NNAPI siempre abre los controladores de archivos de caché con permiso de lectura y escritura.
Seguridad
En la caché de compilación, la caché del modelo puede contener datos sensibles a la seguridad, como el código máquina ejecutable compilado en el formato binario nativo del dispositivo. Si no se protege correctamente, una modificación en la caché del modelo puede afectar el comportamiento de ejecución del controlador. Debido a que el contenido de la caché se almacena en el directorio de la app, el cliente puede modificar los archivos de caché. Un cliente con errores puede dañar la caché por accidente, y un cliente malicioso podría usarla de forma intencional para ejecutar código no verificado en el dispositivo. Según las características del dispositivo, es posible que se trate de un problema de seguridad. Por lo tanto, el controlador debe poder detectar posibles daños en la caché del modelo antes de preparar el modelo a partir de la caché.
Una forma de hacerlo es que el conductor mantenga un mapa del token a un hash criptográfico de la caché del modelo. El controlador puede almacenar el token y el hash de la caché de su modelo cuando guarda la compilación en la caché. El controlador verifica el hash nuevo de la caché del modelo con el token registrado y el par de hash cuando recupera la compilación de la caché. Esta asignación debe persistir en los reinicios del sistema. El controlador puede usar el servicio de almacén de claves de Android, la biblioteca de utilidades en framework/ml/nn/driver/cache
o cualquier otro mecanismo adecuado para implementar un administrador de asignación. Cuando se actualiza el controlador, se debe volver a inicializar este administrador de asignación para evitar que se preparen archivos de caché de una versión anterior.
Para evitar ataques de tiempo de verificación hasta el tiempo de uso (TOCTOU), el controlador debe calcular el hash grabado antes de guardarlo y calcular el hash nuevo después de copiar el contenido del archivo en un búfer interno.
Este código de muestra demuestra cómo implementar esta lógica.
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;
}
}
Casos de uso avanzados
En algunos casos de uso avanzados, un controlador requiere acceso al contenido de la caché (lectura o escritura) después de la llamada de compilación. Estos son algunos ejemplos de casos de uso:
- Compilación justo a tiempo: La compilación se retrasa hasta la primera ejecución.
- Compilación en varias etapas: Inicialmente, se realiza una compilación rápida y, luego, una compilación optimizada opcional, según la frecuencia de uso.
Para acceder al contenido de la caché (lectura o escritura) después de la llamada de compilación, asegúrate de que el controlador cumpla con los siguientes requisitos:
- Duplica los controladores del archivo durante la invocación de
prepareModel_1_2
oprepareModelFromCache
y lee o actualiza el contenido de la caché más adelante. - Implementa la lógica de bloqueo de archivos fuera de la llamada de compilación ordinaria para evitar que se produzca una operación de escritura de forma simultánea con una operación de lectura o con otra operación de escritura.
Implementa un motor de almacenamiento en caché
Además de la interfaz de almacenamiento en caché de compilación de NN HAL 1.2, también puedes encontrar una biblioteca de utilidades de almacenamiento en caché en el directorio frameworks/ml/nn/driver/cache
. El subdirectorio nnCache
contiene código de almacenamiento persistente para que el controlador implemente el almacenamiento en caché de compilación sin usar las funciones de almacenamiento en caché de NNAPI. Esta forma de almacenamiento en caché de compilación se puede implementar con cualquier versión del HAL de NN. Si el controlador elige implementar el almacenamiento en caché desconectado de la interfaz HAL, es responsable de liberar los artefactos almacenados en caché cuando ya no sean necesarios.