تصور کنید یک برنامهنویس برای اتوماسیون پیچیده، عاملی ساخته که در هر مرحله ابزاری را فراخوانی میکند، اما ناگهان متوجه میشود مدل یک دستور واحد را سه بار اجرا کرده و دیتابیس را به هم ریخته است. این اتفاق معمولاً تقصیر مدل نیست، بلکه نتیجهی یک «تقویتکنندهی تکرار» (Retry Amplifier) در صف استنتاج است که باعث میشود برنامههای ابزار تکراری برای یک گام واحد اجرا شوند.
به گزارش تحلیلهای فنی اخیر، وقتی یک صف استنتاج مشترک وجود دارد، یک تلاش مجدد (Retry) ساده میتواند به فاجعه تبدیل شود. در یک بازسازی فنی که سهشنبه گذشته انجام شد، مشخص شد ۱۲ حلقه برنامهریز بهطور همزمان وارد یک صف مشترک شدند. اولین تأخیر (Timeout) باعث لغو درخواست اصلی نشد؛ در عوض، درخواست دومی را بدون دریافت یک اجارهی (Lease) جدید صادر کرد. وقتی هر دو پاسخ با ترتیب متفاوت بازگشتند، برنامهریز دو برنامه ابزار را برای یک مرحله واحد اعمال کرد. این نوع نقص در مدیریت وضعیت، دقیقاً همان ریشهای است که میتواند منجر به خطاهای بحرانی در پرداختها در محاسبات گذرا شود.
بسیاری از حلقههای عامل، یک اصل حیاتی (Invariant) را نادیده میگیرند: برای هر شناسه اجرای ارزیابی (eval-run identifier)، استنتاجهای در جریان باید در یک محدوده declared outstanding bound باقی بمانند و یک پاسخ تنها زمانی میتواند اعمال شود که هنوز یک نسل اجارهی زنده (live lease generation) را در اختیار دارد. بدون این حصار، یک بکاند مشترک بهجای افزایش توان عملیاتی، مکانیزمی برای تخریب وضعیت برنامهریز از طریق پاسخهای خارج از ترتیب فراهم میکند. آیا تا به حال مدل را برای یک فراخوانی ابزار تکراری سرزنش کردهاید، در حالی که در واقع صف استنتاج فقط یک Retry را تقویت کرده بود؟
مفروضات و محدودیتها
پیش از ترسیم پروتکل، باید مفروضات خاصی اعلام شود تا شبیهساز دچار خطا نشود. اول، استنتاج به عنوان یک صف راه دور در نظر گرفته میشود که در مالکیت کاربر نیست؛ به این معنی که ترتیب تکمیل درخواستها لزوماً با ترتیب ارسال آنها یکی نیست. دوم، تایماوتها ساعتهای محلی هستند، نه لغوهای سمت سرور؛ بنابراین درخواست اصلی همچنان میتواند به پایان برسد. سوم، تلاشهای مجدد از نوع «حداقل یکبار» (at-least-once) هستند، به این معنی که بکاند ممکن است هم درخواست اصلی و هم درخواست تکراری را اجرا کند.
آیا حلقه شما در حال حاضر هر یک از این موارد را نادیده میگیرد؟ من همچنین فرض میکنم که آزمایشگاه میتواند از دسترسی رایگان به مدل و یک سرور رایگان به عنوان بکاندی استفاده کند که زمانبندی (scheduled) نشده است. این یک محدودیت است، نه یک وعده در مورد ظرفیت. هیچ ادعایی در مورد GPUهای اختصاصی، مدلهای نامگذاری شده یا سهمیههای دائمی وجود ندارد. مسئله اصلی، یک صف مشترک است که تحت فشار، ترتیب کارها را تغییر میدهد؛ و این دقیقاً همان کلاس شکستی است که این سیستم اجاره برای حل آن طراحی شده است. در واقع، تکیه بر ظرفیت سختافزاری بهتنهایی راهکار نیست، زیرا معیارهای ظرفیت GPU اغلب سیگنالهای نادرستی برای منطق تکرار درخواستها ارسال میکنند.
حوزههای شکست (Failure Domains)
محدودیتی که در عمل آسیب میزند، مسدود شدن ابتدای صف (head-of-line blocking) در صفی است که شما نمیتوانید آن را بازرسی کنید. حلقههای برنامهریز موازی، «ارسال یک درخواست تکمیل دیگر» را به عنوان پیشرفت تلقی میکنند، در حالی که صف، هر Retry را به عنوان کار بیشتری در جلوی درخواست اصلی میبیند. در این حالت، تأخیر به صورت کندی مدل به نظر میرسد و باعث میشود حلقه تایماوت خود را کوتاه کند، که این امر Stampede یا هجوم درخواستها را بدتر میکند. چرا یک بکاند مشترک رایگان باید رفتاری متفاوت داشته باشد؟
برای حل این مشکل، معماری باید حوزههای شکست را به سه خط مجزا تقسیم کند تا از گزارشهای پس از حادثه (postmortems) غیرقابل خواندن جلوگیری شود:
- حوزه A: ساعت تأخیر کلاینت، که میتواند در حالی فعال شود که سرور هنوز درخواست اصلی را در دست دارد.
- حوزه B: صف استنتاج مشترک، که میتواند پاسخها را به تأخیر بیندازد، تکرار کند یا ترتیب آنها را تغییر دهد.
- حوزه C: لاگ برنامهریز، که نباید متنی را که اجارهاش را از دست داده است، اعمال کند.
اگر سعی کنید حوزه C را با معیارهای حوزه A دیباگ کنید، تا ابد زمان تأخیر را تنظیم میکنید بدون اینکه هرگز جلوی تکرار گامها را بگیرید.
پروتکل میز پذیرش (Admission Desk)
راهکار پیشنهادی، ایجاد یک «میز پذیرش» است؛ یک ماشین وضعیت کوچک که بهجای قرارگیری در کلاینت مدل، در مقابل برنامهریز قرار میگیرد، زیرا کلاینت در مورد لغو درخواستها دروغ میگوید. برنامهریز همان چیزی است که نباید گامهای تکراری را ببیند. هر اجرای ارزیابی، یک میز پذیرش دارد. این میز، مقدار Bound، شمارنده in_flight (در جریان)، عدد نسل (generation) و نقشهای از شناسههای اجاره فعال را ذخیره میکند. هر چیز دیگری صرفاً کامنتی است که منتظر است گم شود.
جزئیات جریان داده
اگر حصار را در جای درست رسم کنید، جریان داده کوچک خواهد بود. یک اجرای ارزیابی از میز پذیرش، اجارهای میخواهد که حاوی یک عدد نسل است. کارگر ممکن است یک درخواست استنتاج تگ شده با آن نسل ارسال کند، و سپس باید منتظر بماند یا اجاره را آزاد کند. پاسخها بهجای برنامهریز، دوباره به میز پذیرش وارد میشوند و میز پذیرش یا متن را در لاگ گامها ثبت (commit) میکند یا آن را به عنوان یک نسل منقضیشده رد میکند. تنها یک گام ثبتشده میتواند اجاره بعدی را صادر کند. این تمام صفحه کنترل است.
- کسب (Acquisition): در شروع گام، کارگر
acquire()را فراخوانی میکند. اگرin_flight == boundباشد، سیستم منتظر میماند یا با خطا بسته میشود؛ سیستم اجازه نمیدهد یک Retry بهطور مخفیانه از کنار شمارنده عبور کند. تابعacquire()عدد نسل را افزایش میدهد، یکlease_idایجاد میکند، هر دو را ذخیره کرده وin_flightرا افزایش میدهد. - درخواست (Request): هدر درخواست شامل هر دو مورد، یعنی عدد نسل و شناسه اجاره است. سپس کارگر یک تایمر محلی را فعال میکند.
- انقضا (Expiration): با فعال شدن تایمر، کارگر
expire(lease_id)را فراخوانی میکند. این کار عدد نسل را دوباره افزایش میدهد اماin_flightرا فعال نگه میدارد تا زمانی که فراخوانی معلق محاسبه شود. - اعمال (Application): پاسخ تنها زمانی اعمال میشود که
completion.generation == desk.generationباشد وlease_idهنوز زنده باشد. در غیر این صورت، میز پذیرش آن را به عنوانstale_completionرد میکند. - آزادسازی (Release): تابع
release()مقدارin_flightرا دقیقاً یک بار به ازای هر اجاره کسب شده کاهش میدهد، حتی برای اجارههای منقضیشدهای که بدنه پاسخ آنها دیر رسیده است. آزادسازی دوبار (Double release) به عنوان یک باگ تلقی میشود، نه یک عملیات پاکسازی.
ترتیب رویدادهای ناقض
یک مثال متضاد را در نظر بگیرید: کارگر W نسل اجاره ۴ را دارد و درخواست R1 را میفرستد. تایماوت محلی در زمان T+8s فعال میشود و W درخواست R2 را با همان نسل (یا بدون نسل) ارسال میکند. R2 زودتر با یک برنامه ابزار تکمیل میشود؛ برنامهریز آن را اعمال کرده و نسل ۵ را صادر میکند. سپس R1 با یک برنامه ابزار متفاوت تکمیل میشود. چون هیچ چیزی نسل را بررسی نکرد، برنامهریز R1 را نیز اعمال میکند. حالا کدام یک از این دو برنامه، گام «واقعی» است؟
پیادهسازیهای رایج این اصل را با استفاده از یک سمافور در مسیر موفق (happy path) اما دور زدن آن در زمان Retry شکست میدهند، زیرا تصور میکنند «ما قبلاً هزینه انتظار را پرداختهایم». آنها پاسخ HTTP 200 را به عنوان مرجع میپذیرند حتی زمانی که بدنه پاسخ متعلق به یک اجاره منقضیشده است، و در ثبت نسل در رکورد تکمیل شکست میخورند، که باعث میشود تستهای ویژگی (property tests) چیزی برای تایید نداشته باشند. آیا این یک معماری عامل است یا صرفاً یک دستور if با I/O معلق نامحدود؟
اعتبارسنجی اصل حیاتی
برای اثبات کارایی این روش، نویسنده یک شبیهساز رویداد گسسته (discrete-event simulator) را پیشنهاد میکند؛ ابزاری برای آزمایشگاه، نه یک بنچمارک تولیدی. این شبیهساز را میتوان بهصورت داخلی (in-process) یا در مقابل یک بکاند مشترک مانند گزینه دسترسی رایگان به مدل و سرور MonkeyCode اجرا کرد، جایی که صف را نمیتوان متوقف کرد. آن را به عنوان یک دستور اجرا کنید، نه به عنوان یک اسلاید. اگر تعداد ثبتها (commits) پس از یک تایماوت هرگز از یک بیشتر نشود، یعنی میز پذیرش حصار نسل را حفظ کرده است. اگر in_flight در پایان غیرصفر باشد، یک مسیر آزادسازی نشت کرده است.
شکستهای تزریقی و ویژگیهای قابل تست
شبیهساز باید چهار کلاس شکست را پیش از اعتماد به میز پذیرش در یک صف مشترک تزریق کند:
۱. پاسخهای دیرهنگام اصلی: پاسخی که بعد از expire میرسد باید رد شود.
۲. تخطی از حد مجاز: تکراری که نمیتواند اجاره بگیرد چون bound=1 است، باید بهجای ارسال، با خطا بسته شود.
۳. نسلهای تکراری: دو پاسخ با نسل یکسان باید حداکثر یک بار ثبت شوند.
۴. آزادسازی دوبار: یک آزادسازی که دو بار میرسد نباید مقدار in_flight را منفی کند.
کدام یک از این چهار مورد را کلاینت شما امروز تست میکند؟
ویژگیهای رسمی برای هارنس (Harness)
ویژگیها باید در هارنس با یک مخرج صریح تثبیت شوند:
- P1: مقدار
in_flightپس از هر رویداد همیشه در بازه[0, bound]است. - P2: تعداد ثبتها (commit count) به ازای هر گام اجرای ارزیابی، ۰ یا ۱ است.
- P3: هر
commit.generationدر زمان اعمال، زنده بوده است. - P4: هر تایماوت یا منجر به
reject_staleبرای نسل قدیمی میشود یا هیچ ارسال دومی صورت نمیگیرد.
موفقیت با یک مخرج خاص اندازهگیری میشود: تعداد ثبتهای تکراری در هر ۱۰۰ جفت «تایماوت سپس پاسخ دیرهنگام تزریق شده». قانون پذیرش این است که نتیجه برابر با ۰ باشد و P1 تا P4 روی هر seed در مجموعه ثابت {1..200} برقرار باشند. اگر نمیتوانید مخرج را نام ببرید، شما ارزیابی (eval) ندارید؛ شما فقط یک داشبورد دارید.
موازنه ها و محدودیتها
| طراحی | آنچه حفظ میکند | آنچه هزینه میکند | زمان شکست |
|---|---|---|---|
| تکرار نامحدود | زنده بودن ظاهری | عمق صف، اعمالهای تکراری | هر تایماوت در بکاند مشترک کند |
| سمافور کلاینت | حد مجاز در مسیر موفق | اصل حیاتی در مسیر شکست | ترتیب دقیق رویدادها در مثال ابتدایی |
| اجاره نسل | حداکثر یک اعمال در هر گام | رد/انتظار اضافی | نیاز به موازیسازی واقعی بیش از bound |
| پذیرش گیتوی | حد مجاز + بدنه دیرهنگام کمتر | جفت شدن صفحه کنترل با فروشنده | بکاندی که نمیتواند لغو کند |
| ادغام پرامپتها | بار سیستم | صحت در صورت تفاوت پرامپتها | برنامهریزهای ابزارمحور با وضعیت محلی گام |
این رویکرد صحت (correctness) را بر زنده بودن ظاهری (apparent liveness) ترجیح میدهد. در حالی که تکرارهای نامحدود ممکن است به نظر برسد که عامل را در حرکت نگه میدارند، آنها فقط عمق صف و اعمالهای تکراری را افزایش میدهند. من ابتدا اجاره با شمارش نسل را در سمت برنامهریز انتخاب میکنم، زیرا میتوانم آن را بدون کمک فروشنده پیادهسازی کنم. من تکرار نامحدود را نمیپذیرم زیرا توان عملیاتی ساختگی ایجاد میکند که صف نمیتواند آن را تامین کند.
این سیستم برای چتهای تعاملی تککاربره که هرگز fan-out نمیشوند در نظر گرفته نشده است، و نباید به عنوان یک سیستم صورتحساب، زمانبند عدالت (fairness scheduler) یا وعدهای برای ظرفیت تولیدی استنتاج رایگان مشترک استفاده شود. شبیهساز توکنها، دلارها یا صدکهای تأخیر را اندازهگیری نمیکند؛ بلکه اندازهگیری میکند که آیا نسلهای منقضیشده هنوز میتوانند ثبت شوند یا خیر. اگر یک برنامهریز باید استریمهای جزئی گمانهزن (speculative partial streams) را اعمال کند، این طراحی عمداً آن کار را رد میکند؛ متن جزئی، وضعیت ثبتنشده است و نباید یک فراخوانی ابزار صادر کند. برای تجربه کاربری استریمینگ، توکنها باید بافر شوند و تنها پس از نسل نهایی ثبت گردند. آیا محصول شما میتواند با این تأخیر کنار بیاید، یا در حال فروش یک آتشبار توکن زنده هستید؟
دسترسی رایگان به مدل و سرور رایگان در اینجا فقط به عنوان یک صف مشترک که من کنترلی روی آن ندارم مفید هستند. آنها جایگزین یک مدل بار (load model) نمیشوند و به تنهایی P1 تا P4 را درست نمیکنند. اگر نمیتوانید ابتدا فیکچر را بهصورت محلی اجرا کنید، فعلاً یک صف راه دور را وارد داستان نکنید.
تکرارهای آینده
زمانی که میز پذیرش سمت کلاینت اصل حیاتی را حفظ کرد، گام بعدی انتقال لغو (cancellation) به داخل خود درخواست است. انقضای اجاره باید یک توکن لغو ارسال کند که بکاند آن را رعایت کند و بدین ترتیب حوزه A و حوزه B همسو شوند. علاوه بر این، ذخیره دائمی میز پذیرش در لاگ اجرای ارزیابی، از رستاخیز یک نسل قدیمی توسط یک کارگر کرش کرده جلوگیری میکند. این موضوع در بحث تثبیت سختافزاری در برابر تاریخچه چت برای دستیابی به تکرارپذیری کامل در عاملها نیز حیاتی است. من انتقال پذیرش به گیتوی را به عنوان تغییر بعدی در نظر میگیرم، نه اولین تغییر.
مقیاسپذیری بدون حد مجاز فقط Stampede را تکثیر میکند، بنابراین داستانهای autoscaling اولویت ندارند. علاوه بر این، کیفیت مدل باید از این حلقه کنترل خارج بماند. شکستهای Schema و بدنههای خالی، خطاهای پروتکل هستند، اما حصار متفاوتی دارند. مخلوط کردن امتیازات کیفیت در پذیرش، روشی است که توسعهدهندگان بهاشتباه با آن باگهای Retry را تحت عنوان «امروز مدل بد بود» پنهان میکنند.
مثال متضاد پایانی
این ترتیب را تصور کنید: acquire g=4، ارسال R1، expire g=4، شکست acquire چون bound=1 است، و سپس R1 با یک برنامه ابزار JSON عالی تکمیل میشود. آیا سیستم باید آن برنامه را رد کند، یک acquire جدید را بازپخش کند، یا با اعمال آن جبران کند چون متن معتبر به نظر میرسد؟
پاسخ صحیح این است که رد کند و سپس acquire را تنها زمانی بازپخش کند که in_flight اجازه دهد. هرگز نباید با اعتماد به یک بدنه دیرهنگام، جبران کرد. کدام ترتیب رویداد در ردپاهای (traces) شما هنوز آن برنامه دیرهنگام را اعمال میکند و لاگ شما در واقع کدام یک از این سه فعل را پیادهسازی کرده است؟
گام بعدی شما
- اگر از عاملهای هوش مصنوعی در محیطهای توزیعشده استفاده میکنید، لاگهای خود را برای یافتن پاسخهای دیرهنگام (Late Completions) که پس از Retry اعمال شدهاند، بررسی کنید.
- یک لایه «میز پذیرش» ساده با شمارنده نسل (Generation Counter) را بین کلاینت API و برنامهریز عامل خود پیادهسازی کنید.
- برای تست استواری سیستم، عمداً تأخیرهای مصنوعی در پاسخهای API ایجاد کنید تا ببینید آیا عامل شما دستورات تکراری اجرا میکند یا خیر.
اما داستان سختافزاری این تحول حتی شگفتانگیزتر است — به تحلیل ما دربارهی تراشههای Blackwell مراجعه کنید.




گفتگو