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:

  1. Heavy models — 7B parameters = 14GB in FP16, 3.5GB in INT4
  2. Complex state — KV cache, context, session state
  3. Variable cost — each request costs GPU time
  4. Subjective quality — there is no "500 error" for a bad response
  5. 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 version

Versioning 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) ──→ Staging

Flow:

  1. Deploy v2 in green (receives no traffic)
  2. Test v2 in staging
  3. Switch: LB points to green
  4. v1 stays in standby for rollback

Advantage: Immediate rollback (seconds).

C. Canary

                    ┌── Model v1 ─→ 90% of traffic
User → LB ───────
                    └── Model v2 ──→ 10% of traffic

Flow:

  1. v2 receives 10% of traffic
  2. Monitor metrics for 24h
  3. If OK → 25% → 50% → 100%
  4. 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 8192

Implementation 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: 8000

3. 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, retrain

4. 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 True

Quota 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 team

Target 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 N

Auto-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: 80

Vertical 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.02

Cost 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

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 → deploy

Deployment 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 rollback

Initial 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