Workers multimodaux GPU
Le Control Plane exécute une opération multimodale en ligne quand un adaptateur est disponible dans son
propre processus. Sinon le job reste dans eai.multimodal_jobs au statut queued (« no adapter available,
queued for GPU workers ») et c'est un worker GPU, python -m eai.media_worker, qui le traite.
Lancement
python -m eai.media_worker --kind stt [--kind diarization] [--stream EAI_MULTIMODAL] [--consumer stt-workers]
python -m eai.media_worker --kind tts --once # au plus un job, puis sortie
python -m eai.media_worker --kind stt --max-jobs 10 # sortie après 10 jobs
--kind | Capacité d'adaptateur | Adaptateurs locaux (extra workers) |
|---|---|---|
stt | stt | faster-whisper, ou EAI_STT_URL |
diarization | diarization | pyannote.audio (EAI_HF_TOKEN) |
tts | tts | piper (piper-tts), ou EAI_TTS_URL |
image_generation, image_edit | image_generation | point d'accès compatible OpenAI |
vision_understanding | vision | point d'accès compatible OpenAI, ou métadonnées et OCR locaux |
video_generation | video_generation | point d'accès de génération vidéo |
Au démarrage, le worker refuse de tourner (code de sortie 2, message explicite) si aucun adaptateur n'est
disponible pour l'un des --kind demandés. L'image deploy/docker/worker.Dockerfile installe l'extra
workers de services/control-plane/pyproject.toml (faster-whisper, pyannote.audio, piper-tts, silero-vad,
torch, diffusers ; le client NATS nats-py fait partie des dépendances de base) ; une installation qui échoue fait
échouer la construction de l'image.
Traitement d'un job
- Réservation : une seule requête dans le contexte système,
FOR UPDATE SKIP LOCKED, le jobqueuedle plus ancien des opérations demandées passe àrunning,adapter = worker:<hostname>,attempts + 1. Deux workers ne réservent jamais le même job. - Exécution dans une transaction limitée au tenant du job, avec le principal système de ce tenant : la RLS, l'événement d'usage FinOps et l'audit appartiennent au tenant du job.
- La charge utile de l'adaptateur est reconstruite par
adapter_payload()(moduleeai.domains.multimodal.service) à partir de l'entrée du job et du média déchiffré. Le chemin en ligne utilise la même fonction : les deux exécutions ne peuvent pas diverger. L'entrée du job ne contient jamais de secret ; un média fourni en base64 est stocké chiffré au moment de la mise en file. - Le post-traitement (DLP sur le texte extrait, stockage des médias produits) et
finish_jobsont ceux du chemin en ligne.
Erreurs : une AdapterError met le job en failed avec un message tronqué à 2000 caractères ; toute autre
exception le met en failed avec le code générique worker_error, et le journal ne garde que le type de
l'exception et sa position (jamais le contenu, qui peut contenir des données personnelles).
Arrêt : SIGTERM ou SIGINT arrête les réservations ; le job en cours se termine, un job réservé mais pas encore
démarré est rendu (queued), puis le processus sort avec le code 0.
Bail et remise en file
Pendant l'exécution, le worker rafraîchit updated_at du job (le bail) toutes les
EAI_MEDIA_WORKER_HEARTBEAT_SECONDS. Si le worker meurt entre la réservation et la fin, le job périodique
media_requeue de python -m eai.worker remet en file les jobs running réservés par un worker
(adapter commençant par worker:) dont updated_at dépasse EAI_MEDIA_JOB_LEASE_SECONDS, et les passe en
failed (lease_expired_max_attempts) quand attempts atteint EAI_MEDIA_JOB_MAX_ATTEMPTS. Chaque remise
en file est auditée (multimodal.job.requeue).
NATS JetStream et KEDA
Avec EAI_NATS_URL, la mise en file d'un job écrit aussi {job_id, tenant_id, operation} sur le sujet
multimodal.jobs.<operation> dans l'outbox transactionnelle (eai.event_outbox) : l'événement est validé avec
le job. Le job outbox de python -m eai.worker le relaie vers JetStream, qui préfixe le tenant : le sujet
réel est eai.<tenant_id>.multimodal.jobs.<operation>.
Flux à créer (une fois, par l'équipe plateforme) :
nats stream add EAI_MULTIMODAL --subjects 'eai.*.multimodal.jobs.>' --storage file --retention work \
--replicas 3 --discard old --max-age 7d
Le worker crée ou met à jour son consommateur durable --consumer sur --stream, filtré sur
eai.*.multimodal.jobs.<kind> pour chacun de ses --kind (accusé explicite, ack_wait égal au bail). Chaque
message est un réveil : le worker réserve et traite le job le plus ancien, puis accuse le message après le
traitement, ce qui donne à KEDA (ScaledObject nats-jetstream du chart, workers.items[].lagThreshold) un
retard qui compte les jobs non traités. Les permissions NATS de deploy/data/nats/values.yaml sont alignées :
l'utilisateur worker publie eai.*.multimodal.jobs.>, l'utilisateur multimodal s'y abonne.
Sans NATS (ou sans le client nats-py), le worker interroge la base toutes les EAI_MEDIA_WORKER_POLL_SECONDS.
Déduplication côté consommateur (inbox)
Chaque message reçu passe par la boîte de réception du consommateur (eai.inbox, table eai.inbox_events,
migration V166) avant son traitement. Il y est inscrit sous la clé unique (tenant, --consumer, identifiant du
message) ; l'identifiant est l'en-tête Nats-Msg-Id s'il existe, sinon <flux>:<séquence du flux>, identique à
chaque relivraison. Le tenant vient du sujet eai.<tenant_id>.<sujet> ou du champ tenant_id du message ; la ligne
est donc soumise à la RLS du tenant.
- Un message déjà traité (relivraison après un accusé perdu, publication en double) est accusé sans nouvelle réservation de job.
- Un traitement en échec passe la ligne en
failed(code d'erreur court, jamais le contenu), le message est refusé (nak) et la livraison suivante le rejoue (attempts+ 1). - Le contenu du message n'est jamais copié dans la table, seulement ses identifiants.
- Le job
inbox_purgedepython -m eai.workersupprime les lignesprocessedplus anciennes queEAI_INBOX_RETENTION_DAYS; les lignesreceivedetfailedrestent pour le rejeu et l'analyse.
Déploiement
- Helm :
workers.enabled: trueet une entrée par opération dansworkers.items(name,kind,consumer,gpus,minReplicas,maxReplicas,lagThreshold). Les pods lisent le ConfigMap et le Secret du Control Plane (base, clés de chiffrement, stockage objet, NATS) et gardent le durcissement : non root, système de fichiers racine en lecture seule,drop: [ALL], seccompRuntimeDefault, pas de jeton de compte de service. La politique réseaumultimodal-workersn'ouvre aucune entrée ; en sortie seulement PostgreSQL, NATS, le stockage objet (médias chiffrés), OpenBao, OTLP et le DNS du cluster. - Compose : service
media-workerdu profilgpu(docker compose --profile gpu up media-worker).
Paramètres
| Variable | Défaut | Rôle |
|---|---|---|
EAI_MEDIA_WORKER_POLL_SECONDS | 2 | intervalle d'interrogation de la base sans NATS |
EAI_MEDIA_WORKER_HEARTBEAT_SECONDS | 60 | rafraîchissement du bail pendant un job |
EAI_MEDIA_JOB_LEASE_SECONDS | 900 | âge de updated_at au-delà duquel un job running est remis en file |
EAI_MEDIA_JOB_MAX_ATTEMPTS | 3 | réservations avant l'échec définitif |
EAI_WORKER_MEDIA_REQUEUE_SECONDS | 60 | intervalle du job media_requeue de python -m eai.worker |
EAI_INBOX_RETENTION_DAYS | 7 | conservation des lignes traitées de eai.inbox_events |
EAI_WORKER_INBOX_PURGE_SECONDS | 3600 | intervalle du job inbox_purge de python -m eai.worker |
EAI_NATS_URL | (vide) | active les réveils JetStream |