در پیادهسازی سرویسهای مبتنی بر مدلهای زبانی، اجرای یک حلقهی خام در PyTorch هزینهی سرسامآوری به همراه دارد و پاسخدهی را بهشدت کُند میکند. روشهای سنتی بچینگ نیز با پدینگ بیهوده، بخش بزرگی از حافظهی پردازنده گرافیکی (GPU) را میسوزانند و درخواستهای سریع را معطل کندترین پرامپتها نگه میدارند.
موتورهای مدرن مثل vLLM این بنبست را با سه سازوکار بنیادین حل میکنند: پجد اتنشن (PagedAttention) برای از بین بردن اتلاف حافظهی کش کلید-مقدار، دستهبندی پیوسته (Continuous Batching) برای ورود و خروج بیوقفهی درخواستها در هر تکرار، و پیشپرکنی تکهتکه (Chunked Prefill) برای تثبیت تاخیر بینتوکنی کاربران.
با توجه به اینکه اجارهی GPUهای قدرتمند خارجی برای توسعهدهندگان ایرانی دشوار و پرهزینه است، آشنایی با معماری داخلی این موتورها و زمانبند آنها اهمیتی دوچندان دارد؛ تسلط بر این تکنیکها امکان میدهد تا آخرین قطرهی بازدهی را از سختافزارهای در دسترس استخراج کنید.
نرخ پردازش موتورهای سرو نوین حاصل ترکیب همافزای سه سازوکار پیشفرض است، نه یک ترفند منفرد. پجد اتنشن (PagedAttention) همیشه روشن است. دستهبندی پیوسته (Continuous Batching) بین تکرارهای رمزگشایی (Decode)، درخواستهای تازه را وارد دستهی فعال میکند. پیشپرکنی تکهتکه (Chunked Prefill) پرامپتهای طولانی را قاچقاچ میکند تا توکنهای دیکود هرگز معطل نمانند. با روشن کردن همزمان هر سه، مدل Llama 3.3 70B FP8 روی یک شتابدهندهی H100 SXM5 با 128 درخواست همزمان، به 2,200 تا 2,400 توکن بر ثانیه push میکند؛ یعنی حدود 25% بالاتر از پیشفرض خود vLLM و 3 تا 4 برابر یک حلقهی خام PyTorch. در این درس، اسکژولر (Scheduler) و کرنل (Kernel) اتنشنِ vLLM — موتور مرجع برای پیادهسازی این سه تکنیک — را در سطحی بررسی میکنیم که بتوانید دیاگرام آن را رسم کنید؛ سپس کار را با یک بچر پیوستهی جمعوجور در
code/main.pyتمام میکنیم که پیشپرکنی (Prefill) و دیکود را دقیقاً به سبک vLLM زمانبندی میکند.
اهداف یادگیری
- پجد اتنشن را در قالب یک تخصیصدهندهی کش کلید-مقدار (KV cache) توضیح دهید: بلوکها، جدولهای بلوک و دلایل اینکه چرا اتلاف حافظه زیر بار محیط عملیاتی (Production) کمتر از 4% باقی میماند.
- دستهبندی پیوسته را در سطح تکرارها دیاگرام کنید: خروج توالیهای پایانیافته از دسته و ورود درخواستهای جدید بدون نیاز به تخلیهی کامل آن.
- پیشپرکنی تکهتکه را در یک جمله توصیف کنید و بگویید از کدام معیار تاخیر (Latency) محافظت میکند (راهنمایی: از دنبالهی تاخیر بینتوکنی در دیکودهای همزمان محافظت میکند، نه نرخ پردازش میانگین).
- پیش از فعالسازی همزمان همهی بهینهسازیها، ترکیب قابلیتهای مدنظرتان را با ماتریس سازگاری نسخهی دقیق vLLM خود بسنجید.
صورت مسئله
یک حلقهی سادهی سرو مدل در PyTorch درخواستها را یکییکی پردازش میکند: توکنایز کردن، پیشپرکنی، دیکود تا توکن پایان (EOS) و برگرداندن پاسخ. این روش برای یک کاربر جواب میدهد؛ اما برای صد کاربر، صفی طویل از آدمهای صبور میسازد. راهحل بدیهی — یعنی بچینگ ایستا (Static batching) — تمام درخواستها را با پدینگ (Padding) به اندازهی طولانیترین پرامپتِ پنجره میرساند، تمام دیکودها را با پدینگ تا طولانیترین خروجی مورد انتظار کش میدهد و کل دسته را معطل کندترین توالی نگه میدارد. شما تاوان پدینگی را میدهید که هرگز استفاده نمیکنید و درخواستهای سریع باید معطل کندها بمانند.
موتور vLLM سه مشکل را همزمان حل میکند. پجد اتنشن جلوی هدررفت 60 تا 80 درصدی حافظهی GPU را بهخاطر قطعهقطعه شدن کش کلید-مقدار — اتفاقی که در تخصیص کلاسیکِ پیوسته در حافظه (Contiguous) میافتد — میگیرد. دستهبندی پیوسته اجازه میدهد درخواستها بین هر تکرار دیکود وارد دسته شده یا از آن خارج شوند؛ در نتیجه دسته همیشه پر از کار واقعی است. پیشپرکنی تکهتکه نیز یک پرامپت 32 هزار توکنی را به قاچهای تقریباً 512 توکنی میشکند و آنها را لابهلای دیکود قرار میدهد تا اجرای یک پرامپت طولانی، روند دیکود بقیهی توکنها را روی GPU فریز نکند.
در سال 2026، پیشفرض محیطهای عملیاتی فعال بودن هر سه ویژگی است. باید بدانید هر کدام دقیقاً چه کار میکنند، چرا که تمام حالتهای شکست (Failure modes) متوجه اسکژولر هستند، نه مدل.
مفاهیم پایه
پجد اتنشن بهمثابهی سیستم حافظهی مجازی
اندازهی کش کلید-مقدار برای هر توالی برابر با فرمول num_layers × 2 × num_heads × head_dim × seq_len × bytes_per_element است. برای مدل Llama 3.3 70B با طول 8192 توکن در فرمت BF16، این رقم به ازای هر توالی تقریباً به 1.25 گیگابایت میرسد. اگر به ازای هر درخواست 8192 اسلات رزرو کنید اما میانگین طول یک درخواست تنها 1500 توکن باشد، عملاً حدود 82% از حافظهی پرسرعت (HBM) رزروشده را هدر دادهاید. روشهای کلاسیک بچینگ این تاوان را پرداخت میکنند.
پجد اتنشن این ایده را از حافظهی مجازی در سیستمعامل وام گرفته است. کش کلید-مقدار برای هر توالی پیوسته در حافظه تخصیص نمییابد؛ بلکه در قالب بلوکهایی با اندازهی ثابت (پیشفرض 16 توکن) تخصیص داده میشود. هر توالی یک جدول بلوک (Block table) دارد که موقعیت منطقی توکنهای آن را به شناسهی بلوکهای فیزیکی نگاشت میکند. وقتی طول یک توالی از ظرفیت بلوکهای فعلی فراتر برود، یک بلوک دیگر به آن اضافه میشود؛ پس از پایان کار نیز بلوکهای آن به استخر حافظه بازمیگردند.
اتلاف حافظه از 60 تا 80 درصد در روش کلاسیک، به کمتر از 4 درصد در پجد اتنشن کاهش مییابد. پجد اتنشن را با هیچ فلگی فعال نمیکنید — چون تنها تخصیصدهندهای است که vLLM ارائه میدهد. پیچ تنظیم شما --gpu-memory-utilization (پیشفرض 0.9) است که به vLLM میگوید پس از بارگذاری وزنها و اکتیویشنها، چه کسری از HBM را برای بلوکهای کش کلید-مقدار رزرو کند.
دستهبندی پیوسته در سطح تکرارها
روش قدیمی «بچینگ پویا» منتظر یک پنجرهی زمانی (مثلاً 10 میلیثانیه) میماند تا یک دسته پر شود؛ سپس چرخهی پیشپرکنی + دیکود + دیکود + دیکود را تا پایان کار تمام توالیها ادامه میداد. توالیهای کوتاه زودتر تمام میشدند و در حین تکمیل توالیهای کند توسط GPU، بیکار معطل میماندند.
دستهبندی پیوسته بین تکتک گامهای دیکود عمل میکند. بیایید مجموعهی توالیهای در حال اجرا را فهرست RUNNING بنامیم. در هر تکرار:
- هر توالی در فهرست
RUNNINGکه به توکن EOS یا سقف max_tokens رسیده باشد، حذف میشود. - اسکژولر صف انتظار را بررسی میکند. اگر بلوکهای آزاد برای کش کلید-مقدار وجود داشته باشد، توالیهای جدید (پیشپرکنیهای تازه یا موارد متوقفشده) را پذیرش میکند.
- گذر رو به جلو (Forward pass) روی تمام موارد حاضر در فهرست
RUNNINGاجرا میشود و یک توکن جدید به ازای هر توالی صادر میکند.
اندازهی دسته هرگز با پدینگ به یک عدد ثابت نمیرسد. توالیهایی در موقعیتهای مختلف از خروجی خود، یک گذر رو به جلوی ادغامشده را با هم به اشتراک میگذارند. در نگارشهای 2026 موتور vLLM، به این معماری V1 scheduler میگویند. اصل پایدار (Invariant) کلیدی این است: اسکژولر به ازای هر تکرار دیکود اجرا میشود، نه به ازای هر درخواست.
پیشپرکنی تکهتکه از تاخیر دنباله در ITL محافظت میکند
پیشپرکنی وابسته به محاسبات (Compute-bound) است. پیشپرکنی یک پرامپت 32 هزار توکنی برای Llama 3.3 70B روی یک H100، حدود 800 میلیثانیه زمان خالص محاسباتی میبرد. در تمام طول پیشپرکنی، توکنهای دیکود برای بقیهی توالیهای دسته معطل میمانند. در چرخهی سرو مدل، تاخیر در تولید نخستین توکن (TTFT) ناشی از یک پرامپت طولانی، بهصورت جهش در تاخیر بینتوکنی (ITL) برای دهها کاربر دیگر ظاهر میشود.
پیشپرکنی تکهتکه این فاز را به تکههایی با اندازهی مشخص (پیشفرض 512 توکن) تقسیم میکند و هر تکه را بهعنوان یک واحد زمانبندی میکند. بین تکهها، اسکژولر میتواند توالیهای در حال دیکود را یک توکن به جلو ببرد. در ازای افت ناچیز در تاخیر مطلق پیشپرکنی (چند میلیثانیه به ازای هر تکه)، از نوسان تصادفی (Jitter) زمان دیکود به میزان چشمگیری کم میشود. در بنچمارکهای منتشرشده، صدک 99 تاخیر بینتوکنی (P99 ITL) زیر بار ترافیکی مختلط، از حدود 50 میلیثانیه به 15 میلیثانیه کاهش مییابد.
اندرکنش این سه سازوکار پیشفرض
هر سه ویژگی عملکرد یکدیگر را مفروض میدانند. پجد اتنشن یک منبع ریزدانه از کش کلید-مقدار را برای مانور به اسکژولر میدهد. دستهبندی پیوسته به این منبع ریزدانه نیاز دارد تا پذیرش یک توالی جدید، نیازمند بازآرایی کلی حافظه نباشد. پیشپرکنی تکهتکه نیز تصمیمی است که اسکژولر روی همان فهرست RUNNING میگیرد — این سازوکار صرفاً یک سیاست اسکژولر دیگر است، نه یک سیستم مجزا.
نیازی نیست تکتک فلگها را بدانید. کافی است بدانید اسکژولر چه چیزی را بهینهسازی میکند: بیشینهسازی نرخ پردازش موثر (Goodput) تحت بودجهی بلوکهای کش کلید-مقدار، با رعایت قاچهای پیشپرکنی تکهتکه.
ماتریس سازگاری را بررسی کنید
پیش از فعالسازی همزمان همهی قابلیتها، حتماً سازگاری ترکیب آنها را با ماتریس سازگاریِ دقیقاً همان نسخهی vLLM خود بسنجید؛ چرا که ترکیبپذیری ویژگیها بین نسخههای مختلف تغییر میکند. در v0.18.0، ماتریس ویژگیها دیکودینگ حدسی (Speculative Decoding) را با پیشپرکنی تکهتکه و کش پیشوند (Prefix Caching) سازگار میداند؛ اما صفحهی دیکودینگ حدسی به دو ناسازگاری شناختهشده اشاره کرده است: موازیسازی پایپلاین (Pipeline Parallelism) تا نسخهی v0.15.0 و دیکودینگ با مدل پیشنویس (Draft model) تا نسخهی v0.10.0. برای خودِ روش پیشنویس نیز در سال 2026 معمولاً انتخاب اول EAGLE-3 ("method": "eagle3") است که در بخش 17 · 05 پوشش داده میشود.
اعدادی که باید به خاطر بسپارید
- مدل Llama 3.3 70B FP8، شتابدهندهی H100 SXM5 با 128 درخواست همزمان، فعال بودن هر سه قابلیت: 2,200 تا 2,400 توکن بر ثانیه.
- همان مدل در حالت پیشفرض vLLM (بدون پیشپرکنی تکهتکه): حدود 1,800 توکن بر ثانیه.
- همان مدل با یک حلقهی سادهی گذر رو به جلو در PyTorch: حدود 600 توکن بر ثانیه.
- هدررفت ناشی از اتلاف حافظهی کش کلید-مقدار در پجد اتنشن زیر بار عملیاتی: کمتر از 4%.
- صدک 99 تاخیر بینتوکنی (P99 ITL) زیر بار کاری مختلط: حدود 15 میلیثانیه با پیشپرکنی تکهتکه، حدود 50 میلیثانیه بدون آن.
ساختار اسکژولر در عمل
while True:
finished = [s for s in RUNNING if s.is_done()]
for s in finished: release_blocks(s); RUNNING.remove(s)
while WAITING and have_free_blocks_for(WAITING[0]):
s = WAITING.pop(0)
allocate_initial_blocks(s)
RUNNING.append(s)
# schedule prefill chunks + decode in one batch
batch = []
for s in RUNNING:
if s.in_prefill:
batch.append(next_prefill_chunk(s)) # e.g. 512 tokens
else:
batch.append(decode_one_token(s)) # 1 token
run_forward(batch) # one fused GPU call
فایل code/main.py دقیقاً همین حلقه را در پایتون استاندارد با تعداد توکنهای فرضی و تاخیر گذر رو به جلوی شبیهسازیشده پیاده میکند. با اجرای آن میبینید که چطور پیشپرکنی تکهتکه، توالیهای دیکود را در طول یک پیشپرکنی طولانی زنده نگه میدارد.
استفاده از ابزارهای واقعی
اسکریپت code/main.py یک اسکژولر شبیه به vLLM را با قابلیت فعال و غیرفعال کردن ویژگیها شبیهسازی میکند. آن را اجرا کنید تا نتایج حالتهای زیر را ببینید:
- حالت
NAIVE: یک درخواست در هر لحظه، بدون هیچ بچینگی. - حالت
STATIC: پدینگ و انتظار در صف، مطابق روش کلاسیک بچینگ. - حالت
CONTINUOUS: پذیرش و ترخیص در سطح تکرارها. - حالت
CONTINUOUS + CHUNKED: قرار گرفتن قاچهای پیشپرکنی لابهلای گامهای دیکود.
خروجی برنامه نرخ پردازش کل (توکن بر ثانیهی مجازی)، میانگین TTFT و صدک 99 تاخیر بینتوکنی (P99 ITL) را نشان میدهد. ردیف CONTINUOUS + CHUNKED در ترافیک مختلط عملکرد بهتری به نمایش میگذارد.
خروجی این درس
این درس فایل outputs/skill-vllm-scheduler-reader.md را تولید میکند. این ابزار با گرفتن پیکربندی سرو (اندازهی دسته، میزان بهرهبرداری از حافظهی کش کلید-مقدار، اندازهی تکههای پیشپرکنی و تنظیمات دیکودینگ حدسی)، عیبیابی اسکژولر را انجام میدهد، مشخص میکند کدامیک از این سه پیشفرض گلوگاه شده و چه مواردی را باید تنظیم کرد.
تمرینها
- اسکریپت
code/main.pyرا اجرا کنید. روی یک بار کاری شامل درخواستهای کوتاه و طولانی، حالتSTATICرا باCONTINUOUSمقایسه کنید. این اختلاف نرخ پردازش از کجا ناشی میشود — بازدهی پیشپرکنی، بازدهی دیکود یا تاخیر دنباله (Tail latency)؟ - اسکژولر فرضی را طوری تغییر دهید که گزینهی
--max-num-batched-tokensبه آن اضافه شود. مقدار بهینهی آن برای یک H100 در حال اجرای Llama 3.3 70B FP8 چقدر است؟ (راهنمایی: این مقدار تابعی از اندازهی بلوکهای کش کلید-مقدار و تعداد بلوکهای آزاد است، نه حجم خام HBM.) - یادداشتهای انتشار نگارش v0.18.0 را در vLLM دوباره مرور کنید. کدام ترکیب از فلگها مانعهالجمع هستند؟ آنها را فهرست کنید.
- هدررفت ناشی از اتلاف کش کلید-مقدار را برای یک ردگیری محاسبات (Trace) شامل 1000 درخواست با میانگین 1500 توکن خروجی و انحراف معیار 600 توکن محاسبه کنید: الف) تحت تخصیص پیوسته بهازای هر درخواست با سقف 8192 توکن، ب) با پجد اتنشن و بلوکهای 16 توکنی.
- در یک بند توضیح دهید که چرا پیشپرکنی تکهتکه بهتنهایی P99 ITL را بهبود میدهد اما بر نرخ پردازش اثری ندارد. در عمل، برتری نرخ پردازش از کجا به دست میآید؟
اصطلاحات کلیدی
| اصطلاح | باور رایج | تعریف دقیق فنی |
|---|---|---|
| PagedAttention | «ترفند KV» | تخصیصدهندهی بلوکهایی با اندازهی ثابت برای کش کلید-مقدار؛ اتلاف حافظه کمتر از 4% |
| Block table | «همان جدول پیج» | نگاشت اختصاصی هر توالی از موقعیت منطقی توکن به بلوک فیزیکی کش کلید-مقدار |
| Continuous batching | «همان بچینگ پویا، ولی درست» | تصمیمگیری برای پذیرش/ترخیص در پایان هر تکرار دیکود |
| Chunked prefill | «تکهتکه کردن پیشپرکنی» | شکستن پیشپرکنی طولانی به قاچهای 512 توکنیِ متناوب با دیکود |
| TTFT | «زمان اولین توکن» | پیشپرکنی + صف + شبکه؛ در پرامپتهای طولانی فاز پیشپرکنی بر آن حاکم است |
| ITL | «تاخیر بینتوکنی» | زمان بین دو توکن پیاپی دیکود؛ اندازهی دسته عامل تعیینکنندهی آن است |
| Goodput | «نرخ پردازش منطبق با SLO» | درخواستها در ثانیهای که همچنان به اهداف TTFT و ITL میرسند (فاز 17 · 08 آن را بهصورت سهمی از درخواستها گزارش میکند) |
| V1 scheduler | «اسکژولر جدید» | اسکژولر نسخههای 2026 در vLLM؛ اجرای دستهبندی پیوسته همراه با پیشپرکنی تکهتکه |
--gpu-memory-utilization |
«پیچ تنظیم حافظه» | کسری از HBM که پس از بارگذاری وزنها و اکتیویشنها، برای بلوکهای کش کلید-مقدار رزرو میشود |
برای مطالعهی بیشتر
- مستندات vLLM — بخش Speculative Decoding — منبع رسمی دربارهی سازگاری پیشپرکنی تکهتکه با دیکودینگ حدسی.
- یادداشتهای انتشار vLLM (انویدیا) — چرخهی انتشار در سال 2026 و رفتارهای اختصاصی هر نسخه.
- وبلاگ vLLM — معرفی PagedAttention — مقالهی اصلی که هنوز شیوهی درک تخصیصدهندهی حافظه را تعریف میکند.
- مقاله PagedAttention در آرکایو (arXiv:2309.06180) — تحلیل اتلاف حافظه و طراحی اسکژولر.
- بررسی عمیق درون vLLM به قلم Aleksa Gordic — بررسی موشکافانهی اسکژولر V1 همراه با نمودارهای فلیم (Flame graphs).
