Spaces:
Sleeping
Sleeping
| """ | |
| 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__) | |
| class InferenceRequest: | |
| """Standardized inference request.""" | |
| request_id: str | |
| model_name: str | |
| inputs: Dict[str, Any] | |
| timestamp: float = field(default_factory=time.time) | |
| 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) | |
| 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 | |