در سیستمهای پژوهشی خودکار، مدلها یا کدهای تولیدشده توسط فرآیند خودکار نباید مستقیماً در بستر پردازشی اصلی اجرا شوند. خطای حافظه، کرش ناگهانی یا حلقههای بیپایان در اسکریپت آزمایش بهسادگی میتوانند هماهنگکننده را متوقف کنند و دادههای پیشین را از بین ببرند.
راهکار پایهای و کارآمد برای حل این چالش، استفاده از زیرفرایندها (Subprocesses) است. با تعریف یک مشخصات نوعدار (Typed Spec)، اسکریپت در یک فضای مجزا، با سقف زمانی و نظارت بر مصرف حافظه اجرا میشود و نتایج به شکل دادههای متریک ساختاریافته به سیستم ارزیابی بازگردانده میشوند.
هر چرخهی پژوهشی فقط به اندازهی سنجشهایش قابلاعتماد است. در این درس، رانری (runner) میسازیم که یک مشخصات آزمایش (spec) را تحویل میگیرد، آن را در یک زیرفرایند (subprocess) ایزوله (sandboxed) اجرا میکند و در نهایت یک خروجی ساختاریافتهی JSON تحویل ارزیاب میدهد تا بتوان به دادههایش تکیه کرد.
اهداف یادگیری
- تعریف آزمایش در قالب یک مشخصات نوعدار (typed spec) تا رانر بتواند آن را برای یک زیرفرایند سریالسازی کند.
- اجرای زیرفرایند با سقف زمانی واقعی (hard wall clock timeout) و سقف حافظهی غیرقطعی (soft memory cap)، و ثبت هر دو مورد بهعنوان شرایط خاتمهی اجرا.
- ثبت stdout، stderr و دادههای متریک ساختاریافته در قالب یک رکورد نتیجهی یکپارچه.
- ساخت جدول آزمون مقایسهای سیستماتیک (ablation table) که هر بار فقط یک متغیر تنظیمی را روی یک مشخصات پایهی ثابت تغییر میدهد.
- حفظ قطعیتپذیری (determinism) تمام نتایج به ازای هر سید (seed)، تا ارزیاب در اجراهای مختلف اعداد کاملاً یکسانی ببیند.
چرا اجرای آزمایش در زیرفرایند؟
یک چرخهی پژوهشی، کدهای غیرقابلاعتماد را اجرا میکند. فرضیه از یک نمونهگیر (sampler) آمده و اسکریپت آزمایش هم از دل همان مسیر بیرون آمده است؛ اگر بخواهیم هر کدام را کدی امن فرض کرده و درون همان فرایند اصلی اجرا کنیم، باید منتظر کرشی باشیم که کل هماهنگکننده (orchestrator) را از کار میاندازد. زیرفرایندها سادهترین سازوکار ایزولهسازی هستند که زبان در اختیارتان میگذارد: یک فرایند (process) مجزا، یک فضای آدرسدهی مستقل و یک دستگیرهی سیگنال در سمت فرایند والد.
رانری که در اینجا میسازیم، یک ساندباکس (sandbox) کامل پیادهسازی نمیکند؛ خبری از cgroup، فیلتر seccomp یا نگاشت مجدد namespace نیست. چیزی که این رانر دارد، یک سقف زمانی واقعی، یک حلقهی پایش برای بررسی رشد مصرف حافظه، و مسیر توقفی است که با رسیدن به هر یک از این دو حد، فرایند را میکشد. این همان قرارداد زمان اجرا (runtime contract) است که ساندباکسهای پیشرفتهتر هم بر پایهی آن توسعه مییابند. در این درس، این قرارداد را آنقدر جمعوجور نگهداشتهایم که بشود در یک نشست آن را خواند و فهمید.
ساختار ExperimentSpec
ExperimentSpec
spec_id : str (stable id, "exp_001")
hypothesis_id : int (link back to the queue from lesson 50)
script_path : str (path to the python script to run)
config : dict (passed to the script as one json arg)
seed : int (deterministic seed for the experiment)
wall_timeout_s : float (hard timeout, killed on exceed)
memory_cap_mb : int (soft cap, polled; killed on exceed)
metric_keys : list[str] (which fields the evaluator will read)
اسکریپت روی دیسک قرار دارد؛ رانر پیکربندی را داخل یک فایل موقت مینویسد و اسکریپت آن مسیر را میخواند. انتظار میرود اسکریپت یک سطر JSON روی stdout چاپ کند که کلیدهای آن، ابرمجموعهای از metric_keys باشند. هر چیز دیگری که روی stdout بیاید ثبت میشود، ولی پارسرِ متریکها آن را نادیده میگیرد.
معماری
رانر تنها یک کلاس با یک متد اصلی است. پایشگر (poller) یک ترد (thread) کوچک است که در هر بازهی پایش یکبار بیدار میشود و در صورت دسترسی، معادل psutil را برای زیرفرایند از سیستمفایل proc میخواند؛ اگر پلتفرم چنین قابلیتی در اختیارتان نگذارد، به یک عملیات بیاثر (no-op) برمیگردد.
چرا سقف حافظه را غیرقطعی در نظر میگیریم؟
اعمال سقف حافظهی قطعی نیازمند resource.setrlimit است و فقط روی سیستمهای POSIX کار میکند. در این درس رویکردی پرتابل ارائه میدهیم: اندازهی حافظهی مقیم فیزیکی (Resident Set Size یا RSS) را از پلتفرم استعلام میکنیم و اگر از سقف مدنظر فراتر رفت، زیرفرایند را میکشیم. این سقف از آن جهت غیرقطعی است که پایشگر بازهی زمانی غیرصفر دارد؛ بنابراین ممکن است مصرف حافظهی یک فرایند در فاصلهی بین دو پایش لحظهای اوج بگیرد و دوباره پایین بیاید. رانر بیشترین RSS مشاهدهشده را ثبت میکند تا ارزیاب ببیند اجرا چقدر به مرز محدودیت نزدیک شده است.
روی سیستمهایی که از بازرسی فرایند پشتیبانی نمیکنند، پایشگر یک هشدار یکباره لاگ میکند و غیرفعال میشود. سقف زمانی واقعی همچنان اعمال خواهد شد. آزمونهای این درس هر دو مسیر را پوشش میدهند.
دریافت خروجیهای stdout و stderr
رانر پس از پایان کار، محتوای هر دو لوله (pipe) را کاملاً میخواند. خروجی stdout خطبهخط بررسی میشود؛ آخرین خطی که فرمت معتبر JSON داشته باشد و همهی metric_keys لازم را پوشش دهد، بهعنوان دادهی نهایی متریکها ثبت میشود. سطرهای JSON قبلی در قالب intermediate_metrics در نتیجه نگه داشته میشوند؛ ارزیاب میتواند از این دادهها برای رسم منحنی یادگیری (learning curve) استفاده کند.
خروجی stderr بدون تغییر در نتیجه ثبت میشود. رانر روی کدهای خروج غیرصفر هیچوقت اکسپشن صادر نمیکند، بلکه کد خروج را مستقیماً در نتیجه مینویسد. هر کد خروج غیرصفر با برچسب "crash" ثبت میشود، حتی اگر اسکریپت پیش از آن متریکها را چاپ کرده باشد؛ بنابراین ارزیاب بهطور پیشفرض اجراهای ناقص را ناموفق در نظر میگیرد.
جدول آزمون مقایسهای سیستماتیک
def ablate(base: ExperimentSpec, knob: str, values: list[Any]) -> list[ExperimentSpec]:
...
تابع کمکی با دریافت یک مشخصات پایه و نام یک متغیر تنظیمی، به ازای هر مقدار جدید، یک مشخصات تازه برمیگرداند که config[knob] در آن بازنویسی شده است. به هر مشخصات، یک spec_id مشتقشده تعلق میگیرد (f"{base.spec_id}_{knob}_{value}"). رانر ابزاری به نام AblationRunner ارائه میدهد که این موارد را به ترتیب اجرا میکند و یک AblationTable خروجی میدهد که کلیدهایش همان مقادیر متغیر هستند.
چرا در هر مرحله فقط یک متغیر؟ بررسی تمام حالتها بهصورت ترکیبی (full factorial sweeps) بهصورت نمایی منفجر میشود و نتایجی تولید میکند که ارزیاب نمیتواند آنها را تفسیر کند. تغییر یک متغیر در هر مرحله، محوری تمیز برای ارزیاب میسازد تا بتواند آن را رسم کند. این درس از بررسی چندمتغیره فقط بهصورت اجرای مکرر آزمونهای تکمتغیرهای پشتیبانی میکند که توسط فراخواننده ترکیب شده باشند.
قطعیتپذیری
هر مشخصات یک سید (seed) با خود دارد. رانر این مقدار را از طریق دیکشنری کانفیگ به اسکریپت میفرستد (config["__seed"] = spec.seed). اسکریپتهای ماک در مسیر code/experiments/ این سید را لحاظ میکنند و در طول اجراهای مختلف، دقیقاً متریکهای یکسانی میسازند. ارزیاب درس پنجاهوسه به همین خصیصه متکی است؛ بدون قطعیتپذیری، ممکن است چیزی که به نظر «افت کارایی» میآید صرفاً ناشی از یک مقداردهی اولیهی تصادفی متفاوت باشد.
اسکریپت آزمایش شبیهسازیشده (Mock)
در این درس یک اسکریپت آزمایش ارائه شده است: code/experiments/sparsity_experiment.py. این یک اسکریپت واقعی است که فایل پیکربندی خود را میخواند، اجرای یک آموزش کوچک را با محاسبات تصادفی NumPy شبیهسازی میکند، و در آخر یک سطر متریک JSON چاپ میکند. این اسکریپت متغیر sleep_s را برای سنجش سقف زمانی و متغیر allocate_mb را برای تست پایشگر حافظه در نظر میگیرد.
این شبیهسازی در واقع چیزی را آموزش نمیدهد؛ صرفاً یک محاسبهی عددی است که ساختار حلقهی آموزش را شبیهسازی میکند: یک منحنی تابع زیان (loss)، پرپلکسیتی (perplexity) نهایی و زمان سپریشدهی واقعی. هدف اصلی این درس خودِ رانر است، نه شبیهسازی. یک اسکریپت آزمایش واقعی، یک مدل هوش مصنوعی را ایمپورت میکند.
ساختار نتیجه
ExperimentResult
spec_id : str
hypothesis_id : int
exit_code : int
terminal : "ok" | "timeout" | "oom" | "crash"
wall_time_s : float
peak_rss_mb : float | None
metrics : dict
intermediate_metrics : list[dict]
stdout_tail : str
stderr_tail : str
ارزیاب ابتدا دو فیلد metrics و terminal را میخواند. اگر فیلد terminal چیزی جز "ok" باشد، آزمایش بهعنوان اجرای ناموفق در نظر گرفته میشود و حکم ارزیاب خودکار صادر خواهد شد. در غیر این صورت، متریکها برای ارزیابی به آزمون معناداری آماری فرستاده میشوند.
راهنمای مطالعهی کدها
فایل code/main.py شامل کلاسهای ExperimentSpec، ExperimentResult، ExperimentRunner، AblationRunner و یک دموی قطعی است. مدیریت زیرفرایندها درون یک کلاس خلاصه شده، پایشگر حافظه یک ترد کوچک است، و تابع کمکی آزمون مقایسهای هم یک تابع تکی است.
فایل code/experiments/sparsity_experiment.py همان آزمایش شبیهسازیشدهای است که در آزمونها استفاده میشود؛ این اسکریپت مسیر فایل کانفیگ خود را از argv میخواند و پس از اتمام کار، یک خط خروجی متریک JSON مینویسد.
فایل code/tests/test_runner.py مسیر اجرای موفق، مسیر تایماوت، مسیر کرش، جدول آزمون مقایسهای و بررسی قطعیتپذیری نتایج در دو اجرای متوالی را پوشش میدهد.
جایگاه این درس در مسیر کلی دوره
در درس پنجاه فرضیه تولید میشود. در درس پنجاهویک هر موردی که در مقالات پیشین حل شده باشد کنار گذاشته میشود. در درس پنجاهودو برای آنچه باقی مانده آزمایش اجرا میشود. در درس پنجاهوسه ارزیاب نتیجه را میخواند، آزمون معناداری آماری را اجرا میکند و رأی نهایی را صادر میکند تا هماهنگکننده آن را در برابر شناسهی فرضیه ثبت کند.
