جلسه ۱۰ از ۱۱، بخش ۲: سیستم‌های سرویس‌دهی LLM

مبانی سرویس‌دهی LLM: درون مدل

متنی ۳۵ دقیقه به‌روز شده در ۱۰ آبان ۱۴۰۵

در این جلسه
  1. چرخه‌ی عمر یک درخواست
  2. درون مدل، با اعداد واقعی
  3. KV cache: مهم‌ترین عدد سرویس‌دهی
  4. پارامترها کجا هستند؟
  5. شهود سیستمی در سه عدد
  6. مدل متراکم در برابر MoE
  7. نگاهی به معماری‌های جدیدتر
  8. جمع‌بندی
  9. پرسش‌هایی برای فکر کردن
  10. منابع

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

چرخه‌ی عمر یک درخواست#

۱. توکن‌سازی ─◄ ۲. زمان‌بندی ─◄ ۳. 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 بیشتر از دو برابر می‌شود. چرا؟ کدام جمله‌ی فرمول مسئول است؟
  • برای پشتیبانی از کانتکست طولانی‌تر روی همان سخت‌افزار، سه راه را از این جلسه نام ببرید.

منابع#