نگه داشتن نوبتهای پرتصویر عامل در محدودهی حافظه
یک نوبت عامل در محیط واقعی در ۹۵۲ مگابایت حافظه تمام شد، در حالی که شش اسکن کاتالوگ را برش میداد تا محصولات را جدا کند و برشها را نگاه میکرد. یک نشتی تنها در کار نبود — سه تا بود. هر فراخوانی به مدل تصاویر نوبت را با اندازهی کامل دوباره ارسال میکرد؛ کتابخانهای که برای صدا زدن مدل استفاده میکنیم هر درخواست را تا پایان نوبت زنده نگه میداشت؛ و ردیابی چارچوب عامل ورودیهای هر فراخوانی، از جمله تصاویر، را برای کل نوبت نگه میداشت. هر سه را رفع کردیم: تصاویر پیش از ارسال کوچک میشوند و تصاویر هر فراخوانی بهمحض تمام شدن آن آزاد میشوند.
عامل چگونه تصاویر را میبیند
وقتی روی Mythex میسازید، عامل داخل sandbox ابری پروژهتان کار میکند. میتواند تصاویر را ببیند — یک اسکرینشات که پیوست میکنید، یک ماکت، فایلی در پروژه، یا تصویری که خودش همین حالا ساخته — با ارسال آنها به مدل همراه با مکالمه.
نوبت همهی کارهایی است که عامل در پاسخ به یک پیام از شما انجام میدهد. در یک نوبت، عامل چندین بار مدل را صدا میزند: فکر میکند، یک ابزار را اجرا میکند، نتیجه را میبیند، و دوباره فکر میکند. هر کدام از این فراخوانیها مکالمهی تا آن لحظه را میفرستد، و این شامل تصاویر نوبت است که بهصورت متن (base64، که حدود یکسوم بزرگتر میشود) کدگذاری شدهاند.
نوبتی که کرش کرد کار معقولی انجام میداد. شش اسکن کاتالوگ، حدود ۲.۴ مگابایت هرکدام، گرفت، محصولات تکتک را از آنها برید، و برشها را دوباره میخواند تا بررسی کند. یعنی فراخوانیهای زیادی به مدل، که هرکدام چندین تصویر بزرگ را با خود میبرد.
علت ۱: هر فراخوانی تصاویر با اندازهی کامل را دوباره میفرستاد
هر فراخوانی به مدل بدنهی درخواستش را با تصاویر کامل نوبت بازسازی میکرد — حدود ۲۰ مگابایت base64 در هر فراخوانی. بدنه بهصورت یک رشته در حافظهی عامل ساخته میشود، پس هر فراخوانی حدود ۲۰ مگابایت دیگر تخصیص میداد.
ارسال با اندازهی کامل حتی سودی نداشت. مدل خودش تصاویر را تا حدود ۱۳۰۰ پیکسل در طولانیترین ضلع کوچک میکند، پس پیکسلهای اضافی فقط حافظه و زمان آپلود هزینه میکردند.
راهحل: یکبار کوچک کن، کوچک بفرست
- تصاویری که بیش از ۴۰۰ کیلوبایت هستند، پیش از ارسال به JPEG با حداکثر ۱٬۵۶۸ پیکسل در طولانیترین ضلع تبدیل میشوند. تصاویر کوچکتر همانطور که هستند ارسال میشوند.
- تبدیل با ImageMagick انجام میشود، که در هر sandbox Mythex هست، و برای هر فایل یکبار اجرا میشود. نوبتی که همان اسکنها را در هر فراخوانی نشان میدهد، نسخهی کوچکشده را دوباره استفاده میکند. این کش با مسیر فایل، اندازه و زمان تغییر کلید میخورد، پس فایل ویرایششده دوباره تبدیل میشود، و حد اندازهی جداگانهای دارد تا خودش به چیزی تبدیل نشود که حافظه را پر کند.
- بودجهی تصاویر در یک فراخوانی مدل از ۱۶ مگابایت به ۶ مگابایت کاهش مییابد.
همان شش اسکن حالا ۴.۴ مگابایت در هر فراخوانی هزینه دارند، بهجای ۱۹.۷ مگابایت.
علت ۲: کلاینت مدل هرگز درخواست را رها نمیکرد
اندازهی هر درخواست فقط بخشی از ماجرا بود. حافظه هم فراخوانی به فراخوانی بیشتر میشد. در محیط staging ما، همین نوع نوبت حتی پس از یک گردآوری کامل زباله — نقطهای که هر چیزی که قابل دسترسی نیست آزاد شده — هنوز ۳۶۹ مگابایت حافظهی زنده نگه میداشت. نمونهبرداری از heap تمام رشد را به بخشی از کتابخانهی کلاینت مدل نسبت داد که بدنههای درخواست را کدگذاری میکند.
علت یک listener مربوط به abort بود. وقتی Stop را میزنید، سیگنال abort نوبت فعال میشود و هر درخواستی که به آن گوش میدهد لغو میشود. ما یک سیگنال سراسری نوبت را به هر درخواست میدادیم. کتابخانهی کلاینت به هر سیگنالی که دریافت میکند یک listener با نام «abort» اضافه میکند — و هرگز آن را حذف نمیکند.
پس هر درخواست نوبت تا وقتی نوبت ادامه داشت از سیگنال نوبت قابل دسترسی میماند: listener، که کنترلرِ درخواست را نگه میداشت، که خود درخواست را نگه میداشت، که کل بدنهی درخواست با تصاویرش را.
راهحل: یک سیگنال برای هر درخواست
حالا هر درخواست سیگنال خودش را میگیرد. فقط در حین اجرای آن درخواست به سیگنال نوبت وصل میشود — برای پاسخ استریمشده، تا آخرین تکهای که خوانده شده — و بلافاصله بعدش جدا میشود. Stop هنوز به هر درخواستی که در جریان است میرسد، اما سیگنال نوبت فقط یک listener در هر لحظه نگه میدارد.
تستها هر سه بخش را پوشش میدهند:
- بعد از ۱۲ فراخوانی، روی سیگنال نوبت هیچ listenerی باقی نمیماند. پیش از رفع، ۱۲ بود.
- در ۱۵ درخواست ۸ مگابایتی، حافظه ثابت میماند. پیش از رفع، ۱۲۸ مگابایت بیشتر میشد.
- فشردن Stop هنوز یک درخواست را در میانهی استریمش لغو میکند.
علت ۳: ردیابی ورودیهای هر فراخوانی را نگه میداشت
یک heap snapshot نگهدار سومی را نشان داد: حدود ۶ مگابایت بهازای هر فراخوانی مدل. این از استریم رویدادهای چارچوب عامل میآمد. این استریم هر فراخوانی مدل در یک نوبت را بهعنوان فرزند نوبت ثبت میکند، همراه با ورودیها، تا پایان نوبت. هر تصویری که به هر فراخوانی وصل بود همراهش در حافظه میماند.
راهحل: پس از بازگشت فراخوانی، تصاویر را خالی کن
تصاویر فقط به پیامهایی وصلاند که برای یک فراخوانی مدل خاص ساخته شدهاند. هرگز در وضعیت ذخیرهشدهی مکالمه نگه داشته نمیشوند. پس وقتی یک فراخوانی برمیگردد، خالی کردن دادهی تصویر در آن پیامها امن است. ردیاب یک رکورد متنی از فراخوانی نگه میدارد؛ پیکسلها میروند.
یک جزئیات مهم بود: تصاویر باید درجا خالی شوند. ردیاب ممکن است از طریق بیش از یک ارجاع روی یک پیام به همان دادهی تصویر برسد، پس جایگزین کردن با یک لیست تازه و خالی، اصل را — با پیکسلهایش — همچنان قابل دسترس نگه میداشت. وضعیت ذخیرهشدهی مکالمه هرگز تغییر نمیکند.
یک تست گراف عامل را زیر همان استریم رویدادها اجرا میکند با یک تصویر ۴ مگابایتی در هر فراخوانی. بدون رفع، حافظه در ۱۶ فراخوانی ۷۷ مگابایت رشد کرد. با رفع، حافظه ثابت میماند.
آنچه از آن یاد گرفتیم
- مشکلات حافظه میتوانند لایهلایه باشند. سه علت جداگانه هر کدام تصاویر را بیش از حد نگه میداشتند، و هر سه باید برطرف میشدند تا نوبت در محدودهی حافظه بماند.
- هر چیزی که به یک کتابخانه میدهید ممکن است نگه داشته شود. یک سیگنال و یک ردیاب هر دو از فراخوانیهایی که به آنها داده شده بود بیشتر عمر کردند. وقتی حافظه به ازای هر فراخوانی رشد میکند، ببینید فراخوانی به چه اشیای طولانیعمری دست میزند.
- آنچه طرف مقابل دور میریزد را نفرستید. پیکسلهایی که مدل از قبل کنار میگذاشت، در هر فراخوانی برای ما حافظه هزینه داشتند.
- حافظه را مستقیم آزمایش کنید. هر رفع با تستی همراه بود که رشد heap یا تعداد listener را اندازه میگیرد، پس رگرسیون بهصورت یک عدد دیده میشود، نه یک کرش.
این دومین داستان حافظه از عامل در مدت کوتاهی است؛ اولی یک برچسب پیشرفت که ۴۱٬۰۰۰ بار دوباره فرستاده شد بود. اگر میخواهید بیشتر بدانید عامل چیست و چطور کار میکند، راهنمای عاملهای هوش مصنوعی و مستندات چت را ببینید.