From 188b08e752fc994446b9efe5c6eb0be0a82d5882 Mon Sep 17 00:00:00 2001 From: Ziming Huang Date: Mon, 4 Nov 2024 21:42:14 +0800 Subject: [PATCH 01/10] [Fix] Add node id to BackendSim (#64) --- llumnix/backends/vllm/simulator.py | 4 +++- .../test_llm_engine_manager.py | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/llumnix/backends/vllm/simulator.py b/llumnix/backends/vllm/simulator.py index b5ccb45b..94367d75 100644 --- a/llumnix/backends/vllm/simulator.py +++ b/llumnix/backends/vllm/simulator.py @@ -36,6 +36,7 @@ def __init__( migration_config: MigrationConfig, profiling_result_file_path: str, engine_args: EngineArgs, + node_id: str = None, ) -> None: # multi-instance args latency_mem = self._get_lantecy_mem(profiling_result_file_path, engine_args) @@ -43,7 +44,8 @@ def __init__( output_queue_type=output_queue_type, migration_config=migration_config, instance_id=instance_id, - latency_mem=latency_mem) + latency_mem=latency_mem, + node_id=node_id) self.engine.scheduler = SchedulerLlumnix(self.engine.scheduler_config, self.engine.cache_config, self.engine.lora_config) self.engine.scheduler.add_update_instance_info_callback(self.engine.update_instance_info) self.engine.output_processor.scheduler = self.engine.scheduler diff --git a/tests/unit_test/global_scheduler/test_llm_engine_manager.py b/tests/unit_test/global_scheduler/test_llm_engine_manager.py index b744ced6..5c5fc644 100644 --- a/tests/unit_test/global_scheduler/test_llm_engine_manager.py +++ b/tests/unit_test/global_scheduler/test_llm_engine_manager.py @@ -26,6 +26,7 @@ from llumnix.server_info import ServerInfo from llumnix.queue.queue_type import QueueType from llumnix.global_scheduler.scaling_scheduler import InstanceType +from llumnix.backends.vllm.simulator import BackendSimVLLM # pylint: disable=unused-import from tests.conftest import setup_ray_env @@ -81,6 +82,13 @@ def migrate_out(self, src_instance_name, dst_instance_name): def get_num_migrate_out(self): return self.num_migrate_out +class MockBackendSim(BackendSimVLLM): + + def _get_lantecy_mem(self, *args, **kwargs): + latency_mem = LatencyMemData({}, {}, {}) + latency_mem.prefill_model_params = (0,0) + latency_mem.decode_model_params = (0,0,0) + return latency_mem def init_manager(): try: @@ -138,6 +146,18 @@ def test_init_llumlets(setup_ray_env, engine_manager): engine_manager_args = EngineManagerArgs() assert num_instances == engine_manager_args.initial_instances +def test_init_llumlets_sim(setup_ray_env, engine_manager): + engine_manager.profiling_result_file_path="//" + # pylint: disable=import-outside-toplevel + import llumnix.backends.vllm.simulator + llumnix.backends.vllm.simulator.BackendSimVLLM = MockBackendSim + engine_args = EngineArgs(model="facebook/opt-125m", worker_use_ray=True) + node_id = ray.get_runtime_context().get_node_id() + instance_ids, llumlets = ray.get(engine_manager.init_llumlets.remote(engine_args, node_id, QueueType("rayqueue"))) + num_instances = ray.get(engine_manager.scale_up.remote(instance_ids, llumlets)) + engine_manager_args = EngineManagerArgs() + assert num_instances == engine_manager_args.initial_instances + def test_scale_up_and_down(setup_ray_env, engine_manager): initial_instances = 4 instance_ids, llumlets = init_llumlets(initial_instances) From 2135d8b41ec21607647b749388fb3eea31e3b216 Mon Sep 17 00:00:00 2001 From: Xinyi Zhang <114055322+Xinyi-ECNU@users.noreply.github.com> Date: Wed, 6 Nov 2024 13:08:16 +0800 Subject: [PATCH 02/10] [Misc] Add e2e test for prefill-decoding migration (#65) --- docs/Arguments.md | 8 ++++++++ llumnix/arg_utils.py | 5 ++++- llumnix/config/default.py | 2 +- llumnix/global_scheduler/global_scheduler.py | 1 + llumnix/global_scheduler/scaling_scheduler.py | 5 +++-- tests/e2e_test/test_e2e.py | 10 +++++++--- tests/e2e_test/test_migration.py | 8 ++++++-- 7 files changed, 30 insertions(+), 9 deletions(-) diff --git a/docs/Arguments.md b/docs/Arguments.md index 56474d82..c8397bfa 100644 --- a/docs/Arguments.md +++ b/docs/Arguments.md @@ -38,6 +38,8 @@ usage: -m llumnix.entrypoints.vllm.api_server [-h] [--migration-num-layers MIGRATION_NUM_LAYERS] [--last-stage-max-blocks LAST_STAGE_MAX_BLOCKS] [--max-stages MAX_STAGES] + [--enable-pd-disagg] + [--num-dispatch-instances NUM_DISPATCH_INSTANCES] [--log-request-timestamps] ``` @@ -168,6 +170,12 @@ usage: -m llumnix.entrypoints.vllm.api_server [-h] `--log-request-timestamps` - Enable logging request timestamps. +`--enable-pd-disagg` +- Enable prefill decoding disaggregation. + +`--num-dispatch-instances` +- Number of available instances for dispatch. + # Unsupported vLLM feature options `--device` diff --git a/llumnix/arg_utils.py b/llumnix/arg_utils.py index 70a643cf..dd80276d 100644 --- a/llumnix/arg_utils.py +++ b/llumnix/arg_utils.py @@ -306,6 +306,9 @@ def add_cli_args( type=int, help='drop migration if the number of stages > max_stages') parser.add_argument('--enable-pd-disagg', - type=bool, + action='store_true', help='enable prefill decoding disaggregation') + parser.add_argument('--num-dispatch-instances', + type=int, + help='number of available instances for dispatch') return parser diff --git a/llumnix/config/default.py b/llumnix/config/default.py index 17849463..fb94443b 100644 --- a/llumnix/config/default.py +++ b/llumnix/config/default.py @@ -80,7 +80,7 @@ _C.MANAGER.LOAD_METRIC = 'remaining_steps' # Request dispatch policy _C.MANAGER.DISPATCH_POLICY = 'load' -# Number of available dispatch instances. -1 indicates that all instances can be used for dispatching +# Number of available dispatch instances. math.inf indicates that all instances can be used for dispatching _C.MANAGER.NUM_DISPATCH_INSTANCES = math.inf # ----------------------------------------------------------------------------- diff --git a/llumnix/global_scheduler/global_scheduler.py b/llumnix/global_scheduler/global_scheduler.py index 79d6e88e..201b57de 100644 --- a/llumnix/global_scheduler/global_scheduler.py +++ b/llumnix/global_scheduler/global_scheduler.py @@ -48,6 +48,7 @@ def __init__(self, global_scheduler_config.scale_down_threshold, global_scheduler_config.scaling_policy, self.instance_load_calculator, + self.enable_pd_disagg, global_scheduler_config.num_dispatch_instances) self.num_instances = 0 diff --git a/llumnix/global_scheduler/scaling_scheduler.py b/llumnix/global_scheduler/scaling_scheduler.py index edcc9627..7607d88a 100644 --- a/llumnix/global_scheduler/scaling_scheduler.py +++ b/llumnix/global_scheduler/scaling_scheduler.py @@ -14,7 +14,6 @@ from typing import Dict, List, Tuple, Set from abc import ABC, abstractmethod from enum import Enum -import math import numpy as np from llumnix.logger import init_logger @@ -36,6 +35,7 @@ def __init__(self, scale_down_threshold: float, scaling_policy: str, instance_load_calculator: InstanceLoadCalculator, + enable_pd_disagg: bool, maximum_prefill_instance_num: int) -> None: self.scale_up_threshold = scale_up_threshold self.scale_down_threshold = scale_down_threshold @@ -46,6 +46,7 @@ def __init__(self, self.num_instances = 0 self.instance_id_set: Set[str] = set() self.maximum_prefill_instance_num = maximum_prefill_instance_num + self.enable_pd_disagg = enable_pd_disagg # instance info args self.instance_info: Dict[str, InstanceInfo] = None self.sorted_instance_infos: List[InstanceInfo] = None @@ -78,7 +79,7 @@ def add_instance(self, instance_id: str) -> None: self.instance_id_set.add(instance_id) self.num_instances = len(self.instance_id_set) instance_type = None - if self.maximum_prefill_instance_num == math.inf: + if not self.enable_pd_disagg: instance_type = InstanceType.NO_CONSTRAINTS else: if len(self.instance_type_id_set[InstanceType.PREFILL]) < self.maximum_prefill_instance_num: diff --git a/tests/e2e_test/test_e2e.py b/tests/e2e_test/test_e2e.py index 42f92512..741360f1 100644 --- a/tests/e2e_test/test_e2e.py +++ b/tests/e2e_test/test_e2e.py @@ -11,6 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import math import subprocess import asyncio import pytest @@ -40,7 +41,8 @@ def parse_launch_mode(launch_mode: str): def generate_launch_command(result_filename: str = "", launch_ray_cluster: bool = True, HEAD_NODE_IP: str = "127.0.0.1", ip: str = "127.0.0.1", port: int = 37000, instances_num = 1, dispatch_policy: str = "load", migration_backend = "gloo", model = "facebook/opt-125m", max_model_len: int = 2048, - launch_mode: str = 'eief', log_instance_info: bool = False): + launch_mode: str = 'eief', log_instance_info: bool = False, enable_pd_disagg: bool = False, + num_dispatch_instances: int = math.inf): disable_init_instance_by_manager, disable_fixed_node_init_instance = parse_launch_mode(launch_mode) command = ( f"RAY_DEDUP_LOGS=0 HEAD_NODE_IP={HEAD_NODE_IP} HEAD_NODE=1 " @@ -64,6 +66,8 @@ def generate_launch_command(result_filename: str = "", launch_ray_cluster: bool f"--migration-cache-blocks 32 " f"--tensor-parallel-size 1 " f"--request-output-queue-port {1234+port} " + f"{'--enable-pd-disagg ' if enable_pd_disagg else ''} " + f"{f'--num-dispatch-instances {num_dispatch_instances} ' if num_dispatch_instances != math.inf else ''} " f"{'--launch-ray-cluster ' if launch_ray_cluster else ''}" f"{'> instance_'+result_filename if len(result_filename)> 0 else ''} 2>&1 &" ) @@ -98,7 +102,7 @@ def clear_ray_state(): continue ray.shutdown() -async def get_llumnix_responce(prompt, sampling_params, ip_ports): +async def get_llumnix_response(prompt, sampling_params, ip_ports): timeout = aiohttp.ClientTimeout(total=60) request = { @@ -155,7 +159,7 @@ async def test_e2e(model, migration_backend, launch_mode): llumnix_output = {} for prompt in prompts: - response = await asyncio.wait_for(get_llumnix_responce(prompt, sampling_params, f"127.0.0.1:{base_port}"), + response = await asyncio.wait_for(get_llumnix_response(prompt, sampling_params, f"127.0.0.1:{base_port}"), timeout=60*5) llumnix_output[prompt] = response['text'][0] diff --git a/tests/e2e_test/test_migration.py b/tests/e2e_test/test_migration.py index 7fe167bb..ddf7fb51 100644 --- a/tests/e2e_test/test_migration.py +++ b/tests/e2e_test/test_migration.py @@ -11,6 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import math import asyncio from collections import defaultdict import re @@ -66,17 +67,20 @@ def parse_manager_log_file(log_file): @pytest.mark.skipif(torch.cuda.device_count() < 2, reason="at least 2 gpus required for migration bench") @pytest.mark.parametrize("model", ['/mnt/model/Qwen-7B']) @pytest.mark.parametrize("migration_backend", ['rpc', 'gloo', 'nccl']) -async def test_migration_benchmark(model, migration_backend): +@pytest.mark.parametrize("enable_pd_disagg", [False, True]) +async def test_migration_benchmark(model, migration_backend, enable_pd_disagg): base_port = 37037 instance_output_logs = [] device_count = torch.cuda.device_count() + num_dispatch_instances = device_count//2 if enable_pd_disagg else math.inf for i in range(device_count): output_log = f"{base_port+i}.out" instance_output_logs.append("instance_"+output_log) launch_command = generate_launch_command(result_filename=output_log, launch_ray_cluster=False, port=base_port+i, model=model, dispatch_policy="flood", migration_backend=migration_backend, - log_instance_info=True) + log_instance_info=True, enable_pd_disagg=enable_pd_disagg, + num_dispatch_instances=num_dispatch_instances) subprocess.run(launch_command, shell=True, check=True) await asyncio.sleep(60) From 844c836f110143fa2373b60eb09dbc66d26012ed Mon Sep 17 00:00:00 2001 From: KuilongCui Date: Mon, 11 Nov 2024 10:16:55 +0800 Subject: [PATCH 03/10] [Core] Support one-to-many and many-to-one migration (#63) --- Makefile | 14 +- configs/base.yml | 5 +- docs/Arguments.md | 13 +- llumnix/arg_utils.py | 21 +- .../backends/migration_backend_interface.py | 23 ++ llumnix/backends/vllm/llm_engine.py | 8 +- llumnix/backends/vllm/migration_backend.py | 90 ++++---- llumnix/backends/vllm/worker.py | 20 +- llumnix/config/default.py | 4 +- .../global_scheduler/dispatch_scheduler.py | 4 + .../global_scheduler/migration_scheduler.py | 4 + llumnix/internal_config.py | 8 +- llumnix/llm_engine_manager.py | 34 ++- llumnix/llumlet/llumlet.py | 17 +- llumnix/llumlet/local_migration_scheduler.py | 29 ++- llumnix/llumlet/request.py | 2 + tests/conftest.py | 6 +- tests/e2e_test/test_bench.py | 7 +- tests/e2e_test/test_e2e.py | 21 +- tests/e2e_test/test_migration.py | 25 ++- tests/e2e_test/utils.py | 12 ++ .../unit_test/backends/vllm/test_migration.py | 17 +- .../backends/vllm/test_migration_backend.py | 199 +++++++++++++----- .../unit_test/backends/vllm/test_simulator.py | 2 +- tests/unit_test/backends/vllm/test_worker.py | 26 ++- .../test_dispatch_scheduler.py | 6 +- .../test_llm_engine_manager.py | 65 ++++-- .../llumlet/test_engine_step_exception.py | 22 +- .../llumlet/test_local_migration_scheduler.py | 10 +- tests/unit_test/queue/test_zmq.py | 4 +- 30 files changed, 461 insertions(+), 257 deletions(-) diff --git a/Makefile b/Makefile index 6bc87a9b..b2cd80f3 100644 --- a/Makefile +++ b/Makefile @@ -21,22 +21,22 @@ install: .PHONY: lint lint: check_pylint_installed check_pytest_installed - @pylint --rcfile=.pylintrc -s n --jobs=32 ./llumnix + @pylint --rcfile=.pylintrc -s n --jobs=128 ./llumnix @pylint --rcfile=.pylintrc \ --disable=protected-access,super-init-not-called,unused-argument,redefined-outer-name,invalid-name \ - -s n --jobs=32 ./tests + -s n --jobs=128 ./tests .PHONY: test test: check_pytest_installed - @pytest -x -v --ignore=third_party/ --ignore=tests/e2e_test --disable-warnings + @pytest -v -x --ignore=third_party/ --ignore=tests/e2e_test --disable-warnings @python examlpes/offline_inference.py - @pytest -v tests/e2e_test/test_e2e.py + @pytest -v -x tests/e2e_test/test_e2e.py @pytest -v -x ./tests/e2e_test/test_migration.py .PHONY: unit_test unit_test: check_pytest_installed - @pytest -x -v --ignore=third_party/ --ignore=tests/e2e_test --disable-warnings + @pytest -v -x --ignore=third_party/ --ignore=tests/e2e_test --disable-warnings .PHONY: offline_test offline_test: @@ -44,11 +44,11 @@ offline_test: .PHONY: e2e_test e2e_test: - @pytest -v tests/e2e_test/test_e2e.py + @pytest -v -x tests/e2e_test/test_e2e.py .PHONY: bench_test bench_test: - @pytest -v ./tests/e2e_test/test_bench.py + @pytest -v -x ./tests/e2e_test/test_bench.py .PHONY: migration_test migration_test: diff --git a/configs/base.yml b/configs/base.yml index afce7127..70358339 100644 --- a/configs/base.yml +++ b/configs/base.yml @@ -2,8 +2,6 @@ SERVER: HOST: '127.0.0.1' PORT: 1234 QUEUE_TYPE: "rayqueue" - -RAY: RAY_CLUSTER_PORT: 6379 LAUNCH_RAY_CLUSTER: True @@ -21,6 +19,7 @@ MANAGER: REQUEST_MIGRATION_POLICY: 'SJF' MIGRATION_BACKEND: 'gloo' - MIGRATION_CACHE_BLOCKS: 512 + MIGRATION_BUFFER_BLOCKS: 512 + MIGRATION_INTERNAL_BUFFER_NUM: 2 ENABLE_SCALING: False diff --git a/docs/Arguments.md b/docs/Arguments.md index c8397bfa..a2584417 100644 --- a/docs/Arguments.md +++ b/docs/Arguments.md @@ -32,14 +32,15 @@ usage: -m llumnix.entrypoints.vllm.api_server [-h] [--profiling-result-file-path PROFILING_RESULT_FILE_PATH] [--gpu-type GPU_TYPE] [--polling-interval POLLING_INTERVAL] - [--migration-backend {gloo,nccl,rpc}] - [--migration-cache-blocks MIGRATION_CACHE_BLOCKS] + [--migration-backend {gloo,rpc}] + [--migration-buffer-blocks MIGRATION_BUFFER_BLOCKS] [--migration-backend-init-timeout MIGRATION_BACKEND_INIT_TIMEOUT] [--migration-num-layers MIGRATION_NUM_LAYERS] [--last-stage-max-blocks LAST_STAGE_MAX_BLOCKS] [--max-stages MAX_STAGES] [--enable-pd-disagg] [--num-dispatch-instances NUM_DISPATCH_INSTANCES] + [--migration-internal-buffer-num MIGRATION_INTERNAL_BUFFER_NUM] [--log-request-timestamps] ``` @@ -147,8 +148,8 @@ usage: -m llumnix.entrypoints.vllm.api_server [-h] - Possible choices: gloo, rpc - Default: "rpc" -`--migration-cache-blocks` -- Number of cache blocks in migration. +`--migration-buffer-blocks` +- Number of cache blocks in each migration buffer. - Default: 512 `--migration-backend-init-timeout` @@ -167,6 +168,10 @@ usage: -m llumnix.entrypoints.vllm.api_server [-h] - Drop migration if the number of stages > max_stages. - Default: 3 +`--migration-internal-buffer-num` +- Number of the buffer in migration backend for sending and receiving +- Default: 2 + `--log-request-timestamps` - Enable logging request timestamps. diff --git a/llumnix/arg_utils.py b/llumnix/arg_utils.py index dd80276d..37b3bbc6 100644 --- a/llumnix/arg_utils.py +++ b/llumnix/arg_utils.py @@ -22,7 +22,6 @@ from llumnix.config import LlumnixConfig, get_llumnix_config from llumnix.config.default import _C - class LlumnixArgumentParser(argparse.ArgumentParser): def __init__(self, *args, **kwargs): self.cur_namespace = "llumnix" @@ -134,10 +133,11 @@ class EngineManagerArgs: migration_backend_init_timeout: float = None migration_backend: str = None - migration_cache_blocks: int = None + migration_buffer_blocks: int = None migration_num_layers: int = None last_stage_max_blocks: int = None max_stages: int = None + migration_internal_buffer_num: int = None enable_pd_disagg: bool = None @@ -172,11 +172,12 @@ def create_global_scheduler_configs( def create_migration_config(self) -> MigrationConfig: migration_config = MigrationConfig(self.request_migration_policy, self.migration_backend, - self.migration_cache_blocks, + self.migration_buffer_blocks, self.migration_num_layers, self.last_stage_max_blocks, self.max_stages, - self.migration_backend_init_timeout) + self.migration_backend_init_timeout, + self.migration_internal_buffer_num) return migration_config @classmethod @@ -195,6 +196,9 @@ def check_args(cls, args: 'EngineManagerArgs', parser: argparse.ArgumentParser): if hasattr(action, 'choices') and action.choices is not None and hasattr(args, action.dest): assert getattr(args, action.dest) in action.choices, f"{action.dest} should be one of {action.choices}." + assert args.migration_backend != 'nccl', 'NCCL has been temporarily deprecated due to its incompatibility with \ + concurrent migrations in Llumnix.' + assert args.migration_backend != 'gloo' or (args.migration_backend == 'gloo' \ and not args.disable_init_instance_by_manager and not args.disable_fixed_node_init_instance), \ ("When using gloo as migration backend, " @@ -288,20 +292,23 @@ def add_cli_args( parser.add_argument('--migration-backend', type=str, - choices=['gloo','nccl','rpc'], + choices=['gloo', 'nccl', 'rpc'], help='communication backend of migration') parser.add_argument('--migration-backend-init-timeout', type=float, help='timeout(s) for initializing migration backend') - parser.add_argument('--migration-cache-blocks', + parser.add_argument('--migration-buffer-blocks', type=int, - help='number of cache blocks in migration') + help='number of cache blocks in each migration buffer') parser.add_argument('--migration-num-layers', type=int, help='number of kv-cache layers to transfer in each round during migration') parser.add_argument('--last-stage-max-blocks', type=int, help='if the number pf remain blocks < last_stage_max_blocks, do last stage migration') + parser.add_argument('--migration-internal-buffer-num', + type=int, + help='number of the buffer in migration backend for sending and receiving') parser.add_argument('--max-stages', type=int, help='drop migration if the number of stages > max_stages') diff --git a/llumnix/backends/migration_backend_interface.py b/llumnix/backends/migration_backend_interface.py index 808ba8c8..9fd231cc 100644 --- a/llumnix/backends/migration_backend_interface.py +++ b/llumnix/backends/migration_backend_interface.py @@ -13,7 +13,9 @@ from abc import ABC, abstractmethod from typing import List +import queue +import torch class MigrationBackendBase(ABC): @abstractmethod @@ -39,3 +41,24 @@ def do_send(self, dst_handle, blocks: List[int]): @abstractmethod def do_recv(self, src_handle, blocks: List[int]): raise NotImplementedError + +class BufferMigrationBackend(MigrationBackendBase): + def __init__(self, num_buffer, buffer_shape, buffer_dtype, buffer_device, pin_memory, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.num_buffer = num_buffer + + self.dummy_buffer = [ + torch.empty(size=buffer_shape, dtype=buffer_dtype, device=buffer_device, pin_memory=pin_memory) + for _ in range(self.num_buffer) + ] + + self.avaiable_buffer_queue = queue.Queue() + for i in range(self.num_buffer): + self.avaiable_buffer_queue.put_nowait(i) + + def get_available_cache(self): + return self.avaiable_buffer_queue.get() + + def put_back_cache(self, buffer_id): + self.avaiable_buffer_queue.put_nowait(buffer_id) diff --git a/llumnix/backends/vllm/llm_engine.py b/llumnix/backends/vllm/llm_engine.py index bf583366..4b2a076d 100644 --- a/llumnix/backends/vllm/llm_engine.py +++ b/llumnix/backends/vllm/llm_engine.py @@ -355,10 +355,10 @@ def commit_dst_request(self, backend_request: SequenceGroupLlumnix) -> None: async def send_blocks(self, dst_ray_actor: "ray.actor.ActorHandle", src_blocks: List[int], dst_blocks: List[int]) -> None: await dst_ray_actor.execute_engine_method.remote("_run_workers", - "migrate_cache", - dst_blocks=dst_blocks, - src_blocks=src_blocks, - src_worker_handle_list=self.worker_handle_list) + "migrate_cache", + dst_blocks=dst_blocks, + src_blocks=src_blocks, + src_worker_handle_list=self.worker_handle_list) def _run_workers(self, *args, **kwargs): # pylint: disable=protected-access diff --git a/llumnix/backends/vllm/migration_backend.py b/llumnix/backends/vllm/migration_backend.py index 947d3e7e..e69f3479 100644 --- a/llumnix/backends/vllm/migration_backend.py +++ b/llumnix/backends/vllm/migration_backend.py @@ -15,11 +15,15 @@ import torch from func_timeout import func_set_timeout, FunctionTimedOut +import cupy +from cupy.cuda import nccl import ray import ray.util.collective as col +from ray.util.collective.collective_group import nccl_util + from vllm.worker.cache_engine import CacheEngine from llumnix.internal_config import MigrationConfig -from llumnix.backends.migration_backend_interface import MigrationBackendBase +from llumnix.backends.migration_backend_interface import MigrationBackendBase, BufferMigrationBackend from llumnix.logger import init_logger logger = init_logger(__name__) @@ -40,17 +44,16 @@ def exec_method(self, is_driver_worker, handle, *args, **kwargs): NUMPY_SUPPORTED_DTYPES = [torch.float32, torch.float16] -class RayRpcMigrationBackend(MigrationBackendBase): +class RayRpcMigrationBackend(BufferMigrationBackend): def __init__(self, migration_config: MigrationConfig, cache_engine: CacheEngine, worker_rank, worker_handle_list, \ scheduling_strategy, is_driver_worker, gpu_cache) -> None: - super().__init__() - self.migration_config = migration_config self.cache_engine = cache_engine self.worker_rank = worker_rank self.worker_handle_list = worker_handle_list self.actor = ProxyActor.options(scheduling_strategy=scheduling_strategy).remote() + self.migration_stream = torch.cuda.Stream() self.rpc_dtype = self.cache_engine.dtype if self.cache_engine.dtype in NUMPY_SUPPORTED_DTYPES: @@ -62,17 +65,13 @@ def __init__(self, migration_config: MigrationConfig, cache_engine: CacheEngine, self.is_driver_worker = is_driver_worker self.gpu_cache = gpu_cache self.cache_device = "cpu" - self.num_migration_cache_blocks = self.migration_config.migration_cache_blocks + self.num_migration_buffer_blocks = self.migration_config.migration_buffer_blocks self.num_layers = self.cache_engine.num_layers self.migration_cache_size = self.cache_engine.block_size * self.cache_engine.num_heads * self.cache_engine.head_size + buffer_shape = (self.num_migration_buffer_blocks, self.num_layers, 2, self.migration_cache_size) - self.dummy_cache = torch.empty( - size=(self.num_migration_cache_blocks, self.num_layers, 2, self.migration_cache_size), - dtype=self.cache_engine.dtype, - device=self.cache_device, - pin_memory=True - ) - self.migration_stream = torch.cuda.Stream() + super().__init__(migration_config.migration_internal_buffer_num, buffer_shape, self.cache_engine.dtype, + self.cache_device, pin_memory=True) def init_backend(self, group_name, world_size, rank) -> bool: logger.info("create rpc migration backend successfully.") @@ -94,30 +93,38 @@ def warmup(self) -> bool: def migrate_cache(self, src_handle, src_blocks: List[int], dst_blocks: List[int]) -> None: tot_blocks = len(src_blocks) rpc_numpy_cache = None - for start_idx in range(0, tot_blocks, self.num_migration_cache_blocks): - offset = min(self.num_migration_cache_blocks, tot_blocks - start_idx) + for start_idx in range(0, tot_blocks, self.num_migration_buffer_blocks): + offset = min(self.num_migration_buffer_blocks, tot_blocks - start_idx) send_blocks = src_blocks[start_idx:start_idx+offset] ray_obj = self.actor.exec_method.remote(self.is_driver_worker, src_handle, "do_send", None, send_blocks) if rpc_numpy_cache is not None: self.do_recv(rpc_numpy_cache, recv_blocks) - rpc_numpy_cache = ray.get(ray_obj) + rpc_numpy_cache_ref = ray.get(ray_obj) + rpc_numpy_cache = ray.get(rpc_numpy_cache_ref) recv_blocks = dst_blocks[start_idx:start_idx+offset] self.do_recv(rpc_numpy_cache, recv_blocks) def do_send(self, dst_handle, blocks: List[int]): num_blocks = len(blocks) - send_cache = self.dummy_cache[:num_blocks].view(self.num_layers, 2, num_blocks, self.migration_cache_size) + dummy_cache_idx = self.get_available_cache() + send_cache = self.dummy_buffer[dummy_cache_idx][:num_blocks].view(self.num_layers, 2, num_blocks, self.migration_cache_size) src_to_dst = {block_num: idx for idx, block_num in enumerate(blocks)} with torch.cuda.stream(self.migration_stream): for layer_idx in range(self.num_layers): self.cache_engine.attn_backend.swap_blocks(self.gpu_cache[layer_idx], send_cache[layer_idx], src_to_dst) torch.cuda.Stream.synchronize(self.migration_stream) - return send_cache.to(self.rpc_dtype).numpy() + # Here, we use ray.put to store data and finally return the object reference so that we can release the internal buffer. + # This might seem like an anti-pattern, but it's okay since the kv-cache transferred is in the MB range and won't utilize + # Ray's optimization for returning small objects (<100KB). + data = ray.put(send_cache.to(self.rpc_dtype).numpy()) + self.put_back_cache(dummy_cache_idx) + return data def do_recv(self, src_handle, blocks: List[int]): num_blocks = len(blocks) src_to_dst = dict(enumerate(blocks)) - recv_cache = self.dummy_cache[:num_blocks].view(self.num_layers, 2, num_blocks, self.migration_cache_size) + dummy_cache_idx = self.get_available_cache() + recv_cache = self.dummy_buffer[dummy_cache_idx][:num_blocks].view(self.num_layers, 2, num_blocks, self.migration_cache_size) # use pin memory dummy_cache to speed up data transfer recv_cache.copy_(torch.from_numpy(src_handle)) @@ -125,6 +132,7 @@ def do_recv(self, src_handle, blocks: List[int]): for layer_idx in range(self.num_layers): self.cache_engine.attn_backend.swap_blocks(recv_cache[layer_idx], self.gpu_cache[layer_idx], src_to_dst) torch.cuda.Stream.synchronize(self.migration_stream) + self.put_back_cache(dummy_cache_idx) def try_import_gloo(): try: @@ -139,19 +147,14 @@ def try_import_gloo(): except ImportError as e: raise ImportError("Gloo is not installed. Please install it first.") from e -class RayColMigrationBackend(MigrationBackendBase): +class RayColMigrationBackend(BufferMigrationBackend): def __init__(self, migration_config: MigrationConfig, cache_engine: CacheEngine, local_rank, scheduling_strategy, is_driver_worker, gpu_cache) -> None: - super().__init__() - - # pylint: disable=C0415 - import cupy - self.migration_config = migration_config self.cache_engine = cache_engine self.backend = migration_config.migration_backend self.migration_num_layers = min(migration_config.migration_num_layers, self.cache_engine.num_layers) - self.num_migration_cache_blocks = migration_config.migration_cache_blocks + self.num_migration_buffer_blocks = migration_config.migration_buffer_blocks self.backend = migration_config.migration_backend self.global_world_size = -1 @@ -162,6 +165,7 @@ def __init__(self, migration_config: MigrationConfig, cache_engine: CacheEngine, self.actor = ProxyActor.options(scheduling_strategy=scheduling_strategy).remote() self.is_driver_worker = is_driver_worker self.gpu_cache = gpu_cache + self.migration_stream = cupy.cuda.Stream() self.migration_cache_size = self.cache_engine.block_size * self.cache_engine.num_heads * self.cache_engine.head_size @@ -169,17 +173,13 @@ def __init__(self, migration_config: MigrationConfig, cache_engine: CacheEngine, try_import_gloo() self.cache_device = "cpu" else: + nccl_util.TORCH_NCCL_DTYPE_MAP[torch.bfloat16] = nccl.NCCL_FLOAT16 self.cache_device = torch.device(f"cuda:{self.local_rank}") pin_memory = (self.backend == 'gloo') - self.dummy_cache = torch.empty( - size=(self.num_migration_cache_blocks, self.migration_num_layers, 2, self.migration_cache_size), - dtype=self.cache_engine.dtype, - device=self.cache_device, - pin_memory=pin_memory - ) - - self.migration_stream = cupy.cuda.Stream() + buffer_shape = (self.num_migration_buffer_blocks, self.migration_num_layers, 2, self.migration_cache_size) + super().__init__(migration_config.migration_internal_buffer_num, buffer_shape, self.cache_engine.dtype, + self.cache_device, pin_memory=pin_memory) def init_backend(self, group_name, world_size, rank) -> bool: @func_set_timeout(self.migration_config.migration_backend_init_timeout) @@ -224,7 +224,7 @@ def destory_backend(self) -> None: def warmup(self) -> bool: if self.global_world_size > 1: try: - col.allreduce(self.dummy_cache[0], self.group_name) + col.allreduce(self.dummy_buffer[0][0], self.group_name) # pylint: disable=W0703 except Exception as e: logger.info("warmup migration backend failed (group_name: {}, world_size: {}, rank: {}, backbend: {}), err: {}." @@ -241,8 +241,8 @@ def migrate_cache(self, src_handle, src_blocks: List[int], dst_blocks: List[int] tot_blocks = len(src_blocks) src_rank = ray.get(self.actor.exec_method.remote(self.is_driver_worker, src_handle, "get_global_rank")) - for start_idx in range(0, tot_blocks, self.num_migration_cache_blocks): - offset = min(self.num_migration_cache_blocks, tot_blocks - start_idx) + for start_idx in range(0, tot_blocks, self.num_migration_buffer_blocks): + offset = min(self.num_migration_buffer_blocks, tot_blocks - start_idx) send_blocks = src_blocks[start_idx:start_idx+offset] recv_blocks = dst_blocks[start_idx:start_idx+offset] self.actor.exec_method.remote(self.is_driver_worker, src_handle, "do_send", self.global_rank, send_blocks) @@ -250,7 +250,8 @@ def migrate_cache(self, src_handle, src_blocks: List[int], dst_blocks: List[int] def do_send(self, dst_handle, blocks: List[int]): num_blocks = len(blocks) - send_cache = self.dummy_cache[:num_blocks].view(self.migration_num_layers, 2, num_blocks, self.migration_cache_size) + dummy_cache_idx = self.get_available_cache() + send_cache = self.dummy_buffer[dummy_cache_idx][:num_blocks].view(self.migration_num_layers, 2, num_blocks, self.migration_cache_size) src_to_dst = {block_num: idx for idx, block_num in enumerate(blocks)} with self.migration_stream: @@ -261,11 +262,13 @@ def do_send(self, dst_handle, blocks: List[int]): # TODO(KuilongCui): check the error code if peer is dead col.send(send_cache, dst_handle, self.group_name) self.migration_stream.synchronize() + self.put_back_cache(dummy_cache_idx) def do_recv(self, src_handle, blocks: List[int]): num_blocks = len(blocks) src_to_dst = dict(enumerate(blocks)) - recv_cache = self.dummy_cache[:num_blocks].view(self.migration_num_layers, 2, num_blocks, self.migration_cache_size) + dummy_cache_idx = self.get_available_cache() + recv_cache = self.dummy_buffer[dummy_cache_idx][:num_blocks].view(self.migration_num_layers, 2, num_blocks, self.migration_cache_size) with self.migration_stream: for layer_idx in range(self.cache_engine.num_layers): @@ -274,16 +277,19 @@ def do_recv(self, src_handle, blocks: List[int]): col.recv(recv_cache, src_handle, self.group_name) self.cache_engine.attn_backend.swap_blocks(recv_cache[cache_idx], self.gpu_cache[layer_idx], src_to_dst) self.migration_stream.synchronize() + self.put_back_cache(dummy_cache_idx) def get_migration_backend(migration_config: MigrationConfig, cache_engine: CacheEngine, worker_handle_list, scheduling_strategy, is_driver_worker, gpu_cache, worker_rank, local_rank) -> MigrationBackendBase: - if cache_engine.num_gpu_blocks < migration_config.migration_cache_blocks: - logger.warning("migration_cache_blocks({}) is larger than num_gpu_blocks({}), reducing it to num_gpu_blocks." - .format(migration_config.migration_cache_blocks, cache_engine.num_gpu_blocks)) - migration_config.migration_cache_blocks = cache_engine.num_gpu_blocks + if cache_engine.num_gpu_blocks < migration_config.migration_buffer_blocks: + logger.warning("migration_buffer_blocks({}) is larger than num_gpu_blocks({}), reducing it to num_gpu_blocks." + .format(migration_config.migration_buffer_blocks, cache_engine.num_gpu_blocks)) + migration_config.migration_buffer_blocks = cache_engine.num_gpu_blocks target_col = None backend = migration_config.migration_backend + assert backend in ['nccl', 'gloo', 'rpc'], "Unsupported backend: {} for VLLM".format(backend) + if backend in ['nccl', 'gloo']: target_col = RayColMigrationBackend(migration_config, cache_engine, local_rank, scheduling_strategy, is_driver_worker, gpu_cache) diff --git a/llumnix/backends/vllm/worker.py b/llumnix/backends/vllm/worker.py index 92bf1f1b..2b0cab33 100644 --- a/llumnix/backends/vllm/worker.py +++ b/llumnix/backends/vllm/worker.py @@ -14,7 +14,6 @@ import time from typing import Dict, List import math -import ray import torch from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy, NodeAffinitySchedulingStrategy @@ -50,10 +49,11 @@ def get_global_rank(self): def reserve_memory_for_migration(self, migration_config: MigrationConfig, model_config: ModelConfig, cache_config: CacheConfig, parallel_config: ParallelConfig) -> int: - migrate_cache_blocks_size = migration_config.migration_cache_blocks + migrate_cache_blocks_size = migration_config.migration_buffer_blocks migrate_num_layers = migration_config.migration_num_layers - dummy_cache_size = migrate_num_layers * migrate_cache_blocks_size * CacheEngine.get_cache_block_size( - cache_config, model_config, parallel_config) // model_config.get_num_layers(parallel_config) + dummy_cache_size = migration_config.migration_internal_buffer_num * migrate_num_layers * migrate_cache_blocks_size \ + * CacheEngine.get_cache_block_size(cache_config, model_config, parallel_config) \ + // model_config.get_num_layers(parallel_config) # For nccl migration backend, reserve gpu memory for dummy cache in migration backend. For other backends, # CPU memory is used for the dummy cache, which is almost unlimited, so no special action is needed. @@ -111,14 +111,16 @@ def migrate_cache(self, src_worker_handle_list, src_blocks: List[int], dst_block start_time = time.time() try: self.migration_backend.migrate_cache(src_worker_handle, src_blocks, dst_blocks) - except ray.exceptions.RayActorError: - logger.info("[migrate_cache] self.rank: {}, src_worker_handle {} is dead".format(self.rank, src_worker_handle)) + # pylint: disable=broad-except + except Exception as e: + logger.info("[migrate_cache] self.rank: {}, src_worker_handle {}, meet error : {}" + .format(self.rank, src_worker_handle, e)) end_time = time.time() total_kv_cache_size = len(src_blocks) * CacheEngine.get_cache_block_size( self.cache_config, self.model_config, self.parallel_config) speed = total_kv_cache_size/_GB/(end_time - start_time) - logger.info("[migration_cache] blocks_num: {}, total_kv_cache_size: {}, time: {}s, speed: {}GB/s." + logger.info("[migrate_cache] blocks_num: {}, total_kv_cache_size: {}, time: {}s, speed: {}GB/s." .format(len(src_blocks), convert_bytes(total_kv_cache_size), end_time-start_time, speed)) def do_recv(self, *args, **kwargs): @@ -150,7 +152,3 @@ def shutdown(self) -> None: del self.migration_backend torch.cuda.empty_cache() torch.cuda.reset_max_memory_allocated() - - def restart(self) -> None: - self.init_model() - self.init_cache_engine(self.cache_config) diff --git a/llumnix/config/default.py b/llumnix/config/default.py index fb94443b..2a6c7758 100644 --- a/llumnix/config/default.py +++ b/llumnix/config/default.py @@ -108,9 +108,11 @@ # Timeout(s) for initializing migration backend _C.MANAGER.MIGRATION_BACKEND_INIT_TIMEOUT = 10.0 # Number of cache blocks in migration -_C.MANAGER.MIGRATION_CACHE_BLOCKS = 512 +_C.MANAGER.MIGRATION_BUFFER_BLOCKS = 512 # Number of kv-cache layers to transfer in each round during migration _C.MANAGER.MIGRATION_NUM_LAYERS = 1 +# Number of internal cache size in migration backend for sending and receiving +_C.MANAGER.MIGRATION_INTERNAL_BUFFER_NUM = 2 # ----------------------------------------------------------------------------- # SCALING CONFIGURATION diff --git a/llumnix/global_scheduler/dispatch_scheduler.py b/llumnix/global_scheduler/dispatch_scheduler.py index 175bdbde..27458f26 100644 --- a/llumnix/global_scheduler/dispatch_scheduler.py +++ b/llumnix/global_scheduler/dispatch_scheduler.py @@ -72,6 +72,10 @@ def remove_instance(self, instance_id: str) -> None: if instance_id in self.available_dispatch_instance_set: self.available_dispatch_instance_set.remove(instance_id) + if self.num_instances >= self.num_dispatch_instances: + free_instance_id = next(iter(self.instance_id_set - self.available_dispatch_instance_set)) + self.available_dispatch_instance_set.add(free_instance_id) + def _sort_instance_infos(self, descending: bool = True) -> None: instance_infos: List[InstanceInfo] = list(self.instance_info.values()) diff --git a/llumnix/global_scheduler/migration_scheduler.py b/llumnix/global_scheduler/migration_scheduler.py index 3445b210..77fd9b25 100644 --- a/llumnix/global_scheduler/migration_scheduler.py +++ b/llumnix/global_scheduler/migration_scheduler.py @@ -170,8 +170,10 @@ def pair_migration(self, migrate_instance_pairs = [] for i in range(min(len(sorted_src_instance_infos), len(sorted_dst_instance_infos))): load_diff_before_mig = sorted_src_instance_infos[i].instance_load_migrate - sorted_dst_instance_infos[i].instance_load_migrate + left_load_after_mig = self._compute_instance_load_after_migrate(sorted_src_instance_infos[i], is_migrate_in=False) right_load_after_mig = self._compute_instance_load_after_migrate(sorted_dst_instance_infos[i], is_migrate_in=True) + # Add some constrains to reduce unnecessary migrations if right_load_after_mig > self.migrate_out_load_threshold: continue @@ -184,12 +186,14 @@ def pair_migration(self, def _compute_instance_load_after_migrate(self, instance_info: InstanceInfo, is_migrate_in: bool) -> float: instance_info_after_migrate = copy.deepcopy(instance_info) num_blocks_last_running_request = instance_info_after_migrate.num_blocks_last_running_request + if is_migrate_in: instance_info_after_migrate.num_running_requests += 1 instance_info_after_migrate.num_free_gpu_blocks -= num_blocks_last_running_request else: instance_info_after_migrate.num_running_requests -= 1 instance_info_after_migrate.num_free_gpu_blocks += num_blocks_last_running_request + return self.instance_load_calculator.compute_instance_load(instance_info_after_migrate, action='migrate') class DefragConstrained(PairMigrationPolicy): diff --git a/llumnix/internal_config.py b/llumnix/internal_config.py index 410d38e0..08f5283f 100644 --- a/llumnix/internal_config.py +++ b/llumnix/internal_config.py @@ -16,18 +16,20 @@ def __init__( self, request_migration_policy: str, migration_backend: str, - migration_cache_blocks: int, + migration_buffer_blocks: int, migration_num_layers: int, last_stage_max_blocks: int, max_stages: int, - migration_backend_init_timeout: float) -> None: + migration_backend_init_timeout: float, + migration_internal_buffer_num: int) -> None: self.request_migration_policy = request_migration_policy self.migration_backend = migration_backend self.migration_num_layers = migration_num_layers - self.migration_cache_blocks = migration_cache_blocks + self.migration_buffer_blocks = migration_buffer_blocks self.last_stage_max_blocks = last_stage_max_blocks self.max_stages = max_stages self.migration_backend_init_timeout = migration_backend_init_timeout + self.migration_internal_buffer_num = migration_internal_buffer_num class GlobalSchedulerConfig: def __init__( diff --git a/llumnix/llm_engine_manager.py b/llumnix/llm_engine_manager.py index 7b47728b..d98f3a8e 100644 --- a/llumnix/llm_engine_manager.py +++ b/llumnix/llm_engine_manager.py @@ -42,8 +42,6 @@ RETRIES_INTERVALS = 5.0 # TODO(s5u13b): Fix the logger when manager failover. - - class LLMEngineManager: def __init__(self, engine_manager_args: EngineManagerArgs, @@ -71,10 +69,7 @@ def __init__(self, logger.info("num_instances: {}".format(self.num_instances)) logger.info("max_instances: {}, min_instances: {}".format(self.max_instances, self.min_instances)) - # TODO(s5u13b): refactor auto-scaling - self.instances: Dict[str, Llumlet] = {} - self.instance_migrating: Dict[str, bool] = {} self.pending_rebuild_migration_instances = 0 self.global_scheduler = GlobalScheduler(global_scheduler_config) @@ -92,8 +87,9 @@ def __init__(self, # migrate states self.num_instance_info_updates = 0 - self.migrating = False + self.num_migrating = 0 + # TODO(s5u13b): refactor auto-scaling # auto-scaling states self.scale_up_time = -1 self.scale_down_time = -1 @@ -184,26 +180,31 @@ def update_instance_info_done_callback(instance_id: str, fut): self.global_scheduler.update_instance_infos([ret]) else: dead_instance_ids.append(instance_id) + while True: try: await asyncio.sleep(interval) tasks = [] instance_infos = [] dead_instance_ids = [] + for instance_id, instance in self.instances.items(): # Use asyncio.gather to wrap ray remote call to add done callback. task = asyncio.gather(instance.get_instance_info.remote(), return_exceptions=True) task.add_done_callback(partial(update_instance_info_done_callback, instance_id)) tasks.append(task) await asyncio.gather(*tasks, return_exceptions=True) + if len(dead_instance_ids) > 0: logger.info("[_update_instance_info_loop] dead instances: {}.".format(dead_instance_ids)) self.scale_down(dead_instance_ids) self.num_instance_info_updates += 1 + # Push migrate when the instance_info have updated a certain number of times. if self.enable_migration and self.num_instance_info_updates != 0 \ and self.num_instance_info_updates % self.pair_migration_frequency == 0: asyncio.create_task(self._push_migrations()) + if self.log_instance_info: self._log_instance_infos_to_csv(instance_infos) # pylint: disable=W0703 @@ -217,6 +218,7 @@ async def _clear_request_instance_loop(self, interval: float): while True: await asyncio.sleep(interval) self.request_instance = {} + async def _push_migrations(self) -> None: # Push migrate when the instance_info have updated a certain number of times. if self.enable_pd_disagg: @@ -227,10 +229,7 @@ async def _push_migrations(self) -> None: async def _migrate(self, pair_migration_type: PairMigrationConstraints, migrate_in_num_requests: int) -> None: async def migrate_done_callback(ret, migrate_instance_pair: Tuple[str, str]) -> None: - if migrate_instance_pair[0] in self.instance_migrating: - self.instance_migrating[migrate_instance_pair[0]] = False - if migrate_instance_pair[1] in self.instance_migrating: - self.instance_migrating[migrate_instance_pair[1]] = False + self.num_migrating -= 1 if isinstance(ret, (ray.exceptions.RayActorError, KeyError)): has_error_pair = await self._check_instance_error(migrate_instance_pair) for i, has_error in enumerate(has_error_pair): @@ -252,19 +251,20 @@ async def migrate_done_callback(ret, migrate_instance_pair: Tuple[str, str]) -> self.request_instance[migrate_out_request_id] = migrate_instance_pair[1] logger.info("{}->{} migrate done, migrate request {}".format( migrate_instance_pair[0], migrate_instance_pair[1], migrate_out_request_ids)) + def migrate_done_callback_wrapper(migrate_instance_pair: Tuple[str, str], fut) -> None: ret = fut.result() loop = asyncio.get_event_loop() loop.create_task(migrate_done_callback(ret, migrate_instance_pair)) - migrate_instance_pairs = self.global_scheduler.pair_migration(pair_migration_type) + try: + migrate_instance_pairs = self.global_scheduler.pair_migration(pair_migration_type) + migration_tasks = [] for _, migrate_instance_pair in enumerate(migrate_instance_pairs): + self.num_migrating += 1 migrate_out_instance_id, migrate_in_instance_id = migrate_instance_pair - if self.instance_migrating[migrate_out_instance_id] or self.instance_migrating[migrate_in_instance_id]: - continue - self.instance_migrating[migrate_out_instance_id] = True - self.instance_migrating[migrate_in_instance_id] = True + migrate_in_instance_name = "instance_{}".format(migrate_in_instance_id) # Use asyncio.gather to wrap ray remote call to add done callback. task = asyncio.gather(self.instances[migrate_out_instance_id].migrate_out.remote(migrate_in_instance_name, migrate_in_num_requests), @@ -280,7 +280,7 @@ def migrate_done_callback_wrapper(migrate_instance_pair: Tuple[str, str], fut) - async def rebuild_migrate_backend(self) -> None: # Wait for all instances to finish migration - while any(self.instance_migrating.values()): + while self.num_migrating > 0: await asyncio.sleep(0.1) # During rebuilding migration backend, disable migrate @@ -353,7 +353,6 @@ def scale_up(self, instance_id: Union[str, Iterable[str]], llumlet_actor_handles if ins_id not in self.instances: indeed_update = True self.instances[ins_id] = llumlet_actor_handles[idx] - self.instance_migrating[ins_id] = False if self.log_instance_info: self.instance_last_logged_empty[ins_id] = False self.pending_rebuild_migration_instances += 1 @@ -381,7 +380,6 @@ def scale_down(self, instance_id: Union[str, Iterable[str]], rebuild_migrate_bac if ins_id in self.instances: indeed_update = True del self.instances[ins_id] - del self.instance_migrating[ins_id] if self.log_instance_info: del self.instance_last_logged_empty[ins_id] self.pending_rebuild_migration_instances += 1 diff --git a/llumnix/llumlet/llumlet.py b/llumnix/llumlet/llumlet.py index 5aa3e4c2..42be2f2c 100644 --- a/llumnix/llumlet/llumlet.py +++ b/llumnix/llumlet/llumlet.py @@ -130,6 +130,7 @@ async def check_state(self): async def migrate_out(self, dst_instance_name: str, num_requests: int) -> List[str]: try: + migrate_out_request = None migrate_in_ray_actor = ray.get_actor(dst_instance_name, namespace='llumnix') dst_instance_id = dst_instance_name[len("instance_"):] migrated_request_list = [] @@ -137,12 +138,13 @@ async def migrate_out(self, dst_instance_name: str, num_requests: int) -> List[s while continue_migrate and len(migrated_request_list) < num_requests: t0 = time.time() migrate_out_request = self.migration_scheduler.get_migrate_out_request() - if migrate_out_request is not None: - logger.info("migrate_out {}".format(migrate_out_request.request_id)) if migrate_out_request is None: - return migrated_request_list + break + + migrate_out_request.migrating = True logger.info("{}->{} begin migrate out {}".format(self.instance_id, dst_instance_id, migrate_out_request.request_id)) status = await self.migration_coordinator.migrate_out_multistage(migrate_in_ray_actor, migrate_out_request) + if status == MigrationStatus.FINISHED_DONE: await migrate_in_ray_actor.execute_engine_method.remote("commit_dst_request", migrate_out_request) self.backend_engine.free_src_request(migrate_out_request) @@ -157,8 +159,13 @@ async def migrate_out(self, dst_instance_name: str, num_requests: int) -> List[s logger.info("{}->{} migrate done, migrate request {}, status:{}, len:{} blocks, cost:{} ms" \ .format(self.instance_id, dst_instance_id, migrated_request_list, status, \ sum(migrate_out_request.stage_num_blocks_list), (t1 - t0)*1000)) - except ray.exceptions.RayActorError: - logger.info("[migrate_out] instance {} is dead".format(dst_instance_name[len("instance_"):])) + # pylint: disable=broad-except + except Exception as e: + if migrate_out_request: + migrate_out_request.reset_migration_args() + + logger.info("[migrate_out] src instance {}, dst instance {}, meet error: {}" + .format(self.instance_id, dst_instance_name[len("instance_"):], e)) raise return migrated_request_list diff --git a/llumnix/llumlet/local_migration_scheduler.py b/llumnix/llumlet/local_migration_scheduler.py index e630d982..ad676cc1 100644 --- a/llumnix/llumlet/local_migration_scheduler.py +++ b/llumnix/llumlet/local_migration_scheduler.py @@ -39,35 +39,52 @@ def get_migrate_out_request(self, min_request_len=0, max_request_len=np.inf) -> # and only selects request that migrates from the prefill instance to the decoding instance. def get_ready_migration_request(self, min_request_len, max_request_len): running: List[LlumnixRequest] = self.backend_engine.get_running_queue() + target_request: LlumnixRequest = None for request in reversed(running): + if request.migrating: + continue + if request.output_len >= request.expected_steps \ and request.inference_type == RequestInferenceType.DECODE \ and min_request_len <= request.request_len <= max_request_len: - return request - return None + target_request = request + break + + return target_request def get_last_running_request(self, min_request_len, max_request_len): running: List[LlumnixRequest] = self.backend_engine.get_running_queue() + target_request: LlumnixRequest = None + for request in reversed(running): + if request.migrating: + continue + if request.inference_type == RequestInferenceType.DECODE \ and min_request_len <= request.request_len <= max_request_len: - return request - return None + target_request=request + break + + return target_request def get_longest_running_request(self, min_request_len, max_request_len): running: List[LlumnixRequest] = self.backend_engine.get_running_queue() condition = lambda request : request.inference_type == RequestInferenceType.DECODE \ - and min_request_len <= request.request_len <= max_request_len + and min_request_len <= request.request_len <= max_request_len \ + and (not request.migrating) longest_seq_group = max((request for request in running if condition(request)), \ key=lambda request: request.request_len, default=None) + return longest_seq_group def get_shortest_running_request(self, min_request_len, max_request_len): running: List[LlumnixRequest] = self.backend_engine.get_running_queue() condition = lambda request : request.inference_type == RequestInferenceType.DECODE \ - and min_request_len <= request.request_len <= max_request_len + and min_request_len <= request.request_len <= max_request_len \ + and (not request.migrating) shortest_seq_group = min((request for request in running if condition(request)), \ key=lambda request: request.request_len, default=None) + return shortest_seq_group diff --git a/llumnix/llumlet/request.py b/llumnix/llumlet/request.py index 2319f52f..c2aeda9e 100644 --- a/llumnix/llumlet/request.py +++ b/llumnix/llumlet/request.py @@ -32,6 +32,7 @@ def __init__(self, request_id: int, server_info: ServerInfo, expected_steps: int self.last_preemption_time = None self.stage_timestamps = [] self.stage_num_blocks_list = [] + self.migrating = False def reset_migration_args(self): self.last_preemption_time = None @@ -39,6 +40,7 @@ def reset_migration_args(self): self.stage_num_blocks_list = [] # By default, there is no limit on the number of steps expected for the request. self.expected_steps = math.inf + self.migrating = False def is_finished(self) -> bool: raise NotImplementedError diff --git a/tests/conftest.py b/tests/conftest.py index 2749ba00..ba3b467c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,20 +12,16 @@ # limitations under the License. import subprocess -from time import sleep import ray import pytest def pytest_sessionstart(session): - subprocess.run(["ray", "stop", "--force"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - sleep(3) + subprocess.run(["ray", "stop"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) subprocess.run(["ray", "start", "--head", "--disable-usage-stats", "--port=6379"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - sleep(3) def pytest_sessionfinish(session, exitstatus): subprocess.run(["ray", "stop"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - sleep(3) @pytest.fixture def setup_ray_env(): diff --git a/tests/e2e_test/test_bench.py b/tests/e2e_test/test_bench.py index b6d70d8f..eb93fb89 100644 --- a/tests/e2e_test/test_bench.py +++ b/tests/e2e_test/test_bench.py @@ -20,7 +20,8 @@ import numpy as np from .test_e2e import generate_launch_command, clear_ray_state -from .utils import to_markdown_table +# pylint: disable=unused-import +from .utils import to_markdown_table, clean_ray def launch_llumnix_service(command): subprocess.run(command, shell=True, check=True) @@ -90,7 +91,7 @@ def get_markdown_data(key: str, head_name: str): @pytest.mark.asyncio @pytest.mark.skipif(torch.cuda.device_count() < 1, reason="at least 1 gpus required for simple benchmark") @pytest.mark.parametrize("model", ['/mnt/model/Qwen-7B']) -async def test_simple_benchmark(model): +async def test_simple_benchmark(clean_ray, model): device_count = torch.cuda.device_count() base_port = 37037 for i in range(device_count): @@ -107,7 +108,7 @@ async def run_bench_command(command): tasks = [] for i in range(device_count): - bench_command = generate_bench_command(ip_ports=f"127.0.0.1:{base_port+i}", model=model, num_prompts=500, + bench_command = generate_bench_command(ip_ports=f"127.0.0.1:{base_port+i}", model=model, num_prompts=300, dataset_type="sharegpt", dataset_path="/mnt/dataset/sharegpt_gpt4/sharegpt_gpt4.jsonl" , qps=2, diff --git a/tests/e2e_test/test_e2e.py b/tests/e2e_test/test_e2e.py index 741360f1..11b8617f 100644 --- a/tests/e2e_test/test_e2e.py +++ b/tests/e2e_test/test_e2e.py @@ -20,7 +20,8 @@ import torch from vllm import LLM, SamplingParams - +# pylint: disable=unused-import +from .utils import clean_ray def parse_launch_mode(launch_mode: str): # 'eief' means that enable init instance by manager and enable fixed node init instance, and so on. @@ -46,7 +47,7 @@ def generate_launch_command(result_filename: str = "", launch_ray_cluster: bool disable_init_instance_by_manager, disable_fixed_node_init_instance = parse_launch_mode(launch_mode) command = ( f"RAY_DEDUP_LOGS=0 HEAD_NODE_IP={HEAD_NODE_IP} HEAD_NODE=1 " - f"nohup python -m llumnix.entrypoints.vllm.api_server " + f"nohup python -u -m llumnix.entrypoints.vllm.api_server " f"--host {ip} " f"--port {port} " f"{'--disable-init-instance-by-manager ' if disable_init_instance_by_manager else ''}" @@ -63,7 +64,8 @@ def generate_launch_command(result_filename: str = "", launch_ray_cluster: bool f"--trust-remote-code " f"--request-migration-policy LCFS " f"--migration-backend {migration_backend} " - f"--migration-cache-blocks 32 " + f"--migration-buffer-blocks 32 " + f"--migration-internal-buffer-num 2 " f"--tensor-parallel-size 1 " f"--request-output-queue-port {1234+port} " f"{'--enable-pd-disagg ' if enable_pd_disagg else ''} " @@ -123,6 +125,8 @@ async def get_llumnix_response(prompt, sampling_params, ip_ports): "The future of AI is", ] +vllm_output = {} + @ray.remote(num_gpus=1) def run_vllm(model, max_model_len, sampling_params): vllm_output = {} @@ -137,9 +141,9 @@ def run_vllm(model, max_model_len, sampling_params): @pytest.mark.asyncio @pytest.mark.skipif(torch.cuda.device_count() < 1, reason="at least 1 gpus required for e2e test") @pytest.mark.parametrize("model", ['/mnt/model/Qwen-7B']) -@pytest.mark.parametrize("migration_backend", ['rpc', 'gloo', 'nccl']) +@pytest.mark.parametrize("migration_backend", ['rpc', 'gloo']) @pytest.mark.parametrize("launch_mode", ['eief', 'eidf', 'dief', 'didf']) -async def test_e2e(model, migration_backend, launch_mode): +async def test_e2e(clean_ray, model, migration_backend, launch_mode): if migration_backend == 'gloo' and launch_mode != 'eief': pytest.skip("When the migration backend is gloo, the launch mode of llumnix can only be eief") max_model_len = 370 @@ -165,9 +169,12 @@ async def test_e2e(model, migration_backend, launch_mode): shutdown_llumnix_service() - vllm_output = ray.get(run_vllm.remote(model, max_model_len, sampling_params)) - clear_ray_state() + global vllm_output + if len(vllm_output) == 0: + vllm_output = ray.get(run_vllm.remote(model, max_model_len, sampling_params)) + + clear_ray_state() # compare for prompt in prompts: assert llumnix_output[prompt] == vllm_output[prompt] diff --git a/tests/e2e_test/test_migration.py b/tests/e2e_test/test_migration.py index ddf7fb51..b1f446f1 100644 --- a/tests/e2e_test/test_migration.py +++ b/tests/e2e_test/test_migration.py @@ -22,7 +22,8 @@ from .test_e2e import generate_launch_command from .test_bench import generate_bench_command, clear_ray_state, shutdown_llumnix_service -from .utils import to_markdown_table +# pylint: disable=unused-import +from .utils import to_markdown_table, clean_ray size_pattern = re.compile(r'total_kv_cache_size:\s*([\d.]+)\s*(B|KB|MB|GB|KB|TB)') speed_pattern = re.compile(r'speed:\s*([\d.]+)GB/s') @@ -66,21 +67,19 @@ def parse_manager_log_file(log_file): @pytest.mark.asyncio @pytest.mark.skipif(torch.cuda.device_count() < 2, reason="at least 2 gpus required for migration bench") @pytest.mark.parametrize("model", ['/mnt/model/Qwen-7B']) -@pytest.mark.parametrize("migration_backend", ['rpc', 'gloo', 'nccl']) -@pytest.mark.parametrize("enable_pd_disagg", [False, True]) -async def test_migration_benchmark(model, migration_backend, enable_pd_disagg): +@pytest.mark.parametrize("migration_backend", ['rpc', 'gloo']) +async def test_migration_benchmark(clean_ray, model, migration_backend): base_port = 37037 instance_output_logs = [] device_count = torch.cuda.device_count() - num_dispatch_instances = device_count//2 if enable_pd_disagg else math.inf for i in range(device_count): output_log = f"{base_port+i}.out" instance_output_logs.append("instance_"+output_log) launch_command = generate_launch_command(result_filename=output_log, launch_ray_cluster=False, port=base_port+i, model=model, dispatch_policy="flood", migration_backend=migration_backend, - log_instance_info=True, enable_pd_disagg=enable_pd_disagg, - num_dispatch_instances=num_dispatch_instances) + log_instance_info=True, enable_pd_disagg=False, + num_dispatch_instances=math.inf) subprocess.run(launch_command, shell=True, check=True) await asyncio.sleep(60) @@ -89,13 +88,19 @@ async def run_bench_command(command): await process.wait() assert process.returncode == 0 + tasks = [] for i in range(device_count//2): bench_command = generate_bench_command(ip_ports=f"127.0.0.1:{base_port+i}", model=model, num_prompts=300, dataset_type="sharegpt", dataset_path="/mnt/dataset/sharegpt_gpt4/sharegpt_gpt4.jsonl" , - qps=10) - await asyncio.wait_for(run_bench_command(bench_command), timeout=60*30) - await asyncio.sleep(30) + qps=10, + results_filename=f"{base_port+i}.out") + tasks.append(asyncio.create_task(run_bench_command(bench_command))) + + _, pending = await asyncio.wait(tasks, timeout=60*30) + + if len(pending) > 0: + raise RuntimeError("migration task Timeout") parse_manager_log_file("manager_instance.csv") diff --git a/tests/e2e_test/utils.py b/tests/e2e_test/utils.py index 62d9bff8..492eb2fd 100644 --- a/tests/e2e_test/utils.py +++ b/tests/e2e_test/utils.py @@ -11,6 +11,10 @@ # See the License for the specific language governing permissions and # limitations under the License. + +import subprocess +import pytest + def to_markdown_table(data): headers = data[0] rows = data[1:] @@ -27,3 +31,11 @@ def to_markdown_table(data): table = f"{header_row}\n{separator_row}\n" + "\n".join(data_rows) + "\n\n" return table + +@pytest.fixture +def clean_ray(): + subprocess.run(["ray", "stop"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.run(["ray", "start", "--head", "--disable-usage-stats", "--port=6379"], check=True, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + yield + subprocess.run(["ray", "stop"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) diff --git a/tests/unit_test/backends/vllm/test_migration.py b/tests/unit_test/backends/vllm/test_migration.py index 2a8ad19e..e5bf4567 100644 --- a/tests/unit_test/backends/vllm/test_migration.py +++ b/tests/unit_test/backends/vllm/test_migration.py @@ -56,7 +56,7 @@ def __init__(self): async def test_migration_correctness(setup_ray_env, migration_backend): engine_args = EngineArgs(model="facebook/opt-125m", worker_use_ray=True) id_rank_map = {"0":0, "1":1} - migration_config = MigrationConfig("LCFS", migration_backend, 16, 1, 4, 5, 20) + migration_config = MigrationConfig("LCFS", migration_backend, 16, 1, 4, 5, 20, 2) output_queue_type = QueueType.RAYQUEUE que, server_info = request_output_queue_server(output_queue_type) @@ -144,9 +144,9 @@ async def test_correctness(prompt): @pytest.mark.parametrize("migration_backend", ['rpc', 'gloo', 'nccl']) @pytest.mark.asyncio async def test_pd_diaggregation_correctness(setup_ray_env, migration_backend): - engine_args = EngineArgs(model="facebook/opt-125m",worker_use_ray=True) - id_rank_map = {"0":0,"1":1} - migration_config = MigrationConfig("LCFS", migration_backend, 16, 1, 4, 5, 20) + engine_args = EngineArgs(model="facebook/opt-125m", worker_use_ray=True) + id_rank_map = {"0":0, "1":1} + migration_config = MigrationConfig("LCFS", migration_backend, 16, 1, 4, 5, 20, 2) output_queue_type = QueueType.RAYQUEUE que, server_info = request_output_queue_server(output_queue_type) @@ -174,12 +174,15 @@ async def test_pd_diaggregation_correctness(setup_ray_env, migration_backend): migration_config, engine_args, ) + while True: res = ray.get([llumlet_0.is_ready.remote(),llumlet_1.is_ready.remote()]) if all(res): break - ray.get([llumlet_0.execute_engine_method.remote("_run_workers","rebuild_migration_backend", id_rank_map, "llumnix"), - llumlet_1.execute_engine_method.remote("_run_workers","rebuild_migration_backend", id_rank_map, "llumnix")]) + + ray.get([llumlet_0.execute_engine_method.remote("_run_workers", "rebuild_migration_backend", id_rank_map, "llumnix"), + llumlet_1.execute_engine_method.remote("_run_workers", "rebuild_migration_backend", id_rank_map, "llumnix")]) + # empty instance migrate out res = ray.get(llumlet_0.migrate_out.remote("instance_1", num_requests=math.inf)) assert not res @@ -222,8 +225,10 @@ async def test_correctness(prompt): assert output.text == origin_output.text assert output.cumulative_logprob == origin_output.cumulative_logprob + for prompt in TEST_PROMPTS: await test_correctness(prompt) + que.cleanup() def test_clear_migration_states(): diff --git a/tests/unit_test/backends/vllm/test_migration_backend.py b/tests/unit_test/backends/vllm/test_migration_backend.py index 2bb008ee..12ec324c 100644 --- a/tests/unit_test/backends/vllm/test_migration_backend.py +++ b/tests/unit_test/backends/vllm/test_migration_backend.py @@ -26,6 +26,44 @@ from tests.conftest import setup_ray_env from .test_worker import create_worker +def get_ready_workers(num_worker, num_gpu_blocks, engine_config, migraiton_config): + workers = [] + worker_ids = [] + + for _ in range(num_worker): + worker_id = random_uuid() + worker = create_worker(rank=0, local_rank=0, engine_config=engine_config, + worker_module_name="tests.unit_test.backends.vllm.test_migration_backend", + worker_class_name="MockMigrationWorker") + ray.get(worker.execute_method.remote('initialize_cache', num_gpu_blocks=num_gpu_blocks, num_cpu_blocks=0)) + ray.get(worker.execute_method.remote( + 'init_migration', + instance_id=worker_id, + migration_config=migraiton_config, + src_worker_handle_list=[worker], + node_id=ray.get_runtime_context().get_node_id())) + + workers.append(worker) + worker_ids.append(worker_id) + + instance_rank = {} + for idx, worker_id in enumerate(worker_ids): + instance_rank[worker_id] = idx + group_name = random_uuid() + + init_group_tasks =[] + for worker in workers: + init_group_tasks.append(worker.execute_method.remote('rebuild_migration_backend', + instance_rank=instance_rank, group_name=group_name)) + assert all(ray.get(init_group_tasks)) + + warmup_tasks = [] + for worker in workers: + warmup_tasks.append(worker.execute_method.remote('warmup')) + assert all(ray.get(warmup_tasks)) + + return workers, worker_ids + class MockMigrationWorker(MigrationWorker): def set_gpu_cache(self, data): for layer_idx in range(self.cache_engine.num_layers): @@ -34,75 +72,120 @@ def set_gpu_cache(self, data): def get_gpu_cache(self): torch.cuda.synchronize() - return self.gpu_cache + gpu_data = [] + for layer_idx in range(self.cache_engine.num_layers): + gpu_data.append(self.gpu_cache[layer_idx].clone().cpu()) + return gpu_data -@pytest.mark.skipif(torch.cuda.device_count() < 2, reason="Need at least 2 GPU to run the test.") -@pytest.mark.parametrize("backend", ['rpc', 'gloo', 'nccl']) -def test_migrate_cache(setup_ray_env, backend): +@pytest.mark.skipif(torch.cuda.device_count() < 3, reason="Need at least 3 GPU to run the test.") +@pytest.mark.parametrize("backend", ['rpc', 'gloo']) +def test_one_to_many_migrate_cache(setup_ray_env, backend): engine_config = EngineArgs(model='facebook/opt-125m', max_model_len=8, enforce_eager=True).create_engine_config() - migraiton_config = EngineManagerArgs(migration_cache_blocks=3, migration_num_layers=5).create_migration_config() + migration_internal_buffer_num = 2 + migraiton_config = EngineManagerArgs(migration_buffer_blocks=3, migration_num_layers=5, + migration_internal_buffer_num=migration_internal_buffer_num).create_migration_config() migraiton_config.migration_backend = backend - worker0 = create_worker(rank=0, local_rank=0, engine_config=engine_config, - worker_module_name="tests.unit_test.backends.vllm.test_migration_backend", - worker_class_name="MockMigrationWorker") - worker1 = create_worker(rank=0, local_rank=0, engine_config=engine_config, - worker_module_name="tests.unit_test.backends.vllm.test_migration_backend", - worker_class_name="MockMigrationWorker") - - ray.get(worker0.execute_method.remote('init_device')) - ray.get(worker1.execute_method.remote('init_device')) - - num_gpu_blocks = 8 - ray.get(worker0.execute_method.remote('initialize_cache', num_gpu_blocks=num_gpu_blocks, num_cpu_blocks=0)) - ray.get(worker1.execute_method.remote('initialize_cache', num_gpu_blocks=num_gpu_blocks, num_cpu_blocks=0)) - - worker0_id = random_uuid() - ray.get(worker0.execute_method.remote( - 'init_migration', - instance_id=worker0_id, - migration_config=migraiton_config, - src_worker_handle_list=[worker0], - node_id=ray.get_runtime_context().get_node_id())) - - worker1_id = random_uuid() - ray.get(worker1.execute_method.remote( - 'init_migration', - instance_id=worker1_id, - migration_config=migraiton_config, - src_worker_handle_list=[worker1], - node_id=ray.get_runtime_context().get_node_id())) - - instance_rank = {worker0_id: 0, worker1_id: 1} - group_name = random_uuid() - assert all(ray.get([worker0.execute_method.remote('rebuild_migration_backend', - instance_rank=instance_rank, group_name=group_name), - worker1.execute_method.remote('rebuild_migration_backend', - instance_rank=instance_rank, group_name=group_name)])) - assert all(ray.get([worker0.execute_method.remote('warmup'), - worker1.execute_method.remote('warmup')])) + num_worker = 3 + num_gpu_blocks = 6000 + workers, _ = get_ready_workers(num_worker, num_gpu_blocks, engine_config, migraiton_config) num_layers = engine_config.model_config.get_num_layers(engine_config.parallel_config) head_size = engine_config.model_config.get_head_size() num_heads = engine_config.model_config.get_num_kv_heads(engine_config.parallel_config) block_size = engine_config.cache_config.block_size + dummy_data = torch.randn(size=(num_layers, 2, num_gpu_blocks, block_size*num_heads*head_size)) + ray.get(workers[0].execute_method.remote('set_gpu_cache', data=dummy_data)) + worker0_data = ray.get(workers[0].execute_method.remote('get_gpu_cache')) + + dst_blocks = list(range(num_gpu_blocks)) + random.shuffle(dst_blocks) + + single_worker_num_blocks = len(dst_blocks)//(num_worker-1) + migration_tasks = [] + worker_idx = 1 + per_step_blocks = 500 + for offset in range(0, len(dst_blocks), single_worker_num_blocks): + src_to_dst = dict(enumerate(dst_blocks[offset:offset+single_worker_num_blocks])) + src_blocks = list(src_to_dst.keys()) + dst_blocks = list(src_to_dst.values()) + for idx in range(0, len(src_blocks), per_step_blocks): + cur_src_blocks = src_blocks[idx:idx+per_step_blocks] + cur_dst_blocks = dst_blocks[idx:idx+per_step_blocks] + migration_tasks.append(workers[0].execute_method.remote( + 'migrate_cache', + src_worker_handle_list=[workers[worker_idx]], + src_blocks=cur_src_blocks, + dst_blocks=cur_dst_blocks) + ) + worker_idx += 1 + ray.get(migration_tasks) + + worker_idx = 1 + for offset in range(0, len(dst_blocks), single_worker_num_blocks): + src_to_dst = dict(enumerate(dst_blocks[offset:offset+single_worker_num_blocks])) + dst_worker_data = ray.get(workers[worker_idx].execute_method.remote('get_gpu_cache')) + for layer_idx in range(num_layers): + for src_idx, dst_idx in src_to_dst.items(): + assert torch.allclose(worker0_data[layer_idx][0][src_idx], dst_worker_data[layer_idx][0][dst_idx]) + assert torch.allclose(worker0_data[layer_idx][1][src_idx], dst_worker_data[layer_idx][1][dst_idx]) + worker_idx += 1 + +@pytest.mark.skipif(torch.cuda.device_count() < 3, reason="Need at least 3 GPU to run the test.") +@pytest.mark.parametrize("backend", ['rpc', 'gloo']) +def test_many_to_one_migrate_cache(setup_ray_env, backend): + engine_config = EngineArgs(model='facebook/opt-125m', max_model_len=8, enforce_eager=True).create_engine_config() + migration_internal_buffer_num = 2 + migraiton_config = EngineManagerArgs(migration_buffer_blocks=3, migration_num_layers=5, + migration_internal_buffer_num=migration_internal_buffer_num).create_migration_config() + migraiton_config.migration_backend = backend + num_worker = 3 + num_gpu_blocks = 6000 + workers, _ = get_ready_workers(num_worker, num_gpu_blocks, engine_config, migraiton_config) + + num_layers = engine_config.model_config.get_num_layers(engine_config.parallel_config) + head_size = engine_config.model_config.get_head_size() + num_heads = engine_config.model_config.get_num_kv_heads(engine_config.parallel_config) + block_size = engine_config.cache_config.block_size dummy_data = torch.randn(size=(num_layers, 2, num_gpu_blocks, block_size*num_heads*head_size)) - ray.get(worker0.execute_method.remote('set_gpu_cache', data=dummy_data)) - worker0_data = ray.get(worker0.execute_method.remote('get_gpu_cache')) + + worker_datas = [0] + for idx in range(1, num_worker): + ray.get(workers[idx].execute_method.remote('set_gpu_cache', data=dummy_data)) + worker_datas.append(ray.get(workers[idx].execute_method.remote('get_gpu_cache'))) dst_blocks = list(range(num_gpu_blocks)) random.shuffle(dst_blocks) - src_to_dst = dict(enumerate(dst_blocks)) - ray.get(worker1.execute_method.remote( - 'migrate_cache', - src_worker_handle_list=[worker0], - src_blocks=list(src_to_dst.keys()), - dst_blocks=list(src_to_dst.values()))) - - worker1_data = ray.get(worker1.execute_method.remote('get_gpu_cache')) - - for layer_idx in range(num_layers): - for src_idx, dst_idx in src_to_dst.items(): - assert torch.allclose(worker0_data[layer_idx][0][src_idx], worker1_data[layer_idx][0][dst_idx]) - assert torch.allclose(worker0_data[layer_idx][1][src_idx], worker1_data[layer_idx][1][dst_idx]) + + single_worker_num_blocks = len(dst_blocks)//(num_worker-1) + migration_tasks = [] + worker_idx = 1 + per_step_blocks = 500 + for offset in range(0, len(dst_blocks), single_worker_num_blocks): + src_to_dst = dict(enumerate(dst_blocks[offset:offset+single_worker_num_blocks])) + src_blocks = list(src_to_dst.keys()) + dst_blocks = list(src_to_dst.values()) + for idx in range(0, len(src_blocks), per_step_blocks): + cur_src_blocks = src_blocks[idx:idx+per_step_blocks] + cur_dst_blocks = dst_blocks[idx:idx+per_step_blocks] + migration_tasks.append(workers[0].execute_method.remote( + 'migrate_cache', + src_worker_handle_list=[workers[worker_idx]], + src_blocks=cur_src_blocks, + dst_blocks=cur_dst_blocks) + ) + worker_idx += 1 + ray.get(migration_tasks) + + dst_worker_data = ray.get(workers[0].execute_method.remote('get_gpu_cache')) + + worker_idx = 1 + for offset in range(0, len(dst_blocks), single_worker_num_blocks): + src_to_dst = dict(enumerate(dst_blocks[offset:offset+single_worker_num_blocks])) + + for layer_idx in range(num_layers): + for src_idx, dst_idx in src_to_dst.items(): + assert torch.allclose(worker_datas[worker_idx][layer_idx][0][src_idx], dst_worker_data[layer_idx][0][dst_idx]) + assert torch.allclose(worker_datas[worker_idx][layer_idx][1][src_idx], dst_worker_data[layer_idx][1][dst_idx]) + worker_idx += 1 diff --git a/tests/unit_test/backends/vllm/test_simulator.py b/tests/unit_test/backends/vllm/test_simulator.py index 7fb94baa..c0753b06 100644 --- a/tests/unit_test/backends/vllm/test_simulator.py +++ b/tests/unit_test/backends/vllm/test_simulator.py @@ -71,7 +71,7 @@ async def test_backend(setup_ray_env): # TODO(ZeldaHuang): add tests for BackendSimVLLM methods # (currently BackendSimVLLM is just a wrapper of BackendVLLM) engine_args = EngineArgs(model="facebook/opt-125m", worker_use_ray=True) - migration_config = MigrationConfig("LCFS", "gloo", 16, 1, 4, 5, 20) + migration_config = MigrationConfig("LCFS", "gloo", 16, 1, 4, 5, 20, 2) output_queue_type = QueueType.RAYQUEUE que, server_info = request_output_queue_server(output_queue_type) diff --git a/tests/unit_test/backends/vllm/test_worker.py b/tests/unit_test/backends/vllm/test_worker.py index dc014005..09df9ea0 100644 --- a/tests/unit_test/backends/vllm/test_worker.py +++ b/tests/unit_test/backends/vllm/test_worker.py @@ -39,7 +39,7 @@ def create_worker(rank: int, local_rank: int, engine_config: EngineConfig, trust_remote_code=True ) - worker.init_worker.remote( + ray.get(worker.init_worker.remote( model_config=engine_config.model_config, parallel_config=engine_config.parallel_config, scheduler_config=engine_config.scheduler_config, @@ -52,25 +52,25 @@ def create_worker(rank: int, local_rank: int, engine_config: EngineConfig, lora_config=engine_config.lora_config, vision_language_config=engine_config.vision_language_config, is_driver_worker = False - ) - + )) + ray.get(worker.execute_method.remote('init_device')) return worker @pytest.mark.parametrize("backend", ['rpc', 'gloo', 'nccl']) def test_reserve_memory_for_migration(setup_ray_env, backend): engine_config = EngineArgs(model='facebook/opt-125m', max_model_len=8, enforce_eager=True).create_engine_config() - migraiton_config = EngineManagerArgs(migration_cache_blocks=1).create_migration_config() - migraiton_config.migration_backend = backend + migration_config = EngineManagerArgs(migration_buffer_blocks=1).create_migration_config() + migration_config.migration_backend = backend worker = create_worker(rank=0, local_rank=0, engine_config=engine_config) - ray.get(worker.execute_method.remote('init_device')) block_size = CacheEngine.get_cache_block_size(engine_config.cache_config, engine_config.model_config, engine_config.parallel_config) num_layers = engine_config.model_config.get_num_layers(engine_config.parallel_config) - occupy_memory = migraiton_config.migration_cache_blocks * block_size * migraiton_config.migration_num_layers // num_layers + occupy_memory = migration_config.migration_internal_buffer_num * migration_config.migration_buffer_blocks \ + * block_size * migration_config.migration_num_layers // num_layers migration_cache_size = ray.get(worker.execute_method.remote('reserve_memory_for_migration', - migration_config=migraiton_config, + migration_config=migration_config, model_config=engine_config.model_config, cache_config=engine_config.cache_config, parallel_config=engine_config.parallel_config)) @@ -80,17 +80,16 @@ def test_reserve_memory_for_migration(setup_ray_env, backend): @pytest.mark.parametrize("backend", ['rpc', 'gloo', 'nccl']) def test_rebuild_migration_backend(setup_ray_env, backend): engine_config = EngineArgs(model='facebook/opt-125m', max_model_len=8, enforce_eager=True).create_engine_config() - migraiton_config = EngineManagerArgs(migration_cache_blocks=1).create_migration_config() - migraiton_config.migration_backend = backend + migration_config = EngineManagerArgs(migration_buffer_blocks=1).create_migration_config() + migration_config.migration_backend = backend worker0 = create_worker(rank=0, local_rank=0, engine_config=engine_config) worker0_id = random_uuid() - ray.get(worker0.execute_method.remote('init_device')) ray.get(worker0.execute_method.remote('initialize_cache', num_gpu_blocks=8, num_cpu_blocks=0)) ray.get(worker0.execute_method.remote( 'init_migration', instance_id=worker0_id, - migration_config=migraiton_config, + migration_config=migration_config, src_worker_handle_list=[worker0], node_id=ray.get_runtime_context().get_node_id())) instance_rank = {worker0_id: 0} @@ -100,12 +99,11 @@ def test_rebuild_migration_backend(setup_ray_env, backend): worker1 = create_worker(rank=0, local_rank=0, engine_config=engine_config) worker1_id = random_uuid() - ray.get(worker1.execute_method.remote('init_device')) ray.get(worker1.execute_method.remote('initialize_cache', num_gpu_blocks=8, num_cpu_blocks=0)) ray.get(worker1.execute_method.remote( 'init_migration', instance_id=worker1_id, - migration_config=migraiton_config, + migration_config=migration_config, src_worker_handle_list=[worker1], node_id=ray.get_runtime_context().get_node_id())) diff --git a/tests/unit_test/global_scheduler/test_dispatch_scheduler.py b/tests/unit_test/global_scheduler/test_dispatch_scheduler.py index 8cee3a69..28fc129e 100644 --- a/tests/unit_test/global_scheduler/test_dispatch_scheduler.py +++ b/tests/unit_test/global_scheduler/test_dispatch_scheduler.py @@ -21,7 +21,7 @@ def init_dispatch_scheduler(policy='load'): instance_load_calculator = InstanceLoadCalculator('remaining_steps', True) - dispatch_scheduler = DispatchScheduler(policy, instance_load_calculator, random.randint(1,4)) + dispatch_scheduler = DispatchScheduler(policy, instance_load_calculator, 1) return dispatch_scheduler @pytest.fixture @@ -29,7 +29,9 @@ def dispatch_scheduler(): dispatch_scheduler = init_dispatch_scheduler() yield dispatch_scheduler -def test_add_instance_and_remove_instance(dispatch_scheduler): +@pytest.mark.parametrize("num_dispatch_instances", [1, 2, 3]) +def test_add_instance_and_remove_instance(dispatch_scheduler, num_dispatch_instances): + dispatch_scheduler.num_dispatch_instances = num_dispatch_instances dispatch_scheduler.add_instance('instance_1') assert dispatch_scheduler.num_instances == 1 assert len(dispatch_scheduler.available_dispatch_instance_set) == 1 diff --git a/tests/unit_test/global_scheduler/test_llm_engine_manager.py b/tests/unit_test/global_scheduler/test_llm_engine_manager.py index 5c5fc644..024ad4bf 100644 --- a/tests/unit_test/global_scheduler/test_llm_engine_manager.py +++ b/tests/unit_test/global_scheduler/test_llm_engine_manager.py @@ -27,6 +27,7 @@ from llumnix.queue.queue_type import QueueType from llumnix.global_scheduler.scaling_scheduler import InstanceType from llumnix.backends.vllm.simulator import BackendSimVLLM +from llumnix.backends.profiling import LatencyMemData # pylint: disable=unused-import from tests.conftest import setup_ray_env @@ -41,6 +42,7 @@ def __init__(self, instance_id): self.request_id_set = set() self.instance_info = None self.num_migrate_out = 0 + self.num_migrate_in = 0 def get_instance_id(self) -> str: return self.instance_id @@ -76,14 +78,24 @@ def abort(self, request_id): self.num_requests = len(self.request_id_set) return self.num_requests - def migrate_out(self, src_instance_name, dst_instance_name): + def migrate_out(self, dst_instance_name, num_requests): self.num_migrate_out += 1 + migrate_in_ray_actor = ray.get_actor(dst_instance_name, namespace='llumnix') + ray.get(migrate_in_ray_actor.migrate_in.remote(self.actor_name, num_requests)) + time.sleep(0.1) + return self.num_migrate_out + + def migrate_in(self, src_instance_name, num_requests): + self.num_migrate_in += 1 + return self.num_migrate_in def get_num_migrate_out(self): return self.num_migrate_out -class MockBackendSim(BackendSimVLLM): + def get_num_migrate_in(self): + return self.num_migrate_in +class MockBackendSim(BackendSimVLLM): def _get_lantecy_mem(self, *args, **kwargs): latency_mem = LatencyMemData({}, {}, {}) latency_mem.prefill_model_params = (0,0) @@ -242,20 +254,37 @@ def get_instance_info_migrate_out(instance_id): return instance_info def test_update_instance_info_loop_and_migrate(setup_ray_env, engine_manager): - instance_ids, llumlets = init_llumlets(2) - instance_id, instance_id_1 = instance_ids[0], instance_ids[1] - llumlet, llumlet_1 = llumlets[0], llumlets[1] - request_id = random_uuid() - request_id_1 = random_uuid() - ray.get(llumlet.generate.remote(request_id, None, math.inf, None, None)) - ray.get(llumlet_1.generate.remote(request_id_1, None, math.inf, None, None)) - instance_info_migrate_out = get_instance_info_migrate_out(instance_id) - instance_info_migrate_in = get_instance_info_migrate_in(instance_id_1) - ray.get(llumlet.set_instance_info.remote(instance_info_migrate_out)) - ray.get(llumlet_1.set_instance_info.remote(instance_info_migrate_in)) - num_migrate_out = ray.get(llumlet.get_num_migrate_out.remote()) - assert num_migrate_out == 0 + num_llumlets = 5 + instance_ids, llumlets = init_llumlets(num_llumlets) + + for i in range(num_llumlets): + for _ in range(2*(i+1)): + ray.get(llumlets[i].generate.remote(random_uuid(), None, math.inf, None, None)) + + instance_info = InstanceInfo() + instance_info.instance_type = InstanceType.NO_CONSTRAINTS + + for i in range(num_llumlets): + instance_info.instance_id = instance_ids[i] + instance_info.num_available_gpu_blocks = 40 - i * 10 + instance_info.num_running_requests = i + instance_info.num_blocks_first_waiting_request = i + ray.get(llumlets[i].set_instance_info.remote(instance_info)) + + for i in range(num_llumlets): + num_migrate_out = ray.get(llumlets[i].get_num_migrate_out.remote()) + assert num_migrate_out == 0 + ray.get(engine_manager.scale_up.remote(instance_ids, llumlets)) - time.sleep(0.5) - num_migrate_out = ray.get(llumlet.get_num_migrate_out.remote()) - assert num_migrate_out != 0 + time.sleep(2) + + for i in range(num_llumlets): + num_migrate_out = ray.get(llumlets[i].get_num_migrate_out.remote()) + num_migrate_in = ray.get(llumlets[i].get_num_migrate_in.remote()) + + if i == 0: + assert num_migrate_in > 1 and num_migrate_out == 0 + elif i == num_llumlets - 1: + assert num_migrate_in == 0 and num_migrate_out > 1 + else: + assert num_migrate_in == 0 and num_migrate_out == 0 diff --git a/tests/unit_test/llumlet/test_engine_step_exception.py b/tests/unit_test/llumlet/test_engine_step_exception.py index 56b58322..c630a04f 100644 --- a/tests/unit_test/llumlet/test_engine_step_exception.py +++ b/tests/unit_test/llumlet/test_engine_step_exception.py @@ -11,7 +11,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import asyncio import time import ray import torch @@ -30,28 +29,17 @@ @ray.remote(num_cpus=1, max_concurrency=4) class MockLlumlet(Llumlet): - def __init__(self, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) - self.origin_step = self.backend_engine.engine.step_async - - def set_error_step(self, broken: bool): - self.backend_engine._stop_event.set() - + def set_error_step(self): async def raise_error_step(): await self.origin_step() raise ValueError("Mock engine step error") - if broken: - self.backend_engine.engine.step_async = raise_error_step - else: - self.backend_engine.engine.step_async = self.origin_step - - asyncio.create_task(self.backend_engine._start_engine_step_loop()) + self.backend_engine.engine.step_async = raise_error_step @pytest.mark.skipif(torch.cuda.device_count() < 1, reason="Need at least 1 GPU to run the test.") def test_engine_step_exception(setup_ray_env): - engine_args = EngineArgs(model="facebook/opt-125m", worker_use_ray=True) - migration_config = MigrationConfig("LCFS", "rpc", 16, 1, 4, 5, 20) + engine_args = EngineArgs(model="facebook/opt-125m", max_model_len=8, worker_use_ray=True) + migration_config = MigrationConfig("LCFS", "rpc", 16, 1, 4, 5, 20, 2) node_id = ray.get_runtime_context().get_node_id() scheduling_strategy = NodeAffinitySchedulingStrategy(node_id=node_id, soft=False) @@ -76,7 +64,7 @@ def test_engine_step_exception(setup_ray_env): cur_free_memory, _ = torch.cuda.mem_get_info() assert cur_free_memory < origin_free_memory - ray.get(llumlet.set_error_step.remote(True)) + ray.get(llumlet.set_error_step.remote()) time.sleep(3) all_actors = ray.util.list_named_actors(True) diff --git a/tests/unit_test/llumlet/test_local_migration_scheduler.py b/tests/unit_test/llumlet/test_local_migration_scheduler.py index d585300d..c0c6f834 100644 --- a/tests/unit_test/llumlet/test_local_migration_scheduler.py +++ b/tests/unit_test/llumlet/test_local_migration_scheduler.py @@ -66,14 +66,18 @@ def test_scheduler_policy(): assert scheduler.get_migrate_out_request().request_id == "0" engine.add_request(request_id="3", length=2, expected_steps=1) - request = scheduler.get_migrate_out_request() - assert request.request_id == "3" - assert request.output_len >= request.expected_steps and request.inference_type == RequestInferenceType.DECODE engine.add_request(request_id="4", length=3, expected_steps=math.inf) + engine.add_request(request_id="5", length=4, expected_steps=math.inf) scheduler.request_migration_policy = "LCFS" request = scheduler.get_migrate_out_request() + request.migrating = True assert request.request_id == "3" assert request.output_len >= request.expected_steps and request.inference_type == RequestInferenceType.DECODE + request = scheduler.get_migrate_out_request() + request.migrating = True + assert request.request_id == "5" + request = scheduler.get_migrate_out_request() + assert request.request_id == "4" def test_scheduler_should_abort_migration(): req_0 = MockRequest(request_id="0", length=1, expected_steps=math.inf) diff --git a/tests/unit_test/queue/test_zmq.py b/tests/unit_test/queue/test_zmq.py index d4303d37..6f62935e 100644 --- a/tests/unit_test/queue/test_zmq.py +++ b/tests/unit_test/queue/test_zmq.py @@ -106,8 +106,8 @@ async def benchmark_queue(qps, ip=None, port=None): signal.alarm(0) @pytest.mark.asyncio -@pytest.mark.parametrize("qps", [128.0, 256.0, 512.0, 1024.0]) -async def test_queue_zmq(setup_ray_env, qps): +async def test_queue_zmq(setup_ray_env): ip = '127.0.0.1' port = 1234 + qps = 1024.0 await benchmark_queue(qps, ip, port) From e92c9accc9a69786510e6cfbf9aa2f92217b2aaa Mon Sep 17 00:00:00 2001 From: Biao Sun Date: Tue, 12 Nov 2024 14:33:50 +0800 Subject: [PATCH 04/10] [Core][Migration] Support waiting request and multiple requests migration (#36) --- Makefile | 15 +- configs/base.yml | 2 +- docs/Arguments.md | 6 +- llumnix/arg_utils.py | 24 ++- llumnix/backends/backend_interface.py | 64 ++++++-- llumnix/backends/vllm/llm_engine.py | 47 ++++-- llumnix/backends/vllm/migration_backend.py | 10 +- llumnix/backends/vllm/scheduler.py | 90 ++++++++--- llumnix/backends/vllm/sequence.py | 30 +++- llumnix/backends/vllm/utils.py | 3 +- llumnix/backends/vllm/worker.py | 6 +- llumnix/config/default.py | 2 +- .../global_scheduler/dispatch_scheduler.py | 2 +- .../global_scheduler/migration_scheduler.py | 4 - llumnix/llm_engine_manager.py | 16 +- llumnix/llumlet/llumlet.py | 90 ++++++----- llumnix/llumlet/local_migration_scheduler.py | 136 ++++++++-------- llumnix/llumlet/migration_coordinator.py | 113 +++++++++----- llumnix/llumlet/request.py | 56 +++++-- tests/e2e_test/test_bench.py | 4 +- tests/e2e_test/test_e2e.py | 13 +- tests/e2e_test/test_migration.py | 50 +++--- tests/e2e_test/utils.py | 2 +- .../unit_test/backends/vllm/test_migration.py | 147 +++++++++++++----- .../unit_test/backends/vllm/test_scheduler.py | 62 +++++++- .../unit_test/backends/vllm/test_simulator.py | 2 +- tests/unit_test/backends/vllm/utils.py | 4 +- .../test_dispatch_scheduler.py | 2 +- .../test_llm_engine_manager.py | 6 +- .../llumlet/test_engine_step_exception.py | 2 +- .../llumlet/test_local_migration_scheduler.py | 85 ++++++---- .../llumlet/test_migration_coordinator.py | 76 +++++---- 32 files changed, 790 insertions(+), 381 deletions(-) diff --git a/Makefile b/Makefile index b2cd80f3..8f75c380 100644 --- a/Makefile +++ b/Makefile @@ -29,14 +29,15 @@ lint: check_pylint_installed check_pytest_installed .PHONY: test test: check_pytest_installed - @pytest -v -x --ignore=third_party/ --ignore=tests/e2e_test --disable-warnings + @pytest -v --ignore=third_party/ --ignore=tests/e2e_test --disable-warnings @python examlpes/offline_inference.py - @pytest -v -x tests/e2e_test/test_e2e.py - @pytest -v -x ./tests/e2e_test/test_migration.py + @pytest -v ./tests/e2e_test/test_e2e.py + @pytest -v ./tests/e2e_test/test_bench.py + @pytest -v ./tests/e2e_test/test_migration.py .PHONY: unit_test unit_test: check_pytest_installed - @pytest -v -x --ignore=third_party/ --ignore=tests/e2e_test --disable-warnings + @pytest -v --ignore=third_party/ --ignore=tests/e2e_test --disable-warnings .PHONY: offline_test offline_test: @@ -44,15 +45,15 @@ offline_test: .PHONY: e2e_test e2e_test: - @pytest -v -x tests/e2e_test/test_e2e.py + @pytest -v ./tests/e2e_test/test_e2e.py .PHONY: bench_test bench_test: - @pytest -v -x ./tests/e2e_test/test_bench.py + @pytest -v ./tests/e2e_test/test_bench.py .PHONY: migration_test migration_test: - @pytest -v -x ./tests/e2e_test/test_migration.py + @pytest -v ./tests/e2e_test/test_migration.py #################### pygloo install for gloo migration backend begin #################### diff --git a/configs/base.yml b/configs/base.yml index 70358339..b9ee7077 100644 --- a/configs/base.yml +++ b/configs/base.yml @@ -16,7 +16,7 @@ MANAGER: ENABLE_MIGRATION: True ENABLE_DEFRAG: True - REQUEST_MIGRATION_POLICY: 'SJF' + REQUEST_MIGRATION_POLICY: 'SR' MIGRATION_BACKEND: 'gloo' MIGRATION_BUFFER_BLOCKS: 512 diff --git a/docs/Arguments.md b/docs/Arguments.md index a2584417..09841a5b 100644 --- a/docs/Arguments.md +++ b/docs/Arguments.md @@ -17,7 +17,7 @@ usage: -m llumnix.entrypoints.vllm.api_server [-h] [--pair-migration-frequency PAIR_MIGRATION_FREQUENCY] [--pair-migration-policy {balanced,defrag_constrained,defrag_relaxed}] [--migrate-out-threshold MIGRATE_OUT_THRESHOLD] - [--request-migration-policy {LCFS,SJF,LJF}] + [--request-migration-policy {LCR,SR,LR,FCW,FCWSR}] [--enable-defrag ENABLE_DEFRAG] [--enable-scaling] [--min-instances MIN_INSTANCES] @@ -90,8 +90,8 @@ usage: -m llumnix.entrypoints.vllm.api_server [-h] `--request-migration-policy` - Request migration policy. -- Possible choices: LCFS, SJF, LJF -- Default: "SJF" +- Possible choices: LCR, SR, LR, FCW, FCWSR +- Default: "SR" `--enable-defrag` - Enable defragmentation through migration based on virtual usage. diff --git a/llumnix/arg_utils.py b/llumnix/arg_utils.py index 37b3bbc6..1c4c54b4 100644 --- a/llumnix/arg_utils.py +++ b/llumnix/arg_utils.py @@ -22,6 +22,7 @@ from llumnix.config import LlumnixConfig, get_llumnix_config from llumnix.config.default import _C + class LlumnixArgumentParser(argparse.ArgumentParser): def __init__(self, *args, **kwargs): self.cur_namespace = "llumnix" @@ -228,7 +229,11 @@ def add_cli_args( parser.add_argument('--dispatch-policy', type=str, choices=['balanced', 'load', 'queue', 'flood'], - help='request dispatch policy') + help='The request dispatch policy.\n\n' + '* "balanced" dispatch request to the instance with minimum requests dispatched.\n' + '* "load" dispatch request to the instance with lowest instance load.\n' + '* "queue" dispatch request to the instance with minimum waiting request queue length.\n' + '* "flood" dispatch request to the instance with maximum requests dispatched.\n') parser.add_argument('--num-available-dispatch-instances', type=int, help='number of available instances for dispatching') @@ -242,14 +247,25 @@ def add_cli_args( parser.add_argument('--pair-migration-policy', type=str, choices=['balanced', 'defrag_constrained', 'defrag_relaxed'], - help='pair migration policy') + help='The pair migration policy.\n\n' + '* "balanced" pair migration to make the instance load of instance more balanced.\n' + '* "defrag_constrained" pair migration without balanced constraint to ' + 'achieve defragmentation thoroughly (with instance constraints).\n' + '* "defrag_relaxed" pair migration to without balanced constraint ' + 'to achieve defragmentation thoroughly (without instance constraints).\n') parser.add_argument('--migrate-out-threshold', type=float, help='migrate out instance load threshold') parser.add_argument('--request-migration-policy', type=str, - choices=['LCFS', 'SJF', 'LJF'], - help='request migration policy') + default=None, + choices=['LCR', 'SR', 'LR', 'FCW', 'FCWSR'], + help='The request migration policy.\n\n' + '* "LCR" migrate the running request last come.\n' + '* "SR" migrate the running request shortest.\n' + '* "LR" migrate the running request longest.\n' + '* "FCW" migrate the waiting request first come.\n' + '* "FCWSR" migrate the waiting request first come and running request shortest.\n') parser.add_argument('--enable-defrag', type=bool, help='enable defragmentation through migration based on virtual usage') diff --git a/llumnix/backends/backend_interface.py b/llumnix/backends/backend_interface.py index 16a8ac1f..28e1e802 100644 --- a/llumnix/backends/backend_interface.py +++ b/llumnix/backends/backend_interface.py @@ -13,9 +13,9 @@ from abc import ABC, abstractmethod from enum import Enum -from typing import Iterable, List, Union +from typing import Iterable, List, Union, Deque -from llumnix.llumlet.request import LlumnixRequest +from llumnix.llumlet.request import LlumnixRequest, RequestStatus from llumnix.server_info import ServerInfo class EngineState(str, Enum): @@ -99,14 +99,21 @@ def get_request_incremental_blocks(self, backend_request: LlumnixRequest, pre_st raise NotImplementedError @abstractmethod - def get_running_queue(self) -> List[LlumnixRequest]: + def get_running_queue(self) -> Deque[LlumnixRequest]: """ Return backend's running queue. """ raise NotImplementedError @abstractmethod - def remove_running_request(self, request_id: str) -> None: + def get_waiting_queue(self) -> Deque[LlumnixRequest]: + """ + Return backend's waiting queue. + """ + raise NotImplementedError + + @abstractmethod + def remove_running_request(self, request_id: str) -> bool: """ Removes a request from the backend's running queue. @@ -117,6 +124,26 @@ def remove_running_request(self, request_id: str) -> None: Args: request_id: A string identifier for the request that is to be removed from the running queue. This ID uniquely identifies the request within the backend system. + + Returns: + True if the request was successfully removed from the running queue, False otherwise. + """ + raise NotImplementedError + + @abstractmethod + def remove_waiting_request(self, request_id: str) -> bool: + """ + Removes a request from the backend's waiting queue. + + This method is responsible for safely halting and removing an active request from the waiting + queue of the backend engine. This action is performed in waiting request migration. + + Args: + request_id: A string identifier for the request that is to be removed from the waiting + queue. This ID uniquely identifies the request within the backend system. + + Returns: + True if the request was successfully removed from the waiting queue, False otherwise. """ raise NotImplementedError @@ -164,17 +191,25 @@ def pop_migrating_out_requests_last_stage(self) -> List[LlumnixRequest]: raise NotImplementedError @abstractmethod - def pre_alloc(self, request_id: str, block_num: int) -> List[int]: + def pre_alloc(self, + request_id: str, + request_status: RequestStatus, + request_arrival_time: float, + block_num: int) -> List[int]: """Pre-allocates cache blocks for a migrating request. This method selects a specified number of free cache blocks to be reserved for an incoming migration request identified by the given request ID. It updates the pre-allocation cache dictionary with the allocated blocks, which ensures that these blocks are not used by - another process until the migration is finished. + another process until the migration is finished. For the waiting request, it only reserves + free cache blocks when the request is the earliest arrival one among the requests of dst instance's + waiting queue. Args: request_id: The unique identifier of the migration request for which cache blocks are to be pre-allocated. + request_status: The status (waiting/running) of the request. + request_arrival_time: The arrival time of the request. block_num: The number of cache blocks that need to be pre-allocated for the request. Returns: @@ -187,9 +222,8 @@ def add_running_request(self, backend_request: LlumnixRequest) -> None: """ Adds a backend request to the running queue for processing. - This method enqueues a backend request into engine running queue, marking it for - active processing. It is used when a suspend migrating request should be added back - to running queue. + This method enqueues a backend request into engine running queue. + It is used when a suspend migrating request should be added back to running queue. Args: backend_request: An object representing the backend request. The type of this @@ -199,19 +233,17 @@ def add_running_request(self, backend_request: LlumnixRequest) -> None: raise NotImplementedError @abstractmethod - def is_request_running(self, backend_request: LlumnixRequest) -> bool: - """Checks if a given backend request is currently in the running queue. + def add_waiting_request(self, backend_request: LlumnixRequest) -> None: + """ + Adds a backend request to the waiting queue for processing. - This method determines whether a backend request is present and actively being processed - in the running queue. + This method enqueues a backend request into engine waiting queue. + It is used when a suspend migrating request should be added back to waiting queue. Args: backend_request: An object representing the backend request. The type of this object is dependent on the backend implementation and the details of the request. - - Returns: - True if the backend request is currently in the running queue; False otherwise. """ raise NotImplementedError diff --git a/llumnix/backends/vllm/llm_engine.py b/llumnix/backends/vllm/llm_engine.py index 4b2a076d..59b41fa7 100644 --- a/llumnix/backends/vllm/llm_engine.py +++ b/llumnix/backends/vllm/llm_engine.py @@ -13,7 +13,7 @@ import time import traceback -from typing import Any, List, Optional, Dict, Union, Iterable, Tuple +from typing import Any, List, Optional, Dict, Union, Iterable, Tuple, Deque from collections import defaultdict import threading import asyncio @@ -34,7 +34,7 @@ from llumnix.instance_info import InstanceInfo from llumnix.backends.backend_interface import BackendInterface, EngineState from llumnix.backends.vllm.scheduler import SchedulerLlumnix -from llumnix.backends.vllm.sequence import SequenceGroupLlumnix +from llumnix.backends.vllm.sequence import SequenceGroupLlumnix, RequestStatus from llumnix.backends.profiling import LatencyMemData from llumnix.server_info import ServerInfo from llumnix.internal_config import MigrationConfig @@ -199,7 +199,7 @@ def _process_model_outputs( # TODO(ZeldaHuang): Use LlumnixRequestOutput to store llumnix output args. return request_outputs, server_infos - async def step_async(self) -> None: + async def step_async(self) -> Tuple[List[RequestOutput], List[ServerInfo]]: step_begin_time = time.time() request_outputs, server_infos = await super().step_async() for request_output in request_outputs: @@ -295,9 +295,11 @@ def __init__( self.worker_handle_list = self.engine.model_executor.workers.copy() if len(self.worker_handle_list) + 1 == self.engine.parallel_config.world_size: self.worker_handle_list.insert(0, ray.get_actor(f"instance_{self.instance_id}", namespace="llumnix")) - self._run_workers("init_migration", instance_id=instance_id, migration_config=migration_config,\ - src_worker_handle_list=self.worker_handle_list, - placement_group=placement_group, node_id=node_id) + self._run_workers("init_migration", instance_id=instance_id, + migration_config=migration_config, + src_worker_handle_list=self.worker_handle_list, + placement_group=placement_group, + node_id=node_id) self.state = EngineState.INIT logger.info("engine ({}) current state {}".format(self.instance_id, self.state)) @@ -350,15 +352,22 @@ def commit_dst_request(self, backend_request: SequenceGroupLlumnix) -> None: logger.info("add seq {} to block table".format(seq.seq_id)) pre_alloc_blocks = self.engine.scheduler.pre_alloc_cache_dict.pop(backend_request.request_id) self.engine.scheduler.block_manager.add_block_table(pre_alloc_blocks, seq.seq_id) - backend_request.reset_migration_args() - self.add_running_request(backend_request) + backend_request.reset_migration_args_dst() + assert backend_request.status in [RequestStatus.WAITING_MIGRATING, RequestStatus.RUNNING_MIGRATING], \ + "The status of request migrated to dst instance should be \ + RequestStatus.WAITING_MIGRATING or RequestStatus.RUNNING_MIGRATING" + if backend_request.status == RequestStatus.WAITING_MIGRATING: + self.add_waiting_request(backend_request) + else: # RUNNING_MIGRATING: + backend_request.reset_status() + self.add_running_request(backend_request) async def send_blocks(self, dst_ray_actor: "ray.actor.ActorHandle", src_blocks: List[int], dst_blocks: List[int]) -> None: await dst_ray_actor.execute_engine_method.remote("_run_workers", - "migrate_cache", - dst_blocks=dst_blocks, - src_blocks=src_blocks, - src_worker_handle_list=self.worker_handle_list) + "migrate_cache", + dst_blocks=dst_blocks, + src_blocks=src_blocks, + src_worker_handle_list=self.worker_handle_list) def _run_workers(self, *args, **kwargs): # pylint: disable=protected-access @@ -373,15 +382,21 @@ def abort_request(self, request_id: Union[str, Iterable[str]]) -> None: request_ids = set(request_id) return self.engine.abort_request(request_ids) - def get_running_queue(self) -> List[SequenceGroupLlumnix]: + def get_running_queue(self) -> Deque[SequenceGroupLlumnix]: return self.engine.scheduler.get_running_queue() + def get_waiting_queue(self) -> Deque[SequenceGroupLlumnix]: + return self.engine.scheduler.get_waiting_queue() + def get_request_incremental_blocks(self, *args, **kwargs) -> List[int]: return self.engine.scheduler.get_request_incremental_blocks(*args, **kwargs) - def remove_running_request(self, *args, **kwargs) -> None: + def remove_running_request(self, *args, **kwargs) -> bool: return self.engine.scheduler.remove_running_request(*args, **kwargs) + def remove_waiting_request(self, *args, **kwargs) -> bool: + return self.engine.scheduler.remove_waiting_request(*args, **kwargs) + def add_migrating_out_request_last_stage(self, *args, **kwargs) -> None: return self.engine.scheduler.add_migrating_out_request_last_stage(*args, **kwargs) @@ -400,8 +415,8 @@ def should_abort_migration(self, *args, **kwargs) -> bool: def add_running_request(self, *args, **kwargs) -> None: return self.engine.scheduler.add_running_request(*args, **kwargs) - def is_request_running(self, *args, **kwargs) -> bool: - return self.engine.scheduler.is_request_running(*args, **kwargs) + def add_waiting_request(self, *args, **kwargs) -> None: + return self.engine.scheduler.add_waiting_request(*args, **kwargs) def free_dst_pre_alloc_cache(self, *args, **kwargs) -> None: return self.engine.scheduler.free_dst_pre_alloc_cache(*args, **kwargs) diff --git a/llumnix/backends/vllm/migration_backend.py b/llumnix/backends/vllm/migration_backend.py index e69f3479..950c1b31 100644 --- a/llumnix/backends/vllm/migration_backend.py +++ b/llumnix/backends/vllm/migration_backend.py @@ -286,15 +286,15 @@ def get_migration_backend(migration_config: MigrationConfig, cache_engine: Cache .format(migration_config.migration_buffer_blocks, cache_engine.num_gpu_blocks)) migration_config.migration_buffer_blocks = cache_engine.num_gpu_blocks - target_col = None + target_migration_backend = None backend = migration_config.migration_backend - assert backend in ['nccl', 'gloo', 'rpc'], "Unsupported backend: {} for VLLM".format(backend) + assert backend in ['nccl', 'gloo', 'rpc'], "Unsupported migration backend: {} for llumnix".format(backend) if backend in ['nccl', 'gloo']: - target_col = RayColMigrationBackend(migration_config, cache_engine, local_rank, scheduling_strategy, + target_migration_backend = RayColMigrationBackend(migration_config, cache_engine, local_rank, scheduling_strategy, is_driver_worker, gpu_cache) else: - target_col = RayRpcMigrationBackend(migration_config, cache_engine, worker_rank, worker_handle_list, + target_migration_backend = RayRpcMigrationBackend(migration_config, cache_engine, worker_rank, worker_handle_list, scheduling_strategy, is_driver_worker, gpu_cache) - return target_col + return target_migration_backend diff --git a/llumnix/backends/vllm/scheduler.py b/llumnix/backends/vllm/scheduler.py index a14db0b3..4c6403ae 100644 --- a/llumnix/backends/vllm/scheduler.py +++ b/llumnix/backends/vllm/scheduler.py @@ -13,19 +13,24 @@ from asyncio.log import logger import time -from typing import Dict, List, Optional, Tuple +from typing import Dict, List, Optional, Tuple, Deque from collections import deque from vllm.core.block_manager_v1 import BlockSpaceManagerV1, BlockTable from vllm.core.scheduler import (Scheduler, PreemptionMode, SequenceStatus, SequenceGroupMetadata, SchedulerOutputs) +from vllm.core.policy import PolicyFactory +from vllm.sequence import SequenceGroup +from vllm.core.interfaces import AllocStatus from llumnix.instance_info import InstanceInfo from llumnix.logger import init_logger -from llumnix.llumlet.request import RequestInferenceType +from llumnix.llumlet.request import LlumnixRequest, RequestInferenceType, RequestStatus from llumnix.backends.vllm.sequence import SequenceGroupLlumnix + logger = init_logger(__name__) + # TODO(ZeldaHuang): adapt prefix cache and sliding window, now use v1 manager class BlockManagerLlumnix(BlockSpaceManagerV1): def get_free_blocks(self, num_required_blocks: int) -> BlockTable: @@ -76,9 +81,12 @@ def _get_num_killed_requests(self) -> int: cnt += 1 return cnt - def get_running_queue(self): + def get_running_queue(self) -> Deque[SequenceGroupLlumnix]: return self.running + def get_waiting_queue(self) -> Deque[SequenceGroupLlumnix]: + return self.waiting + def get_all_request_ids(self) -> List[str]: request_ids : List[str] = [] for state_queue in [self.waiting, self.running, self.swapped]: @@ -86,18 +94,26 @@ def get_all_request_ids(self) -> List[str]: request_ids.append(seq_group.request_id) return request_ids - def get_request_incremental_blocks(self, backend_request: SequenceGroupLlumnix, pre_stage_num_blocks: int) -> List[int]: + def get_request_incremental_blocks(self, backend_request: LlumnixRequest, pre_stage_num_blocks: int) -> List[int]: seq = backend_request.get_seqs()[0] blocks = self.block_manager.get_block_table(seq) return blocks[pre_stage_num_blocks:] - def remove_running_request(self, request_id: str) -> None: + def remove_running_request(self, request_id: str) -> bool: for seq_group in self.running: if seq_group.request_id == request_id: - seq = seq_group.get_seqs()[0] self.running.remove(seq_group) - seq.status = SequenceStatus.WAITING - break + seq_group.set_status(RequestStatus.RUNNING_MIGRATING) + return True + return False + + def remove_waiting_request(self, request_id: str) -> bool: + for seq_group in self.waiting: + if seq_group.request_id == request_id: + self.waiting.remove(seq_group) + seq_group.set_status(RequestStatus.WAITING_MIGRATING) + return True + return False def add_migrating_out_request_last_stage(self, backend_request: SequenceGroupLlumnix) -> None: self.migrating_out_request_last_stage.append(backend_request) @@ -110,7 +126,17 @@ def pop_migrating_out_requests_last_stage(self) -> List[SequenceGroupLlumnix]: self.migrating_out_request_last_stage.clear() return migrating_out_request_last_stage - def pre_alloc(self, request_id: str, block_num: int) -> List[int]: + def pre_alloc(self, + request_id: str, + request_status: RequestStatus, + request_arrival_time: float, + block_num: int) -> List[int]: + # Only migrate waiting request when the waiting request is the earliest arrival one + # among the requests of dst instance's waiting queue. + if request_status == RequestStatus.WAITING_MIGRATING: + if (self.waiting and request_arrival_time > self.waiting[0].arrival_time) \ + or block_num * self.cache_config.block_size > self.prompt_limit: + return [] blocks = self.block_manager.get_free_blocks(block_num) pre_blocks = self.pre_alloc_cache_dict.get(request_id, []) pre_blocks.extend(blocks) @@ -118,13 +144,37 @@ def pre_alloc(self, request_id: str, block_num: int) -> List[int]: blocks = [block.block_number for block in blocks] return blocks - def add_running_request(self, backend_request: SequenceGroupLlumnix) -> None: - seq = backend_request.get_seqs()[0] - seq.status = SequenceStatus.RUNNING + def add_running_request(self, backend_request: LlumnixRequest) -> None: + self._set_status(backend_request, status_to=SequenceStatus.RUNNING) self.running.append(backend_request) - def is_request_running(self, backend_request: SequenceGroupLlumnix) -> bool: - return backend_request in self.running + def add_waiting_request(self, backend_request: LlumnixRequest) -> None: + self._set_status(backend_request, status_to=SequenceStatus.WAITING) + # pylint: disable=E0203 + self.waiting.append(backend_request) + fcfs_policy = PolicyFactory.get_policy(policy_name="fcfs") + self.waiting = fcfs_policy.sort_by_priority(time.time(), self.waiting) + + def can_allocate(self, seq_group: SequenceGroup) -> AllocStatus: + if seq_group.status == RequestStatus.WAITING_MIGRATING: + return AllocStatus.OK + return super().can_allocate(seq_group) + + def _allocate_and_set_running(self, seq_group: SequenceGroup) -> None: + # Change seq status to running, but request status is still waiting_migrating. + if seq_group.status == RequestStatus.WAITING_MIGRATING: + # For the waiting request migrated in, blocks have already been allocated when pre alloc. + self._set_status(seq_group, status_to=SequenceStatus.RUNNING) + seq_group.reset_status() + else: + super()._allocate_and_set_running(seq_group) + + def _set_status(self, + seq_group: SequenceGroup, + status_to: SequenceStatus, + status_from: SequenceStatus = None): + for seq in seq_group.get_seqs(status=status_from): + seq.status = status_to def free_dst_pre_alloc_cache(self, request_id: str = None) -> None: if request_id: @@ -132,6 +182,7 @@ def free_dst_pre_alloc_cache(self, request_id: str = None) -> None: # pylint: disable=protected-access self.block_manager._free_block_table(blocks) else: + # TODO(s5u13b): Only effective with one-to-one migration restriction. # Clear all pre-allocated cache of dst instance when src instance encounters exception. request_ids = list(self.pre_alloc_cache_dict.keys()) for req_id in request_ids: @@ -141,7 +192,7 @@ def free_dst_pre_alloc_cache(self, request_id: str = None) -> None: def free_src_request(self, backend_request: SequenceGroupLlumnix) -> None: seq = backend_request.get_seqs()[0] - logger.info("free seq {}".format(seq.seq_id)) + logger.info("free request: {}, free seq: {}".format(backend_request.request_id, seq.seq_id)) self.free_seq(seq) def _get_instance_info(self, scheduled_seq_groups: List[SequenceGroupLlumnix]) -> InstanceInfo: @@ -184,15 +235,18 @@ def _get_instance_info(self, scheduled_seq_groups: List[SequenceGroupLlumnix]) - if scheduled_seq_groups: instance_info.inference_type = scheduled_seq_groups[-1].inference_type # TODO(ZeldaHuang) adapt chunked-prefill - instance_info.num_batched_tokens = sum([seq_group.request_len for seq_group in scheduled_seq_groups])\ - if instance_info.inference_type == RequestInferenceType.PREFILL else len(instance_info.running_seq_lens) - instance_info.finished_request_ids = [seq_group.request_id for seq_group in self.running if seq_group.is_finished()] + instance_info.num_batched_tokens = sum([seq_group.request_len for seq_group in scheduled_seq_groups]) \ + if instance_info.inference_type == RequestInferenceType.PREFILL \ + else len(instance_info.running_seq_lens) + instance_info.finished_request_ids = [seq_group.request_id for seq_group in self.running if seq_group.finished] return instance_info def schedule(self) -> Tuple[List[SequenceGroupMetadata], SchedulerOutputs]: seq_group_metadata_list, scheduler_outputs = super().schedule() self.update_instance_info_callback(self._get_instance_info([scheduled_seq_group.seq_group \ for scheduled_seq_group in scheduler_outputs.scheduled_seq_groups])) + for seq_group in self.waiting: + seq_group.try_schedule_times += 1 return seq_group_metadata_list, scheduler_outputs def _schedule_running(self, running_queue: deque, *args, **kwargs): diff --git a/llumnix/backends/vllm/sequence.py b/llumnix/backends/vllm/sequence.py index 3c41a5c6..5964f96d 100644 --- a/llumnix/backends/vllm/sequence.py +++ b/llumnix/backends/vllm/sequence.py @@ -11,9 +11,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from vllm.sequence import SequenceGroup +from vllm.sequence import SequenceGroup, SequenceStatus -from llumnix.llumlet.request import LlumnixRequest, RequestInferenceType +from llumnix.llumlet.request import LlumnixRequest, RequestInferenceType, RequestStatus class SequenceGroupLlumnix(SequenceGroup, LlumnixRequest): @@ -41,3 +41,29 @@ def inference_type(self) -> RequestInferenceType: if self.is_prefill(): return RequestInferenceType.PREFILL return RequestInferenceType.DECODE + + @property + def finished(self) -> bool: + return self.get_seqs()[0].is_finished() + + @property + def arrival_time(self) -> float: + return self.metrics.arrival_time + + @property + def status(self) -> RequestStatus: + if self._status: + return self._status + status = self.get_seqs()[0].status + if status == SequenceStatus.RUNNING: + request_status = RequestStatus.RUNNING + elif status == SequenceStatus.WAITING: + request_status = RequestStatus.WAITING + else: + request_status = RequestStatus.FINISHED + return request_status + + @property + def prefill_num_blocks(self) -> int: + # Get the prefill len of the waiting request. + return len(self.get_seqs()[0].logical_token_blocks) diff --git a/llumnix/backends/vllm/utils.py b/llumnix/backends/vllm/utils.py index 8aafc9f1..7e49720a 100644 --- a/llumnix/backends/vllm/utils.py +++ b/llumnix/backends/vllm/utils.py @@ -48,8 +48,7 @@ def check_engine_args(engine_args: AsyncEngineArgs, engine_manager_args: EngineM engine_config = engine_args.create_engine_config() parallel_config = engine_config.parallel_config if parallel_config.world_size > 1 and migration_config.migration_backend == 'nccl': - # TODO(s5u13b): fix logger - print("Llumnix does not support TP or PP enabled model when the migration backend is nccl, change migration backend to gloo.") + logger.info("Llumnix does not support TP or PP enabled model when the migration backend is nccl, change migration backend to gloo.") engine_manager_args.migration_backend = 'gloo' detect_unsupported_feature(engine_args) diff --git a/llumnix/backends/vllm/worker.py b/llumnix/backends/vllm/worker.py index 2b0cab33..e38c3423 100644 --- a/llumnix/backends/vllm/worker.py +++ b/llumnix/backends/vllm/worker.py @@ -111,10 +111,8 @@ def migrate_cache(self, src_worker_handle_list, src_blocks: List[int], dst_block start_time = time.time() try: self.migration_backend.migrate_cache(src_worker_handle, src_blocks, dst_blocks) - # pylint: disable=broad-except - except Exception as e: - logger.info("[migrate_cache] self.rank: {}, src_worker_handle {}, meet error : {}" - .format(self.rank, src_worker_handle, e)) + except ray.exceptions.RayActorError: + logger.info("[migrate_cache] self.rank: {}, src_worker_handle {} is dead".format(self.rank, src_worker_handle)) end_time = time.time() total_kv_cache_size = len(src_blocks) * CacheEngine.get_cache_block_size( diff --git a/llumnix/config/default.py b/llumnix/config/default.py index 2a6c7758..358d9e1b 100644 --- a/llumnix/config/default.py +++ b/llumnix/config/default.py @@ -95,7 +95,7 @@ # Migrate out instance load threshold _C.MANAGER.MIGRATE_OUT_THRESHOLD = 3.0 # Request migration policy -_C.MANAGER.REQUEST_MIGRATION_POLICY = 'SJF' +_C.MANAGER.REQUEST_MIGRATION_POLICY = 'SR' # Enable defragmentation through migration based on virtual usage _C.MANAGER.ENABLE_DEFRAG = False # Drop migration if the number of stages > max_stages diff --git a/llumnix/global_scheduler/dispatch_scheduler.py b/llumnix/global_scheduler/dispatch_scheduler.py index 27458f26..51a0d36b 100644 --- a/llumnix/global_scheduler/dispatch_scheduler.py +++ b/llumnix/global_scheduler/dispatch_scheduler.py @@ -71,7 +71,7 @@ def remove_instance(self, instance_id: str) -> None: del self.instance_num_requests[instance_id] if instance_id in self.available_dispatch_instance_set: self.available_dispatch_instance_set.remove(instance_id) - + # TODO(KuilongCui): Check it when there is no decode instance. if self.num_instances >= self.num_dispatch_instances: free_instance_id = next(iter(self.instance_id_set - self.available_dispatch_instance_set)) self.available_dispatch_instance_set.add(free_instance_id) diff --git a/llumnix/global_scheduler/migration_scheduler.py b/llumnix/global_scheduler/migration_scheduler.py index 77fd9b25..3445b210 100644 --- a/llumnix/global_scheduler/migration_scheduler.py +++ b/llumnix/global_scheduler/migration_scheduler.py @@ -170,10 +170,8 @@ def pair_migration(self, migrate_instance_pairs = [] for i in range(min(len(sorted_src_instance_infos), len(sorted_dst_instance_infos))): load_diff_before_mig = sorted_src_instance_infos[i].instance_load_migrate - sorted_dst_instance_infos[i].instance_load_migrate - left_load_after_mig = self._compute_instance_load_after_migrate(sorted_src_instance_infos[i], is_migrate_in=False) right_load_after_mig = self._compute_instance_load_after_migrate(sorted_dst_instance_infos[i], is_migrate_in=True) - # Add some constrains to reduce unnecessary migrations if right_load_after_mig > self.migrate_out_load_threshold: continue @@ -186,14 +184,12 @@ def pair_migration(self, def _compute_instance_load_after_migrate(self, instance_info: InstanceInfo, is_migrate_in: bool) -> float: instance_info_after_migrate = copy.deepcopy(instance_info) num_blocks_last_running_request = instance_info_after_migrate.num_blocks_last_running_request - if is_migrate_in: instance_info_after_migrate.num_running_requests += 1 instance_info_after_migrate.num_free_gpu_blocks -= num_blocks_last_running_request else: instance_info_after_migrate.num_running_requests -= 1 instance_info_after_migrate.num_free_gpu_blocks += num_blocks_last_running_request - return self.instance_load_calculator.compute_instance_load(instance_info_after_migrate, action='migrate') class DefragConstrained(PairMigrationPolicy): diff --git a/llumnix/llm_engine_manager.py b/llumnix/llm_engine_manager.py index d98f3a8e..66739632 100644 --- a/llumnix/llm_engine_manager.py +++ b/llumnix/llm_engine_manager.py @@ -15,7 +15,6 @@ import time import csv import os -import math from typing import Dict, List, Tuple, Union, Iterable from collections import defaultdict import traceback @@ -222,22 +221,23 @@ async def _clear_request_instance_loop(self, interval: float): async def _push_migrations(self) -> None: # Push migrate when the instance_info have updated a certain number of times. if self.enable_pd_disagg: - asyncio.create_task(self._migrate(PairMigrationConstraints.PREFILL_2_DECODING, math.inf)) - asyncio.create_task(self._migrate(PairMigrationConstraints.DECODING_2_DECODING, 1)) + asyncio.create_task(self._migrate(PairMigrationConstraints.PREFILL_2_DECODING)) + asyncio.create_task(self._migrate(PairMigrationConstraints.DECODING_2_DECODING)) else: - asyncio.create_task(self._migrate(PairMigrationConstraints.NO_CONSTRAINTS, 1)) + asyncio.create_task(self._migrate(PairMigrationConstraints.NO_CONSTRAINTS)) - async def _migrate(self, pair_migration_type: PairMigrationConstraints, migrate_in_num_requests: int) -> None: + async def _migrate(self, pair_migration_type: PairMigrationConstraints) -> None: async def migrate_done_callback(ret, migrate_instance_pair: Tuple[str, str]) -> None: self.num_migrating -= 1 - if isinstance(ret, (ray.exceptions.RayActorError, KeyError)): + # TODO(s5u13b): Add more exception types for failover. + if isinstance(ret, (ray.exceptions.RayActorError, ray.exceptions.RayTaskError, KeyError)): has_error_pair = await self._check_instance_error(migrate_instance_pair) for i, has_error in enumerate(has_error_pair): # Instance without error should clear migration states. if not has_error: try: await self.instances[migrate_instance_pair[i]].clear_migration_states.remote(is_migrate_in=bool(i)) - except (ray.exceptions.RayActorError, KeyError): + except (ray.exceptions.RayActorError, ray.exceptions.RayTaskError, KeyError): has_error = True for i, has_error in enumerate(has_error_pair): if has_error: @@ -267,7 +267,7 @@ def migrate_done_callback_wrapper(migrate_instance_pair: Tuple[str, str], fut) - migrate_in_instance_name = "instance_{}".format(migrate_in_instance_id) # Use asyncio.gather to wrap ray remote call to add done callback. - task = asyncio.gather(self.instances[migrate_out_instance_id].migrate_out.remote(migrate_in_instance_name, migrate_in_num_requests), + task = asyncio.gather(self.instances[migrate_out_instance_id].migrate_out.remote(migrate_in_instance_name), return_exceptions=True) task.add_done_callback(partial(migrate_done_callback_wrapper, migrate_instance_pair)) migration_tasks.append(task) diff --git a/llumnix/llumlet/llumlet.py b/llumnix/llumlet/llumlet.py index 42be2f2c..3af73ac5 100644 --- a/llumnix/llumlet/llumlet.py +++ b/llumnix/llumlet/llumlet.py @@ -27,6 +27,7 @@ from llumnix.server_info import ServerInfo from llumnix.internal_config import MigrationConfig from llumnix.queue.queue_type import QueueType +from llumnix.llumlet.request import LlumnixRequest, RequestStatus logger = init_logger(__name__) @@ -55,7 +56,7 @@ def __init__(self, self.backend_engine) self.log_requests = True - self.check_state_thread = asyncio.create_task(self.check_state()) + asyncio.create_task(self._check_state_loop()) # pylint: disable=broad-except except Exception as e: logger.error("Failed to initialize llumlet: {}".format(e)) @@ -118,7 +119,7 @@ def from_args(cls, llumlet = engine_class.remote(instance_id, output_queue_type, backend_type, migration_config, *args, **kwargs) return llumlet - async def check_state(self): + async def _check_state_loop(self): while True: await asyncio.sleep(1) if self.backend_engine.state == EngineState.CRASHED: @@ -128,46 +129,55 @@ async def check_state(self): self_actor = ray.get_actor(self.actor_name) ray.kill(self_actor) - async def migrate_out(self, dst_instance_name: str, num_requests: int) -> List[str]: + async def migrate_out(self, dst_instance_name: str) -> List[str]: + migrate_out_requests = self.migration_scheduler.get_migrate_out_requests() + if len(migrate_out_requests) == 0: + return [] + migrated_request_list = [] + for migrate_out_request in migrate_out_requests: + migrated_request = await self._migrate_out_one_request(migrate_out_request, dst_instance_name) + migrated_request_list.extend(migrated_request) + if len(migrated_request) == 0 and migrate_out_request.eom: + break + return migrated_request_list + + async def _migrate_out_one_request(self, migrate_out_request: LlumnixRequest, dst_instance_name: str) -> List[LlumnixRequest]: try: - migrate_out_request = None + t0 = time.time() migrate_in_ray_actor = ray.get_actor(dst_instance_name, namespace='llumnix') dst_instance_id = dst_instance_name[len("instance_"):] - migrated_request_list = [] - continue_migrate = True - while continue_migrate and len(migrated_request_list) < num_requests: - t0 = time.time() - migrate_out_request = self.migration_scheduler.get_migrate_out_request() - if migrate_out_request is None: - break - - migrate_out_request.migrating = True - logger.info("{}->{} begin migrate out {}".format(self.instance_id, dst_instance_id, migrate_out_request.request_id)) - status = await self.migration_coordinator.migrate_out_multistage(migrate_in_ray_actor, migrate_out_request) - - if status == MigrationStatus.FINISHED_DONE: - await migrate_in_ray_actor.execute_engine_method.remote("commit_dst_request", migrate_out_request) - self.backend_engine.free_src_request(migrate_out_request) - migrated_request_list.append(migrate_out_request.request_id) - migrate_out_request.stage_timestamps.append(time.time()) - self.backend_engine.remove_migrating_out_request_last_stage(migrate_out_request) - else: - migrate_out_request.reset_migration_args() + logger.info("{}->{} begin migrate out".format(self.instance_id, dst_instance_id)) + migrated_request = [] + if migrate_out_request.status == RequestStatus.RUNNING: + status = await self.migration_coordinator.migrate_out_running_request(migrate_in_ray_actor, migrate_out_request) + elif migrate_out_request.status == RequestStatus.WAITING: + status = await self.migration_coordinator.migrate_out_waiting_request(migrate_in_ray_actor, migrate_out_request) + else: + return migrated_request + if status == MigrationStatus.FINISHED: + await migrate_in_ray_actor.execute_engine_method.remote("commit_dst_request", migrate_out_request) + self.backend_engine.free_src_request(migrate_out_request) + self.backend_engine.remove_migrating_out_request_last_stage(migrate_out_request) + migrated_request.append(migrate_out_request.request_id) + else: # ABORTED_SRC or ABORTED_DST + migrate_out_request.reset_migration_args_src() + migrate_out_request.reset_status() + # If dst aborts itself, dst proactively frees the pre allocated cache in migrate_in_pre_alloc. + if status == MigrationStatus.ABORTED_SRC: await migrate_in_ray_actor.execute_migration_method.remote("free_dst_pre_alloc_cache", migrate_out_request.request_id) - continue_migrate = False - t1 = time.time() - logger.info("{}->{} migrate done, migrate request {}, status:{}, len:{} blocks, cost:{} ms" \ - .format(self.instance_id, dst_instance_id, migrated_request_list, status, \ - sum(migrate_out_request.stage_num_blocks_list), (t1 - t0)*1000)) - # pylint: disable=broad-except + t1 = time.time() + logger.info("{}->{} migrate done, migrate request {}, migration status: {}, len: {} blocks, cost: {} ms" \ + .format(self.instance_id, dst_instance_id, migrated_request, status, \ + sum(migrate_out_request.stage_num_blocks_list), (t1 - t0)*1000)) + except ray.exceptions.RayActorError: + logger.info("[migrate_out] instance {} is dead".format(dst_instance_name[len("instance_"):])) + raise + # pylint: disable=W0703 except Exception as e: - if migrate_out_request: - migrate_out_request.reset_migration_args() - - logger.info("[migrate_out] src instance {}, dst instance {}, meet error: {}" - .format(self.instance_id, dst_instance_name[len("instance_"):], e)) + logger.error("unexpected exception occurs: {}".format(e)) + logger.error("exception traceback: {}".format(traceback.format_exc())) raise - return migrated_request_list + return migrated_request def get_instance_info(self) -> InstanceInfo: return self.backend_engine.engine.instance_info @@ -209,7 +219,13 @@ def clear_migration_states(self, is_migrate_in: bool) -> None: migrating_out_requests_last_stage = self.backend_engine.pop_migrating_out_requests_last_stage() for backend_request in migrating_out_requests_last_stage: logger.info("clear_migration_states: add request {} back to engine".format(backend_request.request_id)) - self.backend_engine.add_running_request(backend_request) + assert backend_request.status in [RequestStatus.WAITING_MIGRATING, RequestStatus.RUNNING_MIGRATING], \ + "The status of request in migrating_out_requests_last_stage should be \ + RequestStatus.WAITING_MIGRATING or RequestStatus.RUNNING_MIGRATING" + if backend_request.status == RequestStatus.RUNNING_MIGRATING: + self.backend_engine.add_running_request(backend_request) + else: # WAITING_MIGRATING + self.backend_engine.add_waiting_request(backend_request) def execute_migration_method(self, method, *args, **kwargs): executor = getattr(self.migration_coordinator, method) diff --git a/llumnix/llumlet/local_migration_scheduler.py b/llumnix/llumlet/local_migration_scheduler.py index ad676cc1..4f30f850 100644 --- a/llumnix/llumlet/local_migration_scheduler.py +++ b/llumnix/llumlet/local_migration_scheduler.py @@ -11,80 +11,92 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import List, Optional +from typing import Deque, List import numpy as np -from llumnix.llumlet.request import LlumnixRequest, RequestInferenceType +from llumnix.llumlet.request import LlumnixRequest, RequestStatus, RequestInferenceType from llumnix.backends.backend_interface import BackendInterface + class LocalMigrationScheduler: def __init__(self, request_migration_policy: str, backend_engine: BackendInterface) -> None: self.request_migration_policy = request_migration_policy self.backend_engine = backend_engine - def get_migrate_out_request(self, min_request_len=0, max_request_len=np.inf) -> Optional[LlumnixRequest]: - # Requests meet the strict pre-migration always have higher prioirity than other migration policy. - migrate_out_request = self.get_ready_migration_request(min_request_len, max_request_len) - if migrate_out_request is None: - if self.request_migration_policy == 'LCFS': - migrate_out_request = self.get_last_running_request(min_request_len, max_request_len) - elif self.request_migration_policy == 'LJF': - migrate_out_request = self.get_longest_running_request(min_request_len, max_request_len) - elif self.request_migration_policy == 'SJF': - migrate_out_request = self.get_shortest_running_request(min_request_len, max_request_len) - return migrate_out_request + def get_migrate_out_requests(self, min_request_len=0, max_request_len=np.inf) -> List[LlumnixRequest]: + # Requests meet the strict pre-migration always have higher prioirity than other migration policy. + migrate_out_requests: List[LlumnixRequest] = self.get_required_migration_request() + if len(migrate_out_requests) == 0: + if self.request_migration_policy == 'LCR': + migrate_out_requests = self._get_last_running_request(min_request_len, max_request_len) + elif self.request_migration_policy == 'LR': + migrate_out_requests = self._get_longest_running_request(min_request_len, max_request_len) + elif self.request_migration_policy == 'SR': + migrate_out_requests = self._get_shortest_running_request(min_request_len, max_request_len) + elif self.request_migration_policy == 'FCW': + migrate_out_requests = self._get_first_waiting_request(min_request_len, max_request_len) + elif self.request_migration_policy == 'FCWSR': + migrate_out_requests = self._get_first_waiting_and_shortest_running_requests(min_request_len, max_request_len) + return migrate_out_requests # The function is used to retrieve requests on the backend that have already met the expected_steps. - # TODO(xinyi): Currently, the function is only used for Prefill-decoding disaggregation, + # (xinyi): Currently, the function is only used for Prefill-decoding disaggregation, # and only selects request that migrates from the prefill instance to the decoding instance. - def get_ready_migration_request(self, min_request_len, max_request_len): + def get_required_migration_request(self): running: List[LlumnixRequest] = self.backend_engine.get_running_queue() - target_request: LlumnixRequest = None + required_migration_requests = [] for request in reversed(running): - if request.migrating: - continue - - if request.output_len >= request.expected_steps \ + if request.status == RequestStatus.RUNNING \ and request.inference_type == RequestInferenceType.DECODE \ - and min_request_len <= request.request_len <= max_request_len: - target_request = request - break - - return target_request - - def get_last_running_request(self, min_request_len, max_request_len): - running: List[LlumnixRequest] = self.backend_engine.get_running_queue() - target_request: LlumnixRequest = None - - for request in reversed(running): - if request.migrating: - continue - - if request.inference_type == RequestInferenceType.DECODE \ - and min_request_len <= request.request_len <= max_request_len: - target_request=request - break - - return target_request - - def get_longest_running_request(self, min_request_len, max_request_len): - running: List[LlumnixRequest] = self.backend_engine.get_running_queue() - condition = lambda request : request.inference_type == RequestInferenceType.DECODE \ - and min_request_len <= request.request_len <= max_request_len \ - and (not request.migrating) - - longest_seq_group = max((request for request in running if condition(request)), \ - key=lambda request: request.request_len, default=None) - - return longest_seq_group - - def get_shortest_running_request(self, min_request_len, max_request_len): - running: List[LlumnixRequest] = self.backend_engine.get_running_queue() - condition = lambda request : request.inference_type == RequestInferenceType.DECODE \ - and min_request_len <= request.request_len <= max_request_len \ - and (not request.migrating) - - shortest_seq_group = min((request for request in running if condition(request)), \ - key=lambda request: request.request_len, default=None) - - return shortest_seq_group + and request.output_len >= request.expected_steps: + required_migration_requests.append(request) + return required_migration_requests + + def _filter_running_queue(self, running, min_request_len, max_request_len): + filtered_running = [ + request for request in running \ + if request.status == RequestStatus.RUNNING \ + and request.inference_type == RequestInferenceType.DECODE \ + and min_request_len < request.request_len < max_request_len \ + ] + return filtered_running + + def _filter_waiting_queue(self, waiting, min_request_len, max_request_len): + filtered_waiting = [ + request for request in waiting \ + if request.status == RequestStatus.WAITING \ + and request.try_schedule_times >= 1 \ + and min_request_len < request.request_len < max_request_len \ + ] + return filtered_waiting + + def _get_last_running_request(self, min_request_len, max_request_len): + running: Deque[LlumnixRequest] = self.backend_engine.get_running_queue() + filtered_running = self._filter_running_queue(running, min_request_len, max_request_len) + return [filtered_running[-1]] if filtered_running else [] + + def _get_longest_running_request(self, min_request_len, max_request_len) -> List[LlumnixRequest]: + running: Deque[LlumnixRequest] = self.backend_engine.get_running_queue() + filtered_running = self._filter_running_queue(running, min_request_len, max_request_len) + longest_seq_group = max((request for request in filtered_running), \ + key=lambda request: request.request_len, default=None) + return [longest_seq_group] if longest_seq_group is not None else [] + + def _get_shortest_running_request(self, min_request_len, max_request_len) -> List[LlumnixRequest]: + running: Deque[LlumnixRequest] = self.backend_engine.get_running_queue() + filtered_running = self._filter_running_queue(running, min_request_len, max_request_len) + shortest_seq_group = min((request for request in filtered_running), \ + key=lambda request: request.request_len, default=None) + return [shortest_seq_group] if shortest_seq_group is not None else [] + + def _get_first_waiting_request(self, min_request_len, max_request_len) -> List[LlumnixRequest]: + waiting: Deque[LlumnixRequest] = self.backend_engine.get_waiting_queue() + filtered_waiting = self._filter_waiting_queue(waiting, min_request_len, max_request_len) + return [waiting[0]] if filtered_waiting else [] + + def _get_first_waiting_and_shortest_running_requests(self, min_request_len, max_request_len) -> List[LlumnixRequest]: + waiting_requests = self._get_first_waiting_request(min_request_len, max_request_len) + running_requests = self._get_shortest_running_request(min_request_len, max_request_len) + if waiting_requests: + waiting_requests[0].eom = True + return waiting_requests + running_requests diff --git a/llumnix/llumlet/migration_coordinator.py b/llumnix/llumlet/migration_coordinator.py index 03b20cb2..224c41c3 100644 --- a/llumnix/llumlet/migration_coordinator.py +++ b/llumnix/llumlet/migration_coordinator.py @@ -19,7 +19,7 @@ import ray from llumnix.logger import init_logger -from llumnix.llumlet.request import LlumnixRequest +from llumnix.llumlet.request import LlumnixRequest, RequestStatus from llumnix.backends.backend_interface import BackendInterface logger = init_logger(__name__) @@ -27,18 +27,16 @@ class MigrationStatus(enum.Enum): """Status of Migration.""" RUNNING = enum.auto() - # aborted by src instance - ABORTED_SRC = enum.auto() - # aborted by dst instance ABORTED_DST = enum.auto() - FINISHED_DONE = enum.auto() + ABORTED_SRC = enum.auto() + FINISHED = enum.auto() @staticmethod def is_finished(status: "MigrationStatus") -> bool: return status in [ - MigrationStatus.ABORTED_SRC, MigrationStatus.ABORTED_DST, - MigrationStatus.FINISHED_DONE + MigrationStatus.ABORTED_SRC, + MigrationStatus.FINISHED ] class MigrationCoordinator: @@ -50,36 +48,88 @@ def __init__(self, self.max_stages = max_stages self.backend_engine = backend_engine - async def migrate_out_onestage(self, migrate_in_ray_actor: "ray.actor.ActorHandle", migrate_out_request: LlumnixRequest, ) -> "MigrationStatus": - """one-stage live migration until last stage + async def migrate_out_running_request(self, + migrate_in_ray_actor: "ray.actor.ActorHandle", + migrate_out_request: LlumnixRequest) -> "MigrationStatus": + return await self._migrate_out_multistage(migrate_in_ray_actor, migrate_out_request) + + async def migrate_out_waiting_request(self, + migrate_in_ray_actor: "ray.actor.ActorHandle", + migrate_out_request: LlumnixRequest) -> "MigrationStatus": + """one-stage migration for a waiting request + """ + found = self.backend_engine.remove_waiting_request(migrate_out_request.request_id) + if not found: + return MigrationStatus.ABORTED_SRC + self.backend_engine.add_migrating_out_request_last_stage(migrate_out_request) + dst_blocks = await migrate_in_ray_actor.execute_migration_method \ + .remote("migrate_in_pre_alloc", migrate_out_request.request_id, + migrate_out_request.status, + migrate_out_request.arrival_time, + migrate_out_request.prefill_num_blocks) + if len(dst_blocks) != migrate_out_request.prefill_num_blocks: + self.backend_engine.add_waiting_request(migrate_out_request) + self.backend_engine.remove_migrating_out_request_last_stage(migrate_out_request) + return MigrationStatus.ABORTED_DST + + return MigrationStatus.FINISHED + + async def _migrate_out_multistage(self, + migrate_in_ray_actor: "ray.actor.ActorHandle", + migrate_out_request: LlumnixRequest) -> "MigrationStatus": + """Migrate out requests to a specified instance, return migrated request id. + Args: + migrate_in_ray_actor: instance actor name, used to get ray actor handle + """ + stage_count = 0 + while stage_count < self.max_stages: + stage_count += 1 + status = await self._migrate_out_onestage(migrate_in_ray_actor, migrate_out_request) + if MigrationStatus.is_finished(status): + return status + # exceed max stages + return MigrationStatus.ABORTED_SRC + + async def _migrate_out_onestage(self, + migrate_in_ray_actor: "ray.actor.ActorHandle", + migrate_out_request: LlumnixRequest) -> "MigrationStatus": + """one-stage live migration until last stage for a running request """ pre_stage_num_blocks = sum(migrate_out_request.stage_num_blocks_list) incremental_blocks = self.backend_engine.get_request_incremental_blocks(migrate_out_request, pre_stage_num_blocks) # live migration, transfer all blocks except last one(currently updating) - migration_status = MigrationStatus.RUNNING is_last_stage = (len(incremental_blocks) <= self.last_stage_max_blocks) or migrate_out_request.blocking_migration if not is_last_stage: + migration_status = MigrationStatus.RUNNING src_blocks = incremental_blocks[:-1] stage_block_num = len(incremental_blocks) - 1 dst_blocks = await migrate_in_ray_actor.execute_migration_method \ - .remote("migrate_in_pre_alloc", migrate_out_request.request_id, stage_block_num) + .remote("migrate_in_pre_alloc", migrate_out_request.request_id, + migrate_out_request.status, + migrate_out_request.arrival_time, + stage_block_num) else: # last stage migration, stop inference, transfer all blocks - migration_status = MigrationStatus.FINISHED_DONE - self.backend_engine.remove_running_request(migrate_out_request.request_id) + migration_status = MigrationStatus.FINISHED + found = self.backend_engine.remove_running_request(migrate_out_request.request_id) + if not found: + return MigrationStatus.ABORTED_SRC self.backend_engine.add_migrating_out_request_last_stage(migrate_out_request) - stage_block_num = len(incremental_blocks) src_blocks = incremental_blocks[:] + stage_block_num = len(incremental_blocks) dst_blocks = await migrate_in_ray_actor.execute_migration_method \ - .remote("migrate_in_pre_alloc", migrate_out_request.request_id, stage_block_num) + .remote("migrate_in_pre_alloc", migrate_out_request.request_id, + migrate_out_request.status, + migrate_out_request.arrival_time, + stage_block_num) if len(dst_blocks) != len(src_blocks): - # migrate-in instance failed to prev alloc + # migrate-in instance failed to pre alloc if is_last_stage: self.backend_engine.add_running_request(migrate_out_request) self.backend_engine.remove_migrating_out_request_last_stage(migrate_out_request) - migration_status = MigrationStatus.ABORTED_DST - return migration_status + return MigrationStatus.ABORTED_DST + # do stage send/recv migrate_out_request.stage_timestamps.append(time.time()) migrate_out_request.stage_num_blocks_list.append(stage_block_num) @@ -87,28 +137,21 @@ async def migrate_out_onestage(self, migrate_in_ray_actor: "ray.actor.ActorHandl await self.backend_engine.send_blocks(migrate_in_ray_actor, src_blocks, dst_blocks) if not is_last_stage and migrate_out_request.should_abort_migration(): # migrate-out request abort by scheduler during send/recv - migration_status = MigrationStatus.ABORTED_SRC + return MigrationStatus.ABORTED_SRC return migration_status - async def migrate_out_multistage(self, migrate_in_ray_actor: "ray.actor.ActorHandle", migrate_out_request: LlumnixRequest) -> "MigrationStatus": - """Migrate out requests to a specified instance, return migrated request id. - Args: - dst_instance_name:instance actor name, used to get ray actor handle - """ - state_count = 0 - while state_count < self.max_stages: - state_count += 1 - status = await self.migrate_out_onestage(migrate_in_ray_actor, migrate_out_request) - if MigrationStatus.is_finished(status): - return status - # exceed max stages - return MigrationStatus.ABORTED_SRC - - def migrate_in_pre_alloc(self, request_id: str, block_num: int) -> List[int]: + def migrate_in_pre_alloc(self, + request_id: str, + request_status: RequestStatus, + request_arrival_time: float, + block_num: int) -> List[int]: """prev alloc blocks to migrate in request """ - pre_alloc_blocks = self.backend_engine.pre_alloc(request_id ,block_num) + pre_alloc_blocks = self.backend_engine.pre_alloc(request_id, + request_status, + request_arrival_time, + block_num) if len(pre_alloc_blocks) != block_num: # failed to alloc, abort request self.free_dst_pre_alloc_cache(request_id) diff --git a/llumnix/llumlet/request.py b/llumnix/llumlet/request.py index c2aeda9e..d92e6564 100644 --- a/llumnix/llumlet/request.py +++ b/llumnix/llumlet/request.py @@ -20,6 +20,13 @@ class RequestInferenceType(str, Enum): PREFILL = "prefill" DECODE = "decode" +class RequestStatus(str, Enum): + RUNNING = "running" + WAITING = "waiting" + FINISHED = "finished" + RUNNING_MIGRATING = "running_migrating" + WAITING_MIGRATING = "waiting_migrating" + class LlumnixRequest: def __init__(self, request_id: int, server_info: ServerInfo, expected_steps: int) -> None: self.request_id = request_id @@ -32,18 +39,31 @@ def __init__(self, request_id: int, server_info: ServerInfo, expected_steps: int self.last_preemption_time = None self.stage_timestamps = [] self.stage_num_blocks_list = [] - self.migrating = False + self.try_schedule_times = 0 + self._status = None + + # end-of-migration, for multiple requests migration + self.eom = False + + def reset_migration_args_dst(self): + # By default, there is no limit on the number of steps expected for the request. + self.expected_steps = math.inf - def reset_migration_args(self): self.last_preemption_time = None self.stage_timestamps = [] self.stage_num_blocks_list = [] - # By default, there is no limit on the number of steps expected for the request. - self.expected_steps = math.inf - self.migrating = False + self.try_schedule_times = 0 - def is_finished(self) -> bool: - raise NotImplementedError + def reset_migration_args_src(self): + self.last_preemption_time = None + self.stage_timestamps = [] + self.stage_num_blocks_list = [] + + def reset_status(self): + self._status = None + + def set_status(self, status: RequestStatus): + self._status = status @property def inference_type(self) -> RequestInferenceType: @@ -61,6 +81,22 @@ def prompt_len(self) -> int: def output_len(self) -> int: raise NotImplementedError + @property + def finished(self) -> bool: + raise NotImplementedError + + @property + def arrival_time(self) -> float: + raise NotImplementedError + + @property + def status(self) -> RequestStatus: + raise NotImplementedError + + @property + def prefill_num_blocks(self) -> int: + raise NotImplementedError + # Whether the migration of request is completed within one stage. For requests that have already reached # the expected steps, blocking_migration is True. @property @@ -68,7 +104,5 @@ def blocking_migration(self) -> bool: return self.output_len >= self.expected_steps def should_abort_migration(self) -> bool: - return self.output_len == 0 \ - or (self.last_preemption_time and self.last_preemption_time > self.stage_timestamps[-1]) \ - or self.inference_type == RequestInferenceType.PREFILL \ - or self.is_finished() + return self.finished \ + or (self.last_preemption_time is not None and self.last_preemption_time > self.stage_timestamps[-1]) diff --git a/tests/e2e_test/test_bench.py b/tests/e2e_test/test_bench.py index eb93fb89..5eba27d1 100644 --- a/tests/e2e_test/test_bench.py +++ b/tests/e2e_test/test_bench.py @@ -21,7 +21,7 @@ from .test_e2e import generate_launch_command, clear_ray_state # pylint: disable=unused-import -from .utils import to_markdown_table, clean_ray +from .utils import to_markdown_table, setup_ray_env def launch_llumnix_service(command): subprocess.run(command, shell=True, check=True) @@ -91,7 +91,7 @@ def get_markdown_data(key: str, head_name: str): @pytest.mark.asyncio @pytest.mark.skipif(torch.cuda.device_count() < 1, reason="at least 1 gpus required for simple benchmark") @pytest.mark.parametrize("model", ['/mnt/model/Qwen-7B']) -async def test_simple_benchmark(clean_ray, model): +async def test_simple_benchmark(setup_ray_env, model): device_count = torch.cuda.device_count() base_port = 37037 for i in range(device_count): diff --git a/tests/e2e_test/test_e2e.py b/tests/e2e_test/test_e2e.py index 11b8617f..a3bf1977 100644 --- a/tests/e2e_test/test_e2e.py +++ b/tests/e2e_test/test_e2e.py @@ -11,7 +11,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import math import subprocess import asyncio import pytest @@ -21,7 +20,7 @@ from vllm import LLM, SamplingParams # pylint: disable=unused-import -from .utils import clean_ray +from .utils import setup_ray_env def parse_launch_mode(launch_mode: str): # 'eief' means that enable init instance by manager and enable fixed node init instance, and so on. @@ -42,8 +41,8 @@ def parse_launch_mode(launch_mode: str): def generate_launch_command(result_filename: str = "", launch_ray_cluster: bool = True, HEAD_NODE_IP: str = "127.0.0.1", ip: str = "127.0.0.1", port: int = 37000, instances_num = 1, dispatch_policy: str = "load", migration_backend = "gloo", model = "facebook/opt-125m", max_model_len: int = 2048, - launch_mode: str = 'eief', log_instance_info: bool = False, enable_pd_disagg: bool = False, - num_dispatch_instances: int = math.inf): + launch_mode: str = 'eief', log_instance_info: bool = False, + request_migration_policy: str = 'SR'): disable_init_instance_by_manager, disable_fixed_node_init_instance = parse_launch_mode(launch_mode) command = ( f"RAY_DEDUP_LOGS=0 HEAD_NODE_IP={HEAD_NODE_IP} HEAD_NODE=1 " @@ -62,14 +61,12 @@ def generate_launch_command(result_filename: str = "", launch_ray_cluster: bool f"--max-model-len {max_model_len} " f"--dispatch-policy {dispatch_policy} " f"--trust-remote-code " - f"--request-migration-policy LCFS " + f"--request-migration-policy {request_migration_policy} " f"--migration-backend {migration_backend} " f"--migration-buffer-blocks 32 " f"--migration-internal-buffer-num 2 " f"--tensor-parallel-size 1 " f"--request-output-queue-port {1234+port} " - f"{'--enable-pd-disagg ' if enable_pd_disagg else ''} " - f"{f'--num-dispatch-instances {num_dispatch_instances} ' if num_dispatch_instances != math.inf else ''} " f"{'--launch-ray-cluster ' if launch_ray_cluster else ''}" f"{'> instance_'+result_filename if len(result_filename)> 0 else ''} 2>&1 &" ) @@ -143,7 +140,7 @@ def run_vllm(model, max_model_len, sampling_params): @pytest.mark.parametrize("model", ['/mnt/model/Qwen-7B']) @pytest.mark.parametrize("migration_backend", ['rpc', 'gloo']) @pytest.mark.parametrize("launch_mode", ['eief', 'eidf', 'dief', 'didf']) -async def test_e2e(clean_ray, model, migration_backend, launch_mode): +async def test_e2e(setup_ray_env, model, migration_backend, launch_mode): if migration_backend == 'gloo' and launch_mode != 'eief': pytest.skip("When the migration backend is gloo, the launch mode of llumnix can only be eief") max_model_len = 370 diff --git a/tests/e2e_test/test_migration.py b/tests/e2e_test/test_migration.py index b1f446f1..ced1e0be 100644 --- a/tests/e2e_test/test_migration.py +++ b/tests/e2e_test/test_migration.py @@ -11,7 +11,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import math import asyncio from collections import defaultdict import re @@ -23,7 +22,7 @@ from .test_e2e import generate_launch_command from .test_bench import generate_bench_command, clear_ray_state, shutdown_llumnix_service # pylint: disable=unused-import -from .utils import to_markdown_table, clean_ray +from .utils import to_markdown_table, setup_ray_env size_pattern = re.compile(r'total_kv_cache_size:\s*([\d.]+)\s*(B|KB|MB|GB|KB|TB)') speed_pattern = re.compile(r'speed:\s*([\d.]+)GB/s') @@ -43,18 +42,18 @@ def parse_instance_log_file(log_files): speed = float(speed_match.group(1)) speed_dict[total_kv_cache_size].append(speed) - averger_speed = {} + average_speed = {} for transfer_size, speeds in speed_dict.items(): if len(speeds) <= 2: continue speeds.sort() trimmed_speeds = speeds[1:-1] - averger_speed[transfer_size] = sum(trimmed_speeds) / len(trimmed_speeds) + average_speed[transfer_size] = sum(trimmed_speeds) / len(trimmed_speeds) - assert len(averger_speed) > 0, "Migration should have occurred, but it was not detected. " + assert len(average_speed) > 0, "Migration should have occurred, but it was not detected. " - return averger_speed + return average_speed def parse_manager_log_file(log_file): df = pd.read_csv(log_file) @@ -68,7 +67,13 @@ def parse_manager_log_file(log_file): @pytest.mark.skipif(torch.cuda.device_count() < 2, reason="at least 2 gpus required for migration bench") @pytest.mark.parametrize("model", ['/mnt/model/Qwen-7B']) @pytest.mark.parametrize("migration_backend", ['rpc', 'gloo']) -async def test_migration_benchmark(clean_ray, model, migration_backend): +@pytest.mark.parametrize("migrated_request_status", ['running', 'waiting']) +async def test_migration_benchmark(model, migration_backend, migrated_request_status): + if migrated_request_status == 'waiting' and migration_backend != 'rpc': + pytest.skip("When the migrated request status is waiting, only test the rpc migration backend.") + + request_migration_policy = 'SR' if migrated_request_status == 'running' else 'FCW' + base_port = 37037 instance_output_logs = [] @@ -78,10 +83,11 @@ async def test_migration_benchmark(clean_ray, model, migration_backend): instance_output_logs.append("instance_"+output_log) launch_command = generate_launch_command(result_filename=output_log, launch_ray_cluster=False, port=base_port+i, model=model, dispatch_policy="flood", migration_backend=migration_backend, - log_instance_info=True, enable_pd_disagg=False, - num_dispatch_instances=math.inf) + log_instance_info=True, + request_migration_policy=request_migration_policy) subprocess.run(launch_command, shell=True, check=True) - await asyncio.sleep(60) + await asyncio.sleep(5) + await asyncio.sleep(30) async def run_bench_command(command): process = await asyncio.create_subprocess_shell(command) @@ -99,23 +105,23 @@ async def run_bench_command(command): _, pending = await asyncio.wait(tasks, timeout=60*30) + await asyncio.sleep(10) + if len(pending) > 0: raise RuntimeError("migration task Timeout") parse_manager_log_file("manager_instance.csv") - averger_speed = parse_instance_log_file(instance_output_logs) - - sorted_keys = sorted(averger_speed.keys(), key=lambda x: float(x.split()[0])) - - data = [ - ['migration_size'] + sorted_keys, - [f'{migration_backend}_speed(GB/s)'] + [f"{averger_speed[key]:.2f}" for key in sorted_keys] - ] - - with open("performance.txt", "a", encoding="utf-8") as f: - f.write(to_markdown_table(data)) + if migrated_request_status == 'running': + average_speed = parse_instance_log_file(instance_output_logs) + sorted_keys = sorted(average_speed.keys(), key=lambda x: float(x.split()[0])) + data = [ + ['migration_size'] + sorted_keys, + [f'{migration_backend}_speed(GB/s)'] + [f"{average_speed[key]:.2f}" for key in sorted_keys] + ] + with open("performance.txt", "a", encoding="utf-8") as f: + f.write(to_markdown_table(data)) shutdown_llumnix_service() clear_ray_state() - await asyncio.sleep(3) + await asyncio.sleep(10) diff --git a/tests/e2e_test/utils.py b/tests/e2e_test/utils.py index 492eb2fd..1c38dcc8 100644 --- a/tests/e2e_test/utils.py +++ b/tests/e2e_test/utils.py @@ -33,7 +33,7 @@ def to_markdown_table(data): return table @pytest.fixture -def clean_ray(): +def setup_ray_env(): subprocess.run(["ray", "stop"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) subprocess.run(["ray", "start", "--head", "--disable-usage-stats", "--port=6379"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) diff --git a/tests/unit_test/backends/vllm/test_migration.py b/tests/unit_test/backends/vllm/test_migration.py index e5bf4567..b74950c2 100644 --- a/tests/unit_test/backends/vllm/test_migration.py +++ b/tests/unit_test/backends/vllm/test_migration.py @@ -11,20 +11,22 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import List import asyncio import math +from unittest.mock import MagicMock import pytest import ray +from ray.util.scheduling_strategies import NodeAffinitySchedulingStrategy from vllm import EngineArgs, SamplingParams from vllm.utils import random_uuid +from vllm.sequence import SequenceStatus from llumnix.backends.vllm.llm_engine import BackendVLLM from llumnix.llumlet.llumlet import Llumlet from llumnix.backends.utils import BackendType from llumnix.internal_config import MigrationConfig -from llumnix.llumlet.request import LlumnixRequest, RequestInferenceType +from llumnix.llumlet.request import RequestInferenceType, RequestStatus from llumnix.queue.queue_type import QueueType from tests.unit_test.queue.utils import request_output_queue_server @@ -51,22 +53,60 @@ def __init__(self): self.instance_id = "0" self.backend_engine = MockBackendVLLM() +@ray.remote(num_cpus=1, max_concurrency=4) +class MockLlumletDoNotSchedule(Llumlet): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # stop the schedule in engine step loop + self.backend_engine.engine.scheduler.schedule = MagicMock() + + # For some reason, if MockScheduelrOutputs is defined outside, the constructor would raise error. + class MockScheduelrOutputs: + def __init__(self): + self.scheduled_seq_groups = [] + self.ignored_seq_groups = [] + self.num_batched_tokens = 0 + + def is_empty(self) -> bool: + return not self.scheduled_seq_groups + + scheduler_outputs = MockScheduelrOutputs() + self.backend_engine.engine.scheduler.schedule.return_value = ([], scheduler_outputs) + + self.step_async = self.backend_engine.engine.step_async + + async def step_async_try_schedule(): + request_outputs, server_infos = await self.step_async() + for seq_group in self.backend_engine.engine.scheduler.waiting: + seq_group.try_schedule_times += 1 + return request_outputs, server_infos + + self.backend_engine.engine.step_async = step_async_try_schedule + +# TODO(s5u13b): Test migrate waiting request. @pytest.mark.parametrize("migration_backend", ['rpc', 'gloo', 'nccl']) +@pytest.mark.parametrize("migration_request_status", ['waiting', 'running']) @pytest.mark.asyncio -async def test_migration_correctness(setup_ray_env, migration_backend): +async def test_migration_correctness(setup_ray_env, migration_backend, migration_request_status): engine_args = EngineArgs(model="facebook/opt-125m", worker_use_ray=True) - id_rank_map = {"0":0, "1":1} - migration_config = MigrationConfig("LCFS", migration_backend, 16, 1, 4, 5, 20, 2) + id_rank_map = {"0": 0, "1": 1, "2": 2} + if migration_request_status == 'running': + request_migration_policy = "SR" + elif migration_request_status == 'waiting': + request_migration_policy = "FCW" + migration_config = MigrationConfig(request_migration_policy, migration_backend, 16, 1, 4, 5, 20, 2) output_queue_type = QueueType.RAYQUEUE que, server_info = request_output_queue_server(output_queue_type) asyncio.create_task(que.run_server_loop()) + node_id = ray.get_runtime_context().get_node_id() + scheduling_strategy = NodeAffinitySchedulingStrategy(node_id=node_id, soft=False) llumlet_0: Llumlet = Llumlet.from_args( output_queue_type, False, - True, - ray.get_runtime_context().get_node_id(), + False, + node_id, "0", BackendType.VLLM, 1, @@ -76,24 +116,39 @@ async def test_migration_correctness(setup_ray_env, migration_backend): llumlet_1: Llumlet = Llumlet.from_args( output_queue_type, False, - True, - ray.get_runtime_context().get_node_id(), + False, + node_id, "1", BackendType.VLLM, 1, migration_config, engine_args) + llumlet_2: Llumlet = MockLlumletDoNotSchedule.options( + name='instance_2', + namespace='llumnix', + scheduling_strategy=scheduling_strategy).remote( + instance_id="2", + output_queue_type=output_queue_type, + backend_type=BackendType.VLLM, + migration_config=migration_config, + engine_args=engine_args, + node_id=node_id + ) + while True: - res = ray.get([llumlet_0.is_ready.remote(),llumlet_1.is_ready.remote()]) + res = ray.get([llumlet_0.is_ready.remote(), llumlet_1.is_ready.remote(), llumlet_2.is_ready.remote()]) if all(res): break ray.get([llumlet_0.execute_engine_method.remote("_run_workers", "rebuild_migration_backend", id_rank_map, "llumnix"), - llumlet_1.execute_engine_method.remote("_run_workers", "rebuild_migration_backend", id_rank_map, "llumnix")]) + llumlet_1.execute_engine_method.remote("_run_workers", "rebuild_migration_backend", id_rank_map, "llumnix"), + llumlet_2.execute_engine_method.remote("_run_workers", "rebuild_migration_backend", id_rank_map, "llumnix")]) # empty instance migrate out - res = ray.get(llumlet_0.migrate_out.remote("instance_1", num_requests=math.inf)) + res = ray.get(llumlet_0.migrate_out.remote("instance_1")) + assert not res + res = ray.get(llumlet_2.migrate_out.remote("instance_1")) assert not res # running without migration @@ -110,16 +165,28 @@ async def test_correctness(prompt): origin_output = request_output.outputs[0] finished = request_output.finished - request_id1 = random_uuid() - ray.get(llumlet_0.generate.remote(request_id1, server_info, math.inf, prompt, sampling_params)) - # wait prefill done - while True: - running_queue: List[LlumnixRequest] = ray.get(llumlet_0.execute_engine_method.remote("get_running_queue")) - if len(running_queue) > 0 and running_queue[0].inference_type == RequestInferenceType.DECODE: - break - # migrate request - res = ray.get(llumlet_0.migrate_out.remote("instance_1", num_requests=math.inf)) - assert len(res) == 1 + if migration_request_status == 'running': + request_id1 = random_uuid() + ray.get(llumlet_0.generate.remote(request_id1, server_info, math.inf, prompt, sampling_params)) + # wait prefill done + while True: + running_queue = ray.get(llumlet_0.execute_engine_method.remote("get_running_queue")) + if len(running_queue) > 0 and running_queue[0].inference_type == RequestInferenceType.DECODE: + break + # migrate request + res = ray.get(llumlet_0.migrate_out.remote("instance_1")) + assert len(res) == 1 + elif migration_request_status == 'waiting': + request_id1 = random_uuid() + ray.get(llumlet_2.generate.remote(request_id1, server_info, math.inf, prompt, sampling_params)) + # wait try schedule done + while True: + waiting_queue = ray.get(llumlet_2.execute_engine_method.remote("get_waiting_queue")) + if len(waiting_queue) > 0 and waiting_queue[0].try_schedule_times >= 1: + break + # migrate request + res = ray.get(llumlet_2.migrate_out.remote("instance_1")) + assert len(res) == 1 request_output_queue = que output = None @@ -127,10 +194,6 @@ async def test_correctness(prompt): while not finished: request_outputs = await request_output_queue.get() for request_output in request_outputs: - origin_output = request_output.outputs[0] - finished = request_output.finished - if request_output.request_id != request_id1: - continue output = request_output.outputs[0] finished = request_output.finished @@ -146,7 +209,7 @@ async def test_correctness(prompt): async def test_pd_diaggregation_correctness(setup_ray_env, migration_backend): engine_args = EngineArgs(model="facebook/opt-125m", worker_use_ray=True) id_rank_map = {"0":0, "1":1} - migration_config = MigrationConfig("LCFS", migration_backend, 16, 1, 4, 5, 20, 2) + migration_config = MigrationConfig("SR", migration_backend, 16, 1, 4, 5, 20, 2) output_queue_type = QueueType.RAYQUEUE que, server_info = request_output_queue_server(output_queue_type) @@ -179,12 +242,10 @@ async def test_pd_diaggregation_correctness(setup_ray_env, migration_backend): res = ray.get([llumlet_0.is_ready.remote(),llumlet_1.is_ready.remote()]) if all(res): break - - ray.get([llumlet_0.execute_engine_method.remote("_run_workers", "rebuild_migration_backend", id_rank_map, "llumnix"), - llumlet_1.execute_engine_method.remote("_run_workers", "rebuild_migration_backend", id_rank_map, "llumnix")]) - + ray.get([llumlet_0.execute_engine_method.remote("_run_workers","rebuild_migration_backend", id_rank_map, "llumnix"), + llumlet_1.execute_engine_method.remote("_run_workers","rebuild_migration_backend", id_rank_map, "llumnix")]) # empty instance migrate out - res = ray.get(llumlet_0.migrate_out.remote("instance_1", num_requests=math.inf)) + res = ray.get(llumlet_0.migrate_out.remote("instance_1")) assert not res # running without migration @@ -207,7 +268,7 @@ async def test_correctness(prompt): ray.get(llumlet_0.generate.remote(request_id1, server_info, request_expected_steps_id1, prompt, sampling_params)) # migrate request for decoding while True: - res = ray.get(llumlet_0.migrate_out.remote("instance_1", num_requests = math.inf)) + res = ray.get(llumlet_0.migrate_out.remote("instance_1")) if len(res) == 1: break request_output_queue = que @@ -216,12 +277,8 @@ async def test_correctness(prompt): while not finished: request_outputs = await request_output_queue.get() for request_output in request_outputs: - origin_output = request_output.outputs[0] + output = request_output.outputs[0] finished = request_output.finished - if request_output.request_id != request_id1: - continue - output = request_output.outputs[0] - finished = request_output.finished assert output.text == origin_output.text assert output.cumulative_logprob == origin_output.cumulative_logprob @@ -233,13 +290,19 @@ async def test_correctness(prompt): def test_clear_migration_states(): llumlet = MockLlumlet() - llumlet.backend_engine.pre_alloc("0", 1) + llumlet.backend_engine.pre_alloc("0", RequestStatus.RUNNING, 0.0, 1) num_gpu_blocks = 8 block_size = 4 llumlet.clear_migration_states(is_migrate_in=True) - assert len(llumlet.backend_engine.pre_alloc("0", num_gpu_blocks)) == num_gpu_blocks - _, seq_group = create_dummy_prompt("0",7,block_size) + assert len(llumlet.backend_engine.pre_alloc("0", RequestStatus.RUNNING, 0.0, num_gpu_blocks)) == num_gpu_blocks + _, seq_group = create_dummy_prompt("0",7,block_size,SequenceStatus.RUNNING) + seq_group.set_status(RequestStatus.RUNNING_MIGRATING) + llumlet.backend_engine.add_migrating_out_request_last_stage(seq_group) + llumlet.clear_migration_states(is_migrate_in=False) + assert len(llumlet.backend_engine.get_running_queue()) == 1 + _, seq_group = create_dummy_prompt("0",7,block_size,SequenceStatus.WAITING) + seq_group.set_status(RequestStatus.WAITING_MIGRATING) llumlet.backend_engine.add_migrating_out_request_last_stage(seq_group) llumlet.clear_migration_states(is_migrate_in=False) - assert len(llumlet.backend_engine.get_running_queue()) > 0 + assert len(llumlet.backend_engine.get_waiting_queue()) == 1 diff --git a/tests/unit_test/backends/vllm/test_scheduler.py b/tests/unit_test/backends/vllm/test_scheduler.py index 1c1af7ac..c8a03981 100644 --- a/tests/unit_test/backends/vllm/test_scheduler.py +++ b/tests/unit_test/backends/vllm/test_scheduler.py @@ -12,13 +12,14 @@ # limitations under the License. import math +import time from vllm.sequence import Sequence from vllm.sequence import Logprob from vllm.core.policy import PolicyFactory from llumnix.backends.vllm.scheduler import BlockManagerLlumnix -from llumnix.llumlet.request import RequestInferenceType +from llumnix.llumlet.request import RequestInferenceType, RequestStatus from .utils import create_dummy_prompt, initialize_scheduler, create_token_budget @@ -129,6 +130,25 @@ def test_scheduler_running_request(): scheduler.add_running_request(seq_group) assert scheduler.get_num_unfinished_seq_groups() == 4 +def test_scheduler_waiting_request(): + scheduler = initialize_scheduler() + num_seq_group = 4 + block_size = 4 + _, seq_group_0 = create_dummy_prompt("0", prompt_length=0, block_size=block_size) + for idx in range(1, num_seq_group + 1): + _, seq_group = create_dummy_prompt(str(idx), prompt_length=idx, block_size=block_size) + scheduler.add_seq_group(seq_group) + assert scheduler.get_num_unfinished_seq_groups() == 4 + scheduler.remove_waiting_request("1") + assert scheduler.get_num_unfinished_seq_groups() == 3 + _, seq_group = create_dummy_prompt("6", prompt_length=idx, block_size=block_size) + scheduler.add_waiting_request(seq_group) + assert scheduler.get_num_unfinished_seq_groups() == 4 + # Test if sort the waiting queue by arrival time in add_waiting_request. + scheduler.add_waiting_request(seq_group_0) + waiting_queue = scheduler.get_waiting_queue() + assert waiting_queue[0] == seq_group_0 + def test_scheduler_migrating_out_request_last_stage(): scheduler = initialize_scheduler() block_size = 4 @@ -142,13 +162,13 @@ def test_scheduler_migrating_out_request_last_stage(): def test_scheduler_pre_alloc(): # total 8 blocks scheduler = initialize_scheduler() - blocks = scheduler.pre_alloc("1", 2) + blocks = scheduler.pre_alloc("1", RequestStatus.RUNNING, 0.0, 2) assert len(blocks) == 2 assert len(scheduler.pre_alloc_cache_dict["1"]) == 2 - blocks = scheduler.pre_alloc("1", 4) + blocks = scheduler.pre_alloc("1", RequestStatus.RUNNING, 0.0, 4) assert len(blocks) == 4 assert len(scheduler.pre_alloc_cache_dict["1"]) == 6 - blocks = scheduler.pre_alloc("2,", 4) + blocks = scheduler.pre_alloc("2", RequestStatus.RUNNING, 0.0, 4) assert len(blocks) == 0 def test_schedule_running(): @@ -176,3 +196,37 @@ def test_schedule_running(): assert len(running_scheduled.decode_seq_groups) == 1 assert len(running_scheduled.prefill_seq_groups) == 0 assert len(remainig_running) == 1 + + # test pre alloc waiting condition + # total 8 blocks + scheduler = initialize_scheduler() + before_arrival = time.time() + _, seq_group = create_dummy_prompt("1", prompt_length=1, block_size=2, expected_steps=math.inf) + after_arrival = time.time() + blocks = scheduler.pre_alloc("2", RequestStatus.WAITING_MIGRATING, after_arrival, 2) + assert len(blocks) == 2 + scheduler.add_waiting_request(seq_group) + blocks = scheduler.pre_alloc("3", RequestStatus.WAITING_MIGRATING, after_arrival, 2) + assert len(blocks) == 0 + blocks = scheduler.pre_alloc("4", RequestStatus.WAITING_MIGRATING, before_arrival, 2) + assert len(blocks) == 2 + +def test_try_schedule_times(): + # total 8 blocks + scheduler = initialize_scheduler() + _, seq_group_1 = create_dummy_prompt("1", prompt_length=8, block_size=1) + _, seq_group_2 = create_dummy_prompt("2", prompt_length=8, block_size=1) + scheduler.add_seq_group(seq_group_1) + scheduler.add_seq_group(seq_group_2) + waiting_queue = scheduler.get_waiting_queue() + assert len(waiting_queue) == 2 + assert seq_group_1.try_schedule_times == 0 + assert seq_group_2.try_schedule_times == 0 + scheduler.schedule() + # seq_group_2 cannot be scheduled due to lack of blocks + assert seq_group_1.try_schedule_times == 0 + assert seq_group_2.try_schedule_times == 1 + scheduler.schedule() + # seq_group_1 is preempted to waiting queue + assert seq_group_1.try_schedule_times == 1 + assert seq_group_2.try_schedule_times == 2 diff --git a/tests/unit_test/backends/vllm/test_simulator.py b/tests/unit_test/backends/vllm/test_simulator.py index c0753b06..7a2632cf 100644 --- a/tests/unit_test/backends/vllm/test_simulator.py +++ b/tests/unit_test/backends/vllm/test_simulator.py @@ -71,7 +71,7 @@ async def test_backend(setup_ray_env): # TODO(ZeldaHuang): add tests for BackendSimVLLM methods # (currently BackendSimVLLM is just a wrapper of BackendVLLM) engine_args = EngineArgs(model="facebook/opt-125m", worker_use_ray=True) - migration_config = MigrationConfig("LCFS", "gloo", 16, 1, 4, 5, 20, 2) + migration_config = MigrationConfig("SR", "gloo", 16, 1, 4, 5, 20, 2) output_queue_type = QueueType.RAYQUEUE que, server_info = request_output_queue_server(output_queue_type) diff --git a/tests/unit_test/backends/vllm/utils.py b/tests/unit_test/backends/vllm/utils.py index bc8d1f09..887bdd93 100644 --- a/tests/unit_test/backends/vllm/utils.py +++ b/tests/unit_test/backends/vllm/utils.py @@ -18,7 +18,7 @@ from vllm import SamplingParams from vllm.lora.request import LoRARequest -from vllm.sequence import Logprob, Sequence +from vllm.sequence import Logprob, Sequence, SequenceStatus from vllm.config import SchedulerConfig, CacheConfig from vllm.core.scheduler import SchedulingBudget @@ -45,6 +45,7 @@ def create_dummy_prompt( request_id: str, prompt_length: int, block_size: Optional[int] = None, + status: SequenceStatus = SequenceStatus.WAITING, lora_request: Optional[LoRARequest] = None, use_beam_search: bool = False, best_of: int = 1, @@ -63,6 +64,7 @@ def create_dummy_prompt( request_id, server_info, expected_steps, [prompt], SamplingParams(use_beam_search=use_beam_search, best_of=best_of), time.time(), lora_request) + seq_group.get_seqs()[0].status = status return prompt, seq_group diff --git a/tests/unit_test/global_scheduler/test_dispatch_scheduler.py b/tests/unit_test/global_scheduler/test_dispatch_scheduler.py index 28fc129e..fedaa154 100644 --- a/tests/unit_test/global_scheduler/test_dispatch_scheduler.py +++ b/tests/unit_test/global_scheduler/test_dispatch_scheduler.py @@ -101,7 +101,7 @@ def test_dispatch_queue(): instance_info.instance_id = instance_id instance_info.num_waiting_requests = random.randint(1, 10) instance_info_dict[instance_id] = instance_info - if len(dispatch_scheduler.available_dispatch_instance_set) < dispatch_scheduler.num_dispatch_instances: + if len(dispatch_scheduler.available_dispatch_instance_set) < dispatch_scheduler.num_dispatch_instances: dispatch_scheduler.available_dispatch_instance_set.add(instance_id) instance_num_requests[instance_id] = 0 dispatch_scheduler.instance_num_requests = instance_num_requests diff --git a/tests/unit_test/global_scheduler/test_llm_engine_manager.py b/tests/unit_test/global_scheduler/test_llm_engine_manager.py index 024ad4bf..5f81baf6 100644 --- a/tests/unit_test/global_scheduler/test_llm_engine_manager.py +++ b/tests/unit_test/global_scheduler/test_llm_engine_manager.py @@ -78,14 +78,14 @@ def abort(self, request_id): self.num_requests = len(self.request_id_set) return self.num_requests - def migrate_out(self, dst_instance_name, num_requests): + def migrate_out(self, dst_instance_name): self.num_migrate_out += 1 migrate_in_ray_actor = ray.get_actor(dst_instance_name, namespace='llumnix') - ray.get(migrate_in_ray_actor.migrate_in.remote(self.actor_name, num_requests)) + ray.get(migrate_in_ray_actor.migrate_in.remote(self.actor_name)) time.sleep(0.1) return self.num_migrate_out - def migrate_in(self, src_instance_name, num_requests): + def migrate_in(self, src_instance_name): self.num_migrate_in += 1 return self.num_migrate_in diff --git a/tests/unit_test/llumlet/test_engine_step_exception.py b/tests/unit_test/llumlet/test_engine_step_exception.py index c630a04f..86176ab2 100644 --- a/tests/unit_test/llumlet/test_engine_step_exception.py +++ b/tests/unit_test/llumlet/test_engine_step_exception.py @@ -39,7 +39,7 @@ async def raise_error_step(): @pytest.mark.skipif(torch.cuda.device_count() < 1, reason="Need at least 1 GPU to run the test.") def test_engine_step_exception(setup_ray_env): engine_args = EngineArgs(model="facebook/opt-125m", max_model_len=8, worker_use_ray=True) - migration_config = MigrationConfig("LCFS", "rpc", 16, 1, 4, 5, 20, 2) + migration_config = MigrationConfig("SR", "rpc", 16, 1, 4, 5, 20, 2) node_id = ray.get_runtime_context().get_node_id() scheduling_strategy = NodeAffinitySchedulingStrategy(node_id=node_id, soft=False) diff --git a/tests/unit_test/llumlet/test_local_migration_scheduler.py b/tests/unit_test/llumlet/test_local_migration_scheduler.py index c0c6f834..ecca2b71 100644 --- a/tests/unit_test/llumlet/test_local_migration_scheduler.py +++ b/tests/unit_test/llumlet/test_local_migration_scheduler.py @@ -13,20 +13,25 @@ import math from llumnix.llumlet.local_migration_scheduler import LocalMigrationScheduler -from llumnix.llumlet.request import LlumnixRequest, RequestInferenceType +from llumnix.llumlet.request import LlumnixRequest, RequestInferenceType, RequestStatus class MockRequest(LlumnixRequest): - def __init__(self, request_id, length, expected_steps) -> None: + def __init__(self, request_id, length, expected_steps, status=RequestStatus.RUNNING) -> None: super().__init__(request_id=request_id, server_info=None, expected_steps=expected_steps) self.length = length - self.status = RequestInferenceType.DECODE + self._status = status + self._inference_type = RequestInferenceType.DECODE + self._finished = False + self.try_schedule_times = 0 + self.eom = False - def is_finished(self) -> bool: - return False + @property + def finished(self) -> bool: + return self._finished @property def inference_type(self) -> RequestInferenceType: - return self.status + return self._inference_type @property def request_len(self) -> int: @@ -40,16 +45,37 @@ def prompt_len(self) -> int: def output_len(self) -> int: return self.length + @property + def arrival_time(self) -> float: + pass + + @property + def status(self) -> RequestStatus: + return self._status + + @property + def prefill_num_blocks(self) -> int: + pass + class MockeEngine(): def __init__(self) -> None: self.running = [] + self.waiting = [] def add_request(self, request_id, length, expected_steps) -> None: self.running.append(MockRequest(request_id, length, expected_steps)) + def add_request_waiting(self, request_id, length, expected_steps) -> None: + request = MockRequest(request_id, length, expected_steps, status=RequestStatus.WAITING) + request.try_schedule_times += 1 + self.waiting.append(request) + def get_running_queue(self): return self.running + def get_waiting_queue(self): + return self.waiting + def test_scheduler_policy(): engine = MockeEngine() scheduler = LocalMigrationScheduler("", engine) @@ -57,37 +83,40 @@ def test_scheduler_policy(): engine.add_request(request_id="0", length=1, expected_steps=math.inf) engine.add_request(request_id="1", length=3, expected_steps=math.inf) engine.add_request(request_id="2", length=2, expected_steps=math.inf) - - scheduler.request_migration_policy = "LCFS" - assert scheduler.get_migrate_out_request().request_id == "2" - scheduler.request_migration_policy = "LJF" - assert scheduler.get_migrate_out_request().request_id == "1" - scheduler.request_migration_policy = "SJF" - assert scheduler.get_migrate_out_request().request_id == "0" - - engine.add_request(request_id="3", length=2, expected_steps=1) - engine.add_request(request_id="4", length=3, expected_steps=math.inf) - engine.add_request(request_id="5", length=4, expected_steps=math.inf) - scheduler.request_migration_policy = "LCFS" - request = scheduler.get_migrate_out_request() - request.migrating = True - assert request.request_id == "3" + engine.add_request_waiting(request_id="3", length=2, expected_steps=math.inf) + engine.add_request_waiting(request_id="4", length=2, expected_steps=math.inf) + + scheduler.request_migration_policy = "LCR" + assert scheduler.get_migrate_out_requests()[0].request_id == "2" + scheduler.request_migration_policy = "LR" + assert scheduler.get_migrate_out_requests()[0].request_id == "1" + scheduler.request_migration_policy = "SR" + assert scheduler.get_migrate_out_requests()[0].request_id == "0" + scheduler.request_migration_policy = "FCW" + assert scheduler.get_migrate_out_requests()[0].request_id == "3" + scheduler.request_migration_policy = "FCWSR" + assert scheduler.get_migrate_out_requests()[0].request_id == "3" + assert scheduler.get_migrate_out_requests()[1].request_id == "0" + + engine.add_request(request_id="5", length=2, expected_steps=1) + request = scheduler.get_migrate_out_requests()[0] + assert request.request_id == "5" assert request.output_len >= request.expected_steps and request.inference_type == RequestInferenceType.DECODE - request = scheduler.get_migrate_out_request() - request.migrating = True + engine.add_request(request_id="6", length=3, expected_steps=math.inf) + scheduler.request_migration_policy = "LCR" + request = scheduler.get_migrate_out_requests()[0] assert request.request_id == "5" - request = scheduler.get_migrate_out_request() - assert request.request_id == "4" + assert request.output_len >= request.expected_steps and request.inference_type == RequestInferenceType.DECODE def test_scheduler_should_abort_migration(): req_0 = MockRequest(request_id="0", length=1, expected_steps=math.inf) req_0.stage_timestamps = [1] assert req_0.should_abort_migration() is False - req_0.status = RequestInferenceType.PREFILL - assert req_0.should_abort_migration() is True - req_0.status = RequestInferenceType.DECODE req_0.last_preemption_time = 2 assert req_0.should_abort_migration() is True + req_0.last_preemption_time = None + req_0._finished = True + assert req_0.should_abort_migration() is True def test_blocking_migration(): req_0 = MockRequest(request_id="0", length=1, expected_steps=math.inf) diff --git a/tests/unit_test/llumlet/test_migration_coordinator.py b/tests/unit_test/llumlet/test_migration_coordinator.py index 8a1a4d44..fcdf0638 100644 --- a/tests/unit_test/llumlet/test_migration_coordinator.py +++ b/tests/unit_test/llumlet/test_migration_coordinator.py @@ -38,7 +38,7 @@ async def test_migrate_out_onestage(setup_ray_env): migrate_out_request = MagicMock() # Create an instance of MigrationCoordinator - coordinator = MigrationCoordinator(backend_engine, 1, 3) + coordinator = MigrationCoordinator(backend_engine, last_stage_max_blocks=1, max_stages=3) # Mock method return values and test data src_blocks = [1, 2, 3] @@ -49,7 +49,7 @@ async def test_migrate_out_onestage(setup_ray_env): migrate_in_ray_actor.execute_migration_method.remote.return_value = ray_remote_call.remote(dst_blocks) # Test normal migration scenario - status = await coordinator.migrate_out_onestage(migrate_in_ray_actor, migrate_out_request) + status = await coordinator._migrate_out_onestage(migrate_in_ray_actor, migrate_out_request) assert status == MigrationStatus.RUNNING # Test the last stage of migration @@ -59,20 +59,21 @@ async def test_migrate_out_onestage(setup_ray_env): migrate_out_request.should_abort_migration.return_value = False migrate_out_request.blocking_migration = False migrate_in_ray_actor.execute_migration_method.remote.return_value = ray_remote_call.remote(dst_blocks) - status = await coordinator.migrate_out_onestage(migrate_in_ray_actor, migrate_out_request) - assert status == MigrationStatus.FINISHED_DONE + status = await coordinator._migrate_out_onestage(migrate_in_ray_actor, migrate_out_request) + assert status == MigrationStatus.FINISHED migrate_out_request = MagicMock() - # Test migration aborted scenario + # Test migration dst aborted scenario src_blocks = [1, 2, 3] dst_blocks = [] backend_engine.get_request_incremental_blocks.return_value = src_blocks migrate_out_request.should_abort_migration.return_value = False migrate_out_request.blocking_migration = False migrate_in_ray_actor.execute_migration_method.remote.return_value = ray_remote_call.remote(dst_blocks) - status = await coordinator.migrate_out_onestage(migrate_in_ray_actor, migrate_out_request) + status = await coordinator._migrate_out_onestage(migrate_in_ray_actor, migrate_out_request) assert status == MigrationStatus.ABORTED_DST + # Test migration src aborted scenario migrate_out_request = MagicMock() src_blocks = [1, 2, 3] dst_blocks = [1, 2] @@ -80,23 +81,13 @@ async def test_migrate_out_onestage(setup_ray_env): migrate_out_request.should_abort_migration.return_value = True migrate_out_request.blocking_migration = False migrate_in_ray_actor.execute_migration_method.remote.return_value = ray_remote_call.remote(dst_blocks) - status = await coordinator.migrate_out_onestage(migrate_in_ray_actor, migrate_out_request) + status = await coordinator._migrate_out_onestage(migrate_in_ray_actor, migrate_out_request) assert status == MigrationStatus.ABORTED_SRC - migrate_out_request = MagicMock() - src_blocks = [1, 2, 3] - dst_blocks = [1, 2] - backend_engine.get_request_incremental_blocks.return_value = src_blocks - migrate_out_request.should_abort_migration.return_value = False - migrate_out_request.blocking_migration = True - migrate_in_ray_actor.execute_migration_method.remote.return_value = ray_remote_call.remote(dst_blocks) - status = await coordinator.migrate_out_onestage(migrate_in_ray_actor, migrate_out_request) - assert status == MigrationStatus.ABORTED_DST - -# setup_ray_env should be passed after migrate_out_onestage -@patch.object(MigrationCoordinator, 'migrate_out_onestage') +# setup_ray_env should be passed after _migrate_out_onestage +@patch.object(MigrationCoordinator, '_migrate_out_onestage') @pytest.mark.asyncio -async def test_migrate_out_multistage(_, setup_ray_env): +async def test_migrate_out_running_request(_, setup_ray_env): # Create mock objects backend_engine = MagicMock(spec=BackendInterface) migrate_in_ray_actor = MagicMock() @@ -110,16 +101,41 @@ async def test_migrate_out_multistage(_, setup_ray_env): migrate_in_ray_actor.execute_engine_method.remote = MagicMock() migrate_in_ray_actor.execute_engine_method.remote.return_value = ray_remote_call.remote([1]) migrate_in_ray_actor.execute_migration_method.remote.return_value = ray_remote_call.remote([1]) - coordinator.migrate_out_onestage.side_effect = [MigrationStatus.FINISHED_DONE] - status = await coordinator.migrate_out_multistage(migrate_in_ray_actor, migrate_out_request) - assert coordinator.migrate_out_onestage.call_count == 1 - assert status == MigrationStatus.FINISHED_DONE + coordinator._migrate_out_onestage.side_effect = [MigrationStatus.FINISHED] + status = await coordinator.migrate_out_running_request(migrate_in_ray_actor, migrate_out_request) + assert coordinator._migrate_out_onestage.call_count == 1 + assert status == MigrationStatus.FINISHED max_stages = 3 - coordinator.migrate_out_onestage.side_effect = [MigrationStatus.RUNNING, - MigrationStatus.RUNNING, - MigrationStatus.RUNNING, - MigrationStatus.RUNNING] - status = await coordinator.migrate_out_multistage(migrate_in_ray_actor, migrate_out_request) - assert coordinator.migrate_out_onestage.call_count == max_stages + 1 + coordinator._migrate_out_onestage.side_effect = [MigrationStatus.RUNNING, + MigrationStatus.RUNNING, + MigrationStatus.RUNNING, + MigrationStatus.RUNNING] + status = await coordinator.migrate_out_running_request(migrate_in_ray_actor, migrate_out_request) + assert coordinator._migrate_out_onestage.call_count == max_stages + 1 assert status == MigrationStatus.ABORTED_SRC + +@pytest.mark.asyncio +async def test_migrate_out_waiting_request(): + # Create mock objects + backend_engine = MagicMock(spec=BackendInterface) + migrate_in_ray_actor = MagicMock() + migrate_out_request = MagicMock() + + # Create an instance of MigrationCoordinator + coordinator = MigrationCoordinator(backend_engine, last_stage_max_blocks=1, max_stages=3) + + # Test FINISHED + migrate_out_request.prefill_num_blocks = 3 + dst_blocks = [1, 2, 3] + migrate_in_ray_actor.execute_engine_method = MagicMock() + migrate_in_ray_actor.execute_engine_method.remote = MagicMock() + migrate_in_ray_actor.execute_engine_method.remote.return_value = ray_remote_call.remote(dst_blocks) + migrate_in_ray_actor.execute_migration_method.remote.return_value = ray_remote_call.remote(dst_blocks) + status = await coordinator.migrate_out_waiting_request(migrate_in_ray_actor, migrate_out_request) + assert status == MigrationStatus.FINISHED + + # Test FINISHED_ABORTED + migrate_out_request.prefill_num_blocks = 2 + status = await coordinator.migrate_out_waiting_request(migrate_in_ray_actor, migrate_out_request) + assert status == MigrationStatus.ABORTED_DST From 8e520541b54b00c9a76690d486846c80d961b062 Mon Sep 17 00:00:00 2001 From: KuilongCui Date: Tue, 12 Nov 2024 17:06:46 +0800 Subject: [PATCH 05/10] [Core] Add RoundRobin dispatch policy (#70) --- docs/Arguments.md | 4 +-- llumnix/arg_utils.py | 5 +-- .../global_scheduler/dispatch_scheduler.py | 14 ++++++++ .../test_dispatch_scheduler.py | 33 +++++++++++++++++++ 4 files changed, 52 insertions(+), 4 deletions(-) diff --git a/docs/Arguments.md b/docs/Arguments.md index 09841a5b..32a21ed8 100644 --- a/docs/Arguments.md +++ b/docs/Arguments.md @@ -12,7 +12,7 @@ usage: -m llumnix.entrypoints.vllm.api_server [-h] [--initial-instances INITIAL_INSTANCES] [--load-metric {remaining_steps,usage_ratio}] [--polling-interval POLLING_INTERVAL] - [--dispatch-policy {balanced,load,queue}] + [--dispatch-policy {balanced,load,queue,rr}] [--enable-migration] [--pair-migration-frequency PAIR_MIGRATION_FREQUENCY] [--pair-migration-policy {balanced,defrag_constrained,defrag_relaxed}] @@ -69,7 +69,7 @@ usage: -m llumnix.entrypoints.vllm.api_server [-h] `--dispatch-policy` - Request dispatch policy. -- Possible choices: balanced, load, queue +- Possible choices: balanced, load, queue, rr - Default: "load" `--enable-migration` diff --git a/llumnix/arg_utils.py b/llumnix/arg_utils.py index 1c4c54b4..5394ea24 100644 --- a/llumnix/arg_utils.py +++ b/llumnix/arg_utils.py @@ -228,12 +228,13 @@ def add_cli_args( parser.add_argument('--dispatch-policy', type=str, - choices=['balanced', 'load', 'queue', 'flood'], + choices=['balanced', 'load', 'queue', 'flood', 'rr'], help='The request dispatch policy.\n\n' '* "balanced" dispatch request to the instance with minimum requests dispatched.\n' '* "load" dispatch request to the instance with lowest instance load.\n' '* "queue" dispatch request to the instance with minimum waiting request queue length.\n' - '* "flood" dispatch request to the instance with maximum requests dispatched.\n') + '* "flood" dispatch request to the instance with maximum requests dispatched.\n' + '* "rr" dispatch requests with round-robin policy.\n') parser.add_argument('--num-available-dispatch-instances', type=int, help='number of available instances for dispatching') diff --git a/llumnix/global_scheduler/dispatch_scheduler.py b/llumnix/global_scheduler/dispatch_scheduler.py index 51a0d36b..0f5ae030 100644 --- a/llumnix/global_scheduler/dispatch_scheduler.py +++ b/llumnix/global_scheduler/dispatch_scheduler.py @@ -137,12 +137,26 @@ def dispatch(self, logger.info("dispatch to {}, queue size: {}".format(instance_id, sorted_instance_infos[0].num_waiting_requests)) return instance_id +class RoundRobin(DispatchPolicy): + prev_instance_idx: int = -1 + + def dispatch(self, + instance_num_requests: Dict[str, int], + sorted_instance_infos: List[InstanceInfo]) -> str: + all_instance_ids = sorted(instance_num_requests.keys()) + cur_instance_idx = (self.prev_instance_idx + 1) % len(all_instance_ids) + + target_instance_id = all_instance_ids[cur_instance_idx] + self.prev_instance_idx = cur_instance_idx + return target_instance_id + class DispatchPolicyFactory: _POLICY_REGISTRY = { 'flood': Flood, 'balanced': Balanced, 'load': Load, 'queue': Queue, + 'rr': RoundRobin, } @classmethod diff --git a/tests/unit_test/global_scheduler/test_dispatch_scheduler.py b/tests/unit_test/global_scheduler/test_dispatch_scheduler.py index fedaa154..114ce551 100644 --- a/tests/unit_test/global_scheduler/test_dispatch_scheduler.py +++ b/tests/unit_test/global_scheduler/test_dispatch_scheduler.py @@ -112,3 +112,36 @@ def test_dispatch_queue(): key=lambda item: item[1].num_waiting_requests)) instance_id = dispatch_scheduler.dispatch() assert instance_info_dict[min_instance_id].num_waiting_requests == instance_info_dict[instance_id].num_waiting_requests + +def test_dispatch_rr(): + instance_num = 7 + instance_load_calculator = InstanceLoadCalculator('remaining_steps', True) + dispatch_scheduler = DispatchScheduler('rr', instance_load_calculator, 3) + instance_num_requests = {} + instance_info_dict = {} + + for instance_id in [f'instance_{i}' for i in range(instance_num)]: + instance_info = InstanceInfo() + instance_info.instance_id = instance_id + instance_info.num_waiting_requests = random.randint(1, 10) + instance_info_dict[instance_id] = instance_info + if len(dispatch_scheduler.available_dispatch_instance_set) < dispatch_scheduler.num_dispatch_instances: + dispatch_scheduler.available_dispatch_instance_set.add(instance_id) + instance_num_requests[instance_id] = 0 + dispatch_scheduler.instance_num_requests = instance_num_requests + dispatch_scheduler.instance_info = instance_info_dict + + num_request = 2 * instance_num + 2 + for idx in range(0, num_request): + instance_id = dispatch_scheduler.dispatch() + target_instance_id = idx%dispatch_scheduler.num_dispatch_instances + assert instance_id == f'instance_{target_instance_id}' + + for idx in range(instance_num): + if idx < dispatch_scheduler.num_dispatch_instances: + dispatch_scheduler.instance_num_requests[f'instance_{idx}'] = \ + num_request // dispatch_scheduler.num_dispatch_instances + (1 \ + if num_request % dispatch_scheduler.num_dispatch_instances > \ + idx % dispatch_scheduler.num_dispatch_instances else 0) + else: + dispatch_scheduler.instance_num_requests[f'instance_{idx}'] = 0 From bcd49ba2322096f76f314ebe9c7d6fd81604e6eb Mon Sep 17 00:00:00 2001 From: KuilongCui Date: Fri, 15 Nov 2024 17:16:00 +0800 Subject: [PATCH 06/10] [Refactor] refactor migration scheduler (#66) --- llumnix/global_scheduler/global_scheduler.py | 3 +- llumnix/global_scheduler/migration_filter.py | 149 ++++++++++++++++ llumnix/global_scheduler/migration_policy.py | 113 ++++++++++++ .../global_scheduler/migration_scheduler.py | 165 +----------------- llumnix/internal_config.py | 2 + llumnix/llm_engine_manager.py | 5 +- .../test_migration_scheduler.py | 88 +++++----- 7 files changed, 324 insertions(+), 201 deletions(-) create mode 100644 llumnix/global_scheduler/migration_filter.py create mode 100644 llumnix/global_scheduler/migration_policy.py diff --git a/llumnix/global_scheduler/global_scheduler.py b/llumnix/global_scheduler/global_scheduler.py index 201b57de..ec1568bb 100644 --- a/llumnix/global_scheduler/global_scheduler.py +++ b/llumnix/global_scheduler/global_scheduler.py @@ -18,7 +18,8 @@ from llumnix.internal_config import GlobalSchedulerConfig from llumnix.instance_info import InstanceLoadCalculator, InstanceInfo from llumnix.global_scheduler.dispatch_scheduler import DispatchScheduler -from llumnix.global_scheduler.migration_scheduler import MigrationScheduler, PairMigrationConstraints +from llumnix.global_scheduler.migration_scheduler import MigrationScheduler +from llumnix.global_scheduler.migration_policy import PairMigrationConstraints from llumnix.global_scheduler.scaling_scheduler import ScalingScheduler logger = init_logger(__name__) diff --git a/llumnix/global_scheduler/migration_filter.py b/llumnix/global_scheduler/migration_filter.py new file mode 100644 index 00000000..ea82e55b --- /dev/null +++ b/llumnix/global_scheduler/migration_filter.py @@ -0,0 +1,149 @@ +# Copyright (c) 2024, Alibaba Group; +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Callable, Dict, List, Optional +from abc import ABC, abstractmethod + +from llumnix.logger import init_logger +from llumnix.instance_info import InstanceInfo +from llumnix.global_scheduler.scaling_scheduler import InstanceType +from llumnix.global_scheduler.migration_policy import PairMigrationConstraints + +logger = init_logger(__name__) + +class MigrationFilterConfig: + def __init__(self, migrate_out_load_threshold): + self.migrate_out_load_threshold: float = migrate_out_load_threshold + +# TODO(KuilongCui): A filter might contain other filters; leave this for the future. +class MigrationFilterPolicy(ABC): + @abstractmethod + def filter_src_condition(self, filter_config, pair_migration_type) -> Callable[[InstanceInfo], bool]: + raise NotImplementedError + + @abstractmethod + def filter_dst_condition(self, filter_config, pair_migration_type) -> Callable[[InstanceInfo], bool]: + raise NotImplementedError + +class MigrationInstanceFilter(ABC): + def __init__(self, filter_config: MigrationFilterConfig) -> None: + self.filter_config = filter_config + self.registered_filters: Dict[str, MigrationFilterPolicy] = {} + + def register_filter(self, filter_name: str, migration_filter: MigrationFilterPolicy) -> bool: + if filter_name in self.registered_filters: + logger.warning("migration filter {} has been registered.".format(filter_name)) + return False + + self.registered_filters[filter_name] = migration_filter + return True + + def unregister_filter(self, filter_name: str) -> None: + self.registered_filters.pop(filter_name, None) + + def get_filter(self, filter_name: str) -> Optional[MigrationFilterPolicy]: + return self.registered_filters.get(filter_name, None) + + def filter_instances(self, instance_infos: List[InstanceInfo], + pair_migration_type: PairMigrationConstraints) -> Dict[str, InstanceInfo]: + src_filter_conditions = [filter.filter_src_condition() for filter in self.registered_filters.values()] + dst_filter_conditions = [filter.filter_dst_condition() for filter in self.registered_filters.values()] + + if pair_migration_type == PairMigrationConstraints.NO_CONSTRAINTS: + policy_filter = MigrationFilterPolicyFactory.get_policy("load") + elif pair_migration_type in [PairMigrationConstraints.PREFILL_2_DECODING, PairMigrationConstraints.DECODING_2_DECODING]: + policy_filter = MigrationFilterPolicyFactory.get_policy('prefill_decode') + else: + raise ValueError(f"Unsupported pair migration type: {pair_migration_type}") + src_filter_conditions.append(policy_filter.filter_src_condition(self.filter_config, pair_migration_type)) + dst_filter_conditions.append(policy_filter.filter_dst_condition(self.filter_config, pair_migration_type)) + + filtered_src_instance_infos = [info for info in instance_infos if all(cond(info) for cond in src_filter_conditions)] + filtered_dst_instance_infos = [info for info in instance_infos if all(cond(info) for cond in dst_filter_conditions)] + + return filtered_src_instance_infos, filtered_dst_instance_infos + +class LoadConstrainedFilter(MigrationFilterPolicy): + def filter_src_condition(self, filter_config: MigrationFilterConfig, + pair_migration_type: PairMigrationConstraints) -> Callable[[InstanceInfo], bool]: + return lambda instance_info: instance_info.num_killed_requests > 0 \ + or instance_info.instance_load_migrate > filter_config.migrate_out_load_threshold + + def filter_dst_condition(self, filter_config: MigrationFilterConfig, + pair_migration_type: PairMigrationConstraints) -> Callable[[InstanceInfo], bool]: + return lambda instance_info: instance_info.num_killed_requests == 0 \ + and instance_info.instance_load_migrate < filter_config.migrate_out_load_threshold + +class PddFilter(MigrationFilterPolicy): + INSTANCE_FILTER_RULES = { + PairMigrationConstraints.DECODING_2_DECODING: (InstanceType.DECODE, InstanceType.DECODE), + PairMigrationConstraints.PREFILL_2_DECODING: (InstanceType.PREFILL, InstanceType.DECODE), + } + + def filter_src_condition(self, filter_config: MigrationFilterConfig, + pair_migration_type: PairMigrationConstraints) -> Callable[[InstanceInfo], bool]: + src_type, _ = self.INSTANCE_FILTER_RULES[pair_migration_type] + instance_type_filter = lambda instance_info: instance_info.instance_type == src_type + + if pair_migration_type == PairMigrationConstraints.DECODING_2_DECODING: + inner_policy = MigrationFilterPolicyFactory.get_policy('load') + policy_filter = inner_policy.filter_src_condition(filter_config, pair_migration_type) + else: + policy_filter = lambda instance_info: True + + return lambda instance_info: instance_type_filter(instance_info) and policy_filter(instance_info) + + def filter_dst_condition(self, filter_config: MigrationFilterConfig, + pair_migration_type: PairMigrationConstraints) -> Callable[[InstanceInfo], bool]: + _, dst_type = self.INSTANCE_FILTER_RULES[pair_migration_type] + instance_type_filter = lambda instance_info: instance_info.instance_type == dst_type + + if pair_migration_type == PairMigrationConstraints.DECODING_2_DECODING: + inner_policy = MigrationFilterPolicyFactory.get_policy('load') + policy_filter = inner_policy.filter_dst_condition(filter_config, pair_migration_type) + else: + policy_filter = lambda instance_info: instance_info.num_killed_requests == 0 + + return lambda instance_info: instance_type_filter(instance_info) and policy_filter(instance_info) + +class CustomFilter(MigrationFilterPolicy): + def __init__(self): + super().__init__() + self.src_filter = lambda _: True + self.dst_filter = lambda _: True + + def set_filter_condtition(self, src_filter: Optional[Callable[[InstanceInfo], bool]] = None, + dst_filter: Optional[Callable[[InstanceInfo], bool]] = None) -> None: + if src_filter: + self.src_filter = src_filter + if dst_filter: + self.dst_filter = dst_filter + + def filter_src_condition(self, filter_config: MigrationFilterConfig, + pair_migration_type: PairMigrationConstraints) -> Callable[[InstanceInfo], bool]: + return self.src_filter + + def filter_dst_condition(self, filter_config: MigrationFilterConfig, + pair_migration_type: PairMigrationConstraints) -> Callable[[InstanceInfo], bool]: + return self.dst_filter + +class MigrationFilterPolicyFactory: + _POLICY_REGISTRY = { + 'load': LoadConstrainedFilter, + 'prefill_decode': PddFilter, + 'custom': CustomFilter, + } + + @classmethod + def get_policy(cls, policy_name: PairMigrationConstraints, **kwargs) -> MigrationFilterPolicy: + return cls._POLICY_REGISTRY[policy_name](**kwargs) diff --git a/llumnix/global_scheduler/migration_policy.py b/llumnix/global_scheduler/migration_policy.py new file mode 100644 index 00000000..c917cce7 --- /dev/null +++ b/llumnix/global_scheduler/migration_policy.py @@ -0,0 +1,113 @@ +# Copyright (c) 2024, Alibaba Group; +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import List, Tuple +from abc import ABC, abstractmethod +from enum import Enum +import copy +import numpy as np + +from llumnix.logger import init_logger +from llumnix.instance_info import InstanceInfo, InstanceLoadCalculator + +logger = init_logger(__name__) + +class PairMigrationConstraints(str, Enum): + """Target of Migration.""" + NO_CONSTRAINTS = "NO_CONSTRAINTS" + + # Enable the prefill-decoding disaggregration. + DECODING_2_DECODING = "DECODING_2_DECODING" + PREFILL_2_DECODING = "PREFILL_2_DECODING" + +class PairMigrationPolicy(ABC): + def __init__(self, + migrate_out_load_threshold: float, + instance_load_calculator: InstanceLoadCalculator) -> None: + self.migrate_out_load_threshold = migrate_out_load_threshold + self.instance_load_calculator = instance_load_calculator + + @abstractmethod + def pair_migration(self, + src_instance_infos: List[InstanceInfo], + dst_instance_infos: List[InstanceInfo], + ) -> List[Tuple[str, str]]: + raise NotImplementedError + + def sort_instance_infos(self, instance_infos: List[InstanceInfo], descending: bool = True) -> None: + key_attr = 'instance_load_migrate' + sorted_instance_infos = sorted( + instance_infos, + key=lambda instance_info: getattr(instance_info, key_attr), + reverse=descending + ) + return sorted_instance_infos + +class Balanced(PairMigrationPolicy): + def pair_migration(self, + src_instance_infos: List[InstanceInfo], + dst_instance_infos: List[InstanceInfo], + ) -> List[Tuple[str, str]]: + sorted_src_instance_infos = self.sort_instance_infos(src_instance_infos, descending=True) + sorted_dst_instance_infos = self.sort_instance_infos(dst_instance_infos, descending=False) + migrate_instance_pairs = [] + for i in range(min(len(sorted_src_instance_infos), len(sorted_dst_instance_infos))): + load_diff_before_mig = sorted_src_instance_infos[i].instance_load_migrate - sorted_dst_instance_infos[i].instance_load_migrate + + left_load_after_mig = self._compute_instance_load_after_migrate(sorted_src_instance_infos[i], is_migrate_in=False) + right_load_after_mig = self._compute_instance_load_after_migrate(sorted_dst_instance_infos[i], is_migrate_in=True) + + # Add some constrains to reduce unnecessary migrations + if right_load_after_mig > self.migrate_out_load_threshold: + continue + load_diff_after_mig = left_load_after_mig - right_load_after_mig + if (0 < load_diff_after_mig < load_diff_before_mig) or (sorted_dst_instance_infos[i].instance_load_migrate == -np.inf): + migrate_instance_pairs.append((sorted_src_instance_infos[i].instance_id, + sorted_dst_instance_infos[i].instance_id)) + return migrate_instance_pairs + + def _compute_instance_load_after_migrate(self, instance_info: InstanceInfo, is_migrate_in: bool) -> float: + instance_info_after_migrate = copy.deepcopy(instance_info) + num_blocks_last_running_request = instance_info_after_migrate.num_blocks_last_running_request + + if is_migrate_in: + instance_info_after_migrate.num_running_requests += 1 + instance_info_after_migrate.num_free_gpu_blocks -= num_blocks_last_running_request + else: + instance_info_after_migrate.num_running_requests -= 1 + instance_info_after_migrate.num_free_gpu_blocks += num_blocks_last_running_request + + return self.instance_load_calculator.compute_instance_load(instance_info_after_migrate, action='migrate') + +class DefragConstrained(PairMigrationPolicy): + def pair_migration(self, + src_instance_infos: List[InstanceInfo], + dst_instance_infos: List[InstanceInfo], + ) -> List[Tuple[str, str]]: + sorted_src_instance_infos = self.sort_instance_infos(src_instance_infos, descending=True) + sorted_dst_instance_infos = self.sort_instance_infos(dst_instance_infos, descending=False) + migrate_instance_pairs = [] + for i in range(min(len(sorted_src_instance_infos), len(sorted_dst_instance_infos))): + # without any constrain in order to make prefill migrate happens as soon as possible + migrate_instance_pairs.append((sorted_src_instance_infos[i].instance_id, sorted_dst_instance_infos[i].instance_id)) + return migrate_instance_pairs + +class PairMigrationPolicyFactory: + _POLICY_REGISTRY = { + 'balanced': Balanced, + 'defrag_constrained': DefragConstrained, + } + + @classmethod + def get_policy(cls, policy_name: str, **kwargs) -> PairMigrationPolicy: + return cls._POLICY_REGISTRY[policy_name](**kwargs) diff --git a/llumnix/global_scheduler/migration_scheduler.py b/llumnix/global_scheduler/migration_scheduler.py index 3445b210..ad538f06 100644 --- a/llumnix/global_scheduler/migration_scheduler.py +++ b/llumnix/global_scheduler/migration_scheduler.py @@ -12,31 +12,22 @@ # limitations under the License. from typing import Dict, List, Tuple, Set -from abc import ABC, abstractmethod -from enum import Enum -import copy -import numpy as np from llumnix.logger import init_logger from llumnix.instance_info import InstanceInfo, InstanceLoadCalculator -from llumnix.global_scheduler.scaling_scheduler import InstanceType +from llumnix.global_scheduler.migration_filter import MigrationInstanceFilter, MigrationFilterConfig +from llumnix.global_scheduler.migration_policy import PairMigrationConstraints, PairMigrationPolicyFactory logger = init_logger(__name__) -class PairMigrationConstraints(str, Enum): - """Target of Migration.""" - NO_CONSTRAINTS = "NO_CONSTRAINTS" - - # Enable the prefill-decoding disaggregration. - DECODING_2_DECODING = "DECODING_2_DECODING" - PREFILL_2_DECODING = "PREFILL_2_DECODING" - class MigrationScheduler: def __init__(self, pair_migration_policy: str, migrate_out_load_threshold: float, instance_load_calculator: InstanceLoadCalculator) -> None: - self.migrate_out_load_threshold = migrate_out_load_threshold + self.filter_config = MigrationFilterConfig(migrate_out_load_threshold=migrate_out_load_threshold) + self.migration_filter = MigrationInstanceFilter(self.filter_config) + self.instance_load_calculator = instance_load_calculator self.enable_defrag = instance_load_calculator.enable_defrag if not self.enable_defrag: @@ -57,14 +48,9 @@ def __init__(self, self.sorted_instance_infos: List[InstanceInfo] = None def pair_migration(self, pair_migration_type: PairMigrationConstraints) -> List[Tuple[str, str]]: - self._sort_instance_infos(descending=False) - sorted_src_instance_infos, sorted_dst_instance_infos = self._get_migration_instance_infos(pair_migration_type) - return self.pair_migration_policy.pair_migration(sorted_src_instance_infos, sorted_dst_instance_infos) - - def _get_migration_instance_infos(self, pair_migration_type: PairMigrationConstraints) -> Dict[str, InstanceInfo]: - filter_instance_infos_policy = FilteringInstanceInfosPolicyFactory.get_policy(pair_migration_type, - migrate_out_load_threshold=self.migrate_out_load_threshold) - return filter_instance_infos_policy.filter_instances(self.sorted_instance_infos,pair_migration_type) + src_instance_infos, dst_instance_infos = self.migration_filter.filter_instances( + self.instance_info.values(), pair_migration_type) + return self.pair_migration_policy.pair_migration(src_instance_infos, dst_instance_infos) def update_instance_infos(self, instance_info: Dict[str, InstanceInfo]) -> None: @@ -77,138 +63,3 @@ def add_instance(self, instance_id: str) -> None: def remove_instance(self, instance_id: str) -> None: self.instance_id_set.remove(instance_id) self.num_instances = len(self.instance_id_set) - - def _sort_instance_infos(self, - descending: bool = True) -> None: - instance_infos: List[InstanceInfo] = list(self.instance_info.values()) - key_attr = 'instance_load_migrate' - self.sorted_instance_infos = sorted( - instance_infos, - key=lambda instance_info: getattr(instance_info, key_attr), - reverse=descending - ) - -class FilteringInstanceInfosPolicy(ABC): - def __init__(self, - migrate_out_load_threshold: float) -> None: - self.migrate_out_load_threshold = migrate_out_load_threshold - self.filter_instances_rules = { - PairMigrationConstraints.NO_CONSTRAINTS: (InstanceType.NO_CONSTRAINTS, InstanceType.NO_CONSTRAINTS), - PairMigrationConstraints.DECODING_2_DECODING: (InstanceType.DECODE, InstanceType.DECODE), - PairMigrationConstraints.PREFILL_2_DECODING: (InstanceType.PREFILL, InstanceType.DECODE), - } - - def filter_instances(self, sorted_instance_infos: List[InstanceInfo], - pair_migration_type: PairMigrationConstraints = None) -> Dict[str, InstanceInfo]: - src_type, dst_type = self.filter_instances_rules[pair_migration_type] - filtered_src_instance_infos = [info for info in sorted_instance_infos if info.instance_type == src_type] - filtered_dst_instance_infos = [info for info in sorted_instance_infos if info.instance_type == dst_type] - src_instance_infos = self.filter_src_instances(filtered_src_instance_infos) - dst_instance_infos = self.filter_dst_instances(filtered_dst_instance_infos) - return src_instance_infos, dst_instance_infos - - @abstractmethod - def filter_src_instances(self, filtered_instance_infos) -> Dict[str, InstanceInfo]: - raise NotImplementedError - - @abstractmethod - def filter_dst_instances(self, filtered_instance_infos) -> Dict[str, InstanceInfo]: - raise NotImplementedError - -class FilterConstrained(FilteringInstanceInfosPolicy): - def filter_src_instances(self, filtered_instance_infos: List[InstanceInfo]) -> Dict[str, InstanceInfo]: - src_instance_infos = [i for i in reversed(filtered_instance_infos) - if i.num_killed_requests > 0 or i.instance_load_migrate > self.migrate_out_load_threshold] - return src_instance_infos - - def filter_dst_instances(self, filtered_instance_infos: List[InstanceInfo]) -> Dict[str, InstanceInfo]: - dst_instance_infos = [i for i in filtered_instance_infos - if i.num_killed_requests == 0 and i.instance_load_migrate < self.migrate_out_load_threshold] - return dst_instance_infos - -class FilterRelaxed(FilteringInstanceInfosPolicy): - # The policy is currently used to select the decoding instances to migrate requests from the prefill instances. - def filter_src_instances(self, filtered_instance_infos: List[InstanceInfo]) -> Dict[str, InstanceInfo]: - src_instance_infos = list(reversed(filtered_instance_infos)) - return src_instance_infos - - def filter_dst_instances(self, filtered_instance_infos: List[InstanceInfo]) -> Dict[str, InstanceInfo]: - dst_instance_infos = [i for i in filtered_instance_infos - if i.num_killed_requests == 0] - return dst_instance_infos - -class FilteringInstanceInfosPolicyFactory: - _POLICY_REGISTRY = { - PairMigrationConstraints.NO_CONSTRAINTS: FilterConstrained, - PairMigrationConstraints.DECODING_2_DECODING: FilterConstrained, - PairMigrationConstraints.PREFILL_2_DECODING: FilterRelaxed, - } - - @classmethod - def get_policy(cls, policy_name: PairMigrationConstraints, **kwargs) -> FilteringInstanceInfosPolicy: - return cls._POLICY_REGISTRY[policy_name](**kwargs) - -class PairMigrationPolicy(ABC): - def __init__(self, - migrate_out_load_threshold: float, - instance_load_calculator: InstanceLoadCalculator) -> None: - self.migrate_out_load_threshold = migrate_out_load_threshold - self.instance_load_calculator = instance_load_calculator - - @abstractmethod - def pair_migration(self, - sorted_src_instance_infos: List[InstanceInfo], - sorted_dst_instance_infos: List[InstanceInfo], - ) -> List[Tuple[str, str]]: - raise NotImplementedError - -class Balanced(PairMigrationPolicy): - def pair_migration(self, - sorted_src_instance_infos: List[InstanceInfo], - sorted_dst_instance_infos: List[InstanceInfo], - ) -> List[Tuple[str, str]]: - migrate_instance_pairs = [] - for i in range(min(len(sorted_src_instance_infos), len(sorted_dst_instance_infos))): - load_diff_before_mig = sorted_src_instance_infos[i].instance_load_migrate - sorted_dst_instance_infos[i].instance_load_migrate - left_load_after_mig = self._compute_instance_load_after_migrate(sorted_src_instance_infos[i], is_migrate_in=False) - right_load_after_mig = self._compute_instance_load_after_migrate(sorted_dst_instance_infos[i], is_migrate_in=True) - # Add some constrains to reduce unnecessary migrations - if right_load_after_mig > self.migrate_out_load_threshold: - continue - load_diff_after_mig = left_load_after_mig - right_load_after_mig - if (0 < load_diff_after_mig < load_diff_before_mig) or (sorted_dst_instance_infos[i].instance_load_migrate == -np.inf): - migrate_instance_pairs.append((sorted_src_instance_infos[i].instance_id, - sorted_dst_instance_infos[i].instance_id)) - return migrate_instance_pairs - - def _compute_instance_load_after_migrate(self, instance_info: InstanceInfo, is_migrate_in: bool) -> float: - instance_info_after_migrate = copy.deepcopy(instance_info) - num_blocks_last_running_request = instance_info_after_migrate.num_blocks_last_running_request - if is_migrate_in: - instance_info_after_migrate.num_running_requests += 1 - instance_info_after_migrate.num_free_gpu_blocks -= num_blocks_last_running_request - else: - instance_info_after_migrate.num_running_requests -= 1 - instance_info_after_migrate.num_free_gpu_blocks += num_blocks_last_running_request - return self.instance_load_calculator.compute_instance_load(instance_info_after_migrate, action='migrate') - -class DefragConstrained(PairMigrationPolicy): - def pair_migration(self, - sorted_src_instance_infos: List[InstanceInfo], - sorted_dst_instance_infos: List[InstanceInfo], - ) -> List[Tuple[str, str]]: - migrate_instance_pairs = [] - for i in range(min(len(sorted_src_instance_infos), len(sorted_dst_instance_infos))): - # without any constrain in order to make prefill migrate happens as soon as possible - migrate_instance_pairs.append((sorted_src_instance_infos[i].instance_id, sorted_dst_instance_infos[i].instance_id)) - return migrate_instance_pairs - -class PairMigrationPolicyFactory: - _POLICY_REGISTRY = { - 'balanced': Balanced, - 'defrag_constrained': DefragConstrained, - } - - @classmethod - def get_policy(cls, policy_name: str, **kwargs) -> PairMigrationPolicy: - return cls._POLICY_REGISTRY[policy_name](**kwargs) diff --git a/llumnix/internal_config.py b/llumnix/internal_config.py index 08f5283f..4412c13b 100644 --- a/llumnix/internal_config.py +++ b/llumnix/internal_config.py @@ -51,6 +51,8 @@ def __init__( self.dispatch_policy = dispatch_policy self.pair_migration_policy = pair_migration_policy + # TODO(KuilongCui): Use a better way to set the threshold, as having both positive and negative + # values can cause confusion. self.migrate_out_load_threshold = migrate_out_threshold*(-1) self.enable_defrag = enable_defrag diff --git a/llumnix/llm_engine_manager.py b/llumnix/llm_engine_manager.py index 66739632..5d8c48a5 100644 --- a/llumnix/llm_engine_manager.py +++ b/llumnix/llm_engine_manager.py @@ -363,7 +363,8 @@ def scale_up(self, instance_id: Union[str, Iterable[str]], llumlet_actor_handles # a coroutine is already handling the changes in the number of instances in the cluster and it will account for the changes # caused by this scale-up (see rebuild_migrate_backend for details). Therefore, we simply return in this case. Specifically, # for RPC, the Ray actor handle is used for the migration cache, so there is no need to rebuild the group. - if self.engine_manager_args.migration_backend != 'rpc' and indeed_update and no_pending_instance: + if self.enable_migration and self.engine_manager_args.migration_backend != 'rpc' \ + and indeed_update and no_pending_instance: asyncio.create_task(self.rebuild_migrate_backend()) return self.num_instances @@ -386,7 +387,7 @@ def scale_down(self, instance_id: Union[str, Iterable[str]], rebuild_migrate_bac self.global_scheduler.scale_down(instance_ids) self.num_instances = len(self.instances) - if self.engine_manager_args.migration_backend != 'rpc': + if self.enable_migration and self.engine_manager_args.migration_backend != 'rpc': if len(self.instances) == 0: self.pending_rebuild_migration_instances = 0 diff --git a/tests/unit_test/global_scheduler/test_migration_scheduler.py b/tests/unit_test/global_scheduler/test_migration_scheduler.py index 8fd32105..fa25e1f8 100644 --- a/tests/unit_test/global_scheduler/test_migration_scheduler.py +++ b/tests/unit_test/global_scheduler/test_migration_scheduler.py @@ -17,11 +17,13 @@ import numpy as np from llumnix.instance_info import InstanceLoadCalculator, InstanceInfo -from llumnix.global_scheduler.migration_scheduler import MigrationScheduler, PairMigrationConstraints +from llumnix.global_scheduler.migration_scheduler import MigrationScheduler from llumnix.global_scheduler.scaling_scheduler import InstanceType +from llumnix.global_scheduler.migration_filter import MigrationInstanceFilter, MigrationFilterConfig +from llumnix.global_scheduler.migration_policy import PairMigrationConstraints MIGRATE_OUT_LOAD_THRESHOLD = 3.0 -INSTANCE_NUM = 4 +INSTANCE_NUM = 16 def init_migration_scheduler(policy='balanced'): instance_load_calculator = InstanceLoadCalculator('remaining_steps', True) @@ -43,57 +45,66 @@ def test_add_instance_and_remove_instance(migration_scheduler): migration_scheduler.remove_instance('instance_2') assert migration_scheduler.num_instances == 0 -@pytest.mark.parametrize("pair_migration_type", ['NO_CONSTRAINTS','DECODING_2_DECODING','PREFILL_2_DECODING']) -def test_get_migration_instance_infos(pair_migration_type): +@pytest.mark.parametrize("pair_migration_type", ['NO_CONSTRAINTS', 'DECODING_2_DECODING', 'PREFILL_2_DECODING']) +def test_migration_filter(pair_migration_type): num_tests = 1000 + migration_filter = MigrationInstanceFilter(MigrationFilterConfig(MIGRATE_OUT_LOAD_THRESHOLD)) + for _ in range(num_tests): - instance_info_dict = {} - for instance_id in [f'instance_{i}' for i in range(1, INSTANCE_NUM + 1)]: + instance_infos = [] + + total_prefill_instance_num = 0 + + for instance_id in range(1, INSTANCE_NUM + 1): instance_info = InstanceInfo() instance_info.instance_id = instance_id instance_info.instance_load_migrate = MIGRATE_OUT_LOAD_THRESHOLD + random.uniform(-1, 1) instance_info.num_killed_requests = random.randint(0, 1) + if pair_migration_type == PairMigrationConstraints.NO_CONSTRAINTS: constraint_prefill_instance_num = math.inf else: constraint_prefill_instance_num = random.randint(1, INSTANCE_NUM) - migration_scheduler = init_migration_scheduler() + if constraint_prefill_instance_num == math.inf: instance_info.instance_type = InstanceType.NO_CONSTRAINTS else: - if len([info for info in instance_info_dict.values() - if info.instance_type == InstanceType.PREFILL]) < constraint_prefill_instance_num: + if total_prefill_instance_num < constraint_prefill_instance_num: instance_info.instance_type = InstanceType.PREFILL + total_prefill_instance_num += 1 else: instance_info.instance_type = InstanceType.DECODE - instance_info_dict[instance_id] = instance_info - migration_scheduler.instance_info = instance_info_dict - migration_scheduler._sort_instance_infos(descending=False) - sorted_src_instance_infos, sorted_dst_instance_infos = migration_scheduler._get_migration_instance_infos(pair_migration_type) - for instance in sorted_src_instance_infos: - if pair_migration_type != PairMigrationConstraints.PREFILL_2_DECODING: - assert instance.num_killed_requests > 0 \ - or instance.instance_load_migrate > MIGRATE_OUT_LOAD_THRESHOLD - if pair_migration_type == PairMigrationConstraints.NO_CONSTRAINTS: - assert instance.instance_type == InstanceType.NO_CONSTRAINTS - elif migration_scheduler == PairMigrationConstraints.DECODING_2_DECODING: - assert instance.instance_type == InstanceType.DECODE - else: - assert instance.instance_type == InstanceType.PREFILL - for instance in sorted_dst_instance_infos: - if pair_migration_type != PairMigrationConstraints.PREFILL_2_DECODING: - assert instance.num_killed_requests == 0 and instance.instance_load_migrate < MIGRATE_OUT_LOAD_THRESHOLD - if pair_migration_type == PairMigrationConstraints.NO_CONSTRAINTS: - assert instance.instance_type == InstanceType.NO_CONSTRAINTS - elif migration_scheduler == PairMigrationConstraints.DECODING_2_DECODING: + + instance_infos.append(instance_info) + + src_instance_infos, dst_instance_infos = migration_filter.filter_instances(instance_infos, pair_migration_type) + + for instance in src_instance_infos: + if pair_migration_type != PairMigrationConstraints.PREFILL_2_DECODING: + assert instance.num_killed_requests > 0 \ + or instance.instance_load_migrate > MIGRATE_OUT_LOAD_THRESHOLD + if pair_migration_type == PairMigrationConstraints.NO_CONSTRAINTS: + assert instance.instance_type == InstanceType.NO_CONSTRAINTS + elif pair_migration_type == PairMigrationConstraints.DECODING_2_DECODING: + assert instance.instance_type == InstanceType.DECODE + else: + assert instance.instance_type == InstanceType.PREFILL + + for instance in dst_instance_infos: + if pair_migration_type != PairMigrationConstraints.PREFILL_2_DECODING: + assert instance.num_killed_requests == 0 and instance.instance_load_migrate < MIGRATE_OUT_LOAD_THRESHOLD + if pair_migration_type == PairMigrationConstraints.NO_CONSTRAINTS: + assert instance.instance_type == InstanceType.NO_CONSTRAINTS + elif pair_migration_type == PairMigrationConstraints.DECODING_2_DECODING: + assert instance.instance_type == InstanceType.DECODE + else: assert instance.instance_type == InstanceType.DECODE - else: - assert instance.instance_type == InstanceType.DECODE - assert instance.num_killed_requests == 0 + assert instance.num_killed_requests == 0 -@pytest.mark.parametrize("policy", ['balanced','defrag_constrained']) +@pytest.mark.parametrize("policy", ['balanced', 'defrag_constrained']) def test_pair_migration(policy): num_tests = 1000 + for _ in range(num_tests): migration_scheduler = init_migration_scheduler(policy) instance_info_dict = {} @@ -106,14 +117,9 @@ def test_pair_migration(policy): instance_info.instance_type = InstanceType.NO_CONSTRAINTS instance_info_dict[instance_id] = instance_info migration_scheduler.instance_info = instance_info_dict - migration_scheduler._sort_instance_infos(descending=False) - sorted_src_instance_infos = [i for i in reversed(migration_scheduler.sorted_instance_infos) - if i.instance_type == InstanceType.NO_CONSTRAINTS - and (i.num_killed_requests > 0 or i.instance_load_migrate > migration_scheduler.migrate_out_load_threshold)] - sorted_dst_instance_infos = [i for i in migration_scheduler.sorted_instance_infos - if i.instance_type == InstanceType.NO_CONSTRAINTS - and (i.num_killed_requests == 0 and i.instance_load_migrate < migration_scheduler.migrate_out_load_threshold)] - migrate_instance_pairs = migration_scheduler.pair_migration_policy.pair_migration(sorted_src_instance_infos, sorted_dst_instance_infos) + + migrate_instance_pairs = migration_scheduler.pair_migration(PairMigrationConstraints.NO_CONSTRAINTS) + for migrate_out_instance, migrate_in_instance in migrate_instance_pairs: assert migrate_out_instance != migrate_in_instance if policy == 'balanced': From ba15665ff60e9ba4c95ff6abf97d800b5601a28c Mon Sep 17 00:00:00 2001 From: Xinyi Zhang <114055322+Xinyi-ECNU@users.noreply.github.com> Date: Thu, 5 Dec 2024 18:43:55 +0800 Subject: [PATCH 07/10] [Misc] Usage Doc for Prefill-decoding Disaggregation (#71) Co-authored-by: Hanyu Zhao --- README.md | 3 +- docs/Prefill-decoding_Disaggregation.md | 49 ++++++++++++++++++++++++ docs/pdd_design.png | Bin 0 -> 74438 bytes docs/pdd_rationale.png | Bin 0 -> 100369 bytes 4 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 docs/Prefill-decoding_Disaggregation.md create mode 100644 docs/pdd_design.png create mode 100644 docs/pdd_rationale.png diff --git a/README.md b/README.md index a13ba57e..8c75feb9 100644 --- a/README.md +++ b/README.md @@ -54,10 +54,11 @@ python -m llumnix.entrypoints.vllm.api_server \ During the serving deployment execution, Llumnix will automatically configure itself and serve as the request scheduling layer on top of the multiple vLLM engine instances. Visit our [documentation](./docs/) to get started: -- [QuickStart](./docs/Quickstart.md) +- [Quick Start](./docs/Quickstart.md) - [Supported Models](./docs/Supported_Models.md) - [Fault Tolerance](./docs/Fault_Tolerance.md) - [Simulator](./docs/Simulator.md) +- [Prefill-decoding Disaggregation](./docs/Prefill-decoding_Disaggregation.md) ## Performance We evaluate the performance of the KV-cache-aware load-balancing scheduler and migration mechanism of Llumnix with 16 Llama2-7B/Qwen1.5-7B instances, each using an A10 GPU (24GB). diff --git a/docs/Prefill-decoding_Disaggregation.md b/docs/Prefill-decoding_Disaggregation.md new file mode 100644 index 00000000..e6471653 --- /dev/null +++ b/docs/Prefill-decoding_Disaggregation.md @@ -0,0 +1,49 @@ +# Prefill-decoding Disaggregation (Experimental) + +Prefill-decoding disaggregation is a technique that computes the prefill and decoding phases on separate instances, designed mainly for reducing the inteference between the two phases and better utilizing heterogeneous hardware. For each request, following the prefill phase, the system migrates the generated key-value (KV) cache to the decoding instance and continues the computation. + +We find Llumnix well-suited for implementing P-D disaggregation, because this technique is inherently a special request scheduling policy and fits well in Llumnix's modeling for request scheduling. Specifically, P-D disaggregation can be decomposed into two rules (shown below): (1) a special dispatching rule, i.e., P-instances-only; and (2) a special migration rule, i.e., migrate to D instances after one step. Llumnix provides an implementation of P-D disaggregation following this principle. + +
+ +
+ +## Benefits + +Implementing P-D disaggregation in Llumnix has the following benefits. + +1. **Reuses most of the system-level mechanisms**. As P-D disaggregation is a special case for our cross-instance scheduling abstraction, Llumnix has built-in mechanisms essential for P-D disaggregation from day one, e.g., KV cache transfer, decoupled API server and token forwarding, fault tolerance (for P and D instances, respectively). +2. **Non-intruisive to inference engines**. Llumnix treats all instances simply as inference engines supporting both prefill and decoding. Therefore, inference engines don't need to be aware of the concepts of prefill and decoding instances, making the engine implementation simpler, cleaner, and more focusing on the inference computation itself. +3. **Seamlessly integrates with Llumnix's native scheduling capabilities**. In the P-D disaggregation scheme, we still have scheduling decisions to make: which P instance to dispatch, which D instance to migrate. Llumnix's scheduling policies are readily available for them. Moreover, the migration between D instances is still helpful, e.g., for load balancing. The graph below shows the three scheduling behaviors and how Llumnix combines them. + +
+ +
+ +## Supported Features +1. Requests can be **automatically migrated** from prefill instance to decoding instances. + +2. Users can specify the number of prefill and decoding instances. + +3. Llumnix supports both one-to-many and many-to-one migrations from prefill to decoding instances, e.g., when the numbers of the two types of instances are uneven. + +4. Decoding instances can still migrate requests among themselves based on different scheduling strategies (e.g. load-balance). + +## Limitations + +Currently P-D disaggregation is an experimental feature, mainly to demonstrate the feasibility of implementing it using Llumnix's abstractions. Yet, we haven't added advanced features or performance optimizations, including but not limited to: + +1. Per-layer KV cache transfer (currently we use a simple blocking transfer); +2. Explicit or automatic assignment of P/D instances (currently we only allow users to specify the instance numbers, with simple assignment rules); +3. Smarter fault tolerance (currently, due to the simple P/D assignment, if one of the instance types has all of its instances gone, the service will hang; we will implement better P/D assignment and fault tolerance strategies to ensure high availability); +4. Heterogeneous instances, e.g., different device types, sizes, or parallelisms; +5. Fine tuning of the scheduling policies. + +We are actively working on these items. Stay tuned :) + +## How to use +Llumnix uses two simple arguments to enable prefill-decoding disaggregation in the current version. +- `--enable-pd-disagg True` is used to enable prefill-decoding disaggregation. +- `--num-available-dispatch-instances` is used to configure the initial number of prefill instances. + +Note that one should make sure that `num-available-dispatch-instances` is smaller than `initial_instances` (especially when `--enable-scaling` is not set), otherwise there would be no instances for decoding. \ No newline at end of file diff --git a/docs/pdd_design.png b/docs/pdd_design.png new file mode 100644 index 0000000000000000000000000000000000000000..24f1ef33a86bb1b28d36bffaf93e4a50a6c45625 GIT binary patch literal 74438 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°7w(+6RM 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벪%n(7g?(xaX(7u=heHc!BK`tfFebZ#jd> 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Ȫ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 G?f(FYD4m!9 literal 0 HcmV?d00001 From 312edd8a636e43cc9460484d317e171244f7aa9a Mon Sep 17 00:00:00 2001 From: Hanyu Zhao Date: Thu, 5 Dec 2024 18:55:16 +0800 Subject: [PATCH 08/10] [Doc] v0.1.0 release (#72) --- README.md | 17 ++++++++++------- docs/v0.1.0_benchmark.png | Bin 0 -> 292768 bytes 2 files changed, 10 insertions(+), 7 deletions(-) create mode 100644 docs/v0.1.0_benchmark.png diff --git a/README.md b/README.md index 8c75feb9..09902f69 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,8 @@ Efficient and easy multi-instance LLM serving ## 🔥 Latest News -- [2024.7] We officially released the first version of Llumnix! +- [2024.11] Llumnix v0.1.0 launched! +- [2024.7] We officially released the first version of Llumnix. - [2024.6] We released our OSDI '24 [research paper](https://arxiv.org/abs/2406.03243) on arxiv. ## 🚀 Why Llumnix @@ -22,14 +23,16 @@ Llumnix provides optimized multi-instance serving performance in terms of: - *Low latency* - **Reduced time-to-first-token** (TTFT) and queuing delays with less memory fragmentation - **Reduced time-between-tokens** (TBT) and preemption stalls with better load balancing -- *High throughput* with integration with state-of-the-art inference engines +- *High throughput* + - Integration with state-of-the-art inference engines + - Support for techniques like prefill-decoding disaggregation Llumnix achieves this with: - Dynamic, fine-grained, KV-cache-aware scheduling - Continuous **rescheduling** across instances - Enabled by a KV cache migration mechanism with near-zero overhead - - Exploited for continuous load balancing and de-fragmentation + - Exploited for continuous load balancing, de-fragmentation, and prefill-decoding disaggregation Llumnix is easy to use with: @@ -61,17 +64,17 @@ Visit our [documentation](./docs/) to get started: - [Prefill-decoding Disaggregation](./docs/Prefill-decoding_Disaggregation.md) ## Performance -We evaluate the performance of the KV-cache-aware load-balancing scheduler and migration mechanism of Llumnix with 16 Llama2-7B/Qwen1.5-7B instances, each using an A10 GPU (24GB). +We evaluate the performance of the KV-cache-aware load-balancing scheduler and migration mechanism of Llumnix with 16 Qwen2.5-7B instances (each using an A10-24GB GPU) and 16 Llama2-13B instances (each using an A800-80GB GPU). We use Poisson distributions with different request rates to generate request arrivals. For the input/output lengths of requests, we use ShareGPT dataset.
- +
-With the KV-cache-aware load-balancing scheduler, Llumnix outperforms a simple load balancing scheduler based on queue sizes in TTFT (prefill) by up to 1.8x and 7.7x for mean and P99, and 1.4x for P99 TBT (decode). +Llumnix outperforms a simple round-robin scheduler in TTFT (prefill) by up to 6.4x and 12.1x for mean and P99, and 12% for P99 TBT (decode). Llumnix also shows significantly shorter average preemption stalls (by two orders of magnitude). -With migration mechanism, Llumnix maintains lower preemption stalls, further outperformers load-balance scheduler in TTFT by up to 1.7x and 3.3x for mean and P99, and 1.3x for P99 TBT. +With the KV-cache-aware load-balancing scheduler and the migration mechanism, Llumnix also outperforms a simple load balancing scheduler based on queue sizes in TTFT (prefill) by up to 4.6x and 9.1x for mean and P99, and 15% for P99 TBT (decode). ## Roadmap diff --git a/docs/v0.1.0_benchmark.png b/docs/v0.1.0_benchmark.png new file mode 100644 index 0000000000000000000000000000000000000000..03f11eb09db803642df5754f47dc446e648f8885 GIT binary patch literal 292768 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
qmG*1KmRPBNwZItqT4|8#q#y%sasYCXWc*Q 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%0A5CG?ss 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 V_j#K}gsJ@OaA;&m>w)C_{{ Date: Tue, 10 Dec 2024 17:46:33 +0800 Subject: [PATCH 09/10] [CI][Bugfix] Refine ci tests and revert many-to-many migration commit to avoid ci tests failure (#74) Co-authored-by: Biao Sun --- .github/workflows/bench_test.yml | 6 +- .github/workflows/e2e_test.yml | 6 +- .github/workflows/migration_test.yml | 6 +- .github/workflows/offline_inference.yml | 11 +- .github/workflows/pylint.yml | 4 +- .github/workflows/unit_test.yml | 6 +- .github/workflows/whl_build.yml | 2 +- Makefile | 12 +- configs/base.yml | 3 +- docs/Arguments.md | 9 +- llumnix/arg_utils.py | 18 +- .../backends/migration_backend_interface.py | 23 -- llumnix/backends/utils.py | 6 +- llumnix/backends/vllm/llm_engine.py | 46 ++-- llumnix/backends/vllm/migration_backend.py | 69 +++--- llumnix/backends/vllm/scheduler.py | 9 +- llumnix/backends/vllm/simulator.py | 4 +- llumnix/backends/vllm/worker.py | 8 +- llumnix/config/default.py | 4 +- llumnix/entrypoints/utils.py | 12 +- llumnix/entrypoints/vllm/api_server.py | 2 +- llumnix/entrypoints/vllm/utils.py | 2 +- llumnix/internal_config.py | 4 +- llumnix/llm_engine_manager.py | 58 +++-- llumnix/llumlet/llumlet.py | 24 ++- llumnix/llumlet/local_migration_scheduler.py | 2 + llumnix/llumlet/migration_coordinator.py | 159 ++++++++------ llumnix/llumlet/request.py | 12 +- llumnix/logger.py | 3 +- llumnix/queue/utils.py | 5 +- llumnix/server_info.py | 8 +- tests/conftest.py | 77 +++++-- tests/e2e_test/test_bench.py | 99 ++++----- tests/e2e_test/test_e2e.py | 115 ++-------- tests/e2e_test/test_migration.py | 131 ++++++++---- tests/e2e_test/utils.py | 162 +++++++++++++- .../backends/vllm/test_llm_engine.py | 6 +- .../unit_test/backends/vllm/test_migration.py | 23 +- .../backends/vllm/test_migration_backend.py | 199 +++++------------- .../unit_test/backends/vllm/test_simulator.py | 9 +- tests/unit_test/backends/vllm/test_worker.py | 12 +- tests/unit_test/entrypoints/test_utils.py | 4 +- .../entrypoints/vllm/api_server_manager.py | 22 +- .../entrypoints/vllm/test_api_server.py | 6 +- .../test_llm_engine_manager.py | 4 + .../llumlet/test_engine_step_exception.py | 22 +- tests/unit_test/queue/test_zmq.py | 4 +- tests/unit_test/queue/utils.py | 8 +- tools/bench_test.sh | 6 - tools/e2e_test.sh | 6 - tools/migration_test.sh | 6 - tools/run_test.sh | 11 + tools/unit_test.sh | 6 - 53 files changed, 783 insertions(+), 698 deletions(-) delete mode 100755 tools/bench_test.sh delete mode 100755 tools/e2e_test.sh delete mode 100755 tools/migration_test.sh create mode 100755 tools/run_test.sh delete mode 100755 tools/unit_test.sh diff --git a/.github/workflows/bench_test.yml b/.github/workflows/bench_test.yml index 5753c4f9..b70e97da 100644 --- a/.github/workflows/bench_test.yml +++ b/.github/workflows/bench_test.yml @@ -11,7 +11,7 @@ on: jobs: cancel_previous_workflows: runs-on: ubuntu-latest - timeout-minutes: 3 + timeout-minutes: 1 steps: - uses: styfle/cancel-workflow-action@0.12.1 with: @@ -20,7 +20,7 @@ jobs: bench_tests: needs: cancel_previous_workflows runs-on: [self-hosted] - timeout-minutes: 60 + timeout-minutes: 30 steps: - name: Checkout uses: actions/checkout@v4 @@ -28,7 +28,7 @@ jobs: run: | [[ -n $(docker ps -q) ]] && docker kill $(docker ps -q) || echo "No running containers to kill." - name: Build And Test - run: ./tools/bench_test.sh + run: ./tools/run_test.sh bench_test - name: Create comment from file if: ${{ github.event_name != 'push' }} uses: actions/github-script@v7 diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml index 62d1d144..f247a6eb 100644 --- a/.github/workflows/e2e_test.yml +++ b/.github/workflows/e2e_test.yml @@ -11,7 +11,7 @@ on: jobs: cancel_previous_workflows: runs-on: ubuntu-latest - timeout-minutes: 3 + timeout-minutes: 1 steps: - uses: styfle/cancel-workflow-action@0.12.1 with: @@ -20,7 +20,7 @@ jobs: e2e_tests: needs: cancel_previous_workflows runs-on: [self-hosted] - timeout-minutes: 60 + timeout-minutes: 30 steps: - name: Checkout uses: actions/checkout@v4 @@ -28,4 +28,4 @@ jobs: run: | [[ -n $(docker ps -q) ]] && docker kill $(docker ps -q) || echo "No running containers to kill." - name: Build And Test - run: ./tools/e2e_test.sh + run: ./tools/run_test.sh e2e_test diff --git a/.github/workflows/migration_test.yml b/.github/workflows/migration_test.yml index ff2431ba..6e7ea903 100644 --- a/.github/workflows/migration_test.yml +++ b/.github/workflows/migration_test.yml @@ -11,7 +11,7 @@ on: jobs: cancel_previous_workflows: runs-on: ubuntu-latest - timeout-minutes: 3 + timeout-minutes: 1 steps: - uses: styfle/cancel-workflow-action@0.12.1 with: @@ -20,7 +20,7 @@ jobs: migration_tests: needs: cancel_previous_workflows runs-on: [self-hosted] - timeout-minutes: 60 + timeout-minutes: 90 steps: - name: Checkout uses: actions/checkout@v4 @@ -28,7 +28,7 @@ jobs: run: | [[ -n $(docker ps -q) ]] && docker kill $(docker ps -q) || echo "No running containers to kill." - name: Build And Test - run: ./tools/migration_test.sh + run: ./tools/run_test.sh migration_test - name: Create comment from file if: ${{ github.event_name != 'push' }} uses: actions/github-script@v7 diff --git a/.github/workflows/offline_inference.yml b/.github/workflows/offline_inference.yml index 91a5d46a..24084c6e 100644 --- a/.github/workflows/offline_inference.yml +++ b/.github/workflows/offline_inference.yml @@ -11,7 +11,7 @@ on: jobs: cancel_previous_workflows: runs-on: ubuntu-latest - timeout-minutes: 3 + timeout-minutes: 1 steps: - uses: styfle/cancel-workflow-action@0.12.1 with: @@ -20,13 +20,8 @@ jobs: offline_inference: needs: cancel_previous_workflows runs-on: [self-hosted] - timeout-minutes: 10 + timeout-minutes: 5 steps: - uses: actions/checkout@v4 - name: Run offline inference example - run: | - nvidia-docker run --rm -t --net host --ipc host \ - -v ${PWD}:/workspace \ - -w /workspace \ - registry.cn-beijing.aliyuncs.com/llumnix/llumnix-dev:20240909_action_678a439 \ - bash -c "pip install -e . > /dev/null && make offline_test" + run: ./tools/run_test.sh offline_test diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 2bd3f152..460c2657 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -11,7 +11,7 @@ on: jobs: cancel_previous_workflows: runs-on: ubuntu-latest - timeout-minutes: 3 + timeout-minutes: 1 steps: - uses: styfle/cancel-workflow-action@0.12.1 with: @@ -20,7 +20,7 @@ jobs: pylint_test: needs: cancel_previous_workflows runs-on: [self-hosted] - timeout-minutes: 10 + timeout-minutes: 5 steps: - uses: actions/checkout@v4 - name: Analysing the code with pylint diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index 7bc72029..7bb0052d 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -11,7 +11,7 @@ on: jobs: cancel_previous_workflows: runs-on: ubuntu-latest - timeout-minutes: 3 + timeout-minutes: 1 steps: - uses: styfle/cancel-workflow-action@0.12.1 with: @@ -20,7 +20,7 @@ jobs: unit_tests: needs: cancel_previous_workflows runs-on: [self-hosted] - timeout-minutes: 60 + timeout-minutes: 30 steps: - name: Checkout uses: actions/checkout@v4 @@ -28,4 +28,4 @@ jobs: run: | [[ -n $(docker ps -q) ]] && docker kill $(docker ps -q) || echo "No running containers to kill." - name: Build And Test - run: ./tools/unit_test.sh + run: ./tools/run_test.sh unit_test diff --git a/.github/workflows/whl_build.yml b/.github/workflows/whl_build.yml index e0ca05c1..17ea8b67 100644 --- a/.github/workflows/whl_build.yml +++ b/.github/workflows/whl_build.yml @@ -11,7 +11,7 @@ on: jobs: whl_build: runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 1 steps: - name: Checkout diff --git a/Makefile b/Makefile index 8f75c380..a071a8d0 100644 --- a/Makefile +++ b/Makefile @@ -31,9 +31,9 @@ lint: check_pylint_installed check_pytest_installed test: check_pytest_installed @pytest -v --ignore=third_party/ --ignore=tests/e2e_test --disable-warnings @python examlpes/offline_inference.py - @pytest -v ./tests/e2e_test/test_e2e.py - @pytest -v ./tests/e2e_test/test_bench.py - @pytest -v ./tests/e2e_test/test_migration.py + @pytest -v -x -s --tb=long ./tests/e2e_test/test_e2e.py + @pytest -v -x -s --tb=long ./tests/e2e_test/test_bench.py + @pytest -v -x -s --tb=long ./tests/e2e_test/test_migration.py .PHONY: unit_test unit_test: check_pytest_installed @@ -45,15 +45,15 @@ offline_test: .PHONY: e2e_test e2e_test: - @pytest -v ./tests/e2e_test/test_e2e.py + @pytest -v -x -s --tb=long ./tests/e2e_test/test_e2e.py .PHONY: bench_test bench_test: - @pytest -v ./tests/e2e_test/test_bench.py + @pytest -v -x -s --tb=long ./tests/e2e_test/test_bench.py .PHONY: migration_test migration_test: - @pytest -v ./tests/e2e_test/test_migration.py + @pytest -v -x -s --tb=long ./tests/e2e_test/test_migration.py #################### pygloo install for gloo migration backend begin #################### diff --git a/configs/base.yml b/configs/base.yml index b9ee7077..3bd5d9bf 100644 --- a/configs/base.yml +++ b/configs/base.yml @@ -1,7 +1,7 @@ SERVER: HOST: '127.0.0.1' PORT: 1234 - QUEUE_TYPE: "rayqueue" + REQUEST_OUTPUT_QUEUE_TYPE: "rayqueue" RAY_CLUSTER_PORT: 6379 LAUNCH_RAY_CLUSTER: True @@ -20,6 +20,5 @@ MANAGER: MIGRATION_BACKEND: 'gloo' MIGRATION_BUFFER_BLOCKS: 512 - MIGRATION_INTERNAL_BUFFER_NUM: 2 ENABLE_SCALING: False diff --git a/docs/Arguments.md b/docs/Arguments.md index 32a21ed8..37ff6a99 100644 --- a/docs/Arguments.md +++ b/docs/Arguments.md @@ -32,7 +32,7 @@ usage: -m llumnix.entrypoints.vllm.api_server [-h] [--profiling-result-file-path PROFILING_RESULT_FILE_PATH] [--gpu-type GPU_TYPE] [--polling-interval POLLING_INTERVAL] - [--migration-backend {gloo,rpc}] + [--migration-backend {gloo,nccl,rpc}] [--migration-buffer-blocks MIGRATION_BUFFER_BLOCKS] [--migration-backend-init-timeout MIGRATION_BACKEND_INIT_TIMEOUT] [--migration-num-layers MIGRATION_NUM_LAYERS] @@ -40,7 +40,6 @@ usage: -m llumnix.entrypoints.vllm.api_server [-h] [--max-stages MAX_STAGES] [--enable-pd-disagg] [--num-dispatch-instances NUM_DISPATCH_INSTANCES] - [--migration-internal-buffer-num MIGRATION_INTERNAL_BUFFER_NUM] [--log-request-timestamps] ``` @@ -149,7 +148,7 @@ usage: -m llumnix.entrypoints.vllm.api_server [-h] - Default: "rpc" `--migration-buffer-blocks` -- Number of cache blocks in each migration buffer. +- Number of cache blocks in migration. - Default: 512 `--migration-backend-init-timeout` @@ -168,10 +167,6 @@ usage: -m llumnix.entrypoints.vllm.api_server [-h] - Drop migration if the number of stages > max_stages. - Default: 3 -`--migration-internal-buffer-num` -- Number of the buffer in migration backend for sending and receiving -- Default: 2 - `--log-request-timestamps` - Enable logging request timestamps. diff --git a/llumnix/arg_utils.py b/llumnix/arg_utils.py index 5394ea24..ee9f5b20 100644 --- a/llumnix/arg_utils.py +++ b/llumnix/arg_utils.py @@ -47,7 +47,7 @@ def add_argument(self, *args, **kwargs): class LlumnixEntrypointsArgs: launch_ray_cluster: bool = None ray_cluster_port: int = None - queue_type: str = None + request_output_queue_type: str = None request_output_queue_port: int = None disable_log_requests_server: bool = None log_request_timestamps: bool = None @@ -82,10 +82,10 @@ def add_cli_args(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: parser.add_argument("--ray-cluster-port", type=int, help='ray cluster port') - parser.add_argument("--queue-type", + parser.add_argument("--request-output-queue-type", type=str, choices=['rayqueue', 'zmq'], - help='queue type for request output queue') + help='request output queue type for request output queue') parser.add_argument("--request-output-queue-port", type=int, help='port for zmq') @@ -138,7 +138,6 @@ class EngineManagerArgs: migration_num_layers: int = None last_stage_max_blocks: int = None max_stages: int = None - migration_internal_buffer_num: int = None enable_pd_disagg: bool = None @@ -177,8 +176,7 @@ def create_migration_config(self) -> MigrationConfig: self.migration_num_layers, self.last_stage_max_blocks, self.max_stages, - self.migration_backend_init_timeout, - self.migration_internal_buffer_num) + self.migration_backend_init_timeout) return migration_config @classmethod @@ -197,9 +195,6 @@ def check_args(cls, args: 'EngineManagerArgs', parser: argparse.ArgumentParser): if hasattr(action, 'choices') and action.choices is not None and hasattr(args, action.dest): assert getattr(args, action.dest) in action.choices, f"{action.dest} should be one of {action.choices}." - assert args.migration_backend != 'nccl', 'NCCL has been temporarily deprecated due to its incompatibility with \ - concurrent migrations in Llumnix.' - assert args.migration_backend != 'gloo' or (args.migration_backend == 'gloo' \ and not args.disable_init_instance_by_manager and not args.disable_fixed_node_init_instance), \ ("When using gloo as migration backend, " @@ -316,16 +311,13 @@ def add_cli_args( help='timeout(s) for initializing migration backend') parser.add_argument('--migration-buffer-blocks', type=int, - help='number of cache blocks in each migration buffer') + help='number of cache blocks in migration') parser.add_argument('--migration-num-layers', type=int, help='number of kv-cache layers to transfer in each round during migration') parser.add_argument('--last-stage-max-blocks', type=int, help='if the number pf remain blocks < last_stage_max_blocks, do last stage migration') - parser.add_argument('--migration-internal-buffer-num', - type=int, - help='number of the buffer in migration backend for sending and receiving') parser.add_argument('--max-stages', type=int, help='drop migration if the number of stages > max_stages') diff --git a/llumnix/backends/migration_backend_interface.py b/llumnix/backends/migration_backend_interface.py index 9fd231cc..808ba8c8 100644 --- a/llumnix/backends/migration_backend_interface.py +++ b/llumnix/backends/migration_backend_interface.py @@ -13,9 +13,7 @@ from abc import ABC, abstractmethod from typing import List -import queue -import torch class MigrationBackendBase(ABC): @abstractmethod @@ -41,24 +39,3 @@ def do_send(self, dst_handle, blocks: List[int]): @abstractmethod def do_recv(self, src_handle, blocks: List[int]): raise NotImplementedError - -class BufferMigrationBackend(MigrationBackendBase): - def __init__(self, num_buffer, buffer_shape, buffer_dtype, buffer_device, pin_memory, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.num_buffer = num_buffer - - self.dummy_buffer = [ - torch.empty(size=buffer_shape, dtype=buffer_dtype, device=buffer_device, pin_memory=pin_memory) - for _ in range(self.num_buffer) - ] - - self.avaiable_buffer_queue = queue.Queue() - for i in range(self.num_buffer): - self.avaiable_buffer_queue.put_nowait(i) - - def get_available_cache(self): - return self.avaiable_buffer_queue.get() - - def put_back_cache(self, buffer_id): - self.avaiable_buffer_queue.put_nowait(buffer_id) diff --git a/llumnix/backends/utils.py b/llumnix/backends/utils.py index 9a1a74a8..16e4da4d 100644 --- a/llumnix/backends/utils.py +++ b/llumnix/backends/utils.py @@ -19,16 +19,16 @@ from llumnix.backends.backend_interface import BackendInterface, BackendType from llumnix.queue.queue_type import QueueType -def init_backend_engine(instance_id: str, output_queue_type: QueueType, +def init_backend_engine(instance_id: str, request_output_queue_type: QueueType, backend_type: BackendType, *args, **kwargs) -> BackendInterface: if backend_type == BackendType.VLLM: # pylint: disable=import-outside-toplevel from llumnix.backends.vllm.llm_engine import BackendVLLM - backend_engine = BackendVLLM(instance_id, output_queue_type, *args, **kwargs) + backend_engine = BackendVLLM(instance_id, request_output_queue_type, *args, **kwargs) elif backend_type == BackendType.SIM_VLLM: # pylint: disable=import-outside-toplevel from llumnix.backends.vllm.simulator import BackendSimVLLM - backend_engine = BackendSimVLLM(instance_id, output_queue_type, *args, **kwargs) + backend_engine = BackendSimVLLM(instance_id, request_output_queue_type, *args, **kwargs) else: raise ValueError(f'Unsupported backend: {backend_type}') return backend_engine diff --git a/llumnix/backends/vllm/llm_engine.py b/llumnix/backends/vllm/llm_engine.py index 59b41fa7..13a2f6e9 100644 --- a/llumnix/backends/vllm/llm_engine.py +++ b/llumnix/backends/vllm/llm_engine.py @@ -39,16 +39,18 @@ from llumnix.server_info import ServerInfo from llumnix.internal_config import MigrationConfig from llumnix.queue.queue_client_base import QueueClientBase -from llumnix.queue.utils import init_output_queue_client, QueueType +from llumnix.queue.utils import init_request_output_queue_client, QueueType logger = init_logger(__name__) +NO_OUTPUTS_STEP_INTERVAL = 0.01 + class AsyncPutQueueActor: - def __init__(self, instance_id, output_queue_type: QueueType): + def __init__(self, instance_id, request_output_queue_type: QueueType): self.instance_id = instance_id - self.output_queue_type = output_queue_type - self.request_output_queue_client: QueueClientBase = init_output_queue_client(output_queue_type) + self.request_output_queue_type = request_output_queue_type + self.request_output_queue_client: QueueClientBase = init_request_output_queue_client(request_output_queue_type) self.engine_actor_handle = None async def put_nowait_to_servers(self, @@ -66,13 +68,13 @@ async def put_nowait_to_servers(self, tasks.append(asyncio.create_task(self.request_output_queue_client.put_nowait(req_outputs, server_info))) rets = await asyncio.gather(*tasks, return_exceptions=True) for idx, ret in enumerate(rets): - if isinstance(ret, (TimeoutError, ray.exceptions.RayActorError)): + if isinstance(ret, Exception): server_id = list(server_request_outputs.keys())[idx] server_info = server_info_dict[server_id] - logger.info("Server {} is dead".format(server_id)) - if self.output_queue_type == QueueType.ZMQ: + logger.info("server {} is dead".format(server_id)) + if self.request_output_queue_type == QueueType.ZMQ: logger.info("request output queue ip: {}, port: {}".format(server_info.request_output_queue_ip, - server_info.request_output_queue_port)) + server_info.request_output_queue_port)) req_outputs = list(server_request_outputs.values())[idx] request_ids = [req_output.request_id for req_output in req_outputs] self.engine_actor_handle.abort_request.remote(request_ids) @@ -85,7 +87,7 @@ async def put_nowait_to_servers(self, class LLMEngineLlumnix(_AsyncLLMEngine): def __init__(self, instance_id: str, - output_queue_type: QueueType, + request_output_queue_type: QueueType, placement_group: Optional[PlacementGroup], node_id: Optional[str], *arg, **kwargs) -> None: @@ -116,7 +118,7 @@ def __init__(self, self.async_put_queue_actor = ray.remote( num_cpus=0, scheduling_strategy=scheduling_strategy - )(AsyncPutQueueActor).remote(instance_id, output_queue_type) + )(AsyncPutQueueActor).remote(instance_id, request_output_queue_type) self.put_queue_loop_thread.start() # pylint: disable=W0221 @@ -124,7 +126,7 @@ def __init__(self, def from_engine_args( cls, engine_args: EngineArgs, - output_queue_type: QueueType, + request_output_queue_type: QueueType, migration_config: MigrationConfig, usage_context: UsageContext = UsageContext.ENGINE_CONTEXT, instance_id: str = None, @@ -153,7 +155,7 @@ def from_engine_args( # Create the LLM engine. engine = cls( instance_id=instance_id, - output_queue_type=output_queue_type, + request_output_queue_type=request_output_queue_type, placement_group=placement_group, node_id=node_id, **engine_config.to_dict(), @@ -188,20 +190,27 @@ def _process_model_outputs( seq_group_metadata_list = new_seq_group_metadata_list for ignored_seq_group in ignored_seq_groups: server_infos.append(ignored_seq_group.server_info) + for server_info in server_infos: if hasattr(server_info, 'request_timestamps'): server_info.request_timestamps.engine_process_model_outputs_timestamp_begin = time.time() + request_outputs = super()._process_model_outputs(output, scheduled_seq_groups, ignored_seq_groups, seq_group_metadata_list) + for request_output, server_info in zip(request_outputs, server_infos): if hasattr(server_info, 'request_timestamps'): request_output.request_timestamps = server_info.request_timestamps request_output.request_timestamps.engine_process_model_outputs_timestamp_end = time.time() + if request_output.finished: + logger.info("engine finished request {}".format(request_output.request_id)) + # TODO(ZeldaHuang): Use LlumnixRequestOutput to store llumnix output args. return request_outputs, server_infos async def step_async(self) -> Tuple[List[RequestOutput], List[ServerInfo]]: step_begin_time = time.time() request_outputs, server_infos = await super().step_async() + for request_output in request_outputs: if hasattr(request_output, 'request_timestamps'): request_output.request_timestamps.engine_step_timestamp_begin = step_begin_time @@ -223,9 +232,12 @@ async def step_async(self) -> Tuple[List[RequestOutput], List[ServerInfo]]: tot_blocks.extend(blocks) tot_blocks = set(tot_blocks) instance_info.num_blocks_last_running_request = len(tot_blocks) + + self.instance_info = instance_info + if request_outputs: self.put_queue_args_queue.put_nowait((request_outputs, server_infos)) - self.instance_info = instance_info + for request_output in request_outputs: if hasattr(request_output, 'request_timestamps'): request_output.request_timestamps.engine_step_postprocess_timestamp_end = time.time() @@ -276,14 +288,14 @@ class BackendVLLM(BackendInterface): def __init__( self, instance_id: str, - output_queue_type: QueueType, + request_output_queue_type: QueueType, migration_config: MigrationConfig, engine_args: EngineArgs, placement_group: PlacementGroup = None, node_id: str = None ) -> None: self.engine: LLMEngineLlumnix = LLMEngineLlumnix.from_engine_args(engine_args=engine_args, - output_queue_type=output_queue_type, + request_output_queue_type=request_output_queue_type, migration_config=migration_config, instance_id=instance_id, placement_group=placement_group, @@ -318,7 +330,7 @@ async def _start_engine_step_loop(self) -> None: try: request_outputs, _ = await self.engine.step_async() if len(request_outputs) == 0: - await asyncio.sleep(0.01) + await asyncio.sleep(NO_OUTPUTS_STEP_INTERVAL) # pylint: disable=broad-except except Exception as e: logger.error("Error in engine loop: {}".format(e)) @@ -349,7 +361,7 @@ def add_request(self, def commit_dst_request(self, backend_request: SequenceGroupLlumnix) -> None: seq = backend_request.get_seqs()[0] seq.seq_id = next(self.engine.seq_counter) - logger.info("add seq {} to block table".format(seq.seq_id)) + logger.info("pop request {} from pre_alloc_cache_dict".format(backend_request.request_id)) pre_alloc_blocks = self.engine.scheduler.pre_alloc_cache_dict.pop(backend_request.request_id) self.engine.scheduler.block_manager.add_block_table(pre_alloc_blocks, seq.seq_id) backend_request.reset_migration_args_dst() diff --git a/llumnix/backends/vllm/migration_backend.py b/llumnix/backends/vllm/migration_backend.py index 950c1b31..a6f2c375 100644 --- a/llumnix/backends/vllm/migration_backend.py +++ b/llumnix/backends/vllm/migration_backend.py @@ -15,15 +15,11 @@ import torch from func_timeout import func_set_timeout, FunctionTimedOut -import cupy -from cupy.cuda import nccl import ray import ray.util.collective as col -from ray.util.collective.collective_group import nccl_util - from vllm.worker.cache_engine import CacheEngine from llumnix.internal_config import MigrationConfig -from llumnix.backends.migration_backend_interface import MigrationBackendBase, BufferMigrationBackend +from llumnix.backends.migration_backend_interface import MigrationBackendBase from llumnix.logger import init_logger logger = init_logger(__name__) @@ -44,16 +40,17 @@ def exec_method(self, is_driver_worker, handle, *args, **kwargs): NUMPY_SUPPORTED_DTYPES = [torch.float32, torch.float16] -class RayRpcMigrationBackend(BufferMigrationBackend): +class RayRpcMigrationBackend(MigrationBackendBase): def __init__(self, migration_config: MigrationConfig, cache_engine: CacheEngine, worker_rank, worker_handle_list, \ scheduling_strategy, is_driver_worker, gpu_cache) -> None: + super().__init__() + self.migration_config = migration_config self.cache_engine = cache_engine self.worker_rank = worker_rank self.worker_handle_list = worker_handle_list self.actor = ProxyActor.options(scheduling_strategy=scheduling_strategy).remote() - self.migration_stream = torch.cuda.Stream() self.rpc_dtype = self.cache_engine.dtype if self.cache_engine.dtype in NUMPY_SUPPORTED_DTYPES: @@ -68,10 +65,14 @@ def __init__(self, migration_config: MigrationConfig, cache_engine: CacheEngine, self.num_migration_buffer_blocks = self.migration_config.migration_buffer_blocks self.num_layers = self.cache_engine.num_layers self.migration_cache_size = self.cache_engine.block_size * self.cache_engine.num_heads * self.cache_engine.head_size - buffer_shape = (self.num_migration_buffer_blocks, self.num_layers, 2, self.migration_cache_size) - super().__init__(migration_config.migration_internal_buffer_num, buffer_shape, self.cache_engine.dtype, - self.cache_device, pin_memory=True) + self.dummy_cache = torch.empty( + size=(self.num_migration_buffer_blocks, self.num_layers, 2, self.migration_cache_size), + dtype=self.cache_engine.dtype, + device=self.cache_device, + pin_memory=True + ) + self.migration_stream = torch.cuda.Stream() def init_backend(self, group_name, world_size, rank) -> bool: logger.info("create rpc migration backend successfully.") @@ -99,32 +100,24 @@ def migrate_cache(self, src_handle, src_blocks: List[int], dst_blocks: List[int] ray_obj = self.actor.exec_method.remote(self.is_driver_worker, src_handle, "do_send", None, send_blocks) if rpc_numpy_cache is not None: self.do_recv(rpc_numpy_cache, recv_blocks) - rpc_numpy_cache_ref = ray.get(ray_obj) - rpc_numpy_cache = ray.get(rpc_numpy_cache_ref) + rpc_numpy_cache = ray.get(ray_obj) recv_blocks = dst_blocks[start_idx:start_idx+offset] self.do_recv(rpc_numpy_cache, recv_blocks) def do_send(self, dst_handle, blocks: List[int]): num_blocks = len(blocks) - dummy_cache_idx = self.get_available_cache() - send_cache = self.dummy_buffer[dummy_cache_idx][:num_blocks].view(self.num_layers, 2, num_blocks, self.migration_cache_size) + send_cache = self.dummy_cache[:num_blocks].view(self.num_layers, 2, num_blocks, self.migration_cache_size) src_to_dst = {block_num: idx for idx, block_num in enumerate(blocks)} with torch.cuda.stream(self.migration_stream): for layer_idx in range(self.num_layers): self.cache_engine.attn_backend.swap_blocks(self.gpu_cache[layer_idx], send_cache[layer_idx], src_to_dst) torch.cuda.Stream.synchronize(self.migration_stream) - # Here, we use ray.put to store data and finally return the object reference so that we can release the internal buffer. - # This might seem like an anti-pattern, but it's okay since the kv-cache transferred is in the MB range and won't utilize - # Ray's optimization for returning small objects (<100KB). - data = ray.put(send_cache.to(self.rpc_dtype).numpy()) - self.put_back_cache(dummy_cache_idx) - return data + return send_cache.to(self.rpc_dtype).numpy() def do_recv(self, src_handle, blocks: List[int]): num_blocks = len(blocks) src_to_dst = dict(enumerate(blocks)) - dummy_cache_idx = self.get_available_cache() - recv_cache = self.dummy_buffer[dummy_cache_idx][:num_blocks].view(self.num_layers, 2, num_blocks, self.migration_cache_size) + recv_cache = self.dummy_cache[:num_blocks].view(self.num_layers, 2, num_blocks, self.migration_cache_size) # use pin memory dummy_cache to speed up data transfer recv_cache.copy_(torch.from_numpy(src_handle)) @@ -132,7 +125,6 @@ def do_recv(self, src_handle, blocks: List[int]): for layer_idx in range(self.num_layers): self.cache_engine.attn_backend.swap_blocks(recv_cache[layer_idx], self.gpu_cache[layer_idx], src_to_dst) torch.cuda.Stream.synchronize(self.migration_stream) - self.put_back_cache(dummy_cache_idx) def try_import_gloo(): try: @@ -147,9 +139,14 @@ def try_import_gloo(): except ImportError as e: raise ImportError("Gloo is not installed. Please install it first.") from e -class RayColMigrationBackend(BufferMigrationBackend): +class RayColMigrationBackend(MigrationBackendBase): def __init__(self, migration_config: MigrationConfig, cache_engine: CacheEngine, local_rank, scheduling_strategy, is_driver_worker, gpu_cache) -> None: + super().__init__() + + # pylint: disable=C0415 + import cupy + self.migration_config = migration_config self.cache_engine = cache_engine self.backend = migration_config.migration_backend @@ -165,7 +162,6 @@ def __init__(self, migration_config: MigrationConfig, cache_engine: CacheEngine, self.actor = ProxyActor.options(scheduling_strategy=scheduling_strategy).remote() self.is_driver_worker = is_driver_worker self.gpu_cache = gpu_cache - self.migration_stream = cupy.cuda.Stream() self.migration_cache_size = self.cache_engine.block_size * self.cache_engine.num_heads * self.cache_engine.head_size @@ -173,13 +169,17 @@ def __init__(self, migration_config: MigrationConfig, cache_engine: CacheEngine, try_import_gloo() self.cache_device = "cpu" else: - nccl_util.TORCH_NCCL_DTYPE_MAP[torch.bfloat16] = nccl.NCCL_FLOAT16 self.cache_device = torch.device(f"cuda:{self.local_rank}") pin_memory = (self.backend == 'gloo') - buffer_shape = (self.num_migration_buffer_blocks, self.migration_num_layers, 2, self.migration_cache_size) - super().__init__(migration_config.migration_internal_buffer_num, buffer_shape, self.cache_engine.dtype, - self.cache_device, pin_memory=pin_memory) + self.dummy_cache = torch.empty( + size=(self.num_migration_buffer_blocks, self.migration_num_layers, 2, self.migration_cache_size), + dtype=self.cache_engine.dtype, + device=self.cache_device, + pin_memory=pin_memory + ) + + self.migration_stream = cupy.cuda.Stream() def init_backend(self, group_name, world_size, rank) -> bool: @func_set_timeout(self.migration_config.migration_backend_init_timeout) @@ -224,7 +224,7 @@ def destory_backend(self) -> None: def warmup(self) -> bool: if self.global_world_size > 1: try: - col.allreduce(self.dummy_buffer[0][0], self.group_name) + col.allreduce(self.dummy_cache[0], self.group_name) # pylint: disable=W0703 except Exception as e: logger.info("warmup migration backend failed (group_name: {}, world_size: {}, rank: {}, backbend: {}), err: {}." @@ -250,8 +250,7 @@ def migrate_cache(self, src_handle, src_blocks: List[int], dst_blocks: List[int] def do_send(self, dst_handle, blocks: List[int]): num_blocks = len(blocks) - dummy_cache_idx = self.get_available_cache() - send_cache = self.dummy_buffer[dummy_cache_idx][:num_blocks].view(self.migration_num_layers, 2, num_blocks, self.migration_cache_size) + send_cache = self.dummy_cache[:num_blocks].view(self.migration_num_layers, 2, num_blocks, self.migration_cache_size) src_to_dst = {block_num: idx for idx, block_num in enumerate(blocks)} with self.migration_stream: @@ -262,13 +261,11 @@ def do_send(self, dst_handle, blocks: List[int]): # TODO(KuilongCui): check the error code if peer is dead col.send(send_cache, dst_handle, self.group_name) self.migration_stream.synchronize() - self.put_back_cache(dummy_cache_idx) def do_recv(self, src_handle, blocks: List[int]): num_blocks = len(blocks) src_to_dst = dict(enumerate(blocks)) - dummy_cache_idx = self.get_available_cache() - recv_cache = self.dummy_buffer[dummy_cache_idx][:num_blocks].view(self.migration_num_layers, 2, num_blocks, self.migration_cache_size) + recv_cache = self.dummy_cache[:num_blocks].view(self.migration_num_layers, 2, num_blocks, self.migration_cache_size) with self.migration_stream: for layer_idx in range(self.cache_engine.num_layers): @@ -277,7 +274,6 @@ def do_recv(self, src_handle, blocks: List[int]): col.recv(recv_cache, src_handle, self.group_name) self.cache_engine.attn_backend.swap_blocks(recv_cache[cache_idx], self.gpu_cache[layer_idx], src_to_dst) self.migration_stream.synchronize() - self.put_back_cache(dummy_cache_idx) def get_migration_backend(migration_config: MigrationConfig, cache_engine: CacheEngine, worker_handle_list, scheduling_strategy, is_driver_worker, gpu_cache, worker_rank, local_rank) -> MigrationBackendBase: @@ -288,6 +284,7 @@ def get_migration_backend(migration_config: MigrationConfig, cache_engine: Cache target_migration_backend = None backend = migration_config.migration_backend + assert backend in ['nccl', 'gloo', 'rpc'], "Unsupported migration backend: {} for llumnix".format(backend) if backend in ['nccl', 'gloo']: diff --git a/llumnix/backends/vllm/scheduler.py b/llumnix/backends/vllm/scheduler.py index 4c6403ae..ea0991f7 100644 --- a/llumnix/backends/vllm/scheduler.py +++ b/llumnix/backends/vllm/scheduler.py @@ -140,6 +140,7 @@ def pre_alloc(self, blocks = self.block_manager.get_free_blocks(block_num) pre_blocks = self.pre_alloc_cache_dict.get(request_id, []) pre_blocks.extend(blocks) + logger.info("add request {} to pre_alloc_cache_dict".format(request_id)) self.pre_alloc_cache_dict[request_id] = pre_blocks blocks = [block.block_number for block in blocks] return blocks @@ -170,14 +171,15 @@ def _allocate_and_set_running(self, seq_group: SequenceGroup) -> None: super()._allocate_and_set_running(seq_group) def _set_status(self, - seq_group: SequenceGroup, - status_to: SequenceStatus, - status_from: SequenceStatus = None): + seq_group: SequenceGroup, + status_to: SequenceStatus, + status_from: SequenceStatus = None): for seq in seq_group.get_seqs(status=status_from): seq.status = status_to def free_dst_pre_alloc_cache(self, request_id: str = None) -> None: if request_id: + logger.info("pop request {} from pre_alloc_cache_dict".format(request_id)) blocks = self.pre_alloc_cache_dict.pop(request_id, []) # pylint: disable=protected-access self.block_manager._free_block_table(blocks) @@ -186,6 +188,7 @@ def free_dst_pre_alloc_cache(self, request_id: str = None) -> None: # Clear all pre-allocated cache of dst instance when src instance encounters exception. request_ids = list(self.pre_alloc_cache_dict.keys()) for req_id in request_ids: + logger.info("pop request {} from pre_alloc_cache_dict".format(req_id)) blocks = self.pre_alloc_cache_dict.pop(req_id, []) # pylint: disable=protected-access self.block_manager._free_block_table(blocks) diff --git a/llumnix/backends/vllm/simulator.py b/llumnix/backends/vllm/simulator.py index 94367d75..809c61ce 100644 --- a/llumnix/backends/vllm/simulator.py +++ b/llumnix/backends/vllm/simulator.py @@ -32,7 +32,7 @@ class BackendSimVLLM(BackendVLLM): def __init__( self, instance_id: str, - output_queue_type: QueueType, + request_output_queue_type: QueueType, migration_config: MigrationConfig, profiling_result_file_path: str, engine_args: EngineArgs, @@ -41,7 +41,7 @@ def __init__( # multi-instance args latency_mem = self._get_lantecy_mem(profiling_result_file_path, engine_args) self.engine: LLMEngineLlumnix = LLMEngineLlumnix.from_engine_args(engine_args=engine_args, - output_queue_type=output_queue_type, + request_output_queue_type=request_output_queue_type, migration_config=migration_config, instance_id=instance_id, latency_mem=latency_mem, diff --git a/llumnix/backends/vllm/worker.py b/llumnix/backends/vllm/worker.py index e38c3423..0b2c6fb9 100644 --- a/llumnix/backends/vllm/worker.py +++ b/llumnix/backends/vllm/worker.py @@ -14,6 +14,7 @@ import time from typing import Dict, List import math +import ray import torch from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy, NodeAffinitySchedulingStrategy @@ -50,10 +51,9 @@ def get_global_rank(self): def reserve_memory_for_migration(self, migration_config: MigrationConfig, model_config: ModelConfig, cache_config: CacheConfig, parallel_config: ParallelConfig) -> int: migrate_cache_blocks_size = migration_config.migration_buffer_blocks - migrate_num_layers = migration_config.migration_num_layers - dummy_cache_size = migration_config.migration_internal_buffer_num * migrate_num_layers * migrate_cache_blocks_size \ - * CacheEngine.get_cache_block_size(cache_config, model_config, parallel_config) \ - // model_config.get_num_layers(parallel_config) + migration_num_layers = migration_config.migration_num_layers + dummy_cache_size = migration_num_layers * migrate_cache_blocks_size * CacheEngine.get_cache_block_size( + cache_config, model_config, parallel_config) // model_config.get_num_layers(parallel_config) # For nccl migration backend, reserve gpu memory for dummy cache in migration backend. For other backends, # CPU memory is used for the dummy cache, which is almost unlimited, so no special action is needed. diff --git a/llumnix/config/default.py b/llumnix/config/default.py index 358d9e1b..67de97ee 100644 --- a/llumnix/config/default.py +++ b/llumnix/config/default.py @@ -33,7 +33,7 @@ # Path to SSL certificate file for secure connections _C.SERVER.SSL_CERTFILE = None # Queue type for request output queue -_C.SERVER.QUEUE_TYPE = "rayqueue" +_C.SERVER.REQUEST_OUTPUT_QUEUE_TYPE = "rayqueue" # Port number for the request output queue _C.SERVER.REQUEST_OUTPUT_QUEUE_PORT = 1234 # Disable logging requests in server @@ -111,8 +111,6 @@ _C.MANAGER.MIGRATION_BUFFER_BLOCKS = 512 # Number of kv-cache layers to transfer in each round during migration _C.MANAGER.MIGRATION_NUM_LAYERS = 1 -# Number of internal cache size in migration backend for sending and receiving -_C.MANAGER.MIGRATION_INTERNAL_BUFFER_NUM = 2 # ----------------------------------------------------------------------------- # SCALING CONFIGURATION diff --git a/llumnix/entrypoints/utils.py b/llumnix/entrypoints/utils.py index 7fea885b..496f151d 100644 --- a/llumnix/entrypoints/utils.py +++ b/llumnix/entrypoints/utils.py @@ -28,11 +28,10 @@ from llumnix.arg_utils import EngineManagerArgs from llumnix.queue.queue_type import QueueType from llumnix.server_info import ServerInfo, RequestTimestamps -from llumnix.queue.utils import init_output_queue_server +from llumnix.queue.utils import init_request_output_queue_server logger = init_logger(__name__) -# TODO(s5u13b): Set the values through tests. MAX_RESTARTS = 30 RESTART_INTERVALS = 1 MAX_TASK_RETRIES = 300 @@ -62,7 +61,7 @@ def launch_ray_cluster(port: int) -> subprocess.CompletedProcess: node_ip_address = get_ip_address() try: # Stop the existing ray processes on the node first. - subprocess.run(['ray', 'stop', '--force'], check=True, text=True, capture_output=True) + subprocess.run(['ray', 'stop'], check=True, text=True, capture_output=True) except subprocess.CalledProcessError as e: logger.info("'ray stop' failed with: \n{}".format(e.stderr)) sys.exit(1) @@ -220,7 +219,7 @@ def init_llumnix_components(engine_manager_args: EngineManagerArgs, logger.info("Init Llumnix components done, {} instances are ready, instance_ids: {}." .format(len(available_instance_ids), available_instance_ids)) - request_output_queue = init_output_queue_server(ip, request_output_queue_port, request_output_queue_type) + request_output_queue = init_request_output_queue_server(ip, request_output_queue_port, request_output_queue_type) return engine_manager, available_instance_ids, available_llumlets, request_output_queue @@ -231,12 +230,12 @@ def setup_llumnix(engine_manager_args, engine_args, cfg): init_llumnix_components(engine_manager_args, engine_args, node_id, - cfg.SERVER.QUEUE_TYPE, + cfg.SERVER.REQUEST_OUTPUT_QUEUE_TYPE, ip, cfg.SERVER.REQUEST_OUTPUT_QUEUE_PORT) server_id = random_uuid() server_info = ServerInfo(server_id, - cfg.SERVER.QUEUE_TYPE, + cfg.SERVER.REQUEST_OUTPUT_QUEUE_TYPE, request_output_queue, ip, cfg.SERVER.REQUEST_OUTPUT_QUEUE_PORT) @@ -260,6 +259,7 @@ def setup_llumnix(engine_manager_args, engine_args, cfg): return context +# TODO(s5u13b): Fix the potential output token out-of-order issue caused by the migration. async def _background_process_outputs(llumnix_context): while True: request_outputs = await llumnix_context.request_output_queue.get() diff --git a/llumnix/entrypoints/vllm/api_server.py b/llumnix/entrypoints/vllm/api_server.py index da2c2b10..ca7ead93 100644 --- a/llumnix/entrypoints/vllm/api_server.py +++ b/llumnix/entrypoints/vllm/api_server.py @@ -150,7 +150,7 @@ async def generate_benchmark(request: Request) -> Response: if llumnix_context.log_requests: llumnix_context.num_finished_requests += 1 - logger.info("Finished request {}.".format(request_id)) + logger.info("entrypoints finished request {}.".format(request_id)) logger.info("num_finished_requests {}.".format(llumnix_context.num_finished_requests)) generation = final_output.outputs[0].text diff --git a/llumnix/entrypoints/vllm/utils.py b/llumnix/entrypoints/vllm/utils.py index 64af2dfc..25e2bfb1 100644 --- a/llumnix/entrypoints/vllm/utils.py +++ b/llumnix/entrypoints/vllm/utils.py @@ -91,7 +91,7 @@ async def manager_abort(request_id: str, llumnix_context: LlumnixEntrypointsCont logger.info("abort request: {}.".format(request_id)) await llumnix_context.engine_manager.abort.remote(request_id) except ray.exceptions.RayActorError: - logger.info("Manager is unavailable") + logger.info("manager is unavailable") async def manager_is_ready(llumnix_context: LlumnixEntrypointsContext): ready_status = await llumnix_context.engine_manager.is_ready.remote() diff --git a/llumnix/internal_config.py b/llumnix/internal_config.py index 4412c13b..d17bbd20 100644 --- a/llumnix/internal_config.py +++ b/llumnix/internal_config.py @@ -20,8 +20,7 @@ def __init__( migration_num_layers: int, last_stage_max_blocks: int, max_stages: int, - migration_backend_init_timeout: float, - migration_internal_buffer_num: int) -> None: + migration_backend_init_timeout: float) -> None: self.request_migration_policy = request_migration_policy self.migration_backend = migration_backend self.migration_num_layers = migration_num_layers @@ -29,7 +28,6 @@ def __init__( self.last_stage_max_blocks = last_stage_max_blocks self.max_stages = max_stages self.migration_backend_init_timeout = migration_backend_init_timeout - self.migration_internal_buffer_num = migration_internal_buffer_num class GlobalSchedulerConfig: def __init__( diff --git a/llumnix/llm_engine_manager.py b/llumnix/llm_engine_manager.py index 5d8c48a5..6453745d 100644 --- a/llumnix/llm_engine_manager.py +++ b/llumnix/llm_engine_manager.py @@ -37,10 +37,13 @@ logger = init_logger(__name__) MANAGER_ACTOR_NAME = 'manager' -CLEARING_INTERVAL = 3600 +CLEAR_REQUEST_INSTANCE_INTERVAL = 3600 RETRIES_INTERVALS = 5.0 +WAIT_ALL_MIGRATIONS_DONE_INTERVAL = 1.0 # TODO(s5u13b): Fix the logger when manager failover. + + class LLMEngineManager: def __init__(self, engine_manager_args: EngineManagerArgs, @@ -63,12 +66,8 @@ def __init__(self, self.enable_pd_disagg = global_scheduler_config.enable_pd_disagg - logger.info("LLMEngineManager starts") - logger.info("enable_migration: {}".format(self.enable_migration)) - logger.info("num_instances: {}".format(self.num_instances)) - logger.info("max_instances: {}, min_instances: {}".format(self.max_instances, self.min_instances)) - self.instances: Dict[str, Llumlet] = {} + self.instance_migrating: Dict[str, bool] = {} self.pending_rebuild_migration_instances = 0 self.global_scheduler = GlobalScheduler(global_scheduler_config) @@ -81,14 +80,13 @@ def __init__(self, # request states self.request_instance: Dict[str, str] = {} - self.clearing_interval = CLEARING_INTERVAL - asyncio.create_task(self._clear_request_instance_loop(self.clearing_interval)) + self.clear_request_intance_interval = CLEAR_REQUEST_INSTANCE_INTERVAL + asyncio.create_task(self._clear_request_instance_loop(self.clear_request_intance_interval)) # migrate states self.num_instance_info_updates = 0 - self.num_migrating = 0 + self.migrating = False - # TODO(s5u13b): refactor auto-scaling # auto-scaling states self.scale_up_time = -1 self.scale_down_time = -1 @@ -120,8 +118,8 @@ async def generate( server_info.request_timestamps.manager_generate_timestamp = time.time() await self.instances[instance_id].generate.remote(request_id, server_info, request_expected_steps, *args, **kwargs) if self.log_requests: - logger.info("received request {}.".format(request_id)) - logger.info("dispath to instance {}".format(instance_id)) + logger.info("manager received request {}.".format(request_id)) + logger.info("dispath request {} to instance {}".format(request_id, instance_id)) self.request_instance[request_id] = instance_id except (ray.exceptions.RayActorError, KeyError): logger.info("[generate] instance {} is dead, regenerate request {}".format(instance_id, request_id)) @@ -179,31 +177,26 @@ def update_instance_info_done_callback(instance_id: str, fut): self.global_scheduler.update_instance_infos([ret]) else: dead_instance_ids.append(instance_id) - while True: try: await asyncio.sleep(interval) tasks = [] instance_infos = [] dead_instance_ids = [] - for instance_id, instance in self.instances.items(): # Use asyncio.gather to wrap ray remote call to add done callback. task = asyncio.gather(instance.get_instance_info.remote(), return_exceptions=True) task.add_done_callback(partial(update_instance_info_done_callback, instance_id)) tasks.append(task) await asyncio.gather(*tasks, return_exceptions=True) - if len(dead_instance_ids) > 0: logger.info("[_update_instance_info_loop] dead instances: {}.".format(dead_instance_ids)) self.scale_down(dead_instance_ids) self.num_instance_info_updates += 1 - # Push migrate when the instance_info have updated a certain number of times. if self.enable_migration and self.num_instance_info_updates != 0 \ and self.num_instance_info_updates % self.pair_migration_frequency == 0: asyncio.create_task(self._push_migrations()) - if self.log_instance_info: self._log_instance_infos_to_csv(instance_infos) # pylint: disable=W0703 @@ -217,7 +210,6 @@ async def _clear_request_instance_loop(self, interval: float): while True: await asyncio.sleep(interval) self.request_instance = {} - async def _push_migrations(self) -> None: # Push migrate when the instance_info have updated a certain number of times. if self.enable_pd_disagg: @@ -227,13 +219,17 @@ async def _push_migrations(self) -> None: asyncio.create_task(self._migrate(PairMigrationConstraints.NO_CONSTRAINTS)) async def _migrate(self, pair_migration_type: PairMigrationConstraints) -> None: + # TODO(s5u13b): Remove the migration done callback through decentralized migration refactoring. async def migrate_done_callback(ret, migrate_instance_pair: Tuple[str, str]) -> None: - self.num_migrating -= 1 - # TODO(s5u13b): Add more exception types for failover. + if migrate_instance_pair[0] in self.instance_migrating: + self.instance_migrating[migrate_instance_pair[0]] = False + if migrate_instance_pair[1] in self.instance_migrating: + self.instance_migrating[migrate_instance_pair[1]] = False if isinstance(ret, (ray.exceptions.RayActorError, ray.exceptions.RayTaskError, KeyError)): has_error_pair = await self._check_instance_error(migrate_instance_pair) for i, has_error in enumerate(has_error_pair): # Instance without error should clear migration states. + # TODO(s5u13b): Fix the clear_migration_states to adapt to the many-to-many migration. if not has_error: try: await self.instances[migrate_instance_pair[i]].clear_migration_states.remote(is_migrate_in=bool(i)) @@ -251,7 +247,6 @@ async def migrate_done_callback(ret, migrate_instance_pair: Tuple[str, str]) -> self.request_instance[migrate_out_request_id] = migrate_instance_pair[1] logger.info("{}->{} migrate done, migrate request {}".format( migrate_instance_pair[0], migrate_instance_pair[1], migrate_out_request_ids)) - def migrate_done_callback_wrapper(migrate_instance_pair: Tuple[str, str], fut) -> None: ret = fut.result() loop = asyncio.get_event_loop() @@ -259,19 +254,19 @@ def migrate_done_callback_wrapper(migrate_instance_pair: Tuple[str, str], fut) - try: migrate_instance_pairs = self.global_scheduler.pair_migration(pair_migration_type) - migration_tasks = [] for _, migrate_instance_pair in enumerate(migrate_instance_pairs): - self.num_migrating += 1 migrate_out_instance_id, migrate_in_instance_id = migrate_instance_pair - + if self.instance_migrating[migrate_out_instance_id] or self.instance_migrating[migrate_in_instance_id]: + continue + self.instance_migrating[migrate_out_instance_id] = True + self.instance_migrating[migrate_in_instance_id] = True migrate_in_instance_name = "instance_{}".format(migrate_in_instance_id) # Use asyncio.gather to wrap ray remote call to add done callback. task = asyncio.gather(self.instances[migrate_out_instance_id].migrate_out.remote(migrate_in_instance_name), return_exceptions=True) task.add_done_callback(partial(migrate_done_callback_wrapper, migrate_instance_pair)) migration_tasks.append(task) - # TODO(s5u13b): Migration failover could be implemented in Llumlet rather than manager. await asyncio.gather(*migration_tasks, return_exceptions=True) # pylint: disable=W0703 except Exception as e: @@ -280,8 +275,8 @@ def migrate_done_callback_wrapper(migrate_instance_pair: Tuple[str, str], fut) - async def rebuild_migrate_backend(self) -> None: # Wait for all instances to finish migration - while self.num_migrating > 0: - await asyncio.sleep(0.1) + while any(self.instance_migrating.values()): + await asyncio.sleep(WAIT_ALL_MIGRATIONS_DONE_INTERVAL) # During rebuilding migration backend, disable migrate origin_config = self.enable_migration @@ -353,6 +348,7 @@ def scale_up(self, instance_id: Union[str, Iterable[str]], llumlet_actor_handles if ins_id not in self.instances: indeed_update = True self.instances[ins_id] = llumlet_actor_handles[idx] + self.instance_migrating[ins_id] = False if self.log_instance_info: self.instance_last_logged_empty[ins_id] = False self.pending_rebuild_migration_instances += 1 @@ -381,6 +377,7 @@ def scale_down(self, instance_id: Union[str, Iterable[str]], rebuild_migrate_bac if ins_id in self.instances: indeed_update = True del self.instances[ins_id] + del self.instance_migrating[ins_id] if self.log_instance_info: del self.instance_last_logged_empty[ins_id] self.pending_rebuild_migration_instances += 1 @@ -451,8 +448,7 @@ def from_args(cls, return engine_manager # TODO(s5u13b): Significant duplication with llumlet_utils.init_llumlets. Consider reducing duplicate codes. - # TODO(s5u13b): Fix the logger when enabling init instance by manager. - def init_llumlets(self, engine_args, node_id: str, output_queue_type: QueueType) -> Tuple[List[str], List[Llumlet]]: + def init_llumlets(self, engine_args, node_id: str, request_output_queue_type: QueueType) -> Tuple[List[str], List[Llumlet]]: engine_manager_args = self.engine_manager_args engine_config = engine_args.create_engine_config() parallel_config = engine_config.parallel_config @@ -462,7 +458,7 @@ def init_llumlets(self, engine_args, node_id: str, output_queue_type: QueueType) instance_id = random_uuid() if not engine_manager_args.profiling_result_file_path: llumlet = Llumlet.from_args( - output_queue_type, + request_output_queue_type, engine_manager_args.disable_fixed_node_init_instance, True, node_id, @@ -474,7 +470,7 @@ def init_llumlets(self, engine_args, node_id: str, output_queue_type: QueueType) ) else: llumlet = Llumlet.from_args( - output_queue_type, + request_output_queue_type, engine_manager_args.disable_fixed_node_init_instance, True, node_id, diff --git a/llumnix/llumlet/llumlet.py b/llumnix/llumlet/llumlet.py index 3af73ac5..63dd23e7 100644 --- a/llumnix/llumlet/llumlet.py +++ b/llumnix/llumlet/llumlet.py @@ -31,11 +31,13 @@ logger = init_logger(__name__) +CHECK_ENGINE_STATE_INTERVAL = 1.0 + class Llumlet: def __init__(self, instance_id: str, - output_queue_type: QueueType, + request_output_queue_type: QueueType, backend_type: BackendType, migration_config: MigrationConfig, *args, @@ -44,7 +46,7 @@ def __init__(self, self.instance_id = instance_id self.actor_name = f"instance_{instance_id}" self.backend_engine: BackendInterface = init_backend_engine(self.instance_id, - output_queue_type, + request_output_queue_type, backend_type, migration_config, *args, @@ -56,7 +58,7 @@ def __init__(self, self.backend_engine) self.log_requests = True - asyncio.create_task(self._check_state_loop()) + asyncio.create_task(self._check_engine_state_loop()) # pylint: disable=broad-except except Exception as e: logger.error("Failed to initialize llumlet: {}".format(e)) @@ -64,7 +66,7 @@ def __init__(self, @classmethod def from_args(cls, - output_queue_type: QueueType, + request_output_queue_type: QueueType, disable_fixed_node_init_instance: bool, detached: bool, node_id: str, @@ -116,12 +118,12 @@ def from_args(cls, soft=False, ) ) - llumlet = engine_class.remote(instance_id, output_queue_type, backend_type, migration_config, *args, **kwargs) + llumlet = engine_class.remote(instance_id, request_output_queue_type, backend_type, migration_config, *args, **kwargs) return llumlet - async def _check_state_loop(self): + async def _check_engine_state_loop(self): while True: - await asyncio.sleep(1) + await asyncio.sleep(CHECK_ENGINE_STATE_INTERVAL) if self.backend_engine.state == EngineState.CRASHED: logger.warning("llumlet ({}) detected backend engine crashed. Stopping...".format(self.instance_id)) # pylint: disable=protected-access @@ -133,6 +135,10 @@ async def migrate_out(self, dst_instance_name: str) -> List[str]: migrate_out_requests = self.migration_scheduler.get_migrate_out_requests() if len(migrate_out_requests) == 0: return [] + + for migrate_out_request in migrate_out_requests: + migrate_out_request.is_migrating = True + migrated_request_list = [] for migrate_out_request in migrate_out_requests: migrated_request = await self._migrate_out_one_request(migrate_out_request, dst_instance_name) @@ -148,12 +154,16 @@ async def _migrate_out_one_request(self, migrate_out_request: LlumnixRequest, ds dst_instance_id = dst_instance_name[len("instance_"):] logger.info("{}->{} begin migrate out".format(self.instance_id, dst_instance_id)) migrated_request = [] + if migrate_out_request.status == RequestStatus.RUNNING: + migrate_out_request.migration_start_time = time.time() status = await self.migration_coordinator.migrate_out_running_request(migrate_in_ray_actor, migrate_out_request) elif migrate_out_request.status == RequestStatus.WAITING: + migrate_out_request.migration_start_time = time.time() status = await self.migration_coordinator.migrate_out_waiting_request(migrate_in_ray_actor, migrate_out_request) else: return migrated_request + if status == MigrationStatus.FINISHED: await migrate_in_ray_actor.execute_engine_method.remote("commit_dst_request", migrate_out_request) self.backend_engine.free_src_request(migrate_out_request) diff --git a/llumnix/llumlet/local_migration_scheduler.py b/llumnix/llumlet/local_migration_scheduler.py index 4f30f850..b3aee50d 100644 --- a/llumnix/llumlet/local_migration_scheduler.py +++ b/llumnix/llumlet/local_migration_scheduler.py @@ -58,6 +58,7 @@ def _filter_running_queue(self, running, min_request_len, max_request_len): if request.status == RequestStatus.RUNNING \ and request.inference_type == RequestInferenceType.DECODE \ and min_request_len < request.request_len < max_request_len \ + and (not request.is_migrating) \ ] return filtered_running @@ -67,6 +68,7 @@ def _filter_waiting_queue(self, waiting, min_request_len, max_request_len): if request.status == RequestStatus.WAITING \ and request.try_schedule_times >= 1 \ and min_request_len < request.request_len < max_request_len \ + and (not request.is_migrating) \ ] return filtered_waiting diff --git a/llumnix/llumlet/migration_coordinator.py b/llumnix/llumlet/migration_coordinator.py index 224c41c3..bc356f48 100644 --- a/llumnix/llumlet/migration_coordinator.py +++ b/llumnix/llumlet/migration_coordinator.py @@ -51,28 +51,38 @@ def __init__(self, async def migrate_out_running_request(self, migrate_in_ray_actor: "ray.actor.ActorHandle", migrate_out_request: LlumnixRequest) -> "MigrationStatus": - return await self._migrate_out_multistage(migrate_in_ray_actor, migrate_out_request) + try: + return await self._migrate_out_multistage(migrate_in_ray_actor, migrate_out_request) + except Exception as e: + logger.error("unexpected exception occurs: {}".format(e)) + logger.error("exception traceback: {}".format(traceback.format_exc())) + raise async def migrate_out_waiting_request(self, migrate_in_ray_actor: "ray.actor.ActorHandle", migrate_out_request: LlumnixRequest) -> "MigrationStatus": """one-stage migration for a waiting request """ - found = self.backend_engine.remove_waiting_request(migrate_out_request.request_id) - if not found: - return MigrationStatus.ABORTED_SRC - self.backend_engine.add_migrating_out_request_last_stage(migrate_out_request) - dst_blocks = await migrate_in_ray_actor.execute_migration_method \ - .remote("migrate_in_pre_alloc", migrate_out_request.request_id, - migrate_out_request.status, - migrate_out_request.arrival_time, - migrate_out_request.prefill_num_blocks) - if len(dst_blocks) != migrate_out_request.prefill_num_blocks: - self.backend_engine.add_waiting_request(migrate_out_request) - self.backend_engine.remove_migrating_out_request_last_stage(migrate_out_request) - return MigrationStatus.ABORTED_DST - - return MigrationStatus.FINISHED + try: + found = self.backend_engine.remove_waiting_request(migrate_out_request.request_id) + if not found: + return MigrationStatus.ABORTED_SRC + self.backend_engine.add_migrating_out_request_last_stage(migrate_out_request) + dst_blocks = await migrate_in_ray_actor.execute_migration_method \ + .remote("migrate_in_pre_alloc", migrate_out_request.request_id, + migrate_out_request.status, + migrate_out_request.arrival_time, + migrate_out_request.prefill_num_blocks) + if len(dst_blocks) != migrate_out_request.prefill_num_blocks: + self.backend_engine.add_waiting_request(migrate_out_request) + self.backend_engine.remove_migrating_out_request_last_stage(migrate_out_request) + return MigrationStatus.ABORTED_DST + + return MigrationStatus.FINISHED + except Exception as e: + logger.error("unexpected exception occurs: {}".format(e)) + logger.error("exception traceback: {}".format(traceback.format_exc())) + raise async def _migrate_out_multistage(self, migrate_in_ray_actor: "ray.actor.ActorHandle", @@ -81,65 +91,82 @@ async def _migrate_out_multistage(self, Args: migrate_in_ray_actor: instance actor name, used to get ray actor handle """ - stage_count = 0 - while stage_count < self.max_stages: - stage_count += 1 - status = await self._migrate_out_onestage(migrate_in_ray_actor, migrate_out_request) - if MigrationStatus.is_finished(status): - return status - # exceed max stages - return MigrationStatus.ABORTED_SRC + try: + stage_count = 0 + while stage_count < self.max_stages: + stage_count += 1 + status = await self._migrate_out_onestage(migrate_in_ray_actor, migrate_out_request) + if MigrationStatus.is_finished(status): + return status + # exceed max stages + return MigrationStatus.ABORTED_SRC + except Exception as e: + logger.error("unexpected exception occurs: {}".format(e)) + logger.error("exception traceback: {}".format(traceback.format_exc())) + raise async def _migrate_out_onestage(self, migrate_in_ray_actor: "ray.actor.ActorHandle", migrate_out_request: LlumnixRequest) -> "MigrationStatus": """one-stage live migration until last stage for a running request """ - pre_stage_num_blocks = sum(migrate_out_request.stage_num_blocks_list) - incremental_blocks = self.backend_engine.get_request_incremental_blocks(migrate_out_request, pre_stage_num_blocks) - # live migration, transfer all blocks except last one(currently updating) - is_last_stage = (len(incremental_blocks) <= self.last_stage_max_blocks) or migrate_out_request.blocking_migration - if not is_last_stage: - migration_status = MigrationStatus.RUNNING - src_blocks = incremental_blocks[:-1] - stage_block_num = len(incremental_blocks) - 1 - dst_blocks = await migrate_in_ray_actor.execute_migration_method \ - .remote("migrate_in_pre_alloc", migrate_out_request.request_id, - migrate_out_request.status, - migrate_out_request.arrival_time, - stage_block_num) - else: - # last stage migration, stop inference, transfer all blocks - migration_status = MigrationStatus.FINISHED - found = self.backend_engine.remove_running_request(migrate_out_request.request_id) - if not found: + try: + if migrate_out_request.should_abort_migration(): return MigrationStatus.ABORTED_SRC - self.backend_engine.add_migrating_out_request_last_stage(migrate_out_request) - src_blocks = incremental_blocks[:] - stage_block_num = len(incremental_blocks) - dst_blocks = await migrate_in_ray_actor.execute_migration_method \ - .remote("migrate_in_pre_alloc", migrate_out_request.request_id, - migrate_out_request.status, - migrate_out_request.arrival_time, - stage_block_num) - if len(dst_blocks) != len(src_blocks): - # migrate-in instance failed to pre alloc - if is_last_stage: - self.backend_engine.add_running_request(migrate_out_request) - self.backend_engine.remove_migrating_out_request_last_stage(migrate_out_request) - return MigrationStatus.ABORTED_DST - - # do stage send/recv - migrate_out_request.stage_timestamps.append(time.time()) - migrate_out_request.stage_num_blocks_list.append(stage_block_num) - # TODO(ZeldaHuang): send_blocks in migrate_in_pre_alloc/migrate_in_last_stage - await self.backend_engine.send_blocks(migrate_in_ray_actor, src_blocks, dst_blocks) - if not is_last_stage and migrate_out_request.should_abort_migration(): - # migrate-out request abort by scheduler during send/recv - return MigrationStatus.ABORTED_SRC + pre_stage_num_blocks = sum(migrate_out_request.stage_num_blocks_list) + incremental_blocks = self.backend_engine.get_request_incremental_blocks(migrate_out_request, pre_stage_num_blocks) + # live migration, transfer all blocks except last one(currently updating) + is_last_stage = (len(incremental_blocks) <= self.last_stage_max_blocks) or migrate_out_request.blocking_migration + if not is_last_stage: + migration_status = MigrationStatus.RUNNING + src_blocks = incremental_blocks[:-1] + stage_block_num = len(incremental_blocks) - 1 + dst_blocks = await migrate_in_ray_actor.execute_migration_method \ + .remote("migrate_in_pre_alloc", migrate_out_request.request_id, + migrate_out_request.status, + migrate_out_request.arrival_time, + stage_block_num) + else: + # last stage migration, stop inference, transfer all blocks + migration_status = MigrationStatus.FINISHED + found = self.backend_engine.remove_running_request(migrate_out_request.request_id) + if not found: + return MigrationStatus.ABORTED_SRC + self.backend_engine.add_migrating_out_request_last_stage(migrate_out_request) + src_blocks = incremental_blocks[:] + stage_block_num = len(incremental_blocks) + dst_blocks = await migrate_in_ray_actor.execute_migration_method \ + .remote("migrate_in_pre_alloc", migrate_out_request.request_id, + migrate_out_request.status, + migrate_out_request.arrival_time, + stage_block_num) + + if len(dst_blocks) != len(src_blocks): + # migrate-in instance failed to pre alloc + if is_last_stage: + self.backend_engine.add_running_request(migrate_out_request) + self.backend_engine.remove_migrating_out_request_last_stage(migrate_out_request) + return MigrationStatus.ABORTED_DST + + if migrate_out_request.should_abort_migration(): + return MigrationStatus.ABORTED_SRC + + # do stage send/recv + migrate_out_request.stage_timestamps.append(time.time()) + migrate_out_request.stage_num_blocks_list.append(stage_block_num) + # TODO(ZeldaHuang): send_blocks in migrate_in_pre_alloc/migrate_in_last_stage + await self.backend_engine.send_blocks(migrate_in_ray_actor, src_blocks, dst_blocks) + + if not is_last_stage and migrate_out_request.should_abort_migration(): + # migrate-out request abort by scheduler during send/recv + return MigrationStatus.ABORTED_SRC - return migration_status + return migration_status + except Exception as e: + logger.error("unexpected exception occurs: {}".format(e)) + logger.error("exception traceback: {}".format(traceback.format_exc())) + raise def migrate_in_pre_alloc(self, request_id: str, diff --git a/llumnix/llumlet/request.py b/llumnix/llumlet/request.py index d92e6564..d6c7dac5 100644 --- a/llumnix/llumlet/request.py +++ b/llumnix/llumlet/request.py @@ -41,6 +41,8 @@ def __init__(self, request_id: int, server_info: ServerInfo, expected_steps: int self.stage_num_blocks_list = [] self.try_schedule_times = 0 self._status = None + self.migration_start_time = None + self.is_migrating = False # end-of-migration, for multiple requests migration self.eom = False @@ -53,11 +55,15 @@ def reset_migration_args_dst(self): self.stage_timestamps = [] self.stage_num_blocks_list = [] self.try_schedule_times = 0 + self.migration_start_time = None + self.is_migrating = False def reset_migration_args_src(self): self.last_preemption_time = None self.stage_timestamps = [] self.stage_num_blocks_list = [] + self.migration_start_time = None + self.is_migrating = False def reset_status(self): self._status = None @@ -104,5 +110,7 @@ def blocking_migration(self) -> bool: return self.output_len >= self.expected_steps def should_abort_migration(self) -> bool: - return self.finished \ - or (self.last_preemption_time is not None and self.last_preemption_time > self.stage_timestamps[-1]) + begin_time = self.stage_timestamps[-1] if len(self.stage_timestamps) > 0 else self.migration_start_time + preempted = self.last_preemption_time is not None and self.last_preemption_time > begin_time + + return self.finished or preempted diff --git a/llumnix/logger.py b/llumnix/logger.py index b7cbbad7..55e762c2 100644 --- a/llumnix/logger.py +++ b/llumnix/logger.py @@ -25,7 +25,6 @@ pass _FORMAT = "Llumnix %(levelname)s %(asctime)s %(filename)s:%(lineno)d] %(message)s" -_DATE_FORMAT = "%m-%d %H:%M:%S" class NewLineFormatter(logging.Formatter): @@ -54,7 +53,7 @@ def _setup_logger(): _default_handler.flush = sys.stdout.flush # type: ignore _default_handler.setLevel(logging.INFO) _root_logger.addHandler(_default_handler) - fmt = NewLineFormatter(_FORMAT, datefmt=_DATE_FORMAT) + fmt = NewLineFormatter(_FORMAT) _default_handler.setFormatter(fmt) # Setting this will avoid the message # being propagated to the parent logger. diff --git a/llumnix/queue/utils.py b/llumnix/queue/utils.py index 84e270fa..35472c76 100644 --- a/llumnix/queue/utils.py +++ b/llumnix/queue/utils.py @@ -20,7 +20,8 @@ from llumnix.queue.zmq_utils import get_open_zmq_ipc_path from llumnix.queue.queue_type import QueueType -def init_output_queue_server(zmq_ip: str, zmq_port: int, queue_type: QueueType) -> QueueServerBase: + +def init_request_output_queue_server(zmq_ip: str, zmq_port: int, queue_type: QueueType) -> QueueServerBase: output_queue_server: QueueServerBase = None if queue_type == QueueType.ZMQ: rpc_path = get_open_zmq_ipc_path(zmq_ip, zmq_port) @@ -29,7 +30,7 @@ def init_output_queue_server(zmq_ip: str, zmq_port: int, queue_type: QueueType) output_queue_server = RayQueueServer() return output_queue_server -def init_output_queue_client(queue_type: QueueType) -> QueueClientBase: +def init_request_output_queue_client(queue_type: QueueType) -> QueueClientBase: output_queue_client: QueueClientBase = None if queue_type == QueueType.ZMQ: output_queue_client= ZmqClient() diff --git a/llumnix/server_info.py b/llumnix/server_info.py index 07162bce..d16b5cf4 100644 --- a/llumnix/server_info.py +++ b/llumnix/server_info.py @@ -67,14 +67,14 @@ def generate_benchmark_return_output_latency(self): class ServerInfo: def __init__(self, server_id: str, - output_queue_type: QueueType, + request_output_queue_type: QueueType, request_output_queue: RayQueueServer, request_output_queue_ip: str, request_output_queue_port: int) -> None: self.server_id = server_id - self.output_queue_type = output_queue_type - if output_queue_type == QueueType.RAYQUEUE: + self.request_output_queue_type = request_output_queue_type + if request_output_queue_type == QueueType.RAYQUEUE: assert request_output_queue is not None - self.request_output_queue = request_output_queue.queue if output_queue_type == QueueType.RAYQUEUE else None + self.request_output_queue = request_output_queue.queue if request_output_queue_type == QueueType.RAYQUEUE else None self.request_output_queue_ip = request_output_queue_ip self.request_output_queue_port = request_output_queue_port diff --git a/tests/conftest.py b/tests/conftest.py index ba3b467c..65a1ae9b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,34 +11,75 @@ # See the License for the specific language governing permissions and # limitations under the License. +from datetime import datetime +import shutil +import os import subprocess import ray import pytest +from llumnix.utils import random_uuid + + def pytest_sessionstart(session): - subprocess.run(["ray", "stop"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - subprocess.run(["ray", "start", "--head", "--disable-usage-stats", "--port=6379"], check=True, + subprocess.run(["ray", "stop"], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.run(["ray", "start", "--head", "--disable-usage-stats", "--port=6379"], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) def pytest_sessionfinish(session, exitstatus): - subprocess.run(["ray", "stop"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.run(["ray", "stop"], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) @pytest.fixture def setup_ray_env(): ray.init(namespace="llumnix", ignore_reinit_error=True) yield - named_actors = ray.util.list_named_actors(True) - for actor in named_actors: - try: - actor_handle = ray.get_actor(actor['name'], namespace=actor['namespace']) - # pylint: disable=bare-except - except: - continue - - try: - ray.kill(actor_handle) - # pylint: disable=bare-except - except: - continue - # Should to be placed after killing actors, otherwise it may occur some unexpected errors when re-init ray. - ray.shutdown() + try: + named_actors = ray.util.list_named_actors(True) + for actor in named_actors: + try: + actor_handle = ray.get_actor(actor['name'], namespace=actor['namespace']) + # pylint: disable=bare-except + except: + continue + + try: + ray.kill(actor_handle) + # pylint: disable=bare-except + except: + continue + # Should to be placed after killing actors, otherwise it may occur some unexpected errors when re-init ray. + ray.shutdown() + # pylint: disable=bare-except + except: + pass + +def backup_error_log(func_name): + curr_time = datetime.now().strftime('%Y_%m_%d_%H_%M_%S') + dst_dir = os.path.expanduser(f'/mnt/error_log/{curr_time}_{random_uuid()}') + os.makedirs(dst_dir, exist_ok=True) + + src_dir = os.getcwd() + + for filename in os.listdir(src_dir): + if filename.startswith("instance_"): + src_file = os.path.join(src_dir, filename) + shutil.copy(src_file, dst_dir) + + elif filename.startswith("bench_"): + src_file = os.path.join(src_dir, filename) + shutil.copy(src_file, dst_dir) + + file_path = os.path.join(dst_dir, 'test.info') + with open(file_path, 'w', encoding='utf-8') as file: + file.write(f'{func_name}') + + print(f"Backup error instance log to directory {dst_dir}") + +@pytest.hookimpl(tryfirst=True, hookwrapper=True) +def pytest_runtest_makereport(item, call): + outcome = yield + report = outcome.get_result() + + if report.when == "call" and report.failed: + func_name = item.name + backup_error_log(func_name) diff --git a/tests/e2e_test/test_bench.py b/tests/e2e_test/test_bench.py index 5eba27d1..d9dc1fae 100644 --- a/tests/e2e_test/test_bench.py +++ b/tests/e2e_test/test_bench.py @@ -11,50 +11,21 @@ # See the License for the specific language governing permissions and # limitations under the License. +import subprocess +from concurrent.futures import ThreadPoolExecutor, as_completed import asyncio import json import os -import subprocess import pytest import torch import numpy as np -from .test_e2e import generate_launch_command, clear_ray_state # pylint: disable=unused-import -from .utils import to_markdown_table, setup_ray_env - -def launch_llumnix_service(command): - subprocess.run(command, shell=True, check=True) - -def generate_bench_command(ip_ports: str, model: str, num_prompts: int, dataset_type: str, dataset_path: str, - qps: int, results_filename: str = "", query_distribution: str = "poisson", - coefficient_variation: float = 1.0, priority_ratio: float = 0.0): - command = ( - f"python -u ./benchmark/benchmark_serving.py " - f"--ip_ports {ip_ports} " - f"--backend vLLM " - f"--tokenizer {model} " - f"--trust_remote_code " - f"--log_filename bench_{ip_ports.split(':')[1]} " - f"--random_prompt_count {num_prompts} " - f"--dataset_type {dataset_type} " - f"--dataset_path {dataset_path} " - f"--qps {qps} " - f"--distribution {query_distribution} " - f"--coefficient_variation {coefficient_variation} " - f"--priority_ratio {priority_ratio} " - f"--log_latencies " - f"--fail_on_response_failure " - f"{'> bench_'+results_filename if len(results_filename)> 0 else ''}" - ) - return command - -def shutdown_llumnix_service(): - try: - subprocess.run('pkill -f llumnix.entrypoints.vllm.api_server', shell=True, check=True) - # pylint: disable=broad-except - except Exception: - pass +from .utils import (generate_launch_command, generate_bench_command, to_markdown_table, + cleanup_ray_env, wait_for_llumnix_service_ready, shutdown_llumnix_service) + +BENCH_TEST_TIMEOUT_MINS = 30 + def parse_log_file(): json_files = [f for f in os.listdir('.') if f.endswith('_latency_info.json')] @@ -91,35 +62,57 @@ def get_markdown_data(key: str, head_name: str): @pytest.mark.asyncio @pytest.mark.skipif(torch.cuda.device_count() < 1, reason="at least 1 gpus required for simple benchmark") @pytest.mark.parametrize("model", ['/mnt/model/Qwen-7B']) -async def test_simple_benchmark(setup_ray_env, model): +async def test_simple_benchmark(cleanup_ray_env, shutdown_llumnix_service, model): device_count = torch.cuda.device_count() + ip = "127.0.0.1" base_port = 37037 + ip_ports = [] for i in range(device_count): + port = base_port+i + ip_port = f"{ip}:{port}" + ip_ports.append(ip_port) launch_command = generate_launch_command(result_filename=str(base_port+i)+".out", - launch_ray_cluster=False, port=base_port+i, model=model) + launch_ray_cluster=False, + ip=ip, + port=port, + model=model) subprocess.run(launch_command, shell=True, check=True) - await asyncio.sleep(60) + wait_for_llumnix_service_ready(ip_ports) - async def run_bench_command(command): - process = await asyncio.create_subprocess_shell(command) - await process.wait() - assert process.returncode == 0 + def run_bench_command(command): + # pylint: disable=consider-using-with + process = subprocess.Popen(command, shell=True) + return process tasks = [] for i in range(device_count): - bench_command = generate_bench_command(ip_ports=f"127.0.0.1:{base_port+i}", model=model, num_prompts=300, - dataset_type="sharegpt", - dataset_path="/mnt/dataset/sharegpt_gpt4/sharegpt_gpt4.jsonl" , - qps=2, - results_filename=f"{base_port+i}.out") - tasks.append(run_bench_command(bench_command)) - - await asyncio.wait(tasks, timeout=60*30) + bench_command = generate_bench_command( + ip_ports=f"127.0.0.1:{base_port + i}", + model=model, + num_prompts=200, + dataset_type="sharegpt", + dataset_path="/mnt/dataset/sharegpt_gpt4/sharegpt_gpt4.jsonl", + qps=5, + results_filename=f"{base_port + i}.out" + ) + tasks.append(bench_command) + + with ThreadPoolExecutor() as executor: + future_to_command = {executor.submit(run_bench_command, command): command for command in tasks} + + for future in as_completed(future_to_command): + try: + process = future.result() + process.wait(timeout=60*BENCH_TEST_TIMEOUT_MINS) + + assert process.returncode == 0, "bench_test failed with return code {}.".format(process.returncode) + # pylint: disable=broad-except + except subprocess.TimeoutExpired: + process.kill() + assert False, "bench_test timed out after {} minutes.".format(BENCH_TEST_TIMEOUT_MINS) with open("performance.txt", "w", encoding="utf-8") as f: f.write(parse_log_file()) - shutdown_llumnix_service() - clear_ray_state() await asyncio.sleep(3) diff --git a/tests/e2e_test/test_e2e.py b/tests/e2e_test/test_e2e.py index a3bf1977..24dd896e 100644 --- a/tests/e2e_test/test_e2e.py +++ b/tests/e2e_test/test_e2e.py @@ -19,87 +19,11 @@ import torch from vllm import LLM, SamplingParams + # pylint: disable=unused-import -from .utils import setup_ray_env - -def parse_launch_mode(launch_mode: str): - # 'eief' means that enable init instance by manager and enable fixed node init instance, and so on. - if launch_mode == 'eief': - disable_init_instance_by_manager = False - disable_fixed_node_init_instance = False - elif launch_mode == 'eidf': - disable_init_instance_by_manager = False - disable_fixed_node_init_instance = True - elif launch_mode == 'dief': - disable_init_instance_by_manager = True - disable_fixed_node_init_instance = False - else: - disable_init_instance_by_manager = True - disable_fixed_node_init_instance = True - return disable_init_instance_by_manager, disable_fixed_node_init_instance - -def generate_launch_command(result_filename: str = "", launch_ray_cluster: bool = True, HEAD_NODE_IP: str = "127.0.0.1", - ip: str = "127.0.0.1", port: int = 37000, instances_num = 1, dispatch_policy: str = "load", - migration_backend = "gloo", model = "facebook/opt-125m", max_model_len: int = 2048, - launch_mode: str = 'eief', log_instance_info: bool = False, - request_migration_policy: str = 'SR'): - disable_init_instance_by_manager, disable_fixed_node_init_instance = parse_launch_mode(launch_mode) - command = ( - f"RAY_DEDUP_LOGS=0 HEAD_NODE_IP={HEAD_NODE_IP} HEAD_NODE=1 " - f"nohup python -u -m llumnix.entrypoints.vllm.api_server " - f"--host {ip} " - f"--port {port} " - f"{'--disable-init-instance-by-manager ' if disable_init_instance_by_manager else ''}" - f"{'--disable-fixed-node-init-instance ' if disable_fixed_node_init_instance else ''}" - f"--initial-instances {instances_num} " - f"{'--log-filename manager ' if log_instance_info else ''}" - f"{'--log-instance-info ' if log_instance_info else ''}" - f"--enable-migration " - f"--model {model} " - f"--engine-use-ray " - f"--worker-use-ray " - f"--max-model-len {max_model_len} " - f"--dispatch-policy {dispatch_policy} " - f"--trust-remote-code " - f"--request-migration-policy {request_migration_policy} " - f"--migration-backend {migration_backend} " - f"--migration-buffer-blocks 32 " - f"--migration-internal-buffer-num 2 " - f"--tensor-parallel-size 1 " - f"--request-output-queue-port {1234+port} " - f"{'--launch-ray-cluster ' if launch_ray_cluster else ''}" - f"{'> instance_'+result_filename if len(result_filename)> 0 else ''} 2>&1 &" - ) - return command - -def launch_llumnix_service(model: str, max_model_len: int, port: int, migration_backend: str, launch_mode: str): - command = generate_launch_command(model=model, max_model_len=max_model_len, - port=port, migration_backend=migration_backend, - launch_mode=launch_mode) - subprocess.run(command, shell=True, check=True) - -def shutdown_llumnix_service(): - try: - subprocess.run('pkill -f llumnix.entrypoints.vllm.api_server', shell=True, check=True) - # pylint: disable=broad-except - except Exception: - pass - -def clear_ray_state(): - named_actors = ray.util.list_named_actors(True) - for actor in named_actors: - try: - actor_handle = ray.get_actor(actor['name'], namespace=actor['namespace']) - # pylint: disable=bare-except - except: - continue - - try: - ray.kill(actor_handle) - # pylint: disable=bare-except - except: - continue - ray.shutdown() +from .utils import (generate_launch_command, wait_for_llumnix_service_ready, + cleanup_ray_env, shutdown_llumnix_service) + async def get_llumnix_response(prompt, sampling_params, ip_ports): timeout = aiohttp.ClientTimeout(total=60) @@ -140,7 +64,7 @@ def run_vllm(model, max_model_len, sampling_params): @pytest.mark.parametrize("model", ['/mnt/model/Qwen-7B']) @pytest.mark.parametrize("migration_backend", ['rpc', 'gloo']) @pytest.mark.parametrize("launch_mode", ['eief', 'eidf', 'dief', 'didf']) -async def test_e2e(setup_ray_env, model, migration_backend, launch_mode): +async def test_e2e(cleanup_ray_env, shutdown_llumnix_service, model, migration_backend, launch_mode): if migration_backend == 'gloo' and launch_mode != 'eief': pytest.skip("When the migration backend is gloo, the launch mode of llumnix can only be eief") max_model_len = 370 @@ -153,25 +77,32 @@ async def test_e2e(setup_ray_env, model, migration_backend, launch_mode): "ignore_eos": False, } + global vllm_output + + if len(vllm_output) == 0: + vllm_output = ray.get(run_vllm.remote(model, max_model_len, sampling_params)) + + await asyncio.sleep(5) + # generate llumnix outputs + ip = "127.0.0.1" base_port = 37037 - launch_llumnix_service(model, max_model_len, base_port, migration_backend, launch_mode) - await asyncio.sleep(60) + launch_command = generate_launch_command(model=model, + max_model_len=max_model_len, + ip=ip, + port=base_port, + migration_backend=migration_backend, + launch_mode=launch_mode) + subprocess.run(launch_command, shell=True, check=True) + + wait_for_llumnix_service_ready(ip_ports=[f"{ip}:{base_port}"]) llumnix_output = {} for prompt in prompts: - response = await asyncio.wait_for(get_llumnix_response(prompt, sampling_params, f"127.0.0.1:{base_port}"), + response = await asyncio.wait_for(get_llumnix_response(prompt, sampling_params, f"{ip}:{base_port}"), timeout=60*5) llumnix_output[prompt] = response['text'][0] - shutdown_llumnix_service() - - global vllm_output - - if len(vllm_output) == 0: - vllm_output = ray.get(run_vllm.remote(model, max_model_len, sampling_params)) - - clear_ray_state() # compare for prompt in prompts: assert llumnix_output[prompt] == vllm_output[prompt] diff --git a/tests/e2e_test/test_migration.py b/tests/e2e_test/test_migration.py index ced1e0be..700b8da3 100644 --- a/tests/e2e_test/test_migration.py +++ b/tests/e2e_test/test_migration.py @@ -11,22 +11,24 @@ # See the License for the specific language governing permissions and # limitations under the License. +import subprocess +from concurrent.futures import ThreadPoolExecutor, as_completed import asyncio from collections import defaultdict import re -import subprocess import pytest import torch -import pandas as pd +import ray -from .test_e2e import generate_launch_command -from .test_bench import generate_bench_command, clear_ray_state, shutdown_llumnix_service # pylint: disable=unused-import -from .utils import to_markdown_table, setup_ray_env +from .utils import (generate_launch_command, generate_bench_command, to_markdown_table, + cleanup_ray_env, wait_for_llumnix_service_ready, shutdown_llumnix_service) size_pattern = re.compile(r'total_kv_cache_size:\s*([\d.]+)\s*(B|KB|MB|GB|KB|TB)') speed_pattern = re.compile(r'speed:\s*([\d.]+)GB/s') +MIGRATION_BENCH_TIMEOUT_MINS = 30 + def parse_instance_log_file(log_files): speed_dict = defaultdict(list) @@ -49,68 +51,113 @@ def parse_instance_log_file(log_files): speeds.sort() trimmed_speeds = speeds[1:-1] - average_speed[transfer_size] = sum(trimmed_speeds) / len(trimmed_speeds) + + if len(trimmed_speeds) > 0: + average_speed[transfer_size] = sum(trimmed_speeds) / len(trimmed_speeds) assert len(average_speed) > 0, "Migration should have occurred, but it was not detected. " return average_speed -def parse_manager_log_file(log_file): - df = pd.read_csv(log_file) - instance_id_set = set(df["instance_id"]) - for instance_id in instance_id_set: - df_instance = df[df["instance_id"] == instance_id] - num_available_gpu_blocks_list = df_instance["num_available_gpu_blocks"].to_numpy().tolist() - assert num_available_gpu_blocks_list[0] == num_available_gpu_blocks_list[-1] +def wait_for_all_instances_finished(): + named_actors = ray.util.list_named_actors(True) + instance_actor_handles = [] + for actor in named_actors: + if actor['name'].startswith("instance"): + instance_actor_handles.append(ray.get_actor(actor['name'], namespace=actor['namespace'])) + while True: + all_finished = True + for instance in instance_actor_handles: + all_request_ids = ray.get(instance.execute_engine_method.remote("get_all_request_ids")) + if len(all_request_ids) != 0: + all_finished = False + if all_finished: + break + +def get_instance_num_blocks(): + named_actors = ray.util.list_named_actors(True) + instance_actor_handles = [] + for actor in named_actors: + if actor['name'].startswith("instance"): + instance_actor_handles.append(ray.get_actor(actor['name'], namespace=actor['namespace'])) + instance_num_blocks_list = [] + for instance in instance_actor_handles: + instance_info = ray.get(instance.get_instance_info.remote()) + instance_num_blocks_list.append(instance_info.num_available_gpu_blocks) + + return instance_num_blocks_list @pytest.mark.asyncio @pytest.mark.skipif(torch.cuda.device_count() < 2, reason="at least 2 gpus required for migration bench") @pytest.mark.parametrize("model", ['/mnt/model/Qwen-7B']) @pytest.mark.parametrize("migration_backend", ['rpc', 'gloo']) @pytest.mark.parametrize("migrated_request_status", ['running', 'waiting']) -async def test_migration_benchmark(model, migration_backend, migrated_request_status): +async def test_migration_benchmark(cleanup_ray_env, shutdown_llumnix_service, model, migration_backend, migrated_request_status): if migrated_request_status == 'waiting' and migration_backend != 'rpc': pytest.skip("When the migrated request status is waiting, only test the rpc migration backend.") request_migration_policy = 'SR' if migrated_request_status == 'running' else 'FCW' - + ip = "127.0.0.1" base_port = 37037 + ip_ports = [] instance_output_logs = [] - device_count = torch.cuda.device_count() for i in range(device_count): + port = base_port + i + ip_ports.append(f"{ip}:{base_port+i}") output_log = f"{base_port+i}.out" instance_output_logs.append("instance_"+output_log) - launch_command = generate_launch_command(result_filename=output_log, launch_ray_cluster=False, port=base_port+i, - model=model, dispatch_policy="flood", migration_backend=migration_backend, - log_instance_info=True, + launch_command = generate_launch_command(result_filename=output_log, + launch_ray_cluster=False, + ip=ip, + port=port, + model=model, + dispatch_policy="flood", + migration_backend=migration_backend, request_migration_policy=request_migration_policy) subprocess.run(launch_command, shell=True, check=True) - await asyncio.sleep(5) - await asyncio.sleep(30) - - async def run_bench_command(command): - process = await asyncio.create_subprocess_shell(command) - await process.wait() - assert process.returncode == 0 - tasks = [] - for i in range(device_count//2): - bench_command = generate_bench_command(ip_ports=f"127.0.0.1:{base_port+i}", model=model, num_prompts=300, - dataset_type="sharegpt", - dataset_path="/mnt/dataset/sharegpt_gpt4/sharegpt_gpt4.jsonl" , - qps=10, - results_filename=f"{base_port+i}.out") - tasks.append(asyncio.create_task(run_bench_command(bench_command))) + wait_for_llumnix_service_ready(ip_ports) - _, pending = await asyncio.wait(tasks, timeout=60*30) + instance_num_blocks_list_before_bench = get_instance_num_blocks() - await asyncio.sleep(10) + def run_bench_command(command): + # pylint: disable=consider-using-with + process = subprocess.Popen(command, shell=True) + return process - if len(pending) > 0: - raise RuntimeError("migration task Timeout") - - parse_manager_log_file("manager_instance.csv") + tasks = [] + for i in range(device_count // 2): + bench_command = generate_bench_command( + ip_ports=f"127.0.0.1:{base_port + i}", + model=model, + num_prompts=500, + dataset_type="sharegpt", + dataset_path="/mnt/dataset/sharegpt_gpt4/sharegpt_gpt4.jsonl", + qps=10, + results_filename=f"{base_port + i}.out" + ) + tasks.append(bench_command) + + # Execute the commands concurrently using ThreadPoolExecutor + with ThreadPoolExecutor() as executor: + future_to_command = {executor.submit(run_bench_command, command): command for command in tasks} + + for future in as_completed(future_to_command): + try: + process = future.result() + process.wait(timeout=60*MIGRATION_BENCH_TIMEOUT_MINS) + + assert process.returncode == 0, "migration_test failed with return code {}.".format(process.returncode) + # pylint: disable=broad-except + except subprocess.TimeoutExpired: + process.kill() + assert False, "migration_test timed out after {} minutes.".format(MIGRATION_BENCH_TIMEOUT_MINS) + + wait_for_all_instances_finished() + instance_num_blocks_list_after_bench = get_instance_num_blocks() + + assert instance_num_blocks_list_before_bench == instance_num_blocks_list_after_bench if migrated_request_status == 'running': average_speed = parse_instance_log_file(instance_output_logs) @@ -122,6 +169,4 @@ async def run_bench_command(command): with open("performance.txt", "a", encoding="utf-8") as f: f.write(to_markdown_table(data)) - shutdown_llumnix_service() - clear_ray_state() - await asyncio.sleep(10) + await asyncio.sleep(3) diff --git a/tests/e2e_test/utils.py b/tests/e2e_test/utils.py index 1c38dcc8..5e2b05f6 100644 --- a/tests/e2e_test/utils.py +++ b/tests/e2e_test/utils.py @@ -11,9 +11,161 @@ # See the License for the specific language governing permissions and # limitations under the License. - +import time import subprocess import pytest +import ray +import requests + + +def parse_launch_mode(launch_mode: str): + # 'eief' means that enable init instance by manager and enable fixed node init instance, and so on. + if launch_mode == 'eief': + disable_init_instance_by_manager = False + disable_fixed_node_init_instance = False + elif launch_mode == 'eidf': + disable_init_instance_by_manager = False + disable_fixed_node_init_instance = True + elif launch_mode == 'dief': + disable_init_instance_by_manager = True + disable_fixed_node_init_instance = False + else: + disable_init_instance_by_manager = True + disable_fixed_node_init_instance = True + return disable_init_instance_by_manager, disable_fixed_node_init_instance + +def generate_launch_command(result_filename: str = "", + launch_ray_cluster: bool = True, + HEAD_NODE_IP: str = "127.0.0.1", + ip: str = "127.0.0.1", + port: int = 37000, + instances_num = 1, + dispatch_policy: str = "load", + migration_backend = "gloo", + model = "facebook/opt-125m", + max_model_len: int = 4096, + launch_mode: str = 'eief', + log_instance_info: bool = False, + request_migration_policy: str = 'SR', + max_num_batched_tokens: int = 16000): + disable_init_instance_by_manager, disable_fixed_node_init_instance = parse_launch_mode(launch_mode) + command = ( + f"RAY_DEDUP_LOGS=0 HEAD_NODE_IP={HEAD_NODE_IP} HEAD_NODE=1 " + f"nohup python -u -m llumnix.entrypoints.vllm.api_server " + f"--host {ip} " + f"--port {port} " + f"{'--disable-init-instance-by-manager ' if disable_init_instance_by_manager else ''}" + f"{'--disable-fixed-node-init-instance ' if disable_fixed_node_init_instance else ''}" + f"--initial-instances {instances_num} " + f"{'--log-filename manager ' if log_instance_info else ''}" + f"{'--log-instance-info ' if log_instance_info else ''}" + f"--enable-migration " + f"--model {model} " + f"--engine-use-ray " + f"--worker-use-ray " + f"--max-model-len {max_model_len} " + f"--dispatch-policy {dispatch_policy} " + f"--trust-remote-code " + f"--request-migration-policy {request_migration_policy} " + f"--migration-backend {migration_backend} " + f"--migration-buffer-blocks 32 " + f"--tensor-parallel-size 1 " + f"--request-output-queue-port {1234+port} " + f"{'--launch-ray-cluster ' if launch_ray_cluster else ''}" + f"--max-num-batched-tokens {max_num_batched_tokens} " + f"{'> instance_'+result_filename if len(result_filename)> 0 else ''} 2>&1 &" + ) + return command + +def wait_for_llumnix_service_ready(ip_ports, timeout=60): + start_time = time.time() + while True: + all_ready = True + for ip_port in ip_ports: + try: + response = requests.get(f"http://{ip_port}/is_ready", timeout=5) + if 'true' not in response.text.lower(): + all_ready = False + break + except requests.RequestException: + all_ready = False + break + + if all_ready: + return True + + elapsed_time = time.time() - start_time + if elapsed_time > timeout: + raise TimeoutError(f"Wait for llumnix service timeout({timeout} s).") + + time.sleep(1) + +def generate_bench_command(ip_ports: str, + model: str, + num_prompts: int, + dataset_type: str, + dataset_path: str, + qps: int, + results_filename: str = "", + query_distribution: str = "poisson", + coefficient_variation: float = 1.0, + priority_ratio: float = 0.0): + command = ( + f"python -u ./benchmark/benchmark_serving.py " + f"--ip_ports {ip_ports} " + f"--backend vLLM " + f"--tokenizer {model} " + f"--trust_remote_code " + f"--log_filename bench_{ip_ports.split(':')[1]} " + f"--random_prompt_count {num_prompts} " + f"--dataset_type {dataset_type} " + f"--dataset_path {dataset_path} " + f"--qps {qps} " + f"--distribution {query_distribution} " + f"--coefficient_variation {coefficient_variation} " + f"--priority_ratio {priority_ratio} " + f"--log_latencies " + f"--fail_on_response_failure " + f"{'> bench_'+results_filename if len(results_filename)> 0 else ''}" + ) + return command + +def cleanup_ray_env_func(): + try: + try: + named_actors = ray.util.list_named_actors(True) + for actor in named_actors: + try: + actor_handle = ray.get_actor(actor['name'], namespace=actor['namespace']) + # pylint: disable=bare-except + except: + continue + try: + ray.kill(actor_handle) + # pylint: disable=bare-except + except: + continue + # pylint: disable=bare-except + except: + pass + ray.shutdown() + # pylint: disable=bare-except + except: + pass + +@pytest.fixture +def cleanup_ray_env(): + yield + cleanup_ray_env_func() + +def shutdown_llumnix_service_func(): + subprocess.run('pkill -f llumnix.entrypoints.vllm.api_server', shell=True, check=False) + subprocess.run('pkill -f benchmark_serving.py', shell=True, check=False) + +@pytest.fixture +def shutdown_llumnix_service(): + yield + shutdown_llumnix_service_func() def to_markdown_table(data): headers = data[0] @@ -31,11 +183,3 @@ def to_markdown_table(data): table = f"{header_row}\n{separator_row}\n" + "\n".join(data_rows) + "\n\n" return table - -@pytest.fixture -def setup_ray_env(): - subprocess.run(["ray", "stop"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - subprocess.run(["ray", "start", "--head", "--disable-usage-stats", "--port=6379"], check=True, - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - yield - subprocess.run(["ray", "stop"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) diff --git a/tests/unit_test/backends/vllm/test_llm_engine.py b/tests/unit_test/backends/vllm/test_llm_engine.py index 6e9e6a05..bbd8477e 100644 --- a/tests/unit_test/backends/vllm/test_llm_engine.py +++ b/tests/unit_test/backends/vllm/test_llm_engine.py @@ -89,19 +89,19 @@ def test_llm_engine_process_model_outputs(): def test_llm_engine_from_engine_args(): engine_args = EngineArgs(model="facebook/opt-125m", worker_use_ray=True) - llm_engine = MockEngine.from_engine_args(engine_args, output_queue_type=QueueType.RAYQUEUE, + llm_engine = MockEngine.from_engine_args(engine_args, request_output_queue_type=QueueType.RAYQUEUE, instance_id="0", migration_config=None) assert llm_engine.executor_class == LlumnixRayGPUExecutor latency_data = LatencyMemData({},{},{}) - llm_engine = MockEngine.from_engine_args(engine_args, output_queue_type=QueueType.RAYQUEUE, + llm_engine = MockEngine.from_engine_args(engine_args, request_output_queue_type=QueueType.RAYQUEUE, instance_id="0", migration_config=None, latency_mem=latency_data) assert llm_engine.executor_class == SimGPUExecutor def test_llm_engine_add_requset(): engine_args = EngineArgs(model="facebook/opt-125m", worker_use_ray=True) llm_engine = LLMEngineLlumnix.from_engine_args(engine_args, - output_queue_type=QueueType.RAYQUEUE, + request_output_queue_type=QueueType.RAYQUEUE, instance_id="0", placement_group=None, node_id=ray.get_runtime_context().get_node_id(), diff --git a/tests/unit_test/backends/vllm/test_migration.py b/tests/unit_test/backends/vllm/test_migration.py index b74950c2..d247e160 100644 --- a/tests/unit_test/backends/vllm/test_migration.py +++ b/tests/unit_test/backends/vllm/test_migration.py @@ -83,7 +83,6 @@ async def step_async_try_schedule(): self.backend_engine.engine.step_async = step_async_try_schedule -# TODO(s5u13b): Test migrate waiting request. @pytest.mark.parametrize("migration_backend", ['rpc', 'gloo', 'nccl']) @pytest.mark.parametrize("migration_request_status", ['waiting', 'running']) @pytest.mark.asyncio @@ -94,16 +93,16 @@ async def test_migration_correctness(setup_ray_env, migration_backend, migration request_migration_policy = "SR" elif migration_request_status == 'waiting': request_migration_policy = "FCW" - migration_config = MigrationConfig(request_migration_policy, migration_backend, 16, 1, 4, 5, 20, 2) + migration_config = MigrationConfig(request_migration_policy, migration_backend, 16, 1, 4, 5, 20) - output_queue_type = QueueType.RAYQUEUE - que, server_info = request_output_queue_server(output_queue_type) + request_output_queue_type = QueueType.RAYQUEUE + que, server_info = request_output_queue_server(request_output_queue_type) asyncio.create_task(que.run_server_loop()) node_id = ray.get_runtime_context().get_node_id() scheduling_strategy = NodeAffinitySchedulingStrategy(node_id=node_id, soft=False) llumlet_0: Llumlet = Llumlet.from_args( - output_queue_type, + request_output_queue_type, False, False, node_id, @@ -114,7 +113,7 @@ async def test_migration_correctness(setup_ray_env, migration_backend, migration engine_args) llumlet_1: Llumlet = Llumlet.from_args( - output_queue_type, + request_output_queue_type, False, False, node_id, @@ -129,7 +128,7 @@ async def test_migration_correctness(setup_ray_env, migration_backend, migration namespace='llumnix', scheduling_strategy=scheduling_strategy).remote( instance_id="2", - output_queue_type=output_queue_type, + request_output_queue_type=request_output_queue_type, backend_type=BackendType.VLLM, migration_config=migration_config, engine_args=engine_args, @@ -209,14 +208,14 @@ async def test_correctness(prompt): async def test_pd_diaggregation_correctness(setup_ray_env, migration_backend): engine_args = EngineArgs(model="facebook/opt-125m", worker_use_ray=True) id_rank_map = {"0":0, "1":1} - migration_config = MigrationConfig("SR", migration_backend, 16, 1, 4, 5, 20, 2) + migration_config = MigrationConfig("SR", migration_backend, 16, 1, 4, 5, 20) - output_queue_type = QueueType.RAYQUEUE - que, server_info = request_output_queue_server(output_queue_type) + request_output_queue_type = QueueType.RAYQUEUE + que, server_info = request_output_queue_server(request_output_queue_type) asyncio.create_task(que.run_server_loop()) llumlet_0:Llumlet = Llumlet.from_args( - output_queue_type, + request_output_queue_type, False, True, ray.get_runtime_context().get_node_id(), @@ -227,7 +226,7 @@ async def test_pd_diaggregation_correctness(setup_ray_env, migration_backend): engine_args,) llumlet_1:Llumlet = Llumlet.from_args( - output_queue_type, + request_output_queue_type, False, True, ray.get_runtime_context().get_node_id(), diff --git a/tests/unit_test/backends/vllm/test_migration_backend.py b/tests/unit_test/backends/vllm/test_migration_backend.py index 12ec324c..c6e23e10 100644 --- a/tests/unit_test/backends/vllm/test_migration_backend.py +++ b/tests/unit_test/backends/vllm/test_migration_backend.py @@ -26,44 +26,6 @@ from tests.conftest import setup_ray_env from .test_worker import create_worker -def get_ready_workers(num_worker, num_gpu_blocks, engine_config, migraiton_config): - workers = [] - worker_ids = [] - - for _ in range(num_worker): - worker_id = random_uuid() - worker = create_worker(rank=0, local_rank=0, engine_config=engine_config, - worker_module_name="tests.unit_test.backends.vllm.test_migration_backend", - worker_class_name="MockMigrationWorker") - ray.get(worker.execute_method.remote('initialize_cache', num_gpu_blocks=num_gpu_blocks, num_cpu_blocks=0)) - ray.get(worker.execute_method.remote( - 'init_migration', - instance_id=worker_id, - migration_config=migraiton_config, - src_worker_handle_list=[worker], - node_id=ray.get_runtime_context().get_node_id())) - - workers.append(worker) - worker_ids.append(worker_id) - - instance_rank = {} - for idx, worker_id in enumerate(worker_ids): - instance_rank[worker_id] = idx - group_name = random_uuid() - - init_group_tasks =[] - for worker in workers: - init_group_tasks.append(worker.execute_method.remote('rebuild_migration_backend', - instance_rank=instance_rank, group_name=group_name)) - assert all(ray.get(init_group_tasks)) - - warmup_tasks = [] - for worker in workers: - warmup_tasks.append(worker.execute_method.remote('warmup')) - assert all(ray.get(warmup_tasks)) - - return workers, worker_ids - class MockMigrationWorker(MigrationWorker): def set_gpu_cache(self, data): for layer_idx in range(self.cache_engine.num_layers): @@ -72,120 +34,75 @@ def set_gpu_cache(self, data): def get_gpu_cache(self): torch.cuda.synchronize() - gpu_data = [] - for layer_idx in range(self.cache_engine.num_layers): - gpu_data.append(self.gpu_cache[layer_idx].clone().cpu()) - return gpu_data + return self.gpu_cache -@pytest.mark.skipif(torch.cuda.device_count() < 3, reason="Need at least 3 GPU to run the test.") -@pytest.mark.parametrize("backend", ['rpc', 'gloo']) -def test_one_to_many_migrate_cache(setup_ray_env, backend): +@pytest.mark.skipif(torch.cuda.device_count() < 2, reason="Need at least 2 GPU to run the test.") +@pytest.mark.parametrize("backend", ['rpc', 'gloo', 'nccl']) +def test_migrate_cache(setup_ray_env, backend): engine_config = EngineArgs(model='facebook/opt-125m', max_model_len=8, enforce_eager=True).create_engine_config() - migration_internal_buffer_num = 2 - migraiton_config = EngineManagerArgs(migration_buffer_blocks=3, migration_num_layers=5, - migration_internal_buffer_num=migration_internal_buffer_num).create_migration_config() + migraiton_config = EngineManagerArgs(migration_buffer_blocks=3, migration_num_layers=5).create_migration_config() migraiton_config.migration_backend = backend - num_worker = 3 - num_gpu_blocks = 6000 - workers, _ = get_ready_workers(num_worker, num_gpu_blocks, engine_config, migraiton_config) - - num_layers = engine_config.model_config.get_num_layers(engine_config.parallel_config) - head_size = engine_config.model_config.get_head_size() - num_heads = engine_config.model_config.get_num_kv_heads(engine_config.parallel_config) - block_size = engine_config.cache_config.block_size - dummy_data = torch.randn(size=(num_layers, 2, num_gpu_blocks, block_size*num_heads*head_size)) - ray.get(workers[0].execute_method.remote('set_gpu_cache', data=dummy_data)) - worker0_data = ray.get(workers[0].execute_method.remote('get_gpu_cache')) - - dst_blocks = list(range(num_gpu_blocks)) - random.shuffle(dst_blocks) - - single_worker_num_blocks = len(dst_blocks)//(num_worker-1) - migration_tasks = [] - worker_idx = 1 - per_step_blocks = 500 - for offset in range(0, len(dst_blocks), single_worker_num_blocks): - src_to_dst = dict(enumerate(dst_blocks[offset:offset+single_worker_num_blocks])) - src_blocks = list(src_to_dst.keys()) - dst_blocks = list(src_to_dst.values()) - for idx in range(0, len(src_blocks), per_step_blocks): - cur_src_blocks = src_blocks[idx:idx+per_step_blocks] - cur_dst_blocks = dst_blocks[idx:idx+per_step_blocks] - migration_tasks.append(workers[0].execute_method.remote( - 'migrate_cache', - src_worker_handle_list=[workers[worker_idx]], - src_blocks=cur_src_blocks, - dst_blocks=cur_dst_blocks) - ) - worker_idx += 1 - ray.get(migration_tasks) - - worker_idx = 1 - for offset in range(0, len(dst_blocks), single_worker_num_blocks): - src_to_dst = dict(enumerate(dst_blocks[offset:offset+single_worker_num_blocks])) - dst_worker_data = ray.get(workers[worker_idx].execute_method.remote('get_gpu_cache')) - for layer_idx in range(num_layers): - for src_idx, dst_idx in src_to_dst.items(): - assert torch.allclose(worker0_data[layer_idx][0][src_idx], dst_worker_data[layer_idx][0][dst_idx]) - assert torch.allclose(worker0_data[layer_idx][1][src_idx], dst_worker_data[layer_idx][1][dst_idx]) - worker_idx += 1 - -@pytest.mark.skipif(torch.cuda.device_count() < 3, reason="Need at least 3 GPU to run the test.") -@pytest.mark.parametrize("backend", ['rpc', 'gloo']) -def test_many_to_one_migrate_cache(setup_ray_env, backend): - engine_config = EngineArgs(model='facebook/opt-125m', max_model_len=8, enforce_eager=True).create_engine_config() - migration_internal_buffer_num = 2 - migraiton_config = EngineManagerArgs(migration_buffer_blocks=3, migration_num_layers=5, - migration_internal_buffer_num=migration_internal_buffer_num).create_migration_config() - migraiton_config.migration_backend = backend + worker0 = create_worker(rank=0, local_rank=0, engine_config=engine_config, + worker_module_name="tests.unit_test.backends.vllm.test_migration_backend", + worker_class_name="MockMigrationWorker") + worker1 = create_worker(rank=0, local_rank=0, engine_config=engine_config, + worker_module_name="tests.unit_test.backends.vllm.test_migration_backend", + worker_class_name="MockMigrationWorker") - num_worker = 3 - num_gpu_blocks = 6000 - workers, _ = get_ready_workers(num_worker, num_gpu_blocks, engine_config, migraiton_config) + ray.get(worker0.execute_method.remote('init_device')) + ray.get(worker1.execute_method.remote('init_device')) + + num_gpu_blocks = 8 + ray.get(worker0.execute_method.remote('initialize_cache', num_gpu_blocks=num_gpu_blocks, num_cpu_blocks=0)) + ray.get(worker1.execute_method.remote('initialize_cache', num_gpu_blocks=num_gpu_blocks, num_cpu_blocks=0)) + + worker0_id = random_uuid() + ray.get(worker0.execute_method.remote( + 'init_migration', + instance_id=worker0_id, + migration_config=migraiton_config, + src_worker_handle_list=[worker0], + node_id=ray.get_runtime_context().get_node_id())) + + worker1_id = random_uuid() + ray.get(worker1.execute_method.remote( + 'init_migration', + instance_id=worker1_id, + migration_config=migraiton_config, + src_worker_handle_list=[worker1], + node_id=ray.get_runtime_context().get_node_id())) + + instance_rank = {worker0_id: 0, worker1_id: 1} + group_name = random_uuid() + assert all(ray.get([worker0.execute_method.remote('rebuild_migration_backend', + instance_rank=instance_rank, group_name=group_name), + worker1.execute_method.remote('rebuild_migration_backend', + instance_rank=instance_rank, group_name=group_name)])) + assert all(ray.get([worker0.execute_method.remote('warmup'), + worker1.execute_method.remote('warmup')])) num_layers = engine_config.model_config.get_num_layers(engine_config.parallel_config) head_size = engine_config.model_config.get_head_size() num_heads = engine_config.model_config.get_num_kv_heads(engine_config.parallel_config) block_size = engine_config.cache_config.block_size - dummy_data = torch.randn(size=(num_layers, 2, num_gpu_blocks, block_size*num_heads*head_size)) - worker_datas = [0] - for idx in range(1, num_worker): - ray.get(workers[idx].execute_method.remote('set_gpu_cache', data=dummy_data)) - worker_datas.append(ray.get(workers[idx].execute_method.remote('get_gpu_cache'))) + dummy_data = torch.randn(size=(num_layers, 2, num_gpu_blocks, block_size*num_heads*head_size)) + ray.get(worker0.execute_method.remote('set_gpu_cache', data=dummy_data)) + worker0_data = ray.get(worker0.execute_method.remote('get_gpu_cache')) dst_blocks = list(range(num_gpu_blocks)) random.shuffle(dst_blocks) - - single_worker_num_blocks = len(dst_blocks)//(num_worker-1) - migration_tasks = [] - worker_idx = 1 - per_step_blocks = 500 - for offset in range(0, len(dst_blocks), single_worker_num_blocks): - src_to_dst = dict(enumerate(dst_blocks[offset:offset+single_worker_num_blocks])) - src_blocks = list(src_to_dst.keys()) - dst_blocks = list(src_to_dst.values()) - for idx in range(0, len(src_blocks), per_step_blocks): - cur_src_blocks = src_blocks[idx:idx+per_step_blocks] - cur_dst_blocks = dst_blocks[idx:idx+per_step_blocks] - migration_tasks.append(workers[0].execute_method.remote( - 'migrate_cache', - src_worker_handle_list=[workers[worker_idx]], - src_blocks=cur_src_blocks, - dst_blocks=cur_dst_blocks) - ) - worker_idx += 1 - ray.get(migration_tasks) - - dst_worker_data = ray.get(workers[0].execute_method.remote('get_gpu_cache')) - - worker_idx = 1 - for offset in range(0, len(dst_blocks), single_worker_num_blocks): - src_to_dst = dict(enumerate(dst_blocks[offset:offset+single_worker_num_blocks])) - - for layer_idx in range(num_layers): - for src_idx, dst_idx in src_to_dst.items(): - assert torch.allclose(worker_datas[worker_idx][layer_idx][0][src_idx], dst_worker_data[layer_idx][0][dst_idx]) - assert torch.allclose(worker_datas[worker_idx][layer_idx][1][src_idx], dst_worker_data[layer_idx][1][dst_idx]) - worker_idx += 1 + src_to_dst = dict(enumerate(dst_blocks)) + ray.get(worker1.execute_method.remote( + 'migrate_cache', + src_worker_handle_list=[worker0], + src_blocks=list(src_to_dst.keys()), + dst_blocks=list(src_to_dst.values()))) + + worker1_data = ray.get(worker1.execute_method.remote('get_gpu_cache')) + + for layer_idx in range(num_layers): + for src_idx, dst_idx in src_to_dst.items(): + assert torch.allclose(worker0_data[layer_idx][0][src_idx], worker1_data[layer_idx][0][dst_idx]) + assert torch.allclose(worker0_data[layer_idx][1][src_idx], worker1_data[layer_idx][1][dst_idx]) diff --git a/tests/unit_test/backends/vllm/test_simulator.py b/tests/unit_test/backends/vllm/test_simulator.py index 7a2632cf..9a685e18 100644 --- a/tests/unit_test/backends/vllm/test_simulator.py +++ b/tests/unit_test/backends/vllm/test_simulator.py @@ -18,6 +18,7 @@ from tests.unit_test.queue.utils import request_output_queue_server from .utils import create_dummy_prompt, initialize_scheduler + class MockBackendSim(BackendSimVLLM): def _get_lantecy_mem(self, *args, **kwargs): @@ -71,10 +72,10 @@ async def test_backend(setup_ray_env): # TODO(ZeldaHuang): add tests for BackendSimVLLM methods # (currently BackendSimVLLM is just a wrapper of BackendVLLM) engine_args = EngineArgs(model="facebook/opt-125m", worker_use_ray=True) - migration_config = MigrationConfig("SR", "gloo", 16, 1, 4, 5, 20, 2) + migration_config = MigrationConfig("SR", "gloo", 16, 1, 4, 5, 20) - output_queue_type = QueueType.RAYQUEUE - que, server_info = request_output_queue_server(output_queue_type) + request_output_queue_type = QueueType.RAYQUEUE + que, server_info = request_output_queue_server(request_output_queue_type) asyncio.create_task(que.run_server_loop()) class DummyActor: def __init__(self): @@ -85,7 +86,7 @@ def __init__(self): max_concurrency=4)(DummyActor) dummy_actor = dummy_actor.remote() sim_backend = MockBackendSim(instance_id="0", - output_queue_type=output_queue_type, + request_output_queue_type=request_output_queue_type, migration_config=migration_config, profiling_result_file_path="", engine_args=engine_args) diff --git a/tests/unit_test/backends/vllm/test_worker.py b/tests/unit_test/backends/vllm/test_worker.py index 09df9ea0..a42b9f28 100644 --- a/tests/unit_test/backends/vllm/test_worker.py +++ b/tests/unit_test/backends/vllm/test_worker.py @@ -39,7 +39,7 @@ def create_worker(rank: int, local_rank: int, engine_config: EngineConfig, trust_remote_code=True ) - ray.get(worker.init_worker.remote( + worker.init_worker.remote( model_config=engine_config.model_config, parallel_config=engine_config.parallel_config, scheduler_config=engine_config.scheduler_config, @@ -52,8 +52,8 @@ def create_worker(rank: int, local_rank: int, engine_config: EngineConfig, lora_config=engine_config.lora_config, vision_language_config=engine_config.vision_language_config, is_driver_worker = False - )) - ray.get(worker.execute_method.remote('init_device')) + ) + return worker @pytest.mark.parametrize("backend", ['rpc', 'gloo', 'nccl']) @@ -62,12 +62,12 @@ def test_reserve_memory_for_migration(setup_ray_env, backend): migration_config = EngineManagerArgs(migration_buffer_blocks=1).create_migration_config() migration_config.migration_backend = backend worker = create_worker(rank=0, local_rank=0, engine_config=engine_config) + ray.get(worker.execute_method.remote('init_device')) block_size = CacheEngine.get_cache_block_size(engine_config.cache_config, engine_config.model_config, engine_config.parallel_config) num_layers = engine_config.model_config.get_num_layers(engine_config.parallel_config) - occupy_memory = migration_config.migration_internal_buffer_num * migration_config.migration_buffer_blocks \ - * block_size * migration_config.migration_num_layers // num_layers + occupy_memory = migration_config.migration_buffer_blocks * block_size * migration_config.migration_num_layers // num_layers migration_cache_size = ray.get(worker.execute_method.remote('reserve_memory_for_migration', migration_config=migration_config, @@ -85,6 +85,7 @@ def test_rebuild_migration_backend(setup_ray_env, backend): worker0 = create_worker(rank=0, local_rank=0, engine_config=engine_config) worker0_id = random_uuid() + ray.get(worker0.execute_method.remote('init_device')) ray.get(worker0.execute_method.remote('initialize_cache', num_gpu_blocks=8, num_cpu_blocks=0)) ray.get(worker0.execute_method.remote( 'init_migration', @@ -99,6 +100,7 @@ def test_rebuild_migration_backend(setup_ray_env, backend): worker1 = create_worker(rank=0, local_rank=0, engine_config=engine_config) worker1_id = random_uuid() + ray.get(worker1.execute_method.remote('init_device')) ray.get(worker1.execute_method.remote('initialize_cache', num_gpu_blocks=8, num_cpu_blocks=0)) ray.get(worker1.execute_method.remote( 'init_migration', diff --git a/tests/unit_test/entrypoints/test_utils.py b/tests/unit_test/entrypoints/test_utils.py index d4787b58..033835ce 100644 --- a/tests/unit_test/entrypoints/test_utils.py +++ b/tests/unit_test/entrypoints/test_utils.py @@ -22,7 +22,7 @@ retry_manager_method_sync, retry_manager_method_async) from llumnix.llm_engine_manager import MANAGER_ACTOR_NAME -from llumnix.queue.utils import init_output_queue_server +from llumnix.queue.utils import init_request_output_queue_server # pylint: disable=unused-import from tests.conftest import setup_ray_env @@ -46,7 +46,7 @@ def test_init_manager(setup_ray_env): def test_init_zmq(setup_ray_env): ip = '127.0.0.1' port = 1234 - request_output_queue = init_output_queue_server(ip, port, 'zmq') + request_output_queue = init_request_output_queue_server(ip, port, 'zmq') assert request_output_queue is not None def test_retry_manager_method_sync(setup_ray_env): diff --git a/tests/unit_test/entrypoints/vllm/api_server_manager.py b/tests/unit_test/entrypoints/vllm/api_server_manager.py index 0fd6f64d..f9616555 100644 --- a/tests/unit_test/entrypoints/vllm/api_server_manager.py +++ b/tests/unit_test/entrypoints/vllm/api_server_manager.py @@ -23,7 +23,7 @@ from llumnix.arg_utils import EngineManagerArgs from llumnix.server_info import ServerInfo, RequestTimestamps from llumnix.utils import random_uuid -from llumnix.queue.utils import init_output_queue_server, init_output_queue_client, QueueType +from llumnix.queue.utils import init_request_output_queue_server, init_request_output_queue_client, QueueType from llumnix.entrypoints.utils import LlumnixEntrypointsContext app = llumnix.entrypoints.vllm.api_server.app @@ -33,10 +33,10 @@ @ray.remote(num_cpus=0) class MockLLMEngineManager: - def __init__(self, output_queue_type: QueueType): + def __init__(self, request_output_queue_type: QueueType): self._num_generates = 0 self._num_aborts = 0 - self.request_output_queue = init_output_queue_client(output_queue_type) + self.request_output_queue = init_request_output_queue_client(request_output_queue_type) async def generate(self, request_id, server_info, *args, **kwargs): self._num_generates += 1 @@ -52,9 +52,9 @@ def testing_stats(self): return {"num_aborted_requests": self._num_aborts} -def init_manager(output_queue_type: QueueType): +def init_manager(request_output_queue_type: QueueType): engine_manager = MockLLMEngineManager.options(name=MANAGER_ACTOR_NAME, - namespace='llumnix').remote(output_queue_type) + namespace='llumnix').remote(request_output_queue_type) return engine_manager @app.get("/stats") @@ -67,22 +67,22 @@ def stats() -> Response: parser = argparse.ArgumentParser() parser.add_argument("--host", type=str, default="localhost") parser.add_argument("--port", type=int, default=8000) - parser.add_argument("--output-queue-type", type=str, choices=["zmq", "rayqueue"]) + parser.add_argument("--request-output-queue-type", type=str, choices=["zmq", "rayqueue"]) parser = EngineManagerArgs.add_cli_args(parser) args = parser.parse_args() - output_queue_type = QueueType(args.output_queue_type) - engine_manager = init_manager(output_queue_type) + request_output_queue_type = QueueType(args.request_output_queue_type) + engine_manager = init_manager(request_output_queue_type) llumnix.entrypoints.vllm.api_server.llumnix_context = LlumnixEntrypointsContext() llumnix.entrypoints.vllm.api_server.llumnix_context.engine_manager = engine_manager ip = '127.0.0.1' port = 1234 llumnix.entrypoints.vllm.api_server.llumnix_context.request_output_queue = \ - init_output_queue_server(ip, port, output_queue_type) + init_request_output_queue_server(ip, port, request_output_queue_type) ray_queue_server = None - if output_queue_type == QueueType.RAYQUEUE: + if request_output_queue_type == QueueType.RAYQUEUE: ray_queue_server = llumnix.entrypoints.vllm.api_server.llumnix_context.request_output_queue - server_info = ServerInfo(random_uuid(), output_queue_type, ray_queue_server, ip, port) + server_info = ServerInfo(random_uuid(), request_output_queue_type, ray_queue_server, ip, port) llumnix.entrypoints.vllm.api_server.llumnix_context.server_info = server_info uvicorn.run( diff --git a/tests/unit_test/entrypoints/vllm/test_api_server.py b/tests/unit_test/entrypoints/vllm/test_api_server.py index a6d3f272..9a658375 100644 --- a/tests/unit_test/entrypoints/vllm/test_api_server.py +++ b/tests/unit_test/entrypoints/vllm/test_api_server.py @@ -47,7 +47,7 @@ def _query_server_generate_benchmark(prompt: str) -> dict: @pytest.fixture(params=["zmq", "rayqueue"]) def api_server(request): - output_queue_type = QueueType(request.param) + request_output_queue_type = QueueType(request.param) script_path = Path(__file__).parent.joinpath( "api_server_manager.py").absolute() commands = [ @@ -55,14 +55,14 @@ def api_server(request): "-u", str(script_path), "--host", "127.0.0.1", - "--output-queue-type", output_queue_type, + "--request-output-queue-type", request_output_queue_type, ] # pylint: disable=consider-using-with uvicorn_process = subprocess.Popen(commands) yield uvicorn_process.terminate() # Waiting for api server subprocess to terminate. - time.sleep(1.0) + time.sleep(1) @pytest.mark.parametrize("interface", ['generate', 'generate_benchmark']) def test_api_server(setup_ray_env, api_server, interface: str): diff --git a/tests/unit_test/global_scheduler/test_llm_engine_manager.py b/tests/unit_test/global_scheduler/test_llm_engine_manager.py index 5f81baf6..5a09b283 100644 --- a/tests/unit_test/global_scheduler/test_llm_engine_manager.py +++ b/tests/unit_test/global_scheduler/test_llm_engine_manager.py @@ -288,3 +288,7 @@ def test_update_instance_info_loop_and_migrate(setup_ray_env, engine_manager): assert num_migrate_in == 0 and num_migrate_out > 1 else: assert num_migrate_in == 0 and num_migrate_out == 0 + +@pytest.mark.skip("Not implemented yet") +def test_concurrent_migrate(setup_ray_env): + pass diff --git a/tests/unit_test/llumlet/test_engine_step_exception.py b/tests/unit_test/llumlet/test_engine_step_exception.py index 86176ab2..736520d2 100644 --- a/tests/unit_test/llumlet/test_engine_step_exception.py +++ b/tests/unit_test/llumlet/test_engine_step_exception.py @@ -11,6 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import asyncio import time import ray import torch @@ -29,17 +30,28 @@ @ray.remote(num_cpus=1, max_concurrency=4) class MockLlumlet(Llumlet): - def set_error_step(self): + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.origin_step = self.backend_engine.engine.step_async + + def set_error_step(self, broken: bool): + self.backend_engine._stop_event.set() + async def raise_error_step(): await self.origin_step() raise ValueError("Mock engine step error") - self.backend_engine.engine.step_async = raise_error_step + if broken: + self.backend_engine.engine.step_async = raise_error_step + else: + self.backend_engine.engine.step_async = self.origin_step + + asyncio.create_task(self.backend_engine._start_engine_step_loop()) @pytest.mark.skipif(torch.cuda.device_count() < 1, reason="Need at least 1 GPU to run the test.") def test_engine_step_exception(setup_ray_env): engine_args = EngineArgs(model="facebook/opt-125m", max_model_len=8, worker_use_ray=True) - migration_config = MigrationConfig("SR", "rpc", 16, 1, 4, 5, 20, 2) + migration_config = MigrationConfig("SR", "rpc", 16, 1, 4, 5, 20) node_id = ray.get_runtime_context().get_node_id() scheduling_strategy = NodeAffinitySchedulingStrategy(node_id=node_id, soft=False) @@ -48,7 +60,7 @@ def test_engine_step_exception(setup_ray_env): actor_name = "instance_0" llumlet = MockLlumlet.options(name=actor_name, namespace='llumnix', scheduling_strategy=scheduling_strategy).remote( - output_queue_type=QueueType.RAYQUEUE, + request_output_queue_type=QueueType.RAYQUEUE, instance_id="0", backend_type=BackendType.VLLM, migration_config=migration_config, @@ -64,7 +76,7 @@ def test_engine_step_exception(setup_ray_env): cur_free_memory, _ = torch.cuda.mem_get_info() assert cur_free_memory < origin_free_memory - ray.get(llumlet.set_error_step.remote()) + ray.get(llumlet.set_error_step.remote(True)) time.sleep(3) all_actors = ray.util.list_named_actors(True) diff --git a/tests/unit_test/queue/test_zmq.py b/tests/unit_test/queue/test_zmq.py index 6f62935e..d4303d37 100644 --- a/tests/unit_test/queue/test_zmq.py +++ b/tests/unit_test/queue/test_zmq.py @@ -106,8 +106,8 @@ async def benchmark_queue(qps, ip=None, port=None): signal.alarm(0) @pytest.mark.asyncio -async def test_queue_zmq(setup_ray_env): +@pytest.mark.parametrize("qps", [128.0, 256.0, 512.0, 1024.0]) +async def test_queue_zmq(setup_ray_env, qps): ip = '127.0.0.1' port = 1234 - qps = 1024.0 await benchmark_queue(qps, ip, port) diff --git a/tests/unit_test/queue/utils.py b/tests/unit_test/queue/utils.py index d1e508fd..ded9f29e 100644 --- a/tests/unit_test/queue/utils.py +++ b/tests/unit_test/queue/utils.py @@ -13,12 +13,12 @@ from llumnix.utils import random_uuid from llumnix.server_info import ServerInfo -from llumnix.queue.utils import init_output_queue_server, QueueType +from llumnix.queue.utils import init_request_output_queue_server, QueueType -def request_output_queue_server(output_queue_type: QueueType): +def request_output_queue_server(request_output_queue_type: QueueType): ip = '127.0.0.1' port = 1234 - output_queue = init_output_queue_server(ip, port, output_queue_type) + output_queue = init_request_output_queue_server(ip, port, request_output_queue_type) server_id = random_uuid() - server_info = ServerInfo(server_id, output_queue_type, output_queue, ip, port) + server_info = ServerInfo(server_id, request_output_queue_type, output_queue, ip, port) return output_queue, server_info diff --git a/tools/bench_test.sh b/tools/bench_test.sh deleted file mode 100755 index ec12cef9..00000000 --- a/tools/bench_test.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -set -ex - -nvidia-docker run --rm -t --net host --ipc host -v ${PWD}:/workspace -v /mnt:/mnt -w /workspace \ - registry.cn-beijing.aliyuncs.com/llumnix/llumnix-dev:20240909_action_678a439 \ - bash -c "pip install -e . > /dev/null && make bench_test" diff --git a/tools/e2e_test.sh b/tools/e2e_test.sh deleted file mode 100755 index 867a8aaf..00000000 --- a/tools/e2e_test.sh +++ /dev/null @@ -1,6 +0,0 @@ -# #!/bin/bash -set -ex - -nvidia-docker run --rm -t --net host --ipc host -v ${PWD}:/workspace -v /mnt:/mnt -w /workspace \ - registry.cn-beijing.aliyuncs.com/llumnix/llumnix-dev:20240909_action_678a439 \ - bash -c "pip install -e . > /dev/null && make e2e_test" diff --git a/tools/migration_test.sh b/tools/migration_test.sh deleted file mode 100755 index 3e13ce55..00000000 --- a/tools/migration_test.sh +++ /dev/null @@ -1,6 +0,0 @@ -# #!/bin/bash -set -ex - -nvidia-docker run --rm -t --net host --ipc host -v ${PWD}:/workspace -v /mnt:/mnt -w /workspace \ - registry.cn-beijing.aliyuncs.com/llumnix/llumnix-dev:20240909_action_678a439 \ - bash -c "pip install -e . > /dev/null && make migration_test" diff --git a/tools/run_test.sh b/tools/run_test.sh new file mode 100755 index 00000000..42350908 --- /dev/null +++ b/tools/run_test.sh @@ -0,0 +1,11 @@ +#!/bin/bash +test_mode=$1 + +set -ex + +pgrep -f llumnix.entrypoints.vllm.api_server | { while read pid; do kill -9 "$pid"; done; } +pgrep -f benchmark_serving.py | { while read pid; do kill -9 "$pid"; done; } + +nvidia-docker run --rm -t --net host --ipc host -v ${PWD}:/workspace -v /mnt:/mnt -w /workspace \ + registry.cn-beijing.aliyuncs.com/llumnix/llumnix-dev:20240909_action_678a439 \ + sh -c "pip install -e . > /dev/null && make $test_mode" diff --git a/tools/unit_test.sh b/tools/unit_test.sh deleted file mode 100755 index 0b075df3..00000000 --- a/tools/unit_test.sh +++ /dev/null @@ -1,6 +0,0 @@ -# #!/bin/bash -set -ex - -nvidia-docker run --rm -t --net host --ipc host -v ${PWD}:/workspace -v /mnt:/mnt -w /workspace \ - registry.cn-beijing.aliyuncs.com/llumnix/llumnix-dev:20240909_action_678a439 \ - bash -c "pip install -e . > /dev/null && make unit_test" From b319b23a197cc25e5e9a740c574c46be17fe9743 Mon Sep 17 00:00:00 2001 From: KuilongCui Date: Wed, 11 Dec 2024 10:59:46 +0800 Subject: [PATCH 10/10] [Bugfix] Prevent new Instances from migrating before migration backend group creation (#73) --- llumnix/arg_utils.py | 3 ++- llumnix/global_scheduler/global_scheduler.py | 3 ++- llumnix/global_scheduler/migration_filter.py | 6 ++++-- llumnix/global_scheduler/migration_scheduler.py | 14 ++++++++++++-- llumnix/internal_config.py | 5 ++++- llumnix/llm_engine_manager.py | 7 +++++++ .../global_scheduler/test_global_scheduler.py | 2 +- .../global_scheduler/test_migration_scheduler.py | 2 +- 8 files changed, 33 insertions(+), 9 deletions(-) diff --git a/llumnix/arg_utils.py b/llumnix/arg_utils.py index ee9f5b20..3a2a25c2 100644 --- a/llumnix/arg_utils.py +++ b/llumnix/arg_utils.py @@ -166,7 +166,8 @@ def create_global_scheduler_configs( self.scaling_policy, self.scale_up_threshold, self.scale_down_threshold, - self.enable_pd_disagg) + self.enable_pd_disagg, + self.migration_backend,) return global_scheduler_config def create_migration_config(self) -> MigrationConfig: diff --git a/llumnix/global_scheduler/global_scheduler.py b/llumnix/global_scheduler/global_scheduler.py index ec1568bb..419dd41a 100644 --- a/llumnix/global_scheduler/global_scheduler.py +++ b/llumnix/global_scheduler/global_scheduler.py @@ -43,7 +43,8 @@ def __init__(self, # migrate args self.migration_scheduler = MigrationScheduler(global_scheduler_config.pair_migration_policy, global_scheduler_config.migrate_out_load_threshold, - self.instance_load_calculator) + self.instance_load_calculator, + global_scheduler_config.migration_backend) # auto-scaling args self.scaling_scheduler = ScalingScheduler(global_scheduler_config.scale_up_threshold, global_scheduler_config.scale_down_threshold, diff --git a/llumnix/global_scheduler/migration_filter.py b/llumnix/global_scheduler/migration_filter.py index ea82e55b..7d0a9574 100644 --- a/llumnix/global_scheduler/migration_filter.py +++ b/llumnix/global_scheduler/migration_filter.py @@ -56,8 +56,10 @@ def get_filter(self, filter_name: str) -> Optional[MigrationFilterPolicy]: def filter_instances(self, instance_infos: List[InstanceInfo], pair_migration_type: PairMigrationConstraints) -> Dict[str, InstanceInfo]: - src_filter_conditions = [filter.filter_src_condition() for filter in self.registered_filters.values()] - dst_filter_conditions = [filter.filter_dst_condition() for filter in self.registered_filters.values()] + src_filter_conditions = [filter.filter_src_condition(self.filter_config, pair_migration_type) + for filter in self.registered_filters.values()] + dst_filter_conditions = [filter.filter_dst_condition(self.filter_config, pair_migration_type) + for filter in self.registered_filters.values()] if pair_migration_type == PairMigrationConstraints.NO_CONSTRAINTS: policy_filter = MigrationFilterPolicyFactory.get_policy("load") diff --git a/llumnix/global_scheduler/migration_scheduler.py b/llumnix/global_scheduler/migration_scheduler.py index ad538f06..9c448ebf 100644 --- a/llumnix/global_scheduler/migration_scheduler.py +++ b/llumnix/global_scheduler/migration_scheduler.py @@ -15,7 +15,7 @@ from llumnix.logger import init_logger from llumnix.instance_info import InstanceInfo, InstanceLoadCalculator -from llumnix.global_scheduler.migration_filter import MigrationInstanceFilter, MigrationFilterConfig +from llumnix.global_scheduler.migration_filter import MigrationInstanceFilter, MigrationFilterConfig, CustomFilter from llumnix.global_scheduler.migration_policy import PairMigrationConstraints, PairMigrationPolicyFactory logger = init_logger(__name__) @@ -24,10 +24,20 @@ class MigrationScheduler: def __init__(self, pair_migration_policy: str, migrate_out_load_threshold: float, - instance_load_calculator: InstanceLoadCalculator) -> None: + instance_load_calculator: InstanceLoadCalculator, + migration_backend: str,) -> None: self.filter_config = MigrationFilterConfig(migrate_out_load_threshold=migrate_out_load_threshold) self.migration_filter = MigrationInstanceFilter(self.filter_config) + # Some migration backends require init_process_group before passing the KV cache. Here, we add a filter + # to prevent instances of migration backends that have not been initialized from participating in migration. + migration_backend_init_filter = CustomFilter() + migration_backend_init_filter.set_filter_condtition( + src_filter=lambda _: migration_backend == 'rpc', + dst_filter=lambda _: migration_backend == 'rpc') + self.migration_filter.register_filter("migration_backend_init_filter", + migration_backend_init_filter) + self.instance_load_calculator = instance_load_calculator self.enable_defrag = instance_load_calculator.enable_defrag if not self.enable_defrag: diff --git a/llumnix/internal_config.py b/llumnix/internal_config.py index d17bbd20..9584f983 100644 --- a/llumnix/internal_config.py +++ b/llumnix/internal_config.py @@ -42,7 +42,8 @@ def __init__( scaling_policy: str, scale_up_threshold: float, scale_down_threshold: float, - enable_pd_disagg: bool) -> None: + enable_pd_disagg: bool, + migration_backend: str,) -> None: self.initial_instances = initial_instances self.load_metric = load_metric @@ -60,3 +61,5 @@ def __init__( self.enable_pd_disagg = enable_pd_disagg self.num_dispatch_instances = num_dispatch_instances + + self.migration_backend = migration_backend diff --git a/llumnix/llm_engine_manager.py b/llumnix/llm_engine_manager.py index 6453745d..93c20f37 100644 --- a/llumnix/llm_engine_manager.py +++ b/llumnix/llm_engine_manager.py @@ -25,6 +25,7 @@ from llumnix.logger import init_logger from llumnix.global_scheduler.global_scheduler import GlobalScheduler from llumnix.global_scheduler.migration_scheduler import PairMigrationConstraints +from llumnix.global_scheduler.migration_filter import CustomFilter from llumnix.instance_info import InstanceInfo from llumnix.internal_config import GlobalSchedulerConfig from llumnix.arg_utils import EngineManagerArgs @@ -330,6 +331,12 @@ async def run_task(alive_instances: List[str], task_name: str, *args, **kwargs): self.pending_rebuild_migration_instances = 0 group_name = None + migration_filter: CustomFilter = self.global_scheduler.migration_scheduler\ + .migration_filter.get_filter("migration_backend_init_filter") + migration_filter.set_filter_condtition( + src_filter=lambda instance_info: instance_info.instance_id in alive_instances, + dst_filter=lambda instance_info: instance_info.instance_id in alive_instances) + logger.info("rebuild {} migrate backend done, group_name: {}, alive instance ({}): {}" .format(self.engine_manager_args.migration_backend, group_name, len(alive_instances), alive_instances)) diff --git a/tests/unit_test/global_scheduler/test_global_scheduler.py b/tests/unit_test/global_scheduler/test_global_scheduler.py index adb1f1cc..18c83f85 100644 --- a/tests/unit_test/global_scheduler/test_global_scheduler.py +++ b/tests/unit_test/global_scheduler/test_global_scheduler.py @@ -25,7 +25,7 @@ def init_global_scheduler(): global_scheduler_config = GlobalSchedulerConfig(0, 'remaining_steps', 'load', math.inf, 'defrag_constrained', 3.0, True, 'avg_load', - 10, 60, False) + 10, 60, False, 'rpc') global_scheduler = GlobalScheduler(global_scheduler_config) return global_scheduler diff --git a/tests/unit_test/global_scheduler/test_migration_scheduler.py b/tests/unit_test/global_scheduler/test_migration_scheduler.py index fa25e1f8..89b813c3 100644 --- a/tests/unit_test/global_scheduler/test_migration_scheduler.py +++ b/tests/unit_test/global_scheduler/test_migration_scheduler.py @@ -27,7 +27,7 @@ def init_migration_scheduler(policy='balanced'): instance_load_calculator = InstanceLoadCalculator('remaining_steps', True) - migration_scheduler = MigrationScheduler(policy, MIGRATE_OUT_LOAD_THRESHOLD, instance_load_calculator) + migration_scheduler = MigrationScheduler(policy, MIGRATE_OUT_LOAD_THRESHOLD, instance_load_calculator, 'rpc') return migration_scheduler @pytest.fixture