Aller au contenu principal

Paralléliser sans le programmer

Déclarer une dépendance, c'est déclarer une liberté pour tout le reste.

L'épisode précédent a nommé DAGExecutionOrchestrator sans expliquer ce qu'il fait concrètement. La question mérite mieux qu'une ligne, mais pas un long développement non plus. Le DAG est un mécanisme simple, et c'est précisément cette simplicité qui lui donne sa place décisive dans le reste du système. Sa mécanique n'est pas seulement une façon d'ordonner des passes, c'est une façon de révéler quelles passes n'ont aucune raison d'attendre les autres.

La configuration comme déclaration

Chaque passe de la configuration déclare ses dépendances : les passes dont elle a besoin du résultat avant de pouvoir commencer. Cette déclaration est minimale, une liste d'identifiants de passes. C'est DAGExecutionOrchestrator qui en tire les conséquences.

À partir de ces déclarations, il construit un plan d'exécution par couches successives. Une passe appartient à la couche zéro si elle n'a aucune dépendance : elle peut démarrer immédiatement. Une passe appartient à la couche suivante si toutes ses dépendances appartiennent à des couches précédentes. Le processus se répète jusqu'à ce que toutes les passes soient placées.

flow_dag

Ce graphe se traduit en trois couches d'exécution : A et B ensemble, puis C et D ensemble dès que A et B sont terminées, puis E dès que C est terminée. Le parallélisme n'est pas explicitement configuré, il émerge de l'absence de dépendances entre les passes d'une même couche.

La contrainte d'acyclicité est vérifiée au moment de la construction du plan. Si la configuration déclare une dépendance circulaire (A dépend de B qui dépend de A), le plan ne peut pas être construit. Ce n'est pas un cas d'erreur d'exécution : c'est une configuration invalide, rejetée avant que la première passe ne démarre. J'ai préféré cette rigueur à une détection tardive. Mieux vaut un refus net au démarrage qu'un interblocage silencieux découvert en production.

Retour de R&D : le tri topologique est l'algorithme qui transforme un graphe de dépendances en couches d'exécution ordonnées. Son nom est plus intimidant que son principe. Il parcourt le graphe en assignant à chaque nœud le niveau le plus tardif de ses dépendances, plus un. Les nœuds sans dépendance reçoivent le niveau zéro. C'est un algorithme classique. Ce qui importe ici n'est pas l'algorithme lui-même, c'est ce qu'il produit : un plan d'exécution où le parallélisme maximal est garanti par construction, sans qu'aucun développeur n'ait à raisonner explicitement sur quelles passes peuvent tourner ensemble.

Un mécanisme, deux niveaux

DAGExecutionOrchestrator ordonne les passes. PassExecutionOrchestrator ordonne les règles à l'intérieur d'une passe. Ce ne sont pas deux mécanismes différents, c'est le même, appliqué récursivement.

Les règles d'une passe peuvent déclarer des dépendances sur d'autres règles de la même passe, exactement comme les passes déclarent des dépendances entre elles. PassExecutionOrchestrator construit le même plan d'exécution par couches : les règles sans dépendance démarrent immédiatement, les autres attendent que leurs prérequis soient satisfaits. Le parallélisme, là encore, émerge de l'absence de dépendances. Il n'est pas imposé, il est révélé.

flow_dag_multiLevel

C'est ici que la décision de scope du sixième épisode révèle sa conséquence directe. AgentService est Transient, une nouvelle instance à chaque injection. Chaque règle, qu'elle soit indépendante ou qu'elle attende le résultat d'une autre, obtient sa propre instance, son propre historique de messages, son propre LoopDetector, son propre StructuredOutputHandler. Elles ne partagent rien, et c'est précisément ce qui leur permet de coexister, et de se succéder sans contamination d'état.

Ce choix de symétrie entre les deux niveaux n'est pas anodin, c'est celui dont je suis le plus satisfait dans cette partie du système. Il dit que la granularité de la dépendance n'est pas fixée par l'architecture, elle est laissée à la configuration. Une passe peut être un monolithe de règles indépendantes, ou un enchaînement fin où chaque règle affine le contexte de la suivante. L'orchestrateur s'adapte ; il ne contraint pas.

L'angle mort

DAGExecutionOrchestrator ordonne, parallélise, isole. Il fait tout cela dans le cadre d'une exécution continue, un processus qui tourne sans interruption du premier message à l'événement final.

Ce cadre est fragile. Deux choses peuvent le rompre.

La première est une règle qui échoue. Un timeout LLM, un outil qui ne répond plus. Le DAG continue d'ordonner les autres règles, mais celle-ci doit être retentée : une ou deux fois tout de suite, puis avec un délai si les retries immédiats n'ont pas suffi. Le mécanisme complet, son arborescence de tentatives, son état persisté, ses conditions d'arrêt, mérite un traitement à part.

La seconde est un processus qui s'arrête. Un pod qui redémarre en pleine exécution. Un message RabbitMQ relivré parce que l'ACK n'est jamais arrivé. Le DAG ne sait pas que l'exécution a été interrompue : il recommencerait depuis le début, sans mémoire de ce qui a déjà été fait.

Ces deux questions ne sont pas du ressort du DAG. Elles exigent une mémoire qu'il n'a pas.


La prochaine fois : Le DAG ordonne les passes et maximise le parallélisme. Mais que se passe-t-il quand une règle échoue, ou quand l'exécution s'arrête au milieu ? La prochaine fois, on entre dans la mémoire qui permet à une évaluation de ne jamais recommencer à zéro.