File size: 10,488 Bytes
b558108
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
"""
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