مقاله حسین نریمانی ۱۴۰۵/۰۳/۳۱ AI & Intelligent Systems

معماری Inference Pipeline برای LLM‌های بزرگ در Production: از Token Streaming تا Request Batching

بیشتر تیم‌هایی که LLM را به production می‌برند، اول از همه روی کیفیت مدل تمرکز می‌کنند — و این اشتباه نیست. اما وقتی مدل دپلوی می‌شود، مشکل اصلی جای دیگری است: معماری inference pipeline تعیین می‌کند که سیستم چقدر...

بیشتر تیم‌هایی که LLM را به production می‌برند، اول از همه روی کیفیت مدل تمرکز می‌کنند — و این اشتباه نیست. اما وقتی مدل دپلوی می‌شود، مشکل اصلی جای دیگری است: معماری inference pipeline تعیین می‌کند که سیستم چقدر گران تمام می‌شود، چه latency‌ای می‌دهد، و آیا اصلاً زیر بار واقعی دوام می‌آورد.

این مقاله برای کسانی نوشته شده که می‌خواهند یک LLM بزرگ را در محیط production واقعی اجرا کنند — نه در notebook، نه با یک request تستی، بلکه با صدها یا هزاران request همزمان.

چرا inference pipeline اهمیت دارد؟

Training یک LLM یک‌بار انجام می‌شود. Inference هر روز، هر ساعت، هر ثانیه اتفاق می‌افتد. هزینهٔ واقعی عملیاتی یک سیستم هوش مصنوعی عمدتاً در inference است، نه training.

یک GPU A100 در حدود ۳ دلار در ساعت هزینه دارد. اگر inference pipeline شما بهینه نباشد، ممکن است برای همان throughput، دو برابر GPU لازم داشته باشید. این یعنی هزینهٔ عملیاتی دو برابر — بدون هیچ بهبودی در کیفیت خروجی.

اما مسئله فقط هزینه نیست. Latency تجربهٔ کاربر را مستقیم تعریف می‌کند. یک مدل GPT-4-level که با latency بالا جواب می‌دهد، در اپلیکیشن‌های real-time قابل استفاده نیست.

اجزای اصلی یک Inference Pipeline

قبل از رفتن به بهینه‌سازی، باید ساختار را درست بشناسیم. یک inference pipeline برای LLM از این لایه‌ها تشکیل می‌شود:

۱. Request Ingestion و Queue Management

هر inference request باید وارد یک صف شود. این صف نه فقط یک buffer ساده است — باید priority، timeout، و load-shedding را مدیریت کند. اگر صف مدیریت نشود، یک burst کوچک می‌تواند کل سیستم را block کند.

۲. Tokenization

متن ورودی باید به token تبدیل شود. این مرحله معمولاً CPU-bound است و در pipeline‌های پر ترافیک می‌تواند bottleneck شود. Tokenizer باید thread-safe باشد و به‌صورت batch قابل اجرا باشد.

۳. Batch Formation

اینجاست که اکثر تیم‌ها اشتباه می‌کنند. GPU برای parallel computation طراحی شده — وقتی یک request را تنها می‌فرستید، اکثر compute هدر می‌رود. Batching چند request را با هم به GPU می‌فرستد تا GPU utilization بالا برود.

۴. Model Forward Pass

خود مدل. اینجا attention computation، KV cache، و matrix multiplication اتفاق می‌افتد. این بخش GPU-bound است و مستقیم با memory bandwidth و compute FLOPS ارتباط دارد.

۵. Token Sampling و Decoding

بعد از forward pass، باید از distribution احتمال، یک token انتخاب شود. روش‌های مختلف sampling (greedy، top-k، top-p) اینجا اعمال می‌شوند.

۶. Response Streaming

در نهایت توکن‌های تولید شده باید به client برسند — یا به‌صورت batch کامل یا به‌صورت streaming. Token streaming (مثل آنچه در ChatGPT می‌بینید) نیاز به پیاده‌سازی SSE یا WebSocket دارد.

Request Batching: قلب بهینه‌سازی inference

Batching مهم‌ترین اهرم بهینه‌سازی inference است. اما نه هر نوع batching.

Static Batching در مقابل Continuous Batching

Static batching ساده است: یک batch از requests جمع می‌کنی، به GPU می‌فرستی، منتظر می‌مانی تا همه تمام شوند، بعد batch بعدی. مشکل: طولانی‌ترین request در batch، همه را منتظر نگه می‌دارد. GPU در حین انتظار idle می‌شود.

Continuous batching (که vLLM آن را معروف کرد) متفاوت است. به‌جای انتظار برای تمام شدن کل batch، هر وقت یک sequence تمام شد، یک sequence جدید جای آن را می‌گیرد. GPU همیشه full است. این تفاوت می‌تواند throughput را ۳-۵ برابر کند.

مقایسهٔ Static Batching و Continuous Batching
ویژگی Static Batching Continuous Batching
GPU Utilization متوسط — idle بین batch‌ها بالا — GPU همیشه مشغول است
Latency برای requests کوتاه بالا — منتظر requests بلند پایین — بلافاصله خارج می‌شوند
پیچیدگی پیاده‌سازی پایین متوسط تا بالا
مناسب برای batch offline processing online serving با traffic متنوع

Batch Size بهینه چقدر است؟

Batch size بزرگ‌تر به معنای GPU utilization بالاتر است — اما latency هم بالاتر می‌رود. Batch size کوچک‌تر latency را کاهش می‌دهد — اما GPU هدر می‌رود.

قانون کلی: برای online serving، latency SLA را تعریف کنید (مثلاً ۵۰۰ms TTFT)، بعد بزرگ‌ترین batch size‌ای که در آن constraint جا می‌شود را پیدا کنید. این یک optimization problem است، نه یک عدد ثابت.

Token Streaming: از نظر معماری چطور کار می‌کند؟

Token streaming یعنی هر توکن که تولید شد، بلافاصله به client ارسال شود — بدون انتظار برای تمام شدن generation. این به ظاهر ساده است اما معماری اش پیچیدگی دارد.

مکانیزم پشت Token Streaming

LLM به‌صورت autoregressive عمل می‌کند: هر توکن به‌صورت مستقل generate می‌شود و به input بعدی اضافه می‌شود. این یعنی هر توکن می‌تواند بلافاصله بعد از محاسبه‌اش ارسال شود.

در سمت server، streaming معمولاً با Server-Sent Events (SSE) یا WebSocket پیاده‌سازی می‌شود. SSE برای یک‌طرفه بودن stream (server به client) ساده‌تر و lightweight‌تر است.

Time to First Token (TTFT) در مقابل Throughput

دو متریک کلیدی وجود دارد که اغلب با هم confuse می‌شوند:

  • TTFT (Time to First Token): چقدر طول می‌کشد تا اولین توکن به client برسد. این مستقیم با perceived responsiveness ارتباط دارد. برای interactive applications، TTFT مهم‌ترین متریک است.
  • Throughput (tokens/second): چقدر توکن در ثانیه تولید می‌شود. این برای هزینه و capacity planning مهم است.

این دو متریک اغلب با هم trade-off دارند. Prefill phase (پردازش input prompt) را می‌توان با batching بهینه کرد — اما این TTFT را افزایش می‌دهد. باید بر اساس use case تصمیم بگیرید.

Prefill و Decode: دو مرحلهٔ جداگانه

Generation در LLM دو مرحلهٔ کاملاً متفاوت دارد:

  • Prefill: پردازش موازی کل prompt. این compute-bound است و می‌تواند در GPU به‌خوبی parallel شود.
  • Decode: تولید یک توکن در هر step. این memory-bandwidth bound است چون KV cache باید هر بار read شود.

Disaggregated prefill-decode — که در سیستم‌هایی مثل Distserve پیاده‌سازی شده — این دو مرحله را روی GPU های جداگانه اجرا می‌کند. نتیجه: هم TTFT بهتر، هم throughput بالاتر. هزینه: پیچیدگی بیشتر در infrastructure.

KV Cache: مهم‌ترین چیزی که باید بفهمید

KV Cache (Key-Value Cache) در attention mechanism، محاسبات قبلی را ذخیره می‌کند تا در decode steps بعدی دوباره محاسبه نشوند. بدون KV cache، هر decode step باید تمام توکن‌های قبلی را دوباره پردازش کند — که O(n²) می‌شود.

چرا KV Cache گران است؟

برای یک مدل ۷۰ میلیارد پارامتر، KV cache یک sequence با طول ۲۰۴۸ توکن می‌تواند چندین گیگابایت GPU memory مصرف کند. وقتی batch size بالا می‌رود، memory برای KV cache به سرعت پر می‌شود — این همان OOM (Out of Memory) ای است که همه باهاش آشنا هستند.

PagedAttention: راه‌حل vLLM

vLLM مشکل KV cache را با PagedAttention حل کرد — ایده‌ای که از virtual memory در OS گرفته شده. به‌جای اینکه KV cache هر sequence به‌صورت contiguous block ذخیره شود، به page های کوچک تقسیم می‌شود. این fragmentation داخلی را به حداقل می‌رساند و امکان می‌دهد GPU memory خیلی بهتر استفاده شود.

نتیجهٔ عملی: با همان GPU memory، می‌توانید batch sizes بزرگ‌تری اجرا کنید. throughput بالاتر. همین.

KV Cache Sharing و Prefix Caching

یک بهینه‌سازی مهم دیگر: وقتی چند request یک prefix مشترک دارند (مثلاً یک system prompt ثابت)، می‌توان KV cache آن prefix را بین همه shared کرد. این در سناریوهایی که یک system prompt بلند برای همه userها ثابت است، می‌تواند TTFT را به‌شدت کاهش دهد.

Quantization: کاهش هزینه با هزینهٔ کیفیت

Quantization وزن‌های مدل را از float32 یا bfloat16 به فرمت‌های کم‌دقت‌تر مثل INT8 یا INT4 تبدیل می‌کند. کمتر memory، throughput بیشتر — اما کیفیت مدل ممکن است کاهش یابد.

انواع Quantization در production

  • GPTQ: post-training quantization با calibration data. برای INT4 و INT8 به‌خوبی جواب می‌دهد. کیفیت نسبتاً خوب حفظ می‌شود.
  • AWQ (Activation-aware Weight Quantization): به‌جای فقط وزن‌ها، activation‌ها را هم در نظر می‌گیرد. معمولاً از GPTQ بهتر است.
  • FP8: نسل جدید quantization که hardware جدید (H100) از آن پشتیبانی می‌کند. بهترین trade-off بین کیفیت و سرعت.

قانون عملی: INT4 برای مدل‌های زبانی general-purpose معمولاً قابل قبول است. برای task های دقیق (coding، math)، به INT8 یا FP8 بروید. همیشه با benchmark واقعی validation کنید — نه فقط perplexity.

ابزارهای inference در production: مقایسهٔ واقعی

vLLM

بهترین گزینه برای اکثر تیم‌ها. PagedAttention، continuous batching، OpenAI-compatible API. خیلی mature شده، community قوی دارد، و برای اکثر مدل‌های open-source به‌خوبی کار می‌کند. محدودیت: برای custom architectures یا multi-modal models کار اضافه لازم دارد.

TensorRT-LLM

ساختهٔ NVIDIA، برای GPU های NVIDIA بهینه شده. اگر روی A100/H100 هستید و throughput حداکثر می‌خواهید، این گزینهٔ جدی است. مشکل: پیچیدگی بالا، build pipeline سنگین، و تغییر مدل وقت می‌برد. برای تیم‌های کوچک معمولاً overkill است.

Triton Inference Server

نه فقط برای LLM، بلکه برای هر نوع model serving. اگر یک platform می‌خواهید که انواع مدل (CV، NLP، tabular) را serve کند، Triton گزینهٔ خوبی است. اما برای LLM به‌تنهایی، vLLM معمولاً ساده‌تر است.

SGLang

نسبتاً جدید اما خیلی امیدوارکننده. برای multi-turn conversations و اپلیکیشن‌هایی که graph-based generation دارند بهینه شده. Prefix caching در آن first-class citizen است. در حال رشد سریع است.

مقایسهٔ ابزارهای inference برای LLM در production
ابزار مناسب برای قوت اصلی محدودیت
vLLM اکثر use case ها Continuous batching، PagedAttention Custom architecture دشوارتر
TensorRT-LLM NVIDIA GPU های high-end حداکثر throughput روی NVIDIA پیچیدگی build، vendor lock-in
Triton Multi-model platform انعطاف model types LLM-specific optimization کمتر
SGLang Multi-turn، agentic Prefix caching، graph execution Community کوچک‌تر

اشتباهات رایج در production inference

اشتباه ۱: نادیده گرفتن memory fragmentation

تیم‌های زیادی با یک GPU سفارش می‌دهند، همه چیز خوب کار می‌کند، بعد در production زیر بار واقعی OOM می‌گیرند. مشکل: KV cache با requests طولانی‌تر به‌شدت رشد می‌کند و GPU memory را تمام می‌کند. راه‌حل: از ابتدا با worst-case sequence length تست کنید.

اشتباه ۲: batch size ثابت

Batch size ثابت یعنی یا GPU idle می‌ماند (batch size کوچک در traffic کم) یا latency بالا می‌رود (batch size بزرگ در traffic زیاد). Dynamic batching با timeout (مثلاً max_wait=50ms) این مشکل را حل می‌کند.

اشتباه ۳: tokenizer در hot path

Tokenizer اغلب Python-based و single-threaded است. وقتی هزاران request در ثانیه دارید، tokenizer bottleneck می‌شود. راه‌حل: tokenizer را به async worker های جداگانه منتقل کنید یا از Rust-based tokenizers استفاده کنید.

اشتباه ۴: load balancing ساده

Round-robin load balancing برای stateless services خوب است. برای LLM inference اما، اگر KV cache بین instances shared نباشد، یک multi-turn conversation که به instance های مختلف می‌رود، prefix caching را بی‌اثر می‌کند. Session affinity یا distributed KV cache لازم است.

اشتباه ۵: monitoring ناکافی

CPU utilization و memory معمول کافی نیست. باید GPU utilization، GPU memory، KV cache hit rate، queue depth، و TTFT/throughput را track کنید. بدون این‌ها نمی‌توانید بفهمید مشکل کجاست.

یک معماری واقعی: چطور یک inference cluster طراحی می‌شود؟

یک سناریوی واقعی: می‌خواهید یک LLM ۷۰B را برای ۱۰۰۰ concurrent user serve کنید. چطور فکر می‌کنید؟

گام اول: تعریف SLA

قبل از هر چیز: TTFT target چقدر است؟ throughput target چقدر؟ max sequence length چقدر؟ این‌ها تعیین می‌کنند که چقدر GPU لازم دارید و چه configuration‌ای باید بزنید. بدون SLA، هر تصمیم تکنیکال arbitrary است.

گام دوم: انتخاب hardware

یک مدل ۷۰B با bfloat16 حدود ۱۴۰GB GPU memory نیاز دارد — یعنی حداقل ۲ عدد H100-80GB یا ۴ عدد A100-40GB. به علاوه KV cache برای concurrent sequences. هزینهٔ per-request را محاسبه کنید و با API providers مقایسه کنید — شاید self-hosting اصلاً cost-effective نباشد.

گام سوم: inference layer

vLLM با tensor parallelism برای multi-GPU. هر node یک replica از مدل را با tensor parallelism اجرا می‌کند. چند node در پشت یک load balancer قرار می‌گیرند. اگر multi-turn دارید، session affinity در load balancer تنظیم کنید.

گام چهارم: queue و rate limiting

یک queue (مثلاً Redis یا Kafka) بین client و inference server. این burst traffic را smooth می‌کند. Rate limiting per user از queue flooding جلوگیری می‌کند. Priority queue اگر SLA های متفاوت دارید.

گام پنجم: observability

Prometheus برای metrics، Grafana برای dashboard. Metric های کلیدی: GPU utilization، KV cache usage، request queue depth، p50/p95/p99 latency، tokens per second. بدون این‌ها در production کور هستید.

Trade-off های واقعی که باید با آن‌ها کنار بیایید

هیچ‌کدام از بهینه‌سازی‌های بالا رایگان نیستند. اینجا trade-off های اصلی است:

  • Throughput در مقابل Latency: batch بزرگ‌تر یعنی throughput بیشتر اما TTFT بالاتر. این trade-off اصلی است.
  • Quantization در مقابل Quality: INT4 هزینه را نصف می‌کند اما ممکن است quality در task های دقیق drop کند.
  • Self-hosting در مقابل API: self-hosting هزینهٔ per-token را کاهش می‌دهد اما CAPEX بالا، ops overhead، و risk را اضافه می‌کند.
  • Model size در مقابل Performance: مدل کوچک‌تر ارزان‌تر اما ممکن است برای use case شما کافی نباشد.
  • Disaggregated prefill-decode در مقابل Simplicity:

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

نظرات (0)

اولین نفری باشید که نظر می‌دهد.

برای ثبت نظر باید وارد حساب کاربری خود شوید.

ورود / ثبت‌نام