TranscrIA

August 2, 2026 · View on GitHub

Statut : 🟱 Plan complet livrĂ© sur main (commits 6423fa1→e379e0f). CƓur + activation runtime + re-queue diffĂ©rĂ© (§7.2) + concurrence (v1.1) + idle-stop minimal (v1.2), testĂ©s (ruff/mypy/pytest, couverture ~77 %). Évolutions possibles : idle-stop par tĂąche de fond, relocalisation par dĂ©faut, profil d'install « nƓud seul » (cf. §10/§13). Auteur : Martossien Date : 2026-05-30 Objectif : lever l'asymĂ©trie de gestion VRAM entre le service maison et le STT vLLM, et formaliser les deux topologies de dĂ©ploiement (tout-en-un / frontale + ressources), pour faire passer TranscrIA d'un « clone » Ă  un produit auto-hĂ©bergeable professionnel. PrĂ©requis de lecture : MIGRATION_API_SERVEUR_GPU.md (plan de migration global).

Banc containerisĂ© (vLLM) : un dĂ©ploiement split entiĂšrement Docker — nƓud GPU servant diarisation + STT Cohere via vLLM + LLM d'arbitrage Qwen3.6-27B-FP8 via vLLM — est dĂ©crit dans PLAN_TEST_SPLIT_VLLM.md (docker-compose.split-gpu.yml, Dockerfile.resource-node) et rĂ©sumĂ© dans DOCKER.md.


État d'implĂ©mentation (v1)

BriqueModule / routeÉtat
Planificateur VRAM (fraction×total, place/relocate/busy)transcria/gpu/stt_vram_planner.py✅
Correctif allocator (pas de VRAM locale pour phase distante)transcria/workflow/runner.py✅
Superviseur cycle de vie A/B/Ctranscria/gpu/stt_engine_supervisor.py✅
DĂ©tection ressources + inventaireGET /capabilities (inference_service)✅
Auto-lancement STT à la demandePOST /engines/ensure✅
Admission §7.2 + prĂ©-voltranscria/inference/resource_gate.py, branchĂ© dans PipelineService.run_process ET dans la transcription rapide du rĂ©sumĂ© (runner._preflight_remote_stt) — sinon, sur un nƓud frais, le rĂ©sumĂ© tourne hors pipeline et le moteur STT n'est jamais lancĂ© (« connection refused »)✅
Panneau d'Ă©tat frontaleGET /api/resources/status + dashboard_status.html✅
Concurrence STT par tour (v1.1)inference.stt.concurrency (transcria/stt/transcription.py)✅
Re-queue diffĂ©rĂ© avec backoff (§7.2)QueueStore.requeue_later + job_executor (scheduled_at)✅
Idle-stop moteurs externes (v1.2)SttEngineSupervisor.reap_idle (opportuniste via /capabilities)✅ (minimal)
Admission VRAM distante (backpressure)remote_vram_admits — donnĂ©es GPU manquantes ⇒ None (dĂ©fĂ©rer au prĂ©-vol, pas de famine) + log throttlĂ© scheduler✅
Check d'install GPU du nƓuddoctor.check_inference_node_gpus (nƓud joignable doit Ă©numĂ©rer ses GPU via /capabilities)✅
LLM d'arbitrage DISTANTE (cycle de vie non gĂ©rĂ© localement)vram_manager._is_remote_arbitrage() honore l'endpoint rĂ©solu par opencode_setup.resolve_arbitrage_endpoint (env TRANSCRIA_ARBITRAGE_LLM_HOST > services.arbitrage_llm_host > 127.0.0.1), source unique partagĂ©e avec le provider opencode ; sondes + admission, ne lance/arrĂȘte jamais une LLM distante✅ (2026-06-23)
Diarisation : placement auto (carte la plus libre, plus de cuda:0 figĂ©)diarization.py → squim_scorer.pick_device au chargement (diarization.device: auto)✅ (2026-06-23)
resource_node.engines[].gpu_mem appliquĂ© au lancement (pas seulement Ă  l'admission)make_script_launcher transmet STT_GPU_MEM✅ (2026-06-23)
Banc split entiĂšrement containerisĂ© (vLLM) — validĂ© E2E fichier son rĂ©eldocker-compose.split-gpu.yml, Dockerfile.{worker,resource-node}, scripts/launch_arbitrage_vllm.sh, scripts/verify_split_topology.py — cf. PLAN_TEST_SPLIT_VLLM.md✅ (2026-06-23)
CapacitĂ© d'admission du nƓud configurable + concurrence validĂ©e sous chargeresource_node.max_concurrent_jobs annoncĂ© dans /capabilities ; available_remote_slots = min(node_max, stt_slots) (moteurs sĂ©rialisĂ©s exclus) ; verrou LLM no-op si distant. Split robuste jusqu'Ă  8 jobs, sweet spot ≈4 — cf. PLAN_TEST_CHARGE.md✅ (2026-06-23)

Validation E2E (2026-06-23, 8× RTX 3090) : frontale CPU → STT Cohere (vLLM) + diarisation (pyannote, auto-placĂ©e) + LLM Qwen3.6-27B-FP8 (vLLM, TP=4, FP8 Marlin) tout en distant, pipeline complet produisant SRT/ZIP/DOCX. 14 correctifs (cf. PLAN_TEST_SPLIT_VLLM.md § journal).


0. Résumé exécutif

Les adaptateurs distants existent et sont validés E2E (STT, diarisation, voice-embed, avec LLM d'arbitrage). Reste un manque évident : la gestion de la VRAM n'est pas symétrique.

RessourceGestion VRAM aujourd'hui
Service Flask inference_service (diarize / voice-embed)Autonome — A/B/C in-process (charge Ă  la demande, 503 si saturĂ©, dĂ©chargement idle)
LLM d'arbitrage (llama.cpp)GĂ©rĂ© par la frontale — VRAMManager + arbitrage_script/stop_script (CAS A/B/C)
STT via vLLM (cohere/whisper)Statique — serveurs rĂ©sidents lancĂ©s Ă  la main, aucun arbitrage VRAM

La cible : donner au STT vLLM la mĂȘme autonomie, en Ă©tendant un pattern qui existe dĂ©jĂ  (celui de la LLM d'arbitrage), sans construire d'orchestrateur de process complexe.

Principe directeur : l'admin dĂ©cide du placement (quels moteurs, quels GPU) ; le service dĂ©cide du quand (dĂ©marrage Ă  la demande, rĂ©utilisation, arrĂȘt sur idle, contention). Le code n'est jamais intrusif sur le placement.


1. Les deux topologies de déploiement

TOUT-EN-UN (une machine)                  SPLIT (frontale + ressources)
┌──────────────────────────────┐          ┌─────────────────┐   HTTP   ┌──────────────────────────┐
│ TranscrIA (web, DB, workflow, │          │ TranscrIA        │ ───────â–ș │ NƓud ressources           │
│ calendrier, lexique, exports) │          │ FRONTALE         │          │ ‱ service ressources      │
│ + ressources GPU locales      │          │ (CPU, pas de     │ ◄─────── │ ‱ vLLM STT (cohere/whisper)│
│   (vLLM, llama.cpp, Flask)    │          │  modĂšle chargĂ©)  │  status  │ ‱ llama.cpp (arbitrage)   │
└──────────────────────────────┘          └─────────────────┘          │ ‱ Flask (diarize/v-embed) │
                                                                         └──────────────────────────┘
Tout-en-unSplit
Frontaleweb, DB, calendrier, workflow, lexique, participants, exportsidem (le calendrier reste toujours ici)
RessourcesmĂȘmes process, sur la mĂȘme machinesur une (ou des) machine(s) dĂ©diĂ©e(s)
Niveaugrand public / mono-posteadmin systÚme (assumé : doc claire, pas de « clic-bouton »)
Qui lance les moteursle service local (Ă  la demande, A/B/C)l'admin dĂ©clare ; le service du nƓud gĂšre le cycle de vie

Le calendrier / la planification sont de la logique métier : ils restent cÎté frontale dans les deux cas.


1 bis. All-in-one = son propre nƓud de ressources (0.3.6)

Depuis les runtimes STT servis (audio.cpp/parakeet.cpp — docs/EXTERNAL_STT_RUNTIMES.md), la section resource_node.engines[] vaut AUSSI pour l'all-in-one : une URL de backend loopback + un moteur homonyme dĂ©clarĂ© ⇒ le prĂ©-vol des jobs lance le moteur en process (resource_gate._ensure_local_served_stt → build_stt_supervisor().ensure_ready(), mĂȘme cycle A/B/C que /engines/ensure) — aucun nƓud de contrĂŽle requis, aucun faux WARN doctor. Le chemin split est INCHANGÉ : dĂšs qu'un inference.url/inference.nodes existe, il prime (le nƓud assure ses moteurs lui-mĂȘme). SantĂ© par moteur : health_path (dĂ©faut /v1/models) et health_mode (http_2xx dĂ©faut ; http_any = toute rĂ©ponse HTTP prouve la vie, rĂ©servĂ© aux serveurs mono-modĂšle qui chargent leurs poids AVANT de binder — jamais pour un vLLM).

2. Placement (admin) vs cycle de vie (service)

C'est le point qui garantit la non-intrusivité.

2.1 Placement = l'admin

  • Quels moteurs, sur quels GPU, combien d'instances. DĂ©clarĂ© via les scripts/launch_stt_*.sh (+ launch_arbitrage.sh) et un manifeste lu par le service (cf. §6).
  • L'admin peut partager une grosse carte entre plusieurs instances (mĂȘme STT_GPU, ports distincts, STT_GPU_MEM rĂ©duit pour chacune) ou rĂ©partir sur plusieurs cartes. Les scripts le permettent dĂ©jĂ . Le code n'impose ni ne réécrit ce choix.

2.2 Cycle de vie = le service (configurable)

À partir de ce que l'admin a dĂ©clarĂ©, le service peut :

  • CAS A — moteur dĂ©jĂ  up et sain → rĂ©utilise directement ;
  • CAS B — moteur dĂ©clarĂ© mais Ă©teint, VRAM disponible → le dĂ©marre (via son script) puis sert ;
  • CAS C — VRAM saturĂ©e → 503 + Retry-After (la frontale re-queue), avec relocalisation optionnelle avant d'abandonner (cf. §4) ;
  • idle-stop — arrĂȘte un moteur inactif depuis N secondes (opt-in, off par dĂ©faut, cf. §3).

C'est exactement le pattern déjà utilisé pour la LLM d'arbitrage (VRAMManager + scripts), généralisé aux moteurs STT vLLM. On ne réinvente rien.


3. Idle-stop : pourquoi off par défaut

Type de modÚleDécharger sur idle ?
In-process (service Flask)Oui, dĂ©jĂ  le cas (idle_timeout_s) — charge/dĂ©charge en VRAM, peu coĂ»teux
Serveur externe (vLLM, llama.cpp)Opt-in, off par défaut

ArrĂȘter un serveur vLLM externe = tuer le process → on perd le cache chaud et le redĂ©marrage coĂ»te 25–105 s (compile JIT FlashInfer). Donc :

  • dĂ©faut : moteurs STT rĂ©sidents (rĂ©activitĂ© maximale) ;
  • l'idle-stop ne se justifie que sous contention VRAM → c'est le rĂŽle du CAS C, pas d'un timer systĂ©matique. Opt-in par moteur (idle_timeout_s > 0).

ImplĂ©mentĂ© (v1.2, minimal) : SttEngineSupervisor.reap_idle() arrĂȘte un moteur dĂ©clarĂ© avec idle_timeout_s > 0, up, et dont le dernier ensure_ready dĂ©passe le timeout. DĂ©clenchĂ© de façon opportuniste (poll /capabilities ~10 s + chaque ensure_ready), sans tĂąche de fond. Non intrusif : ne touche que les moteurs qu'on a nous-mĂȘmes servis (_last_used). Évolution possible : reaper en tĂąche de fond ou dĂ©clenchement sous contention CAS C.


4. Gestion VRAM au lancement : deux niveaux

Niveau 1 — prĂ©-check (toujours actif)

Avant de démarrer un moteur sur le GPU assigné : lire la VRAM libre (nvidia-smi) et refuser proprement (503 / message clair) si ça ne tient pas, au lieu de laisser le process OOM-crasher. ~20 lignes ; c'est l'essentiel du bénéfice « éviter un crash ».

Niveau 2 — relocalisation auto (le « plus » pro)

Si le GPU assignĂ© ne tient pas : parcourir les autres GPU, prendre le premier oĂč ça rentre, surcharger le placement (STT_GPU) et lancer lĂ .

  • Log bruyant systĂ©matique (« GPU 3 plein → repli sur GPU 5 ») — filet de sĂ©curitĂ©, pas de magie.
  • RĂ©utilise le verrou existant du VRAMManager pour Ă©viter que deux lancements concurrents visent le mĂȘme GPU.
  • S'enchaĂźne sur le CAS C : avant de renvoyer 503, on tente une relocalisation si activĂ©e.

⚠ SĂ©mantique VRAM spĂ©cifique Ă  vLLM (Ă  ne pas oublier)

vLLM réserve une fraction de la VRAM totale de la carte (--gpu-memory-utilization 0.85), pas la taille du modÚle. Donc :

« ça rentre »  ⇔  VRAM_libre ≄ fraction × VRAM_totale     (et NON ≄ taille_modĂšle)

Conséquences :

  • packer plusieurs instances sur une carte impose de baisser la fraction de chacune (c'est Ă  l'admin) ;
  • le calcul de relocalisation/prĂ©-check doit raisonner en fraction × total, pas en taille de modĂšle ;
  • contrepartie positive : cette rĂ©servation alimente le batching continu de vLLM → une mĂȘme instance peut servir plusieurs requĂȘtes concomitantes.

5. Concurrence — implĂ©mentĂ©e et validĂ©e sous charge (2026-06-23)

Deux axes de concurrence, tous deux exploitant le batching continu de vLLM :

Inter-jobs (admission) — le scheduler lance jusqu'Ă  workflow.execution.max_concurrent_jobs pipelines en parallĂšle (pool bornĂ© 1-8), plafonnĂ© par la capacitĂ© d'admission du nƓud resource_node.max_concurrent_jobs (annoncĂ©e dans /capabilities, dĂ©faut 1). Les moteurs in-process sĂ©rialisĂ©s (diarisation/voice-embed) ne bornent plus l'admission : ils s'auto-sĂ©rialisent via leur verrou moteur (les jobs en surplus y font la queue), tandis que la LLM d'arbitrage (vLLM) batche les requĂȘtes concurrentes — pour le STT servi, cela dĂ©pend du runtime : vLLM batche, audiocpp_server SÉRIALISE (cf. §5 intra-job, multi-instance). Le verrou LLM de l'allocator est no-op pour une LLM distante (elle batche seule ; le sĂ©rialiser l'Ă©tranglerait).

Intra-job (STT par tour) — inference.stt.concurrency (>1) transcrit les tours de parole d'un mĂȘme job en parallĂšle (ThreadPoolExecutor, backends concurrent_safe).

⚠ Le batching dĂ©pend du runtime serveur : vLLM batche les requĂȘtes concurrentes, mais audiocpp_server (qwen3asr/nemotron servis) les sĂ©rialise sous un mutex global par modĂšle (mesurĂ© : 2 requĂȘtes concurrentes = 2× le temps d'une seule). Sur audio.cpp, monter concurrency seul ne gagne que le recouvrement I/O ; le parallĂ©lisme rĂ©el s'obtient en multi-instance :

resource_node:
  engines:
  - {name: qwen3asr, script: scripts/launch_stt_qwen3asr.sh, gpu: 1, port: 8021}
  - {name: qwen3asr-gpu0, backend: qwen3asr,          # 2e instance du MÊME backend
     script: scripts/launch_stt_qwen3asr.sh, gpu: 0, port: 8022}
inference:
  stt:
    concurrency: 4                                    # ≄ nombre d'instances
    backends:
      qwen3asr:
        url: http://127.0.0.1:8021/v1
        extra_urls: [http://127.0.0.1:8022/v1]

« Assurer qwen3asr » (prĂ©-vol all-in-one comme /engines/ensure en split) dĂ©marre toutes les instances ; la premiĂšre porte le verdict, les autres sont best-effort. CĂŽtĂ© client, chaque worker STT garde son instance (affinitĂ©) et bascule sur les vivantes si la sienne tombe. Mono-GPU : laisser une seule instance (dĂ©faut inchangĂ©) — deux instances sur la mĂȘme carte se partagent le mĂȘme moteur de calcul, gain Ă  mesurer avant d'en mettre.

Validation (test de charge, docs/archive/PLAN_TEST_CHARGE.md, 8×RTX 3090) : split robuste jusqu'Ă  8 jobs concurrents (0 Ă©chec serveur), dĂ©bit qui scale 1→4 puis le LLM 27B sature (compute-bound, GPU Ă  100 %). Sweet spot ≈ 4 sur ce matĂ©riel : rĂ©gler max_concurrent_jobs (frontale et nƓud) au sweet spot ⇒ le surplus attend en file (claim atomique, aucun perdu) plutĂŽt que de thrasher la LLM — meilleure latence ET dĂ©bit qu'en sur-souscription. La sur-charge dĂ©grade en douceur (ralentit), ne plante pas.


6. Le service de ressources

Candidat : inference_service Flask Ă©tendu (il fait dĂ©jĂ  l'A/B/C in-process pour diarize/voice-embed) — pas de nouveau service Ă  maintenir.

Responsabilités ajoutées :

  1. Détection au démarrage : énumÚre GPU, VRAM libre, modÚles présents localement, moteurs déclarés dans le manifeste.
  2. GET /capabilities : ce que le nƓud peut servir (moteurs, modùles, GPU, fraction VRAM).
  3. GET /health : Ă©tat temps rĂ©el (moteurs up/down, VRAM, CAS A/B/C courant) — interrogeable par la frontale sans auth (supervision).
  4. Cycle de vie des moteurs déclarés (CAS A/B/C, pré-check, relocalisation opt-in, idle-stop opt-in).
  5. Pas d'UI : le nƓud ressources reste mince ; l'affichage est sur la frontale (§7).
inference_service (étendu)
├── /health         ← feu vert/rouge par moteur, VRAM         (libre)
├── /ready          ← prĂȘt Ă  servir (200 mĂȘme modĂšles dĂ©chargĂ©s — CAS B = chargeable) (libre)
├── /capabilities   ← inventaire ressources & moteurs          (libre)
├── /infer/diarize        (existant)
├── /infer/voice-embed    (existant)
└── superviseur VRAM  ── pilote launch_stt_*.sh / stop_stt.sh (placement admin respectĂ©)

7. Visibilité & résilience cÎté frontale

La frontale interroge périodiquement /health + /capabilities et affiche :

  • le mode de dĂ©ploiement (tout-en-un / frontale+ressources) ;
  • un feu vert/rouge par moteur : STT cohere, STT whisper, LLM arbitrage, service diarize/voice-embed ;
  • VRAM / activitĂ© par GPU.
┌─ État des ressources ───────────────────────────┐
│ Mode : frontale + ressources (192.168.1.59)      │
│  ● STT cohere      up   GPU3  3.9/24 GiB         │
│  ● STT whisper     up   GPU5  2.9/24 GiB         │
│  ● LLM arbitrage   up   GPU0                      │
│  ● diarize/v-embed up   GPU6  (idle, dĂ©chargĂ©)   │
└──────────────────────────────────────────────────┘

7.1 Politique de polling

  • FrĂ©quence : /health toutes les ~10 s (lĂ©ger, sans auth) ; /capabilities Ă  la connexion + au changement d'Ă©tat.
  • Timeout court (~3 s) ; au-delĂ , le moteur/nƓud est marquĂ© rouge dans le panneau.
  • Le polling est best-effort : il alimente l'affichage, il ne bloque jamais le rendu de l'UI.

7.2 IndisponibilitĂ© des ressources (mode dĂ©gradĂ©) — dĂ©cidĂ©

Scénario probable en split (réseau, redémarrage, crash GPU). Politique explicite :

SituationComportement
Indispo transitoire (503 / timeout ponctuel)re-queue diffĂ©rĂ© (implĂ©mentĂ© : QueueStore.requeue_later + scheduled_at) — le job attend puis re-tente, il n'Ă©choue pas
VRAM locale insuffisante pour une phase GPU (STT/transcription/diarisation/locuteurs)mise en attente waiting_vram (statut d'exĂ©cution non terminal), pas de FAILED : le job re-queue et reprend automatiquement dĂšs libĂ©ration de la VRAM. L'admin est alertĂ© une seule fois par Ă©pisode (e-mail + log WARNING + bandeau in-app). TranscrIA ne tue jamais un process GPU tiers (force_free_gpu reste bridĂ© aux kill_patterns dans la fenĂȘtre calendaire). CĂŽtĂ© rĂ©sumĂ© synchrone, le client relance /summary automatiquement. Voir le dĂ©tail §7.2-bis ci-dessous.
Indispo prolongĂ©e (nƓud rouge)nouvelles transcriptions acceptĂ©es mais mises en file (jamais perdues), statut clair « ressources indisponibles » + notification ; on ne bloque pas la soumission et on ne boucle pas indĂ©finiment en silence
FenĂȘtre de retry dĂ©passĂ©e (max_unavailable_s, configurable)le job est marquĂ© Ă©chec avec raison explicite (pas de crash, pas de blocage)
fallback_local actif et GPU local prĂ©sentbascule locale possible ; en frontale CPU-only, pas de fallback → file + notification est la seule issue saine

Principe : jamais d'Ă©chec silencieux ni de spin infini. Le job est soit en file (visible), soit en attente VRAM (visible, admin alertĂ©), soit en Ă©chec explicite aprĂšs une fenĂȘtre bornĂ©e.

7.2-bis — Attente de VRAM locale (mĂ©canique)

Une VRAM insuffisante est traitée comme une indisponibilité transitoire, jamais comme un échec :

  • DĂ©tection : les phases GPU (WorkflowRunner.run_transcription / run_diarization / run_speaker_detection, et _run_quick_transcription pour le rĂ©sumĂ©) renvoient un signal {"vram_wait": True, "required_mb", "phase", "reason"} au lieu d'appeler update_state(FAILED).
  • File principale : PipelineService._run_pipeline_steps propage vram_wait ; JobExecutorService._run_process re-queue via QueueStore.requeue_later + marque mark_execution_waiting_vram. Le scheduler (_resources_available) garde le job en attente tant que GPUAllocator.can_allocate Ă©choue, puis le redispatche → reprise automatique.
  • Pipeline reprenable + admission par phases restantes : le pipeline saute les phases dĂ©jĂ  faites au redispatch (extra_data.pipeline.completed_phases, cf. docs/PIPELINE_REPRISE.md) → un re-queue ne refait pas le STT. Et l'admission n'exige que la VRAM des phases restantes (_done_profile_phases → _local_required_mb exclut les phases faites) : un job oĂč il ne reste que la correction exige la VRAM LLM, pas le STT. C'est ce qui rĂ©sout « par construction » le cas d'un STT bloquĂ© qui boucle, et permet Ă  run_correction de renvoyer simplement vram_wait.
  • Frontal role=web sans GPU (split) : aucune phase GPU n'est exĂ©cutĂ©e sur le frontal. Les Ă©tapes GPU synchrones du wizard — rĂ©sumĂ© (api_summary) et dĂ©tection de locuteurs (api_speakers_detect) — sont enfilĂ©es sur le worker GPU (modes de file summary/speakers, JobExecutorService.STEP_MODES) ; le client poll GET /status et la page se rafraĂźchit. Le frontal ne fait qu'orchestrer ; toute la charge GPU (STT, diarisation, dĂ©tection, LLM comprise) est portĂ©e par la machine GPU (worker/nƓud de ressources). La dĂ©cision repose sur le rĂŽle, pas sur une dĂ©tection matĂ©rielle (un Ă©ventuel petit GPU frontal est ignorĂ©). Rappel : la LLM d'arbitrage est locale au worker, pas servie par le nƓud de ressources — donc le worker doit avoir un GPU. Une LLM 35B en CPU est inexploitable (≈100-300× plus lente).
  • Fichiers de jobs en split (frontale ≠ machine worker) : les deux tiers partagent la base mais PAS le disque — l'audio uploadĂ© sur la frontale, le contexte (invitation/lexique/mapping) et les artefacts produits par le worker (SRT, qualitĂ©, clips, rĂ©sumĂ©) doivent circuler. Solution intĂ©grĂ©e : storage.shared_backend: pg — les fichiers sont rĂ©pliquĂ©s via PostgreSQL (push Ă  l'upload/ enfilage et Ă  chaque checkpoint de phase, matĂ©rialisation paresseuse cĂŽtĂ© frontale, intĂ©gritĂ© sha256, purge de l'audio aux Ă©tats terminaux). Le nƓud de ressources, lui, ne stocke jamais de fichier utilisateur (audio reçu par upload HTTP en fichier temporaire, supprimĂ© Ă  la fin de la requĂȘte). DĂ©tails : docs/STOCKAGE_PARTAGE_JOBS.md ; garde-fou : check « Stockage des fichiers de jobs (split) » du doctor.
  • RĂ©sumĂ© synchrone (api_summary, role=all) : la 1Êłá”‰ tentative reste synchrone (UX immĂ©diate). Sur vram_wait, l'Ă©tat prĂ©-rĂ©sumĂ© est restaurĂ©, le job passe waiting_vram, et une reprise serveur est enfilĂ©e (submit_process(mode="summary"), profil VRAM summary_stt). Le scheduler relance alors run_summary via _run_process dĂšs que l'admission VRAM le permet — mĂȘme sans page ouverte. _run_process traite ce mode Ă  part : run_summary gĂšre l'Ă©tat (SUMMARY_DONE/FAILED), l'exĂ©cuteur libĂšre seulement la file (pas de COMPLETED ni d'e-mail propriĂ©taire). Le wizard (wizard.js) ne relance plus /summary ; il poll GET /status et recharge Ă  summary_done (zĂ©ro double-exĂ©cution : api_summary refuse une relance synchrone tant qu'une entrĂ©e summary est active).
  • Alerte admin (une fois par Ă©pisode) : transcria/notifications/admin_alerts.alert_admin_vram_wait → e-mail aux comptes ADMIN actifs (send_admin_vram_alert_async) + log WARNING structurĂ©. L'anti-spam repose sur un drapeau persistant extra_data.vram_alert_sent, rĂ©armĂ© uniquement aux transitions terminales (completed/failed/cancelled) — pas Ă  chaque re-dispatch.
  • Bandeau in-app : base.html affiche le nombre de jobs en attente (JobStore.count_waiting_vram) aux administrateurs, via le context processor inject_vram_waiting_count.
  • DĂ©blocage par arrĂȘt de NOTRE LLM inactive (catĂ©gorie 1, Ă  deux niveaux) : si un STT/diarisation manque de VRAM parce que la LLM d'arbitrage chaude (souvent Ă©talĂ©e sur tous les GPU via --tensor-split) la dĂ©tient encore, l'attente serait sans fin (rien ne la libĂšre). On arrĂȘte alors proprement notre LLM d'arbitrage si elle est inactive (verrou LLM libre = aucun job ne s'en sert ; stop_arbitrage_llm, relancĂ©e Ă  la phase de correction) — helper partagĂ© transcria/gpu/vram_reclaim.stop_idle_arbitrage_llm. Deux points de dĂ©clenchement :
    • en cours de phase (WorkflowRunner, sur GPUSessionError) puis re-rĂ©servation ;
    • Ă  l'admission du scheduler (_resources_available) avant dispatch — indispensable, car sinon un job en file resterait waiting indĂ©finiment derriĂšre notre propre LLM (le reclaim de phase ne tourne jamais tant que le job n'est pas dispatchĂ©). On ne stoppe jamais la LLM « pour la phase LLM elle-mĂȘme » : la phase llm_arbitration est dĂ©jĂ  ignorĂ©e Ă  l'admission quand la LLM est partagĂ©e (llm_shared). Le besoin dĂ©clencheur est toujours une phase non-LLM. C'est notre process gĂ©rĂ©, jamais un tiers ; indĂ©pendant du calendrier.
  • VRAM de la LLM d'arbitrage = besoin MULTI-GPU (audit du 11/06/2026) : la LLM (ex. 35B Q8 ≈ 60 Go) s'Ă©tale sur plusieurs cartes via son script de lancement (CUDA_VISIBLE_DEVICES + --tensor-split) — son besoin ne tient JAMAIS sur un seul GPU. L'ancien modĂšle (rĂ©servation mono-GPU de llm_vram_mb) Ă©tait insatisfaisable par construction : code mort tant que la LLM tournait, et deadlock vram_wait dĂšs qu'il fallait la relancer (aprĂšs un reclaim, un crash, ou en lancement Ă  la demande). De plus le drapeau stockĂ© llm_shared Ă©tait inconditionnellement vrai → l'admission ne vĂ©rifiait jamais la LLM. Nouveau modĂšle :
    • gpu.llm_vram_mb = empreinte totale, gpu.llm_gpu_indices = cartes du script (dĂ©faut : tous les GPU visibles) ; le besoin par carte = total Ă· nb de cartes ;
    • GPUAllocator.can_host_llm (lecture) et try_reserve_llm (rĂ©servation tout-ou-rien, une part par carte, libĂ©rĂ©e d'un bloc par release_phase) ;
    • phases summary_llm et llm_arbitration du runner : rĂ©servation multi-GPU ;
    • admission (_llm_admissible) sur la vĂ©ritĂ© vivante : LLM en marche → rĂ©ellement partagĂ©e (rien Ă  exiger) ; Ă©teinte → can_host_llm requis ; le max mono-GPU (_local_required_mb) ne compte plus que les phases NON-LLM (STT/diarisation). À recalibrer (llm_vram_mb + llm_gpu_indices) Ă  chaque changement de modĂšle/script.
  • Parc hĂ©tĂ©rogĂšne (cartes de 2 Ă  64 Go) : aucune taille n'est supposĂ©e — la mesure rĂ©elle par carte (nvidia-smi) est l'autoritĂ©, les besoins par moteur sont des rĂ©glages. En plus : gpu.llm_vram_mb_per_gpu dĂ©clare une part inĂ©gale par carte (--tensor-split 3,1 sur 24+8 Go) ; la sĂ©lection de GPU des petites phases prĂ©fĂšre les cartes hors placement LLM (Ă  disponibilitĂ© suffisante) pour ne pas bloquer la relance de la LLM ; et gpu.min_free_vram_mb (marge par carte) est Ă  rĂ©duire sur les petites cartes.
  • Politique gpu.preemption (rĂ©glable dans /admin/config → « Ressources GPU ») :
    • own-only (dĂ©faut, infra partagĂ©e) : catĂ©gorie 1 seulement (nos process trackĂ©s inactifs).
    • aggressive : autorise en plus la prĂ©emption de serveurs d'infĂ©rence tiers (kill_patterns, process non trackĂ©s via force_free_gpu), uniquement dans la fenĂȘtre calendaire force_gpu. À rĂ©server Ă  un GPU dĂ©diĂ© Ă  TranscrIA. La distinction « Ă  nous / tiers » s'appuie sur GPUAllocator._tracked_pids (PID que nous avons lancĂ©s).

En tout-en-un, le service ressources est local : une indisponibilitĂ© = crash process, restaurĂ© par systemd en quelques secondes. Seule la 1Êłá”‰ ligne (transitoire) s'applique alors — le job patiente via re-queue le temps du redĂ©marrage. Les cas « prolongĂ© » et « fenĂȘtre dĂ©passĂ©e » sont des prĂ©occupations de la topologie split (rĂ©seau, nƓud distant, crash GPU durable).


8. Configuration (esquisse)

deployment:
  mode: all_in_one          # all_in_one | frontale | resource_node

inference:
  mode: remote              # local | remote | hybrid (existant)
  url: "http://192.168.1.59:8002"     # service Flask ressources
  transport: { audio: upload }        # OBLIGATOIRE en distant (cf. §9)
  resilience: { timeout_s: 1800, retries: 2, max_unavailable_s: 600 }  # cf. §7.2 (mode dégradé)
  stt:
    backends:
      cohere:  { url: "http://192.168.1.59:8003/v1", model: cohere-transcribe,  response_format: json }
      whisper: { url: "http://192.168.1.59:8005/v1", model: whisper-large-v3, response_format: verbose_json }

# CĂŽtĂ© nƓud ressources uniquement : manifeste des moteurs gĂ©rĂ©s.
resource_node:
  vram:
    preflight: true         # niveau 1 — toujours
    auto_relocate: true     # niveau 2 — repli GPU si saturĂ© (log bruyant)
  engines:
    - name: cohere   ; script: scripts/launch_stt_cohere.sh  ; gpu: 3 ; gpu_mem: 0.85 ; idle_timeout_s: 0
    - name: whisper  ; script: scripts/launch_stt_whisper.sh ; gpu: 5 ; gpu_mem: 0.85 ; idle_timeout_s: 0

idle_timeout_s: 0 = résident (défaut). gpu/gpu_mem = placement admin, jamais réécrit (seule la relocalisation peut surcharger gpu, et seulement si auto_relocate: true).


9. Rappels & correctifs liés

  • transport.audio: upload obligatoire en distant. file_ref envoie un chemin que le nƓud distant ne peut pas rĂ©soudre (filesystem non partagĂ©). DĂ©montrĂ© par les tests d'intĂ©gration.
  • Correctif allocator (bug silencieux, Ă  prioriser) : en mode distant, try_reserve(job_id, phase, 
) rĂ©serve quand mĂȘme de la VRAM pour les phases stt/diarization alors que rien ne se charge localement (observĂ© : phase=stt gpu=5 vram=6000 pendant un run 100 % distant). Impact : fausse contention VRAM → OOM possible ou rejets Ă  tort de tĂąches locales ; et incohĂ©rence sur une frontale CPU-only (rĂ©server une VRAM qui n'existe pas). Correction : ne pas rĂ©server de VRAM locale pour une phase servie Ă  distance (WorkflowRunner._phase_runs_remotely → _reserve_gpu_phase retourne une rĂ©servation Ă  0 VRAM). Toutes les phases sont couvertes : run_transcription (stt, via _reserve_gpu_phase), _run_quick_transcription (summary_stt) et run_diarization (diarization) sautent toute rĂ©servation/_gpu_session locale quand la phase est servie Ă  distance (sinon rĂ©servation fantĂŽme — 6000 Mo STT, 2000 Mo diarisation — et attente VRAM Ă  tort sur un tier sans GPU). La dĂ©tection de locuteurs du rĂ©sumĂ© (pyannote) reste toujours locale (jamais dĂ©lĂ©guĂ©e).
  • SĂ©curitĂ© rĂ©seau : clĂ© API partagĂ©e dĂ©jĂ  en place (Flask enforce_api_key ; vLLM --api-key). Un 401 est dĂ©finitif (pas de retry ni de bascule locale) — testĂ©.
  • ObservabilitĂ© du lancement LLM d'arbitrage : VRAMManager.launch_arbitrage_llm (et ScriptLLMBackend.ensure_available) capturent la sortie du script dans services.arbitrage_log_path (dĂ©faut /tmp/arbitrage_llm_<port>.log, comme le superviseur STT le fait dĂ©jĂ  via stt_<name>_<port>.log). L'attente du port dĂ©tecte la mort prĂ©coce du process (proc.poll()) et abandonne sans attendre les 600 s ; en cas d'Ă©chec (mort prĂ©coce ou timeout), le code de sortie et les derniĂšres lignes du log sont Ă©crits en ERROR. Sans cela, un dĂ©marrage ratĂ© (binaire absent, OOM, --tensor-split ≠ nb GPUs) restait invisible — seul subsistait le placeholder « RĂ©sumĂ© indisponible (LLM non configurĂ©e) ». Cf. dĂ©pannage INSTALL.md §12.

10. Déploiement sur l'autre machine

  • Installation : un profil dĂ©diĂ© resource-node existe dĂ©sormais (nƓud ressources seul, sans frontale web ni base applicative) — ./install.sh --profile resource-node (gĂ©nĂšre le manifeste resource_node.engines, configure la clĂ© API, n'installe pas de base). En conteneur : image GPU + python -m transcria.deploy.entrypoint resource-node (cf. DOCKER.md). DĂ©pendances : pyannote, librosa/soundfile, ffmpeg, llama.cpp (cf. INSTALL.md).
  • ParamĂštres exposĂ©s cĂŽtĂ© nƓud (manifeste §8, ports, fractions VRAM, clĂ© API).
  • DĂ©tection ressources : GPU, VRAM, modĂšles prĂ©sents — au dĂ©marrage + via /capabilities.
  • RĂ©seau : bind 0.0.0.0, ports (service 8002, STT 8003/8005/8007, arbitrage 8080), pare-feu.
  • Supervision / redĂ©marrage : dĂ©cidĂ© → units systemd pour tous les serveurs persistants (vLLM, llama.cpp, service Flask), avec Restart=on-failure. C'est la rĂ©ponse v1 au « qui redĂ©marre un moteur tombĂ© ». Un agent de redĂ©marrage interne au service reste une option v2 si besoin.

11. Arbitrages (pistes écartées)

PisteDécisionRaison
B — STT dans le service Flask in-process (load/offload via transformers)❌ Ă©cartĂ©eperd le dĂ©bit et le batching continu de vLLM
Superviseur de process complet (supervision fine, redĂ©marrages, arbitrage hĂ©tĂ©rogĂšne)❌ Ă©cartĂ©e (v1)usine Ă  gaz, fragile ; on Ă©tend l'existant Ă  la place
A — Ă©tendre le pattern arbitrage-LLM aux STT vLLM✅ retenuerĂ©utilise VRAMManager + scripts, incrĂ©mental, non intrusif

12. Plan d'implémentation (incrémental)

État (commits 6423fa1→e379e0f) : tout le plan est livrĂ©. items 1-6 ✅, re-queue diffĂ©rĂ© §7.2 ✅, relocalisation auto (7) cĂąblĂ©e opt-in ✅, idle-stop (8) ✅ en version minimale (rĂ©clamation opportuniste via /capabilities, sans tĂąche de fond).

v1 (cƓur)

  1. PrĂ©-check VRAM (niveau 1) au lancement des moteurs STT — transforme l'OOM en 503 clair.
  2. Correctif allocator (bug silencieux §9) : pas de rĂ©servation VRAM locale pour une phase distante — remontĂ© car corruption de comptabilitĂ© VRAM, indĂ©pendant et peu risquĂ©.
  3. Cycle de vie STT (CAS A/B/C) via scripts + VRAMManager, calqué sur l'arbitrage LLM.
  4. /capabilities + détection ressources au démarrage du service.
  5. Panneau d'état frontale (mode + feu vert) + mode dégradé (§7.2).

v1.1 (gain UX immĂ©diat) 6. Concurrence bornĂ©e du STT par tour (§5) — latence la plus visible cĂŽtĂ© utilisateur.

v1.2 (confort) 7. Relocalisation auto (niveau 2) opt-in + log bruyant. 8. idle-stop opt-in par moteur.


13. Risques & points ouverts

  • SĂ©mantique fraction-de-total de vLLM (§4) : bien la coder dans le prĂ©-check/relocalisation.
  • Courses au dĂ©marrage concurrent → verrou VRAMManager (dĂ©jĂ  prĂ©sent) Ă  rĂ©utiliser strictement.
  • Cold start (25–105 s) sur CAS B / relocalisation : la frontale gĂšre l'attente via re-queue diffĂ©rĂ© (requeue_later + scheduled_at, implĂ©mentĂ© §7.2).
  • En split, redĂ©marrage d'un moteur tombĂ© : dĂ©cidĂ© → systemd Restart=on-failure en v1 (§10) ; agent interne au service en option v2.