#ai /

AI Inference Service Deployment: The Complete Pipeline from Model to API

A complete introduction to deploying AI model inference services, covering model optimization, service architecture, performance tuning, and monitoring

Goal

Training an AI model is only the beginning -- deploying it as a reliable production service is the real challenge. This article provides a complete introduction to the entire pipeline from model training to API service, helping you understand how to build high-performance, highly available AI inference services.

Background

Core Challenges of Inference Services

  1. Latency Requirements: Users expect millisecond-level responses, but large model inference may take several seconds.
  2. Throughput: Need to serve a large volume of user requests simultaneously.
  3. Cost Control: GPU resources are expensive and need maximum utilization.
  4. Model Updates: Need smooth model version upgrades without affecting service.
  5. High Availability: Inference services cannot be a single point of failure.

Technology Stack Choices

| Phase | Tool | Description | |-------|------|-------------| | Model Optimization | ONNX Runtime, TensorRT | Model compression and acceleration | | Inference Framework | vLLM, TGI, Triton | High-performance inference engines | | API Gateway | Kong, APISIX | Traffic management, rate limiting, authentication | | Orchestration | Kubernetes, Docker Compose | Containerized deployment | | Monitoring | Prometheus, Grafana | Performance monitoring and alerting |

Model Optimization

Model Quantization

from transformers import AutoModelForCausalLM, AutoTokenizer, GPTQConfig
model_id = "meta-llama/Llama-2-7b-hf"
quantization_config = GPTQConfig(
bits=4,
dataset="c4",
group_size=128,
desc_act=True,
)
model = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=quantization_config,
device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained(model_id)
model.save_pretrained("./llama-2-7b-gptq")
tokenizer.save_pretrained("./llama-2-7b-gptq")

ONNX Conversion

import torch
from transformers import AutoModelForSequenceClassification
model = AutoModelForSequenceClassification.from_pretrained("bert-base-uncased", num_labels=2)
model.eval()
dummy_input = torch.randint(0, 1000, (1, 128))
torch.onnx.export(
model, dummy_input, "model.onnx",
input_names=["input_ids", "attention_mask"],
output_names=["logits"],
dynamic_axes={
"input_ids": {0: "batch_size", 1: "sequence"},
"attention_mask": {0: "batch_size", 1: "sequence"},
"logits": {0: "batch_size"},
},
opset_version=14,
)

Inference Service Deployment

Deploying with vLLM

version: '3.8'
services:
vllm:
image: vllm/vllm-openai:latest
runtime: nvidia
environment:
- NVIDIA_VISIBLE_DEVICES=all
- MODEL_NAME=meta-llama/Llama-2-7b-hf
ports:
- "8000:8000"
volumes:
- ~/.cache/huggingface:/root/.cache/huggingface
command: >
--model meta-llama/Llama-2-7b-hf
--served-model-name llama-2-7b
--tensor-parallel-size 1
--max-model-len 4096
--gpu-memory-utilization 0.9
--trust-remote-code
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]

Custom Inference Service

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
import asyncio
from typing import List, Optional
app = FastAPI()
class ChatMessage(BaseModel):
role: str
content: str
class ChatRequest(BaseModel):
model: str
messages: List[ChatMessage]
max_tokens: int = 500
temperature: float = 0.7
stream: bool = False
class ModelManager:
def __init__(self):
self.models = {}
self.tokenizers = {}
def load_model(self, model_name: str):
if model_name not in self.models:
self.tokenizers[model_name] = AutoTokenizer.from_pretrained(model_name)
self.models[model_name] = AutoModelForCausalLM.from_pretrained(
model_name, torch_dtype=torch.float16, device_map="auto",
)
def generate(self, model_name: str, messages: List[ChatMessage],
max_tokens: int, temperature: float) -> str:
self.load_model(model_name)
model = self.models[model_name]
tokenizer = self.tokenizers[model_name]
input_text = self._format_messages(messages)
inputs = tokenizer(input_text, return_tensors="pt").to(model.device)
with torch.no_grad():
outputs = model.generate(
**inputs, max_new_tokens=max_tokens,
temperature=temperature, do_sample=temperature > 0,
top_p=min(1.0, temperature + 0.1),
)
return tokenizer.decode(outputs[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)
def _format_messages(self, messages: List[ChatMessage]) -> str:
formatted = ""
for msg in messages:
if msg.role == "system":
formatted += f"<|system|>\n{msg.content}\n"
elif msg.role == "user":
formatted += f"<|user|>\n{msg.content}\n"
elif msg.role == "assistant":
formatted += f"<|assistant|>\n{msg.content}\n"
formatted += "<|assistant|>\n"
return formatted
manager = ModelManager()
@app.post("/v1/chat/completions")
async def chat_completions(request: ChatRequest):
try:
response = await asyncio.to_thread(
manager.generate, request.model, request.messages,
request.max_tokens, request.temperature,
)
return {
"id": f"chatcmpl-{torch.randint(0, 10000, (1,)).item()}",
"object": "chat.completion",
"created": int(asyncio.get_event_loop().time()),
"model": request.model,
"choices": [{"index": 0, "message": {"role": "assistant", "content": response}, "finish_reason": "stop"}],
"usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0},
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/v1/models")
async def list_models():
return {
"object": "list",
"data": [{"id": name, "object": "model", "owned_by": "local"} for name in manager.models.keys()],
}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

Performance Optimization

Batch Processing

class BatchProcessor:
def __init__(self, max_batch_size: int = 32, max_wait_time: float = 0.1):
self.max_batch_size = max_batch_size
self.max_wait_time = max_wait_time
self.queue = asyncio.Queue()
self.results = {}
async def add_request(self, request_id: str, request: dict) -> dict:
future = asyncio.Future()
self.results[request_id] = future
await self.queue.put((request_id, request))
return await future
async def process_batch(self):
batch, batch_ids = [], []
while len(batch) < self.max_batch_size:
try:
request_id, request = await asyncio.wait_for(self.queue.get(), timeout=self.max_wait_time)
batch.append(request)
batch_ids.append(request_id)
except asyncio.TimeoutError:
break
if batch:
results = await self._batch_inference(batch)
for request_id, result in zip(batch_ids, results):
self.results[request_id].set_result(result)
del self.results[request_id]
async def _batch_inference(self, batch: list) -> list:
pass

Caching Strategy

import hashlib, json
class InferenceCache:
def __init__(self, max_size: int = 1000):
self.cache = {}
self.max_size = max_size
self.access_order = []
def _make_key(self, request: dict) -> str:
cache_data = {"model": request.get("model"), "messages": request.get("messages"), "temperature": request.get("temperature", 0)}
return hashlib.md5(json.dumps(cache_data, sort_keys=True)).hexdigest()
def get(self, request: dict) -> Optional[dict]:
key = self._make_key(request)
if key in self.cache:
self.access_order.remove(key)
self.access_order.append(key)
return self.cache[key]
return None
def set(self, request: dict, response: dict):
key = self._make_key(request)
if len(self.cache) >= self.max_size:
oldest = self.access_order.pop(0)
del self.cache[oldest]
self.cache[key] = response
self.access_order.append(key)

Monitoring and Alerting

Prometheus Metrics

from prometheus_client import Counter, Histogram, Gauge
import time
REQUEST_COUNT = Counter('inference_requests_total', 'Total inference requests', ['model', 'status'])
INFERENCE_LATENCY = Histogram('inference_latency_seconds', 'Inference latency', ['model'], buckets=[0.1, 0.5, 1.0, 2.0, 5.0, 10.0])
GPU_UTILIZATION = Gauge('gpu_utilization_percent', 'GPU utilization', ['gpu_id'])
QUEUE_LENGTH = Gauge('inference_queue_length', 'Current inference queue length')
@app.middleware("http")
async def metrics_middleware(request, call_next):
start_time = time.time()
response = await call_next(request)
duration = time.time() - start_time
INFERENCE_LATENCY.labels(model="default").observe(duration)
status = "success" if response.status_code == 200 else "error"
REQUEST_COUNT.labels(model="default", status=status).inc()
return response

Summary

Core steps for building production-grade AI inference services:

  1. Model Optimization: Reduce model size and inference latency through quantization, pruning, and other techniques.
  2. Inference Engine: Choose the right inference framework (vLLM, TGI, etc.).
  3. Service Architecture: Design a highly available, scalable service architecture.
  4. Performance Optimization: Batch processing, caching, and async processing strategies.
  5. Monitoring and Alerting: A comprehensive monitoring system for timely issue detection and resolution.

Deploying AI inference services is a systems engineering challenge that requires balancing performance, cost, and availability across multiple dimensions.

Like this post? Tweet to share it with others or open an issue to discuss with me!