1use crate::model_zoo::{ModelMetadata, ModelZooEntry};
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use std::fs;
16use std::path::{Path, PathBuf};
17use torsh_core::error::{Result, TorshError};
18
19const GRAPH_EXECUTOR_PY: &str = r#"
27def _predecessors(node_id):
28 """Return the ids feeding a node, in edge declaration order."""
29 return [src for src, dst in GRAPH_EDGES if dst == node_id]
30
31
32def _topological_order():
33 """Return graph node ids in dependency order."""
34 indegree = {node_id: 0 for node_id in GRAPH_NODES}
35 for _, dst in GRAPH_EDGES:
36 if dst in indegree:
37 indegree[dst] += 1
38 ready = [node_id for node_id, degree in indegree.items() if degree == 0]
39 order = []
40 while ready:
41 current = ready.pop()
42 order.append(current)
43 for src, dst in GRAPH_EDGES:
44 if src == current and dst in indegree:
45 indegree[dst] -= 1
46 if indegree[dst] == 0:
47 ready.append(dst)
48 if len(order) != len(GRAPH_NODES):
49 raise ValueError('exported graph contains a cycle and cannot be executed')
50 return order
51
52
53def _apply_op(op_name, operands):
54 """Evaluate a single graph operation with numpy."""
55 if op_name in ('relu', 'relu_inplace'):
56 return np.maximum(operands[0], 0.0)
57 if op_name in ('sigmoid', 'sigmoid_inplace'):
58 return 1.0 / (1.0 + np.exp(-operands[0]))
59 if op_name in ('tanh', 'tanh_inplace'):
60 return np.tanh(operands[0])
61 if op_name == 'gelu':
62 x = operands[0]
63 inner = np.sqrt(2.0 / np.pi) * (x + 0.044715 * np.power(x, 3))
64 return 0.5 * x * (1.0 + np.tanh(inner))
65 if op_name == 'softmax':
66 x = operands[0]
67 shifted = x - np.max(x, axis=-1, keepdims=True)
68 exps = np.exp(shifted)
69 return exps / np.sum(exps, axis=-1, keepdims=True)
70 if op_name == 'exp':
71 return np.exp(operands[0])
72 if op_name == 'log':
73 return np.log(operands[0])
74 if op_name == 'sqrt':
75 return np.sqrt(operands[0])
76 if op_name == 'neg':
77 return -operands[0]
78 if op_name == 'abs':
79 return np.abs(operands[0])
80 if op_name in ('add', 'add_inplace'):
81 return operands[0] + operands[1]
82 if op_name == 'sub':
83 return operands[0] - operands[1]
84 if op_name in ('mul', 'mul_inplace'):
85 return operands[0] * operands[1]
86 if op_name == 'div':
87 return operands[0] / operands[1]
88 if op_name == 'matmul':
89 return np.matmul(operands[0], operands[1])
90 if op_name == 'identity':
91 return operands[0]
92 if op_name == 'constant_zero':
93 return np.zeros(1, dtype=np.float32)
94 if op_name == 'constant_one':
95 return np.ones(1, dtype=np.float32)
96 raise NotImplementedError(
97 "operation '" + op_name + "' cannot be executed from the exported graph: it "
98 "requires model parameters that are not part of model.json. Supply a runtime "
99 "that provides the weights for this operation, or export a graph that only "
100 "uses parameter-free operations."
101 )
102
103
104def run_graph(input_arrays):
105 """Execute the exported graph and return its outputs as nested lists."""
106 values = {}
107 graph_inputs = GRAPH['inputs']
108 if len(input_arrays) != len(graph_inputs):
109 raise ValueError(
110 'expected %d input(s), received %d' % (len(graph_inputs), len(input_arrays))
111 )
112 for node_id, array in zip(graph_inputs, input_arrays):
113 values[node_id] = array
114
115 for node_id in _topological_order():
116 if node_id in values:
117 continue
118 node = GRAPH_NODES.get(node_id)
119 if node is None:
120 raise ValueError('graph references unknown node %r' % (node_id,))
121 node_type = node['node_type']
122 if node_type.startswith('input:'):
123 raise ValueError('no value supplied for graph input %r' % (node_id,))
124 preds = _predecessors(node_id)
125 if node_type == 'output':
126 if preds:
127 values[node_id] = values[preds[0]]
128 continue
129 if node_type.startswith('call:'):
130 op_name = node_type.split(':', 1)[1]
131 if op_name == 'constant':
132 params = node.get('params', {})
133 values[node_id] = np.asarray(
134 [float(params.get('value', 0.0))], dtype=np.float32
135 )
136 continue
137 values[node_id] = _apply_op(op_name, [values[p] for p in preds])
138 continue
139 raise NotImplementedError(
140 "graph node type '" + node_type + "' is not supported by the generated server"
141 )
142
143 return [
144 np.asarray(values[node_id]).tolist()
145 for node_id in GRAPH['outputs']
146 if node_id in values
147 ]
148
149
150"#;
151
152const PREDICT_ENDPOINT_PY: &str = r#"@app.route('/predict', methods=['POST'])
157def predict():
158 global request_count, total_inference_time, error_count
159 try:
160 start_time = time.time()
161
162 data = request.get_json()
163 if not data or 'inputs' not in data:
164 return jsonify({'error': 'Missing inputs'}), 400
165
166 inputs = data.get('inputs')
167
168 # Convert the request payload into the graph's input arrays
169 if len(GRAPH['inputs']) == 1:
170 input_arrays = [np.array(inputs, dtype=np.float32)]
171 else:
172 input_arrays = [np.array(item, dtype=np.float32) for item in inputs]
173
174 # Run the exported graph
175 outputs = run_graph(input_arrays)
176
177 inference_time = time.time() - start_time
178 request_count += 1
179 total_inference_time += inference_time
180
181 return jsonify({
182 'outputs': outputs,
183 'inference_time_ms': inference_time * 1000,
184 'request_id': request_count
185 })
186 except NotImplementedError as exc:
187 error_count += 1
188 logger.error(f'Model cannot be executed: {exc}')
189 return jsonify({'error': str(exc)}), 501
190 except Exception as e:
191 error_count += 1
192 logger.error(f'Prediction error: {e}')
193 return jsonify({'error': str(e)}), 500
194
195"#;
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
199pub enum CloudPlatform {
200 AWS { region: String, service: AWSService },
202 GCP {
204 project_id: String,
205 region: String,
206 service: GCPService,
207 },
208 Azure {
210 subscription_id: String,
211 resource_group: String,
212 region: String,
213 service: AzureService,
214 },
215 Custom { name: String, endpoint: String },
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
221pub enum AWSService {
222 SageMaker {
224 instance_type: String,
225 endpoint_name: String,
226 },
227 Lambda {
229 runtime: String,
230 memory_mb: usize,
231 timeout_seconds: usize,
232 },
233 ECS {
235 cluster_name: String,
236 task_definition: String,
237 },
238 EKS {
240 cluster_name: String,
241 namespace: String,
242 },
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
247pub enum GCPService {
248 VertexAI {
250 model_name: String,
251 machine_type: String,
252 },
253 CloudRun {
255 service_name: String,
256 memory_mb: usize,
257 max_instances: usize,
258 },
259 GKE {
261 cluster_name: String,
262 namespace: String,
263 },
264 CloudFunctions {
266 function_name: String,
267 runtime: String,
268 memory_mb: usize,
269 },
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize)]
274pub enum AzureService {
275 AzureML {
277 workspace_name: String,
278 endpoint_name: String,
279 },
280 AzureFunctions {
282 function_app_name: String,
283 runtime: String,
284 },
285 AKS {
287 cluster_name: String,
288 namespace: String,
289 },
290 ACI {
292 container_group_name: String,
293 cpu_cores: f32,
294 memory_gb: f32,
295 },
296}
297
298#[derive(Debug, Clone, Serialize, Deserialize)]
300pub struct DeploymentConfig {
301 pub name: String,
303 pub platform: CloudPlatform,
305 pub container: ContainerConfig,
307 pub resources: ResourceRequirements,
309 pub autoscaling: Option<AutoScalingConfig>,
311 pub environment_variables: HashMap<String, String>,
313 pub health_check: HealthCheckConfig,
315 pub monitoring: MonitoringConfig,
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct ContainerConfig {
322 pub base_image: String,
324 pub python_version: String,
326 pub system_packages: Vec<String>,
328 pub python_packages: Vec<String>,
330 pub entrypoint: Vec<String>,
332 pub port: u16,
334}
335
336#[derive(Debug, Clone, Serialize, Deserialize)]
338pub struct ResourceRequirements {
339 pub cpu_cores: f32,
341 pub memory_gb: f32,
343 pub gpu_count: u32,
345 pub gpu_type: Option<String>,
347 pub storage_gb: u32,
349}
350
351#[derive(Debug, Clone, Serialize, Deserialize)]
353pub struct AutoScalingConfig {
354 pub min_instances: u32,
356 pub max_instances: u32,
358 pub target_cpu_utilization: f32,
360 pub target_memory_utilization: f32,
362 pub scale_up_cooldown: u32,
364 pub scale_down_cooldown: u32,
366}
367
368#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct HealthCheckConfig {
371 pub path: String,
373 pub interval_seconds: u32,
375 pub timeout_seconds: u32,
377 pub healthy_threshold: u32,
379 pub unhealthy_threshold: u32,
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize)]
385pub struct MonitoringConfig {
386 pub enable_metrics: bool,
388 pub enable_logging: bool,
390 pub enable_tracing: bool,
392 pub metrics_endpoint: Option<String>,
394 pub log_level: String,
396 pub custom_metrics: Vec<String>,
398}
399
400pub struct CloudDeploymentPackager {
402 output_dir: PathBuf,
404 config: DeploymentConfig,
406}
407
408impl CloudDeploymentPackager {
409 pub fn new<P: AsRef<Path>>(output_dir: P, config: DeploymentConfig) -> Result<Self> {
411 let output_dir = output_dir.as_ref().to_path_buf();
412
413 fs::create_dir_all(&output_dir).map_err(|e| TorshError::IoError(e.to_string()))?;
415
416 Ok(Self { output_dir, config })
417 }
418
419 pub fn package_model(&self, entry: &ModelZooEntry) -> Result<DeploymentPackage> {
421 let deployment_dir = self.output_dir.join(&self.config.name);
423 fs::create_dir_all(&deployment_dir).map_err(|e| TorshError::IoError(e.to_string()))?;
424
425 let dockerfile = self.generate_dockerfile(entry)?;
427 fs::write(deployment_dir.join("Dockerfile"), dockerfile)
428 .map_err(|e| TorshError::IoError(e.to_string()))?;
429
430 let server_code = self.generate_inference_server(entry)?;
432 fs::write(deployment_dir.join("server.py"), server_code)
433 .map_err(|e| TorshError::IoError(e.to_string()))?;
434
435 let requirements = self.generate_requirements()?;
437 fs::write(deployment_dir.join("requirements.txt"), requirements)
438 .map_err(|e| TorshError::IoError(e.to_string()))?;
439
440 let platform_config = self.generate_platform_config(entry)?;
442 fs::write(
443 deployment_dir.join("platform_config.json"),
444 serde_json::to_string_pretty(&platform_config)
445 .map_err(|e| TorshError::SerializationError(e.to_string()))?,
446 )
447 .map_err(|e| TorshError::IoError(e.to_string()))?;
448
449 let deploy_script = self.generate_deployment_script()?;
451 fs::write(deployment_dir.join("deploy.sh"), deploy_script)
452 .map_err(|e| TorshError::IoError(e.to_string()))?;
453
454 entry.save_to_file(deployment_dir.join("model.json"))?;
456
457 Ok(DeploymentPackage {
458 path: deployment_dir,
459 config: self.config.clone(),
460 metadata: entry.metadata.clone(),
461 })
462 }
463
464 fn generate_dockerfile(&self, _entry: &ModelZooEntry) -> Result<String> {
466 let mut dockerfile = String::new();
467
468 dockerfile.push_str(&format!("FROM {}\n\n", self.config.container.base_image));
469
470 dockerfile.push_str("WORKDIR /app\n\n");
471
472 if !self.config.container.system_packages.is_empty() {
474 dockerfile.push_str("RUN apt-get update && apt-get install -y \\\n");
475 for pkg in &self.config.container.system_packages {
476 dockerfile.push_str(&format!(" {} \\\n", pkg));
477 }
478 dockerfile.push_str(" && rm -rf /var/lib/apt/lists/*\n\n");
479 }
480
481 dockerfile.push_str("COPY requirements.txt .\n");
483 dockerfile.push_str("RUN pip install --no-cache-dir -r requirements.txt\n\n");
484 dockerfile.push_str("COPY . .\n\n");
485
486 dockerfile.push_str(&format!("EXPOSE {}\n\n", self.config.container.port));
488
489 dockerfile.push_str(&format!(
491 "HEALTHCHECK --interval={}s --timeout={}s --start-period=30s --retries={} \\\n",
492 self.config.health_check.interval_seconds,
493 self.config.health_check.timeout_seconds,
494 self.config.health_check.healthy_threshold
495 ));
496 dockerfile.push_str(&format!(
497 " CMD curl -f http://localhost:{}{} || exit 1\n\n",
498 self.config.container.port, self.config.health_check.path
499 ));
500
501 dockerfile.push_str("CMD ");
503 dockerfile.push_str(
504 &serde_json::to_string(&self.config.container.entrypoint)
505 .map_err(|e| TorshError::SerializationError(e.to_string()))?,
506 );
507 dockerfile.push('\n');
508
509 Ok(dockerfile)
510 }
511
512 fn generate_inference_server(&self, entry: &ModelZooEntry) -> Result<String> {
514 let mut server = String::new();
515
516 server.push_str("#!/usr/bin/env python3\n");
517 server.push_str("\"\"\"Inference server for ToRSh FX model deployment.\"\"\"\n\n");
518
519 server.push_str("import json\n");
520 server.push_str("import logging\n");
521 server.push_str("import os\n");
522 server.push_str("import time\n");
523 server.push_str("from typing import Any, Dict, List\n\n");
524 server.push_str("from flask import Flask, request, jsonify\n");
525 server.push_str("import numpy as np\n\n");
526
527 server.push_str(&format!(
529 "logging.basicConfig(level=logging.{})\n",
530 self.config.monitoring.log_level.to_uppercase()
531 ));
532 server.push_str("logger = logging.getLogger(__name__)\n\n");
533
534 server.push_str("app = Flask(__name__)\n\n");
536
537 server.push_str("# Global metrics\n");
539 server.push_str("request_count = 0\n");
540 server.push_str("total_inference_time = 0.0\n");
541 server.push_str("error_count = 0\n");
542 server.push_str("start_timestamp = time.time()\n\n");
543
544 server.push_str("# Load model\n");
546 server.push_str("logger.info('Loading model...')\n");
547 server.push_str("with open('model.json', 'r') as f:\n");
548 server.push_str(" model_data = json.load(f)\n");
549 server.push_str("GRAPH = model_data['graph']\n");
550 server.push_str("GRAPH_NODES = {node['id']: node for node in GRAPH['nodes']}\n");
551 server.push_str("GRAPH_EDGES = [tuple(edge) for edge in GRAPH['edges']]\n");
552 server.push_str(&format!(
553 "logger.info('Loaded model: {}')\n\n",
554 entry.metadata.name
555 ));
556
557 server.push_str(GRAPH_EXECUTOR_PY);
561
562 server.push_str("@app.route('/health', methods=['GET'])\n");
564 server.push_str("def health_check():\n");
565 server.push_str(" return jsonify({'status': 'healthy'})\n\n");
566
567 server.push_str(PREDICT_ENDPOINT_PY);
569
570 if self.config.monitoring.enable_metrics {
572 server.push_str("@app.route('/metrics', methods=['GET'])\n");
573 server.push_str("def metrics():\n");
574 server.push_str(" \"\"\"Return comprehensive server and model metrics.\"\"\"\n");
575 server.push_str(
576 " global request_count, total_inference_time, error_count, start_timestamp\n",
577 );
578 server.push_str(" \n");
579 server.push_str(" uptime = time.time() - start_timestamp\n");
580 server.push_str(" avg_inference_time = (total_inference_time / request_count) if request_count > 0 else 0\n");
581 server.push_str(" \n");
582 server.push_str(" metrics_data = {\n");
583 server.push_str(" 'server': {\n");
584 server.push_str(" 'uptime_seconds': uptime,\n");
585 server.push_str(" 'uptime_hours': uptime / 3600,\n");
586 server.push_str(" 'start_time': start_timestamp\n");
587 server.push_str(" },\n");
588 server.push_str(" 'requests': {\n");
589 server.push_str(" 'total': request_count,\n");
590 server.push_str(" 'errors': error_count,\n");
591 server.push_str(" 'success_rate': ((request_count - error_count) / request_count * 100) if request_count > 0 else 100,\n");
592 server.push_str(
593 " 'requests_per_second': request_count / uptime if uptime > 0 else 0\n",
594 );
595 server.push_str(" },\n");
596 server.push_str(" 'inference': {\n");
597 server.push_str(" 'total_time_seconds': total_inference_time,\n");
598 server.push_str(" 'average_time_ms': avg_inference_time * 1000,\n");
599 server.push_str(" 'throughput': request_count / total_inference_time if total_inference_time > 0 else 0\n");
600 server.push_str(" },\n");
601 server.push_str(" 'system': {\n");
602 server.push_str(" 'memory_usage_mb': __import__('psutil').Process().memory_info().rss / 1024 / 1024 if __import__('importlib').util.find_spec('psutil') else 0,\n");
603 server.push_str(" 'cpu_percent': __import__('psutil').Process().cpu_percent() if __import__('importlib').util.find_spec('psutil') else 0\n");
604 server.push_str(" }\n");
605 server.push_str(" }\n");
606 server.push_str(" \n");
607 server.push_str(" return jsonify(metrics_data)\n\n");
608 }
609
610 server.push_str("if __name__ == '__main__':\n");
612 server.push_str(&format!(
613 " app.run(host='0.0.0.0', port={}, debug=False)\n",
614 self.config.container.port
615 ));
616
617 Ok(server)
618 }
619
620 fn generate_requirements(&self) -> Result<String> {
622 let mut requirements = String::new();
623
624 requirements.push_str("# Core dependencies\n");
625 requirements.push_str("flask>=2.0.0\n");
626 requirements.push_str("numpy>=1.20.0\n");
627 requirements.push_str("torch>=2.0.0\n\n");
628
629 requirements.push_str("# Additional packages\n");
630 for pkg in &self.config.container.python_packages {
631 requirements.push_str(&format!("{}\n", pkg));
632 }
633
634 if self.config.monitoring.enable_metrics {
635 requirements.push_str("\n# Monitoring\n");
636 requirements.push_str("prometheus-client>=0.14.0\n");
637 }
638
639 if self.config.monitoring.enable_tracing {
640 requirements.push_str("opentelemetry-api>=1.0.0\n");
641 requirements.push_str("opentelemetry-sdk>=1.0.0\n");
642 }
643
644 Ok(requirements)
645 }
646
647 fn generate_platform_config(&self, _entry: &ModelZooEntry) -> Result<serde_json::Value> {
649 match &self.config.platform {
650 CloudPlatform::AWS { service, .. } => self.generate_aws_config(service),
651 CloudPlatform::GCP { service, .. } => self.generate_gcp_config(service),
652 CloudPlatform::Azure { service, .. } => self.generate_azure_config(service),
653 CloudPlatform::Custom { .. } => Ok(serde_json::json!({"type": "custom"})),
654 }
655 }
656
657 fn generate_aws_config(&self, service: &AWSService) -> Result<serde_json::Value> {
659 match service {
660 AWSService::SageMaker {
661 instance_type,
662 endpoint_name,
663 } => Ok(serde_json::json!({
664 "service": "sagemaker",
665 "instance_type": instance_type,
666 "endpoint_name": endpoint_name,
667 "resources": {
668 "initial_instance_count": 1,
669 }
670 })),
671 AWSService::Lambda {
672 runtime,
673 memory_mb,
674 timeout_seconds,
675 } => Ok(serde_json::json!({
676 "service": "lambda",
677 "runtime": runtime,
678 "memory_mb": memory_mb,
679 "timeout_seconds": timeout_seconds,
680 })),
681 AWSService::ECS {
682 cluster_name,
683 task_definition,
684 } => Ok(serde_json::json!({
685 "service": "ecs",
686 "cluster_name": cluster_name,
687 "task_definition": task_definition,
688 })),
689 AWSService::EKS {
690 cluster_name,
691 namespace,
692 } => Ok(serde_json::json!({
693 "service": "eks",
694 "cluster_name": cluster_name,
695 "namespace": namespace,
696 })),
697 }
698 }
699
700 fn generate_gcp_config(&self, service: &GCPService) -> Result<serde_json::Value> {
702 match service {
703 GCPService::VertexAI {
704 model_name,
705 machine_type,
706 } => Ok(serde_json::json!({
707 "service": "vertex_ai",
708 "model_name": model_name,
709 "machine_type": machine_type,
710 })),
711 GCPService::CloudRun {
712 service_name,
713 memory_mb,
714 max_instances,
715 } => Ok(serde_json::json!({
716 "service": "cloud_run",
717 "service_name": service_name,
718 "memory_mb": memory_mb,
719 "max_instances": max_instances,
720 })),
721 GCPService::GKE {
722 cluster_name,
723 namespace,
724 } => Ok(serde_json::json!({
725 "service": "gke",
726 "cluster_name": cluster_name,
727 "namespace": namespace,
728 })),
729 GCPService::CloudFunctions {
730 function_name,
731 runtime,
732 memory_mb,
733 } => Ok(serde_json::json!({
734 "service": "cloud_functions",
735 "function_name": function_name,
736 "runtime": runtime,
737 "memory_mb": memory_mb,
738 })),
739 }
740 }
741
742 fn generate_azure_config(&self, service: &AzureService) -> Result<serde_json::Value> {
744 match service {
745 AzureService::AzureML {
746 workspace_name,
747 endpoint_name,
748 } => Ok(serde_json::json!({
749 "service": "azure_ml",
750 "workspace_name": workspace_name,
751 "endpoint_name": endpoint_name,
752 })),
753 AzureService::AzureFunctions {
754 function_app_name,
755 runtime,
756 } => Ok(serde_json::json!({
757 "service": "azure_functions",
758 "function_app_name": function_app_name,
759 "runtime": runtime,
760 })),
761 AzureService::AKS {
762 cluster_name,
763 namespace,
764 } => Ok(serde_json::json!({
765 "service": "aks",
766 "cluster_name": cluster_name,
767 "namespace": namespace,
768 })),
769 AzureService::ACI {
770 container_group_name,
771 cpu_cores,
772 memory_gb,
773 } => Ok(serde_json::json!({
774 "service": "aci",
775 "container_group_name": container_group_name,
776 "cpu_cores": cpu_cores,
777 "memory_gb": memory_gb,
778 })),
779 }
780 }
781
782 fn generate_deployment_script(&self) -> Result<String> {
784 let mut script = String::new();
785
786 script.push_str("#!/bin/bash\n");
787 script.push_str("# Deployment script for ToRSh FX model\n\n");
788 script.push_str("set -e\n\n");
789
790 script.push_str("echo 'Building Docker image...'\n");
791 script.push_str(&format!("docker build -t {} .\n\n", self.config.name));
792
793 match &self.config.platform {
794 CloudPlatform::AWS { region, .. } => {
795 script.push_str("echo 'Deploying to AWS...'\n");
796 script.push_str(&format!("export AWS_REGION={}\n", region));
797 script.push_str("# Add AWS-specific deployment commands here\n\n");
798 }
799 CloudPlatform::GCP {
800 project_id, region, ..
801 } => {
802 script.push_str("echo 'Deploying to GCP...'\n");
803 script.push_str(&format!("export GCP_PROJECT={}\n", project_id));
804 script.push_str(&format!("export GCP_REGION={}\n", region));
805 script.push_str("# Add GCP-specific deployment commands here\n\n");
806 }
807 CloudPlatform::Azure {
808 subscription_id, ..
809 } => {
810 script.push_str("echo 'Deploying to Azure...'\n");
811 script.push_str(&format!(
812 "export AZURE_SUBSCRIPTION_ID={}\n",
813 subscription_id
814 ));
815 script.push_str("# Add Azure-specific deployment commands here\n\n");
816 }
817 CloudPlatform::Custom { endpoint, .. } => {
818 script.push_str("echo 'Deploying to custom platform...'\n");
819 script.push_str(&format!("export ENDPOINT={}\n", endpoint));
820 script.push_str("# Add custom deployment commands here\n\n");
821 }
822 }
823
824 script.push_str("echo 'Deployment complete!'\n");
825
826 Ok(script)
827 }
828}
829
830#[derive(Debug, Clone)]
832pub struct DeploymentPackage {
833 pub path: PathBuf,
835 pub config: DeploymentConfig,
837 pub metadata: ModelMetadata,
839}
840
841impl DeploymentPackage {
842 pub fn path(&self) -> &Path {
844 &self.path
845 }
846
847 pub fn config(&self) -> &DeploymentConfig {
849 &self.config
850 }
851
852 pub fn metadata(&self) -> &ModelMetadata {
854 &self.metadata
855 }
856}
857
858impl DeploymentConfig {
860 pub fn aws_sagemaker(name: String, region: String) -> Self {
862 Self {
863 name,
864 platform: CloudPlatform::AWS {
865 region,
866 service: AWSService::SageMaker {
867 instance_type: "ml.m5.xlarge".to_string(),
868 endpoint_name: "torsh-fx-endpoint".to_string(),
869 },
870 },
871 container: ContainerConfig::default(),
872 resources: ResourceRequirements::default(),
873 autoscaling: Some(AutoScalingConfig::default()),
874 environment_variables: HashMap::new(),
875 health_check: HealthCheckConfig::default(),
876 monitoring: MonitoringConfig::default(),
877 }
878 }
879
880 pub fn gcp_vertex_ai(name: String, project_id: String, region: String) -> Self {
882 Self {
883 name,
884 platform: CloudPlatform::GCP {
885 project_id,
886 region,
887 service: GCPService::VertexAI {
888 model_name: "torsh-fx-model".to_string(),
889 machine_type: "n1-standard-4".to_string(),
890 },
891 },
892 container: ContainerConfig::default(),
893 resources: ResourceRequirements::default(),
894 autoscaling: Some(AutoScalingConfig::default()),
895 environment_variables: HashMap::new(),
896 health_check: HealthCheckConfig::default(),
897 monitoring: MonitoringConfig::default(),
898 }
899 }
900}
901
902impl Default for ContainerConfig {
903 fn default() -> Self {
904 Self {
905 base_image: "python:3.11-slim".to_string(),
906 python_version: "3.11".to_string(),
907 system_packages: vec!["curl".to_string()],
908 python_packages: Vec::new(),
909 entrypoint: vec!["python".to_string(), "server.py".to_string()],
910 port: 8080,
911 }
912 }
913}
914
915impl Default for ResourceRequirements {
916 fn default() -> Self {
917 Self {
918 cpu_cores: 2.0,
919 memory_gb: 4.0,
920 gpu_count: 0,
921 gpu_type: None,
922 storage_gb: 10,
923 }
924 }
925}
926
927impl Default for AutoScalingConfig {
928 fn default() -> Self {
929 Self {
930 min_instances: 1,
931 max_instances: 10,
932 target_cpu_utilization: 70.0,
933 target_memory_utilization: 80.0,
934 scale_up_cooldown: 60,
935 scale_down_cooldown: 300,
936 }
937 }
938}
939
940impl Default for HealthCheckConfig {
941 fn default() -> Self {
942 Self {
943 path: "/health".to_string(),
944 interval_seconds: 30,
945 timeout_seconds: 10,
946 healthy_threshold: 2,
947 unhealthy_threshold: 3,
948 }
949 }
950}
951
952impl Default for MonitoringConfig {
953 fn default() -> Self {
954 Self {
955 enable_metrics: true,
956 enable_logging: true,
957 enable_tracing: false,
958 metrics_endpoint: None,
959 log_level: "INFO".to_string(),
960 custom_metrics: Vec::new(),
961 }
962 }
963}
964
965#[cfg(test)]
966mod tests {
967 use super::*;
968
969 #[test]
970 fn test_aws_config_generation() {
971 let config =
972 DeploymentConfig::aws_sagemaker("test-deployment".to_string(), "us-east-1".to_string());
973
974 assert_eq!(config.name, "test-deployment");
975 matches!(config.platform, CloudPlatform::AWS { .. });
976 }
977
978 #[test]
979 fn test_gcp_config_generation() {
980 let config = DeploymentConfig::gcp_vertex_ai(
981 "test-deployment".to_string(),
982 "my-project".to_string(),
983 "us-central1".to_string(),
984 );
985
986 assert_eq!(config.name, "test-deployment");
987 matches!(config.platform, CloudPlatform::GCP { .. });
988 }
989
990 #[test]
991 fn test_deployment_packager_creation() {
992 let temp_dir = std::env::temp_dir().join("torsh_fx_cloud_deploy_test");
993 let config = DeploymentConfig::aws_sagemaker("test".to_string(), "us-east-1".to_string());
994
995 let result = CloudDeploymentPackager::new(&temp_dir, config);
996 assert!(result.is_ok());
997
998 let _ = fs::remove_dir_all(&temp_dir);
1000 }
1001}