""" Bharat Tech Atlas — Model Serving Layer Production-grade model serving with batching, caching, and health monitoring. Supports: - Direct Python inference (development) - ONNX Runtime (optimized CPU inference) - TorchServe integration (scalable GPU serving) - NVIDIA Triton integration (multi-model, multi-framework) Architecture: Request → Rate Limiter → Model Router → Inference Engine → Response Cache → Response """ import logging import time import asyncio from typing import Dict, List, Optional, Any from dataclasses import dataclass, field from collections import OrderedDict from datetime import datetime logger = logging.getLogger(__name__) @dataclass class InferenceRequest: """Standardized inference request.""" request_id: str model_name: str inputs: Dict[str, Any] timestamp: float = field(default_factory=time.time) @dataclass class InferenceResponse: """Standardized inference response.""" request_id: str model_name: str outputs: Dict[str, Any] latency_ms: float cached: bool = False class LRUCache: """Simple LRU cache for inference results.""" def __init__(self, max_size: int = 1000): self.cache = OrderedDict() self.max_size = max_size self.hits = 0 self.misses = 0 def get(self, key: str) -> Optional[Any]: if key in self.cache: self.cache.move_to_end(key) self.hits += 1 return self.cache[key] self.misses += 1 return None def put(self, key: str, value: Any): if key in self.cache: self.cache.move_to_end(key) self.cache[key] = value if len(self.cache) > self.max_size: self.cache.popitem(last=False) @property def hit_rate(self) -> float: total = self.hits + self.misses return self.hits / total if total > 0 else 0.0 class ModelServer: """ Production model serving infrastructure. Handles: - Model lifecycle (load, warm-up, inference, unload) - Request batching for GPU throughput - Response caching (LRU with TTL) - Health monitoring and metrics - Graceful degradation on model failure Production deployment options: 1. Standalone FastAPI (current, good for <100 RPS) 2. TorchServe (PyTorch models, auto-scaling, GPU batching) 3. NVIDIA Triton (multi-model, dynamic batching, ensemble) 4. HuggingFace Inference Endpoints (managed, zero-config) """ def __init__(self, config: Optional[Dict] = None): self.config = config or {} self._models: Dict[str, Any] = {} self._cache = LRUCache(max_size=self.config.get("cache_size", 1000)) self._metrics = { "total_requests": 0, "total_errors": 0, "avg_latency_ms": 0.0, "models_loaded": 0, } self._request_queue: asyncio.Queue = asyncio.Queue() self._batch_size = self.config.get("batch_size", 8) self._max_wait_ms = self.config.get("max_batch_wait_ms", 50) async def initialize(self): """Initialize model server — load all configured models.""" logger.info("Initializing Model Server...") # Load sector classifier from .classifier import StartupSectorClassifier classifier = StartupSectorClassifier( model_name=self.config.get("classifier_model", "facebook/bart-large-mnli"), use_onnx=self.config.get("use_onnx", False), ) classifier.load_model() self._models["sector_classifier"] = classifier # Load growth predictor from .predictor import GrowthPredictor predictor = GrowthPredictor( model_path=self.config.get("predictor_model_path") ) predictor.load_model() self._models["growth_predictor"] = predictor self._metrics["models_loaded"] = len(self._models) logger.info(f"Model Server ready: {len(self._models)} models loaded") async def predict(self, request: InferenceRequest) -> InferenceResponse: """ Handle a single inference request. Checks cache first, then routes to appropriate model. """ self._metrics["total_requests"] += 1 start_time = time.time() # Check cache cache_key = f"{request.model_name}:{hash(str(request.inputs))}" cached_result = self._cache.get(cache_key) if cached_result: return InferenceResponse( request_id=request.request_id, model_name=request.model_name, outputs=cached_result, latency_ms=round((time.time() - start_time) * 1000, 2), cached=True, ) # Route to model try: model = self._models.get(request.model_name) if not model: raise ValueError(f"Model not found: {request.model_name}") outputs = self._run_inference(model, request) # Cache result self._cache.put(cache_key, outputs) latency_ms = round((time.time() - start_time) * 1000, 2) self._update_latency(latency_ms) return InferenceResponse( request_id=request.request_id, model_name=request.model_name, outputs=outputs, latency_ms=latency_ms, ) except Exception as e: self._metrics["total_errors"] += 1 logger.error(f"Inference failed for {request.model_name}: {e}") return InferenceResponse( request_id=request.request_id, model_name=request.model_name, outputs={"error": str(e)}, latency_ms=round((time.time() - start_time) * 1000, 2), ) def _run_inference(self, model: Any, request: InferenceRequest) -> Dict: """Execute inference on the model.""" if request.model_name == "sector_classifier": description = request.inputs.get("description", "") result = model.classify(description) return { "sector": result.sector, "confidence": result.confidence, "top_sectors": result.top_sectors, "model_version": result.model_version, } elif request.model_name == "growth_predictor": entity = request.inputs.get("entity", {}) result = model.predict(entity) return { "growth_score": result.growth_score, "growth_label": result.growth_label, "factors": result.factors, "confidence": result.confidence, } else: raise ValueError(f"Unknown model: {request.model_name}") def _update_latency(self, new_latency: float): """Update rolling average latency.""" total = self._metrics["total_requests"] current_avg = self._metrics["avg_latency_ms"] self._metrics["avg_latency_ms"] = round( (current_avg * (total - 1) + new_latency) / total, 2 ) def get_health(self) -> Dict: """Get model server health status.""" return { "status": "healthy" if self._models else "degraded", "models_loaded": list(self._models.keys()), "metrics": self._metrics, "cache_hit_rate": round(self._cache.hit_rate, 3), "timestamp": datetime.utcnow().isoformat(), } async def shutdown(self): """Graceful shutdown — flush caches, unload models.""" logger.info("Shutting down Model Server...") self._models.clear() logger.info("Model Server shut down") class TorchServeAdapter: """ Adapter for deploying models via TorchServe. TorchServe provides: - Dynamic batching (groups requests for GPU efficiency) - Model versioning (A/B testing) - Auto-scaling (workers scale with load) - RESTful management API Deployment: torch-model-archiver --model-name sector_classifier \\ --version 1.0 \\ --model-file model.py \\ --serialized-file model.pt \\ --handler handler.py torchserve --start --model-store model_store \\ --models sector_classifier=sector_classifier.mar """ def __init__(self, endpoint: str = "http://localhost:8080"): self.endpoint = endpoint async def predict(self, model_name: str, data: Dict) -> Dict: """Send prediction request to TorchServe.""" # Production: # async with aiohttp.ClientSession() as session: # url = f"{self.endpoint}/predictions/{model_name}" # async with session.post(url, json=data) as resp: # return await resp.json() logger.info(f"TorchServe prediction: {model_name}") return {} async def get_models(self) -> List[Dict]: """List registered models on TorchServe.""" # GET {endpoint}/models return [] class TritonAdapter: """ Adapter for NVIDIA Triton Inference Server. Triton provides: - Multi-framework support (PyTorch, TensorFlow, ONNX, TensorRT) - Dynamic batching across multiple models - Model ensemble pipelines - GPU memory management - Prometheus metrics Config (config.pbtxt): name: "sector_classifier" platform: "onnxruntime_onnx" max_batch_size: 32 input [{ name: "input_ids" data_type: TYPE_INT64 dims: [-1] }] output [{ name: "logits" data_type: TYPE_FP32 dims: [-1] }] dynamic_batching { preferred_batch_size: [8, 16] max_queue_delay_microseconds: 50000 } """ def __init__(self, url: str = "localhost:8001"): self.url = url async def predict(self, model_name: str, inputs: Dict) -> Dict: """Send gRPC inference request to Triton.""" # Production: # import tritonclient.grpc as grpcclient # client = grpcclient.InferenceServerClient(url=self.url) # input_tensor = grpcclient.InferInput("input_ids", shape, "INT64") # input_tensor.set_data_from_numpy(input_data) # result = client.infer(model_name, [input_tensor]) logger.info(f"Triton prediction: {model_name}") return {} async def health_check(self) -> bool: """Check if Triton server is healthy.""" # client.is_server_ready() return True