66from contextlib import contextmanager
77from typing import Any , Callable , Dict , Iterator , List , Optional , Union
88
9+ from ray ._private .label_utils import validate_label_selector
910from ray .autoscaler ._private import commands
1011from ray .autoscaler ._private .cli_logger import cli_logger
1112from ray .autoscaler ._private .event_system import (
@@ -21,7 +22,7 @@ def create_or_update_cluster(
2122 * ,
2223 no_restart : bool = False ,
2324 restart_only : bool = False ,
24- no_config_cache : bool = False
25+ no_config_cache : bool = False ,
2526) -> Dict [str , Any ]:
2627 """Create or updates an autoscaling Ray cluster from a config json.
2728
@@ -87,7 +88,7 @@ def run_on_cluster(
8788 stop : bool = False ,
8889 no_config_cache : bool = False ,
8990 port_forward : Optional [commands .Port_forward ] = None ,
90- with_output : bool = False
91+ with_output : bool = False ,
9192) -> Optional [str ]:
9293 """Runs a command on the specified cluster.
9394
@@ -133,7 +134,7 @@ def rsync(
133134 ip_address : Optional [str ] = None ,
134135 use_internal_ip : bool = False ,
135136 no_config_cache : bool = False ,
136- should_bootstrap : bool = True
137+ should_bootstrap : bool = True ,
137138):
138139 """Rsyncs files to or from the cluster.
139140
@@ -206,7 +207,9 @@ def get_worker_node_ips(cluster_config: Union[dict, str]) -> List[str]:
206207
207208@DeveloperAPI
208209def request_resources (
209- num_cpus : Optional [int ] = None , bundles : Optional [List [dict ]] = None
210+ num_cpus : Optional [int ] = None ,
211+ bundles : Optional [List [dict ]] = None ,
212+ bundle_label_selectors : Optional [List [dict ]] = None ,
210213) -> None :
211214 """Command the autoscaler to scale to accommodate the specified requests.
212215
@@ -230,6 +233,11 @@ def request_resources(
230233 bundles (List[ResourceDict]): Scale the cluster to ensure this set of
231234 resource shapes can fit. This request is persistent until another
232235 call to request_resources() is made to override.
236+ bundle_label_selectors: A list of label selectors, applied per-bundle to the same
237+ index in the `bundles` list. For bundles without a label requirement, the
238+ corresponding item in the list is an empty dictionary. For each bundle.
239+ Label selectors consist of zero or more key-value pairs where the key is
240+ a label and the value is a operator (in, !in, etc.) and label value.
233241
234242 Examples:
235243 >>> from ray.autoscaler.sdk import request_resources
@@ -241,6 +249,13 @@ def request_resources(
241249 >>> # Same as requesting num_cpus=3.
242250 >>> request_resources( # doctest: +SKIP
243251 ... bundles=[{"CPU": 1}, {"CPU": 1}, {"CPU": 1}])
252+ >>> # Requests 2 num_cpus=1 bundles, the first with
253+ >>> # label_selector={"accelerator-type": "in(A100)"} and second with
254+ >>> # label_selector={"market-type": "spot"}.
255+ >>> request_resources( # doctest: +SKIP
256+ ... bundles=[{"CPU": 1}, {"CPU": 1}]),
257+ ... bundle_label_selectors=[{"accelerator-type": "in(A100)"},
258+ ... {"market-type": "spot"}])
244259 """
245260 if num_cpus is not None and not isinstance (num_cpus , int ):
246261 raise TypeError ("num_cpus should be of type int." )
@@ -257,8 +272,34 @@ def request_resources(
257272 raise TypeError ("each bundle should be a Dict." )
258273 else :
259274 raise TypeError ("bundles should be of type List" )
260-
261- return commands .request_resources (num_cpus , bundles )
275+ if bundle_label_selectors is not None :
276+ if bundles is None :
277+ raise ValueError (
278+ "`bundles` must be provided when `bundle_label_selectors` is specified."
279+ )
280+ if len (bundle_label_selectors ) != len (bundles ):
281+ raise ValueError (
282+ "`bundle_label_selector` must be a list with length equal to the number of bundles."
283+ )
284+ for label_selector in bundle_label_selectors :
285+ if (
286+ not isinstance (label_selector , dict )
287+ or not all (isinstance (k , str ) for k in label_selector .keys ())
288+ or not all (isinstance (v , str ) for v in label_selector .values ())
289+ ):
290+ raise ValueError (
291+ "Bundle label selector must be a list of string dictionary"
292+ " label selectors. For example: "
293+ '`[{ray.io/market_type": "spot"}, {"ray.io/accelerator-type": "A100"}]`.'
294+ )
295+ error_message = validate_label_selector (label_selector )
296+ if error_message :
297+ raise ValueError (
298+ f"Invalid label selector provided in bundle_label_selectors list."
299+ f" Detailed error: '{ error_message } '"
300+ )
301+
302+ return commands .request_resources (num_cpus , bundles , bundle_label_selectors )
262303
263304
264305@DeveloperAPI
0 commit comments