Chapter 8: Production Deployment

WIA-AI-025 | From Research to Real World 🚀

8.1 From Lab to Production

Deploying RL systems in production requires careful consideration of performance, safety, monitoring, and maintenance. This chapter covers the engineering practices needed to successfully deploy RL agents in real-world systems.

Reality Check:

Production RL is very different from research RL. Real systems need to handle edge cases, maintain safety guarantees, scale efficiently, and adapt to changing environments—all while operating 24/7.

Production Requirements

8.2 Model Serving Architecture

Inference Server

from fastapi import FastAPI
import torch
import uvicorn

app = FastAPI()

class RLModelServer:
    def __init__(self, model_path):
        // Load trained model
        self.model = torch.load(model_path)
        self.model.eval()

        // Move to GPU if available
        self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
        self.model.to(self.device)

    def preprocess(self, observation):
        """Normalize and convert to tensor."""
        obs = torch.FloatTensor(observation).unsqueeze(0)
        return obs.to(self.device)

    def postprocess(self, action_tensor):
        """Convert tensor to action."""
        return action_tensor.cpu().numpy()[0]

    @torch.no_grad()
    def predict(self, observation):
        """Run inference."""
        obs_tensor = self.preprocess(observation)
        action_tensor = self.model(obs_tensor)
        action = self.postprocess(action_tensor)
        return action

// Global model server instance
model_server = RLModelServer(model_path='models/agent.pt')

@app.post("/predict")
async def predict(observation: list):
    """
    Inference endpoint.

    Request: {"observation": [0.1, 0.2, 0.3, ...]}
    Response: {"action": [1.5, -0.3, ...], "latency_ms": 12.5}
    """
    import time
    start = time.time()

    action = model_server.predict(observation)

    latency = (time.time() - start) * 1000

    return {
        "action": action.tolist(),
        "latency_ms": latency
    }

@app.get("/health")
async def health():
    return {"status": "healthy"}

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

TensorFlow Serving

// Export model for TF Serving
import tensorflow as tf

// Save model in SavedModel format
tf.saved_model.save(model, 'serving_model/1')

// Serve using TensorFlow Serving (Docker)
docker run -p 8501:8501 \
  --mount type=bind,source=$(pwd)/serving_model,target=/models/rl_agent \
  -e MODEL_NAME=rl_agent \
  -t tensorflow/serving

// Client code
import requests
import json

def predict(observation):
    url = 'http://localhost:8501/v1/models/rl_agent:predict'

    data = json.dumps({
        "signature_name": "serving_default",
        "instances": [observation.tolist()]
    })

    response = requests.post(url, data=data, headers={"content-type": "application/json"})
    prediction = response.json()['predictions'][0]

    return prediction

8.3 Distributed Training

Data Parallel Training

import torch
import torch.distributed as dist
import torch.multiprocessing as mp
from torch.nn.parallel import DistributedDataParallel as DDP

def setup(rank, world_size):
    os.environ['MASTER_ADDR'] = 'localhost'
    os.environ['MASTER_PORT'] = '12355'
    dist.init_process_group("nccl", rank=rank, world_size=world_size)

def cleanup():
    dist.destroy_process_group()

def train_worker(rank, world_size):
    setup(rank, world_size)

    // Create model and move to GPU
    model = PolicyNetwork().to(rank)
    ddp_model = DDP(model, device_ids=[rank])

    // Create environment
    env = gym.make('Humanoid-v2')

    // Training loop
    for episode in range(num_episodes):
        state = env.reset()
        done = False

        while not done:
            action = ddp_model(torch.FloatTensor(state).to(rank))
            next_state, reward, done, _ = env.step(action.cpu().numpy())

            // Compute loss
            loss = compute_loss(...)

            // Backward pass (gradients averaged across GPUs)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            state = next_state

    cleanup()

def main():
    world_size = torch.cuda.device_count()
    mp.spawn(train_worker, args=(world_size,), nprocs=world_size, join=True)

if __name__ == "__main__":
    main()

Ray for Distributed RL

import ray
from ray.rllib.agents.ppo import PPOTrainer

ray.init()

// Configuration
config = {
    "env": "CartPole-v1",
    "num_workers": 16,           // Parallel environment workers
    "num_gpus": 4,               // GPUs for training
    "train_batch_size": 4000,
    "sgd_minibatch_size": 128,
    "num_sgd_iter": 10,
    "lr": 3e-4,
    "framework": "torch"
}

// Create trainer
trainer = PPOTrainer(config=config)

// Training
for iteration in range(1000):
    result = trainer.train()

    print(f"Iteration {iteration}")
    print(f"  Reward: {result['episode_reward_mean']:.2f}")
    print(f"  Length: {result['episode_len_mean']:.2f}")

    // Save checkpoint
    if iteration % 100 == 0:
        checkpoint = trainer.save()
        print(f"  Checkpoint saved: {checkpoint}")

// Export for deployment
trainer.export_policy_model("./exported_models")

8.4 Safety and Constraints

Safe Exploration

class SafetyLayer:
    """
    Safety wrapper that prevents unsafe actions.
    """
    def __init__(self, env, safety_constraints):
        self.env = env
        self.constraints = safety_constraints

    def is_safe(self, state, action):
        """Check if action satisfies all safety constraints."""
        for constraint in self.constraints:
            if not constraint.check(state, action):
                return False
        return True

    def get_safe_action(self, state, proposed_action):
        """
        Project unsafe action to nearest safe action.
        """
        if self.is_safe(state, proposed_action):
            return proposed_action

        // Find nearest safe action (optimization problem)
        safe_action = self.project_to_safe_set(state, proposed_action)
        return safe_action

// Usage
safe_env = SafetyLayer(env, safety_constraints=[
    MaxVelocityConstraint(max_speed=5.0),
    WorkspaceConstraint(bounds=workspace_bounds),
    CollisionConstraint(obstacles=obstacles)
])

action = policy(state)
safe_action = safe_env.get_safe_action(state, action)
next_state, reward, done = env.step(safe_action)

Constrained RL

class CPO:  // Constrained Policy Optimization
    """
    Optimize policy while satisfying safety constraints:

    maximize E[reward]
    subject to E[cost] ≤ threshold
    """
    def __init__(self, cost_limit=25):
        self.policy = PolicyNetwork()
        self.value = ValueNetwork()
        self.cost_value = ValueNetwork()  // Estimate cost
        self.cost_limit = cost_limit

    def update(self, trajectories):
        // Compute advantage and cost advantage
        advantages = self.compute_advantages(trajectories)
        cost_advantages = self.compute_cost_advantages(trajectories)

        // Current policy performance
        current_reward = self.evaluate_reward()
        current_cost = self.evaluate_cost()

        // Constrained optimization
        // Use Lagrangian: L = reward - λ * (cost - limit)
        policy_loss = -(advantages - λ * cost_advantages).mean()

        // Update policy
        optimizer.zero_grad()
        policy_loss.backward()

        // Project gradient to satisfy constraints
        self.project_gradients(current_cost, self.cost_limit)

        optimizer.step()

        // Update λ (Lagrange multiplier)
        if current_cost > self.cost_limit:
            λ *= 1.05  // Increase penalty
        else:
            λ *= 0.95  // Decrease penalty

8.5 Monitoring and Observability

Metrics Collection

from prometheus_client import Counter, Histogram, Gauge
import time

// Define metrics
inference_counter = Counter('rl_inference_total', 'Total inference requests')
inference_latency = Histogram('rl_inference_latency_seconds', 'Inference latency')
action_distribution = Histogram('rl_action_values', 'Distribution of action values')
reward_gauge = Gauge('rl_episode_reward', 'Episode reward')

class MonitoredAgent:
    def __init__(self, policy):
        self.policy = policy

    @inference_latency.time()
    def predict(self, observation):
        inference_counter.inc()

        action = self.policy(observation)

        // Track action distribution
        action_distribution.observe(action.mean())

        return action

    def on_episode_end(self, total_reward):
        reward_gauge.set(total_reward)

// Logging
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class LoggingAgent:
    def predict(self, observation):
        logger.info(f"Observation: {observation}")

        action = self.policy(observation)

        logger.info(f"Action: {action}")

        return action

Anomaly Detection

class AnomalyDetector:
    """
    Detect distribution shift and anomalous behavior.
    """
    def __init__(self):
        self.observation_stats = {
            'mean': None,
            'std': None,
            'min': None,
            'max': None
        }

    def fit(self, observations):
        """Learn normal observation distribution."""
        import numpy as np

        self.observation_stats['mean'] = np.mean(observations, axis=0)
        self.observation_stats['std'] = np.std(observations, axis=0)
        self.observation_stats['min'] = np.min(observations, axis=0)
        self.observation_stats['max'] = np.max(observations, axis=0)

    def detect_anomaly(self, observation):
        """Check if observation is anomalous."""
        import numpy as np

        // Z-score based detection
        z_score = np.abs(
            (observation - self.observation_stats['mean']) /
            (self.observation_stats['std'] + 1e-8)
        )

        is_anomaly = np.any(z_score > 3.0)  // 3-sigma rule

        if is_anomaly:
            logger.warning(f"Anomalous observation detected: {observation}")
            // Trigger alert, use fallback policy, etc.

        return is_anomaly

8.6 Online Learning and Adaptation

Continual Learning

class ContinualLearner:
    """
    Continue learning from production data.
    """
    def __init__(self, policy, update_frequency=1000):
        self.policy = policy
        self.buffer = ReplayBuffer(capacity=100000)
        self.update_frequency = update_frequency
        self.step_count = 0

    def act(self, observation):
        action = self.policy(observation)
        return action

    def observe(self, observation, action, reward, next_observation):
        // Store experience
        self.buffer.add(observation, action, reward, next_observation)

        self.step_count += 1

        // Periodic updates
        if self.step_count % self.update_frequency == 0:
            self.update_policy()

    def update_policy(self):
        """Update policy from recent experience."""
        if len(self.buffer) < batch_size:
            return

        batch = self.buffer.sample(batch_size)

        // Compute loss
        loss = compute_td_loss(self.policy, batch)

        // Update with small learning rate (stability)
        optimizer = torch.optim.Adam(self.policy.parameters(), lr=1e-5)
        optimizer.zero_grad()
        loss.backward()
        torch.nn.utils.clip_grad_norm_(self.policy.parameters(), 0.5)
        optimizer.step()

        logger.info(f"Policy updated. Loss: {loss.item():.4f}")

A/B Testing

class ABTestingFramework:
    """
    Compare multiple policy versions in production.
    """
    def __init__(self, policies, traffic_split=None):
        self.policies = policies  // {'baseline': policy_A, 'treatment': policy_B}

        if traffic_split is None:
            traffic_split = {k: 1.0/len(policies) for k in policies}

        self.traffic_split = traffic_split
        self.metrics = {k: [] for k in policies}

    def select_policy(self, user_id):
        """Route traffic to policy variants."""
        import random

        // Deterministic routing based on user_id
        hash_val = hash(user_id) % 100

        cumulative = 0
        for policy_name, percentage in self.traffic_split.items():
            cumulative += percentage * 100
            if hash_val < cumulative:
                return policy_name, self.policies[policy_name]

        return list(self.policies.items())[0]

    def record_metric(self, policy_name, reward):
        self.metrics[policy_name].append(reward)

    def analyze_results(self):
        """Statistical analysis of A/B test."""
        import numpy as np
        from scipy import stats

        baseline_rewards = self.metrics['baseline']
        treatment_rewards = self.metrics['treatment']

        // T-test
        t_stat, p_value = stats.ttest_ind(treatment_rewards, baseline_rewards)

        print(f"Baseline mean reward: {np.mean(baseline_rewards):.2f}")
        print(f"Treatment mean reward: {np.mean(treatment_rewards):.2f}")
        print(f"P-value: {p_value:.4f}")

        if p_value < 0.05:
            if np.mean(treatment_rewards) > np.mean(baseline_rewards):
                print("Treatment is significantly better!")
            else:
                print("Baseline is significantly better!")
        else:
            print("No significant difference")

8.7 Model Compression and Optimization

Quantization

import torch

// Post-training quantization
model = torch.load('policy.pt')
model.eval()

// Dynamic quantization (weights + activations)
quantized_model = torch.quantization.quantize_dynamic(
    model, {torch.nn.Linear}, dtype=torch.qint8
)

// Save quantized model
torch.save(quantized_model, 'policy_quantized.pt')

// Model size comparison
import os
original_size = os.path.getsize('policy.pt') / 1024  // KB
quantized_size = os.path.getsize('policy_quantized.pt') / 1024

print(f"Original: {original_size:.2f} KB")
print(f"Quantized: {quantized_size:.2f} KB")
print(f"Compression: {original_size/quantized_size:.2f}x")

Knowledge Distillation

class PolicyDistillation:
    """
    Train smaller student policy to mimic large teacher.
    """
    def __init__(self, teacher_policy, student_policy):
        self.teacher = teacher_policy
        self.student = student_policy

        self.teacher.eval()  // Freeze teacher

    def distill(self, states, temperature=2.0):
        """
        Train student to match teacher's action distribution.
        """
        with torch.no_grad():
            // Soft teacher predictions
            teacher_logits = self.teacher(states) / temperature
            teacher_probs = F.softmax(teacher_logits, dim=-1)

        // Student predictions
        student_logits = self.student(states) / temperature
        student_log_probs = F.log_softmax(student_logits, dim=-1)

        // KL divergence loss
        loss = F.kl_div(student_log_probs, teacher_probs, reduction='batchmean')

        return loss

// Training loop
teacher = LargePolicy()  // 10M parameters
student = SmallPolicy()  // 1M parameters

optimizer = torch.optim.Adam(student.parameters(), lr=1e-3)

for epoch in range(100):
    states = sample_states()

    loss = distillation.distill(states)

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

print(f"Student trained! Parameters: {count_parameters(student)}")

8.8 Deployment Checklist

Pre-Deployment Checklist:
  1. ✓ Comprehensive testing in simulation
  2. ✓ Safety constraints implemented and verified
  3. ✓ Fallback policy for anomalies
  4. ✓ Monitoring and alerting configured
  5. ✓ Latency requirements met
  6. ✓ A/B testing framework ready
  7. ✓ Rollback plan prepared
  8. ✓ Documentation complete
  9. ✓ Team trained on operations
  10. ✓ Gradual rollout plan (canary deployment)

Deployment Stages

Stage Traffic % Duration Criteria
Canary 1% 24 hours No critical errors
Limited 10% 3 days Performance ≥ baseline
Extended 50% 1 week Statistically significant improvement
Full 100% Ongoing Continued monitoring

8.9 Case Studies

Google Data Center Cooling

DeepMind's RL system reduced cooling energy by 40%:

Recommendation Systems

class RecommenderRL:
    """
    RL for personalized recommendations.
    """
    def __init__(self):
        self.policy = SlateGenerationPolicy()

    def recommend(self, user_id, context):
        """
        Generate slate of recommendations.

        State: user history, context, time
        Action: slate of items to show
        Reward: clicks, watch time, satisfaction
        """
        state = self.get_state(user_id, context)

        // Generate candidate items
        candidates = self.candidate_generation(user_id)

        // Rank and select top-k
        slate = self.policy(state, candidates)

        return slate

    def update(self, user_id, slate, feedback):
        """Learn from user feedback."""
        reward = self.compute_reward(feedback)

        self.buffer.add(state, slate, reward)

        if len(self.buffer) > batch_size:
            self.train_step()

8.10 Future Directions

Emerging Trends

The Future is Bright:

RL is moving from research labs to production systems at an accelerating pace. With better tools, frameworks, and best practices, deploying RL in real-world applications is becoming increasingly accessible.

Summary

  • Production RL requires careful consideration of safety, latency, and reliability
  • Model serving frameworks enable scalable inference
  • Distributed training accelerates learning on large-scale problems
  • Safety layers and constrained RL ensure bounded behavior
  • Monitoring and observability are critical for detecting issues
  • Online learning enables adaptation to changing environments
  • A/B testing validates improvements in production
  • Model compression reduces deployment costs
  • Gradual rollout minimizes risks
  • Real-world case studies demonstrate RL's practical impact

Review Questions

1. Why is safety particularly important in production RL systems?

Answer: RL agents can take unexpected actions during exploration or in novel situations. Without safety constraints, this could cause damage, injuries, or system failures in real-world deployments.

2. What is the purpose of A/B testing in RL deployment?

Answer: To compare a new policy against the baseline policy in production with real users, providing statistical evidence of improvement before full rollout.

3. How does knowledge distillation help with deployment?

Answer: It trains a smaller student model to mimic a large teacher model, reducing computational requirements and latency while maintaining performance.

4. What is the benefit of continual learning in production?

Answer: Allows the agent to adapt to changing environments and improve from real production data, maintaining performance as conditions evolve.

🎉 Congratulations!

You've completed the WIA-AI-025 Reinforcement Learning course!

You now have the knowledge to:

  • Understand RL fundamentals and MDPs
  • Implement value-based and policy gradient methods
  • Apply actor-critic algorithms
  • Use model-based RL for planning
  • Coordinate multi-agent systems
  • Deploy RL systems in production

🎮 Continue practicing in the simulator