LLM Inference Serving: Architecture, Routing & Auto-Scaling › Putting It Together: A Complete Serving Pipeline

Michael BrenndoerferJanuary 19, 20264 min read
Language AI HandbookAll chapters
Part 42 of 67Inference Optimization
Chapter 326LLM Inference Serving: Architecture, Routing & Auto-Scaling › Putting It Together: A Complete Serving Pipeline
Reading section 9 of 12 · Putting It Together: A Complete Serving Pipeline · 4 minSection 9 of 12 · 4 min

Book contents

Section 9 of 12 · 4 min read

LLM Inference Serving: Architecture, Routing & Auto-Scaling › Putting It Together: A Complete Serving PipelineOne section · Sections in this chapter9 / 12 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Let's implement a simplified inference serving pipeline that combines the concepts from this chapter. This example demonstrates how request handling, streaming responses, concurrency limits, and metrics collection work together in a complete system:

In[45]:
Code
import asyncio
import random
import time
from collections import deque
from dataclasses import dataclass, field
from typing import AsyncIterator, List, Optional


@dataclass
class InferenceRequest:
    """A request to the inference server."""

    request_id: str
    prompt: str
    max_tokens: int
    priority: int = 0
    arrival_time: float = field(default_factory=time.time)


@dataclass
class InferenceResponse:
    """Response from inference, supports streaming."""

    request_id: str
    tokens: List[str]
    finished: bool
    ttft_ms: Optional[float] = None
    total_ms: Optional[float] = None


class MockModel:
    """Simulates model inference with realistic timing."""

    def __init__(self, tokens_per_second: float = 50):
        self.tokens_per_second = tokens_per_second

    async def generate(
        self, prompt: str, max_tokens: int
    ) -> AsyncIterator[str]:
        """Simulate token-by-token generation."""
        # Simulate prefill time based on prompt length
        prompt_tokens = len(prompt.split())
        prefill_time = prompt_tokens * 0.0001  # 0.1ms per token
        await asyncio.sleep(prefill_time)

        # Simulate decode, yielding tokens
        words = [
            "The",
            "quick",
            "brown",
            "fox",
            "jumps",
            "over",
            "the",
            "lazy",
            "dog",
            ".",
            "This",
            "is",
            "a",
            "test",
            ".",
        ]

        for i in range(min(max_tokens, 50)):
            await asyncio.sleep(1.0 / self.tokens_per_second)
            yield random.choice(words)


class InferenceServer:
    """Simple inference server with request handling."""

    def __init__(self, model: MockModel, max_concurrent: int = 10):
        self.model = model
        self.max_concurrent = max_concurrent
        self.active_requests = 0
        self.request_queue: deque = deque()
        self.metrics = MetricsCollector()

    async def handle_request(
        self, request: InferenceRequest
    ) -> AsyncIterator[InferenceResponse]:
        """Process a request and yield streaming responses."""

        # Wait if at capacity
        while self.active_requests >= self.max_concurrent:
            await asyncio.sleep(0.01)

        self.active_requests += 1
        tokens = []
        first_token_time = None

        try:
            async for token in self.model.generate(
                request.prompt, request.max_tokens
            ):
                if first_token_time is None:
                    first_token_time = time.time()

                tokens.append(token)
                yield InferenceResponse(
                    request_id=request.request_id,
                    tokens=tokens.copy(),
                    finished=False,
                    ttft_ms=(first_token_time - request.arrival_time) * 1000,
                )

            # Final response
            completion_time = time.time()
            ttft_ms = (first_token_time - request.arrival_time) * 1000
            total_ms = (completion_time - request.arrival_time) * 1000

            self.metrics.record_request(
                latency_ms=total_ms,
                ttft_ms=ttft_ms,
                prompt_tokens=len(request.prompt.split()),
                generated_tokens=len(tokens),
            )

            yield InferenceResponse(
                request_id=request.request_id,
                tokens=tokens,
                finished=True,
                ttft_ms=ttft_ms,
                total_ms=total_ms,
            )

        finally:
            self.active_requests -= 1
In[46]:
Code
async def simulate_traffic(
    server: InferenceServer,
    num_requests: int = 20,
    arrival_rate: float = 5.0,  # requests per second
):
    """Simulate concurrent traffic to the server."""

    async def process_request(request: InferenceRequest):
        final_response = None
        async for response in server.handle_request(request):
            if response.finished:
                final_response = response
        return final_response

    # Create requests with Poisson arrivals
    tasks = []
    for i in range(num_requests):
        request = InferenceRequest(
            request_id=f"req-{i:03d}",
            prompt=f"Request number {i}: "
            + " ".join(["word"] * random.randint(10, 100)),
            max_tokens=random.randint(20, 50),
        )
        task = asyncio.create_task(process_request(request))
        tasks.append(task)

        # Random inter-arrival time
        await asyncio.sleep(random.expovariate(arrival_rate))

    # Wait for all requests to complete
    responses = await asyncio.gather(*tasks)
    return responses
In[47]:
Code
# Run the simulation
model = MockModel(tokens_per_second=50)
server = InferenceServer(model, max_concurrent=5)

# Run async simulation
responses = await simulate_traffic(server, num_requests=30, arrival_rate=3.0)

ttfts = [r.ttft_ms for r in responses if r.ttft_ms]
totals = [r.total_ms for r in responses if r.total_ms]

# Server metrics
server_metrics = server.metrics.get_current_metrics()
Out[48]:
Console
Simulation Results:
--------------------------------------------------
Completed requests: 30

Time to First Token:
  P50: 32ms
  P95: 36ms

Total Latency:
  P50: 692ms
  P95: 955ms

Server Metrics:
  Total tokens processed: 3102

This simulation demonstrates the key components working together in a realistic pattern: requests arrive at varying rates following a Poisson process, queue when the server is at capacity (limited to 5 concurrent requests), stream responses as tokens generate, and metrics accumulate for monitoring. The async design allows concurrent request processing while respecting capacity limits, and the streaming response pattern provides tokens to clients as soon as they're available rather than waiting for complete generation.

The key parameters for the inference server simulation are:

  • max_concurrent: Maximum number of requests the server processes simultaneously. Requests beyond this limit are queued, which increases their TTFT but prevents overwhelming the model or running out of GPU memory.
  • tokens_per_second: The rate at which the model generates tokens during the decode phase. Higher values indicate faster hardware or more efficient implementation.
  • arrival_rate: The average number of requests arriving per second. When arrival rate exceeds processing rate, queues build up and latency increases.