جلسه ۱۰ از ۱۱، بخش ۲: سیستمهای سرویسدهی LLM
مبانی سرویسدهی LLM: درون مدل
در این جلسه
تا اینجا مدل را جعبهسیاهی دیدیم که متن میگیرد و متن پس میدهد. از این جلسه داخل جعبه را باز میکنیم، با یک هدف مشخص: شهود سیستمی. برای هر قسمت مدل میپرسیم چقدر محاسبه میخواهد، چقدر حافظه و چقدر جابهجایی داده. همهی بهینهسازیهای بخش دوم مجموعه روی این سه عدد بنا شدهاند.
چرخهی عمر یک درخواست#
۱. توکنسازی ─◄ ۲. زمانبندی ─◄ ۳. prefill ─◄ ۴. decode و نمونهبرداری ─◄ ۵. تبدیل به متن و stream
مرحلهی ۴ برای هر توکن خروجی یک بار تکرار میشود.
دو معیار تأخیر از همینجا میآیند:
- TTFT (زمان تا اولین توکن): را prefill تعیین میکند
- TPOT (زمان هر توکن خروجی): را decode تعیین میکند
۱. توکنسازی#
متن به توکن تبدیل میشود، نه به کلمه. در انگلیسی هر توکن تقریباً ۳ تا ۴ حرف است. «Serving is hard» سه کلمه است ولی ۴ توکن میشود. تعداد توکن هزینه و حافظه را تعیین میکند و توکنسازهای مدلها با هم فرق دارند. متن فارسی معمولاً در توکنسازهای انگلیسیمحور توکن بیشتری مصرف میکند، که یعنی هزینهی بیشتر برای همان محتوا.
BPE بایتی از ۲۵۶ مقدار بایت شروع میکند و پرتکرارترین جفت مجاور را بارها ادغام میکند. چون از بایت شروع میشود، هیچ توکن «ناشناخته»ای ندارد. مصالحهاش این است: واژگان بزرگتر توکن کمتری تولید میکند ولی جدول embedding بزرگتری دارد.
۲. زمانبندی و batching#
درخواستها مستقل از هم میرسند، اما GPU کار دستهای را ترجیح میدهد. در continuous batching، درخواستها وسط کار به دسته وارد و از آن خارج میشوند. درخواستی که تمام شد بلافاصله جایش را آزاد میکند و لازم نیست منتظر بماند تا کل دسته تمام شود. سقف تعداد درخواست همزمان را حافظهی KV cache تعیین میکند.
۳. prefill#
کل پرامپت در یک گذر پردازش میشود و همهی توکنهای پرامپت بهطور موازی به هم توجه میکنند. این مرحله محدود به محاسبه است و هزینهاش با طول پرامپت رشد میکند. خروجیاش KV cache پرامپت و اولین توکن پاسخ است.
۴. decode و نمونهبرداری#
برای هر توکن جدید یک گذر کامل لازم است و KV cache هر قدم یک مدخل بزرگتر میشود. decode محدود به پهنای باند حافظه است، پس batching کمکش میکند. اگر حافظهی KV پر شود، سیستم باید درخواستی را متوقف یا در صف نگه دارد.
خروجی هر گذر یک امتیاز برای هر توکن واژگان است و نمونهبرداری از میان آنها انتخاب میکند:
import numpy as np
def sample(logits, temperature=0.7, top_k=50, top_p=0.9, rng=np.random.default_rng()):
if temperature == 0:
return int(np.argmax(logits)) # همیشه محتملترین
logits = logits / temperature # دمای بالا پخشتر، پایین تیزتر
idx = np.argsort(logits)[::-1][:top_k] # فقط k توکن برتر
probs = np.exp(logits[idx] - logits[idx].max())
probs /= probs.sum()
keep = np.cumsum(probs) - probs < top_p # کوچکترین مجموعه با جمع احتمال p
idx, probs = idx[keep], probs[keep] / probs[keep].sum()
return int(rng.choice(idx, p=probs))
یک پیامد سیستمی: همان مدل و همان پرامپت با تنظیمات متفاوت، هم جواب متفاوت و هم طول متفاوت میدهد و طول خروجی زمانبندی را سخت میکند.
۵. تبدیل به متن و stream#
شناسهها همزمان با تولید به متن تبدیل و فرستاده میشوند. کاراکترهای چندبایتی ناقص، از جمله حروف فارسی، بافر میشوند تا کامل شوند. توکن توقف، حد طول یا قطع اتصال کاربر stream را تمام میکند. توقف زودتر حافظهی KV را زودتر آزاد میکند.
درون مدل، با اعداد واقعی#
همهی عددها برای Llama 3 70B است:
| نماد | معنا | مقدار |
|---|---|---|
| d | پهنای مدل | ۸۱۹۲ |
| L | تعداد لایه | ۸۰ |
| H | تعداد head کوئری | ۶۴ |
| H_kv | تعداد head کلید و مقدار | ۸ |
| d_k | بُعد هر head (d ÷ H) | ۱۲۸ |
| d_ff | پهنای لایهی پنهان MLP | ۲۸۶۷۲ |
| V | اندازهی واژگان | ۱۲۸۲۵۶ |
| N | کل پارامترها | ۷۰٫۶ میلیارد |
یک پشته از بلوکهای یکسان#
شناسهی توکنها ─◄ embedding (بردار ۸۱۹۲ بعدی) ─◄ ۸۰ بلوک ترنسفورمر ─◄ LM head (یک امتیاز برای هر توکن) ─◄ softmax
هر بلوک دو بخش دارد: self-attention که اطلاعات را بین توکنها مخلوط میکند، و FFN که هر توکن را جداگانه تبدیل میکند. هر دو روی یک «جریان باقیمانده» (residual stream) سوار میشوند:
h = x + Attn(RMSNorm(x))
y = h + FFN(RMSNorm(h))
RMSNorm(x) = x / sqrt(mean(x²) + ε) ⊙ g
نرمالسازی پیش از هر زیرلایه (pre-norm) آموزش پشتههای عمیق را پایدار میکند. x از هر ۸۰ بلوک عبور میکند و هر زیرلایه فقط یک بهروزرسانی به آن اضافه میکند.
self-attention#
هر توکن سه بردار میسازد: کوئری (دنبال چه میگردم)، کلید (چه چیزی عرضه میکنم) و مقدار (چه چیزی منتقل میکنم):
Q = X·W_Q K = X·W_K V = X·W_V
Attention(Q, K, V) = softmax(Q·Kᵀ / √d_k + M) · V
Mماسک علّی است که توکنهای آینده را پنهان میکند.- چرا تقسیم بر
√d_k؟ ضرب داخلی جمع d_k حاصلضرب است و بزرگیاش با d_k رشد میکند. تقسیم جلوی اشباع شدن softmax را میگیرد. - با n توکن، n² امتیاز داریم، پس هزینه با طول کانتکست بهشکل مربعی رشد میکند.
هزینهی هر لایه حدود 4·n²·d FLOP است. اگر ماتریس امتیازها را ذخیره میکردیم، برای n = 8192 با ۶۴ head و ۸۰ لایه حدود ۶۹۰ گیگابایت حافظه لازم بود. FlashAttention softmax را کاشیبهکاشی حساب میکند و هرگز این ماتریس را کامل در حافظه نمیسازد.
multi-head attention: Q، K و V به ۶۴ head با بُعد ۱۲۸ تقسیم میشوند و هر head الگوی خودش را یاد میگیرد: توکن قبلی، اولین توکن، همسایهها یا کل کانتکست. کل FLOPها با یک head پهن برابر است (۶۴ × ۱۲۸ = ۸۱۹۲). تقسیم چیزی را که یاد گرفته میشود عوض میکند، نه هزینه را.
KV cache: مهمترین عدد سرویسدهی#
در decode، کلید و مقدار توکنهای قبلی تغییر نمیکنند. پس آنها را یک بار حساب و نگه میداریم. این همان KV cache است:
KV cache برای هر توکن = 2 × L × H_kv × d_k × (بایت هر عدد)
| روش | head کوئری | head کلید و مقدار | اندازهی KV |
|---|---|---|---|
| MHA | ۸ | ۸ | ۱× |
| GQA | ۸ | ۲ | ¼× |
| MQA | ۸ | ۱ | ⅛× |
در Llama 3 70B، ۶۴ head کوئری فقط ۸ head کلید و مقدار را به اشتراک میگذارند (GQA):
2 × 80 × 8 × 128 × 2 بایت = 327,680 بایت ≈ 320 کیلوبایت برای هر توکن
با MHA کامل، هر توکن ۲٫۵ مگابایت میشد، یعنی ۸ برابر بیشتر.
def kv_bytes_per_token(layers, kv_heads, head_dim, bytes_per_value=2):
return 2 * layers * kv_heads * head_dim * bytes_per_value # 2 = K و V
print(kv_bytes_per_token(80, 8, 128) / 1024, "KiB") # Llama 3 70B → 320.0
print(kv_bytes_per_token(32, 8, 128) / 1024, "KiB") # Llama 3 8B → 128.0
پارامترها کجا هستند؟#
| بخش | فرمول | مقدار |
|---|---|---|
| attention در هر لایه | 2·d·H·d_k + 2·d·H_kv·d_k |
۱۵۱ میلیون |
| FFN در هر لایه (SwiGLU، سه ماتریس) | 3·d·d_ff |
۷۰۵ میلیون |
| همهی لایهها | 80 × 856M |
۶۸٫۵ میلیارد |
| embedding و LM head | 2·V·d |
۲٫۱ میلیارد |
| کل | ۷۰٫۶ میلیارد |
بیش از ۸۰٪ پارامترها در FFN هستند، نه attention.
شهود سیستمی در سه عدد#
برای Llama 3 70B در BF16:
- وزنها: ۲ بایت × پارامتر ≈ ۱۴۱ گیگابایت
- محاسبه: ≈ ۲ × پارامتر ≈ ۱۴۱ گیگافلاپ به ازای هر توکن
- KV cache: ≈ ۳۲۰ کیلوبایت به ازای هر توکن
حالا یک پرامپت ۸۰ هزار توکنی را روی دو H200 تصور کنید (۱۹۷۸ ترافلاپ بر ثانیه و ۲۸۲ گیگابایت حافظهی کل):
زمان prefill ≈ B × (2·N·n + 2·L·d·n²) ÷ FLOP/s حافظه ≈ W + B × n × KV
| ۱ درخواست | ۶۴ درخواست همزمان | |
|---|---|---|
| زمان prefill | حدود ۱۰ ثانیه | حدود ۶۳۷ ثانیه |
| حافظهی لازم | حدود ۱۶۷ گیگابایت | حدود ۱۷۷۹ گیگابایت |
یک درخواست جا میشود، اما ۶۴ درخواست با این طول شش برابر حافظهی موجود را لازم دارند. KV cache، نه وزنها، سقف همزمانی را تعیین میکند. برای ایجنتهایی که کانتکست صدها هزار توکنی دارند (جلسهی ۸)، این مسئلهی اصلی است. جلسههای بعدی، paging، prefix caching و بهینهسازی KV، دقیقاً همین را حل میکنند.
مدل متراکم در برابر MoE#
در Mixture of Experts، FFN هر بلوک با یک مسیریاب و چند FFN کوچکتر به نام «متخصص» جایگزین میشود. مثلاً از ۸ متخصص، مسیریاب برای هر توکن ۲ تا را انتخاب میکند و خروجیشان با وزن ترکیب میشود.
| مدل | متخصص فعال | کل پارامتر | پارامتر فعال در هر توکن | وزنها (BF16) | گیگافلاپ در هر توکن |
|---|---|---|---|---|---|
| Llama 2 70B | متراکم | ۶۹B | ۶۹B | ۱۳۸ GB | ۱۳۸ |
| Mixtral 8x7B | ۲ از ۸ | ۴۶٫۷B | ۱۲٫۹B | ۹۳ GB | ۲۶ |
| Llama 3.1 405B | متراکم | ۴۰۵B | ۴۰۵B | ۸۱۰ GB | ۸۱۰ |
| DeepSeek-V3 | ۹ از ۲۵۷ | ۶۷۱B | ۳۷B | ۱۳۴۲ GB | ۷۴ |
قاعدهی کلیدی: حافظه از کل پارامترها پیروی میکند و محاسبه از پارامترهای فعال. Mixtral با حدود یکپنجم محاسبه با Llama 2 70B برابری میکند. DeepSeek-V3 با حدود یکیازدهم محاسبه رقیب Llama 3.1 405B است، اما ۱٫۷ برابر حافظه میخواهد.
نگاهی به معماریهای جدیدتر#
همهی این نوآوریها یک هدف سیستمی مشترک دارند: کوچک کردن یا محدود کردن KV cache یا محاسبهی attention.
| ایده | چه میکند | اثر بر سرویسدهی |
|---|---|---|
| پنجرهی لغزان | هر توکن فقط w توکن آخر را میبیند | KV به w محدود میشود؛ Gemma 3 با ۵ لایهی محلی به ازای هر لایهی سراسری، در ۱۲۸K حدود ۶ برابر KV کمتر |
| MLA (DeepSeek) | به جای K و V یک بردار نهفتهی فشرده ذخیره میکند | KV حدود ۹۳٪ کوچکتر نسبت به DeepSeek 67B |
| attention خطی و Gated DeltaNet | حالت با اندازهی ثابت که درجا بهروز میشود | در Qwen3-Next، ۳ لایه از هر ۴ لایه حالت ثابت دارند |
| مدل فضای حالت (Mamba) | حالت کوچک ثابت برای هر کانال | هیچ KV رشدکنندهای ندارد |
| attention پراکنده (DeepSeek-V3.2) | هر کوئری فقط ۲۰۴۸ توکن برتر را میخواند | کار هر توکن محدود است، اما KV همهی توکنها هنوز ذخیره میشود |
| ترنسفورمر حلقهای | یک بلوک K بار تکرار میشود | عمق بیشتر بدون پارامتر بیشتر، اما تأخیر K برابر |
| مدل زبانی diffusion | چند توکن موازی در چند مرحلهی پالایش | گذر کمتر، ممکن است به قیمت کیفیت |
اسلایدها این بخش را «برای مرور یا مطالعهی بعدی» علامت زدهاند. جزئیات هر کدام:
پنجرهی لغزان#
در attention علّی کامل، KV با n رشد میکند. در پنجرهی لغزان با اندازهی w، هر توکن فقط w توکن آخر را میخواند، پس KV به w محدود میشود. اطلاعات از بیرون پنجره هم از لایهای به لایهی بعد منتقل میشود. سه الگوی واقعی:
| مدل | الگو |
|---|---|
| Mistral 7B | همهی لایهها محلی، w = 4096 |
| Gemma 2 | یک لایهی محلی (w = 4096) و یک لایهی سراسری، بهتناوب |
| Gemma 3 | پنج لایهی محلی (w = 1024) به ازای هر لایهی سراسری |
فقط لایههای سراسری KV متناسب با کل کانتکست نگه میدارند. در کانتکست ۱۲۸K، Gemma 3 حدود ۶ برابر کمتر از یک طراحی تماماً سراسری KV ذخیره میکند.
Multi-latent attention (MLA)#
به جای K و V برای هر head، برای هر توکن فقط یک بردار نهفتهی فشردهی c ذخیره میشود و K و V هنگام نیاز از c بازسازی میشوند. ترفند اصلی: ماتریس بازسازی در زمان decode درون وزنهای کوئری و خروجی ادغام میشود، پس K و V کامل هرگز واقعاً ساخته نمیشوند. این محاسبهی بیشتر را با حافظهی کمتر معاوضه میکند. DeepSeek-V2 در مقایسه با DeepSeek 67B، KV را ۹۳٪ کم کرد و توان تولید را ۵٫۸ برابر کرد. Kimi K2 هم از MLA استفاده میکند.
attention خطی و Gated DeltaNet#
attention با softmax برای هر توکن یک مدخل KV برای همیشه اضافه میکند. attention خطی یک حالت با اندازهی ثابت S_t (به ابعاد d_k × d_v) نگه میدارد که درجا بهروز میشود:
- attention خطی ساده: هر توکن حاصلضرب کلید و مقدارش را به حالت اضافه میکند و کوئریها از حالت میخوانند.
- Gated DeltaNet: اول حالت قدیمی با دروازهی فراموشی
α_tکمرنگ میشود، بعد بخشی که کلید جدید به آن اشاره میکند با دروازهی نوشتنβ_tبازنویسی میشود.
Qwen3-Next با ۴۸ لایه به نسبت ۳ به ۱ ساخته شده است: ۳۶ لایهی Gated DeltaNet و ۱۲ لایهی attention. فقط همان ۱۲ لایه KV cache دارند. Kimi Linear و Qwen3.5 هم همین دستور را دنبال میکنند.
مدل فضای حالت (Mamba)#
gated linear attention: S ← α·S + β·v·kᵀ (S is d_k × d_v)
Mamba (state space): h ← Ā·h + B̄·x (small state per channel)
در SSM انتخابی، Ā و B̄ به هر توکن وابستهاند، پس لایه خودش انتخاب میکند چه چیزی را نگه دارد. حافظهی decode با رشد n ثابت میماند و Mamba تا ۵ برابر توان تولید ترنسفورمر گزارش کرده است. یک scan موازی آگاه از سختافزار حالت را در SRAM نگه میدارد و Mamba-2 همین را بهشکل ضرب ماتریس بازنویسی کرده است.
attention پراکنده (DeepSeek-V3.2)#
هر کوئری فقط ۲۰۴۸ توکن گذشتهی برتر را میخواند، در سه قدم:
1. score: I(t,s) = Σ_j w(t,j) · ReLU( q(t,j) · k(s) )
2. select: S_t = top-k of I(t,s) (k = 2048)
3. attend: o_t = Attn( q_t , { c_s : s ∈ S_t } )
ایندکسگذار از چند head کوچک در FP8 و کلیدی ۱۲۸ عددی برای هر توکن استفاده میکند. attention اصلی به جای همهی n توکن فقط ۲۰۴۸ توکن را لمس میکند. کار هر توکن محدود میشود، اما KV همهی توکنها هنوز ذخیره میشود.
ترنسفورمر حلقهای#
یک بلوک R بین یک پیشدرآمد P و یک پایان C، K بار تکرار میشود. عمق محاسبه بیشتر میشود بیآنکه پارامتر اضافه شود. یک مدل ۳٫۵ میلیارد پارامتری با عمق بازگشتی، با بزرگتر شدن K در کارهای استدلالی بهتر شد. هزینه: K حلقه K برابر FLOPهای بلوک را میخورد، بدون حافظهی وزن اضافه. اما هر حلقه ترتیبی است، پس تأخیر هم K برابر میشود.
مدلهای زبانی diffusion#
از توکنهای ماسکشده شروع میکنند و در چند مرحلهی موازی آنها را پر میکنند. هر مرحله یک بار همهی وزنها را میخواند، پس مرحلهی کمتر یعنی گذر کمتر، اما ممکن است به کیفیت آسیب بزند. LLaDA یک مدل diffusion ماسکشدهی ۸ میلیارد پارامتری است که از صفر روی ۲٫۳ تریلیون توکن آموزش دیده و با Llama 3 8B رقابت میکند. Dream 7B از وزنهای Qwen2.5 7B شروع کرده است؛ شبکه هنوز یک ترنسفورمر است.
اسلایدها پیشبینی چندتوکنی (multi-token prediction) را هم در همین فهرست نام بردهاند. در DeepSeek-V3، سرهای MTP بعدها به یک مسیر speculative decoding در vLLM و SGLang تبدیل شدند.
جمعبندی#
| مرحله | ماهیت | معیار |
|---|---|---|
| توکنسازی | واژگان بزرگتر = توکن کمتر، جدول بزرگتر | تعداد توکن |
| زمانبندی | continuous batching؛ حافظهی KV سقف است | همزمانی |
| prefill | یک گذر، محدود به محاسبه | TTFT |
| decode | یک گذر برای هر توکن، محدود به پهنای باند | TPOT |
| stream | هر توکن بهمحض تولید | تجربهی کاربر |
پرسشهایی برای فکر کردن#
- اگر کانتکست را از ۸۰ هزار به ۱۶۰ هزار توکن دو برابر کنید، زمان prefill بیشتر از دو برابر میشود. چرا؟ کدام جملهی فرمول مسئول است؟
- برای پشتیبانی از کانتکست طولانیتر روی همان سختافزار، سه راه را از این جلسه نام ببرید.
منابع#
- اسلاید جلسه: Efficient LLM serving basics I (9_llm_serving_basics_1.pdf)
- یادداشت درس: Transformers and LLM Foundations
- Vaswani و همکاران، Attention Is All You Need
- Meta، The Llama 3 Herd of Models
- Jiang و همکاران، Mixtral of Experts
- Ainslie و همکاران، GQA
- Dao و همکاران، FlashAttention