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

330 lines
12 KiB
Python

"""Command runners specific to TPU VM pods.
TPU VM pods may contain multiple hosts, each including attached TPU chips and
associated internal/external IP addresses.
To support TPU VM pods, we represent entire TPU pods as "Ray Nodes", meaning
that TPU pods will need to run the operations specified in `CommandRunnerInterface`
N times, where N denotes the number of hosts that comprise a TPU pod.
To maintain feature completeness, we simply wrap the existing `SSHCommandRunner` and
`DockerCommandRunner` and run them as batched calls.
"""
import copy
from concurrent.futures import ThreadPoolExecutor
from types import ModuleType
from typing import Any, Dict, Optional
from ray._private import ray_constants
from ray.autoscaler._private.command_runner import DockerCommandRunner, SSHCommandRunner
from ray.autoscaler._private.gcp.node import GCPTPUNode
from ray.autoscaler.command_runner import CommandRunnerInterface
from ray.autoscaler.node_provider import NodeProvider
class TPUVMSSHCommandRunner(SSHCommandRunner):
"""An SSH command runner with overwritten IP address calls."""
def __init__(
self,
internal_ip: str,
external_ip: str,
worker_id: int,
accelerator_type: str,
*args,
**kwargs,
):
self._internal_ip = internal_ip
self._external_ip = external_ip
self._worker_id = worker_id
self._accelerator_type = accelerator_type
super().__init__(*args, **kwargs)
def _get_node_ip(self) -> str:
if self.use_internal_ip:
return self._internal_ip
else:
return self._external_ip
def run(
self,
cmd,
timeout=120,
exit_on_fail=False,
port_forward=None,
with_output=False,
environment_variables: Dict[str, object] = None,
run_env="auto", # Unused argument.
ssh_options_override_ssh_key="",
shutdown_after_run=False,
) -> str:
"""Override the SSH run for TPU VM pods.
Main functionality here we need to inject is to intercept the resources
provided by the node_provider TPU node type fillout.
node_provider will provide a resource "TPU-{TPU_POD_TYPE}-head" which:
1) allows application developers to target worker 0 of an arbitary TPU pod, and
2) signals to the autoscaler how to address the demand for more TPU pods.
Without this intercept, then all workers of a TPU pod will have the
"TPU-{TPU_POD_TYPE}-head" resource which will violate functionality (1)
above.
"""
if environment_variables:
environment_variables = _maybe_remove_head_resource(
environment_variables, self._worker_id, self._accelerator_type
)
return super().run(
cmd=cmd,
timeout=timeout,
exit_on_fail=exit_on_fail,
port_forward=port_forward,
with_output=with_output,
environment_variables=environment_variables,
run_env=run_env,
ssh_options_override_ssh_key=ssh_options_override_ssh_key,
shutdown_after_run=shutdown_after_run,
)
class TPUVMDockerCommandRunner(DockerCommandRunner):
"""A Docker command runner with overwritten IP addresses."""
def __init__(
self,
docker_config: Dict[str, Any],
internal_ip: str,
external_ip: str,
worker_id: int,
accelerator_type: str,
**common_args,
):
super().__init__(docker_config=docker_config, **common_args)
self._worker_id = worker_id
self._accelerator_type = accelerator_type
self.ssh_command_runner = TPUVMSSHCommandRunner(
internal_ip=internal_ip,
external_ip=external_ip,
worker_id=worker_id,
accelerator_type=accelerator_type,
**common_args,
)
def run(
self,
cmd,
timeout=120,
exit_on_fail=False,
port_forward=None,
with_output=False,
environment_variables: Optional[Dict[str, object]] = None,
run_env="auto",
ssh_options_override_ssh_key="",
shutdown_after_run=False,
):
if environment_variables:
environment_variables = _maybe_remove_head_resource(
environment_variables, self._worker_id, self._accelerator_type
)
return super().run(
cmd,
timeout,
exit_on_fail,
port_forward,
with_output,
environment_variables,
run_env,
ssh_options_override_ssh_key,
shutdown_after_run,
)
class TPUCommandRunner(CommandRunnerInterface):
"""A TPU pod command runner."""
def __init__(
self,
instance: GCPTPUNode,
log_prefix: str,
node_id: str,
auth_config: Dict[str, Any],
provider: NodeProvider,
cluster_name: str,
process_runner: ModuleType,
use_internal_ip: bool,
docker_config: Optional[Dict[str, Any]] = None,
):
def create_command_runner(
worker_id: int, accelerator_type: str, internal_ip: str, external_ip: str
) -> CommandRunnerInterface:
"""Returns the correct base command runner."""
common_args = {
"internal_ip": internal_ip,
"external_ip": external_ip,
"worker_id": worker_id,
"accelerator_type": accelerator_type,
"log_prefix": "[tpu_worker_{}] ".format(worker_id) + log_prefix,
"node_id": node_id,
"provider": provider,
"auth_config": auth_config,
"cluster_name": cluster_name,
"process_runner": process_runner,
"use_internal_ip": use_internal_ip,
}
if docker_config and docker_config["container_name"] != "":
return TPUVMDockerCommandRunner(
docker_config=docker_config, **common_args
)
else:
return TPUVMSSHCommandRunner(**common_args)
self._command_runners = []
self._num_workers = instance.num_workers
for i in range(self._num_workers):
self._command_runners.append(
create_command_runner(
worker_id=i,
accelerator_type=instance.get("acceleratorType"),
internal_ip=instance.get_internal_ip(i),
external_ip=instance.get_external_ip(i),
)
)
@property
def num_connections(self) -> int:
"""Return the number of active connections allowed at a time.
We occasionally see issues where too many concurrent connections may lead to
failed SSH connections when there are too many TPU hosts.
We utilize this property to cap the maximum number of active connections
at a time until a proper fix is found.
"""
num_max_concurrent_active_connections = ray_constants.env_integer(
ray_constants.RAY_TPU_MAX_CONCURRENT_CONNECTIONS_ENV_VAR, default=16
)
return min(self._num_workers, num_max_concurrent_active_connections)
def run(
self,
cmd,
timeout=120,
exit_on_fail=False,
port_forward=None,
with_output=False,
environment_variables: Dict[str, object] = None,
run_env="auto", # Unused argument.
ssh_options_override_ssh_key="",
shutdown_after_run=False,
) -> str:
with ThreadPoolExecutor(self.num_connections) as executor:
results = executor.map(
lambda i: self._command_runners[i].run(
cmd=cmd,
timeout=timeout,
exit_on_fail=exit_on_fail,
port_forward=port_forward,
with_output=with_output,
environment_variables=copy.deepcopy(environment_variables),
run_env=run_env,
ssh_options_override_ssh_key=ssh_options_override_ssh_key,
shutdown_after_run=shutdown_after_run,
),
range(self._num_workers),
)
# Note: the `run` abstract function may return a string representing
# representing command output, but this result is rarely used - especially
# if the node is a worker (which a TPU pod is).
# We return only the results from worker 0 which may not always be expected.
return list(results)[0]
def run_rsync_up(self, *args, **kwargs) -> None:
with ThreadPoolExecutor(self.num_connections) as executor:
executor.map(
lambda i: self._command_runners[i].run_rsync_up(*args, **kwargs),
range(self._num_workers),
)
def run_rsync_down(self, *args: Any, **kwargs: Any) -> None:
"""Rsync files down from the cluster node.
Args:
*args: Forwarded to each per-worker ``run_rsync_down`` call.
Includes the (remote) source path and (local) target path.
**kwargs: Forwarded to each per-worker ``run_rsync_down`` call.
"""
with ThreadPoolExecutor(self.num_connections) as executor:
executor.map(
lambda i: self._command_runners[i].run_rsync_down(*args, **kwargs),
range(self._num_workers),
)
def remote_shell_command_str(self) -> str:
"""Return the command the user can use to open a shell."""
# Note: this function is rarely used if the node is a worker.
# We return only the results from worker 0 which may not always be expected.
return self._command_runners[0].remote_shell_command_str()
def run_init(self, *args: Any, **kwargs: Any) -> Optional[bool]:
"""Used to run extra initialization commands.
Args:
*args: Forwarded to each per-worker ``run_init`` call. Includes
``as_head``, ``file_mounts``, and ``sync_run_yet``.
**kwargs: Forwarded to each per-worker ``run_init`` call.
Returns:
Whether initialization is necessary on any worker.
"""
with ThreadPoolExecutor(self.num_connections) as executor:
results = executor.map(
lambda i: self._command_runners[i].run_init(*args, **kwargs),
range(self._num_workers),
)
# Note: the `run_init` abstract function may return a bool representing
# whether initialization is necessary, but this result is rarely used -
# especially if the node is a worker (which a TPU pod is).
# Here we return whether any workers require initialization, which may not be
# the expected result.
return any(results)
def _maybe_remove_head_resource(
environment_variables: Dict[str, Any], worker_id: int, accelerator_type: str
):
"""
node_provider will provide a resource "TPU-{TPU_POD_TYPE}-head" which:
1) allows application developers to target worker 0 of an arbitary TPU pod, and
2) signals to the autoscaler how to address the demand for more TPU pods.
Without this intercept, then all workers of a TPU pod will have the
"TPU-{TPU_POD_TYPE}-head" resource which will violate functionality (1)
above.
"""
resources = environment_variables.get(
ray_constants.RESOURCES_ENVIRONMENT_VARIABLE, None
)
if resources:
# For TPU pod support, we need to ensure that the
# tpu pod resource type only propagates to worker 0.
if worker_id != 0:
tpu_pod_resource_type = f"TPU-{accelerator_type}-head"
if tpu_pod_resource_type in resources:
resources = copy.copy(resources)
resources.pop(tpu_pod_resource_type, None)
environment_variables = {
**environment_variables,
ray_constants.RESOURCES_ENVIRONMENT_VARIABLE: resources,
}
return environment_variables