Files
2026-07-13 13:17:40 +08:00

181 lines
6.9 KiB
Python

import hashlib
import json
import logging
import time
from typing import Any, Dict, Optional, Union
import ray
import ray.util.serialization_addons
from ray.serve._private.common import DeploymentID
from ray.serve._private.config import DeploymentConfig, ReplicaConfig
from ray.serve._private.constants import (
RAY_SERVE_DIRECT_INGRESS_MIN_DRAINING_PERIOD_S,
RAY_SERVE_DIRECT_INGRESS_SHUTDOWN_BUFFER_S,
RAY_SERVE_ENABLE_DIRECT_INGRESS,
SERVE_LOGGER_NAME,
)
from ray.serve._private.deployment_info import DeploymentInfo
from ray.serve.exceptions import RayServeException
from ray.serve.schema import ServeApplicationSchema
logger = logging.getLogger(SERVE_LOGGER_NAME)
def get_deploy_args(
name: str,
replica_config: ReplicaConfig,
ingress: bool = False,
ingress_request_router: bool = False,
deployment_config: Optional[Union[DeploymentConfig, Dict[str, Any]]] = None,
version: Optional[str] = None,
route_prefix: Optional[str] = None,
serialized_autoscaling_policy_def: Optional[bytes] = None,
serialized_request_router_cls: Optional[bytes] = None,
serialized_deployment_actors: Optional[Dict[str, bytes]] = None,
uses_multiplexing: bool = False,
) -> Dict:
"""
Takes a deployment's configuration, and returns the arguments needed
for the controller to deploy it.
"""
if deployment_config is None:
deployment_config = {}
if isinstance(deployment_config, dict):
deployment_config = DeploymentConfig.model_validate(deployment_config)
elif not isinstance(deployment_config, DeploymentConfig):
raise TypeError("config must be a DeploymentConfig or a dictionary.")
deployment_config.version = version
controller_deploy_args = {
"deployment_name": name,
"deployment_config_proto_bytes": deployment_config.to_proto_bytes(),
"replica_config_proto_bytes": replica_config.to_proto_bytes(),
"route_prefix": route_prefix,
"deployer_job_id": ray.get_runtime_context().get_job_id(),
"ingress": ingress,
"ingress_request_router": ingress_request_router,
"serialized_autoscaling_policy_def": serialized_autoscaling_policy_def,
"serialized_request_router_cls": serialized_request_router_cls,
"serialized_deployment_actors": serialized_deployment_actors,
"uses_multiplexing": uses_multiplexing,
}
return controller_deploy_args
def deploy_args_to_deployment_info(
deployment_name: str,
deployment_config_proto_bytes: bytes,
replica_config_proto_bytes: bytes,
deployer_job_id: Union[str, bytes],
app_name: Optional[str] = None,
ingress: bool = False,
ingress_request_router: bool = False,
route_prefix: Optional[str] = None,
uses_multiplexing: bool = False,
**kwargs,
) -> DeploymentInfo:
"""Takes deployment args passed to the controller after building an application and
constructs a DeploymentInfo object.
"""
deployment_config = DeploymentConfig.from_proto_bytes(deployment_config_proto_bytes)
if ingress and RAY_SERVE_ENABLE_DIRECT_INGRESS:
# Model multiplexing relies on the multiplexed model ID being propagated through
# the proxy, which direct ingress bypasses (the model ID is never populated).
# Only the *statically* detectable case is caught here; dynamically-initialized
# multiplexing is caught at replica initialization.
if uses_multiplexing:
raise RayServeException(
f'Ingress deployment "{deployment_name}" in application "{app_name}" uses '
"model multiplexing (`@serve.multiplexed`), which is not supported on the "
"ingress deployment when direct ingress or HAProxy is enabled."
)
# Floor the timeout so the controller's force-kill can't cut the
# direct-ingress drain (min draining period) short.
floor_s = (
RAY_SERVE_DIRECT_INGRESS_MIN_DRAINING_PERIOD_S
+ RAY_SERVE_DIRECT_INGRESS_SHUTDOWN_BUFFER_S
)
if deployment_config.graceful_shutdown_timeout_s < floor_s:
logger.info(
f"Raising graceful_shutdown_timeout_s for ingress deployment "
f"'{deployment_name}' from "
f"{deployment_config.graceful_shutdown_timeout_s}s to {floor_s}s so "
f"the force-kill deadline covers the direct-ingress drain period."
)
deployment_config.graceful_shutdown_timeout_s = floor_s
version = deployment_config.version
replica_config = ReplicaConfig.from_proto_bytes(
replica_config_proto_bytes, deployment_config.needs_pickle()
)
# Java API passes in JobID as bytes
if isinstance(deployer_job_id, bytes):
deployer_job_id = ray.JobID.from_int(
int.from_bytes(deployer_job_id, "little")
).hex()
return DeploymentInfo(
actor_name=DeploymentID(
name=deployment_name, app_name=app_name
).to_replica_actor_class_name(),
version=version,
deployment_config=deployment_config,
replica_config=replica_config,
deployer_job_id=deployer_job_id,
start_time_ms=int(time.time() * 1000),
route_prefix=route_prefix,
ingress=ingress,
ingress_request_router=ingress_request_router,
)
def get_app_code_version(app_config: ServeApplicationSchema) -> str:
"""Returns the code version of an application.
Args:
app_config: The application config.
Returns:
str: A hash of the import path and (application level) runtime env
representing the code version of the application.
"""
request_router_configs = [
deployment.request_router_config
for deployment in app_config.deployments
if isinstance(deployment.request_router_config, dict)
]
deployment_autoscaling_policies = [
deployment_config.autoscaling_config.get("policy", None)
for deployment_config in app_config.deployments
if isinstance(deployment_config.autoscaling_config, dict)
]
deployment_actors_configs = [
deployment.deployment_actors
for deployment in app_config.deployments
if isinstance(deployment.deployment_actors, list)
]
encoded = json.dumps(
{
"import_path": app_config.import_path,
"runtime_env": app_config.runtime_env,
"args": app_config.args,
# NOTE: trigger a change in the code version when
# application level autoscaling policy is changed or
# any one of the deployment level autoscaling policy is changed
"autoscaling_policy": app_config.autoscaling_policy,
"deployment_autoscaling_policies": deployment_autoscaling_policies,
"request_router_configs": request_router_configs,
"deployment_actors": deployment_actors_configs,
},
sort_keys=True,
).encode("utf-8")
return hashlib.sha256(encoded).hexdigest()