Jak utrzymać tury agenta z dużą liczbą obrazów w granicach pamięci
Tura agenta na produkcji wyczerpała pamięć przy 952 MB, podczas gdy wycinała produkty z sześciu skanów katalogu i oglądała swoje wycinki. Nie było jednego wycieku — były trzy. Każde wywołanie modelu wysyłało ponownie obrazy tury w pełnym rozmiarze; biblioteka, której używamy do wywoływania modelu, trzymała każde żądanie przy życiu aż do końca tury; a śledzenie (tracing) frameworka agenta przechowywało wejścia każdego wywołania, łącznie z obrazami, przez całą turę. Naprawiliśmy wszystkie trzy: obrazy są zmniejszane przed wysłaniem, a obrazy każdego wywołania są zwalniane zaraz po jego zakończeniu.
Jak agent widzi obrazy
Gdy budujesz w Mythex, agent działa w chmurowej piaskownicy projektu. Może oglądać obrazy — zrzut ekranu, który załączysz, makietę, plik w projekcie albo obraz, który sam właśnie wygenerował — wysyłając je do modelu razem z rozmową.
Tura to wszystko, co agent robi w odpowiedzi na jedną Twoją wiadomość. W jej obrębie agent wywołuje model wiele razy: myśli, uruchamia narzędzie, ogląda wynik, znów myśli. Każde z tych wywołań wysyła rozmowę do tej pory, a wraz z nią obrazy tury, zakodowane jako tekst (base64, co czyni je ok. jednej trzeciej większymi).
Tura, która się wywróciła, robiła coś rozsądnego. Wzięła sześć skanów katalogu, po ok. 2,4 MB każdy, wycięła z nich poszczególne produkty i wciąż czytała wycinki z powrotem, żeby je sprawdzić. To wiele wywołań modelu, każde z kilkoma dużymi obrazami.
Przyczyna 1: każde wywołanie wysyłało obrazy w pełnym rozmiarze
Każde wywołanie modelu budowało treść żądania od nowa, z obrazami tury w pełnym rozmiarze — ok. 20 MB base64 na wywołanie. Treść powstaje jako jeden ciąg w pamięci agenta, więc każde wywołanie alokowało kolejne ok. 20 MB.
Wysyłanie w pełnym rozmiarze nawet niczego nie dawało. Model sam zmniejsza obrazy do ok. 1300 pikseli po dłuższym boku, więc dodatkowe piksele kosztowały tylko pamięć i czas wysyłania.
Naprawa: zmniejsz raz, wysyłaj mało
- Obrazy większe niż 400 KB są teraz konwertowane do JPEG o długim boku najwyżej 1568 pikseli, zanim zostaną wysłane. Mniejsze idą bez zmian.
- Konwersja używa ImageMagick, który jest w każdej piaskownicy Mythex, i działa raz na plik. Tura, która pokazuje te same skany w każdym wywołaniu, używa ponownie zmniejszonej kopii. Pamięć podręczna jest kluczowana ścieżką pliku, rozmiarem i czasem modyfikacji, więc edytowany plik jest konwertowany ponownie, a ona sama ma limit rozmiaru, by nie stać się tym, co zapełnia pamięć.
- Budżet obrazów w jednym wywołaniu modelu spadł z 16 MB do 6 MB.
Te same sześć skanów kosztuje teraz 4,4 MB na wywołanie zamiast 19,7 MB.
Przyczyna 2: klient modelu nigdy nie puszczał żądania
Rozmiar każdego żądania to tylko część problemu. Pamięć rosła też z wywołania na wywołanie. Na naszym środowisku stagingowym ten sam rodzaj tury trzymał 369 MB żywej pamięci nawet po pełnym odśmiecaniu — punkcie, w którym wszystko, do czego nic się nie odwołuje, zostaje zwolnione. Próbkowanie sterty wskazało, że cały wzrost pochodzi z części biblioteki klienta modelu, która koduje treść żądań.
Przyczyną był nasłuchiwacz przerwania (abort listener). Gdy naciskasz Stop, sygnał przerwania tury się uruchamia i każde nasłuchujące żądanie się anuluje. Przekazywaliśmy ten jeden sygnał obejmujący całą turę do każdego żądania. Biblioteka klienta dodaje nasłuchiwacz „abort” do każdego sygnału, który dostanie — i nigdy go nie usuwa.
Więc każde żądanie tury pozostawało osiągalne z sygnału tury przez cały czas jej trwania: nasłuchiwacz trzymał kontroler żądania, kontroler trzymał żądanie, a żądanie trzymało całą treść z obrazami.
Naprawa: jeden sygnał na żądanie
Każde żądanie dostaje teraz własny sygnał. Jest połączony z sygnałem tury tylko na czas, w którym to żądanie trwa — przy odpowiedzi strumieniowej do odczytania ostatniego fragmentu — i odłączany zaraz po tym. Stop nadal dociera do żądania w toku, ale sygnał tury trzyma w danej chwili tylko jednego nasłuchiwacza.
Testy pokrywają wszystkie trzy części:
- Po 12 wywołaniach na sygnale tury nie ma żadnych nasłuchiwaczy. Przed poprawką było ich 12.
- Przy 15 żądaniach po 8 MB każde pamięć pozostaje płaska. Wcześniej rosła o 128 MB.
- Stop nadal przerywa żądanie w połowie strumienia.
Przyczyna 3: śledzenie zapisywało wejścia każdego wywołania
Zrzut sterty pokazał trzeciego posiadacza: ok. 6 MB na wywołanie modelu. To pochodziło ze strumieniowania zdarzeń frameworka agenta. Rejestruje ono każde wywołanie modelu w turze jako potomka tury, łącznie z wejściami, aż tura się skończy. Każdy obraz załączony do każdego wywołania zostawał w pamięci razem z nim.
Naprawa: wyczyść obrazy, gdy wywołanie wróci
Obrazy są dołączane wyłącznie do wiadomości zbudowanych dla konkretnego wywołania modelu. Nigdy nie są zapisywane w zapisanym stanie rozmowy. Więc gdy wywołanie zwróci wynik, można bezpiecznie wyczyścić dane obrazów w tych wiadomościach. Ślad zachowuje tekstowy zapis wywołania; piksele znikają.
Jeden szczegół był ważny: obrazy trzeba czyścić w miejscu. Tracer może dotrzeć do tych samych danych obrazu przez więcej niż jedno odwołanie w wiadomości, więc podmiana na nową, pustą listę zostawiłaby oryginał — z pikselami — osiągalny. Zapisany stan rozmowy nigdy nie jest modyfikowany.
Test uruchamia graf agenta pod tym samym strumieniowaniem zdarzeń z obrazem 4 MB w każdym wywołaniu. Bez poprawki pamięć rosła o 77 MB przez 16 wywołań. Z poprawką pozostaje płaska.
Co z tego wynieśliśmy
- Problemy z pamięcią mogą mieć warstwy. Trzy osobne przyczyny trzymały obrazy zbyt długo i wszystkie trzy musiały zniknąć, zanim tura zmieściła się w pamięci.
- Wszystko, co przekażesz bibliotece, może zostać zatrzymane. Sygnał i ślad przeżyły wywołania, którym zostały przekazane. Gdy pamięć rośnie na każde wywołanie, sprawdź, jakie długo żyjące obiekty dotyka wywołanie.
- Nie wysyłaj tego, co druga strona i tak wyrzuca. Piksele, które model i tak by odrzucił, kosztowały nas pamięć przy każdym wywołaniu.
- Testuj pamięć bezpośrednio. Każda poprawka miała test mierzący wzrost sterty albo liczbę nasłuchiwaczy, więc regresja pojawia się jako liczba, a nie jako awaria.
To drugie memory-story agenta w krótkim czasie; pierwsze to etykieta postępu, która ustawiła w kolejce 41 000 aktualizacji. Jeśli chcesz wiedzieć więcej o tym, czym jest agent i jak działa, zajrzyj do naszego poradnika o agentach AI i do dokumentacji czatu.