chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
import json
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from typing import Any, Dict, List, Optional
|
||||
from zlib import crc32
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ray.serve._private.config import DeploymentConfig
|
||||
from ray.serve._private.constants import SERVE_LOGGER_NAME
|
||||
from ray.serve._private.utils import DeploymentOptionUpdateType, get_random_string
|
||||
from ray.serve.config import AutoscalingConfig
|
||||
from ray.serve.generated.serve_pb2 import DeploymentVersion as DeploymentVersionProto
|
||||
|
||||
logger = logging.getLogger(SERVE_LOGGER_NAME)
|
||||
|
||||
|
||||
class DeploymentVersion:
|
||||
def __init__(
|
||||
self,
|
||||
code_version: Optional[str],
|
||||
deployment_config: DeploymentConfig,
|
||||
ray_actor_options: Optional[Dict],
|
||||
placement_group_bundles: Optional[List[Dict[str, float]]] = None,
|
||||
placement_group_strategy: Optional[str] = None,
|
||||
placement_group_bundle_label_selector: Optional[List[Dict[str, str]]] = None,
|
||||
placement_group_fallback_strategy: Optional[List[Dict[str, Any]]] = None,
|
||||
max_replicas_per_node: Optional[int] = None,
|
||||
route_prefix: Optional[str] = None,
|
||||
):
|
||||
if code_version is not None and not isinstance(code_version, str):
|
||||
raise TypeError(f"code_version must be str, got {type(code_version)}.")
|
||||
if code_version is None:
|
||||
self.code_version = get_random_string()
|
||||
else:
|
||||
self.code_version = code_version
|
||||
|
||||
# Options for this field may be mutated over time, so any logic that uses this
|
||||
# should access this field directly.
|
||||
self.deployment_config = deployment_config
|
||||
self.ray_actor_options = ray_actor_options
|
||||
self.placement_group_bundles = placement_group_bundles
|
||||
self.placement_group_strategy = placement_group_strategy
|
||||
self.placement_group_bundle_label_selector = (
|
||||
placement_group_bundle_label_selector
|
||||
)
|
||||
self.placement_group_fallback_strategy = placement_group_fallback_strategy
|
||||
self.max_replicas_per_node = max_replicas_per_node
|
||||
self.route_prefix = route_prefix
|
||||
self.compute_hashes()
|
||||
|
||||
@classmethod
|
||||
def from_deployment_version(
|
||||
cls, deployment_version, deployment_config, route_prefix: Optional[str] = None
|
||||
):
|
||||
version_copy = deepcopy(deployment_version)
|
||||
version_copy.deployment_config = deployment_config
|
||||
version_copy.route_prefix = route_prefix
|
||||
version_copy.compute_hashes()
|
||||
return version_copy
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return self._hash
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
if not isinstance(other, DeploymentVersion):
|
||||
return False
|
||||
return self._hash == other._hash
|
||||
|
||||
def requires_actor_restart(self, new_version):
|
||||
"""Determines whether the new version requires actors of the current version to
|
||||
be restarted.
|
||||
"""
|
||||
return (
|
||||
self.code_version != new_version.code_version
|
||||
or self.ray_actor_options_hash != new_version.ray_actor_options_hash
|
||||
or self.placement_group_options_hash
|
||||
!= new_version.placement_group_options_hash
|
||||
or self.max_replicas_per_node != new_version.max_replicas_per_node
|
||||
or self.gang_scheduling_config_hash
|
||||
!= new_version.gang_scheduling_config_hash
|
||||
)
|
||||
|
||||
def requires_actor_reconfigure(self, new_version):
|
||||
"""Determines whether the new version requires calling reconfigure() on the
|
||||
replica actor.
|
||||
"""
|
||||
return self.reconfigure_actor_hash != new_version.reconfigure_actor_hash
|
||||
|
||||
def requires_long_poll_broadcast(self, new_version):
|
||||
"""Determines whether lightweightly updating an existing replica to the new
|
||||
version requires broadcasting through long poll that the running replicas has
|
||||
changed.
|
||||
"""
|
||||
return (
|
||||
self.deployment_config.max_ongoing_requests
|
||||
!= new_version.deployment_config.max_ongoing_requests
|
||||
)
|
||||
|
||||
def compute_hashes(self):
|
||||
# If these change, the controller will rolling upgrade existing replicas.
|
||||
serialized_ray_actor_options = _serialize(self.ray_actor_options or {})
|
||||
self.ray_actor_options_hash = crc32(serialized_ray_actor_options)
|
||||
combined_placement_group_options = {}
|
||||
if self.placement_group_bundles is not None:
|
||||
combined_placement_group_options["bundles"] = self.placement_group_bundles
|
||||
if self.placement_group_strategy is not None:
|
||||
combined_placement_group_options["strategy"] = self.placement_group_strategy
|
||||
if self.placement_group_bundle_label_selector is not None:
|
||||
combined_placement_group_options[
|
||||
"bundle_label_selector"
|
||||
] = self.placement_group_bundle_label_selector
|
||||
if self.placement_group_fallback_strategy is not None:
|
||||
combined_placement_group_options[
|
||||
"fallback_strategy"
|
||||
] = self.placement_group_fallback_strategy
|
||||
serialized_placement_group_options = _serialize(
|
||||
combined_placement_group_options
|
||||
)
|
||||
self.placement_group_options_hash = crc32(serialized_placement_group_options)
|
||||
serialized_gang_scheduling_config = _serialize(
|
||||
self.deployment_config.gang_scheduling_config.model_dump()
|
||||
if self.deployment_config.gang_scheduling_config is not None
|
||||
else {}
|
||||
)
|
||||
self.gang_scheduling_config_hash = crc32(serialized_gang_scheduling_config)
|
||||
# Include app-level route prefix in the version hashes so changing
|
||||
# it triggers an in-place reconfigure of running replicas.
|
||||
serialized_route_prefix = _serialize(self.route_prefix)
|
||||
|
||||
# If this changes, DeploymentReplica.reconfigure() will call reconfigure on the
|
||||
# actual replica actor
|
||||
self.reconfigure_actor_hash = crc32(
|
||||
serialized_route_prefix
|
||||
+ self._get_serialized_options(
|
||||
[DeploymentOptionUpdateType.NeedsActorReconfigure]
|
||||
)
|
||||
)
|
||||
|
||||
# Used by __eq__ in deployment state to either reconfigure the replicas or
|
||||
# stop and restart them
|
||||
self._hash = crc32(
|
||||
self.code_version.encode("utf-8")
|
||||
+ serialized_ray_actor_options
|
||||
+ serialized_placement_group_options
|
||||
+ str(self.max_replicas_per_node).encode("utf-8")
|
||||
+ serialized_route_prefix
|
||||
+ self._get_serialized_options(
|
||||
[
|
||||
DeploymentOptionUpdateType.NeedsReconfigure,
|
||||
DeploymentOptionUpdateType.NeedsActorReconfigure,
|
||||
]
|
||||
)
|
||||
+ serialized_gang_scheduling_config
|
||||
)
|
||||
|
||||
def to_proto(self) -> bytes:
|
||||
# TODO(simon): enable cross language user config
|
||||
|
||||
placement_group_bundles = (
|
||||
json.dumps(self.placement_group_bundles)
|
||||
if self.placement_group_bundles is not None
|
||||
else ""
|
||||
)
|
||||
|
||||
placement_group_bundle_label_selector = (
|
||||
json.dumps(self.placement_group_bundle_label_selector)
|
||||
if self.placement_group_bundle_label_selector is not None
|
||||
else ""
|
||||
)
|
||||
|
||||
placement_group_fallback_strategy = (
|
||||
json.dumps(self.placement_group_fallback_strategy)
|
||||
if self.placement_group_fallback_strategy is not None
|
||||
else ""
|
||||
)
|
||||
|
||||
placement_group_strategy = (
|
||||
self.placement_group_strategy
|
||||
if self.placement_group_strategy is not None
|
||||
else ""
|
||||
)
|
||||
max_replicas_per_node = (
|
||||
self.max_replicas_per_node if self.max_replicas_per_node is not None else 0
|
||||
)
|
||||
|
||||
return DeploymentVersionProto(
|
||||
code_version=self.code_version,
|
||||
deployment_config=self.deployment_config.to_proto(),
|
||||
ray_actor_options=json.dumps(self.ray_actor_options),
|
||||
placement_group_bundles=placement_group_bundles,
|
||||
placement_group_strategy=placement_group_strategy,
|
||||
placement_group_bundle_label_selector=placement_group_bundle_label_selector,
|
||||
placement_group_fallback_strategy=placement_group_fallback_strategy,
|
||||
max_replicas_per_node=max_replicas_per_node,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_proto(cls, proto: DeploymentVersionProto):
|
||||
return DeploymentVersion(
|
||||
proto.code_version,
|
||||
DeploymentConfig.from_proto(proto.deployment_config),
|
||||
json.loads(proto.ray_actor_options),
|
||||
placement_group_bundles=(
|
||||
json.loads(proto.placement_group_bundles)
|
||||
if proto.placement_group_bundles
|
||||
else None
|
||||
),
|
||||
placement_group_bundle_label_selector=(
|
||||
json.loads(proto.placement_group_bundle_label_selector)
|
||||
if proto.placement_group_bundle_label_selector
|
||||
else None
|
||||
),
|
||||
placement_group_fallback_strategy=(
|
||||
json.loads(proto.placement_group_fallback_strategy)
|
||||
if proto.placement_group_fallback_strategy
|
||||
else None
|
||||
),
|
||||
placement_group_strategy=(
|
||||
proto.placement_group_strategy
|
||||
if proto.placement_group_strategy
|
||||
else None
|
||||
),
|
||||
max_replicas_per_node=(
|
||||
proto.max_replicas_per_node if proto.max_replicas_per_node else None
|
||||
),
|
||||
)
|
||||
|
||||
def _get_serialized_options(
|
||||
self, update_types: List[DeploymentOptionUpdateType]
|
||||
) -> bytes:
|
||||
"""Returns a serialized dictionary containing fields of a deployment config that
|
||||
should prompt a deployment version update.
|
||||
"""
|
||||
reconfigure_dict = {}
|
||||
# In pydantic 2.0, `__fields__` has been renamed to `model_fields`.
|
||||
# Access from class, not instance, to avoid deprecation warning.
|
||||
fields = DeploymentConfig.model_fields
|
||||
for option_name, field in fields.items():
|
||||
# In Pydantic v2, extra kwargs passed to Field() are in json_schema_extra
|
||||
json_schema_extra = field.json_schema_extra
|
||||
option_weight = (
|
||||
json_schema_extra.get("update_type")
|
||||
if isinstance(json_schema_extra, dict)
|
||||
else None
|
||||
)
|
||||
if option_weight in update_types:
|
||||
reconfigure_dict[option_name] = getattr(
|
||||
self.deployment_config, option_name
|
||||
)
|
||||
# If autoscaling config was changed, only broadcast to
|
||||
# replicas if metrics_interval_s or look_back_period_s
|
||||
# was changed, because the rest of the fields are only
|
||||
# used in deployment state manager
|
||||
if isinstance(reconfigure_dict[option_name], AutoscalingConfig):
|
||||
reconfigure_dict[option_name] = reconfigure_dict[
|
||||
option_name
|
||||
].model_dump(include={"metrics_interval_s", "look_back_period_s"})
|
||||
elif isinstance(reconfigure_dict[option_name], BaseModel):
|
||||
reconfigure_dict[option_name] = reconfigure_dict[
|
||||
option_name
|
||||
].model_dump()
|
||||
|
||||
# Can't serialize bytes. The request router class is already
|
||||
# included in the serialized config as request_router_class.
|
||||
if "request_router_config" in reconfigure_dict:
|
||||
reconfigure_dict["request_router_config"].pop(
|
||||
"_serialized_request_router_cls", None
|
||||
)
|
||||
|
||||
if (
|
||||
isinstance(self.deployment_config.user_config, bytes)
|
||||
and "user_config" in reconfigure_dict
|
||||
):
|
||||
del reconfigure_dict["user_config"]
|
||||
return self.deployment_config.user_config + _serialize(reconfigure_dict)
|
||||
|
||||
return _serialize(reconfigure_dict)
|
||||
|
||||
|
||||
def _serialize(json_object):
|
||||
return str.encode(json.dumps(json_object, sort_keys=True))
|
||||
Reference in New Issue
Block a user