La fuite de mémoire cachée dans une étiquette de progression
Un soir de fin septembre, l'agent IA d'un espace de travail d'utilisateur a planté avec une erreur de mémoire insuffisante au milieu d'un tour. Les suspects évidents — trop de sous-agents actifs en même temps, ou un contexte devenu trop gros — étaient tous deux faux. La vraie cause était une étiquette d'état : pendant que le modèle diffusait une grosse commande shell quelques caractères à la fois, nous renvoyions la commande entière telle qu'elle était jusque-là comme étiquette de progression à chaque morceau. Une commande de 124 Ko est devenue environ 41 000 événements et environ 690 Mo de chaînes de caractères en file d'attente.
Comment l'agent diffuse son travail
Lorsque vous construisez sur Mythex, l'agent s'exécute dans le bac à sable cloud privé de votre projet. Au fur et à mesure que le modèle écrit, sa sortie est diffusée vers votre navigateur : le texte apparaît mot par mot, et lorsqu'il exécute un outil — par exemple une commande shell qui écrit un fichier —, vous voyez une étiquette qui se met à jour en direct, pour savoir ce qu'il fait.
Le modèle diffuse par petits morceaux. Avec le modèle que nous utilisions, ces morceaux ne faisaient souvent que trois caractères.
Les premières hypothèses étaient fausses
Le plantage ressemblait à un problème de passage à l'échelle, alors nous avons commencé par là.
- Des sous-agents parallèles ? Le tour n'en avait utilisé que deux, chacun détenant environ 1,5 Mo d'état. Ce n'était pas assez.
- Des traces conservées ? Notre traçage gardait un arbre d'exécution en mémoire, mais il n'avait rien de comparable à la taille du tas au moment du plantage.
Les deux pistes étaient plausibles, et les deux étaient faciles à croire. Aucune ne reproduisait le plantage. La leçon que nous réapprenons sans cesse : une hypothèse n'est pas une cause tant qu'elle ne reproduit pas la panne.
Reproduire le problème pour de vrai
Nous avons construit un bac à sable jetable avec les mêmes limites de mémoire que la production, et un faux modèle qui diffusait un gros fichier en morceaux réalistes de trois caractères. Nous avons aussi rendu lent le lecteur à l'autre bout de la connexion, comme un vrai réseau.
Cela a fonctionné. Le tas a grimpé, et le processus est mort avec la même erreur.
Le mécanisme était simple une fois que nous pouvions l'observer. Pendant que le modèle écrivait une grosse commande — dans ce cas un heredoc qui écrivait un fichier de 124 Ko —, notre code de diffusion mettait à jour l'étiquette de l'outil à chaque morceau. L'étiquette ne contenait pas les trois nouveaux caractères ; elle contenait la commande entière telle qu'elle était jusque-là. Les événements grossissaient donc : 3 caractères, puis 6, puis 9, … jusqu'à 124 Ko, environ 41 000 fois. La connexion ne pouvait pas envoyer aussi vite qu'ils étaient produits, si bien qu'ils s'accumulaient dans son tampon d'écriture sous forme de chaînes — environ 690 Mo.
La correction
- Limiter l'étiquette. Une étiquette d'état n'a besoin que de dire ce qui se passe. Elle est maintenant limitée à 200 caractères, et nous espaçons la fréquence de relecture de la commande partielle.
- Supprimer la concaténation quadratique. Assembler des morceaux diffusés par concaténations répétées de chaînes est en O(n²) pour les grandes sorties. Nous sommes passés à une approche par blocs qui ajoute en temps linéaire.
Nous avons ensuite cherché tout autre élément qui pourrait croître sans limite sous un flot de sorties, et nous l'avons borné :
- La sortie en direct des commandes affichée dans le chat est regroupée et limitée aux 8 000 derniers caractères.
- Chaque ligne d'événement est limitée à 4 Mo.
- La mémoire tampon de rejeu qu'un navigateur utilise pour rattraper son retard après une reconnexion est limitée à 8 Mo par projet.
- Les anciens points de contrôle sont élagués au fil du tour, en conservant l'état le plus récent et les ancres dont les tours précédents ont besoin.
Tester les inondations honnêtement
Deux choses ont rendu ce test plus difficile qu'il ne devrait l'être :
- Les vrais modèles ne coopèrent pas. Demandez à un modèle d'afficher un fichier gigantesque et il refusera généralement, le résumera ou l'écrira plus raisonnablement. Pour tester les inondations, il faut un modèle scripté qui fait exactement ce que vous lui dites, en passant par le vrai code de l'agent.
- Les scripts doivent se comporter comme des modèles. Notre premier modèle scripté réutilisait le même identifiant de réponse à chaque réponse, si bien que le cadre de l'agent traitait chaque réponse comme le remplacement de la précédente et terminait les tours trop tôt. Cela ressemblait à un bogue d'actualisation de l'interface. Ce n'en était pas un : c'était le banc d'essai qui nous mentait. Nous avons aussi dû imposer la vraie limite de contexte du modèle dans le script, sinon les chiffres de mémoire n'étaient pas réalistes.
Ce que nous en avons retenu
- Les interfaces de diffusion amplifient les petites erreurs. Tout ce qui est envoyé « par morceau » est envoyé des dizaines de milliers de fois. Envoyez des deltas, ou limitez la charge utile.
- Bornez chaque tampon qui traverse une frontière lente. Les sockets, les journaux de rejeu et les points de contrôle ont tous besoin d'un plafond.
- Reproduisez avant de corriger. Nos deux premières explications semblaient justes et étaient fausses.
Si vous avez récemment vu un très long fichier apparaître dans le chat de Mythex sans que l'agent ne cale, c'est en partie pour cette raison.