mirror of
https://github.com/wassname/ray.git
synced 2026-07-30 12:30:30 +08:00
[Autoscaler] New output log format (#12772)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
from collections import defaultdict, namedtuple
|
||||
from collections import defaultdict, namedtuple, Counter
|
||||
from typing import Any, Optional, Dict, List
|
||||
from urllib3.exceptions import MaxRetryError
|
||||
import copy
|
||||
@@ -16,8 +16,10 @@ from ray.experimental.internal_kv import _internal_kv_put, \
|
||||
from ray.autoscaler.tags import (
|
||||
TAG_RAY_LAUNCH_CONFIG, TAG_RAY_RUNTIME_CONFIG,
|
||||
TAG_RAY_FILE_MOUNTS_CONTENTS, TAG_RAY_NODE_STATUS, TAG_RAY_NODE_KIND,
|
||||
TAG_RAY_USER_NODE_TYPE, STATUS_UP_TO_DATE, NODE_KIND_WORKER,
|
||||
NODE_KIND_UNMANAGED, NODE_KIND_HEAD)
|
||||
TAG_RAY_USER_NODE_TYPE, STATUS_UNINITIALIZED, STATUS_WAITING_FOR_SSH,
|
||||
STATUS_SYNCING_FILES, STATUS_SETTING_UP, STATUS_UP_TO_DATE,
|
||||
NODE_KIND_WORKER, NODE_KIND_UNMANAGED, NODE_KIND_HEAD)
|
||||
from ray.autoscaler._private.legacy_info_string import legacy_log_info_string
|
||||
from ray.autoscaler._private.providers import _get_node_provider
|
||||
from ray.autoscaler._private.updater import NodeUpdaterThread
|
||||
from ray.autoscaler._private.node_launcher import NodeLauncher
|
||||
@@ -25,8 +27,8 @@ from ray.autoscaler._private.resource_demand_scheduler import \
|
||||
get_bin_pack_residual, ResourceDemandScheduler, NodeType, NodeID, NodeIP, \
|
||||
ResourceDict
|
||||
from ray.autoscaler._private.util import ConcurrentCounter, validate_config, \
|
||||
with_head_node_ip, hash_launch_conf, hash_runtime_conf, add_prefix, \
|
||||
DEBUG_AUTOSCALING_STATUS, DEBUG_AUTOSCALING_ERROR
|
||||
with_head_node_ip, hash_launch_conf, hash_runtime_conf, \
|
||||
DEBUG_AUTOSCALING_ERROR, format_info_string
|
||||
from ray.autoscaler._private.constants import \
|
||||
AUTOSCALER_MAX_NUM_FAILURES, AUTOSCALER_MAX_LAUNCH_BATCH, \
|
||||
AUTOSCALER_MAX_CONCURRENT_LAUNCHES, AUTOSCALER_UPDATE_INTERVAL_S, \
|
||||
@@ -41,20 +43,23 @@ UpdateInstructions = namedtuple(
|
||||
"UpdateInstructions",
|
||||
["node_id", "init_commands", "start_ray_commands", "docker_config"])
|
||||
|
||||
AutoscalerSummary = namedtuple(
|
||||
"AutoscalerSummary",
|
||||
["active_nodes", "pending_nodes", "pending_launches", "failed_nodes"])
|
||||
|
||||
|
||||
class StandardAutoscaler:
|
||||
"""The autoscaling control loop for a Ray cluster.
|
||||
|
||||
There are two ways to start an autoscaling cluster: manually by running
|
||||
`ray start --head --autoscaling-config=/path/to/config.yaml` on a
|
||||
instance that has permission to launch other instances, or you can also use
|
||||
`ray up /path/to/config.yaml` from your laptop, which will
|
||||
configure the right AWS/Cloud roles automatically.
|
||||
|
||||
StandardAutoscaler's `update` method is periodically called by `monitor.py`
|
||||
to add and remove nodes as necessary. Currently, load-based autoscaling is
|
||||
not implemented, so all this class does is try to maintain a constant
|
||||
cluster size.
|
||||
`ray start --head --autoscaling-config=/path/to/config.yaml` on a instance
|
||||
that has permission to launch other instances, or you can also use `ray up
|
||||
/path/to/config.yaml` from your laptop, which will configure the right
|
||||
AWS/Cloud roles automatically. See the documentation for a full definition
|
||||
of autoscaling behavior:
|
||||
https://docs.ray.io/en/master/cluster/autoscaling.html
|
||||
StandardAutoscaler's `update` method is periodically called in
|
||||
`monitor.py`'s monitoring loop.
|
||||
|
||||
StandardAutoscaler is also used to bootstrap clusters (by adding workers
|
||||
until the cluster size that can handle the resource demand is met).
|
||||
@@ -120,9 +125,6 @@ class StandardAutoscaler:
|
||||
for local_path in self.config["file_mounts"].values():
|
||||
assert os.path.exists(local_path)
|
||||
|
||||
# List of resource bundles the user is requesting of the cluster.
|
||||
self.resource_demand_vector = []
|
||||
|
||||
logger.info("StandardAutoscaler: {}".format(self.config))
|
||||
|
||||
def update(self):
|
||||
@@ -161,7 +163,6 @@ class StandardAutoscaler:
|
||||
self.provider.internal_ip(node_id)
|
||||
for node_id in self.all_workers()
|
||||
])
|
||||
self.log_info_string(nodes)
|
||||
|
||||
# Terminate any idle or out of date nodes
|
||||
last_used = self.load_metrics.last_used_time_by_ip
|
||||
@@ -175,7 +176,7 @@ class StandardAutoscaler:
|
||||
sorted_node_ids = self._sort_based_on_last_used(nodes, last_used)
|
||||
# Don't terminate nodes needed by request_resources()
|
||||
nodes_allowed_to_terminate: Dict[NodeID, bool] = {}
|
||||
if self.resource_demand_vector:
|
||||
if self.load_metrics.get_resource_requests():
|
||||
nodes_allowed_to_terminate = self._get_nodes_allowed_to_terminate(
|
||||
sorted_node_ids)
|
||||
|
||||
@@ -201,7 +202,6 @@ class StandardAutoscaler:
|
||||
if nodes_to_terminate:
|
||||
self.provider.terminate_nodes(nodes_to_terminate)
|
||||
nodes = self.workers()
|
||||
self.log_info_string(nodes)
|
||||
|
||||
# Terminate nodes if there are too many
|
||||
nodes_to_terminate = []
|
||||
@@ -216,8 +216,6 @@ class StandardAutoscaler:
|
||||
self.provider.terminate_nodes(nodes_to_terminate)
|
||||
nodes = self.workers()
|
||||
|
||||
self.log_info_string(nodes)
|
||||
|
||||
to_launch = self.resource_demand_scheduler.get_nodes_to_launch(
|
||||
self.provider.non_terminated_nodes(tag_filters={}),
|
||||
self.pending_launches.breakdown(),
|
||||
@@ -225,7 +223,7 @@ class StandardAutoscaler:
|
||||
self.load_metrics.get_resource_utilization(),
|
||||
self.load_metrics.get_pending_placement_groups(),
|
||||
self.load_metrics.get_static_node_resources_by_ip(),
|
||||
ensure_min_cluster_size=self.resource_demand_vector)
|
||||
ensure_min_cluster_size=self.load_metrics.get_resource_requests())
|
||||
for node_type, count in to_launch.items():
|
||||
self.launch_new_node(count, node_type=node_type)
|
||||
|
||||
@@ -255,7 +253,6 @@ class StandardAutoscaler:
|
||||
self.provider.terminate_nodes(nodes_to_terminate)
|
||||
|
||||
nodes = self.workers()
|
||||
self.log_info_string(nodes)
|
||||
|
||||
# Update nodes with out-of-date files.
|
||||
# TODO(edoakes): Spawning these threads directly seems to cause
|
||||
@@ -281,6 +278,9 @@ class StandardAutoscaler:
|
||||
for node_id in nodes:
|
||||
self.recover_if_needed(node_id, now)
|
||||
|
||||
logger.info(self.info_string())
|
||||
legacy_log_info_string(self, nodes)
|
||||
|
||||
def _sort_based_on_last_used(self, nodes: List[NodeID],
|
||||
last_used: Dict[str, float]) -> List[NodeID]:
|
||||
"""Sort the nodes based on the last time they were used.
|
||||
@@ -361,7 +361,7 @@ class StandardAutoscaler:
|
||||
used_resource_requests: List[ResourceDict]
|
||||
_, used_resource_requests = \
|
||||
get_bin_pack_residual(max_node_resources,
|
||||
self.resource_demand_vector)
|
||||
self.load_metrics.get_resource_requests())
|
||||
# Remove the first entry (the head node).
|
||||
max_node_resources.pop(0)
|
||||
# Remove the first entry (the head node).
|
||||
@@ -533,15 +533,17 @@ class StandardAutoscaler:
|
||||
if not self.can_update(node_id):
|
||||
return
|
||||
key = self.provider.internal_ip(node_id)
|
||||
if key not in self.load_metrics.last_heartbeat_time_by_ip:
|
||||
self.load_metrics.last_heartbeat_time_by_ip[key] = now
|
||||
last_heartbeat_time = self.load_metrics.last_heartbeat_time_by_ip[key]
|
||||
delta = now - last_heartbeat_time
|
||||
if delta < AUTOSCALER_HEARTBEAT_TIMEOUT_S:
|
||||
return
|
||||
|
||||
if key in self.load_metrics.last_heartbeat_time_by_ip:
|
||||
last_heartbeat_time = self.load_metrics.last_heartbeat_time_by_ip[
|
||||
key]
|
||||
delta = now - last_heartbeat_time
|
||||
if delta < AUTOSCALER_HEARTBEAT_TIMEOUT_S:
|
||||
return
|
||||
|
||||
logger.warning("StandardAutoscaler: "
|
||||
"{}: No heartbeat in {}s, "
|
||||
"restarting Ray to recover...".format(node_id, delta))
|
||||
"{}: No recent heartbeat, "
|
||||
"restarting Ray to recover...".format(node_id))
|
||||
updater = NodeUpdaterThread(
|
||||
node_id=node_id,
|
||||
provider_config=self.config["provider"],
|
||||
@@ -678,43 +680,6 @@ class StandardAutoscaler:
|
||||
return self.provider.non_terminated_nodes(
|
||||
tag_filters={TAG_RAY_NODE_KIND: NODE_KIND_UNMANAGED})
|
||||
|
||||
def log_info_string(self, nodes):
|
||||
tmp = "Cluster status: "
|
||||
tmp += self.info_string(nodes)
|
||||
tmp += "\n"
|
||||
tmp += self.load_metrics.info_string()
|
||||
tmp += "\n"
|
||||
tmp += self.resource_demand_scheduler.debug_string(
|
||||
nodes, self.pending_launches.breakdown(),
|
||||
self.load_metrics.get_resource_utilization())
|
||||
if _internal_kv_initialized():
|
||||
_internal_kv_put(DEBUG_AUTOSCALING_STATUS, tmp, overwrite=True)
|
||||
if self.prefix_cluster_info:
|
||||
tmp = add_prefix(tmp, self.config["cluster_name"])
|
||||
logger.debug(tmp)
|
||||
|
||||
def info_string(self, nodes):
|
||||
suffix = ""
|
||||
if self.updaters:
|
||||
suffix += " ({} updating)".format(len(self.updaters))
|
||||
if self.num_failed_updates:
|
||||
suffix += " ({} failed to update)".format(
|
||||
len(self.num_failed_updates))
|
||||
|
||||
return "{} nodes{}".format(len(nodes), suffix)
|
||||
|
||||
def request_resources(self, resources: List[dict]):
|
||||
"""Called by monitor to request resources.
|
||||
|
||||
Args:
|
||||
resources: A list of resource bundles.
|
||||
"""
|
||||
if resources:
|
||||
logger.info(
|
||||
"StandardAutoscaler: resource_requests={}".format(resources))
|
||||
assert isinstance(resources, list), resources
|
||||
self.resource_demand_vector = resources
|
||||
|
||||
def kill_workers(self):
|
||||
logger.error("StandardAutoscaler: kill_workers triggered")
|
||||
nodes = self.workers()
|
||||
@@ -722,3 +687,66 @@ class StandardAutoscaler:
|
||||
self.provider.terminate_nodes(nodes)
|
||||
logger.error("StandardAutoscaler: terminated {} node(s)".format(
|
||||
len(nodes)))
|
||||
|
||||
def summary(self):
|
||||
"""Summarizes the active, pending, and failed node launches.
|
||||
|
||||
An active node is a node whose raylet is actively reporting heartbeats.
|
||||
A pending node is non-active node whose node tag is uninitialized,
|
||||
waiting for ssh, syncing files, or setting up.
|
||||
If a node is not pending or active, it is failed.
|
||||
|
||||
Returns:
|
||||
AutoscalerSummary: The summary.
|
||||
"""
|
||||
all_node_ids = self.provider.non_terminated_nodes(tag_filters={})
|
||||
|
||||
active_nodes = Counter()
|
||||
pending_nodes = []
|
||||
failed_nodes = []
|
||||
|
||||
for node_id in all_node_ids:
|
||||
ip = self.provider.internal_ip(node_id)
|
||||
node_tags = self.provider.node_tags(node_id)
|
||||
if node_tags[TAG_RAY_NODE_KIND] == NODE_KIND_UNMANAGED:
|
||||
continue
|
||||
node_type = node_tags[TAG_RAY_USER_NODE_TYPE]
|
||||
|
||||
# TODO (Alex): If a node's raylet has died, it shouldn't be marked
|
||||
# as active.
|
||||
is_active = self.load_metrics.is_active(ip)
|
||||
if is_active:
|
||||
active_nodes[node_type] += 1
|
||||
else:
|
||||
status = node_tags[TAG_RAY_NODE_STATUS]
|
||||
pending_states = [
|
||||
STATUS_UNINITIALIZED, STATUS_WAITING_FOR_SSH,
|
||||
STATUS_SYNCING_FILES, STATUS_SETTING_UP
|
||||
]
|
||||
is_pending = status in pending_states
|
||||
if is_pending:
|
||||
pending_nodes.append((ip, node_type))
|
||||
else:
|
||||
# TODO (Alex): Failed nodes are now immediately killed, so
|
||||
# this list will almost always be empty. We should ideally
|
||||
# keep a cache of recently failed nodes and their startup
|
||||
# logs.
|
||||
failed_nodes.append((ip, node_type))
|
||||
|
||||
# The concurrent counter leaves some 0 counts in, so we need to
|
||||
# manually filter those out.
|
||||
pending_launches = {}
|
||||
for node_type, count in self.pending_launches.breakdown().items():
|
||||
if count:
|
||||
pending_launches[node_type] = count
|
||||
|
||||
return AutoscalerSummary(
|
||||
active_nodes=active_nodes,
|
||||
pending_nodes=pending_nodes,
|
||||
pending_launches=pending_launches,
|
||||
failed_nodes=failed_nodes)
|
||||
|
||||
def info_string(self):
|
||||
lm_summary = self.load_metrics.summary()
|
||||
autoscaler_summary = self.summary()
|
||||
return "\n" + format_info_string(lm_summary, autoscaler_summary)
|
||||
|
||||
@@ -43,6 +43,10 @@ from ray.worker import global_worker # type: ignore
|
||||
from ray.util.debug import log_once
|
||||
|
||||
import ray.autoscaler._private.subprocess_output_util as cmd_output_util
|
||||
from ray.autoscaler._private.load_metrics import LoadMetricsSummary
|
||||
from ray.autoscaler._private.autoscaler import AutoscalerSummary
|
||||
from ray.autoscaler._private.util import format_info_string, \
|
||||
format_info_string_no_node_types
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -94,6 +98,14 @@ def debug_status() -> str:
|
||||
status = "No cluster status."
|
||||
else:
|
||||
status = status.decode("utf-8")
|
||||
as_dict = json.loads(status)
|
||||
lm_summary = LoadMetricsSummary(**as_dict["load_metrics_report"])
|
||||
if "autoscaler_report" in as_dict:
|
||||
autoscaler_summary = AutoscalerSummary(
|
||||
**as_dict["autoscaler_report"])
|
||||
status = format_info_string(lm_summary, autoscaler_summary)
|
||||
else:
|
||||
status = format_info_string_no_node_types(lm_summary)
|
||||
if error:
|
||||
status += "\n"
|
||||
status += error.decode("utf-8")
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import logging
|
||||
from ray.autoscaler._private.util import DEBUG_AUTOSCALING_STATUS_LEGACY
|
||||
from ray.experimental.internal_kv import _internal_kv_put, \
|
||||
_internal_kv_initialized
|
||||
"""This file provides legacy support for the old info string in order to
|
||||
ensure the dashboard's `api/cluster_status` does not break backwards
|
||||
compatibilty.
|
||||
"""
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def legacy_log_info_string(autoscaler, nodes):
|
||||
tmp = "Cluster status: "
|
||||
tmp += info_string(autoscaler, nodes)
|
||||
tmp += "\n"
|
||||
tmp += autoscaler.load_metrics.info_string()
|
||||
tmp += "\n"
|
||||
tmp += autoscaler.resource_demand_scheduler.debug_string(
|
||||
nodes, autoscaler.pending_launches.breakdown(),
|
||||
autoscaler.load_metrics.get_resource_utilization())
|
||||
if _internal_kv_initialized():
|
||||
_internal_kv_put(DEBUG_AUTOSCALING_STATUS_LEGACY, tmp, overwrite=True)
|
||||
logger.debug(tmp)
|
||||
|
||||
|
||||
def info_string(autoscaler, nodes):
|
||||
suffix = ""
|
||||
if autoscaler.updaters:
|
||||
suffix += " ({} updating)".format(len(autoscaler.updaters))
|
||||
if autoscaler.num_failed_updates:
|
||||
suffix += " ({} failed to update)".format(
|
||||
len(autoscaler.num_failed_updates))
|
||||
|
||||
return "{} nodes{}".format(len(nodes), suffix)
|
||||
@@ -1,16 +1,26 @@
|
||||
from collections import namedtuple
|
||||
from functools import reduce
|
||||
import logging
|
||||
import time
|
||||
from typing import Dict, List
|
||||
|
||||
import numpy as np
|
||||
import ray._private.services as services
|
||||
from ray.autoscaler._private.constants import MEMORY_RESOURCE_UNIT_BYTES
|
||||
from ray.autoscaler._private.constants import MEMORY_RESOURCE_UNIT_BYTES,\
|
||||
AUTOSCALER_MAX_RESOURCE_DEMAND_VECTOR_SIZE
|
||||
from ray.autoscaler._private.util import add_resources, freq_of_dicts
|
||||
from ray.gcs_utils import PlacementGroupTableData
|
||||
from ray.autoscaler._private.resource_demand_scheduler import \
|
||||
NodeIP, ResourceDict
|
||||
from ray.core.generated.common_pb2 import PlacementStrategy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
LoadMetricsSummary = namedtuple("LoadMetricsSummary", [
|
||||
"head_ip", "usage", "resource_demand", "pg_demand", "request_demand",
|
||||
"node_types"
|
||||
])
|
||||
|
||||
|
||||
class LoadMetrics:
|
||||
"""Container for cluster load metrics.
|
||||
@@ -31,6 +41,7 @@ class LoadMetrics:
|
||||
self.waiting_bundles = []
|
||||
self.infeasible_bundles = []
|
||||
self.pending_placement_groups = []
|
||||
self.resource_requests = []
|
||||
|
||||
def update(self,
|
||||
ip: str,
|
||||
@@ -72,9 +83,12 @@ class LoadMetrics:
|
||||
|
||||
def mark_active(self, ip):
|
||||
assert ip is not None, "IP should be known at this time"
|
||||
logger.info("Node {} is newly setup, treating as active".format(ip))
|
||||
logger.debug("Node {} is newly setup, treating as active".format(ip))
|
||||
self.last_heartbeat_time_by_ip[ip] = time.time()
|
||||
|
||||
def is_active(self, ip):
|
||||
return ip in self.last_heartbeat_time_by_ip
|
||||
|
||||
def prune_active_ips(self, active_ips):
|
||||
active_ips = set(active_ips)
|
||||
active_ips.add(self.local_ip)
|
||||
@@ -155,12 +169,82 @@ class LoadMetrics:
|
||||
|
||||
return resources_used, resources_total
|
||||
|
||||
def get_resource_demand_vector(self):
|
||||
return self.waiting_bundles + self.infeasible_bundles
|
||||
def get_resource_demand_vector(self, clip=True):
|
||||
if clip:
|
||||
# Bound the total number of bundles to
|
||||
# 2xMAX_RESOURCE_DEMAND_VECTOR_SIZE. This guarantees the resource
|
||||
# demand scheduler bin packing algorithm takes a reasonable amount
|
||||
# of time to run.
|
||||
return (
|
||||
self.
|
||||
waiting_bundles[:AUTOSCALER_MAX_RESOURCE_DEMAND_VECTOR_SIZE] +
|
||||
self.
|
||||
infeasible_bundles[:AUTOSCALER_MAX_RESOURCE_DEMAND_VECTOR_SIZE]
|
||||
)
|
||||
else:
|
||||
return self.waiting_bundles + self.infeasible_bundles
|
||||
|
||||
def get_resource_requests(self):
|
||||
return self.resource_requests
|
||||
|
||||
def get_pending_placement_groups(self):
|
||||
return self.pending_placement_groups
|
||||
|
||||
def summary(self):
|
||||
available_resources = reduce(add_resources,
|
||||
self.dynamic_resources_by_ip.values()
|
||||
) if self.dynamic_resources_by_ip else {}
|
||||
total_resources = reduce(add_resources,
|
||||
self.static_resources_by_ip.values()
|
||||
) if self.static_resources_by_ip else {}
|
||||
usage_dict = {}
|
||||
for key in total_resources:
|
||||
total = total_resources[key]
|
||||
usage_dict[key] = (total - available_resources[key], total)
|
||||
|
||||
summarized_demand_vector = freq_of_dicts(
|
||||
self.get_resource_demand_vector(clip=False))
|
||||
summarized_resource_requests = freq_of_dicts(
|
||||
self.get_resource_requests())
|
||||
|
||||
def placement_group_serializer(pg):
|
||||
bundles = tuple(
|
||||
frozenset(bundle.unit_resources.items())
|
||||
for bundle in pg.bundles)
|
||||
return (bundles, pg.strategy)
|
||||
|
||||
def placement_group_deserializer(pg_tuple):
|
||||
# We marshal this as a dictionary so that we can easily json.dumps
|
||||
# it later.
|
||||
# TODO (Alex): Would there be a benefit to properly
|
||||
# marshalling this (into a protobuf)?
|
||||
bundles = list(map(dict, pg_tuple[0]))
|
||||
return {
|
||||
"bundles": freq_of_dicts(bundles),
|
||||
"strategy": PlacementStrategy.Name(pg_tuple[1])
|
||||
}
|
||||
|
||||
summarized_placement_groups = freq_of_dicts(
|
||||
self.get_pending_placement_groups(),
|
||||
serializer=placement_group_serializer,
|
||||
deserializer=placement_group_deserializer)
|
||||
nodes_summary = freq_of_dicts(self.static_resources_by_ip.values())
|
||||
|
||||
return LoadMetricsSummary(
|
||||
head_ip=self.local_ip,
|
||||
usage=usage_dict,
|
||||
resource_demand=summarized_demand_vector,
|
||||
pg_demand=summarized_placement_groups,
|
||||
request_demand=summarized_resource_requests,
|
||||
node_types=nodes_summary)
|
||||
|
||||
def set_resource_requests(self, requested_resources):
|
||||
if requested_resources is not None:
|
||||
assert isinstance(requested_resources, list), requested_resources
|
||||
self.resource_requests = [
|
||||
request for request in requested_resources if len(request) > 0
|
||||
]
|
||||
|
||||
def info_string(self):
|
||||
return " - " + "\n - ".join(
|
||||
["{}: {}".format(k, v) for k, v in sorted(self._info().items())])
|
||||
|
||||
@@ -149,8 +149,8 @@ class ResourceDemandScheduler:
|
||||
node_resources, node_type_counts = self.calculate_node_resources(
|
||||
nodes, launching_nodes, unused_resources_by_ip)
|
||||
|
||||
logger.info("Cluster resources: {}".format(node_resources))
|
||||
logger.info("Node counts: {}".format(node_type_counts))
|
||||
logger.debug("Cluster resources: {}".format(node_resources))
|
||||
logger.debug("Node counts: {}".format(node_type_counts))
|
||||
# Step 2: add nodes to add to satisfy min_workers for each type
|
||||
(node_resources,
|
||||
node_type_counts,
|
||||
@@ -160,7 +160,7 @@ class ResourceDemandScheduler:
|
||||
self.max_workers, self.head_node_type, ensure_min_cluster_size)
|
||||
|
||||
# Step 3: add nodes for strict spread groups
|
||||
logger.info(f"Placement group demands: {pending_placement_groups}")
|
||||
logger.debug(f"Placement group demands: {pending_placement_groups}")
|
||||
placement_group_demand_vector, strict_spreads = \
|
||||
placement_groups_to_resource_demands(pending_placement_groups)
|
||||
resource_demands.extend(placement_group_demand_vector)
|
||||
@@ -187,8 +187,8 @@ class ResourceDemandScheduler:
|
||||
# groups
|
||||
unfulfilled, _ = get_bin_pack_residual(node_resources,
|
||||
resource_demands)
|
||||
logger.info("Resource demands: {}".format(resource_demands))
|
||||
logger.info("Unfulfilled demands: {}".format(unfulfilled))
|
||||
logger.debug("Resource demands: {}".format(resource_demands))
|
||||
logger.debug("Unfulfilled demands: {}".format(unfulfilled))
|
||||
# Add 1 to account for the head node.
|
||||
max_to_add = self.max_workers + 1 - sum(node_type_counts.values())
|
||||
nodes_to_add_based_on_demand = get_nodes_for(
|
||||
@@ -211,7 +211,7 @@ class ResourceDemandScheduler:
|
||||
total_nodes_to_add, unused_resources_by_ip.keys(), nodes,
|
||||
launching_nodes, adjusted_min_workers)
|
||||
|
||||
logger.info("Node requests: {}".format(total_nodes_to_add))
|
||||
logger.debug("Node requests: {}".format(total_nodes_to_add))
|
||||
return total_nodes_to_add
|
||||
|
||||
def _legacy_worker_node_to_launch(
|
||||
@@ -615,8 +615,14 @@ def get_nodes_for(node_types: Dict[NodeType, NodeTypeConfigDict],
|
||||
# starts up because placement groups are scheduled via custom
|
||||
# resources. This will behave properly with the current utilization
|
||||
# score heuristic, but it's a little dangerous and misleading.
|
||||
logger.info(
|
||||
"No feasible node type to add for {}".format(resources))
|
||||
logger.warning(
|
||||
f"The autoscaler could not find a node type to satisfy the"
|
||||
f"request: {resources}. If this request is related to "
|
||||
f"placement groups the resource request will resolve itself, "
|
||||
f"otherwise please specify a node type with the necessary "
|
||||
f"resource "
|
||||
f"https://docs.ray.io/en/master/cluster/autoscaling.html#multiple-node-type-autoscaling." # noqa: E501
|
||||
)
|
||||
break
|
||||
|
||||
utilization_scores = sorted(utilization_scores, reverse=True)
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import collections
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import hashlib
|
||||
import json
|
||||
import jsonschema
|
||||
import os
|
||||
import threading
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import ray
|
||||
import ray.ray_constants
|
||||
import ray._private.services as services
|
||||
from ray.autoscaler._private.providers import _get_default_config
|
||||
from ray.autoscaler._private.docker import validate_docker_config
|
||||
@@ -20,6 +22,7 @@ RAY_SCHEMA_PATH = os.path.join(
|
||||
# Internal kv keys for storing debug status.
|
||||
DEBUG_AUTOSCALING_ERROR = "__autoscaling_error"
|
||||
DEBUG_AUTOSCALING_STATUS = "__autoscaling_status"
|
||||
DEBUG_AUTOSCALING_STATUS_LEGACY = "__autoscaling_status_legacy"
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -246,6 +249,47 @@ def hash_runtime_conf(file_mounts,
|
||||
return (_hash_cache[conf_str], file_mounts_contents_hash)
|
||||
|
||||
|
||||
def add_resources(dict1: Dict[str, float],
|
||||
dict2: Dict[str, float]) -> Dict[str, float]:
|
||||
"""Add the values in two dictionaries.
|
||||
|
||||
Returns:
|
||||
dict: A new dictionary (inputs remain unmodified).
|
||||
"""
|
||||
new_dict = dict1.copy()
|
||||
for k, v in dict2.items():
|
||||
new_dict[k] = v + new_dict.get(k, 0)
|
||||
return new_dict
|
||||
|
||||
|
||||
def freq_of_dicts(dicts: List[Dict],
|
||||
serializer=lambda d: frozenset(d.items()),
|
||||
deserializer=dict):
|
||||
"""Count a list of dictionaries (or unhashable types).
|
||||
|
||||
This is somewhat annoying because mutable data structures aren't hashable,
|
||||
and set/dict keys must be hashable.
|
||||
|
||||
Args:
|
||||
dicts (List[D]): A list of dictionaries to be counted.
|
||||
serializer (D -> S): A custom serailization function. The output type S
|
||||
must be hashable. The default serializer converts a dictionary into
|
||||
a frozenset of KV pairs.
|
||||
deserializer (S -> U): A custom deserialization function. See the
|
||||
serializer for information about type S. For dictionaries U := D.
|
||||
|
||||
Returns:
|
||||
List[Tuple[U, int]]: Returns a list of tuples. Each entry in the list
|
||||
is a tuple containing a unique entry from `dicts` and its
|
||||
corresponding frequency count.
|
||||
"""
|
||||
freqs = collections.Counter(map(lambda d: serializer(d), dicts))
|
||||
as_list = []
|
||||
for as_set, count in freqs.items():
|
||||
as_list.append((deserializer(as_set), count))
|
||||
return as_list
|
||||
|
||||
|
||||
def add_prefix(info_string, prefix):
|
||||
"""Prefixes each line of info_string, except the first, by prefix."""
|
||||
lines = info_string.split("\n")
|
||||
@@ -255,3 +299,132 @@ def add_prefix(info_string, prefix):
|
||||
prefixed_lines.append(prefixed_line)
|
||||
prefixed_info_string = "\n".join(prefixed_lines)
|
||||
return prefixed_info_string
|
||||
|
||||
|
||||
def format_pg(pg):
|
||||
strategy = pg["strategy"]
|
||||
bundles = pg["bundles"]
|
||||
shape_strs = []
|
||||
for bundle, count in bundles:
|
||||
shape_strs.append(f"{bundle} * {count}")
|
||||
bundles_str = ", ".join(shape_strs)
|
||||
return f"{bundles_str} ({strategy})"
|
||||
|
||||
|
||||
def get_usage_report(lm_summary):
|
||||
usage_lines = []
|
||||
for resource, (used, total) in lm_summary.usage.items():
|
||||
line = f" {used}/{total} {resource}"
|
||||
if resource in ["memory", "object_store_memory"]:
|
||||
to_GiB = ray.ray_constants.MEMORY_RESOURCE_UNIT_BYTES / 2**30
|
||||
used *= to_GiB
|
||||
total *= to_GiB
|
||||
line = f" {used:.2f}/{total:.3f} GiB {resource}"
|
||||
usage_lines.append(line)
|
||||
usage_report = "\n".join(usage_lines)
|
||||
return usage_report
|
||||
|
||||
|
||||
def get_demand_report(lm_summary):
|
||||
demand_lines = []
|
||||
for bundle, count in lm_summary.resource_demand:
|
||||
line = f" {bundle}: {count}+ pending tasks/actors"
|
||||
demand_lines.append(line)
|
||||
for entry in lm_summary.pg_demand:
|
||||
pg, count = entry
|
||||
pg_str = format_pg(pg)
|
||||
line = f" {pg_str}: {count}+ pending placement groups"
|
||||
demand_lines.append(line)
|
||||
for bundle, count in lm_summary.request_demand:
|
||||
line = f" {bundle}: {count}+ from request_resources()"
|
||||
demand_lines.append(line)
|
||||
if len(demand_lines) > 0:
|
||||
demand_report = "\n".join(demand_lines)
|
||||
else:
|
||||
demand_report = " (no resource demands)"
|
||||
return demand_report
|
||||
|
||||
|
||||
def format_info_string(lm_summary, autoscaler_summary, time=None):
|
||||
if time is None:
|
||||
time = datetime.now()
|
||||
header = "=" * 8 + f" Autoscaler status: {time} " + "=" * 8
|
||||
separator = "-" * len(header)
|
||||
available_node_report_lines = []
|
||||
for node_type, count in autoscaler_summary.active_nodes.items():
|
||||
line = f" {count} {node_type}"
|
||||
available_node_report_lines.append(line)
|
||||
available_node_report = "\n".join(available_node_report_lines)
|
||||
|
||||
pending_lines = []
|
||||
for node_type, count in autoscaler_summary.pending_launches.items():
|
||||
line = f" {node_type}, {count} launching"
|
||||
pending_lines.append(line)
|
||||
for ip, node_type in autoscaler_summary.pending_nodes:
|
||||
line = f" {ip}: {node_type}, setting up"
|
||||
pending_lines.append(line)
|
||||
if pending_lines:
|
||||
pending_report = "\n".join(pending_lines)
|
||||
else:
|
||||
pending_report = " (no pending nodes)"
|
||||
|
||||
failure_lines = []
|
||||
for ip, node_type in autoscaler_summary.failed_nodes:
|
||||
line = f" {ip}: {node_type}"
|
||||
failure_report = "Recent failures:\n"
|
||||
if failure_lines:
|
||||
failure_report += "\n".join(failure_lines)
|
||||
else:
|
||||
failure_report += " (no failures)"
|
||||
|
||||
usage_report = get_usage_report(lm_summary)
|
||||
demand_report = get_demand_report(lm_summary)
|
||||
|
||||
formatted_output = f"""{header}
|
||||
Node status
|
||||
{separator}
|
||||
Healthy:
|
||||
{available_node_report}
|
||||
Pending:
|
||||
{pending_report}
|
||||
{failure_report}
|
||||
|
||||
Resources
|
||||
{separator}
|
||||
|
||||
Usage:
|
||||
{usage_report}
|
||||
|
||||
Demands:
|
||||
{demand_report}"""
|
||||
return formatted_output
|
||||
|
||||
|
||||
def format_info_string_no_node_types(lm_summary, time=None):
|
||||
if time is None:
|
||||
time = datetime.now()
|
||||
header = "=" * 8 + f" Cluster status: {time} " + "=" * 8
|
||||
separator = "-" * len(header)
|
||||
|
||||
node_lines = []
|
||||
for node_type, count in lm_summary.node_types:
|
||||
line = f" {count} node(s) with resources: {node_type}"
|
||||
node_lines.append(line)
|
||||
node_report = "\n".join(node_lines)
|
||||
|
||||
usage_report = get_usage_report(lm_summary)
|
||||
demand_report = get_demand_report(lm_summary)
|
||||
|
||||
formatted_output = f"""{header}
|
||||
Node status
|
||||
{separator}
|
||||
{node_report}
|
||||
|
||||
Resources
|
||||
{separator}
|
||||
Usage:
|
||||
{usage_report}
|
||||
|
||||
Demands:
|
||||
{demand_report}"""
|
||||
return formatted_output
|
||||
|
||||
Reference in New Issue
Block a user