anyscale 0.26.42__py3-none-any.whl → 0.26.44__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.
Files changed (39) hide show
  1. anyscale/_private/anyscale_client/common.py +1 -1
  2. anyscale/_private/docgen/__main__.py +2 -0
  3. anyscale/_private/docgen/models.md +2 -2
  4. anyscale/_private/workload/workload_sdk.py +6 -0
  5. anyscale/client/README.md +7 -0
  6. anyscale/client/openapi_client/__init__.py +6 -0
  7. anyscale/client/openapi_client/api/default_api.py +132 -0
  8. anyscale/client/openapi_client/models/__init__.py +6 -0
  9. anyscale/client/openapi_client/models/baseimagesenum.py +68 -1
  10. anyscale/client/openapi_client/models/describe_machine_pool_machines_filters.py +150 -0
  11. anyscale/client/openapi_client/models/describe_machine_pool_machines_request.py +31 -3
  12. anyscale/client/openapi_client/models/describe_machine_pool_requests_request.py +31 -3
  13. anyscale/client/openapi_client/models/describe_machine_pool_workloads_filters.py +150 -0
  14. anyscale/client/openapi_client/models/describe_machine_pool_workloads_request.py +151 -0
  15. anyscale/client/openapi_client/models/file_storage.py +33 -5
  16. anyscale/client/openapi_client/models/supportedbaseimagesenum.py +68 -1
  17. anyscale/client/openapi_client/models/workload_machine_info.py +210 -0
  18. anyscale/client/openapi_client/models/workload_state_info.py +295 -0
  19. anyscale/client/openapi_client/models/workloadstateinfo_list_response.py +147 -0
  20. anyscale/commands/cloud_commands.py +1 -0
  21. anyscale/commands/compute_config_commands.py +10 -3
  22. anyscale/compute_config/__init__.py +16 -0
  23. anyscale/compute_config/_private/compute_config_sdk.py +176 -61
  24. anyscale/compute_config/commands.py +66 -1
  25. anyscale/compute_config/models.py +160 -3
  26. anyscale/conf.py +1 -1
  27. anyscale/controllers/cloud_controller.py +149 -9
  28. anyscale/job/_private/job_sdk.py +22 -0
  29. anyscale/sdk/anyscale_client/models/baseimagesenum.py +68 -1
  30. anyscale/sdk/anyscale_client/models/supportedbaseimagesenum.py +68 -1
  31. anyscale/shared_anyscale_utils/latest_ray_version.py +1 -1
  32. anyscale/version.py +1 -1
  33. {anyscale-0.26.42.dist-info → anyscale-0.26.44.dist-info}/METADATA +16 -16
  34. {anyscale-0.26.42.dist-info → anyscale-0.26.44.dist-info}/RECORD +39 -33
  35. {anyscale-0.26.42.dist-info → anyscale-0.26.44.dist-info}/LICENSE +0 -0
  36. {anyscale-0.26.42.dist-info → anyscale-0.26.44.dist-info}/NOTICE +0 -0
  37. {anyscale-0.26.42.dist-info → anyscale-0.26.44.dist-info}/WHEEL +0 -0
  38. {anyscale-0.26.42.dist-info → anyscale-0.26.44.dist-info}/entry_points.txt +0 -0
  39. {anyscale-0.26.42.dist-info → anyscale-0.26.44.dist-info}/top_level.txt +0 -0
@@ -4,7 +4,7 @@ from typing import Any, Dict, List, Optional, Tuple, Union
4
4
  from anyscale._private.sdk.base_sdk import BaseSDK
5
5
  from anyscale.client.openapi_client.models import (
6
6
  Cloud,
7
- CloudProviders,
7
+ CloudDeploymentComputeConfig,
8
8
  ComputeNodeType,
9
9
  ComputeTemplateConfig,
10
10
  DecoratedComputeTemplate,
@@ -19,6 +19,7 @@ from anyscale.compute_config.models import (
19
19
  ComputeConfigVersion,
20
20
  HeadNodeConfig,
21
21
  MarketType,
22
+ MultiDeploymentComputeConfig,
22
23
  WorkerNodeGroupConfig,
23
24
  )
24
25
  from anyscale.sdk.anyscale_client.models import ClusterComputeConfig
@@ -31,26 +32,6 @@ UNSCHEDULABLE_RESOURCES = Resources(cpu=0, gpu=0)
31
32
 
32
33
 
33
34
  class PrivateComputeConfigSDK(BaseSDK):
34
- def _populate_advanced_instance_config(
35
- self,
36
- config: Union[ComputeConfig, HeadNodeConfig, WorkerNodeGroupConfig],
37
- api_model: Union[ComputeTemplateConfig, ComputeNodeType, WorkerNodeType],
38
- *,
39
- cloud: Cloud,
40
- ):
41
- """Populates the appropriate advanced instance config field of the API model in place."""
42
- if not config.advanced_instance_config:
43
- return
44
-
45
- # Always pass the advanced configuration through `advanced_configurations_json`.
46
- # After fully migrated, we can stop setting the cloud-specific advanced configurations here.
47
- api_model.advanced_configurations_json = config.advanced_instance_config
48
-
49
- if cloud.provider == CloudProviders.AWS:
50
- api_model.aws_advanced_configurations_json = config.advanced_instance_config
51
- elif cloud.provider == CloudProviders.GCP:
52
- api_model.gcp_advanced_configurations_json = config.advanced_instance_config
53
-
54
35
  def _convert_resource_dict_to_api_model(
55
36
  self, resource_dict: Optional[Dict[str, float]]
56
37
  ) -> Optional[Resources]:
@@ -103,18 +84,13 @@ class PrivateComputeConfigSDK(BaseSDK):
103
84
  if config.resources is not None or schedulable_by_default
104
85
  else UNSCHEDULABLE_RESOURCES,
105
86
  flags=flags or None,
106
- )
107
- self._populate_advanced_instance_config(
108
- config, api_model, cloud=cloud,
87
+ advanced_configurations_json=config.advanced_instance_config or None,
109
88
  )
110
89
 
111
90
  return api_model
112
91
 
113
92
  def _convert_worker_node_group_configs_to_api_models(
114
- self,
115
- configs: Optional[List[Union[Dict, WorkerNodeGroupConfig]]],
116
- *,
117
- cloud: Cloud,
93
+ self, configs: Optional[List[Union[Dict, WorkerNodeGroupConfig]]],
118
94
  ) -> Optional[List[WorkerNodeType]]:
119
95
  if configs is None:
120
96
  return None
@@ -139,9 +115,7 @@ class PrivateComputeConfigSDK(BaseSDK):
139
115
  in {MarketType.SPOT, MarketType.PREFER_SPOT},
140
116
  fallback_to_ondemand=config.market_type == MarketType.PREFER_SPOT,
141
117
  flags=flags or None,
142
- )
143
- self._populate_advanced_instance_config(
144
- config, api_model, cloud=cloud,
118
+ advanced_configurations_json=config.advanced_instance_config or None,
145
119
  )
146
120
  api_models.append(api_model)
147
121
 
@@ -149,7 +123,7 @@ class PrivateComputeConfigSDK(BaseSDK):
149
123
 
150
124
  def _convert_compute_config_to_api_model(
151
125
  self, compute_config: ComputeConfig
152
- ) -> ComputeTemplateConfig:
126
+ ) -> CloudDeploymentComputeConfig:
153
127
  # We should only make the head node schedulable when it's the *only* node in the cluster.
154
128
  # `worker_nodes=None` uses the default serverless config, so this only happens if `worker_nodes`
155
129
  # is explicitly set to an empty list.
@@ -172,29 +146,31 @@ class PrivateComputeConfigSDK(BaseSDK):
172
146
  if compute_config.max_resources:
173
147
  flags["max_resources"] = compute_config.max_resources
174
148
 
175
- api_model = ComputeTemplateConfig(
176
- cloud_id=cloud_id,
149
+ return CloudDeploymentComputeConfig(
150
+ cloud_deployment=compute_config.cloud_deployment,
177
151
  allowed_azs=compute_config.zones,
178
- region="",
179
152
  head_node_type=self._convert_head_node_config_to_api_model(
180
153
  compute_config.head_node,
181
154
  cloud=cloud,
182
- schedulable_by_default=compute_config.worker_nodes == [],
155
+ schedulable_by_default=(
156
+ not compute_config.worker_nodes
157
+ and not compute_config.auto_select_worker_config
158
+ ),
183
159
  ),
184
160
  worker_node_types=self._convert_worker_node_group_configs_to_api_models(
185
- compute_config.worker_nodes, cloud=cloud,
161
+ compute_config.worker_nodes,
186
162
  ),
187
163
  auto_select_worker_config=compute_config.auto_select_worker_config,
188
164
  flags=flags,
165
+ advanced_configurations_json=compute_config.advanced_instance_config
166
+ or None,
189
167
  )
190
- self._populate_advanced_instance_config(
191
- compute_config, api_model, cloud=cloud,
192
- )
193
- return api_model
194
168
 
195
169
  def create_compute_config(
196
170
  self, compute_config: ComputeConfig, *, name: Optional[str] = None
197
171
  ) -> Tuple[str, str]:
172
+ """Register the provided compute config and return its internal ID."""
173
+
198
174
  if name is not None:
199
175
  _, version = parse_cluster_compute_name_version(name)
200
176
  if version is not None:
@@ -203,35 +179,101 @@ class PrivateComputeConfigSDK(BaseSDK):
203
179
  "The latest version tag will be generated and returned."
204
180
  )
205
181
 
206
- """Register the provided compute config and return its internal ID."""
207
- compute_config_api_model = self._convert_compute_config_to_api_model(
208
- compute_config
182
+ # Returns the default cloud if user-provided cloud is not specified (`None`).
183
+ cloud_id = self.client.get_cloud_id(cloud_name=compute_config.cloud) # type: ignore
184
+
185
+ deployment_config = self._convert_compute_config_to_api_model(compute_config)
186
+
187
+ compute_config_api_model = ComputeTemplateConfig(
188
+ cloud_id=cloud_id,
189
+ deployment_configs=[deployment_config],
190
+ # For compatibility, continue setting the top-level fields.
191
+ allowed_azs=deployment_config.allowed_azs,
192
+ head_node_type=deployment_config.head_node_type,
193
+ worker_node_types=deployment_config.worker_node_types,
194
+ auto_select_worker_config=deployment_config.auto_select_worker_config,
195
+ flags=deployment_config.flags,
196
+ advanced_configurations_json=deployment_config.advanced_configurations_json
197
+ or None,
209
198
  )
199
+
210
200
  full_name, compute_config_id = self.client.create_compute_config(
211
201
  compute_config_api_model, name=name
212
202
  )
213
203
  self.logger.info(f"Created compute config: '{full_name}'")
214
204
  ui_url = self.client.get_compute_config_ui_url(
215
- compute_config_id, cloud_id=compute_config_api_model.cloud_id
205
+ compute_config_id, cloud_id=cloud_id
216
206
  )
217
207
  self.logger.info(f"View the compute config in the UI: '{ui_url}'")
218
208
  return full_name, compute_config_id
219
209
 
220
- def _convert_api_model_to_advanced_instance_config(
210
+ def create_multi_deployment_compute_config(
221
211
  self,
222
- api_model: Union[DecoratedComputeTemplate, ComputeNodeType, WorkerNodeType],
212
+ compute_config: MultiDeploymentComputeConfig,
223
213
  *,
224
- cloud: Cloud,
214
+ name: Optional[str] = None,
215
+ ) -> Tuple[str, str]:
216
+ """Register the provided multi-deployment compute config and return its internal ID."""
217
+ if name is not None:
218
+ _, version = parse_cluster_compute_name_version(name)
219
+ if version is not None:
220
+ raise ValueError(
221
+ "A version tag cannot be provided when creating a compute config. "
222
+ "The latest version tag will be generated and returned."
223
+ )
224
+
225
+ # Returns the default cloud if user-provided cloud is not specified (`None`).
226
+ cloud_id = self.client.get_cloud_id(cloud_name=compute_config.cloud) # type: ignore
227
+
228
+ # Convert each compute config to the CloudDeploymentComputeConfig API model.
229
+ assert compute_config.configs
230
+ deployment_configs = []
231
+ for config in compute_config.configs:
232
+ assert isinstance(config, ComputeConfig)
233
+ deployment_configs.append(self._convert_compute_config_to_api_model(config))
234
+ default_config = deployment_configs[0]
235
+
236
+ compute_config_api_model = ComputeTemplateConfig(
237
+ cloud_id=cloud_id,
238
+ deployment_configs=deployment_configs,
239
+ # For compatibility, use the first deployment config to set the top-level fields.
240
+ allowed_azs=default_config.allowed_azs,
241
+ head_node_type=default_config.head_node_type,
242
+ worker_node_types=default_config.worker_node_types,
243
+ auto_select_worker_config=default_config.auto_select_worker_config,
244
+ flags=default_config.flags,
245
+ advanced_configurations_json=default_config.advanced_configurations_json
246
+ or None,
247
+ )
248
+ full_name, compute_config_id = self.client.create_compute_config(
249
+ compute_config_api_model, name=name
250
+ )
251
+ self.logger.info(f"Created compute config: '{full_name}'")
252
+
253
+ # TODO(janet): add this back after the UI has been updated to support multi-deployment compute configs.
254
+ # ui_url = self.client.get_compute_config_ui_url(
255
+ # compute_config_id, cloud_id=cloud_id
256
+ # )
257
+ # self.logger.info(f"View the compute config in the UI: '{ui_url}'")
258
+
259
+ return full_name, compute_config_id
260
+
261
+ def _convert_api_model_to_advanced_instance_config(
262
+ self,
263
+ api_model: Union[
264
+ DecoratedComputeTemplateConfig, ComputeNodeType, WorkerNodeType
265
+ ],
225
266
  ) -> Optional[Dict]:
226
267
  if api_model.advanced_configurations_json:
227
268
  return api_model.advanced_configurations_json
228
269
 
229
- if cloud.provider == CloudProviders.AWS:
230
- return api_model.aws_advanced_configurations_json or None
231
- elif cloud.provider == CloudProviders.GCP:
232
- return api_model.gcp_advanced_configurations_json or None
233
- else:
234
- return None
270
+ # Only one of aws_advanced_configurations_json or gcp_advanced_configurations_json will be set.
271
+ if api_model.aws_advanced_configurations_json:
272
+ return api_model.aws_advanced_configurations_json
273
+ if api_model.gcp_advanced_configurations_json:
274
+ return api_model.gcp_advanced_configurations_json
275
+
276
+ return None
235
277
 
236
278
  def _convert_api_model_to_resource_dict(
237
279
  self, resources: Optional[Resources]
@@ -253,7 +295,7 @@ class PrivateComputeConfigSDK(BaseSDK):
253
295
  }
254
296
 
255
297
  def _convert_api_model_to_head_node_config(
256
- self, api_model: ComputeNodeType, *, cloud: Cloud
298
+ self, api_model: ComputeNodeType
257
299
  ) -> HeadNodeConfig:
258
300
  flags: Dict[str, Any] = deepcopy(api_model.flags) or {}
259
301
 
@@ -268,14 +310,14 @@ class PrivateComputeConfigSDK(BaseSDK):
268
310
  instance_type=api_model.instance_type,
269
311
  resources=self._convert_api_model_to_resource_dict(api_model.resources),
270
312
  advanced_instance_config=self._convert_api_model_to_advanced_instance_config(
271
- api_model, cloud=cloud,
313
+ api_model,
272
314
  ),
273
315
  flags=flags or None,
274
316
  cloud_deployment=cloud_deployment,
275
317
  )
276
318
 
277
319
  def _convert_api_models_to_worker_node_group_configs(
278
- self, api_models: List[WorkerNodeType], *, cloud: Cloud
320
+ self, api_models: List[WorkerNodeType]
279
321
  ) -> List[WorkerNodeGroupConfig]:
280
322
  # TODO(edoakes): support advanced_instance_config.
281
323
  configs = []
@@ -315,7 +357,7 @@ class PrivateComputeConfigSDK(BaseSDK):
315
357
  api_model.resources
316
358
  ),
317
359
  advanced_instance_config=self._convert_api_model_to_advanced_instance_config(
318
- api_model, cloud=cloud,
360
+ api_model,
319
361
  ),
320
362
  min_nodes=min_nodes,
321
363
  max_nodes=max_nodes,
@@ -327,6 +369,54 @@ class PrivateComputeConfigSDK(BaseSDK):
327
369
 
328
370
  return configs
329
371
 
372
+ def _convert_cloud_deployment_compute_config_api_model_to_compute_config(
373
+ self, cloud_name: str, api_model: CloudDeploymentComputeConfig,
374
+ ) -> ComputeConfig:
375
+ worker_nodes = None
376
+ if not api_model.auto_select_worker_config:
377
+ if api_model.worker_node_types is not None:
378
+ # Convert worker node types when they are present.
379
+ worker_nodes = self._convert_api_models_to_worker_node_group_configs(
380
+ api_model.worker_node_types
381
+ )
382
+ else:
383
+ # An explicit head-node-only cluster (no worker nodes configured).
384
+ worker_nodes = []
385
+
386
+ zones = None
387
+ # NOTE(edoakes): the API returns '["any"]' if no AZs are passed in on the creation path.
388
+ if api_model.allowed_azs not in [["any"], []]:
389
+ zones = api_model.allowed_azs
390
+
391
+ enable_cross_zone_scaling = False
392
+ flags: Dict[str, Any] = deepcopy(api_model.flags) or {}
393
+ enable_cross_zone_scaling = flags.pop("allow-cross-zone-autoscaling", False)
394
+ min_resources = flags.pop("min_resources", None)
395
+ max_resources = flags.pop("max_resources", None)
396
+ if max_resources is None:
397
+ max_resources = {}
398
+ max_cpus = flags.pop("max-cpus", None)
399
+ if max_cpus:
400
+ max_resources["CPU"] = max_cpus
401
+ max_gpus = flags.pop("max-gpus", None)
402
+ if max_gpus:
403
+ max_resources["GPU"] = max_gpus
404
+
405
+ return ComputeConfig(
406
+ cloud=cloud_name,
407
+ cloud_deployment=api_model.cloud_deployment,
408
+ zones=zones,
409
+ advanced_instance_config=api_model.advanced_configurations_json or None,
410
+ enable_cross_zone_scaling=enable_cross_zone_scaling,
411
+ head_node=self._convert_api_model_to_head_node_config(
412
+ api_model.head_node_type
413
+ ),
414
+ worker_nodes=worker_nodes,
415
+ min_resources=min_resources,
416
+ max_resources=max_resources or None,
417
+ flags=flags,
418
+ )
419
+
330
420
  def _convert_api_model_to_compute_config_version(
331
421
  self, api_model: DecoratedComputeTemplate # noqa: ARG002
332
422
  ) -> ComputeConfigVersion:
@@ -338,12 +428,37 @@ class PrivateComputeConfigSDK(BaseSDK):
338
428
  "This should never happen; please reach out to Anyscale support."
339
429
  )
340
430
 
431
+ configs = None
432
+ if api_model_config.deployment_configs:
433
+ configs = [
434
+ self._convert_cloud_deployment_compute_config_api_model_to_compute_config(
435
+ cloud.name, config
436
+ )
437
+ for config in api_model_config.deployment_configs
438
+ ]
439
+ if len(configs) == 1:
440
+ # If there's only one deployment config, return it directly.
441
+ return ComputeConfigVersion(
442
+ name=f"{api_model.name}:{api_model.version}",
443
+ id=api_model.id,
444
+ config=configs[0],
445
+ )
446
+ return ComputeConfigVersion(
447
+ name=f"{api_model.name}:{api_model.version}",
448
+ id=api_model.id,
449
+ multi_deployment_config=MultiDeploymentComputeConfig(
450
+ cloud=cloud.name, configs=configs,
451
+ ),
452
+ )
453
+
454
+ # If there are no deployment configs, this is a compute config for a single cloud deployment - parse the top-level fields.
455
+
341
456
  worker_nodes = None
342
457
  if not api_model_config.auto_select_worker_config:
343
458
  if api_model_config.worker_node_types is not None:
344
459
  # Convert worker node types when they are present.
345
460
  worker_nodes = self._convert_api_models_to_worker_node_group_configs(
346
- api_model_config.worker_node_types, cloud=cloud,
461
+ api_model_config.worker_node_types
347
462
  )
348
463
  else:
349
464
  # An explicit head-node-only cluster (no worker nodes configured).
@@ -375,11 +490,11 @@ class PrivateComputeConfigSDK(BaseSDK):
375
490
  cloud=cloud.name,
376
491
  zones=zones,
377
492
  advanced_instance_config=self._convert_api_model_to_advanced_instance_config(
378
- api_model_config, cloud=cloud,
493
+ api_model_config
379
494
  ),
380
495
  enable_cross_zone_scaling=enable_cross_zone_scaling,
381
496
  head_node=self._convert_api_model_to_head_node_config(
382
- api_model_config.head_node_type, cloud=cloud
497
+ api_model_config.head_node_type
383
498
  ),
384
499
  worker_nodes=worker_nodes, # type: ignore
385
500
  min_resources=min_resources,
@@ -2,7 +2,11 @@ from typing import Optional
2
2
 
3
3
  from anyscale._private.sdk import sdk_command
4
4
  from anyscale.compute_config._private.compute_config_sdk import PrivateComputeConfigSDK
5
- from anyscale.compute_config.models import ComputeConfig, ComputeConfigVersion
5
+ from anyscale.compute_config.models import (
6
+ ComputeConfig,
7
+ ComputeConfigVersion,
8
+ MultiDeploymentComputeConfig,
9
+ )
6
10
 
7
11
 
8
12
  _COMPUTE_CONFIG_SDK_SINGLETON_KEY = "compute_config_sdk"
@@ -58,6 +62,67 @@ def create(
58
62
  return full_name
59
63
 
60
64
 
65
+ _CREATE_MULTI_DEPLOYMENT_EXAMPLE = """
66
+ import anyscale
67
+ from anyscale.compute_config.models import MultiDeploymentComputeConfig, ComputeConfig, HeadNodeConfig, WorkerNodeGroupConfig
68
+ config = MultiDeploymentComputeConfig(
69
+ configs=[
70
+ ComputeConfig(
71
+ cloud_deployment="vm-aws-us-west-1",
72
+ head_node=HeadNodeConfig(
73
+ instance_type="m5.2xlarge",
74
+ ),
75
+ worker_nodes=[
76
+ WorkerNodeGroupConfig(
77
+ instance_type="m5.4xlarge",
78
+ min_nodes=1,
79
+ max_nodes=10,
80
+ ),
81
+ ],
82
+ ),
83
+ ComputeConfig(
84
+ cloud_deployment="vm-aws-us-west-2",
85
+ head_node=HeadNodeConfig(
86
+ instance_type="m5.2xlarge",
87
+ ),
88
+ worker_nodes=[
89
+ WorkerNodeGroupConfig(
90
+ instance_type="m5.4xlarge",
91
+ min_nodes=1,
92
+ max_nodes=10,
93
+ ),
94
+ ],
95
+ )
96
+ ]
97
+ )
98
+ full_name: str = anyscale.compute_config.create_multi_deployment(config, name="my-compute-config")
99
+ """
100
+
101
+ _CREATE_MULTI_DEPLOYMENT_ARG_DOCSTRINGS = {
102
+ "config": "The config options defining the multi-deployment compute config.",
103
+ "name": "The name of the compute config. This should *not* include a version tag. If a name is not provided, one will be automatically generated.",
104
+ }
105
+
106
+
107
+ @sdk_command(
108
+ _COMPUTE_CONFIG_SDK_SINGLETON_KEY,
109
+ PrivateComputeConfigSDK,
110
+ doc_py_example=_CREATE_MULTI_DEPLOYMENT_EXAMPLE,
111
+ arg_docstrings=_CREATE_MULTI_DEPLOYMENT_ARG_DOCSTRINGS,
112
+ )
113
+ def create_multi_deployment(
114
+ config: MultiDeploymentComputeConfig,
115
+ *,
116
+ name: Optional[str],
117
+ _private_sdk: Optional[PrivateComputeConfigSDK] = None,
118
+ ) -> str:
119
+ """EXPERIMENTAL. Create a new version of a compute config with multiple possible cloud deployments.
120
+ Returns the full name of the registered compute config, including the version.
121
+ """
122
+ full_name, _ = _private_sdk.create_multi_deployment_compute_config(config, name=name) # type: ignore
123
+ return full_name
124
+
125
+
61
126
  _GET_EXAMPLE = """
62
127
  import anyscale
63
128
  from anyscale.compute_config.models import ComputeConfig
@@ -417,6 +417,19 @@ advanced_instance_config: # (Optional) Defaults to no advanced configurations.
417
417
  if cloud is not None and not isinstance(cloud, str):
418
418
  raise TypeError("'cloud' must be a string")
419
419
 
420
+ cloud_deployment: Optional[str] = field(
421
+ default=None,
422
+ repr=False,
423
+ metadata={
424
+ "docstring": "The cloud deployment to use for this workload. Defaults to the primary deployment of the Cloud.",
425
+ "customer_hosted_only": True,
426
+ },
427
+ )
428
+
429
+ def _validate_cloud_deployment(self, cloud_deployment: Optional[str]):
430
+ if cloud_deployment is not None and not isinstance(cloud_deployment, str):
431
+ raise TypeError("'cloud_deployment' must be a string")
432
+
420
433
  head_node: Union[HeadNodeConfig, Dict, None] = field(
421
434
  default=None,
422
435
  repr=False,
@@ -586,6 +599,129 @@ advanced_instance_config: # (Optional) Defaults to no advanced configurations.
586
599
  )
587
600
 
588
601
 
602
+ @dataclass(frozen=True)
603
+ class MultiDeploymentComputeConfig(ModelBase):
604
+ """EXPERIMENTAL. Compute configuration for a cluster with multiple possible cloud deployments."""
605
+
606
+ __doc_py_example__ = """
607
+ from anyscale.compute_config.models import (
608
+ MultiDeploymentComputeConfig, ComputeConfig, HeadNodeConfig, WorkerNodeGroupConfig
609
+ )
610
+ config = MultiDeploymentComputeConfig(
611
+ configs=[
612
+ ComputeConfig(
613
+ cloud_deployment="vm-aws-us-west-1",
614
+ head_node=HeadNodeConfig(
615
+ instance_type="m5.2xlarge",
616
+ ),
617
+ worker_nodes=[
618
+ WorkerNodeGroupConfig(
619
+ instance_type="m5.4xlarge",
620
+ min_nodes=1,
621
+ max_nodes=10,
622
+ ),
623
+ ],
624
+ ),
625
+ ComputeConfig(
626
+ cloud_deployment="vm-aws-us-west-2",
627
+ head_node=HeadNodeConfig(
628
+ instance_type="m5.2xlarge",
629
+ ),
630
+ worker_nodes=[
631
+ WorkerNodeGroupConfig(
632
+ instance_type="m5.4xlarge",
633
+ min_nodes=1,
634
+ max_nodes=10,
635
+ ),
636
+ ],
637
+ )
638
+ ]
639
+ )
640
+ """
641
+
642
+ __doc_yaml_example__ = """
643
+ cloud: my-cloud
644
+ configs:
645
+ - cloud_deployment: vm-aws-us-west-1
646
+ head_node:
647
+ instance_type: m5.2xlarge
648
+ worker_nodes:
649
+ - instance_type: m5.4xlarge
650
+ min_nodes: 1
651
+ max_nodes: 10
652
+ - cloud_deployment: vm-aws-us-west-2
653
+ head_node:
654
+ instance_type: m5.2xlarge
655
+ worker_nodes:
656
+ - instance_type: m5.4xlarge
657
+ min_nodes: 1
658
+ max_nodes: 10
659
+ """
660
+
661
+ cloud: Optional[str] = field(
662
+ default=None,
663
+ metadata={
664
+ "docstring": "The Anyscale Cloud to run this workload on. If not provided, the organization default will be used (or, if running in a workspace, the cloud of the workspace)."
665
+ },
666
+ )
667
+
668
+ def _validate_cloud(self, cloud: Optional[str]):
669
+ if cloud is not None and not isinstance(cloud, str):
670
+ raise TypeError("'cloud' must be a string")
671
+
672
+ configs: List[Union[ComputeConfig, Dict]] = field(
673
+ default_factory=list,
674
+ repr=False,
675
+ metadata={
676
+ "docstring": "List of compute configurations, one for each cloud deployment.",
677
+ "customer_hosted_only": True,
678
+ },
679
+ )
680
+
681
+ def _validate_configs(
682
+ self, configs: List[Union[ComputeConfig, Dict]]
683
+ ) -> List[ComputeConfig]:
684
+ if not isinstance(configs, list) or not all(
685
+ isinstance(c, (dict, ComputeConfig)) for c in configs
686
+ ):
687
+ raise TypeError(
688
+ "'configs' must be a list of ComputeConfigs or corresponding dicts"
689
+ )
690
+
691
+ config_models: List[ComputeConfig] = []
692
+ unique_clouds = set()
693
+ unique_deployments = set()
694
+ for config in configs:
695
+ if isinstance(config, dict):
696
+ config = ComputeConfig.from_dict(config)
697
+
698
+ assert isinstance(config, ComputeConfig)
699
+ config_models.append(config)
700
+
701
+ if not config.cloud_deployment:
702
+ raise ValueError("'cloud_deployment' is required for each config.")
703
+
704
+ if config.cloud:
705
+ unique_clouds.add(config.cloud)
706
+
707
+ unique_deployments.add(config.cloud_deployment)
708
+
709
+ if len(unique_clouds) > 1:
710
+ raise ValueError("'cloud' must be the same for all configs.")
711
+
712
+ if len(unique_deployments) != len(configs):
713
+ raise ValueError(
714
+ "'cloud_deployment' must be unique for each compute configuration."
715
+ )
716
+
717
+ if len(configs) == 0:
718
+ raise ValueError(
719
+ "'configs' must include at least one compute configuration."
720
+ )
721
+
722
+ return config_models
723
+
724
+
589
725
  @dataclass(frozen=True)
590
726
  class ComputeConfigVersion(ModelBase):
591
727
  """Details of a created version of a compute config.
@@ -639,8 +775,29 @@ config:
639
775
  if not isinstance(id, str):
640
776
  raise TypeError("'id' must be a string.")
641
777
 
642
- config: ComputeConfig = field(metadata={"docstring": "The compute configuration."},)
778
+ config: Optional[ComputeConfig] = field(
779
+ default=None, metadata={"docstring": "The compute configuration."},
780
+ )
643
781
 
644
- def _validate_config(self, config: ComputeConfig):
645
- if not isinstance(config, ComputeConfig):
782
+ def _validate_config(self, config: Optional[ComputeConfig]):
783
+ if config is not None and not isinstance(config, ComputeConfig):
646
784
  raise TypeError("'config' must be a ComputeConfig")
785
+
786
+ multi_deployment_config: Optional[MultiDeploymentComputeConfig] = field(
787
+ default=None,
788
+ repr=False,
789
+ metadata={
790
+ "docstring": "Compute configuration for a cluster with multiple possible cloud deployments.",
791
+ "customer_hosted_only": True,
792
+ },
793
+ )
794
+
795
+ def _validate_multi_deployment_config(
796
+ self, multi_deployment_config: Optional[MultiDeploymentComputeConfig]
797
+ ):
798
+ if multi_deployment_config is not None and not isinstance(
799
+ multi_deployment_config, MultiDeploymentComputeConfig
800
+ ):
801
+ raise TypeError(
802
+ "'multi_deployment_config' must be a MultiDeploymentComputeConfig"
803
+ )
anyscale/conf.py CHANGED
@@ -16,7 +16,7 @@ ANYSCALE_IAM_ROLE_NAME = "anyscale-iam-role"
16
16
  # Minimum default Ray version to return when a user either asks for `anyscale.connect required_ray_version`
17
17
  # or when a Default Cluster Env is used
18
18
  # TODO(ilr/nikita) Convert this to a backend call for the most recent Ray Version in the Dataplane!
19
- MINIMUM_RAY_VERSION = "1.7.0"
19
+ MINIMUM_RAY_VERSION = "2.7.0"
20
20
 
21
21
  IDLE_TIMEOUT_DEFAULT_MINUTES = 120
22
22