flowyml 1.7.1__py3-none-any.whl → 1.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flowyml/assets/base.py +15 -0
- flowyml/assets/dataset.py +570 -17
- flowyml/assets/metrics.py +5 -0
- flowyml/assets/model.py +1052 -15
- flowyml/cli/main.py +709 -0
- flowyml/cli/stack_cli.py +138 -25
- flowyml/core/__init__.py +17 -0
- flowyml/core/executor.py +231 -37
- flowyml/core/image_builder.py +129 -0
- flowyml/core/log_streamer.py +227 -0
- flowyml/core/orchestrator.py +59 -4
- flowyml/core/pipeline.py +65 -13
- flowyml/core/routing.py +558 -0
- flowyml/core/scheduler.py +88 -5
- flowyml/core/step.py +9 -1
- flowyml/core/step_grouping.py +49 -35
- flowyml/core/types.py +407 -0
- flowyml/integrations/keras.py +247 -82
- flowyml/monitoring/alerts.py +10 -0
- flowyml/monitoring/notifications.py +104 -25
- flowyml/monitoring/slack_blocks.py +323 -0
- flowyml/plugins/__init__.py +251 -0
- flowyml/plugins/alerters/__init__.py +1 -0
- flowyml/plugins/alerters/slack.py +168 -0
- flowyml/plugins/base.py +752 -0
- flowyml/plugins/config.py +478 -0
- flowyml/plugins/deployers/__init__.py +22 -0
- flowyml/plugins/deployers/gcp_cloud_run.py +200 -0
- flowyml/plugins/deployers/sagemaker.py +306 -0
- flowyml/plugins/deployers/vertex.py +290 -0
- flowyml/plugins/integration.py +369 -0
- flowyml/plugins/manager.py +510 -0
- flowyml/plugins/model_registries/__init__.py +22 -0
- flowyml/plugins/model_registries/mlflow.py +159 -0
- flowyml/plugins/model_registries/sagemaker.py +489 -0
- flowyml/plugins/model_registries/vertex.py +386 -0
- flowyml/plugins/orchestrators/__init__.py +13 -0
- flowyml/plugins/orchestrators/sagemaker.py +443 -0
- flowyml/plugins/orchestrators/vertex_ai.py +461 -0
- flowyml/plugins/registries/__init__.py +13 -0
- flowyml/plugins/registries/ecr.py +321 -0
- flowyml/plugins/registries/gcr.py +313 -0
- flowyml/plugins/registry.py +454 -0
- flowyml/plugins/stack.py +494 -0
- flowyml/plugins/stack_config.py +537 -0
- flowyml/plugins/stores/__init__.py +13 -0
- flowyml/plugins/stores/gcs.py +460 -0
- flowyml/plugins/stores/s3.py +453 -0
- flowyml/plugins/trackers/__init__.py +11 -0
- flowyml/plugins/trackers/mlflow.py +316 -0
- flowyml/plugins/validators/__init__.py +3 -0
- flowyml/plugins/validators/deepchecks.py +119 -0
- flowyml/registry/__init__.py +2 -1
- flowyml/registry/model_environment.py +109 -0
- flowyml/registry/model_registry.py +241 -96
- flowyml/serving/__init__.py +17 -0
- flowyml/serving/model_server.py +628 -0
- flowyml/stacks/__init__.py +60 -0
- flowyml/stacks/aws.py +93 -0
- flowyml/stacks/base.py +62 -0
- flowyml/stacks/components.py +12 -0
- flowyml/stacks/gcp.py +44 -9
- flowyml/stacks/plugins.py +115 -0
- flowyml/stacks/registry.py +2 -1
- flowyml/storage/sql.py +401 -12
- flowyml/tracking/experiment.py +8 -5
- flowyml/ui/backend/Dockerfile +87 -16
- flowyml/ui/backend/auth.py +12 -2
- flowyml/ui/backend/main.py +149 -5
- flowyml/ui/backend/routers/ai_context.py +226 -0
- flowyml/ui/backend/routers/assets.py +23 -4
- flowyml/ui/backend/routers/auth.py +96 -0
- flowyml/ui/backend/routers/deployments.py +660 -0
- flowyml/ui/backend/routers/model_explorer.py +597 -0
- flowyml/ui/backend/routers/plugins.py +103 -51
- flowyml/ui/backend/routers/projects.py +91 -8
- flowyml/ui/backend/routers/runs.py +132 -1
- flowyml/ui/backend/routers/schedules.py +54 -29
- flowyml/ui/backend/routers/templates.py +319 -0
- flowyml/ui/backend/routers/websocket.py +2 -2
- flowyml/ui/frontend/Dockerfile +55 -6
- flowyml/ui/frontend/dist/assets/index-B5AsPTSz.css +1 -0
- flowyml/ui/frontend/dist/assets/index-dFbZ8wD8.js +753 -0
- flowyml/ui/frontend/dist/index.html +2 -2
- flowyml/ui/frontend/dist/logo.png +0 -0
- flowyml/ui/frontend/nginx.conf +65 -4
- flowyml/ui/frontend/package-lock.json +1415 -74
- flowyml/ui/frontend/package.json +4 -0
- flowyml/ui/frontend/public/logo.png +0 -0
- flowyml/ui/frontend/src/App.jsx +10 -7
- flowyml/ui/frontend/src/app/assets/page.jsx +890 -321
- flowyml/ui/frontend/src/app/auth/Login.jsx +90 -0
- flowyml/ui/frontend/src/app/dashboard/page.jsx +8 -8
- flowyml/ui/frontend/src/app/deployments/page.jsx +786 -0
- flowyml/ui/frontend/src/app/model-explorer/page.jsx +1031 -0
- flowyml/ui/frontend/src/app/pipelines/page.jsx +12 -2
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +19 -6
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectMetricsPanel.jsx +1 -1
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +601 -101
- flowyml/ui/frontend/src/app/runs/page.jsx +8 -2
- flowyml/ui/frontend/src/app/settings/page.jsx +267 -253
- flowyml/ui/frontend/src/components/ArtifactViewer.jsx +62 -2
- flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +424 -29
- flowyml/ui/frontend/src/components/AssetTreeHierarchy.jsx +119 -11
- flowyml/ui/frontend/src/components/DatasetViewer.jsx +753 -0
- flowyml/ui/frontend/src/components/Layout.jsx +6 -0
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +79 -29
- flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +36 -6
- flowyml/ui/frontend/src/components/RunMetaPanel.jsx +113 -0
- flowyml/ui/frontend/src/components/TrainingHistoryChart.jsx +514 -0
- flowyml/ui/frontend/src/components/TrainingMetricsPanel.jsx +175 -0
- flowyml/ui/frontend/src/components/ai/AIAssistantButton.jsx +71 -0
- flowyml/ui/frontend/src/components/ai/AIAssistantPanel.jsx +420 -0
- flowyml/ui/frontend/src/components/header/Header.jsx +22 -0
- flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +4 -4
- flowyml/ui/frontend/src/components/plugins/{ZenMLIntegration.jsx → StackImport.jsx} +38 -12
- flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +36 -13
- flowyml/ui/frontend/src/contexts/AIAssistantContext.jsx +245 -0
- flowyml/ui/frontend/src/contexts/AuthContext.jsx +108 -0
- flowyml/ui/frontend/src/hooks/useAIContext.js +156 -0
- flowyml/ui/frontend/src/hooks/useWebGPU.js +54 -0
- flowyml/ui/frontend/src/layouts/MainLayout.jsx +6 -0
- flowyml/ui/frontend/src/router/index.jsx +47 -20
- flowyml/ui/frontend/src/services/pluginService.js +3 -1
- flowyml/ui/server_manager.py +5 -5
- flowyml/ui/utils.py +157 -39
- flowyml/utils/config.py +37 -15
- flowyml/utils/model_introspection.py +123 -0
- flowyml/utils/observability.py +30 -0
- flowyml-1.8.0.dist-info/METADATA +174 -0
- {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/RECORD +134 -73
- {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/WHEEL +1 -1
- flowyml/ui/frontend/dist/assets/index-BqDQvp63.js +0 -630
- flowyml/ui/frontend/dist/assets/index-By4trVyv.css +0 -1
- flowyml-1.7.1.dist-info/METADATA +0 -477
- {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/entry_points.txt +0 -0
- {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""SageMaker Endpoint Deployer - Native FlowyML Plugin.
|
|
2
|
+
|
|
3
|
+
This plugin provides direct integration with AWS SageMaker
|
|
4
|
+
endpoints for model serving and inference.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from flowyml.plugins import get_plugin
|
|
8
|
+
|
|
9
|
+
deployer = get_plugin("sagemaker_endpoint", region="us-east-1")
|
|
10
|
+
endpoint = deployer.deploy(
|
|
11
|
+
model_name="my-model",
|
|
12
|
+
endpoint_name="my-endpoint",
|
|
13
|
+
instance_type="ml.m5.large"
|
|
14
|
+
)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from typing import Any
|
|
19
|
+
import time
|
|
20
|
+
|
|
21
|
+
from flowyml.plugins.base import ModelDeployerPlugin, PluginMetadata, PluginType
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SageMakerEndpointDeployer(ModelDeployerPlugin):
|
|
27
|
+
"""Native SageMaker Endpoint deployer for FlowyML.
|
|
28
|
+
|
|
29
|
+
This plugin deploys models to SageMaker Endpoints for real-time inference.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
region: AWS region.
|
|
33
|
+
role_arn: IAM role ARN for SageMaker.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
metadata = PluginMetadata(
|
|
37
|
+
name="sagemaker_endpoint",
|
|
38
|
+
version="1.0.0",
|
|
39
|
+
description="AWS SageMaker Endpoint Deployer",
|
|
40
|
+
author="FlowyML Team",
|
|
41
|
+
plugin_type=PluginType.CUSTOM,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
region: str = None,
|
|
47
|
+
role_arn: str = None,
|
|
48
|
+
**kwargs,
|
|
49
|
+
):
|
|
50
|
+
"""Initialize the SageMaker Endpoint deployer.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
region: AWS region (uses default if not specified).
|
|
54
|
+
role_arn: IAM role ARN for SageMaker operations.
|
|
55
|
+
**kwargs: Additional plugin arguments.
|
|
56
|
+
"""
|
|
57
|
+
super().__init__(**kwargs)
|
|
58
|
+
self.region = region
|
|
59
|
+
self.role_arn = role_arn
|
|
60
|
+
self._boto_session = None
|
|
61
|
+
self._sm_client = None
|
|
62
|
+
self._runtime_client = None
|
|
63
|
+
self._initialized = False
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def plugin_type(self) -> PluginType:
|
|
67
|
+
return PluginType.CUSTOM
|
|
68
|
+
|
|
69
|
+
def initialize(self) -> None:
|
|
70
|
+
"""Initialize connection to SageMaker."""
|
|
71
|
+
if self._initialized:
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
import boto3
|
|
76
|
+
|
|
77
|
+
self._boto_session = boto3.Session(region_name=self.region)
|
|
78
|
+
self._sm_client = self._boto_session.client("sagemaker")
|
|
79
|
+
self._runtime_client = self._boto_session.client("sagemaker-runtime")
|
|
80
|
+
|
|
81
|
+
self._initialized = True
|
|
82
|
+
logger.info(f"SageMaker Endpoint Deployer initialized in region {self.region}")
|
|
83
|
+
except ImportError:
|
|
84
|
+
raise ImportError(
|
|
85
|
+
"boto3 is required. Install with: pip install boto3",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def _ensure_initialized(self) -> None:
|
|
89
|
+
"""Ensure SageMaker is initialized."""
|
|
90
|
+
if not self._initialized:
|
|
91
|
+
self.initialize()
|
|
92
|
+
|
|
93
|
+
def deploy(
|
|
94
|
+
self,
|
|
95
|
+
model_uri: str,
|
|
96
|
+
endpoint_name: str,
|
|
97
|
+
instance_type: str = "ml.m5.large",
|
|
98
|
+
instance_count: int = 1,
|
|
99
|
+
inference_image_uri: str = None,
|
|
100
|
+
wait: bool = False,
|
|
101
|
+
**kwargs,
|
|
102
|
+
) -> str:
|
|
103
|
+
"""Deploy a model to a SageMaker endpoint.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
model_uri: S3 URI to model artifacts or model package ARN.
|
|
107
|
+
endpoint_name: Name for the endpoint.
|
|
108
|
+
instance_type: Instance type (e.g., ml.m5.large).
|
|
109
|
+
instance_count: Number of instances.
|
|
110
|
+
inference_image_uri: Docker image for inference.
|
|
111
|
+
wait: Whether to wait for deployment to complete.
|
|
112
|
+
**kwargs: Additional deployment arguments.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Endpoint ARN.
|
|
116
|
+
"""
|
|
117
|
+
self._ensure_initialized()
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
model_name = f"{endpoint_name}-model"
|
|
121
|
+
config_name = f"{endpoint_name}-config"
|
|
122
|
+
|
|
123
|
+
# Create model (if model_uri is S3 path, need image)
|
|
124
|
+
if model_uri.startswith("s3://"):
|
|
125
|
+
if not inference_image_uri:
|
|
126
|
+
raise ValueError("inference_image_uri required for S3 model")
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
self._sm_client.create_model(
|
|
130
|
+
ModelName=model_name,
|
|
131
|
+
PrimaryContainer={
|
|
132
|
+
"Image": inference_image_uri,
|
|
133
|
+
"ModelDataUrl": model_uri,
|
|
134
|
+
},
|
|
135
|
+
ExecutionRoleArn=self.role_arn,
|
|
136
|
+
)
|
|
137
|
+
except self._sm_client.exceptions.ResourceInUse:
|
|
138
|
+
logger.info(f"Model {model_name} already exists")
|
|
139
|
+
elif model_uri.startswith("arn:"):
|
|
140
|
+
# Model package ARN
|
|
141
|
+
try:
|
|
142
|
+
self._sm_client.create_model(
|
|
143
|
+
ModelName=model_name,
|
|
144
|
+
PrimaryContainer={
|
|
145
|
+
"ModelPackageName": model_uri,
|
|
146
|
+
},
|
|
147
|
+
ExecutionRoleArn=self.role_arn,
|
|
148
|
+
)
|
|
149
|
+
except self._sm_client.exceptions.ResourceInUse:
|
|
150
|
+
logger.info(f"Model {model_name} already exists")
|
|
151
|
+
else:
|
|
152
|
+
raise ValueError(f"Invalid model_uri format: {model_uri}")
|
|
153
|
+
|
|
154
|
+
# Create endpoint config
|
|
155
|
+
try:
|
|
156
|
+
self._sm_client.create_endpoint_config(
|
|
157
|
+
EndpointConfigName=config_name,
|
|
158
|
+
ProductionVariants=[
|
|
159
|
+
{
|
|
160
|
+
"VariantName": "primary",
|
|
161
|
+
"ModelName": model_name,
|
|
162
|
+
"InstanceType": instance_type,
|
|
163
|
+
"InitialInstanceCount": instance_count,
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
)
|
|
167
|
+
except self._sm_client.exceptions.ResourceInUse:
|
|
168
|
+
logger.info(f"Endpoint config {config_name} already exists")
|
|
169
|
+
|
|
170
|
+
# Create or update endpoint
|
|
171
|
+
try:
|
|
172
|
+
self._sm_client.create_endpoint(
|
|
173
|
+
EndpointName=endpoint_name,
|
|
174
|
+
EndpointConfigName=config_name,
|
|
175
|
+
)
|
|
176
|
+
logger.info(f"Creating endpoint: {endpoint_name}")
|
|
177
|
+
except self._sm_client.exceptions.ResourceInUse:
|
|
178
|
+
self._sm_client.update_endpoint(
|
|
179
|
+
EndpointName=endpoint_name,
|
|
180
|
+
EndpointConfigName=config_name,
|
|
181
|
+
)
|
|
182
|
+
logger.info(f"Updating endpoint: {endpoint_name}")
|
|
183
|
+
|
|
184
|
+
if wait:
|
|
185
|
+
self._wait_for_endpoint(endpoint_name)
|
|
186
|
+
|
|
187
|
+
# Get endpoint ARN
|
|
188
|
+
endpoint_desc = self._sm_client.describe_endpoint(
|
|
189
|
+
EndpointName=endpoint_name,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
logger.info(f"Deployed to endpoint: {endpoint_name}")
|
|
193
|
+
return endpoint_desc["EndpointArn"]
|
|
194
|
+
|
|
195
|
+
except Exception as e:
|
|
196
|
+
logger.error(f"Failed to deploy model: {e}")
|
|
197
|
+
raise
|
|
198
|
+
|
|
199
|
+
def _wait_for_endpoint(self, endpoint_name: str, timeout: int = 600) -> None:
|
|
200
|
+
"""Wait for endpoint to be in service."""
|
|
201
|
+
start = time.time()
|
|
202
|
+
while time.time() - start < timeout:
|
|
203
|
+
desc = self._sm_client.describe_endpoint(EndpointName=endpoint_name)
|
|
204
|
+
status = desc["EndpointStatus"]
|
|
205
|
+
|
|
206
|
+
if status == "InService":
|
|
207
|
+
logger.info(f"Endpoint {endpoint_name} is InService")
|
|
208
|
+
return
|
|
209
|
+
elif status == "Failed":
|
|
210
|
+
raise RuntimeError(f"Endpoint deployment failed: {desc.get('FailureReason')}")
|
|
211
|
+
|
|
212
|
+
logger.debug(f"Waiting for endpoint, status: {status}")
|
|
213
|
+
time.sleep(30)
|
|
214
|
+
|
|
215
|
+
raise TimeoutError(f"Endpoint deployment timed out after {timeout}s")
|
|
216
|
+
|
|
217
|
+
def undeploy(
|
|
218
|
+
self,
|
|
219
|
+
endpoint_name: str,
|
|
220
|
+
) -> bool:
|
|
221
|
+
"""Delete an endpoint.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
endpoint_name: Endpoint name.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
True if successful.
|
|
228
|
+
"""
|
|
229
|
+
self._ensure_initialized()
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
self._sm_client.delete_endpoint(EndpointName=endpoint_name)
|
|
233
|
+
logger.info(f"Deleted endpoint: {endpoint_name}")
|
|
234
|
+
return True
|
|
235
|
+
|
|
236
|
+
except Exception as e:
|
|
237
|
+
logger.error(f"Failed to delete endpoint: {e}")
|
|
238
|
+
return False
|
|
239
|
+
|
|
240
|
+
def get_endpoint(
|
|
241
|
+
self,
|
|
242
|
+
endpoint_name: str,
|
|
243
|
+
) -> dict | None:
|
|
244
|
+
"""Get endpoint details.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
endpoint_name: Endpoint name.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Endpoint details dictionary.
|
|
251
|
+
"""
|
|
252
|
+
self._ensure_initialized()
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
desc = self._sm_client.describe_endpoint(EndpointName=endpoint_name)
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
"name": endpoint_name,
|
|
259
|
+
"arn": desc["EndpointArn"],
|
|
260
|
+
"status": desc["EndpointStatus"],
|
|
261
|
+
"config_name": desc["EndpointConfigName"],
|
|
262
|
+
"created": str(desc.get("CreationTime", "")),
|
|
263
|
+
"last_modified": str(desc.get("LastModifiedTime", "")),
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
except self._sm_client.exceptions.ResourceNotFoundException:
|
|
267
|
+
return None
|
|
268
|
+
except Exception as e:
|
|
269
|
+
logger.error(f"Failed to get endpoint: {e}")
|
|
270
|
+
return None
|
|
271
|
+
|
|
272
|
+
def predict(
|
|
273
|
+
self,
|
|
274
|
+
endpoint_name: str,
|
|
275
|
+
data: Any,
|
|
276
|
+
content_type: str = "application/json",
|
|
277
|
+
) -> Any:
|
|
278
|
+
"""Make predictions using a deployed endpoint.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
endpoint_name: Endpoint name.
|
|
282
|
+
data: Input data (will be JSON serialized if dict/list).
|
|
283
|
+
content_type: Content type of the request.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Prediction result.
|
|
287
|
+
"""
|
|
288
|
+
self._ensure_initialized()
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
import json
|
|
292
|
+
|
|
293
|
+
body = json.dumps(data) if isinstance(data, (dict, list)) else str(data)
|
|
294
|
+
|
|
295
|
+
response = self._runtime_client.invoke_endpoint(
|
|
296
|
+
EndpointName=endpoint_name,
|
|
297
|
+
ContentType=content_type,
|
|
298
|
+
Body=body.encode("utf-8"),
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
result = json.loads(response["Body"].read().decode("utf-8"))
|
|
302
|
+
return result
|
|
303
|
+
|
|
304
|
+
except Exception as e:
|
|
305
|
+
logger.error(f"Failed to predict: {e}")
|
|
306
|
+
raise
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""Vertex AI Endpoint Deployer - Native FlowyML Plugin.
|
|
2
|
+
|
|
3
|
+
This plugin provides direct integration with Google Cloud Vertex AI
|
|
4
|
+
endpoints for model serving and inference.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from flowyml.plugins import get_plugin
|
|
8
|
+
|
|
9
|
+
deployer = get_plugin("vertex_endpoint", project="my-project")
|
|
10
|
+
endpoint = deployer.deploy(
|
|
11
|
+
model_name="my-model",
|
|
12
|
+
endpoint_name="my-endpoint",
|
|
13
|
+
machine_type="n1-standard-4"
|
|
14
|
+
)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
|
|
19
|
+
from flowyml.plugins.base import ModelDeployerPlugin, PluginMetadata, PluginType
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class VertexEndpointDeployer(ModelDeployerPlugin):
|
|
25
|
+
"""Native Vertex AI Endpoint deployer for FlowyML.
|
|
26
|
+
|
|
27
|
+
This plugin deploys models to Vertex AI Endpoints for real-time inference.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
project: GCP project ID.
|
|
31
|
+
location: GCP region (default: us-central1).
|
|
32
|
+
staging_bucket: GCS bucket for staging artifacts.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
metadata = PluginMetadata(
|
|
36
|
+
name="vertex_endpoint",
|
|
37
|
+
version="1.0.0",
|
|
38
|
+
description="Google Cloud Vertex AI Endpoint Deployer",
|
|
39
|
+
author="FlowyML Team",
|
|
40
|
+
plugin_type=PluginType.CUSTOM,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
project: str,
|
|
46
|
+
location: str = "us-central1",
|
|
47
|
+
staging_bucket: str = None,
|
|
48
|
+
**kwargs,
|
|
49
|
+
):
|
|
50
|
+
"""Initialize the Vertex Endpoint deployer.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
project: GCP project ID.
|
|
54
|
+
location: GCP region.
|
|
55
|
+
staging_bucket: GCS bucket for artifacts.
|
|
56
|
+
**kwargs: Additional plugin arguments.
|
|
57
|
+
"""
|
|
58
|
+
super().__init__(**kwargs)
|
|
59
|
+
self.project = project
|
|
60
|
+
self.location = location
|
|
61
|
+
self.staging_bucket = staging_bucket
|
|
62
|
+
self._aiplatform = None
|
|
63
|
+
self._initialized = False
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def plugin_type(self) -> PluginType:
|
|
67
|
+
return PluginType.CUSTOM
|
|
68
|
+
|
|
69
|
+
def initialize(self) -> None:
|
|
70
|
+
"""Initialize connection to Vertex AI."""
|
|
71
|
+
if self._initialized:
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
from google.cloud import aiplatform
|
|
76
|
+
|
|
77
|
+
aiplatform.init(
|
|
78
|
+
project=self.project,
|
|
79
|
+
location=self.location,
|
|
80
|
+
staging_bucket=self.staging_bucket,
|
|
81
|
+
)
|
|
82
|
+
self._aiplatform = aiplatform
|
|
83
|
+
self._initialized = True
|
|
84
|
+
logger.info(f"Vertex Endpoint Deployer initialized for project {self.project}")
|
|
85
|
+
except ImportError:
|
|
86
|
+
raise ImportError(
|
|
87
|
+
"google-cloud-aiplatform is required. " "Install with: pip install google-cloud-aiplatform",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def _ensure_initialized(self) -> None:
|
|
91
|
+
"""Ensure Vertex AI is initialized."""
|
|
92
|
+
if not self._initialized:
|
|
93
|
+
self.initialize()
|
|
94
|
+
|
|
95
|
+
def deploy(
|
|
96
|
+
self,
|
|
97
|
+
model_uri: str,
|
|
98
|
+
endpoint_name: str,
|
|
99
|
+
machine_type: str = "n1-standard-4",
|
|
100
|
+
min_replica_count: int = 1,
|
|
101
|
+
max_replica_count: int = 1,
|
|
102
|
+
accelerator_type: str = None,
|
|
103
|
+
accelerator_count: int = 0,
|
|
104
|
+
service_account: str = None,
|
|
105
|
+
**kwargs,
|
|
106
|
+
) -> str:
|
|
107
|
+
"""Deploy a model to a Vertex AI endpoint.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
model_uri: Model resource name or GCS URI.
|
|
111
|
+
endpoint_name: Name for the endpoint.
|
|
112
|
+
machine_type: Machine type (e.g., n1-standard-4).
|
|
113
|
+
min_replica_count: Minimum replicas.
|
|
114
|
+
max_replica_count: Maximum replicas.
|
|
115
|
+
accelerator_type: GPU type (NVIDIA_TESLA_T4, etc.).
|
|
116
|
+
accelerator_count: Number of GPUs.
|
|
117
|
+
service_account: Service account for endpoint.
|
|
118
|
+
**kwargs: Additional deployment arguments.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Endpoint resource name.
|
|
122
|
+
"""
|
|
123
|
+
self._ensure_initialized()
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
# Get or create endpoint
|
|
127
|
+
endpoints = self._aiplatform.Endpoint.list(
|
|
128
|
+
filter=f'display_name="{endpoint_name}"',
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if endpoints:
|
|
132
|
+
endpoint = endpoints[0]
|
|
133
|
+
logger.info(f"Using existing endpoint: {endpoint_name}")
|
|
134
|
+
else:
|
|
135
|
+
endpoint = self._aiplatform.Endpoint.create(
|
|
136
|
+
display_name=endpoint_name,
|
|
137
|
+
)
|
|
138
|
+
logger.info(f"Created endpoint: {endpoint_name}")
|
|
139
|
+
|
|
140
|
+
# Get the model
|
|
141
|
+
if model_uri.startswith("projects/"):
|
|
142
|
+
model = self._aiplatform.Model(model_name=model_uri)
|
|
143
|
+
else:
|
|
144
|
+
# Search by display name
|
|
145
|
+
models = self._aiplatform.Model.list(
|
|
146
|
+
filter=f'display_name="{model_uri}"',
|
|
147
|
+
order_by="create_time desc",
|
|
148
|
+
)
|
|
149
|
+
if not models:
|
|
150
|
+
raise ValueError(f"Model '{model_uri}' not found")
|
|
151
|
+
model = models[0]
|
|
152
|
+
|
|
153
|
+
# Build deployment config
|
|
154
|
+
deploy_config = {
|
|
155
|
+
"machine_type": machine_type,
|
|
156
|
+
"min_replica_count": min_replica_count,
|
|
157
|
+
"max_replica_count": max_replica_count,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if accelerator_type and accelerator_count > 0:
|
|
161
|
+
deploy_config["accelerator_type"] = accelerator_type
|
|
162
|
+
deploy_config["accelerator_count"] = accelerator_count
|
|
163
|
+
|
|
164
|
+
if service_account:
|
|
165
|
+
deploy_config["service_account"] = service_account
|
|
166
|
+
|
|
167
|
+
# Deploy model to endpoint
|
|
168
|
+
model.deploy(endpoint=endpoint, **deploy_config)
|
|
169
|
+
|
|
170
|
+
logger.info(f"Deployed model to endpoint: {endpoint.resource_name}")
|
|
171
|
+
return endpoint.resource_name
|
|
172
|
+
|
|
173
|
+
except Exception as e:
|
|
174
|
+
logger.error(f"Failed to deploy model: {e}")
|
|
175
|
+
raise
|
|
176
|
+
|
|
177
|
+
def undeploy(
|
|
178
|
+
self,
|
|
179
|
+
endpoint_name: str,
|
|
180
|
+
deployed_model_id: str = None,
|
|
181
|
+
) -> bool:
|
|
182
|
+
"""Undeploy a model from an endpoint.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
endpoint_name: Endpoint name or resource name.
|
|
186
|
+
deployed_model_id: Specific deployed model ID (undeployes all if None).
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
True if successful.
|
|
190
|
+
"""
|
|
191
|
+
self._ensure_initialized()
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
if endpoint_name.startswith("projects/"):
|
|
195
|
+
endpoint = self._aiplatform.Endpoint(endpoint_name=endpoint_name)
|
|
196
|
+
else:
|
|
197
|
+
endpoints = self._aiplatform.Endpoint.list(
|
|
198
|
+
filter=f'display_name="{endpoint_name}"',
|
|
199
|
+
)
|
|
200
|
+
if not endpoints:
|
|
201
|
+
logger.warning(f"Endpoint '{endpoint_name}' not found")
|
|
202
|
+
return False
|
|
203
|
+
endpoint = endpoints[0]
|
|
204
|
+
|
|
205
|
+
if deployed_model_id:
|
|
206
|
+
endpoint.undeploy(deployed_model_id=deployed_model_id)
|
|
207
|
+
else:
|
|
208
|
+
endpoint.undeploy_all()
|
|
209
|
+
|
|
210
|
+
logger.info(f"Undeployed model(s) from endpoint: {endpoint_name}")
|
|
211
|
+
return True
|
|
212
|
+
|
|
213
|
+
except Exception as e:
|
|
214
|
+
logger.error(f"Failed to undeploy: {e}")
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
def get_endpoint(
|
|
218
|
+
self,
|
|
219
|
+
endpoint_name: str,
|
|
220
|
+
) -> dict | None:
|
|
221
|
+
"""Get endpoint details.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
endpoint_name: Endpoint name or resource name.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Endpoint details dictionary.
|
|
228
|
+
"""
|
|
229
|
+
self._ensure_initialized()
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
if endpoint_name.startswith("projects/"):
|
|
233
|
+
endpoint = self._aiplatform.Endpoint(endpoint_name=endpoint_name)
|
|
234
|
+
else:
|
|
235
|
+
endpoints = self._aiplatform.Endpoint.list(
|
|
236
|
+
filter=f'display_name="{endpoint_name}"',
|
|
237
|
+
)
|
|
238
|
+
if not endpoints:
|
|
239
|
+
return None
|
|
240
|
+
endpoint = endpoints[0]
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
"name": endpoint.display_name,
|
|
244
|
+
"resource_name": endpoint.resource_name,
|
|
245
|
+
"deployed_models": [
|
|
246
|
+
{
|
|
247
|
+
"id": dm.id,
|
|
248
|
+
"model": dm.model,
|
|
249
|
+
}
|
|
250
|
+
for dm in endpoint.deployed_models
|
|
251
|
+
],
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
except Exception as e:
|
|
255
|
+
logger.error(f"Failed to get endpoint: {e}")
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
def predict(
|
|
259
|
+
self,
|
|
260
|
+
endpoint_name: str,
|
|
261
|
+
instances: list[dict],
|
|
262
|
+
) -> list[dict]:
|
|
263
|
+
"""Make predictions using a deployed endpoint.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
endpoint_name: Endpoint name or resource name.
|
|
267
|
+
instances: List of input instances.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
List of predictions.
|
|
271
|
+
"""
|
|
272
|
+
self._ensure_initialized()
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
if endpoint_name.startswith("projects/"):
|
|
276
|
+
endpoint = self._aiplatform.Endpoint(endpoint_name=endpoint_name)
|
|
277
|
+
else:
|
|
278
|
+
endpoints = self._aiplatform.Endpoint.list(
|
|
279
|
+
filter=f'display_name="{endpoint_name}"',
|
|
280
|
+
)
|
|
281
|
+
if not endpoints:
|
|
282
|
+
raise ValueError(f"Endpoint '{endpoint_name}' not found")
|
|
283
|
+
endpoint = endpoints[0]
|
|
284
|
+
|
|
285
|
+
response = endpoint.predict(instances=instances)
|
|
286
|
+
return response.predictions
|
|
287
|
+
|
|
288
|
+
except Exception as e:
|
|
289
|
+
logger.error(f"Failed to predict: {e}")
|
|
290
|
+
raise
|