MLOps for Large Models
Model versioning, canary deployment, drift monitoring, rate limiting, quota management. Updated in April 2026.
Overview
Deploying an LLM in production is different from deploying a traditional API. The specific challenges include:
- Heavy models — 7B parameters = 14GB in FP16, 3.5GB in INT4
- Complex state — KV cache, context, session state
- Variable cost — each request costs GPU time
- Subjective quality — there is no "500 error" for a bad response
- Silent drift — the model does not "break", it just gets worse gradually
This document covers how to operate LLMs in production reliably and cost-effectively.
1. Model Versioning
What to version
| Artifact | What it is | Why |
|---|---|---|
| Weights | .safetensors or .pt file | The model itself |
| Config | config.json, tokenizer_config.json | Hyperparameters, vocabulary |
| Tokenizer | Tokenizer file | Same tokenizer = same tokenization |
| Checkpoint | Training state (optimizer, scheduler) | For resuming training |
| Adapter | LoRA/QLoRA weights | Specific fine-tune |
| Prompt templates | System/instruction templates | Same prompt = same behavior |
| Eval results | Evaluation results | For comparison between versions |
Versioning structure
models/
── kode-coder/
│ ├── v0.1.0/ ← Base version (Qwen2.5-Coder-32B)
│ │ ├── model/ ← Weights
│ │ ├── config.json
│ │ ├── tokenizer/
│ │ ├── eval/ ← Evaluation results
│ │ │ ├── swe-bench.json
│ │ │ ├── human-eval.json
│ │ │ └── privado.json
│ │ └── metadata.json ← Date, commit, notes
│ │
│ ├── v0.2.0/ ← SFT fine-tune
│ │ ├── base: v0.1.0 ← Reference to the base model
│ │ ├── adapter/ ← LoRA weights
│ │ ├── config.json
│ │ ├── eval/
│ │ └── metadata.json
│ │
│ ├── v0.3.0/ ← SFT + DPO fine-tune
│ │ ├── base: v0.2.0
│ │ ├── adapter/
│ │ ├── config.json
│ │ ├── eval/
│ │ ── metadata.json
│ │
│ ── current → v0.3.0 ← Symlink to the active versionVersioning tools
| Tool | Use | Pros |
|---|---|---|
| DVC | Versioning of large files | Git-like, open-source |
| MLflow Models | Model registry | UI, experiment tracking |
| Weights & Biases Artifacts | Versioning + lineage | Integration with W&B |
| HuggingFace Hub | Model hosting | Community, APIs |
| OCI Registry | Model container | Cloud-native standard |
Recommendation for Kode: W&B Artifacts + HuggingFace Hub (private).
2. Deployment
Deployment patterns
A. Single model (simplest)
User → Load Balancer → Model v1 (vLLM)Use: Single environment, no need for A/B testing.
B. Blue-Green
┌── Model v1 (blue) ──→ Production
User → LB ──────────┤
└── Model v2 (green) ──→ StagingFlow:
- Deploy v2 in green (receives no traffic)
- Test v2 in staging
- Switch: LB points to green
- v1 stays in standby for rollback
Advantage: Immediate rollback (seconds).
C. Canary
┌── Model v1 ─→ 90% of traffic
User → LB ───────
└── Model v2 ──→ 10% of trafficFlow:
- v2 receives 10% of traffic
- Monitor metrics for 24h
- If OK → 25% → 50% → 100%
- If a problem → rollback to 0%
Advantage: Gradual exposure, a problem affects few users.
D. Shadow
┌── Model v1 ──→ Responds to the user
User → LB ──────┤
└── Model v2 ──→ Runs in shadow (no response)Use: Test v2 without affecting users. Compare outputs of v1 vs v2.
Implementation with vLLM
# Deploy of model v1
vllm serve koder/kode-coder-v0.3.0 \
--port 8000 \
--tensor-parallel-size 2 \
--max-model-len 8192
# Deploy of model v2 (canary)
vllm serve koder/kode-coder-v0.4.0 \
--port 8001 \
--tensor-parallel-size 2 \
--max-model-len 8192Implementation with Kubernetes
apiVersion: apps/v1
kind: Deployment
metadata:
name: kode-model-v0-3-0
spec:
replicas: 3
selector:
matchLabels:
app: kode-model
version: v0.3.0
template:
metadata:
labels:
app: kode-model
version: v0.3.0
spec:
containers:
- name: vllm
image: vllm/vllm-openai:latest
args:
- --model
- koder/kode-coder-v0.3.0
- --tensor-parallel-size
- "2"
resources:
limits:
nvidia.com/gpu: 2
---
apiVersion: v1
kind: Service
metadata:
name: kode-model
spec:
selector:
app: kode-model
# version: v0.3.0 ← change to v0.4.0 to switch version
ports:
- port: 80
targetPort: 80003. Quality Monitoring
Production metrics
| Metric | How to measure | Threshold |
|---|---|---|
| Latency p50 | Time to first token | < 500ms |
| Latency p95 | Time to first token | < 2000ms |
| Latency p99 | Time to first token | < 5000ms |
| Throughput | Tokens generated / second | > 50 tok/s per GPU |
| Error rate | Requests with error / total | < 1% |
| Timeout rate | Timeout requests / total | < 0.5% |
| GPU utilization | % GPU in use | > 70% |
| GPU memory | VRAM used | < 90% |
| KV cache hit rate | Cache reuse | > 30% (with RadixAttention) |
Output quality monitoring
| Metric | How to measure |
|---|---|
| Continuous eval score | Run automatic eval on a production sample |
| User feedback | Thumbs up/down per response |
| Regeneration rate | % of times the user asks "try again" |
| Truncation rate | % of responses cut off (max tokens) |
| Empty response rate | % of empty or whitespace-only responses |
Monitoring pipeline
Production → Metrics collection (LangFuse, Prometheus) → Dashboard (Grafana)
↓
Alerts (Slack, email)
↓
Analysis (weekly)
↓
Decision: keep, rollback, retrain4. Drift Detection
Types of drift
| Type | Description | Example |
|---|---|---|
| Data drift | Input distribution changes | New code libs, new frameworks |
| Concept drift | What is "good" changes | Code best practices evolve |
| Label drift | User preference changes | Users prefer shorter responses |
| Model drift | Model degrades with use | KV cache corruption, quantization drift |
Automatic detection
def detect_drift(current_eval, baseline_eval, threshold=0.05):
"""Detect drift by comparing current eval with baseline."""
metrics = ["code_pass_rate", "llm_judge_score", "latency_p95"]
drifts = []
for metric in metrics:
current = current_eval[metric]
baseline = baseline_eval[metric]
change = abs(current - baseline) / baseline
if change > threshold:
drifts.append({
"metric": metric,
"change": change,
"current": current,
"baseline": baseline,
})
return drifts
# Usage
drifts = detect_drift(eval_semanal, eval_baseline)
if drifts:
alert(f"Drift detected: {drifts}")Actions by type of drift
| Drift detected | Action |
|---|---|
| Data drift | Add new data to the training dataset |
| Concept drift | Retrain with recent data |
| Label drift | Update the reward model with current preferences |
| Model drift | Reload the checkpoint, check hardware |
5. Rate Limiting and Quota Management
Why it is necessary
LLMs are expensive. Without rate limiting:
- A single user can consume the entire GPU
- Infinite loops of API calls exhaust quota
- Attacks (prompt injection) generate cost
Strategies
| Strategy | How it works | Use |
|---|---|---|
| Rate limiting | Maximum requests per minute | All users |
| Token quota | Maximum tokens per day | Free tier users |
| Concurrency limit | Maximum simultaneous requests | Prevent overload |
| Priority queue | Premium users have priority | Multi-tenant |
| Circuit breaker | Stop if error rate > threshold | Prevent cascade |
Implementation
# Rate limiting with Redis
import redis
import time
class RateLimiter:
def __init__(self, redis_client, max_requests=100, window=60):
self.redis = redis_client
self.max_requests = max_requests
self.window = window
def is_allowed(self, user_id):
key = f"rate_limit:{user_id}"
current = self.redis.get(key)
if current is None:
self.redis.setex(key, self.window, 1)
return True
if int(current) >= self.max_requests:
return False
self.redis.incr(key)
return True
# Token quota
class TokenQuota:
def __init__(self, redis_client, daily_limit=100000):
self.redis = redis_client
self.daily_limit = daily_limit
def check_and_consume(self, user_id, tokens):
key = f"token_quota:{user_id}:{date.today()}"
used = int(self.redis.get(key) or 0)
if used + tokens > self.daily_limit:
return False
self.redis.incrby(key, tokens)
return TrueQuota tiers
| Tier | Tokens/day | Requests/min | Priority |
|---|---|---|---|
| Free | 10K | 10 | Low |
| Pro | 100K | 50 | Medium |
| Enterprise | 1M | 200 | High |
| Internal | Unlimited | 500 | Maximum |
6. Rollback
When to roll back
| Signal | Action |
|---|---|
| Eval score drops > 5% | Immediate rollback |
| Error rate > 5% | Immediate rollback |
| Latency p95 > 5s | Rollback within 1h |
| Negative feedback > 20% | Investigate, possibly rollback |
| Drift detected | Evaluate, possibly rollback |
Rollback procedure
# 1. Switch to the previous version (Kubernetes)
kubectl set image deployment/kode-model \
vllm=vllm/vllm-openai:latest \
--record
# 2. Or change the symlink (file-based)
ln -sfn models/kode-coder/v0.3.0 models/kode-coder/current
# 3. Or swap the weight in vLLM (hot reload)
curl -X POST http://localhost:8000/v1/reload \
-d '{"model": "koder/kode-coder-v0.3.0"}'
# 4. Check health
curl http://localhost:8000/health
# 5. Notify the teamTarget rollback time: < 5 minutes.
7. Scalability
Horizontal scaling
┌── Pod 1 (vLLM) ──→ GPU 1
Load Balancer ─────┼── Pod 2 (vLLM) ──→ GPU 2
┼── Pod 3 (vLLM) ──→ GPU 3
└── Pod N (vLLM) ──→ GPU NAuto-scaling:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: kode-model-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: kode-model
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Pods
pods:
metric:
name: gpu_utilization
target:
type: AverageValue
averageValue: 80Vertical scaling (model parallelism)
If the model does not fit in one GPU:
| Technique | When to use | |
|---|---|---|
| Tensor Parallelism | Model > VRAM of 1 GPU | Split weights across GPUs |
| Pipeline Parallelism | Very large model | Split layers across GPUs |
| KV cache offloading | Long context | Move KV cache to CPU/disk |
8. Cost per Request
Calculation
Cost per request = (GPU-hours per request) × (cost per GPU-hour)
GPU-hours per request = (tokens generated × time per token) / 3600
Example:
- 500 tokens generated
- 50ms per token = 25s total
- 25s / 3600 = 0.007 GPU-hours
- A100 cost: $2.80/hour
- Cost per request: 0.007 × $2.80 = $0.02Cost optimization
| Technique | Savings |
|---|---|
| Batching | 30–50% (process multiple requests together) |
| Speculative decoding | 30–50% (fewer tokens generated) |
| Quantization | 0% cost, but allows a smaller GPU (50% savings) |
| KV cache reuse | 20–40% (RadixAttention for repeated prompts) |
| Model switching | 50–80% (use a smaller model for simple tasks) |
For Kode
Recommended MLOps stack
Versioning: W&B Artifacts + HuggingFace Hub (private)
Deployment: Kubernetes + vLLM
Monitoring: Prometheus + Grafana + LangFuse
Alerts: Slack + PagerDuty
Rate limiting: Redis + NGINX
CI/CD: GitHub Actions → build Docker → push → deployDeployment pipeline
1. New checkpoint → Automatic eval (SWE-bench, private eval)
2. If eval OK → Push to HuggingFace Hub + W&B
3. Canary deploy (10% of traffic)
4. Monitor 24h
5. If OK → Gradual ramp-up (25% → 50% → 100%)
6. If a problem → Automatic rollbackInitial configuration
| Item | Configuration |
|---|---|
| Models | v0.1.0 (base) + v0.2.0 (fine-tune) |
| GPUs | 2× RTX 4090 (development) |
| Serving | vLLM with tensor parallelism |
| Monitoring | LangFuse + Prometheus |
| Rate limit | 100 requests/min per user |
| Quota | 100K tokens/day (free), 1M (pro) |
References
| Resource | Description |
|---|---|
| vLLM docs | Serving LLMs with high throughput |
| LangFuse | LLM observability |
| W&B Artifacts | Model versioning |
| Prometheus | Metrics monitoring |
| Kubernetes HPA | Horizontal auto-scaling |