ray_utils.py 16.5 KB
Newer Older
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3

4
import os
5
6
import time
from collections import defaultdict
7
from concurrent.futures import Future
8
from typing import TYPE_CHECKING, Union
9

10
import vllm.platforms
11
from vllm.config import ParallelConfig
12
from vllm.distributed import get_pp_group
13
from vllm.distributed.kv_transfer.kv_connector.utils import KVOutputAggregator
14
from vllm.logger import init_logger
15
from vllm.platforms import current_platform
16
from vllm.sequence import IntermediateTensors
17
from vllm.utils.network_utils import get_ip
18
from vllm.v1.outputs import AsyncModelRunnerOutput
19
from vllm.v1.worker.worker_base import WorkerWrapperBase
20

21
if TYPE_CHECKING:
22
    from vllm.v1.core.sched.output import SchedulerOutput
23
24
    from vllm.v1.outputs import ModelRunnerOutput

25
logger = init_logger(__name__)
26
PG_WAIT_TIMEOUT = 1800
27
28
29

try:
    import ray
30
31
    from ray.util import placement_group_table
    from ray.util.placement_group import PlacementGroup
32

33
34
35
36
37
    try:
        from ray._private.state import available_resources_per_node
    except ImportError:
        # Ray 2.9.x doesn't expose `available_resources_per_node`
        from ray._private.state import state as _state
38

39
        available_resources_per_node = _state._available_resources_per_node
40

41
    class RayWorkerWrapper(WorkerWrapperBase):
42
        """Ray wrapper for vllm.worker.Worker, allowing Worker to be
43
        lazily initialized after Ray sets CUDA_VISIBLE_DEVICES."""
44

45
46
        def __init__(self, *args, **kwargs) -> None:
            super().__init__(*args, **kwargs)
47
48
49
50
51
            # Since the compiled DAG runs a main execution
            # in a different thread that calls cuda.set_device.
            # The flag indicates is set_device is called on
            # that thread.
            self.compiled_dag_cuda_device_set = False
52

53
54
55
        def get_node_ip(self) -> str:
            return get_ip()

56
        def get_node_and_gpu_ids(self) -> tuple[str, list[int]]:
57
            node_id = ray.get_runtime_context().get_node_id()
58
            device_key = vllm.platforms.current_platform.ray_device_key
59
            if not device_key:
60
61
62
63
64
                raise RuntimeError(
                    "current platform %s does not support ray.",
                    vllm.platforms.current_platform.device_name,
                )
            gpu_ids = ray.get_runtime_context().get_accelerator_ids()[device_key]
65
66
            return node_id, gpu_ids

67
68
69
70
71
72
73
        def setup_device_if_necessary(self):
            # TODO(swang): This is needed right now because Ray CG executes
            # on a background thread, so we need to reset torch's current
            # device.
            # We can remove this API after it is fixed in compiled graph.
            assert self.worker is not None, "Worker is not initialized"
            if not self.compiled_dag_cuda_device_set:
74
75
76
77
                if current_platform.is_tpu():
                    # Not needed
                    pass
                else:
78
                    assert self.worker.device is not None
79
                    current_platform.set_device(self.worker.device)
80

81
82
                self.compiled_dag_cuda_device_set = True

83
        def execute_model_ray(
84
            self,
85
            scheduler_output: Union[
86
                "SchedulerOutput", tuple["SchedulerOutput", "IntermediateTensors"]
87
88
            ],
        ) -> Union[
89
            "ModelRunnerOutput", tuple["SchedulerOutput", "IntermediateTensors"]
90
        ]:
91
            # This method is used by Ray Compiled Graph to execute the model,
92
            # and it needs a special logic of self.setup_device_if_necessary()
93
94
            self.setup_device_if_necessary()
            assert self.worker is not None, "Worker is not initialized"
95
96
97
98
            if isinstance(scheduler_output, tuple):
                scheduler_output, intermediate_tensors = scheduler_output
            else:
                scheduler_output, intermediate_tensors = scheduler_output, None
99
            assert self.worker.model_runner is not None
100
            output = self.worker.model_runner.execute_model(
101
102
                scheduler_output, intermediate_tensors
            )
103
104
            if isinstance(output, IntermediateTensors):
                output = scheduler_output, output
105
106
107
108
109
            elif not get_pp_group().is_last_rank:
                # Case where there are no scheduled requests
                # but may still be finished requests.
                assert not output or not output.req_ids
                output = scheduler_output, None
110
111
112
113
114
            # Ensure outputs crossing Ray compiled DAG are serializable.
            # AsyncModelRunnerOutput holds CUDA events and cannot be
            # pickled.
            if isinstance(output, AsyncModelRunnerOutput):
                output = output.get_output()
115
116
            return output

117
        def override_env_vars(self, vars: dict[str, str]):
118
119
            os.environ.update(vars)

120
121
    ray_import_err = None

122
except ImportError as e:
123
    ray = None  # type: ignore
124
125
126
    # only capture string to avoid variable references in the traceback that can
    # prevent garbage collection in some cases
    ray_import_err = str(e)
127
    RayWorkerWrapper = None  # type: ignore
128
129


130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
class FutureWrapper(Future):
    """A wrapper around Ray output reference to meet the interface
    of .execute_model(): The top level (core busy loop) expects .result() api
    to block and return a single output.

    If aggregator is provided, the outputs from all workers are aggregated upon
    the result() call. If not only the first worker's output is returned.
    """

    def __init__(self, refs, aggregator: KVOutputAggregator | None = None):
        super().__init__()
        self.refs = refs
        self.aggregator = aggregator

    def result(self, timeout=None):
        if timeout is not None:
            raise NotImplementedError("timeout is not supported")

        if self.aggregator is None:
            return self.refs[0].get()

        outputs = [ref.get() for ref in self.refs]
        return self.aggregator.aggregate(outputs, output_rank=0)


155
156
157
158
159
160
161
162
def ray_is_available() -> bool:
    """Returns True if Ray is available."""
    return ray is not None


def assert_ray_available():
    """Raise an exception if Ray is not available."""
    if ray is None:
163
164
165
166
        raise ValueError(
            f"Failed to import Ray: {ray_import_err}."
            "Please install Ray with `pip install ray`."
        )
167
168


169
170
171
def _verify_bundles(
    placement_group: "PlacementGroup", parallel_config: ParallelConfig, device_str: str
):
172
173
174
175
176
177
178
    """Verify a given placement group has bundles located in the right place.

    There are 2 rules.
    - Warn if all tensor parallel workers cannot fit in a single node.
    - Fail if driver node is not included in a placement group.
    """
    assert ray.is_initialized(), (
179
180
        "Ray is not initialized although distributed-executor-backend is ray."
    )
181
182
183
184
185
186
    pg_data = placement_group_table(placement_group)
    # bundle_idx -> node_id
    bundle_to_node_ids = pg_data["bundles_to_node_id"]
    # bundle_idx -> bundle (e.g., {"GPU": 1})
    bundles = pg_data["bundles"]
    # node_id -> List of bundle (e.g., {"GPU": 1})
187
    node_id_to_bundle: dict[str, list[dict[str, float]]] = defaultdict(list)
188
189
190
191
192
193
194
195
196
197
198

    for bundle_idx, node_id in bundle_to_node_ids.items():
        node_id_to_bundle[node_id].append(bundles[bundle_idx])
    driver_node_id = ray.get_runtime_context().get_node_id()

    if driver_node_id not in node_id_to_bundle:
        raise RuntimeError(
            f"driver node id {driver_node_id} is not included in a placement "
            f"group {placement_group.id}. Node id -> bundles "
            f"{node_id_to_bundle}. "
            "You don't have enough GPUs available in a current node. Check "
199
200
201
            "`ray status` and `ray list nodes` to see if you have available "
            "GPUs in a node `{driver_node_id}` before starting an vLLM engine."
        )
202
203
204
205
206
207
208
209
210
211
212

    for node_id, bundles in node_id_to_bundle.items():
        if len(bundles) < parallel_config.tensor_parallel_size:
            logger.warning(
                "tensor_parallel_size=%d "
                "is bigger than a reserved number of %ss (%d "
                "%ss) in a node %s. Tensor parallel workers can be "
                "spread out to 2+ nodes which can degrade the performance "
                "unless you have fast interconnect across nodes, like "
                "Infiniband. To resolve this issue, make sure you have more "
                "than %d GPUs available at each node.",
213
214
215
216
217
218
219
                parallel_config.tensor_parallel_size,
                device_str,
                len(bundles),
                device_str,
                node_id,
                parallel_config.tensor_parallel_size,
            )
220
221
222
223
224
225
226
227
228
229


def _wait_until_pg_ready(current_placement_group: "PlacementGroup"):
    """Wait until a placement group is ready.

    It prints the informative log messages if the placement group is
    not created within time.

    """
    # Wait until PG is ready - this will block until all
230
    # requested resources are available, and will time out
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
    # if they cannot be provisioned.
    placement_group_specs = current_placement_group.bundle_specs

    s = time.time()
    pg_ready_ref = current_placement_group.ready()
    wait_interval = 10
    while time.time() - s < PG_WAIT_TIMEOUT:
        ready, _ = ray.wait([pg_ready_ref], timeout=wait_interval)
        if len(ready) > 0:
            break

        # Exponential backoff for warning print.
        wait_interval *= 2
        logger.info(
            "Waiting for creating a placement group of specs for "
246
247
            "%d seconds. specs=%s. Check `ray status` and "
            "`ray list nodes` to see if you have enough resources,"
248
249
250
            " and make sure the IP addresses used by ray cluster"
            " are the same as VLLM_HOST_IP environment variable"
            " specified in each node if you are running on a multi-node.",
251
252
253
            int(time.time() - s),
            placement_group_specs,
        )
254
255
256
257
258
259
260

    try:
        ray.get(pg_ready_ref, timeout=0)
    except ray.exceptions.GetTimeoutError:
        raise ValueError(
            "Cannot provide a placement group of "
            f"{placement_group_specs=} within {PG_WAIT_TIMEOUT} seconds. See "
261
            "`ray status` and `ray list nodes` to make sure the cluster has "
262
263
            "enough resources."
        ) from None
264
265
266
267
268
269
270
271
272
273
274
275
276
277


def _wait_until_pg_removed(current_placement_group: "PlacementGroup"):
    ray.util.remove_placement_group(current_placement_group)
    s = time.time()
    wait_interval = 10
    while time.time() - s < PG_WAIT_TIMEOUT:
        pg = ray.util.get_current_placement_group()
        if pg is None:
            break

        # Exponential backoff for warning print.
        wait_interval *= 2
        logger.info(
278
279
280
            "Waiting for removing a placement group of specs for %d seconds.",
            int(time.time() - s),
        )
281
282
283
        time.sleep(wait_interval)


284
def initialize_ray_cluster(
285
    parallel_config: ParallelConfig,
286
    ray_address: str | None = None,
287
288
289
290
291
292
):
    """Initialize the distributed cluster with Ray.

    it will connect to the Ray cluster and create a placement group
    for the workers, which includes the specification of the resources
    for each distributed worker.
293
294
295

    Args:
        parallel_config: The configurations for parallel execution.
Zhuohan Li's avatar
Zhuohan Li committed
296
        ray_address: The address of the Ray cluster. If None, uses
297
298
            the default Ray cluster address.
    """
299
    assert_ray_available()
300
    from vllm.platforms import current_platform
301

302
303
304
    if ray.is_initialized():
        logger.info("Ray is already initialized. Skipping Ray initialization.")
    elif current_platform.is_rocm() or current_platform.is_xpu():
305
306
        # Try to connect existing ray instance and create a new one if not found
        try:
307
            ray.init("auto")
308
309
310
        except ConnectionError:
            logger.warning(
                "No existing RAY instance detected. "
311
312
313
314
315
316
317
                "A new instance will be launched with current node resources."
            )
            ray.init(
                address=ray_address,
                num_gpus=parallel_config.world_size,
                runtime_env=parallel_config.ray_runtime_env,
            )
318
    else:
319
        ray.init(address=ray_address, runtime_env=parallel_config.ray_runtime_env)
320

321
322
323
    device_str = current_platform.ray_device_key
    if not device_str:
        raise ValueError(
324
325
            f"current platform {current_platform.device_name} does not support ray."
        )
326

327
328
329
330
331
332
    # Create or get the placement group for worker processes
    if parallel_config.placement_group:
        current_placement_group = parallel_config.placement_group
    else:
        current_placement_group = ray.util.get_current_placement_group()

333
    if current_placement_group:
334
335
        logger.info("Using the existing placement group")

336
337
338
        # We are in a placement group
        bundles = current_placement_group.bundle_specs
        # Verify that we can use the placement group.
339
        device_bundles = 0
340
        for bundle in bundles:
341
342
            bundle_devices = bundle.get(device_str, 0)
            if bundle_devices > 1:
343
                raise ValueError(
344
345
                    f"Placement group bundle cannot have more than 1 {device_str}."
                )
346
347
348
            if bundle_devices:
                device_bundles += 1
        if parallel_config.world_size > device_bundles:
349
            raise ValueError(
350
                f"The number of required {device_str}s exceeds the total "
351
                f"number of available {device_str}s in the placement group. "
352
                f"Required number of devices: {parallel_config.world_size}. "
353
354
                f"Total number of devices: {device_bundles}."
            )
355
    else:
356
        logger.info("No current placement group found. Creating a new placement group.")
357
        num_devices_in_cluster = ray.cluster_resources().get(device_str, 0)
358
359
360
        # Log a warning message and delay resource allocation failure response.
        # Avoid immediate rejection to allow user-initiated placement group
        # created and wait cluster to be ready
361
        if parallel_config.world_size > num_devices_in_cluster:
362
363
            logger.warning(
                "The number of required %ss exceeds the total "
364
365
366
367
                "number of available %ss in the placement group.",
                device_str,
                device_str,
            )
368
        # Create a new placement group
369
        placement_group_specs: list[dict[str, float]] = [
370
371
            {device_str: 1.0} for _ in range(parallel_config.world_size)
        ]
372
373
374
375
376
377
378
379
380
381
382
383

        # vLLM engine is also a worker to execute model with an accelerator,
        # so it requires to have the device in a current node. Check if
        # the current node has at least one device.
        current_ip = get_ip()
        current_node_id = ray.get_runtime_context().get_node_id()
        current_node_resource = available_resources_per_node()[current_node_id]
        if current_node_resource.get(device_str, 0) < 1:
            raise ValueError(
                f"Current node has no {device_str} available. "
                f"{current_node_resource=}. vLLM engine cannot start without "
                f"{device_str}. Make sure you have at least 1 {device_str} "
384
385
                f"available in a node {current_node_id=} {current_ip=}."
            )
386
387
388
389
390
        # This way, at least bundle is required to be created in a current
        # node.
        placement_group_specs[0][f"node:{current_ip}"] = 0.001

        # By default, Ray packs resources as much as possible.
391
        current_placement_group = ray.util.placement_group(
392
393
            placement_group_specs, strategy="PACK"
        )
394
        _wait_until_pg_ready(current_placement_group)
395

396
397
    assert current_placement_group is not None
    _verify_bundles(current_placement_group, parallel_config, device_str)
398
399
    # Set the placement group in the parallel config
    parallel_config.placement_group = current_placement_group
400
401
402
403


def get_num_tpu_nodes() -> int:
    from ray._private.accelerators import TPUAcceleratorManager
404

405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
    cluster_resources = ray.cluster_resources()
    total_tpus = int(cluster_resources["TPU"])
    tpus_per_node = TPUAcceleratorManager.get_current_node_num_accelerators()
    assert total_tpus % tpus_per_node == 0
    return total_tpus // tpus_per_node


def get_num_nodes_in_placement_group() -> int:
    pg_table = ray.util.placement_group_table()
    current_pg = ray.util.get_current_placement_group()
    num_nodes = 0

    if current_pg:
        nodes_in_pg = set()
        for pg_key, pg in pg_table.items():
            if pg_key == current_pg.id.hex():
                for _, node in pg["bundles_to_node_id"].items():
                    nodes_in_pg.add(node)
        num_nodes = len(nodes_in_pg)

    return num_nodes