محاسبهٔ سختافزار برای اجرای مدل زبانی درونسازمانی: ۵۰، ۲۰۰ و ۱۰۰۰ کاربر همزمان
حساب کامل ابعاد سختافزار برای استنتاج درونسازمانی: بایت وزنها به تفکیک کوانتش، حافظهٔ نهان کلید-مقدار به ازای هر توکن از روی فایل پیکربندی، قانون لیتل برای درخواستهای در جریان، سقف پهنای باند حافظه، و سه مثال کارشده روی ۵۰، ۲۰۰ و ۱۰۰۰ کاربر همزمان.
چرا این نوشته
یک محاسبهٔ ابعاد کامل از ابتدا تا انتها — از کاربر همزمان با قانون لیتل و دنبالهٔ پواسون تا درخواستهای در جریان، بعد از راه حساب دقیق حافظهٔ نهان کلید-مقدار و سقف پهنای باند حافظه تا شمار کارت گرافیک — با نامبردن تکتک فرضها، و این نتیجهٔ غیرشهودی که پس از چند ده جریانِ بلندبافت، این حافظهٔ نهان کلید-مقدار است که صورتحساب سختافزار را تعیین میکند، نه وزنها.
بیشتر جلسههای خرید سختافزار برای مدل زبانی با یک عدد شروع میشود که هیچکس نمیداند از کجا آمده: «چند کارت لازم داریم؟» و بیشترشان با همان عدد تمام میشود، فقط با اعتماد به نفس بیشتر.
حقیقت آرامکننده این است که این کار تا گام آخر حساب است، نه قضاوت. چهار عدد تقریباً همهچیز را تعیین میکنند: بایتی که وزنها اشغال میکنند، بایتی که یک توکن حافظهٔ نهان کلید-مقدار (KV cache) اشغال میکند، تعداد درخواستهایی که واقعاً در یک لحظه در جریاناند، و تعداد بایتی که کارتهای شما در هر ثانیه جابهجا میکنند.
این نوشته همان حساب را برای سه اندازهٔ استقرار تا انتها انجام میدهد، هر فرض را به شکلی مینویسد که بتوانید با اندازهگیری خودتان جایگزینش کنید، و به نتیجهای میرسد که بیشتر تیمها را غافلگیر میکند: از چند ده درخواست همزمانِ بلندبافت به بعد، شما دیگر مدل سرویس نمیدهید؛ دارید حافظهٔ نهان کلید-مقدار پخش میکنید.
یک نکتهٔ واژگانی پیش از شروع: برای «quantisation» معادل جاافتادهای در فارسی نداریم و در متون مختلف «کوانتیزاسیون»، «کوانتش» و «کمدقتسازی» هر سه دیده میشود. اینجا کوانتش (quantisation) را به کار میبریم، چون کوتاه است و صفت «کوانتیده» از آن ساخته میشود. همینطور برای «in-flight request» از درخواست در جریان استفاده میکنیم و برای «roofline» از سقف پهنای باند.
همهٔ ارقام حافظه در این نوشته گیگابایت را ۱۰۹ بایت میگیرند، مطابق همان چیزی که سازندهٔ کارت روی جعبه مینویسد.
از فایل پیکربندی مدل شروع کنید، نه از تعداد پارامتر
دو عدد حافظهای که اهمیت دارند هر دو از config.json خود مدل بیرون میآیند، و فقط یکیشان با تعداد پارامتر بالا و پایین میرود. حافظهٔ وزنها یعنی تعداد پارامتر ضرب در بایت بهازای هر پارامتر. حافظهٔ نهان کلید-مقدار را تعداد لایه، تعداد سرهای کلید/مقدار و بُعد سر تعیین میکند — و تقریباً مستقل از بزرگی مدل است.
برای Llama 3.1 70B Instruct، طبق config.json منتشرشدهاش: ۸۰ لایه، اندازهٔ پنهان ۸۱۹۲، ۶۴ سر توجه، ۸ سر کلید/مقدار، بُعد سر ۱۲۸، واژگان ۱۲۸٬۲۵۶.
| دقت | بایت/پارامتر | حافظهٔ وزنها، 70.6B پارامتر | یادداشت |
|---|---|---|---|
| BF16 / FP16 | 2 | 141 GB | کیفیت مرجع |
| FP8 (E4M3) | 1 | 71 GB | فقط Hopper و Ada |
| INT4 / AWQ / GPTQ، گروه 128 | ~0.52 مؤثر | ~37 GB + embed/lm_head کوانتیدهنشده | حدود 40 GB بودجه بگیرید؛ خود چکپوینت را اندازه بگیرید |
سطر چهاربیتی همان جایی است که آدمها اشتباه میکنند. طرح گروه-۱۲۸ برای هر گروه یک ضریب مقیاس FP16 ذخیره میکند که تقریباً ۰٫۰۲ بایت به هر پارامتر اضافه میکند، و بیشتر چکپوینتهای منتشرشده لایهٔ تعبیه و تصویر خروجی را با دقت بالاتر باقی میگذارند — که روی Llama 3.1 70B یعنی ۲ × ۱۲۸٬۲۵۶ × ۸۱۹۲ پارامتر، حدود ۳ گیگابایت وزن اضافه که برایش بودجه نگرفته بودید. اندازهٔ فایلها را روی دیسک بخوانید؛ ضرب نکنید.
حافظهٔ نهان کلید-مقدار یک فرمول است و به تعداد پارامتر کاری ندارد:
bytes_per_token = 2 · num_hidden_layers · num_key_value_heads · head_dim · dtype_bytes
آن ۲ ابتدایی به این دلیل است که کلید و مقدار جدا ذخیره میشوند. برای Llama 3.1 70B با FP16:
2 · 80 · 8 · 128 · 2 = 327,680 bytes = 0.328 MB per token
یعنی یک بافت کاری ۸٬۰۰۰ توکنی برای هر درخواست همزمان ۲٫۶۲ گیگابایت خرج برمیدارد. حالا این را با Llama 3.1 8B مقایسه کنید — ۳۲ لایه، ۸ سر کلید/مقدار، بُعد سر ۱۲۸ — که ۱۳۱٬۰۷۲ بایت بر توکن میشود، یا ۱٫۰۵ گیگابایت برای همان ۸٬۰۰۰ توکن. مدل 70B نُه برابر پارامتر دارد و فقط ۲٫۵ برابر هزینهٔ کلید-مقدار، چون این حافظه با عمق مدل بزرگ میشود، نه با عرض آن.
این را از روی فایل پیکربندی حساب کنید، نه از حافظه:
# kvcalc.py — python 3.11, no dependencies. Point it at a downloaded config.json.
import json, sys
cfg = json.load(open(sys.argv[1]))
layers = cfg["num_hidden_layers"]
kv_heads = cfg.get("num_key_value_heads", cfg["num_attention_heads"])
# Qwen3 and several newer families set head_dim independently of
# hidden_size / num_attention_heads. Do not assume the division holds.
head_dim = cfg.get("head_dim", cfg["hidden_size"] // cfg["num_attention_heads"])
for name, nbytes in (("fp16", 2), ("fp8", 1)):
per_tok = 2 * layers * kv_heads * head_dim * nbytes
print(f"{name}: {per_tok/1e6:8.3f} MB/token"
f" {per_tok*8000/1e9:7.3f} GB per 8k-token request")
کاربر همزمان با درخواست همزمان یکی نیست، و همین فاصله کل محاسبه است
«هزار کاربر همزمان» یک نیازمندی سختافزاری نیست. نیازمندی سختافزاری، تعداد درخواستهایی است که در یک لحظه در جریاناند، و برای کار تحلیلی تعاملی این عدد بین یکپنجم تا یکچهارم تعداد کاربران است. اشتباهکردن این تبدیل به اندازهٔ چهار برابر، گرانترین خطای ابعادگیری درونسازمانی است — و در هر دو جهت گران است.
قانون لیتل (Little’s law) میانگین را میدهد: تعداد در جریان = نرخ ورود × زمان سرویس.
فرضها را صریح بنویسید، چون همینها پارامترهایی هستند که باید پیش از امضای سفارش خرید با لاگ خودتان جایگزینشان کنید:
| فرض | مقدار بهکاررفته | چطور جایگزینش کنید |
|---|---|---|
| فاصلهٔ میان دو درخواست یک کاربر، در نشست فعال | 90 s | میانهٔ فاصلهٔ درخواستها در هر نشست، از لاگ دسترسی خودتان |
| طول ورودی | 6,000 توکن | یک هفته ورودی واقعی را با بافت بازیابیشده توکنایز کنید |
| طول تولیدشده | 600 توکن | همان روش |
| نرخ هدف تولید در هر جریان | 30 توکن/ثانیه | سریعتر از سرعت خواندن بیشتر آدمها؛ زیر 15 حس خرابی میدهد |
| زمان سرویس W = پیشپرکردن + تولید | ≈ 1 s + 20 s = 21 s | سر تا سر و زیر بار هدف اندازه بگیرید |
میانگین درخواست در جریان برای C کاربر همزمان میشود C × 21/90 = 0.233 C. اما برای میانگین ابعاد نمیگیرید، چون ورودها خوشهای میآیند. با فرض ورود پواسونی، صدک نودونهم تقریباً mean + 2.33 · √mean است:
| کاربر همزمان | میانگین در جریان | p99 در جریان | جایگاه طراحی (+۲۰٪ حاشیه) |
|---|---|---|---|
| 50 | 11.7 | 19.6 | 24 |
| 200 | 46.7 | 62.6 | 76 |
| 1,000 | 233 | 269 | 323 |
به کاری که آن جذر میکند دقت کنید: تعداد جایگاههای طراحی زیرخطی با تعداد کاربر رشد میکند. رفتن از ۵۰ به ۱۰۰۰ کاربر — ضریب ۲۰ — به ۱۳٫۵ برابر جایگاه نیاز دارد، نه ۲۰ برابر. این تنها صرفهٔ مقیاسِ کل این محاسبه است، و پیش از طراحی یک استقرار مرحلهای ارزش دانستن دارد.
سقف پهنای باند حافظه میگوید کدام پیکربندی ناممکن است
تولید توکن به پهنای باند حافظه محدود است، نه به توان محاسباتی. هر گام تولید، وزنهایی را که آن کارت مسئولشان است دوباره میخواند، بهعلاوهٔ حافظهٔ نهان کلید-مقدارِ همهٔ دنبالههای فعال. همین یک سقف سخت میدهد که هیچ بهینهسازی کرنلی از آن رد نمیشود:
step_time ≥ (weight_bytes_per_card + kv_bytes_of_active_sequences_per_card) / HBM_bandwidth
tokens_per_second_per_stream = 1 / step_time
پهنای باندهای منتشرشده: H100 SXM 80 GB با 3.35 TB/s، A100 80 GB حدود 2.0 TB/s، و L40S 48 GB با 864 GB/s. سامانههای واقعی، پس از احتساب زمانبندی، سربار راهاندازی کرنل و درهمبافی پیشپرکردن، جایی بین ۴۰٪ تا ۶۰٪ این سقف مینشینند. از سقف برای حذف پیکربندیها استفاده کنید؛ برای تأیید یکی از آنها آزمون بار لازم است.
پیشپرکردن (prefill) فیزیک دیگری دارد. این مرحله به توان محاسباتی محدود است، تقریباً 2 · N_params · prompt_tokens عملیات ممیز شناور — که برای ورودی ۶٬۰۰۰ توکنی روی ۷۰٫۶ میلیارد پارامتر حدود ۸۴۷ ترافلاپ میشود، یعنی روی چهار کارت امروزی خیلی کمتر از یک ثانیه. اگر ورودیهایتان پیشوند بلند مشترک دارند — که در هر سامانهای که مستندات اسکیما را جلوی هر پرسش میگذارد دارند — حافظهٔ نهان خودکار پیشوند در vLLM همان بلوکهای کلید-مقدار را بازاستفاده میکند و بیشتر این هزینه را برمیدارد. پیش از خرید کارت بیشتر، این را روشن کنید.
مثال کارشده: ۵۰ کاربر همزمان
۲۴ جایگاه طراحی، Llama 3.1 70B چهاربیتی، بافت کاری ۸٬۰۰۰ توکنی. پاسخ: دو کارت H100 80 GB، یا چهار کارت A100 80 GB.
حافظه:
weights 40 GB
KV 24 slots × 2.62 GB 63 GB
framework overhead ~10 GB (CUDA context, graphs, NCCL buffers, activations)
-------
total 113 GB
با مقدار پیشفرض gpu_memory_utilization در vLLM که ۰٫۹ است، یک کارت ۸۰ گیگابایتی حدود ۷۲ گیگابایت در اختیار میگذارد. دو کارت با موازیسازی تانسوری (tensor parallel) درجهٔ ۲ میشود ۱۴۴ گیگابایت فضای قابل استفاده در برابر نیاز ۱۱۳ گیگابایتی — و TP=2 آن ۸ سر کلید/مقدار را تمیز تقسیم میکند، ۴ تا برای هر رنک. سهم هر کارت: ۲۰ گیگابایت وزن، ۳۱٫۵ گیگابایت کلید-مقدار، یعنی حدود ۵۸ از ۷۲ گیگابایت موجود.
حالا سقف را وارسی کنید. هر کارت در هر گام تولید ۲۰ گیگابایت وزن بهعلاوهٔ ۳۱٫۵ گیگابایت کلید-مقدار میخواند: ۵۱٫۵ گیگابایت تقسیم بر ۳٫۳۵ ترابایت بر ثانیه میشود ۱۵٫۴ میلیثانیه، یعنی سقف ۶۵ توکن بر ثانیه در هر جریان. با فرض واقعبینانهٔ ۵۰٪ سقف، میشود ۳۲ توکن بر ثانیه — کمی بالای هدف ۳۰. کار میکند، بدون حاشیهٔ زیاد.
همین پیکربندی روی دو کارت A100 کار نمیکند. با ۲٫۰ ترابایت بر ثانیه کف گام میشود ۲۵٫۸ میلیثانیه، سقف ۳۹ توکن بر ثانیه، که با بازدهی ۵۰٪ به ۱۹ توکن بر ثانیه در هر جریان میرسد — بهطور محسوس کند. تقسیم روی چهار کارت A100 با TP=4 بایت هر کارت را نصف میکند به ۲۵٫۸ گیگابایت، کف ۱۲٫۹ میلیثانیه و حدود ۳۹ توکن بر ثانیه در عمل. کارتهای قدیمیتر اینجا به حافظهٔ بیشتر نیاز ندارند؛ به تعداد بیشتر نیاز دارند، برای پهنای باند.
مثال کارشده: ۲۰۰ کاربر همزمان
۷۶ جایگاه طراحی، همان مدل و همان بافت. پاسخ: چهار کارت H100 80 GB با حافظهٔ نهان کلید-مقدار FP8، یا هشت کارت با FP16.
weights 40 GB
KV 76 slots × 2.62 GB 199 GB <-- five times the weights
framework overhead ~15 GB
-------
total 254 GB
اینجا همان نقطهای است که کل این محاسبه دربارهٔ آن است. روی ۲۴ جایگاه، وزنها ۳۵٪ صورتحساب حافظه بودند. روی ۷۶ جایگاه، وزنها ۱۶٪اند و حافظهٔ نهان کلید-مقدار خودِ سامانه است. چهار کارت با ۷۲ گیگابایت قابل استفاده میشود ۲۸۸ گیگابایت، که جا میشود — اما سهم هر کارت ۱۰ گیگابایت وزن و ۴۹٫۸ گیگابایت کلید-مقدار است، یعنی ۶۵ از ۷۲ گیگابایت بودجه، بدون هیچ جایی برای جهش بافت.
تغییر حافظهٔ نهان کلید-مقدار به FP8 (پرچم --kv-cache-dtype fp8، روی Hopper و Ada) آن را نصف میکند به ۹۹٫۵ گیگابایت، مصرف هر کارت را به حدود ۴۰ گیگابایت میرساند و حاشیه را برمیگرداند. ضمناً بایت خواندهشده در هر گام تولید را هم نصف میکند: ۳۴٫۹ گیگابایت بر کارت، کف ۱۰٫۴ میلیثانیه، و حدود ۴۸ توکن بر ثانیه در هر جریان با ۵۰٪ سقف. چهار H100 با کلید-مقدار FP8 پیکربندی راحتی است؛ چهار H100 با کلید-مقدار FP16 یک روز بد با پسگیری (preemption) فاصله دارد — همان حالتی که vLLM درخواستها را بیرون میاندازد و بعداً از نو محاسبه میکند تا بلوک آزاد کند.
اگر کلید-مقدار FP8 از نظر کیفیت پذیرفتنی نیست — و بهجای فرضکردن ارزش ارزیابی دارد — جایگزینش هشت کارت با FP16 است، نه چهار.
مثال کارشده: ۱۰۰۰ کاربر همزمان
۳۲۳ جایگاه طراحی. پاسخ: شانزده کارت H100 80 GB به شکل دو نسخهٔ مستقل هشتتایی — و پیش از آن، یک بحث جدی دربارهٔ مدل کوچکتر.
با ۳۲۳ جایگاه و کلید-مقدار FP8، فقط همین حافظهٔ نهان ۴۲۳ گیگابایت است. هشت کارت ۵۷۶ گیگابایت قابل استفاده میدهند، پس یک نسخهٔ واحد با TP=8 روی کاغذ جا میشود: ۵ گیگابایت وزن و ۵۲٫۹ گیگابایت کلید-مقدار بر کارت، یعنی حدود ۶۲ از ۷۲. سقف میگوید ۵۷٫۹ گیگابایت در هر گام روی ۳٫۳۵ ترابایت بر ثانیه، کف ۱۷٫۳ میلیثانیه، و حدود ۲۹ توکن بر ثانیه در هر جریان با بازدهی ۵۰٪. این دقیقاً هدف است، و یعنی هشت کارت کفِ امکانپذیری است، نه یک طراحی.
دو نسخهٔ هشتتایی هر دو مسئله را با هم حل میکند. هر نسخه حدود ۱۶۲ جایگاه برمیدارد، ۲۶٫۵ گیگابایت کلید-مقدار بر کارت، کف گام ۹٫۴ میلیثانیه و حدود ۵۳ توکن بر ثانیه در هر جریان — و از دست دادن یک نسخه سرویس را تنزل میدهد نه اینکه تمامش کند. در این اندازه، استقرار تکنسخهای یعنی تصمیم به نداشتن هیچ پنجرهٔ نگهداری.
اما پرسش بهتر روی ۱۰۰۰ کاربر این است که آیا واقعاً هر درخواست به مدل 70B نیاز دارد. یک مدل ردهٔ 32B چهاربیتی حدود ۱۸ گیگابایت وزن دارد و — چون Qwen3-32B شصتوچهار لایه دارد در برابر هشتاد لایهٔ Llama 3.1 70B، با همان ۸ سر کلید/مقدار و همان بُعد سر ۱۲۸ — حدود ۸۰٪ هزینهٔ کلید-مقدار بر توکن. هشت کارت آن را با آسودگی سرویس میدهند. مسیریابی آن ۸۰٪ ترافیک معمولی به مدل کوچکتر و نگهداشتن مدل بزرگ برای پرسشهایی که واقعاً به آن نیاز دارند، از هر تصمیم کوانتشی در این فهرست ارزش بیشتری دارد — و همین دلیلی است که دیتاکوپایلوت مدل فعال را در جدول تنظیمات نگه میدارد نه در پیکربندی: انتخاب مدل باید بدون استقرار قابل تغییر باشد.
سربارهایی که فراموش میشوند
شش چیز حافظهٔ کارت را مصرف میکنند که هرگز در صفحهگسترهٔ ابعادگیری ظاهر نمیشوند:
- رزرو خودِ چارچوب. پیشفرض
gpu_memory_utilizationدر vLLM برابر ۰٫۹ است؛ شما هرگز از یک کارت ۸۰ گیگابایتی ۸۰ گیگابایت نمیگیرید. - گرافهای CUDA، که به ازای هر سطل اندازهٔ دسته ضبط میشوند و معمولاً ۱ تا ۳ گیگابایتاند.
- حافظهٔ فعالسازی برای پیشپرکردن تکهای، که
max_num_batched_tokensتعیینش میکند — و با بلندترین ورودی شما مقیاس میگیرد، نه با میانگین آن. - بافرهای ارتباطی NCCL، که با درجهٔ موازیسازی تانسوری بزرگ میشوند.
- قطعهقطعهشدن حافظه، که اصلاً دلیل وجود توجه صفحهبندیشده (paged attention) است و دلیل اینکه رزرو صفر نیست.
- سرویسهای همساکن. یک سرویس تعبیهسازی خودمیزبان BGE-M3 — بردار ۱۰۲۴-بعدی، بر پایهٔ XLM-RoBERTa — با FP16 حدود ۱٫۲ گیگابایت است. کوچک است، اما به یک کارت نیاز دارد، و گذاشتنش روی کارت استنتاج به قیمت بلوکهای کلید-مقدار تمام میشود.
جایی که این محاسبه دیگر کار نمیکند
این حساب فرض میگیرد که با رمزگشای ترانسفورمر چگال، با توجه پرسوجوی گروهی (grouped-query attention)، روی زمان اجرایی با توجه صفحهبندیشده، و با شکل درخواستهای تقریباً یکنواخت سر و کار دارید. چهار وضعیت آن را میشکند.
مدلهای آمیزهٔ خبرگان (mixture-of-experts) پیوند میان تعداد پارامتر و بایت خواندهشده بر توکن را قطع میکنند. حافظهٔ وزن از کل پارامترها پیروی میکند؛ سقف پهنای باند از پارامترهای فعال — اما فقط اگر مسیریابی خبرگان آنقدر پایدار باشد که مجموعهٔ کاری مقیم بماند. هر دو کران را حساب کنید و انتظار داشته باشید فاصلهشان زیاد باشد.
مدلهای استدلالی با خروجیهای بلند فرض زمان سرویس را بهکلی بیاعتبار میکنند. اگر میانگین خروجی از ۶۰۰ به ۶٬۰۰۰ توکن برود، زمان سرویس از ۲۱ ثانیه به حدود سه دقیقه میرسد و درخواستهای در جریان برای همان تعداد کاربر تقریباً نُه برابر میشوند. قانون لیتل را از نو اجرا کنید؛ تعداد کارتی را که قبلاً حساب کردهاید ضرب نکنید.
بافتهای خیلی بلند مدل خطی کلید-مقدار را در عمل میشکنند، نه در نظریه. فرمول دقیق میماند، اما روی ۱۲۸٬۰۰۰ توکن یک درخواست تنهای Llama 3.1 70B معادل ۴۲ گیگابایت حافظهٔ نهان نگه میدارد و همزمانی به عدد یکرقمی در هر کارت سقوط میکند. بالاتر از حدود ۳۲٬۰۰۰ توکن، طول بافت هر درخواست یک سیاست ظرفیت است، نه یک ترجیح کاربر.
بارهای دستهای انفجاری — گزارش شبانه، یک کار طبقهبندی انبوه — اصلاً با قانون لیتل مدل نمیشوند، چون زمان اندیشه ندارند. آنها را بهعنوان مسئلهٔ توان عملیاتی با یک مهلت ابعادگیری کنید و بیرون از ساعتهای تعاملی زمانبندیشان کنید؛ وگرنه یک کار واحد تمام بلوکهای کلید-مقداری را که کاربران تعاملی لازم دارند میبلعد.
و در پایان، هر عدد این نوشته را نقطهٔ شروعی بدانید که بعد اندازهاش میگیرید. این حساب به شما میگوید کدام پیکربندیها ناممکناند، که واقعاً مفید است و یک سفارش خرید را نجات میدهد. اما نمیگوید کدامشان برای آدمی که منتظر پاسخ نشسته، سریع حس میشود.