AI Deployment·23 min read

Deploying AI Applications: Production Best Practices and Scaling

Lyubo
Lyubo·
Deploying AI Applications: Production Best Practices and Scaling

Learn production deployment strategies for AI applications including containerization, monitoring, scaling, and maintaining AI systems in enterprise environments.

Deploying AI Applications: Production Best Practices and Scaling

Published on December 15, 2024 • 22 min read

Deploying AI applications to production requires careful consideration of scalability, reliability, and performance. This comprehensive guide covers everything you need to know about taking your AI models from development to production-ready systems that can handle real-world traffic and demands.

Table of Contents

  1. Production Readiness Checklist
  2. Infrastructure Architecture
  3. Model Serving Strategies
  4. Monitoring and Observability
  5. Security and Compliance
  6. Performance Optimization
  7. Scaling Strategies
  8. CI/CD for AI Applications
  9. Real-World Case Study
  10. Troubleshooting Guide

Production Readiness Checklist

Before deploying any AI application to production, ensure you've addressed these critical areas:

Model Validation

  • Performance Metrics: Accuracy, precision, recall, F1-score
  • Latency Requirements: Response time under various loads
  • Resource Consumption: Memory, CPU, GPU utilization
  • Edge Cases: Handling of unexpected inputs
  • Data Drift Detection: Monitoring for model degradation

Infrastructure Requirements

  • Compute Resources: CPU, GPU, memory specifications
  • Storage: Model artifacts, logs, temporary data
  • Network: Bandwidth, latency, security
  • Backup and Recovery: Data and model versioning
  • Disaster Recovery: Multi-region deployment strategies

Infrastructure Architecture

Microservices Architecture

1# docker-compose.yml for AI microservices
2version: '3.8'
3services:
4 model-server:
5 image: ai-model-server:latest
6 ports:
7 - "8080:8080"
8 environment:
9 - MODEL_PATH=/models/latest
10 - BATCH_SIZE=32
11 - MAX_WORKERS=4
12 volumes:
13 - ./models:/models
14 deploy:
15 resources:
16 limits:
17 memory: 4G
18 cpus: '2'
19 reservations:
20 memory: 2G
21 cpus: '1'
22
23 api-gateway:
24 image: nginx:alpine
25 ports:
26 - "80:80"
27 volumes:
28 - ./nginx.conf:/etc/nginx/nginx.conf
29 depends_on:
30 - model-server
31
32 redis-cache:
33 image: redis:alpine
34 ports:
35 - "6379:6379"
36 command: redis-server --maxmemory 1gb --maxmemory-policy allkeys-lru
37
38 monitoring:
39 image: prometheus/prometheus
40 ports:
41 - "9090:9090"
42 volumes:
43 - ./prometheus.yml:/etc/prometheus/prometheus.yml

Load Balancer Configuration

1# nginx.conf
2upstream model_servers {
3 least_conn;
4 server model-server-1:8080 max_fails=3 fail_timeout=30s;
5 server model-server-2:8080 max_fails=3 fail_timeout=30s;
6 server model-server-3:8080 max_fails=3 fail_timeout=30s;
7}
8
9server {
10 listen 80;
11
12 location /predict {
13 proxy_pass http://model_servers;
14 proxy_set_header Host $host;
15 proxy_set_header X-Real-IP $remote_addr;
16 proxy_connect_timeout 30s;
17 proxy_send_timeout 30s;
18 proxy_read_timeout 30s;
19
20 # Enable caching for GET requests
21 proxy_cache_methods GET HEAD;
22 proxy_cache_valid 200 5m;
23 }
24
25 location /health {
26 proxy_pass http://model_servers/health;
27 access_log off;
28 }
29}

Model Serving Strategies

1. REST API Serving

1# app.py - Flask-based model server
2from flask import Flask, request, jsonify
3import torch
4import numpy as np
5from transformers import AutoTokenizer, AutoModel
6import redis
7import json
8import logging
9from prometheus_client import Counter, Histogram, generate_latest
10
11app = Flask(__name__)
12
13# Metrics
14REQUEST_COUNT = Counter('model_requests_total', 'Total model requests')
15REQUEST_LATENCY = Histogram('model_request_duration_seconds', 'Model request latency')
16
17# Initialize model and cache
18model = None
19tokenizer = None
20cache = redis.Redis(host='redis-cache', port=6379, db=0)
21
22def load_model():
23 global model, tokenizer
24 model_path = os.getenv('MODEL_PATH', './model')
25 tokenizer = AutoTokenizer.from_pretrained(model_path)
26 model = AutoModel.from_pretrained(model_path)
27 model.eval()
28 logging.info("Model loaded successfully")
29
30@app.before_first_request
31def initialize():
32 load_model()
33
34@app.route('/predict', methods=['POST'])
35@REQUEST_LATENCY.time()
36def predict():
37 REQUEST_COUNT.inc()
38
39 try:
40 data = request.get_json()
41 text = data.get('text', '')
42
43 # Check cache first
44 cache_key = f"prediction:{hash(text)}"
45 cached_result = cache.get(cache_key)
46
47 if cached_result:
48 return jsonify(json.loads(cached_result))
49
50 # Tokenize and predict
51 inputs = tokenizer(text, return_tensors='pt', truncation=True, padding=True)
52
53 with torch.no_grad():
54 outputs = model(**inputs)
55 predictions = torch.nn.functional.softmax(outputs.logits, dim=-1)
56
57 result = {
58 'predictions': predictions.tolist(),
59 'confidence': float(torch.max(predictions)),
60 'model_version': os.getenv('MODEL_VERSION', '1.0.0')
61 }
62
63 # Cache result for 5 minutes
64 cache.setex(cache_key, 300, json.dumps(result))
65
66 return jsonify(result)
67
68 except Exception as e:
69 logging.error(f"Prediction error: {str(e)}")
70 return jsonify({'error': 'Internal server error'}), 500
71
72@app.route('/health', methods=['GET'])
73def health_check():
74 try:
75 # Check model availability
76 if model is None:
77 return jsonify({'status': 'unhealthy', 'reason': 'Model not loaded'}), 503
78
79 # Check cache connectivity
80 cache.ping()
81
82 return jsonify({
83 'status': 'healthy',
84 'model_version': os.getenv('MODEL_VERSION', '1.0.0'),
85 'timestamp': datetime.utcnow().isoformat()
86 })
87 except Exception as e:
88 return jsonify({'status': 'unhealthy', 'reason': str(e)}), 503
89
90@app.route('/metrics', methods=['GET'])
91def metrics():
92 return generate_latest()
93
94if __name__ == '__main__':
95 logging.basicConfig(level=logging.INFO)
96 app.run(host='0.0.0.0', port=8080)

2. gRPC Serving for High Performance

1# grpc_server.py
2import grpc
3from concurrent import futures
4import model_pb2
5import model_pb2_grpc
6import torch
7import time
8
9class ModelServicer(model_pb2_grpc.ModelServiceServicer):
10 def __init__(self):
11 self.model = self.load_model()
12
13 def load_model(self):
14 # Load your model here
15 model = torch.jit.load('model.pt')
16 model.eval()
17 return model
18
19 def Predict(self, request, context):
20 try:
21 # Convert request to tensor
22 input_tensor = torch.tensor(request.features).float()
23
24 # Make prediction
25 with torch.no_grad():
26 output = self.model(input_tensor)
27 predictions = torch.nn.functional.softmax(output, dim=-1)
28
29 # Create response
30 response = model_pb2.PredictionResponse()
31 response.predictions.extend(predictions.tolist())
32 response.confidence = float(torch.max(predictions))
33 response.latency_ms = int((time.time() - start_time) * 1000)
34
35 return response
36
37 except Exception as e:
38 context.set_code(grpc.StatusCode.INTERNAL)
39 context.set_details(f'Prediction failed: {str(e)}')
40 return model_pb2.PredictionResponse()
41
42def serve():
43 server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
44 model_pb2_grpc.add_ModelServiceServicer_to_server(ModelServicer(), server)
45
46 listen_addr = '[::]:50051'
47 server.add_insecure_port(listen_addr)
48
49 print(f"Starting gRPC server on {listen_addr}")
50 server.start()
51 server.wait_for_termination()
52
53if __name__ == '__main__':
54 serve()

Monitoring and Observability

Prometheus Metrics Configuration

1# prometheus.yml
2global:
3 scrape_interval: 15s
4
5scrape_configs:
6 - job_name: 'model-servers'
7 static_configs:
8 - targets: ['model-server-1:8080', 'model-server-2:8080']
9 metrics_path: '/metrics'
10 scrape_interval: 10s
11
12 - job_name: 'node-exporter'
13 static_configs:
14 - targets: ['node-exporter:9100']
15
16rule_files:
17 - "alert_rules.yml"
18
19alerting:
20 alertmanagers:
21 - static_configs:
22 - targets:
23 - alertmanager:9093

Custom Metrics Dashboard

1# monitoring.py
2from prometheus_client import Counter, Histogram, Gauge, start_http_server
3import psutil
4import torch
5import time
6
7# Define metrics
8model_requests = Counter('model_requests_total', 'Total model requests', ['endpoint', 'status'])
9model_latency = Histogram('model_latency_seconds', 'Model inference latency')
10gpu_utilization = Gauge('gpu_utilization_percent', 'GPU utilization percentage')
11memory_usage = Gauge('memory_usage_bytes', 'Memory usage in bytes')
12
13class ModelMonitor:
14 def __init__(self):
15 self.start_time = time.time()
16
17 def record_request(self, endpoint, status, latency):
18 model_requests.labels(endpoint=endpoint, status=status).inc()
19 model_latency.observe(latency)
20
21 def update_system_metrics(self):
22 # CPU and Memory
23 memory_usage.set(psutil.virtual_memory().used)
24
25 # GPU metrics (if available)
26 if torch.cuda.is_available():
27 gpu_utilization.set(torch.cuda.utilization())
28
29 def start_monitoring(self, port=8000):
30 start_http_server(port)
31 print(f"Metrics server started on port {port}")

Logging Configuration

1# logging_config.py
2import logging
3import json
4from datetime import datetime
5
6class JSONFormatter(logging.Formatter):
7 def format(self, record):
8 log_entry = {
9 'timestamp': datetime.utcnow().isoformat(),
10 'level': record.levelname,
11 'message': record.getMessage(),
12 'module': record.module,
13 'function': record.funcName,
14 'line': record.lineno
15 }
16
17 if hasattr(record, 'request_id'):
18 log_entry['request_id'] = record.request_id
19
20 if hasattr(record, 'user_id'):
21 log_entry['user_id'] = record.user_id
22
23 return json.dumps(log_entry)
24
25def setup_logging():
26 logger = logging.getLogger()
27 logger.setLevel(logging.INFO)
28
29 handler = logging.StreamHandler()
30 handler.setFormatter(JSONFormatter())
31 logger.addHandler(handler)
32
33 return logger

Security and Compliance

API Authentication and Authorization

1# auth.py
2from functools import wraps
3from flask import request, jsonify
4import jwt
5import os
6
7def require_auth(f):
8 @wraps(f)
9 def decorated_function(*args, **kwargs):
10 token = request.headers.get('Authorization')
11
12 if not token:
13 return jsonify({'error': 'No token provided'}), 401
14
15 try:
16 # Remove 'Bearer ' prefix
17 token = token.replace('Bearer ', '')
18
19 # Verify JWT token
20 payload = jwt.decode(
21 token,
22 os.getenv('JWT_SECRET'),
23 algorithms=['HS256']
24 )
25
26 # Add user info to request context
27 request.user_id = payload.get('user_id')
28 request.permissions = payload.get('permissions', [])
29
30 except jwt.ExpiredSignatureError:
31 return jsonify({'error': 'Token expired'}), 401
32 except jwt.InvalidTokenError:
33 return jsonify({'error': 'Invalid token'}), 401
34
35 return f(*args, **kwargs)
36 return decorated_function
37
38def require_permission(permission):
39 def decorator(f):
40 @wraps(f)
41 def decorated_function(*args, **kwargs):
42 if permission not in request.permissions:
43 return jsonify({'error': 'Insufficient permissions'}), 403
44 return f(*args, **kwargs)
45 return decorated_function
46 return decorator

Input Validation and Sanitization

1# validation.py
2from marshmallow import Schema, fields, validate, ValidationError
3import bleach
4
5class PredictionRequestSchema(Schema):
6 text = fields.Str(
7 required=True,
8 validate=validate.Length(min=1, max=10000),
9 missing=""
10 )
11 model_version = fields.Str(
12 validate=validate.OneOf(['1.0.0', '1.1.0', '2.0.0']),
13 missing='latest'
14 )
15 options = fields.Dict(missing={})
16
17def validate_and_sanitize_input(data):
18 schema = PredictionRequestSchema()
19
20 try:
21 # Validate input
22 validated_data = schema.load(data)
23
24 # Sanitize text input
25 validated_data['text'] = bleach.clean(
26 validated_data['text'],
27 tags=[], # No HTML tags allowed
28 strip=True
29 )
30
31 return validated_data, None
32
33 except ValidationError as err:
34 return None, err.messages

Performance Optimization

Model Optimization Techniques

1# optimization.py
2import torch
3import torch.quantization as quantization
4from torch.jit import script
5
6class ModelOptimizer:
7 def __init__(self, model):
8 self.model = model
9
10 def quantize_model(self):
11 """Apply dynamic quantization for faster inference"""
12 quantized_model = quantization.quantize_dynamic(
13 self.model,
14 {torch.nn.Linear},
15 dtype=torch.qint8
16 )
17 return quantized_model
18
19 def compile_model(self):
20 """Compile model with TorchScript for production"""
21 self.model.eval()
22 example_input = torch.randn(1, 512) # Adjust based on your model
23
24 traced_model = torch.jit.trace(self.model, example_input)
25 return traced_model
26
27 def optimize_for_inference(self):
28 """Apply multiple optimization techniques"""
29 # Quantization
30 quantized = self.quantize_model()
31
32 # TorchScript compilation
33 compiled = self.compile_model()
34
35 # Freeze model parameters
36 for param in compiled.parameters():
37 param.requires_grad = False
38
39 return compiled
40
41# Usage example
42def optimize_model(model_path, output_path):
43 model = torch.load(model_path)
44 optimizer = ModelOptimizer(model)
45
46 optimized_model = optimizer.optimize_for_inference()
47 torch.jit.save(optimized_model, output_path)
48
49 print(f"Optimized model saved to {output_path}")

Caching Strategies

1# caching.py
2import redis
3import pickle
4import hashlib
5from functools import wraps
6import json
7
8class ModelCache:
9 def __init__(self, redis_host='localhost', redis_port=6379):
10 self.redis_client = redis.Redis(host=redis_host, port=redis_port, db=0)
11
12 def generate_cache_key(self, input_data, model_version):
13 """Generate a unique cache key for input data"""
14 data_str = json.dumps(input_data, sort_keys=True)
15 hash_obj = hashlib.md5(f"{data_str}:{model_version}".encode())
16 return f"prediction:{hash_obj.hexdigest()}"
17
18 def get_cached_prediction(self, input_data, model_version):
19 """Retrieve cached prediction if available"""
20 cache_key = self.generate_cache_key(input_data, model_version)
21 cached_result = self.redis_client.get(cache_key)
22
23 if cached_result:
24 return pickle.loads(cached_result)
25 return None
26
27 def cache_prediction(self, input_data, model_version, prediction, ttl=3600):
28 """Cache prediction result"""
29 cache_key = self.generate_cache_key(input_data, model_version)
30 serialized_prediction = pickle.dumps(prediction)
31
32 self.redis_client.setex(cache_key, ttl, serialized_prediction)
33
34def cache_predictions(cache_instance, ttl=3600):
35 """Decorator to cache model predictions"""
36 def decorator(predict_func):
37 @wraps(predict_func)
38 def wrapper(input_data, model_version='latest'):
39 # Try to get from cache first
40 cached_result = cache_instance.get_cached_prediction(input_data, model_version)
41 if cached_result:
42 return cached_result
43
44 # If not in cache, compute prediction
45 result = predict_func(input_data, model_version)
46
47 # Cache the result
48 cache_instance.cache_prediction(input_data, model_version, result, ttl)
49
50 return result
51 return wrapper
52 return decorator

Scaling Strategies

Horizontal Pod Autoscaling (Kubernetes)

1# hpa.yaml
2apiVersion: autoscaling/v2
3kind: HorizontalPodAutoscaler
4metadata:
5 name: model-server-hpa
6spec:
7 scaleTargetRef:
8 apiVersion: apps/v1
9 kind: Deployment
10 name: model-server
11 minReplicas: 2
12 maxReplicas: 10
13 metrics:
14 - type: Resource
15 resource:
16 name: cpu
17 target:
18 type: Utilization
19 averageUtilization: 70
20 - type: Resource
21 resource:
22 name: memory
23 target:
24 type: Utilization
25 averageUtilization: 80
26 - type: Pods
27 pods:
28 metric:
29 name: model_requests_per_second
30 target:
31 type: AverageValue
32 averageValue: "100"

Auto-scaling with Custom Metrics

1# autoscaler.py
2import kubernetes
3from kubernetes import client, config
4import time
5import requests
6
7class ModelAutoscaler:
8 def __init__(self):
9 config.load_incluster_config() # For in-cluster usage
10 self.apps_v1 = client.AppsV1Api()
11 self.metrics_url = "http://prometheus:9090/api/v1/query"
12
13 def get_current_replicas(self, deployment_name, namespace='default'):
14 deployment = self.apps_v1.read_namespaced_deployment(
15 name=deployment_name,
16 namespace=namespace
17 )
18 return deployment.spec.replicas
19
20 def scale_deployment(self, deployment_name, replicas, namespace='default'):
21 # Update deployment replica count
22 deployment = self.apps_v1.read_namespaced_deployment(
23 name=deployment_name,
24 namespace=namespace
25 )
26
27 deployment.spec.replicas = replicas
28
29 self.apps_v1.patch_namespaced_deployment(
30 name=deployment_name,
31 namespace=namespace,
32 body=deployment
33 )
34
35 def get_metric_value(self, query):
36 response = requests.get(self.metrics_url, params={'query': query})
37 data = response.json()
38
39 if data['data']['result']:
40 return float(data['data']['result'][0]['value'][1])
41 return 0
42
43 def auto_scale_loop(self):
44 while True:
45 try:
46 # Get current metrics
47 avg_latency = self.get_metric_value(
48 'avg(model_latency_seconds)'
49 )
50 request_rate = self.get_metric_value(
51 'rate(model_requests_total[5m])'
52 )
53
54 current_replicas = self.get_current_replicas('model-server')
55
56 # Scaling logic
57 if avg_latency > 2.0 or request_rate > 100:
58 # Scale up
59 new_replicas = min(current_replicas + 1, 10)
60 elif avg_latency < 0.5 and request_rate < 20:
61 # Scale down
62 new_replicas = max(current_replicas - 1, 2)
63 else:
64 new_replicas = current_replicas
65
66 if new_replicas != current_replicas:
67 print(f"Scaling from {current_replicas} to {new_replicas} replicas")
68 self.scale_deployment('model-server', new_replicas)
69
70 except Exception as e:
71 print(f"Auto-scaling error: {e}")
72
73 time.sleep(30) # Check every 30 seconds
74
75if __name__ == '__main__':
76 autoscaler = ModelAutoscaler()
77 autoscaler.auto_scale_loop()

CI/CD for AI Applications

GitHub Actions Workflow

1# .github/workflows/deploy.yml
2name: Deploy AI Model
3
4on:
5 push:
6 branches: [main]
7 pull_request:
8 branches: [main]
9
10jobs:
11 test:
12 runs-on: ubuntu-latest
13 steps:
14 - uses: actions/checkout@v3
15
16 - name: Set up Python
17 uses: actions/setup-python@v4
18 with:
19 python-version: '3.9'
20
21 - name: Install dependencies
22 run: |
23 pip install -r requirements.txt
24 pip install pytest pytest-cov
25
26 - name: Run tests
27 run: |
28 pytest tests/ --cov=src/ --cov-report=xml
29
30 - name: Model validation
31 run: |
32 python scripts/validate_model.py
33
34 build:
35 needs: test
36 runs-on: ubuntu-latest
37 if: github.ref == 'refs/heads/main'
38
39 steps:
40 - uses: actions/checkout@v3
41
42 - name: Build Docker image
43 run: |
44 docker build -t ${{ secrets.REGISTRY_URL }}/model-server:${{ github.sha }} .
45
46 - name: Push to registry
47 run: |
48 echo ${{ secrets.REGISTRY_PASSWORD }} | docker login ${{ secrets.REGISTRY_URL }} -u ${{ secrets.REGISTRY_USERNAME }} --password-stdin
49 docker push ${{ secrets.REGISTRY_URL }}/model-server:${{ github.sha }}
50
51 deploy:
52 needs: build
53 runs-on: ubuntu-latest
54 if: github.ref == 'refs/heads/main'
55
56 steps:
57 - name: Deploy to Kubernetes
58 run: |
59 kubectl set image deployment/model-server model-server=${{ secrets.REGISTRY_URL }}/model-server:${{ github.sha }}
60 kubectl rollout status deployment/model-server

Model Validation Pipeline

1# scripts/validate_model.py
2import torch
3import numpy as np
4import json
5import sys
6from pathlib import Path
7
8class ModelValidator:
9 def __init__(self, model_path, test_data_path):
10 self.model_path = model_path
11 self.test_data_path = test_data_path
12 self.model = None
13
14 def load_model(self):
15 try:
16 self.model = torch.load(self.model_path, map_location='cpu')
17 self.model.eval()
18 return True
19 except Exception as e:
20 print(f"Failed to load model: {e}")
21 return False
22
23 def validate_model_structure(self):
24 """Validate model has expected structure"""
25 required_methods = ['forward']
26
27 for method in required_methods:
28 if not hasattr(self.model, method):
29 print(f"Model missing required method: {method}")
30 return False
31
32 return True
33
34 def validate_performance(self):
35 """Validate model performance on test data"""
36 with open(self.test_data_path, 'r') as f:
37 test_data = json.load(f)
38
39 correct_predictions = 0
40 total_predictions = len(test_data)
41
42 for item in test_data:
43 input_tensor = torch.tensor(item['input']).float()
44 expected_output = item['expected_output']
45
46 with torch.no_grad():
47 output = self.model(input_tensor)
48 predicted_class = torch.argmax(output).item()
49
50 if predicted_class == expected_output:
51 correct_predictions += 1
52
53 accuracy = correct_predictions / total_predictions
54
55 if accuracy < 0.85: # Minimum acceptable accuracy
56 print(f"Model accuracy {accuracy:.3f} below threshold 0.85")
57 return False
58
59 print(f"Model validation passed with accuracy: {accuracy:.3f}")
60 return True
61
62 def validate_inference_speed(self):
63 """Validate model inference speed"""
64 import time
65
66 # Dummy input for speed test
67 dummy_input = torch.randn(1, 512) # Adjust based on your model
68
69 # Warm up
70 for _ in range(10):
71 with torch.no_grad():
72 _ = self.model(dummy_input)
73
74 # Measure inference time
75 start_time = time.time()
76 for _ in range(100):
77 with torch.no_grad():
78 _ = self.model(dummy_input)
79 end_time = time.time()
80
81 avg_inference_time = (end_time - start_time) / 100
82
83 if avg_inference_time > 0.1: # Max 100ms per inference
84 print(f"Inference time {avg_inference_time:.3f}s exceeds threshold 0.1s")
85 return False
86
87 print(f"Inference speed validation passed: {avg_inference_time:.3f}s")
88 return True
89
90 def run_all_validations(self):
91 """Run all validation checks"""
92 checks = [
93 ("Model Loading", self.load_model),
94 ("Model Structure", self.validate_model_structure),
95 ("Performance", self.validate_performance),
96 ("Inference Speed", self.validate_inference_speed)
97 ]
98
99 for check_name, check_func in checks:
100 print(f"Running {check_name} validation...")
101 if not check_func():
102 print(f"❌ {check_name} validation failed")
103 return False
104 print(f"✅ {check_name} validation passed")
105
106 print("🎉 All model validations passed!")
107 return True
108
109if __name__ == '__main__':
110 model_path = sys.argv[1] if len(sys.argv) > 1 else 'models/latest.pt'
111 test_data_path = sys.argv[2] if len(sys.argv) > 2 else 'data/test_data.json'
112
113 validator = ModelValidator(model_path, test_data_path)
114
115 if not validator.run_all_validations():
116 sys.exit(1)

Real-World Case Study

E-commerce Recommendation System

Let's walk through deploying a real-world recommendation system for an e-commerce platform:

Architecture Overview

1# recommendation_service.py
2from flask import Flask, request, jsonify
3import pandas as pd
4import numpy as np
5from sklearn.metrics.pairwise import cosine_similarity
6import redis
7import logging
8from datetime import datetime, timedelta
9
10app = Flask(__name__)
11
12class RecommendationEngine:
13 def __init__(self):
14 self.user_item_matrix = None
15 self.item_features = None
16 self.model_version = "1.2.0"
17 self.cache = redis.Redis(host='redis', port=6379, db=0)
18
19 def load_data(self):
20 """Load user-item interaction data and item features"""
21 # In production, this would load from your data warehouse
22 self.user_item_matrix = pd.read_parquet('data/user_item_matrix.parquet')
23 self.item_features = pd.read_parquet('data/item_features.parquet')
24
25 def get_user_recommendations(self, user_id, num_recommendations=10):
26 """Generate recommendations for a user"""
27 cache_key = f"recommendations:{user_id}:{num_recommendations}"
28
29 # Check cache first
30 cached_result = self.cache.get(cache_key)
31 if cached_result:
32 return json.loads(cached_result)
33
34 # Generate recommendations
35 user_vector = self.user_item_matrix.loc[user_id].values.reshape(1, -1)
36
37 # Calculate similarity with all items
38 similarities = cosine_similarity(user_vector, self.item_features.values)[0]
39
40 # Get top recommendations
41 top_indices = np.argsort(similarities)[::-1][:num_recommendations]
42 recommendations = [
43 {
44 'item_id': self.item_features.index[idx],
45 'score': float(similarities[idx]),
46 'reason': self._get_recommendation_reason(user_id, idx)
47 }
48 for idx in top_indices
49 ]
50
51 # Cache for 1 hour
52 self.cache.setex(cache_key, 3600, json.dumps(recommendations))
53
54 return recommendations
55
56 def _get_recommendation_reason(self, user_id, item_idx):
57 """Generate explanation for recommendation"""
58 # Simplified reasoning logic
59 return "Based on your purchase history and similar users"
60
61# Initialize recommendation engine
62rec_engine = RecommendationEngine()
63
64@app.before_first_request
65def initialize():
66 rec_engine.load_data()
67 logging.info("Recommendation engine initialized")
68
69@app.route('/recommendations/<int:user_id>')
70def get_recommendations(user_id):
71 try:
72 num_recs = request.args.get('count', 10, type=int)
73 recommendations = rec_engine.get_user_recommendations(user_id, num_recs)
74
75 return jsonify({
76 'user_id': user_id,
77 'recommendations': recommendations,
78 'model_version': rec_engine.model_version,
79 'timestamp': datetime.utcnow().isoformat()
80 })
81
82 except Exception as e:
83 logging.error(f"Recommendation error for user {user_id}: {str(e)}")
84 return jsonify({'error': 'Failed to generate recommendations'}), 500
85
86@app.route('/health')
87def health_check():
88 return jsonify({
89 'status': 'healthy',
90 'model_version': rec_engine.model_version,
91 'cache_status': 'connected' if rec_engine.cache.ping() else 'disconnected'
92 })
93
94if __name__ == '__main__':
95 logging.basicConfig(level=logging.INFO)
96 app.run(host='0.0.0.0', port=8080)

Deployment Configuration

1# k8s-deployment.yaml
2apiVersion: apps/v1
3kind: Deployment
4metadata:
5 name: recommendation-service
6spec:
7 replicas: 3
8 selector:
9 matchLabels:
10 app: recommendation-service
11 template:
12 metadata:
13 labels:
14 app: recommendation-service
15 spec:
16 containers:
17 - name: recommendation-service
18 image: recommendation-service:latest
19 ports:
20 - containerPort: 8080
21 env:
22 - name: REDIS_HOST
23 value: "redis-service"
24 - name: MODEL_VERSION
25 value: "1.2.0"
26 resources:
27 requests:
28 memory: "1Gi"
29 cpu: "500m"
30 limits:
31 memory: "2Gi"
32 cpu: "1000m"
33 livenessProbe:
34 httpGet:
35 path: /health
36 port: 8080
37 initialDelaySeconds: 30
38 periodSeconds: 10
39 readinessProbe:
40 httpGet:
41 path: /health
42 port: 8080
43 initialDelaySeconds: 5
44 periodSeconds: 5
45---
46apiVersion: v1
47kind: Service
48metadata:
49 name: recommendation-service
50spec:
51 selector:
52 app: recommendation-service
53 ports:
54 - port: 80
55 targetPort: 8080
56 type: LoadBalancer

Troubleshooting Guide

Common Issues and Solutions

1. High Latency Issues

Symptoms:

  • Response times > 2 seconds
  • Timeout errors
  • User complaints about slow responses

Diagnosis:

1# Check response times
2curl -w "@curl-format.txt" -o /dev/null -s "http://your-api/predict"
3
4# Monitor system resources
5kubectl top pods
6kubectl top nodes
7
8# Check application logs
9kubectl logs -f deployment/model-server

Solutions:

  • Implement request batching
  • Add caching layer
  • Optimize model inference
  • Scale horizontally

2. Memory Leaks

Symptoms:

  • Gradually increasing memory usage
  • Out of memory errors
  • Pod restarts

Diagnosis:

1# memory_profiler.py
2import psutil
3import gc
4import torch
5
6def monitor_memory():
7 process = psutil.Process()
8 memory_info = process.memory_info()
9
10 print(f"RSS: {memory_info.rss / 1024 / 1024:.2f} MB")
11 print(f"VMS: {memory_info.vms / 1024 / 1024:.2f} MB")
12
13 if torch.cuda.is_available():
14 print(f"GPU Memory: {torch.cuda.memory_allocated() / 1024 / 1024:.2f} MB")
15
16def cleanup_memory():
17 gc.collect()
18 if torch.cuda.is_available():
19 torch.cuda.empty_cache()

Solutions:

  • Implement proper garbage collection
  • Clear GPU cache after inference
  • Use context managers for resource cleanup
  • Set memory limits in containers

3. Model Drift Detection

Symptoms:

  • Declining accuracy over time
  • Unexpected prediction patterns
  • User feedback indicating poor results

Monitoring:

1# drift_detector.py
2import numpy as np
3from scipy import stats
4import logging
5
6class DriftDetector:
7 def __init__(self, reference_data, threshold=0.05):
8 self.reference_data = reference_data
9 self.threshold = threshold
10
11 def detect_drift(self, new_data):
12 """Detect data drift using Kolmogorov-Smirnov test"""
13 statistic, p_value = stats.ks_2samp(self.reference_data, new_data)
14
15 if p_value < self.threshold:
16 logging.warning(f"Data drift detected! p-value: {p_value}")
17 return True
18
19 return False
20
21 def update_reference(self, new_reference_data):
22 """Update reference data for drift detection"""
23 self.reference_data = new_reference_data

Conclusion

Deploying AI applications to production requires careful planning and implementation of robust infrastructure, monitoring, and scaling strategies. Key takeaways:

  1. Start with a solid foundation: Proper containerization, health checks, and monitoring
  2. Plan for scale: Implement auto-scaling and load balancing from the beginning
  3. Monitor everything: Track performance, errors, and business metrics
  4. Automate deployments: Use CI/CD pipelines for consistent, reliable deployments
  5. Prepare for failures: Implement proper error handling and recovery mechanisms

By following these best practices and using the provided code examples, you'll be well-equipped to deploy and maintain production-ready AI applications that can handle real-world traffic and requirements.

Remember that production deployment is an iterative process. Start with a minimal viable deployment and gradually add complexity as your requirements grow and you gain operational experience.


This guide provides a comprehensive foundation for deploying AI applications to production. For specific use cases or advanced scenarios, consider consulting with DevOps and ML engineering specialists.

Share:
AI DeploymentProductionDevOpsScaling