Greška nedostatka memorije skrivena u oznaci napretka
Jedne večeri krajem rujna AI agent u radnom prostoru jednog korisnika srušio se zbog greške nedostatka memorije usred izvođenja. Očigledni krivci, previše pod-agenata koji rade u isto vrijeme ili kontekst koji je previše narastao, bili su krivi. Pravi uzrok bila je oznaka statusa: dok je model streamao veliku shell naredbu nekoliko znakova odjednom, mi smo pri svakom djeliću ponovno slali cijelu naredbu do tog trenutka kao oznaku napretka. Naredba od 124 KB pretvorila se u otprilike 41.000 događaja i oko 690 MB stringova u redu čekanja.
Kako agent streama svoj rad
Kad gradite na Mythexu, agent radi unutar privatnog cloud sandboxa vašeg projekta. Dok model piše, njegov izlaz se streama natrag u vaš preglednik: tekst se pojavljuje riječ po riječ, a kad pokrene alat, primjerice shell naredbu koja piše datoteku, vidite oznaku za to koja se uživo ažurira, pa znate što radi.
Model streama u malim komadima. Kod modela koji smo koristili ti su komadi često bili duljine tri znaka.
Prve hipoteze bile su krive
Pad je izgledao kao problem skaliranja, pa smo tamo i krenuli.
- Paralelni pod-agenti? Izvođenje je koristilo samo dva, a svaki je držao oko 1,5 MB stanja. Nedovoljno.
- Zadržani tragovi? Naše praćenje držalo je stablo izvođenja u memoriji, ali nije bilo ni blizu veličine heapa u trenutku pada.
Obje priče bile su uvjerljive, a obje su bile jeftine za vjerovati. Nijedna nije reproducirala pad. Lekcija koju stalno ponovno učimo: hipoteza nije uzrok dok ne reproducira grešku.
Reproduciranje stvarnog stanja
Napravili smo jednokratni sandbox s istim ograničenjima memorije kao u produkciji i lažni model koji je streamao veliku datoteku u realističnim komadima od tri znaka. Također smo usporili čitača na drugom kraju socketa, kao u stvarnoj mreži.
To je to. Heap je rastao i proces je umro s istom greškom.
Mehanizam je bio jednostavan kad smo ga mogli promatrati. Dok je model pisao veliku naredbu, u ovom slučaju heredoc koji je zapisao datoteku od 124 KB, naš streaming kod ažurirao je oznaku alata pri svakom djeliću. Oznaka nije bila novih tri znaka, nego cijela naredba do tada. Tako su događaji rasli: 3 znaka, pa 6, pa 9, … do 124 KB, otprilike 41.000 puta. Socket ih nije mogao poslati jednako brzo kojom su nastajali, pa su se nagomilali u njegovom write bufferu kao stringovi, oko 690 MB.
Popravak
- Ograničite oznaku. Oznaka statusa treba samo reći što se događa. Sada je ograničena na 200 znakova, a ponovno skeniranje djelomične naredbe usporeno je.
- Zaustavite kvadratično spajanje. Spajanje streamanih komadića ponovljenim nadovezivanjem stringova je O(n²) za velike izlaze. Prešli smo na pristup temeljen na dijelovima koji nadodaje u linearnom vremenu.
Zatim smo potražili sve drugo što bi moglo rasti bez granica pri poplavi izlaza i to ograničili:
- Izlaz naredbi uživo prikazan u chatu grupira se i zadržava zadnjih 8.000 znakova.
- Pojedinačni redovi događaja ograničeni su na 4 MB.
- Replay buffer koji preglednik koristi za nadoknadu nakon ponovnog spajanja ograničen je na 8 MB po projektu.
- Stari checkpointi se orežu dok izvođenje napreduje, zadržavajući najnovije stanje i sidrišta koja su potrebna ranijim izvođenjima.
Iskreno testiranje poplava
Dvije stvari su to učinile težim za testiranje nego što bi trebalo biti:
- Pravi modeli ne surađuju. Ako modelu zadate da ispiše golemu datoteku, obično će odbiti, sažeti ili napisati to razumnije. Za testiranje poplava treba vam skriptirani model koji radi točno ono što mu kažete, kroz pravi kod agenta.
- Skripte se moraju ponašati kao modeli. Naš prvi skriptirani model koristio je isti ID odgovora pri svakom odgovoru, pa je okvir agenta svaki odgovor tretirao kao zamjenu za prethodni i izvođenja je prerano završavao. Izgledalo je kao bug osvježavanja sučelja. Nije bio: testni okvir nas je lagao. Također smo morali u skripti provesti stvarno ograničenje konteksta modela, inače su brojke memorije bile nerealne.
Što smo iz toga izvukli
- Streaming sučelja pojačavaju male greške. Sve što šaljete „po komadu” šalje se desecima tisuća puta. Šaljite delte ili ograničite korisni teret.
- Ograničite svaki buffer koji prelazi sporu granicu. Socketi, replay zapisi i checkpointi trebaju gornju granicu.
- Reproducirajte prije popravka. Naša prva dva objašnjenja zvučala su točno i bila su kriva.
Ako ste nedavno u Mythex chatu vidjeli vrlo dugačku datoteku kako se pojavljuje bez zastoja agenta, ovo je dio razloga zašto.