اگر تا به حال تماشای تولید یک پاسخ طولانی توسط یک LLM کرده‌اید و از خود پرسیده‌اید که چرا پس از آن جرقه اولیه، سرعت آن به شدت کاهش می‌یابد، در واقع در حال مشاهده یک گلوگاه سخت‌افزاری در لحظه هستید. اکثر توسعه‌دهندگان کد پایتون، فریم‌ورک یا حجم عظیم مدل را مقصر می‌دانند. آن‌ها توابع را پروفایل می‌کنند، بهینه‌سازها را تغییر می‌دهند و میلی‌ثانیه‌ها را از پیش‌پردازش کم می‌کنند. هیچ‌کدام از این‌ها مشکل اصلی را حل نمی‌کند. محدودیت سرعت در نرم‌افزار شما نیست؛ بلکه در سیلیکون است.

هر عملیات استنتاج (inference) در مدل‌های زبانی بزرگ به دو ویژگی فیزیکی GPU موجود در سرور شما بستگی دارد: سرعت انجام محاسبات و سرعت انتقال آن اعداد به موقعیت مورد نظر برای محاسبه.

ریاضیات ارزان است، اما انتقال داده نه

بازاریابی GPU عاشق صحبت از محاسبات است. تریلیون‌ها عملیات ممیز شناور در ثانیه. اعداد خیره‌کننده‌ای هستند. اما محاسبات تنها نیمی از داستان است. نیم دیگر، پهنای باند حافظه است؛ یعنی نرخی که داده‌ها از حافظه با پهنای باند بالا به هسته‌های محاسباتی (جایی که محاسبات واقعی انجام می‌شود) منتقل می‌شوند.

یک LLM نمی‌تواند سریع‌تر از ضعیف‌ترین حلقه از این دو حلقه کار کند. یک آشپزخانه تجاری با بیست سرآشپز ماهر را تصور کنید. اجاق‌ها داغ هستند، چاقوها تیز هستند و هر آشپز آماده است. اما محصولات با دوچرخه و هر بار فقط یک سبد، می‌رسند. آشپزخانه متوقف می‌شود. اضافه کردن آشپز بیشتر یا خرید اجاق‌های سریع‌تر مشکل را حل نمی‌کند. گلوگاه، جاده است.

در GPUهای مدرن مراکز داده، واحدهای محاسباتی چنان قدرتمند هستند که اغلب محاسبات خود را تمام کرده و سپس بیکار می‌نشینند و در حالی که منتظر جریان یافتن وزن‌ها و فعال‌سازها از حافظه هستند، سیکل‌های پردازشی را هدر می‌دهند. این عدم تعادل یک باگ در کد شما نیست؛ بلکه واقعیت فیزیکی ساخت تراشه‌هاست. پهنای باند حافظه با قدرت محاسبات خام همگام نشده است و LLMها به دلیل اینکه در هر مرحله عبور (forward pass) برای هر توکن خروجی باید با تک‌تک پارامترها درگیر شوند، نسبت به این عدم تعادل بسیار حساس هستند.

چرا پرامپت‌ها سریع و تولید متن کند به نظر می‌رسد

استنتاج LLM به دو مرحله متمایز تقسیم می‌شود که هر کدام فشار متفاوتی به سخت‌افزار وارد می‌کنند.

Prefill زمانی رخ می‌دهد که پرامپت شما برای اولین بار وارد مدل می‌شود. تمام توکن‌ها با هم می‌رسند. GPU می‌تواند آن‌ها را با استفاده از ضرب‌های ماتریس در ماتریس بزرگ، به صورت موازی پردازش کند. هزاران واحد محاسباتی همزمان فعال می‌شوند و بار کاری متراکم باقی می‌ماند. این مرحله Compute-bound (محدود به محاسبات) است. آن سرعت ناگهانی که در ابتدا می‌بینید؟ آن دقیقاً همان کاری است که GPU برای انجامش ساخته شده است.

Decode جایی است که اوضاع دشوار می‌شود. وقتی مدل توکن بعدی را تولید می‌کند، این کار را توکن به توکن انجام می‌دهد. این مرحله به عملیات ماتریس در بردار متکی است که تنها بخش بسیار کوچکی از ظرفیت موازی GPU را استفاده می‌کند. بدتر از آن، هر توکن جدید GPU را مجبور می‌کند تا تمام وزن‌های مدل را دوباره از حافظه بارگذاری کند. واحدهای محاسباتی تشنه‌ی کار هستند، اما در عوض منتظر می‌مانند. مرحله Decode Memory-bound (محدود به حافظه) است. GPU در واقع مانند یک کنترل‌کننده ترافیک گران‌قیمت عمل می‌کند که پارامترها را در حالی که موتورهای ریاضی در حال خنک شدن هستند، به رفت و برگشت در گذرگاه حافظه (memory bus) وادار می‌کند. به همین دلیل است که یک پاسخ صد کلمه‌ای می‌تواند ده ثانیه طول بکشد، حتی اگر تحلیل اولیه پرامپت آنی به نظر رسیده باشد.

وجود KV cache این موضوع را جالب‌تر هم می‌کند. در طول مرحله Decode، مدل تنسورهای کلید (key) و مقدار (value) را برای هر توکن قبلی ذخیره می‌کند تا مجبور نباشد توجه (attention) را از ابتدا دوباره محاسبه کند. این حافظه پنهان با طول توالی رشد می‌کند و در حافظه قرار دارد. بنابراین اکنون GPU نه تنها وزن‌ها را دوباره بارگذاری می‌کند، بلکه در هر مرحله عبور، در حال خواندن و نوشتن در یک کش در حال گسترش است. هسته‌های محاسباتی به سختی فعالیت می‌کنند، در حالی که گذرگاه حافظه برای هر دوی آن‌ها به شدت تحت فشار است.

مبارزه با دیوار حافظه

مهندسان مجموعه‌ای از تکنیک‌ها را برای کاهش میزان داده‌های مورد نیاز برای جابجایی، یا حداقل برای تقسیم هزینه جابجایی آن‌ها، توسعه داده‌اند.

Batching مستقیم‌ترین راه است. اگر درخواست یک کاربر باعث بارگذاری کامل وزن‌ها از حافظه شود، پردازش هشت یا شانزده درخواست به صورت همزمان به GPU اجازه می‌دهد تا آن بار را بین همه آن‌ها توزیع کند. وزن‌ها یک بار خوانده شده و برای هر توالی در آن دسته (batch) مجدداً استفاده می‌شوند. در محیط عملیاتی، سیستم‌های زمان‌بندی پیشرفته، درخواست‌ها را به صورت پویا گروه‌بندی می‌کنند که گاهی اوقات به آن continuous یا in-flight batching گفته می‌شود، تا GPU به ندرت متوقف شود. این تفاوت بین یک اتوبوس و شانزده خودروی مجزا در یک مسیر است.

کوانتیزاسیون مستقیماً با مشکل پهنای باند مقابله می‌کند. وزن‌های مدل معمولاً در قالب‌های ممیز شناور شانزده بیتی ذخیره می‌شوند. با فشرده‌سازی آن‌ها به اعداد صحیح هشت بیتی یا حتی چهار بیتی، شما عملاً مقدار داده‌های در حال انتقال در گذرگاه (bus) را به نصف یا حتی کمتر کاهش می‌دهید. مدل همچنان به دقت کافی برای تولید خروجی منسجم نیاز دارد، اما روش‌های مدرن کوانتیزاسیون پس از آموزش می‌توانند بدون تخریب کیفیت، میزان اشغال حافظه مدل را به طرز چشمگیری کاهش دهند. داده‌های کمتر در حال انتقال به معنای زمان کمتر برای انتظار در کنترل‌کننده حافظه است.

FlashAttention مکانیزم توجه را بازسازی می‌کند تا نتایج میانی را درون حافظه سریع روی تراشه (on-chip) در GPU نگه دارد. مکانیزم توجه استاندارد مجبور بود ماتریس‌های بزرگ توجه را در حافظه خارجی کند بنویسد و سپس دوباره آن‌ها را بخواند. FlashAttention محاسبات را به کاشی‌های (tiles) کوچک‌تری تقسیم می‌کند که در SRAM جای می‌گیرند، مراحل softmax و مقیاس‌گذاری (scaling) را روی تراشه انجام می‌دهد و تنها خروجی‌های نهایی را به حافظه با پهنای باند بالا می‌نویسد. این روش مقداری محاسبات اضافی را در ازای کاهش بسیار زیاد رفت و برگشت‌ها به حافظه اصلی معامله می‌کند، که تقریباً همیشه یک انتخاب برنده است.

PagedAttention نوع متفاوتی از اتلاف حافظه را حل می‌کند. در طول مرحله رمزگشایی (decode)، حافظه پنهان KV به شکلی غیرقابل پیش‌بینی رشد می‌کند. سیستم‌های سنتی بلوک‌های حافظه ثابت و پیوسته‌ای را برای هر توالی اختصاص می‌دهند، که با پایان زودرس برخی توالی‌ها و گسترش برخی دیگر، حفره‌های بزرگی باقی می‌گذارد. PagedAttention مفهوم حافظه مجازی را از سیستم‌عامل‌ها قرض می‌گیرد. این روش ورودی‌های KV cache را در بلوک‌هایی با اندازه ثابت ذخیره می‌کند که می‌توانند به صورت غیرپیوسته اختصاص داده شوند و از طریق یک جدول واسطه (indirection table) نگاشت شوند. این کار مانع از بلااستفاده ماندن حافظه در بافرهای رزرو شده اما نیمه‌خالی می‌شود و اجازه استفاده از اندازه‌های دسته‌ای (batch sizes) بزرگتر را می‌دهد، که به نوبه خود با مشغول نگه داشتن گذرگاه حافظه با کارهای مفید به جای صرف زمان برای سربار تکه‌تکه شدن (fragmentation overhead)، توان عملیاتی کلی را بهبود می‌بخشد.

تغییر پرسش

وقتی تأخیر (latency) ناگهان بالا می‌رود، تیم‌های زیادی می‌پرسند که آیا باید به مدل کوچک‌تری سوئیچ کنند یا سرور استنتاج خود را بازنویسی کنند. این سوالات مهم هستند، اما ثانویه محسوب می‌شوند. اولین سوال باید درباره خود سخت‌افزار باشد. آیا GPU شما واقعاً مشغول محاسبات است، یا تشنه داده است؟

به معیارهای بهره‌وری خود نگاه کنید. اشباع پهنای باند حافظه را در کنار میزان اشغال محاسباتی GPU بررسی کنید. اگر در طول رمزگشایی، با رقابت شدید بر سر حافظه (memory contention) و شدت محاسباتی (arithmetic intensity) پایین مواجه هستید، مشکل شما معماری مدل نیست؛ مشکل شما فیزیک است. راه حل از کدنویسی تمیزتر در پایتون به دست نمی‌آید. راه حل از دسته‌بندی (batching) تهاجمی‌تر، کوانتیزه کردن وزن‌ها برای عبور سریع‌تر از مسیر انتقال داده، بازسازی مکانیزم توجه برای ماندن در روی تراشه، و مدیریت KV cache برای گنجاندن دسته‌های بزرگتر بدون اتمام فضا حاصل خواهد شد.

وقتی استنتاج را از این منظر ببینید، بهینه‌سازی به امری مکانیکی تبدیل می‌شود. شما دیگر به دنبال افسانه‌هایی مبنی بر اینکه هوش مدل باعث کندی می‌شود نمی‌دوید، بلکه شروع به اتخاذ تصمیمات مهندسی بر پایه آنچه سخت‌افزار واقعاً می‌تواند ارائه دهد می‌کنید. این همان تغییری است که سیستم‌های تولیدی مقیاس‌پذیر را از سیستم‌هایی که صرفاً کار می‌کنند، متمایز می‌کند.