Mantenir els torns de l'agent amb moltes imatges dins de la memòria
Un torn de l'agent en producció es va quedar sense memòria a 952 MB mentre retallava productes de sis escanejos de catàleg i mirava els seus retalls. No hi havia una sola fuita: n'hi havia tres. Cada crida al model tornava a enviar les imatges del torn a mida completa; la biblioteca que fem servir per cridar el model mantenia viva cada sol·licitud fins que acabava el torn; i el traçat del marc de l'agent guardava les entrades de cada crida, imatges incloses, durant tot el torn. Els hem arreglat tots tres: les imatges es redueixen abans d'enviar-les, i les imatges de cada crida s'alliberen tan bon punt la crida acaba.
Com veu l'agent les imatges
Quan construeixes a Mythex, l'agent treballa dins del sandbox al núvol del teu projecte. Pot mirar imatges —una captura de pantalla que adjuntes, una maqueta, un fitxer del teu projecte o una imatge que acaba de produir ell mateix— enviant-les al model juntament amb la conversa.
Un torn és tot el que fa l'agent en resposta a un missatge teu. Dins d'un torn, l'agent crida el model moltes vegades: pensa, executa una eina, mira el resultat, torna a pensar. Cada una d'aquestes crides envia la conversa fins aquell moment, i això inclou les imatges del torn, codificades com a text (base64, que les fa uns terç més grans).
El torn que va fallar feia una cosa raonable. Va agafar sis escanejos de catàleg, d'uns 2,4 MB cadascun, va retallar productes individuals i després va llegir els retalls una vegada i una altra per comprovar-los. Això són moltes crides al model, cadascuna amb diverses imatges grans.
Causa 1: cada crida tornava a enviar imatges a mida completa
Cada crida al model reconstruïa el cos de la sol·licitud amb les imatges del torn a mida completa —uns 20 MB de base64 per crida. El cos es construeix com una cadena a la memòria de l'agent, així que cada crida reservava uns altres 20 MB.
Enviar la mida completa ni tan sols aportava res. El model redueix les imatges a uns 1.300 píxels al costat llarg per si mateix, així que els píxels extra només costaven memòria i temps de pujada.
La solució: reduir una vegada, enviar petit
- Les imatges de més de 400 KB es converteixen a JPEG de com a màxim 1.568 píxels al costat llarg abans d'enviar-les. Les més petites s'envien tal com són.
- La conversió usa ImageMagick, que ve instal·lat a cada sandbox de Mythex, i s'executa una vegada per fitxer. Un torn que mostra els mateixos escanejos a cada crida reutilitza la còpia reduïda. Aquesta memòria cau es identifica pel camí, la mida i la data de modificació del fitxer, de manera que un fitxer editat es torna a convertir, i té el seu propi límit de mida perquè no pugui acabar omplint la memòria.
- El pressupost d'imatges en una sola crida al model baixa de 16 MB a 6 MB.
Ara els mateixos sis escanejos costen 4,4 MB per crida en lloc de 19,7 MB.
Causa 2: el client del model no deixava anar mai una sol·licitud
La mida de cada sol·licitud només n'era una part. La memòria també creixia crida a crida. Al nostre entorn de staging, el mateix tipus de torn retenia 369 MB de memòria viva fins i tot després d'una recollida completa d'escombraries —el punt on tot el que no és accessible ja s'ha alliberat. El mostreig de l'heap situava tot el creixement a la part de la biblioteca del client del model que codifica els cossos de les sol·licituds.
La causa era un escoltador d'avortament. Quan prems Atura, el senyal d'avortament del torn s'activa, i cada sol·licitud que l'escolta es cancel·la. Nosaltres passàvem aquell senyal de tot el torn a cada sol·licitud. La biblioteca del client afegeix un escoltador «abort» a qualsevol senyal que rep —i mai no l'elimina.
Així, cada sol·licitud d'un torn es mantenia accessible des del senyal del torn durant tota la seva durada: l'escoltador, que retenia el controlador de la sol·licitud, que retenia la sol·licitud, que retenia tot el cos amb les seves imatges.
La solució: un senyal per sol·licitud
Cada sol·licitud ara té el seu propi senyal. Només s'enllaça amb el senyal del torn mentre aquella sol·licitud s'executa —en una resposta en streaming, fins que s'ha llegit l'últim fragment— i es desenllaça just després. Atura encara arriba a qualsevol sol·licitud en curs, però el senyal del torn només té un escoltador alhora.
Les proves cobreixen les tres parts:
- Després de 12 crides, no queda cap escoltador al senyal del torn. Abans de la solució n'hi havia 12.
- Durant 15 sol·licituds de 8 MB cadascuna, la memòria es manté plana. Abans creixia 128 MB.
- Prémer Atura encara avorta una sol·licitud a mig streaming.
Causa 3: el traçat guardava les entrades de cada crida
Una instantània de l'heap mostrava un tercer retenidor: uns 6 MB per crida al model. Aquest venia del streaming d'esdeveniments del marc de l'agent. Registra cada crida al model d'un torn com a fill del torn, incloses les entrades, fins que el torn acaba. Cada imatge adjunta a cada crida es quedava a la memòria amb ella.
La solució: buidar les imatges quan la crida retorna
Les imatges només s'adjunten als missatges construïts per a una crida concreta al model. Mai no es guarden a l'estat desat de la conversa. Així doncs, quan una crida retorna, és segur buidar les dades d'imatge d'aquests missatges. El traçat conserva un registre de text de la crida; els píxels desapareixen.
Un detall important: les imatges s'han de buidar en el lloc. El traçador pot arribar a les mateixes dades d'imatge a través de més d'una referència en un missatge, així que substituir la llista per una de nova i buida deixaria l'original —amb els píxels— accessible. L'estat de conversa desat mai no es modifica.
Una prova executa el graf de l'agent amb el mateix streaming d'esdeveniments i una imatge de 4 MB a cada crida. Sense la solució, la memòria creixia 77 MB durant 16 crides. Amb la solució, es manté plana.
Què n'hem après
- Els problemes de memòria poden venir per capes. Tres causes separades retenien imatges massa temps, i les tres havien de desaparèixer perquè el torn es mantingués dins de la memòria.
- Tot el que dones a una biblioteca pot quedar retingut. Un senyal i un traçat van sobreviure a les crides a les quals s'havien donat. Quan la memòria creix per crida, mira quins objectes de llarga vida toca la crida.
- No enviïs el que l'altre costat descartaria igualment. Els píxels que el model descartaria de totes maneres ens costaven memòria a cada crida.
- Prova la memòria directament. Cada solució va venir amb una prova que mesura el creixement de l'heap o el recompte d'escoltadors, perquè una regressió aparegui com un número, no com un fallo.
Aquesta és la segona història de memòria de l'agent en poc temps; la primera va ser una etiqueta de progrés que es va reenviar 41.000 vegades. Si vols saber més sobre què és l'agent i com funciona, consulta la nostra guia sobre agents d'IA i els documents del xat.