Compilación determinista de procesamiento federado de personalización integrada en el dispositivo

Las compilaciones determinísticas son necesarias para la certificación de cargas de trabajo en el entorno de ejecución confiable (TEE) de la Personalización en el dispositivo (ODP), disponible públicamente en Google Cloud como Confidential Space (CS).

Las imágenes de carga de trabajo deben generar un hash de imagen determinístico que CS pueda usar para la certificación de la carga de trabajo (que usa la arquitectura de Procedimientos de certificación remota (RATS) de RFC 9334 del NIST).

En este documento, se analizarán la implementación y la compatibilidad con las compilaciones determinísticas en el repositorio de odp-federatedcompute. Los servicios de actualización del modelo y el agregador de ODP se ejecutarán dentro de Confidential Space. El repositorio admite compilaciones determinísticas para todos nuestros servicios, que son necesarias para los casos de uso de producción.

Compilaciones determinísticas

Las compilaciones determinísticas constan de dos componentes principales:

  1. La compilación de los archivos binarios obligatorios Esto incluye archivos .jar, bibliotecas compartidas y metadatos.
  2. La imagen base y las dependencias del entorno de ejecución Imagen base del entorno de ejecución que se usa para ejecutar los archivos binarios compilados.

Actualmente, el repositorio de Federated Compute de ODP admite los siguientes tipos de cargas de trabajo:

  • Cargas de trabajo de Java y Spring
    • TaskAssignment, TaskManagement, Collector
  • Java y Spring con cargas de trabajo de TensorFlow de JNI
    • ModelUpdater, Aggregator
  • Cargas de trabajo de Python
    • TaskBuilder

Dependencias

En la siguiente lista, se indican las dependencias en las que se basa el ODP para mantener el determinismo y la disponibilidad:

  • Bazel
  • GitHub
  • Maven
  • PyPi
  • Instantáneas de Debian
  • Registro de DockerHub
  • Google Container Registry (GCR)

Cargas de trabajo determinísticas

Todas las cargas de trabajo se compilan con Bazel con cadenas de herramientas específicas del lenguaje y con imágenes de contenedores compiladas con rules_oci. El archivo WORKSPACE define todas las dependencias con sus versiones y hashes correspondientes.

Instantáneas de Debian

Todas las imágenes de carga de trabajo deben compilarse dentro del dockerfile proporcionado, que se compila sobre una instantánea de Debian. Las instantáneas de Debian proporcionan una instantánea estable del repositorio con las siguientes características determinísticas:

Cargas de trabajo de Java Spring

El remotejdk_17 de Bazel se usa para proporcionar un Java hermético para la compilación. Otras dependencias de Java se administran y definen en el archivo WORKSPACE.

Las cargas de trabajo de Java Spring se compilan en un archivo jar llamado <service>_application.jar. El frasco contiene lo siguiente:

  • Archivos de clase Java
  • META-INF/
    • Datos del manifiesto de Bazel
  • build-data.properties
    • Datos de compilación de Bazel
  • BOOT-INF/
    • Son las dependencias de archivos .jar empaquetados, generadas por rules_spring.

Capas de imágenes

La imagen de carga de trabajo de Java Spring consta de dos capas:

Configuración de la imagen

  • Punto de entrada
    • java -jar <service>_application.jar

Cargas de trabajo de TensorFlow con JNI

Las cargas de trabajo de JNI TensorFlow se compilan sobre las cargas de trabajo de Java Spring. Se proporciona una cadena de herramientas hermética de Clang + LLVM de Bazel con Clang + LLVM 16 compilado previamente con un sysroot proporcionado por la imagen de instantánea de Debian para compilar código máquina.

Las cargas de trabajo de JNI se compilan en una biblioteca compartida llamada libtensorflow.so junto con <service>_application.jar.

Capas de imágenes

La imagen de carga de trabajo de TensorFlow de JNI consta de varias capas:

  • Capa de imagen base
  • Son las capas de dependencia de paquetes de Debian. Las capas se generan con archivos .deb descargados de debian-snapshot y se vuelven a empaquetar como capas de imágenes.
    • libc++1-16_amd64.tar
    • libc++abi1-16_amd64.tar
    • libc6_amd64.tar
    • libunwind-16_amd64.tar
    • libgcc-s1_amd64.tar
    • gcc-13-base_amd64.tar
  • Capa de carga de trabajo
    • binary_tar.tar
      • <service>_application.jar
      • libtensorflow-jni.so
      • libaggregation-jni.so

Configuración de la imagen

  • Etiquetas (solo para imágenes creadas para ejecutarse dentro del TEE)
    • "tee.launch_policy.allow_env_override": "FCP_OPTS"
      • Permite que la variable de entorno FCP_OPTS se configure en un espacio confidencial. La carga de trabajo consumirá FCP_OPTS durante el inicio para configurar los parámetros requeridos.
      • La variable de entorno FCP_OPTS se establece cuando se ejecuta la imagen (en lugar de compilarse) para mantener el determinismo de la compilación.
    • "tee.launch_policy.log_redirect": "always"
    • "tee.launch_policy.monitoring_memory_allow": "always"
  • Punto de entrada
    • java -Djava.library.path=. -jar <service>_application.jar

Cargas de trabajo de Python

rules_python de Bazel se usa para proporcionar una cadena de herramientas hermética de Python 3.10. Se usa un archivo de requisitos de pip bloqueado para recuperar de forma determinística las dependencias de pip. La imagen de instantánea de Debian garantiza que se recuperen distribuciones determinísticas según la compatibilidad de la plataforma y proporciona una cadena de herramientas de C++ para compilar distribuciones de origen.

Las cargas de trabajo de Python se empaquetarán en un conjunto de paquetes pip descargados, una distribución de Python 3.10, el código fuente de Python de ODP y una secuencia de comandos de inicio de Python.

  • <service>.runfiles/
    • La distribución de Python se almacena en python_x86_64-unknown-linux-gnu/.
    • El código fuente se almacena en com_google_ondevicepersonalization_federatedcompute/.
    • Los paquetes de pip se almacenan en pypi_<dependency_name>/
  • <service>.runfiles_manifest
    • Archivo de manifiesto para el directorio <service>.runfiles/
  • <service>
    • Secuencia de comandos de Python para ejecutar la carga de trabajo de Python con los archivos ejecutables

Capas de imágenes

La imagen de carga de trabajo de Python consta de cuatro capas:

  • Capa de imagen base
  • Capa de intérprete
    • interpreter_layer.jar
      • <service>/<service>.runfiles/python_x86_64-unknown-linux-gnu/**
  • Capa de paquetes
    • packages_layer.jar
      • <service>/<service>.runfiles/**/site-packages/**
  • Capa de carga de trabajo
    • app_tar_manifest.tar
      • Contiene el código fuente, la secuencia de comandos de inicio y el manifiesto.
        • <service>/<service>.runfiles_manifest
        • <service>/<service>
        • <service>/<service>.runfiles/com_google_ondevicepersonalization_federatedcompute/**

Configuración de la imagen

  • Punto de entrada
    • /<service>/<service>

Compila imágenes

Una vez que hayas elegido tus cargas de trabajo, podrás compilar y publicar tus imágenes.

Requisitos previos

  • Bazel 6.4.0
    • Requiere instalaciones de Java y C++
  • Docker

Procedimiento

Las imágenes se deben compilar dentro del contenedor de Docker creado por el dockerfile proporcionado. Se proporcionan dos secuencias de comandos para ayudar a compilar las imágenes determinísticas finales.

  • docker_run.sh
    • docker_run.sh compilará la imagen de Docker a partir del Dockerfile, activará el directorio de trabajo, activará el daemon de Docker del host y ejecutará Docker con el comando bash proporcionado. Cualquier variable que se pase antes del comando bash se tratará como marcas de ejecución de Docker.
  • build_images.sh
    • build_images.sh ejecutará bazel build para todas las imágenes y generará los hashes de las imágenes generadas para cada imagen compilada.

Compila todas las imágenes

./scripts/docker/docker_run.sh "./scripts/build_images.sh"

Los hashes de imagen esperados para cada versión se pueden encontrar en las versiones de odp-federatedcompute en GitHub.

Publica imágenes

La publicación se configura con las reglas de Bazel oci_push. Para cada servicio, el repositorio de destino debe configurarse para todos los siguientes elementos:

  • agregador
  • Recopilador
  • model_updater
  • task_assignment
  • task_management
  • task_scheduler
  • task_builder

Publica una sola imagen

Para publicar una sola imagen, sigue estos pasos:

./scripts/docker/docker_run.sh "bazel run //shuffler/services/<servicename_no_underscore>:<servicename_with_underscore>_image_publish"

Imágenes compiladas

El creador deberá almacenar y alojar todas las imágenes compiladas, por ejemplo, en un registro de artefactos de GCP.