مدلهای زبانی کد مینویسند، اما تا زمانی که آن کد اجرا نشود و آزمونها را پاس نکند، عیار واقعی مدل مشخص نمیشود. اجرای مستقیم کدی که یک مدل تولید کرده با متدهایی مثل `exec`، سیستم میزبان شما را با خطرات امنیتی جدی، حلقههای بینهایت و مصرف بیرویهی منابع حافظه روبهرو میکند.
در بنچمارکهای مطرحی مثل HumanEval و MBPP، راهحل استاندارد برای سنجش خروجیها اجرای هر قطعهکد در یک زیرفرایند (subprocess) مجزا همراه با سقف زمانی و مهار خروجی است. این درس نشان میدهد چگونه بدون نیاز به ابزارهای پیچیده و صرفاً با امکانات استاندارد پایتون، یک بستر ارزیابی پایدار برای اجرای کدهای تولیدی بسازید و خطاهای گوناگون را ساختاریافته مدیریت کنید.
علاوه بر ایمنسازی اجرا، با فرمول ریاضی برآوردگر نااریب pass-at-k نیز آشنا میشوید تا بتوانید عملکرد مدل را در سناریوهای تولید چندگانه بهشکل دقیق و آماری ارزیابی و مقایسه کنید.
کد تولیدشده زمانی درست است که آزمونها را با موفقیت پشت سر بگذارد. بستر ارزیابی (eval harness) باید کد را استخراج کند، آن را طوری اجرا کند که سیستم میزبان از کار نیفتد و نرخ قبولی را صادقانه محاسبه کند. این درس چنین زیرساختی را پیادهسازی میکند.
اهداف یادگیری
- استخراج بلوک کد از خروجی متنی آزاد، بهگونهای که با قاعدهی پسپردازش درس 70 همخوانی داشته باشد.
- اجرای کد کاندید در یک زیرفرایند (subprocess) ایزوله با محدودیت زمانی واقعی (wall-clock timeout)، سقف حجم خروجی و فهرست سیاه واردات (import denylist).
- امتیازدهی به وظیفه بر اساس نسبت رشتههای اعتبارسنجی (assertions) ارائهشدهای که روی کاندید با موفقیت پاس میشوند.
- محاسبهی معیار pass-at-k برای وظایفی که در آنها چندین خروجی از یک مدل نمونهبرداری میشود.
- برخورد با کرش ساندباکس (محیط ایزوله)، خطاهای نگارشی (SyntaxError) و اتمام مهلت زمانی بهعنوان حالتهای شکست درجهیک، با کدهای خروج متمایز که اجراکننده بتواند آنها را ثبت کند.
چرا زیرفرایند ایزوله؟
استفادهی مستقیم از exec از نظر امنیت و پایداری خطرساز است. یک خط کد تولیدشده مثل while True: pass ارزیابی را برای همیشه مسدود میکند. کد تولیدشدهای مثل import shutil; shutil.rmtree('/') هم دقیقاً همانقدر فاجعهبار است که به نظر میرسد. راهحل این است که برای هر کاندید یک مفسر پایتون تازه راهاندازی کنیم، کد را از طریق stdin به آن بدهیم، نتایج اعتبارسنجی را در stdout بنویسیم و اگر فرایند از زمان تعیینشده فراتر رفت، آن را متوقف کنیم (kill کنیم). به این ترتیب فرایند ارزیابی میزبان همچنان به کارش ادامه میدهد.
ارزیابیهای واقعی مثل HumanEval، MBPP، BigCodeBench و LiveCodeBench همگی از ساندباکس مبتنی بر زیرفرایند استفاده میکنند. برخی از آنها لایهای از Docker را هم روی آن میآورند. ما به یک دلیل مشخص در حد زیرفرایند متوقف میشویم: قابلحمل است، جزو کتابخانهی استاندارد پایتون (stdlib) است و حالتهای شکستی را که برای ارزیابی آموزشی مهم هستند پوشش میدهد. پیادهسازیهای محیط عملیاتی (پروداکشن) مواردی مثل seccomp، ایزولهسازی شبکه و سیستمفایل فقطخواندنی را هم اضافه میکنند. درس بعدی دربارهی امنسازی، خارج از این مسیر آموزشی قرار دارد.
ساختار وظیفهی اجرای کد
یک وظیفهی code_exec شامل رشتههای اعتبارسنجی در targets است. اجراکننده بلوک کد محصورشده را از متن تولیدشده استخراج میکند، یک بستر آزمایشی (test harness) پیرامون آن میسازد و نتیجه را اجرا میکند.
امتیاز کسری در بازهی [0, 1] است. وظیفهای با سه اعتبارسنجی که دو تای آنها پاس شوند، امتیاز 0.667 میگیرد. صرفنظر از اینکه چه خطایی رخ دهد، خروجی اجراکننده ساختار ثابتی دارد: کرشهای زیرفرایند به یک کد خطای استاندارد نگاشت میشوند، نه اینکه ردگیری محاسبات خطای پایتون (traceback) به بستر آزمایشی نشت کند.
فهرست سیاه واردات
فهرست سیاه بر پایهی دستورات import عمل میکند. پیش از اجرای کد کاندید، اسکریپتِ اجراکننده کدهای واردکنندهی ماژولهای خطرناک را با یک تکهکد جایگزین (stub) بازنویسی میکند که خطای ImportError("denied") صادر میکند. این فهرست عمداً محافظهکارانه انتخاب شده است: os.system، subprocess، socket، requests، urllib، urllib.request، urllib.error، urllib.parse، ctypes، shutil، http.client، asyncio.subprocess.
ادعا نمیکنیم این روش نفوذناپذیر است. کد خرابکارانهی مصمم میتواند از هر ساندباکسِ درونفرایندی در پایتون فرار کند. فهرست سیاه فقط یک لایهی محافظتی پشتیبان است. کنترلهای اصلی و بنیادین، محدودیت زمانی واقعی و سقف حجم خروجی هستند.
DENIED = {
"os.system": True,
"subprocess": True,
"socket": True,
"shutil": True,
"requests": True,
"urllib": True,
"ctypes": True,
}
ما کد کاندید را با افزودن import sys در ابتدا و یک سازوکار محافظتی که os.system را با مانکیپچ (monkey-patch) به یک تابع خطاانداز تبدیل میکند، احاطه میکنیم. الگوی کامل در main.py موجود است.
محدودیت زمانی واقعی
به هر زیرفرایند بهطور پیشفرض بودجهی زمانی 3 ثانیهی واقعی داده میشود. اجراکننده از subprocess.run(..., timeout=t) استفاده میکند. اگر زمان به پایان برسد، اجراکننده استثنای TimeoutExpired را میگیرد، فرایند را متوقف میکند و دلیل خروج را timeout ثبت میکند. امتیاز این وظیفه صفر منظور میشود و اجراکننده به سراغ وظیفهی بعدی میرود.
این زمان مجاز با آرگومان timeout_s به تابع run_candidate داده میشود؛ اجراکنندهی درس 75 این مقدار را از طریق code_exec_timeout_s یکبار برای کل اجرا تنظیم میکند. آزمونهای واحد زمانبر میتوانند زمان بیشتری بخواهند، اما run_candidate این مقدار را بین 0.1 تا 30 ثانیه محدود میکند تا زمان اجرای مجموعه آزمونها مهارشده بماند.
سقف حجم خروجی
زیرفرایند ممکن است stdout را با دادههای متوالی پر کند و حافظهی میزبان را ببلعد. اجراکننده خروجی stdout را در یک بافر جریان میدهد و بهمحض اینکه مجموع حجم دادهها از 256 کیلوبایت بگذرد، فرایند فرزند را میکشد. نتیجه با exit_code = error و متن تکمیلی "output overflow" ثبت میشود. این حالت در عمل زمانی رخ میدهد که کد تولیدشده اشتباهاً یک حلقهی بینهایت بنویسد که مدام در خروجی چاپ میکند.
معیار Pass-at-k
معیار Pass-at-k برآوردگر نااریبی است که در HumanEval و موارد مشابه استفاده میشود. اگر برای هر وظیفه n نمونهی مستقل داشته باشیم و c مورد از آنها پاس شوند، احتمال اینکه در یک نمونه به اندازهی k از میان آن n نمونه، دستکم یک راهحل قبولی وجود داشته باشد برابر است با:
pass_at_k(n, c, k) = 1 - C(n - c, k) / C(n, k)
وقتی n - c < k باشد، طبق قاعدهی معمول صورت کسر یعنی C(n - c, k) صفر میشود (نمیتوانید k شکست را از بین تعداد کمتری شکست انتخاب کنید)؛ بنابراین مقدار نهایی برابر 1 خواهد بود. پیادهسازی ما این حالت خاص را بهطور مستقیم مدیریت میکند. ما تابع pass_at_k(n, c, k) را برای استفاده در لایهی جدول ردهبندی (leaderboard) در درس 74 ارائه میدهیم.
کدهای خروج
اجراکننده برای هر وظیفه یکی از پنج نتیجهی زیر را برمیگرداند:
pass: وقتی همهی اعتبارسنجیها پاس شوند.assertion_fail: وقتی کد اجرا شود اما دستکم یکی از اعتبارسنجیها شکست بخورد.syntax_error: وقتی کد import نشود یا خطای نحوی (SyntaxError) داشته باشد.timeout: وقتی مهلت زمانی تعیینشده به پایان برسد.error: برای هر کرش دیگر، شامل موارد فهرست سیاه و سرریز خروجی (سرریز با پیام تکمیلی"output overflow"مشخص میشود).
امتیاز همچنان بهصورت کسری است و کد خروج صرفاً دادهی توصیفی (متادیتا) به حساب میآید. درسهای بعدی میتوانند تصمیم بگیرند که اتمام زمان را صفر لحاظ کنند یا دادهی ازدسترفته.
این درس چه کارهایی انجام نمیدهد
این درس یک ساندباکس واقعی در اختیارتان نمیگذارد. کدهای غیرقابلاعتماد از اینترنت باز را اجرا نمیکند. وظایف دارای حالت مثل ورودی/خروجی فایل یا فراخوانیهای شبکه را هم مدیریت نمیکند. برای این موارد به یک کانتینر یا microVM نیاز دارید. هدف این درس پیادهسازی قرارداد است: زیرفرایند ایزوله، فهرست سیاه، محدودیت زمانی، سقف خروجی، مجموعهای مشخص از کدهای خروج و محاسبات ریاضی pass-at-k.
راهنمای خواندن کد
فایل main.py توابع extract_code، run_candidate، score_code_exec و pass_at_k را تعریف میکند. اسکریپت اجراکنندهی زیرفرایند بهصورت یک رشته ساخته شده و با فلگ -c به مفسر تازهی پایتون داده میشود. آزمونهای فایل code/tests/test_exec.py چهار کد خروج و تابع pass-at-k را روی نمونههای برگرفته از سبک HumanEval ارزیابی میکنند.
فایل main.py را از بالا به پایین بخوانید. الگوی اجراکننده (runner template) بخش کلیدی کار است. به حلقهی اعتبارسنجی آنقدر دقیق شوید تا بتوانید ساختار بستهی JSON ارسالی به فرایند والد را پیشبینی کنید.
گامهای بعدی
پس از اینکه سازوکار زیرفرایند درست کار کرد، مسئلهی بعدی قابلیت حمل است. نسخههای مختلف پایتون در ویندوز سیگنال SIGKILL را به شکلهای متفاوتی مدیریت میکنند. تمیزترین راهحل قرار دادن اجراکننده داخل یک ایمیج Docker است. مرحلهی بعد از آن، جایگزینی رشتههای اعتبارسنجی با فایلهای آزمون واحدِ واقعی است تا فرایند ارزیابی با خطوط لولهی CI در محیط عملیاتی همخوانی پیدا کند. از آن نقطه به بعد دیگر رشتههای اعتبارسنجی را آزمون ننامید؛ آنها آزمونهای اسباببازی هستند و حالتهای شکست سادهانگارانهای دارند.
