From 95396e51c5de42040c8e7039df6f5154ea7a6385 Mon Sep 17 00:00:00 2001 From: z00452769 Date: Thu, 14 Aug 2025 14:45:21 +0800 Subject: [PATCH 01/12] =?UTF-8?q?=E6=9A=82=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/mooncake.json | 4 +- test/mooncake_kv_offload.py | 44 +++ test/test_mooncake1.py | 154 ++++++++ unifiedcache/ucm_connector/ucm_mooncake.py | 84 ++-- .../ucm_connector/ucm_mooncake_bak.py | 358 ++++++++++++++++++ 5 files changed, 582 insertions(+), 62 deletions(-) create mode 100644 test/mooncake_kv_offload.py create mode 100644 test/test_mooncake1.py create mode 100644 unifiedcache/ucm_connector/ucm_mooncake_bak.py diff --git a/test/mooncake.json b/test/mooncake.json index 43d64b66..118ea95f 100644 --- a/test/mooncake.json +++ b/test/mooncake.json @@ -1,7 +1,7 @@ { "local_hostname": "127.0.0.1", - "metadata_server": "http://127.0.0.1:23790/metadata", + "metadata_server": "http://127.0.0.1:33790/metadata", "protocol": "tcp", "device_name": "", - "master_server_address": "127.0.0.1:50001" + "master_server_address": "127.0.0.1:50002" } diff --git a/test/mooncake_kv_offload.py b/test/mooncake_kv_offload.py new file mode 100644 index 00000000..e1726990 --- /dev/null +++ b/test/mooncake_kv_offload.py @@ -0,0 +1,44 @@ +import os +import hashlib +import torch + +from unifiedcache.ucm_connector.ucm_mooncake import UcmMooncakeStore, MooncakeTask +from unifiedcache.logger import init_logger + +logger = init_logger(__name__) + + +def tensor_hash(tensor: torch.Tensor) -> str: + """Calculate the hash value of the tensor.""" + tensor_bytes = tensor.clone().detach().cpu().numpy().tobytes() + hash_object = hashlib.blake2b(tensor_bytes) + hash_hex = hash_object.hexdigest() + return str(int(hash_hex[:16], 16)) + +store = UcmMooncakeStore() +src_block_data = [torch.randint(0, 1000, (1,10), dtype=torch.int) for _ in range(5)] +dst_block_data = [torch.empty(data.shape, dtype=data.dtype) for data in src_block_data] +block_ids = [tensor_hash(data) for data in src_block_data] + +mask = store.lookup(block_ids) +logger.info(f"First lookup: {mask=}") + +task: MooncakeTask = store.dump(block_ids=block_ids, offset=[], src_tensor=src_block_data) +store.wait(task) +logger.info(f"Dump end: {task=}") + +mask = store.lookup(block_ids) +logger.info(f"Second lookup: {mask=}") + +task: MooncakeTask = store.load(block_ids=block_ids, offset=[], dst_tensor=dst_block_data) +store.wait(task) +logger.info(f"Load end: {task=}") + +logger.info("原始张量Hash:") +logger.info(f"{block_ids=}") +logger.info("原始张量:") +logger.info(src_block_data) +logger.info("还原后的张量:") +logger.info(dst_block_data) +logger.info("是否一致:") +logger.info(f"{[torch.equal(src_block_data[i], dst_block_data[i]) for i in range(len(src_block_data))]}") diff --git a/test/test_mooncake1.py b/test/test_mooncake1.py new file mode 100644 index 00000000..a149b5a4 --- /dev/null +++ b/test/test_mooncake1.py @@ -0,0 +1,154 @@ +import hashlib +import uuid + +import torch + +from unifiedcache.logger import init_logger +from unifiedcache.ucm_connector.base import Task +from unifiedcache.ucm_connector.ucm_mooncake import UcmMooncakeStore + +logger = init_logger(__name__) + +mooncake_dict_config = { + "local_hostname": "127.0.0.1", + "metadata_server": "http://127.0.0.1:33790/metadata", + "protocol": "tcp", + "device_name": "", + "master_server_address": "127.0.0.1:50002" +} + +def tensor_hash(tensor: torch.Tensor) -> str: + """Calculate the hash value of the tensor.""" + tensor_bytes = tensor.clone().detach().cpu().numpy().tobytes() + hash_object = hashlib.blake2b(tensor_bytes) + hash_hex = hash_object.hexdigest() + return str(int(hash_hex[:16], 16)) + + +def test_lookup_not_found(): + """Test that lookup returns False for non-existent block IDs.""" + store = UcmMooncakeStore(mooncake_dict_config) + block_ids = [uuid.uuid4().hex for _ in range(10)] + masks = store.lookup(block_ids) + assert all(mask is False for mask in masks) + + +def test_lookup_found(): + """Test that lookup returns True for existing block IDs after dumping data.""" + src_block_data = [ + torch.randint(0, 1000, (1, 100), dtype=torch.int) for _ in range(5) + ] + block_ids = [tensor_hash(data) for data in src_block_data] + offset = [0] * len(block_ids) + + store = UcmMooncakeStore(mooncake_dict_config) + task: Task = store.dump( + block_ids=block_ids, offset=offset, src_tensor=src_block_data + ) + ret = store.wait(task) + assert ret == 0 + masks = store.lookup(block_ids) + assert all(mask is True for mask in masks) + + +def test_dump_once(): + """Test dumping data once and verifying it exists in the store.""" + src_block_data = [ + torch.randint(0, 1000, (1, 100), dtype=torch.int) for _ in range(5) + ] + block_ids = [tensor_hash(data) for data in src_block_data] + offset = [0] * len(block_ids) + + store = UcmMooncakeStore(mooncake_dict_config) + task: Task = store.dump( + block_ids=block_ids, offset=offset, src_tensor=src_block_data + ) + ret = store.wait(task) + assert ret == 0 + masks = store.lookup(block_ids) + assert all(mask is True for mask in masks) + + +def test_dump_repeated(): + """Test that repeated dumping of the same data doesn't cause errors.""" + src_block_data = [ + torch.randint(0, 1000, (1, 100), dtype=torch.int) for _ in range(5) + ] + block_ids = [tensor_hash(data) for data in src_block_data] + offset = [0] * len(block_ids) + + store = UcmMooncakeStore(mooncake_dict_config) + task: Task = store.dump( + block_ids=block_ids, offset=offset, src_tensor=src_block_data + ) + ret = store.wait(task) + assert ret == 0 + masks = store.lookup(block_ids) + assert all(mask is True for mask in masks) + + task: Task = store.dump( + block_ids=block_ids, offset=offset, src_tensor=src_block_data + ) + ret = store.wait(task) + assert ret == 0 + + +def test_load_existing_data(): + """Test loading data that was previously dumped into the store.""" + src_block_data = [ + torch.randint(0, 1000, (1, 100), dtype=torch.int) for _ in range(5) + ] + dst_block_data = [ + torch.empty(data.shape, dtype=data.dtype) for data in src_block_data + ] + block_ids = [tensor_hash(data) for data in src_block_data] + offset = [0] * len(block_ids) + + store = UcmMooncakeStore(mooncake_dict_config) + task: Task = store.dump( + block_ids=block_ids, offset=offset, src_tensor=src_block_data + ) + ret = store.wait(task) + assert ret == 0 + + masks = store.lookup(block_ids) + assert all(mask is True for mask in masks) + + task: Task = store.load( + block_ids=block_ids, offset=offset, dst_tensor=dst_block_data + ) + ret = store.wait(task) + assert ret == 0 + assert all( + [ + torch.equal(src_block_data[i], dst_block_data[i]) is True + for i in range(len(src_block_data)) + ] + ) + + +def test_load_non_existent_data(): + """Test loading data that doesn't exist in the store verifies the destination remains unchanged.""" + src_block_data = [ + torch.randint(0, 1000, (1, 100), dtype=torch.int) for _ in range(5) + ] + dst_block_data = [ + torch.empty(data.shape, dtype=data.dtype) for data in src_block_data + ] + block_ids = [tensor_hash(data) for data in src_block_data] + offset = [0] * len(block_ids) + store = UcmMooncakeStore(mooncake_dict_config) + masks = store.lookup(block_ids) + assert all(mask is False for mask in masks) + + task: Task = store.load( + block_ids=block_ids, offset=offset, dst_tensor=dst_block_data + ) + ret = store.wait(task) + assert ret != 0 + assert all( + [ + torch.equal(src_block_data[i], dst_block_data[i]) is False + for i in range(len(src_block_data)) + ] + ) diff --git a/unifiedcache/ucm_connector/ucm_mooncake.py b/unifiedcache/ucm_connector/ucm_mooncake.py index 385b1bf0..997d0123 100644 --- a/unifiedcache/ucm_connector/ucm_mooncake.py +++ b/unifiedcache/ucm_connector/ucm_mooncake.py @@ -1,10 +1,11 @@ -import asyncio +from dataclasses import dataclass import json import os +from typing import List, Dict, Optional +import asyncio import threading -from concurrent.futures import Future, TimeoutError -from dataclasses import dataclass -from typing import Dict, List +import uuid +from concurrent.futures import TimeoutError, Future import torch from safetensors.torch import load as safetensors_load @@ -33,24 +34,14 @@ class MooncakeStoreConfig: @staticmethod def from_file(file_path: str) -> "MooncakeStoreConfig": - """ - # NOTE: - # This method currently loads connection information from a file. - # In the future, it should be updated to load configuration from a YAML file, - # as the UC connector plans to support YAML-based config. - """ """Load the config from a JSON file.""" with open(file_path) as fin: config = json.load(fin) return MooncakeStoreConfig( local_hostname=config.get("local_hostname"), metadata_server=config.get("metadata_server"), - global_segment_size=config.get( - "global_segment_size", DEFAULT_GLOBAL_SEGMENT_SIZE - ), - local_buffer_size=config.get( - "local_buffer_size", DEFAULT_LOCAL_BUFFER_SIZE - ), + global_segment_size=config.get("global_segment_size", DEFAULT_GLOBAL_SEGMENT_SIZE), + local_buffer_size=config.get("local_buffer_size", DEFAULT_LOCAL_BUFFER_SIZE), protocol=config.get("protocol", "tcp"), device_name=config.get("device_name", ""), master_server_address=config.get("master_server_address"), @@ -61,16 +52,13 @@ def load_from_env() -> "MooncakeStoreConfig": """Load config from a file specified in the environment variable.""" config_file_path = os.getenv("MOONCAKE_CONFIG_PATH") if config_file_path is None: - raise ValueError( - "The environment variable 'MOONCAKE_CONFIG_PATH' is not set." - ) + raise ValueError("The environment variable 'MOONCAKE_CONFIG_PATH' is not set.") return MooncakeStoreConfig.from_file(config_file_path) @dataclass class MooncakeTask(Task): """A task class for Mooncake operations with a task identifier.""" - task_id: int = -1 @@ -112,7 +100,7 @@ def __init__(self, config: Dict = {}): except Exception as exc: logger.error("An error occurred while loading the configuration: %s", exc) raise - + # Task management variables self.task_id: int = 0 self.tasks: Dict[int, Future] = {} @@ -121,7 +109,7 @@ def __init__(self, config: Dict = {}): self.loop = asyncio.new_event_loop() self.lock = threading.Lock() self._shutting_down = threading.Event() - + # Start the event loop thread self.thread = threading.Thread(target=self._run_event_loop, daemon=True) self.thread.start() @@ -147,7 +135,7 @@ def lookup(self, block_ids: List[str]) -> List[bool]: """ Get number of blocks that can be loaded from the external KV cache. - Mooncake integration uses hash = block_id + offset (default offset=0 if not provided). + Args: block_ids (List[str]): vLLM block hash. @@ -157,10 +145,7 @@ def lookup(self, block_ids: List[str]) -> List[bool]: if self._shutting_down.is_set(): raise RuntimeError("UcmMooncakeStore is shutting down.") - mask = [ - True if self.store.is_exist(f"{block_key}_0") == 1 else False - for block_key in block_ids - ] + mask = [True if self.store.is_exist(block_key) == 1 else False for block_key in block_ids] return mask def prefetch(self, block_ids: List[str]) -> None: @@ -173,12 +158,9 @@ def prefetch(self, block_ids: List[str]) -> None: # Mooncake only has get and put interfaces, this operation is not supported pass - def load( - self, block_ids: List[str], offset: List[int], dst_tensor: List[torch.Tensor] - ) -> Task: + def load(self, block_ids: List[str], offset: List[int], dst_tensor: List[torch.Tensor]) -> Task: """ load kv cache to device. - Mooncake integration uses hash = block_id + offset (default offset=0 if not provided). Args: block_ids (List[str]): vLLM block hash. @@ -197,17 +179,12 @@ def load( self.tasks[self.task_id] = future return MooncakeTask(task_id=self.task_id) - async def _load_impl( - self, block_ids: List[str], offset: List[int], dst_tensor: List[torch.Tensor] - ) -> int: + async def _load_impl(self, block_ids: List[str], offset: List[int], dst_tensor: List[torch.Tensor]) -> Task: """Internal implementation of loading KV cache from Mooncake Store.""" - assert len(block_ids) == len( - dst_tensor - ), "block_ids and dst_tensor have different lengths, please check!" + assert len(block_ids) == len(dst_tensor), "block_ids and dst_tensor have different lengths, please check!" for i in range(len(block_ids)): try: - block_hash = f"{block_ids[i]}_{offset[i]}" - data = self.store.get(block_hash) + data = self.store.get(block_ids[i]) except TypeError as err: logger.error("Failed to get value from Mooncake Store: %s", err) raise TypeError("Mooncake Store Get Type Error.") from err @@ -218,16 +195,10 @@ async def _load_impl( assert dst_tensor[i].shape == tensor_cpu.shape assert dst_tensor[i].dtype == tensor_cpu.dtype dst_tensor[i].copy_(tensor_cpu) - else: - return 1 - return 0 - def dump( - self, block_ids: List[str], offset: List[int], src_tensor: List[torch.Tensor] - ) -> Task: + def dump(self, block_ids: List[str], offset: List[int], src_tensor: List[torch.Tensor]) -> Task: """ dump kv cache to device. - Mooncake integration uses hash = block_id + offset (default offset=0 if not provided). Args: block_ids (List[str]): vLLM block hash. @@ -246,24 +217,16 @@ def dump( self.tasks[self.task_id] = future return MooncakeTask(task_id=self.task_id) - async def _dump_impl( - self, block_ids: List[str], offset: List[int], src_tensor: List[torch.Tensor] - ) -> int: + async def _dump_impl(self, block_ids: List[str], offset: List[int], src_tensor: List[torch.Tensor]) -> Task: """Internal implementation of dumping KV cache to Mooncake Store.""" - assert len(block_ids) == len( - src_tensor - ), "block_ids and src_tensor have different lengths, please check!" + assert len(block_ids) == len(src_tensor), "block_ids and src_tensor have different lengths, please check!" for i in range(len(block_ids)): value_bytes = safetensors_save({"tensor": src_tensor[i]}) try: - block_hash = f"{block_ids[i]}_{offset[i]}" - ret = self.store.put(block_hash, value_bytes) - if ret != 0: - return ret + self.store.put(block_ids[i], value_bytes) except TypeError as err: logger.error("Failed to put value into Mooncake Store: %s", err) raise TypeError("Mooncake Store Put Type Error.") from err - return 0 def wait(self, task: Task) -> int: """ @@ -284,19 +247,20 @@ def wait(self, task: Task) -> int: return 1 try: - ret = future.result(TIMEOUT_S_THR) - return ret + future.result(TIMEOUT_S_THR) except TimeoutError: # Cancel the task if it times out future.cancel() logger.error(f"Task {task.task_id} timed out after {TIMEOUT_S_THR}s") return 1 + except asyncio.CancelledError: logger.error(f"Task {task.task_id} was cancelled") return 1 except Exception as e: logger.error(f"Task {task.task_id} failed: {str(e)}") return 1 + return 0 def commit(self, block_ids: List[str], is_success: bool = True) -> None: """ @@ -335,5 +299,5 @@ def shutdown(self): # Force close the loop if thread didn't exit if not self.loop.is_closed(): self.loop.close() - + self.store.close() diff --git a/unifiedcache/ucm_connector/ucm_mooncake_bak.py b/unifiedcache/ucm_connector/ucm_mooncake_bak.py new file mode 100644 index 00000000..53a9e6dc --- /dev/null +++ b/unifiedcache/ucm_connector/ucm_mooncake_bak.py @@ -0,0 +1,358 @@ +import asyncio +import json +import os +import threading +from concurrent.futures import Future, TimeoutError +from dataclasses import dataclass +from typing import Dict, List + +import torch +from safetensors.torch import load as safetensors_load +from safetensors.torch import save as safetensors_save + +from unifiedcache.logger import init_logger +from unifiedcache.ucm_connector import Task, UcmKVStoreBase + +TIMEOUT_S_THR: int = 60 * 60 +DEFAULT_GLOBAL_SEGMENT_SIZE: int = 1 * 1024 * 1024 * 1024 # 3.125 GiB +DEFAULT_LOCAL_BUFFER_SIZE: int = 1 * 1024 * 1024 * 1024 # 1.0 GiB + +logger = init_logger(__name__) + + +# TODO To keep it consistent with the vllm source code(vllm/distributed/kv_transfer/kv_lookup_buffer/mooncake_store.py), the source code is fully reused here. The code here will be deleted after vllm is implemented. +@dataclass +class MooncakeStoreConfig: + local_hostname: str + metadata_server: str + global_segment_size: int + local_buffer_size: int + protocol: str + device_name: str + master_server_address: str + + @staticmethod + def load_from_dict(config: Dict = {}) -> "MooncakeStoreConfig": + return MooncakeStoreConfig( + local_hostname=config.get("local_hostname"), + metadata_server=config.get("metadata_server"), + global_segment_size=config.get( + "global_segment_size", DEFAULT_GLOBAL_SEGMENT_SIZE + ), + local_buffer_size=config.get( + "local_buffer_size", DEFAULT_LOCAL_BUFFER_SIZE + ), + protocol=config.get("protocol", "tcp"), + device_name=config.get("device_name", ""), + master_server_address=config.get("master_server_address"), + ) + + @staticmethod + def from_file(file_path: str) -> "MooncakeStoreConfig": + """ + # NOTE: + # This method currently loads connection information from a file. + # In the future, it should be updated to load configuration from a YAML file, + # as the UC connector plans to support YAML-based config. + """ + """Load the config from a JSON file.""" + with open(file_path) as fin: + config = json.load(fin) + return MooncakeStoreConfig( + local_hostname=config.get("local_hostname"), + metadata_server=config.get("metadata_server"), + global_segment_size=config.get( + "global_segment_size", DEFAULT_GLOBAL_SEGMENT_SIZE + ), + local_buffer_size=config.get( + "local_buffer_size", DEFAULT_LOCAL_BUFFER_SIZE + ), + protocol=config.get("protocol", "tcp"), + device_name=config.get("device_name", ""), + master_server_address=config.get("master_server_address"), + ) + + @staticmethod + def load_from_env() -> "MooncakeStoreConfig": + """Load config from a file specified in the environment variable.""" + config_file_path = os.getenv("MOONCAKE_CONFIG_PATH") + if config_file_path is None: + raise ValueError( + "The environment variable 'MOONCAKE_CONFIG_PATH' is not set." + ) + return MooncakeStoreConfig.from_file(config_file_path) + + +@dataclass +class MooncakeTask(Task): + """A task class for Mooncake operations with a task identifier.""" + + task_id: int = -1 + + +class UcmMooncakeStore(UcmKVStoreBase): + """ + A wrapper class for MooncakeDistributedStore that implements the UcmKVStoreBase interface. + Provides key-value store functionality for vLLM using Mooncake as the backend. + """ + + def __init__(self, config: Dict = {}): + """Initialize the Mooncake store with configuration.""" + super().__init__(config) + try: + from mooncake.store import MooncakeDistributedStore + except ImportError as e: + raise ImportError( + "Please install mooncake by following the instructions at " + "https://github.com/kvcache-ai/Mooncake/blob/main/doc/en/build.md " # noqa: E501 + "to run vLLM with MooncakeConnector." + ) from e + + try: + self.store = MooncakeDistributedStore() + if config != {}: + mooncake_config = MooncakeStoreConfig.load_from_dict(config) + else: + mooncake_config = MooncakeStoreConfig.load_from_env() + logger.info("Mooncake Configuration loaded successfully.") + self.store.setup( + mooncake_config.local_hostname, + mooncake_config.metadata_server, + mooncake_config.global_segment_size, + mooncake_config.local_buffer_size, + mooncake_config.protocol, + mooncake_config.device_name, + mooncake_config.master_server_address, + ) + + except ValueError as e: + logger.error("Configuration loading failed: %s", e) + raise + except Exception as exc: + logger.error("An error occurred while loading the configuration: %s", exc) + raise + + # Task management variables + self.task_id: int = 0 + self.tasks: Dict[int, Future] = {} + + # Threading and synchronization variables + self.loop = asyncio.new_event_loop() + self.lock = threading.Lock() + self._shutting_down = threading.Event() + + # Start the event loop thread + self.thread = threading.Thread(target=self._run_event_loop, daemon=True) + self.thread.start() + + def _run_event_loop(self): + """Run the asyncio event loop in a separate thread.""" + asyncio.set_event_loop(self.loop) + self.loop.run_forever() + + def create(self, block_ids: List[str]) -> int: + """ + create kv cache space in storafe (not implemented for Mooncake). + + Args: + block_ids (List[str]): vLLM block hash. + Returns: + Always returns 0 as this operation is not supported by Mooncake + """ + # Mooncake only has get and put interfaces, this operation is not supported + return 0 + + def lookup(self, block_ids: List[str]) -> List[bool]: + """ + Get number of blocks that can be loaded from the + external KV cache. + Mooncake integration uses hash = block_id + offset (default offset=0 if not provided). + Args: + block_ids (List[str]): vLLM block hash. + + Returns: + hit block mask, True -> hit + """ + if self._shutting_down.is_set(): + raise RuntimeError("UcmMooncakeStore is shutting down.") + + mask = [ + True if self.store.is_exist(f"{block_key}_0") == 1 else False + for block_key in block_ids + ] + return mask + + def prefetch(self, block_ids: List[str]) -> None: + """ + prefetch kv cache to high speed cache according to block_ids (not implemented for Mooncake). + + Args: + block_ids (List[str]): vLLM block hash. + """ + # Mooncake only has get and put interfaces, this operation is not supported + pass + + def load( + self, block_ids: List[str], offset: List[int], dst_tensor: List[torch.Tensor] + ) -> Task: + """ + load kv cache to device. + Mooncake integration uses hash = block_id + offset (default offset=0 if not provided). + + Args: + block_ids (List[str]): vLLM block hash. + offset(List[int]): tp > 1 scene + dst_tensor: List[torch.Tensor]: device tensor addr. + Returns: + task(Task). + """ + if self._shutting_down.is_set(): + raise RuntimeError("UcmMooncakeStore is shutting down.") + + coro = self._load_impl(block_ids, offset, dst_tensor) + future = asyncio.run_coroutine_threadsafe(coro, self.loop) + with self.lock: + self.task_id += 1 + self.tasks[self.task_id] = future + return MooncakeTask(task_id=self.task_id) + + async def _load_impl( + self, block_ids: List[str], offset: List[int], dst_tensor: List[torch.Tensor] + ) -> int: + """Internal implementation of loading KV cache from Mooncake Store.""" + assert len(block_ids) == len( + dst_tensor + ), "block_ids and dst_tensor have different lengths, please check!" + for i in range(len(block_ids)): + try: + block_hash = f"{block_ids[i]}_{offset[i]}" + data = self.store.get(block_hash) + except TypeError as err: + logger.error("Failed to get value from Mooncake Store: %s", err) + raise TypeError("Mooncake Store Get Type Error.") from err + + if data: + loaded_tensors = safetensors_load(data) + tensor_cpu = loaded_tensors["tensor"] + assert dst_tensor[i].shape == tensor_cpu.shape + assert dst_tensor[i].dtype == tensor_cpu.dtype + dst_tensor[i].copy_(tensor_cpu) + else: + return 1 + return 0 + + def dump( + self, block_ids: List[str], offset: List[int], src_tensor: List[torch.Tensor] + ) -> Task: + """ + dump kv cache to device. + Mooncake integration uses hash = block_id + offset (default offset=0 if not provided). + + Args: + block_ids (List[str]): vLLM block hash. + offset(List[int]): tp > 1 scene + src_tensor: List[torch.Tensor]: device tensor addr. + Returns: + task(Task). + """ + if self._shutting_down.is_set(): + raise RuntimeError("UcmMooncakeStore is shutting down.") + + coro = self._dump_impl(block_ids, offset, src_tensor) + future = asyncio.run_coroutine_threadsafe(coro, self.loop) + with self.lock: + self.task_id += 1 + self.tasks[self.task_id] = future + return MooncakeTask(task_id=self.task_id) + + async def _dump_impl( + self, block_ids: List[str], offset: List[int], src_tensor: List[torch.Tensor] + ) -> int: + """Internal implementation of dumping KV cache to Mooncake Store.""" + assert len(block_ids) == len( + src_tensor + ), "block_ids and src_tensor have different lengths, please check!" + for i in range(len(block_ids)): + value_bytes = safetensors_save({"tensor": src_tensor[i]}) + try: + block_hash = f"{block_ids[i]}_{offset[i]}" + ret = self.store.put(block_hash, value_bytes) + if ret != 0: + return ret + except TypeError as err: + logger.error("Failed to put value into Mooncake Store: %s", err) + raise TypeError("Mooncake Store Put Type Error.") from err + return 0 + + def wait(self, task: Task) -> int: + """ + wait kv cache kv transfer task finished. + + Args: + task (Task): transfer engine task. + Returns: + 0 - success + others - failed. + """ + # Safely retrieve the Future object + with self.lock: + future = self.tasks.pop(task.task_id, None) + + if future is None: + logger.error(f"Invalid task ID: {task.task_id}") + return 1 + + try: + ret = future.result(TIMEOUT_S_THR) + return ret + except TimeoutError: + # Cancel the task if it times out + future.cancel() + logger.error(f"Task {task.task_id} timed out after {TIMEOUT_S_THR}s") + return 1 + except asyncio.CancelledError: + logger.error(f"Task {task.task_id} was cancelled") + return 1 + except Exception as e: + logger.error(f"Task {task.task_id} failed: {str(e)}") + return 1 + + def commit(self, block_ids: List[str], is_success: bool = True) -> None: + """ + commit kv cache, now kv cache can be reused (not implemented for Mooncake). + + Args: + block_ids (List[str]): vLLM block hash. + is_success(bool): if False, we need release block + """ + # Mooncake only has get and put interfaces, this operation is not supported + pass + + def shutdown(self): + """Safely shutdown all components of the store.""" + if self._shutting_down.is_set(): + return + + self._shutting_down.set() + + # Safely cancel all pending tasks (atomic operation) + with self.lock: + tasks_to_cancel = list(self.tasks.values()) + self.tasks.clear() + + for future in tasks_to_cancel: + if not future.done(): + future.cancel() + + # Stop the event loop + self.loop.call_soon_threadsafe(self.loop.stop) + + # Wait for thread termination + if self.thread.is_alive(): + self.thread.join(TIMEOUT_S_THR) + + # Force close the loop if thread didn't exit + if not self.loop.is_closed(): + self.loop.close() + + self.store.close() From 5daf6fe7ed60fa8427c8397ee47263508f74a648 Mon Sep 17 00:00:00 2001 From: propanone1006 <1035097916@qq.com> Date: Thu, 14 Aug 2025 17:22:29 +0800 Subject: [PATCH 02/12] [Feature] Monncake connector support both config and file --- test/mooncake.json | 4 +- test/mooncake_kv_offload.py | 25 +- ...est_mooncake1.py => test_mooncake_dict.py} | 5 +- test/test_mooncake_env.py | 147 +++++++ unifiedcache/ucm_connector/ucm_mooncake.py | 106 ++++-- .../ucm_connector/ucm_mooncake_bak.py | 358 ------------------ 6 files changed, 248 insertions(+), 397 deletions(-) rename test/{test_mooncake1.py => test_mooncake_dict.py} (97%) create mode 100644 test/test_mooncake_env.py delete mode 100644 unifiedcache/ucm_connector/ucm_mooncake_bak.py diff --git a/test/mooncake.json b/test/mooncake.json index 118ea95f..43d64b66 100644 --- a/test/mooncake.json +++ b/test/mooncake.json @@ -1,7 +1,7 @@ { "local_hostname": "127.0.0.1", - "metadata_server": "http://127.0.0.1:33790/metadata", + "metadata_server": "http://127.0.0.1:23790/metadata", "protocol": "tcp", "device_name": "", - "master_server_address": "127.0.0.1:50002" + "master_server_address": "127.0.0.1:50001" } diff --git a/test/mooncake_kv_offload.py b/test/mooncake_kv_offload.py index e1726990..583887f4 100644 --- a/test/mooncake_kv_offload.py +++ b/test/mooncake_kv_offload.py @@ -1,9 +1,10 @@ -import os import hashlib + import torch -from unifiedcache.ucm_connector.ucm_mooncake import UcmMooncakeStore, MooncakeTask from unifiedcache.logger import init_logger +from unifiedcache.ucm_connector.base import Task +from unifiedcache.ucm_connector.ucm_mooncake import UcmMooncakeStore logger = init_logger(__name__) @@ -15,24 +16,26 @@ def tensor_hash(tensor: torch.Tensor) -> str: hash_hex = hash_object.hexdigest() return str(int(hash_hex[:16], 16)) + store = UcmMooncakeStore() -src_block_data = [torch.randint(0, 1000, (1,10), dtype=torch.int) for _ in range(5)] +src_block_data = [torch.randint(0, 1000, (1, 10), dtype=torch.int) for _ in range(5)] dst_block_data = [torch.empty(data.shape, dtype=data.dtype) for data in src_block_data] block_ids = [tensor_hash(data) for data in src_block_data] +offset = [0] * len(block_ids) mask = store.lookup(block_ids) logger.info(f"First lookup: {mask=}") -task: MooncakeTask = store.dump(block_ids=block_ids, offset=[], src_tensor=src_block_data) -store.wait(task) -logger.info(f"Dump end: {task=}") +task: Task = store.dump(block_ids=block_ids, offset=offset, src_tensor=src_block_data) +ret = store.wait(task) +logger.info(f"Dump end: {task=} with return: {ret}") mask = store.lookup(block_ids) logger.info(f"Second lookup: {mask=}") -task: MooncakeTask = store.load(block_ids=block_ids, offset=[], dst_tensor=dst_block_data) -store.wait(task) -logger.info(f"Load end: {task=}") +task: Task = store.load(block_ids=block_ids, offset=offset, dst_tensor=dst_block_data) +ret = store.wait(task) +logger.info(f"Load end: {task=} with return: {ret}") logger.info("原始张量Hash:") logger.info(f"{block_ids=}") @@ -41,4 +44,6 @@ def tensor_hash(tensor: torch.Tensor) -> str: logger.info("还原后的张量:") logger.info(dst_block_data) logger.info("是否一致:") -logger.info(f"{[torch.equal(src_block_data[i], dst_block_data[i]) for i in range(len(src_block_data))]}") +logger.info( + f"{[torch.equal(src_block_data[i], dst_block_data[i]) for i in range(len(src_block_data))]}" +) diff --git a/test/test_mooncake1.py b/test/test_mooncake_dict.py similarity index 97% rename from test/test_mooncake1.py rename to test/test_mooncake_dict.py index a149b5a4..37bc7972 100644 --- a/test/test_mooncake1.py +++ b/test/test_mooncake_dict.py @@ -11,12 +11,13 @@ mooncake_dict_config = { "local_hostname": "127.0.0.1", - "metadata_server": "http://127.0.0.1:33790/metadata", + "metadata_server": "http://127.0.0.1:23790/metadata", "protocol": "tcp", "device_name": "", - "master_server_address": "127.0.0.1:50002" + "master_server_address": "127.0.0.1:50001", } + def tensor_hash(tensor: torch.Tensor) -> str: """Calculate the hash value of the tensor.""" tensor_bytes = tensor.clone().detach().cpu().numpy().tobytes() diff --git a/test/test_mooncake_env.py b/test/test_mooncake_env.py new file mode 100644 index 00000000..1b57d238 --- /dev/null +++ b/test/test_mooncake_env.py @@ -0,0 +1,147 @@ +import hashlib +import uuid + +import torch + +from unifiedcache.logger import init_logger +from unifiedcache.ucm_connector.base import Task +from unifiedcache.ucm_connector.ucm_mooncake import UcmMooncakeStore + +logger = init_logger(__name__) + + +def tensor_hash(tensor: torch.Tensor) -> str: + """Calculate the hash value of the tensor.""" + tensor_bytes = tensor.clone().detach().cpu().numpy().tobytes() + hash_object = hashlib.blake2b(tensor_bytes) + hash_hex = hash_object.hexdigest() + return str(int(hash_hex[:16], 16)) + + +def test_lookup_not_found(): + """Test that lookup returns False for non-existent block IDs.""" + store = UcmMooncakeStore() + block_ids = [uuid.uuid4().hex for _ in range(10)] + masks = store.lookup(block_ids) + assert all(mask is False for mask in masks) + + +def test_lookup_found(): + """Test that lookup returns True for existing block IDs after dumping data.""" + src_block_data = [ + torch.randint(0, 1000, (1, 100), dtype=torch.int) for _ in range(5) + ] + block_ids = [tensor_hash(data) for data in src_block_data] + offset = [0] * len(block_ids) + + store = UcmMooncakeStore() + task: Task = store.dump( + block_ids=block_ids, offset=offset, src_tensor=src_block_data + ) + ret = store.wait(task) + assert ret == 0 + masks = store.lookup(block_ids) + assert all(mask is True for mask in masks) + + +def test_dump_once(): + """Test dumping data once and verifying it exists in the store.""" + src_block_data = [ + torch.randint(0, 1000, (1, 100), dtype=torch.int) for _ in range(5) + ] + block_ids = [tensor_hash(data) for data in src_block_data] + offset = [0] * len(block_ids) + + store = UcmMooncakeStore() + task: Task = store.dump( + block_ids=block_ids, offset=offset, src_tensor=src_block_data + ) + ret = store.wait(task) + assert ret == 0 + masks = store.lookup(block_ids) + assert all(mask is True for mask in masks) + + +def test_dump_repeated(): + """Test that repeated dumping of the same data doesn't cause errors.""" + src_block_data = [ + torch.randint(0, 1000, (1, 100), dtype=torch.int) for _ in range(5) + ] + block_ids = [tensor_hash(data) for data in src_block_data] + offset = [0] * len(block_ids) + + store = UcmMooncakeStore() + task: Task = store.dump( + block_ids=block_ids, offset=offset, src_tensor=src_block_data + ) + ret = store.wait(task) + assert ret == 0 + masks = store.lookup(block_ids) + assert all(mask is True for mask in masks) + + task: Task = store.dump( + block_ids=block_ids, offset=offset, src_tensor=src_block_data + ) + ret = store.wait(task) + assert ret == 0 + + +def test_load_existing_data(): + """Test loading data that was previously dumped into the store.""" + src_block_data = [ + torch.randint(0, 1000, (1, 100), dtype=torch.int) for _ in range(5) + ] + dst_block_data = [ + torch.empty(data.shape, dtype=data.dtype) for data in src_block_data + ] + block_ids = [tensor_hash(data) for data in src_block_data] + offset = [0] * len(block_ids) + + store = UcmMooncakeStore() + task: Task = store.dump( + block_ids=block_ids, offset=offset, src_tensor=src_block_data + ) + ret = store.wait(task) + assert ret == 0 + + masks = store.lookup(block_ids) + assert all(mask is True for mask in masks) + + task: Task = store.load( + block_ids=block_ids, offset=offset, dst_tensor=dst_block_data + ) + ret = store.wait(task) + assert ret == 0 + assert all( + [ + torch.equal(src_block_data[i], dst_block_data[i]) is True + for i in range(len(src_block_data)) + ] + ) + + +def test_load_non_existent_data(): + """Test loading data that doesn't exist in the store verifies the destination remains unchanged.""" + src_block_data = [ + torch.randint(0, 1000, (1, 100), dtype=torch.int) for _ in range(5) + ] + dst_block_data = [ + torch.empty(data.shape, dtype=data.dtype) for data in src_block_data + ] + block_ids = [tensor_hash(data) for data in src_block_data] + offset = [0] * len(block_ids) + store = UcmMooncakeStore() + masks = store.lookup(block_ids) + assert all(mask is False for mask in masks) + + task: Task = store.load( + block_ids=block_ids, offset=offset, dst_tensor=dst_block_data + ) + ret = store.wait(task) + assert ret != 0 + assert all( + [ + torch.equal(src_block_data[i], dst_block_data[i]) is False + for i in range(len(src_block_data)) + ] + ) diff --git a/unifiedcache/ucm_connector/ucm_mooncake.py b/unifiedcache/ucm_connector/ucm_mooncake.py index 997d0123..b838c59a 100644 --- a/unifiedcache/ucm_connector/ucm_mooncake.py +++ b/unifiedcache/ucm_connector/ucm_mooncake.py @@ -1,11 +1,10 @@ -from dataclasses import dataclass +import asyncio import json import os -from typing import List, Dict, Optional -import asyncio import threading -import uuid -from concurrent.futures import TimeoutError, Future +from concurrent.futures import Future, TimeoutError +from dataclasses import dataclass +from typing import Dict, List import torch from safetensors.torch import load as safetensors_load @@ -32,16 +31,43 @@ class MooncakeStoreConfig: device_name: str master_server_address: str + @staticmethod + def load_from_dict(config: Dict = {}) -> "MooncakeStoreConfig": + """Load the config from dict.""" + return MooncakeStoreConfig( + local_hostname=config.get("local_hostname"), + metadata_server=config.get("metadata_server"), + global_segment_size=config.get( + "global_segment_size", DEFAULT_GLOBAL_SEGMENT_SIZE + ), + local_buffer_size=config.get( + "local_buffer_size", DEFAULT_LOCAL_BUFFER_SIZE + ), + protocol=config.get("protocol", "tcp"), + device_name=config.get("device_name", ""), + master_server_address=config.get("master_server_address"), + ) + @staticmethod def from_file(file_path: str) -> "MooncakeStoreConfig": + """ + # NOTE: + # This method currently loads connection information from a file. + # In the future, it should be updated to load configuration from a YAML file, + # as the UC connector plans to support YAML-based config. + """ """Load the config from a JSON file.""" with open(file_path) as fin: config = json.load(fin) return MooncakeStoreConfig( local_hostname=config.get("local_hostname"), metadata_server=config.get("metadata_server"), - global_segment_size=config.get("global_segment_size", DEFAULT_GLOBAL_SEGMENT_SIZE), - local_buffer_size=config.get("local_buffer_size", DEFAULT_LOCAL_BUFFER_SIZE), + global_segment_size=config.get( + "global_segment_size", DEFAULT_GLOBAL_SEGMENT_SIZE + ), + local_buffer_size=config.get( + "local_buffer_size", DEFAULT_LOCAL_BUFFER_SIZE + ), protocol=config.get("protocol", "tcp"), device_name=config.get("device_name", ""), master_server_address=config.get("master_server_address"), @@ -52,13 +78,16 @@ def load_from_env() -> "MooncakeStoreConfig": """Load config from a file specified in the environment variable.""" config_file_path = os.getenv("MOONCAKE_CONFIG_PATH") if config_file_path is None: - raise ValueError("The environment variable 'MOONCAKE_CONFIG_PATH' is not set.") + raise ValueError( + "The environment variable 'MOONCAKE_CONFIG_PATH' is not set." + ) return MooncakeStoreConfig.from_file(config_file_path) @dataclass class MooncakeTask(Task): """A task class for Mooncake operations with a task identifier.""" + task_id: int = -1 @@ -82,7 +111,10 @@ def __init__(self, config: Dict = {}): try: self.store = MooncakeDistributedStore() - mooncake_config = MooncakeStoreConfig.load_from_env() + if config != {}: + mooncake_config = MooncakeStoreConfig.load_from_dict(config) + else: + mooncake_config = MooncakeStoreConfig.load_from_env() logger.info("Mooncake Configuration loaded successfully.") self.store.setup( mooncake_config.local_hostname, @@ -100,7 +132,7 @@ def __init__(self, config: Dict = {}): except Exception as exc: logger.error("An error occurred while loading the configuration: %s", exc) raise - + # Task management variables self.task_id: int = 0 self.tasks: Dict[int, Future] = {} @@ -109,7 +141,7 @@ def __init__(self, config: Dict = {}): self.loop = asyncio.new_event_loop() self.lock = threading.Lock() self._shutting_down = threading.Event() - + # Start the event loop thread self.thread = threading.Thread(target=self._run_event_loop, daemon=True) self.thread.start() @@ -135,7 +167,7 @@ def lookup(self, block_ids: List[str]) -> List[bool]: """ Get number of blocks that can be loaded from the external KV cache. - + Mooncake integration uses hash = block_id + offset (default offset=0 if not provided). Args: block_ids (List[str]): vLLM block hash. @@ -145,7 +177,10 @@ def lookup(self, block_ids: List[str]) -> List[bool]: if self._shutting_down.is_set(): raise RuntimeError("UcmMooncakeStore is shutting down.") - mask = [True if self.store.is_exist(block_key) == 1 else False for block_key in block_ids] + mask = [ + True if self.store.is_exist(f"{block_key}_0") == 1 else False + for block_key in block_ids + ] return mask def prefetch(self, block_ids: List[str]) -> None: @@ -158,9 +193,12 @@ def prefetch(self, block_ids: List[str]) -> None: # Mooncake only has get and put interfaces, this operation is not supported pass - def load(self, block_ids: List[str], offset: List[int], dst_tensor: List[torch.Tensor]) -> Task: + def load( + self, block_ids: List[str], offset: List[int], dst_tensor: List[torch.Tensor] + ) -> Task: """ load kv cache to device. + Mooncake integration uses hash = block_id + offset (default offset=0 if not provided). Args: block_ids (List[str]): vLLM block hash. @@ -179,12 +217,17 @@ def load(self, block_ids: List[str], offset: List[int], dst_tensor: List[torch.T self.tasks[self.task_id] = future return MooncakeTask(task_id=self.task_id) - async def _load_impl(self, block_ids: List[str], offset: List[int], dst_tensor: List[torch.Tensor]) -> Task: + async def _load_impl( + self, block_ids: List[str], offset: List[int], dst_tensor: List[torch.Tensor] + ) -> int: """Internal implementation of loading KV cache from Mooncake Store.""" - assert len(block_ids) == len(dst_tensor), "block_ids and dst_tensor have different lengths, please check!" + assert len(block_ids) == len( + dst_tensor + ), "block_ids and dst_tensor have different lengths, please check!" for i in range(len(block_ids)): try: - data = self.store.get(block_ids[i]) + block_hash = f"{block_ids[i]}_{offset[i]}" + data = self.store.get(block_hash) except TypeError as err: logger.error("Failed to get value from Mooncake Store: %s", err) raise TypeError("Mooncake Store Get Type Error.") from err @@ -195,10 +238,16 @@ async def _load_impl(self, block_ids: List[str], offset: List[int], dst_tensor: assert dst_tensor[i].shape == tensor_cpu.shape assert dst_tensor[i].dtype == tensor_cpu.dtype dst_tensor[i].copy_(tensor_cpu) + else: + return 1 + return 0 - def dump(self, block_ids: List[str], offset: List[int], src_tensor: List[torch.Tensor]) -> Task: + def dump( + self, block_ids: List[str], offset: List[int], src_tensor: List[torch.Tensor] + ) -> Task: """ dump kv cache to device. + Mooncake integration uses hash = block_id + offset (default offset=0 if not provided). Args: block_ids (List[str]): vLLM block hash. @@ -217,16 +266,24 @@ def dump(self, block_ids: List[str], offset: List[int], src_tensor: List[torch.T self.tasks[self.task_id] = future return MooncakeTask(task_id=self.task_id) - async def _dump_impl(self, block_ids: List[str], offset: List[int], src_tensor: List[torch.Tensor]) -> Task: + async def _dump_impl( + self, block_ids: List[str], offset: List[int], src_tensor: List[torch.Tensor] + ) -> int: """Internal implementation of dumping KV cache to Mooncake Store.""" - assert len(block_ids) == len(src_tensor), "block_ids and src_tensor have different lengths, please check!" + assert len(block_ids) == len( + src_tensor + ), "block_ids and src_tensor have different lengths, please check!" for i in range(len(block_ids)): value_bytes = safetensors_save({"tensor": src_tensor[i]}) try: - self.store.put(block_ids[i], value_bytes) + block_hash = f"{block_ids[i]}_{offset[i]}" + ret = self.store.put(block_hash, value_bytes) + if ret != 0: + return ret except TypeError as err: logger.error("Failed to put value into Mooncake Store: %s", err) raise TypeError("Mooncake Store Put Type Error.") from err + return 0 def wait(self, task: Task) -> int: """ @@ -247,20 +304,19 @@ def wait(self, task: Task) -> int: return 1 try: - future.result(TIMEOUT_S_THR) + ret = future.result(TIMEOUT_S_THR) + return ret except TimeoutError: # Cancel the task if it times out future.cancel() logger.error(f"Task {task.task_id} timed out after {TIMEOUT_S_THR}s") return 1 - except asyncio.CancelledError: logger.error(f"Task {task.task_id} was cancelled") return 1 except Exception as e: logger.error(f"Task {task.task_id} failed: {str(e)}") return 1 - return 0 def commit(self, block_ids: List[str], is_success: bool = True) -> None: """ @@ -299,5 +355,5 @@ def shutdown(self): # Force close the loop if thread didn't exit if not self.loop.is_closed(): self.loop.close() - + self.store.close() diff --git a/unifiedcache/ucm_connector/ucm_mooncake_bak.py b/unifiedcache/ucm_connector/ucm_mooncake_bak.py deleted file mode 100644 index 53a9e6dc..00000000 --- a/unifiedcache/ucm_connector/ucm_mooncake_bak.py +++ /dev/null @@ -1,358 +0,0 @@ -import asyncio -import json -import os -import threading -from concurrent.futures import Future, TimeoutError -from dataclasses import dataclass -from typing import Dict, List - -import torch -from safetensors.torch import load as safetensors_load -from safetensors.torch import save as safetensors_save - -from unifiedcache.logger import init_logger -from unifiedcache.ucm_connector import Task, UcmKVStoreBase - -TIMEOUT_S_THR: int = 60 * 60 -DEFAULT_GLOBAL_SEGMENT_SIZE: int = 1 * 1024 * 1024 * 1024 # 3.125 GiB -DEFAULT_LOCAL_BUFFER_SIZE: int = 1 * 1024 * 1024 * 1024 # 1.0 GiB - -logger = init_logger(__name__) - - -# TODO To keep it consistent with the vllm source code(vllm/distributed/kv_transfer/kv_lookup_buffer/mooncake_store.py), the source code is fully reused here. The code here will be deleted after vllm is implemented. -@dataclass -class MooncakeStoreConfig: - local_hostname: str - metadata_server: str - global_segment_size: int - local_buffer_size: int - protocol: str - device_name: str - master_server_address: str - - @staticmethod - def load_from_dict(config: Dict = {}) -> "MooncakeStoreConfig": - return MooncakeStoreConfig( - local_hostname=config.get("local_hostname"), - metadata_server=config.get("metadata_server"), - global_segment_size=config.get( - "global_segment_size", DEFAULT_GLOBAL_SEGMENT_SIZE - ), - local_buffer_size=config.get( - "local_buffer_size", DEFAULT_LOCAL_BUFFER_SIZE - ), - protocol=config.get("protocol", "tcp"), - device_name=config.get("device_name", ""), - master_server_address=config.get("master_server_address"), - ) - - @staticmethod - def from_file(file_path: str) -> "MooncakeStoreConfig": - """ - # NOTE: - # This method currently loads connection information from a file. - # In the future, it should be updated to load configuration from a YAML file, - # as the UC connector plans to support YAML-based config. - """ - """Load the config from a JSON file.""" - with open(file_path) as fin: - config = json.load(fin) - return MooncakeStoreConfig( - local_hostname=config.get("local_hostname"), - metadata_server=config.get("metadata_server"), - global_segment_size=config.get( - "global_segment_size", DEFAULT_GLOBAL_SEGMENT_SIZE - ), - local_buffer_size=config.get( - "local_buffer_size", DEFAULT_LOCAL_BUFFER_SIZE - ), - protocol=config.get("protocol", "tcp"), - device_name=config.get("device_name", ""), - master_server_address=config.get("master_server_address"), - ) - - @staticmethod - def load_from_env() -> "MooncakeStoreConfig": - """Load config from a file specified in the environment variable.""" - config_file_path = os.getenv("MOONCAKE_CONFIG_PATH") - if config_file_path is None: - raise ValueError( - "The environment variable 'MOONCAKE_CONFIG_PATH' is not set." - ) - return MooncakeStoreConfig.from_file(config_file_path) - - -@dataclass -class MooncakeTask(Task): - """A task class for Mooncake operations with a task identifier.""" - - task_id: int = -1 - - -class UcmMooncakeStore(UcmKVStoreBase): - """ - A wrapper class for MooncakeDistributedStore that implements the UcmKVStoreBase interface. - Provides key-value store functionality for vLLM using Mooncake as the backend. - """ - - def __init__(self, config: Dict = {}): - """Initialize the Mooncake store with configuration.""" - super().__init__(config) - try: - from mooncake.store import MooncakeDistributedStore - except ImportError as e: - raise ImportError( - "Please install mooncake by following the instructions at " - "https://github.com/kvcache-ai/Mooncake/blob/main/doc/en/build.md " # noqa: E501 - "to run vLLM with MooncakeConnector." - ) from e - - try: - self.store = MooncakeDistributedStore() - if config != {}: - mooncake_config = MooncakeStoreConfig.load_from_dict(config) - else: - mooncake_config = MooncakeStoreConfig.load_from_env() - logger.info("Mooncake Configuration loaded successfully.") - self.store.setup( - mooncake_config.local_hostname, - mooncake_config.metadata_server, - mooncake_config.global_segment_size, - mooncake_config.local_buffer_size, - mooncake_config.protocol, - mooncake_config.device_name, - mooncake_config.master_server_address, - ) - - except ValueError as e: - logger.error("Configuration loading failed: %s", e) - raise - except Exception as exc: - logger.error("An error occurred while loading the configuration: %s", exc) - raise - - # Task management variables - self.task_id: int = 0 - self.tasks: Dict[int, Future] = {} - - # Threading and synchronization variables - self.loop = asyncio.new_event_loop() - self.lock = threading.Lock() - self._shutting_down = threading.Event() - - # Start the event loop thread - self.thread = threading.Thread(target=self._run_event_loop, daemon=True) - self.thread.start() - - def _run_event_loop(self): - """Run the asyncio event loop in a separate thread.""" - asyncio.set_event_loop(self.loop) - self.loop.run_forever() - - def create(self, block_ids: List[str]) -> int: - """ - create kv cache space in storafe (not implemented for Mooncake). - - Args: - block_ids (List[str]): vLLM block hash. - Returns: - Always returns 0 as this operation is not supported by Mooncake - """ - # Mooncake only has get and put interfaces, this operation is not supported - return 0 - - def lookup(self, block_ids: List[str]) -> List[bool]: - """ - Get number of blocks that can be loaded from the - external KV cache. - Mooncake integration uses hash = block_id + offset (default offset=0 if not provided). - Args: - block_ids (List[str]): vLLM block hash. - - Returns: - hit block mask, True -> hit - """ - if self._shutting_down.is_set(): - raise RuntimeError("UcmMooncakeStore is shutting down.") - - mask = [ - True if self.store.is_exist(f"{block_key}_0") == 1 else False - for block_key in block_ids - ] - return mask - - def prefetch(self, block_ids: List[str]) -> None: - """ - prefetch kv cache to high speed cache according to block_ids (not implemented for Mooncake). - - Args: - block_ids (List[str]): vLLM block hash. - """ - # Mooncake only has get and put interfaces, this operation is not supported - pass - - def load( - self, block_ids: List[str], offset: List[int], dst_tensor: List[torch.Tensor] - ) -> Task: - """ - load kv cache to device. - Mooncake integration uses hash = block_id + offset (default offset=0 if not provided). - - Args: - block_ids (List[str]): vLLM block hash. - offset(List[int]): tp > 1 scene - dst_tensor: List[torch.Tensor]: device tensor addr. - Returns: - task(Task). - """ - if self._shutting_down.is_set(): - raise RuntimeError("UcmMooncakeStore is shutting down.") - - coro = self._load_impl(block_ids, offset, dst_tensor) - future = asyncio.run_coroutine_threadsafe(coro, self.loop) - with self.lock: - self.task_id += 1 - self.tasks[self.task_id] = future - return MooncakeTask(task_id=self.task_id) - - async def _load_impl( - self, block_ids: List[str], offset: List[int], dst_tensor: List[torch.Tensor] - ) -> int: - """Internal implementation of loading KV cache from Mooncake Store.""" - assert len(block_ids) == len( - dst_tensor - ), "block_ids and dst_tensor have different lengths, please check!" - for i in range(len(block_ids)): - try: - block_hash = f"{block_ids[i]}_{offset[i]}" - data = self.store.get(block_hash) - except TypeError as err: - logger.error("Failed to get value from Mooncake Store: %s", err) - raise TypeError("Mooncake Store Get Type Error.") from err - - if data: - loaded_tensors = safetensors_load(data) - tensor_cpu = loaded_tensors["tensor"] - assert dst_tensor[i].shape == tensor_cpu.shape - assert dst_tensor[i].dtype == tensor_cpu.dtype - dst_tensor[i].copy_(tensor_cpu) - else: - return 1 - return 0 - - def dump( - self, block_ids: List[str], offset: List[int], src_tensor: List[torch.Tensor] - ) -> Task: - """ - dump kv cache to device. - Mooncake integration uses hash = block_id + offset (default offset=0 if not provided). - - Args: - block_ids (List[str]): vLLM block hash. - offset(List[int]): tp > 1 scene - src_tensor: List[torch.Tensor]: device tensor addr. - Returns: - task(Task). - """ - if self._shutting_down.is_set(): - raise RuntimeError("UcmMooncakeStore is shutting down.") - - coro = self._dump_impl(block_ids, offset, src_tensor) - future = asyncio.run_coroutine_threadsafe(coro, self.loop) - with self.lock: - self.task_id += 1 - self.tasks[self.task_id] = future - return MooncakeTask(task_id=self.task_id) - - async def _dump_impl( - self, block_ids: List[str], offset: List[int], src_tensor: List[torch.Tensor] - ) -> int: - """Internal implementation of dumping KV cache to Mooncake Store.""" - assert len(block_ids) == len( - src_tensor - ), "block_ids and src_tensor have different lengths, please check!" - for i in range(len(block_ids)): - value_bytes = safetensors_save({"tensor": src_tensor[i]}) - try: - block_hash = f"{block_ids[i]}_{offset[i]}" - ret = self.store.put(block_hash, value_bytes) - if ret != 0: - return ret - except TypeError as err: - logger.error("Failed to put value into Mooncake Store: %s", err) - raise TypeError("Mooncake Store Put Type Error.") from err - return 0 - - def wait(self, task: Task) -> int: - """ - wait kv cache kv transfer task finished. - - Args: - task (Task): transfer engine task. - Returns: - 0 - success - others - failed. - """ - # Safely retrieve the Future object - with self.lock: - future = self.tasks.pop(task.task_id, None) - - if future is None: - logger.error(f"Invalid task ID: {task.task_id}") - return 1 - - try: - ret = future.result(TIMEOUT_S_THR) - return ret - except TimeoutError: - # Cancel the task if it times out - future.cancel() - logger.error(f"Task {task.task_id} timed out after {TIMEOUT_S_THR}s") - return 1 - except asyncio.CancelledError: - logger.error(f"Task {task.task_id} was cancelled") - return 1 - except Exception as e: - logger.error(f"Task {task.task_id} failed: {str(e)}") - return 1 - - def commit(self, block_ids: List[str], is_success: bool = True) -> None: - """ - commit kv cache, now kv cache can be reused (not implemented for Mooncake). - - Args: - block_ids (List[str]): vLLM block hash. - is_success(bool): if False, we need release block - """ - # Mooncake only has get and put interfaces, this operation is not supported - pass - - def shutdown(self): - """Safely shutdown all components of the store.""" - if self._shutting_down.is_set(): - return - - self._shutting_down.set() - - # Safely cancel all pending tasks (atomic operation) - with self.lock: - tasks_to_cancel = list(self.tasks.values()) - self.tasks.clear() - - for future in tasks_to_cancel: - if not future.done(): - future.cancel() - - # Stop the event loop - self.loop.call_soon_threadsafe(self.loop.stop) - - # Wait for thread termination - if self.thread.is_alive(): - self.thread.join(TIMEOUT_S_THR) - - # Force close the loop if thread didn't exit - if not self.loop.is_closed(): - self.loop.close() - - self.store.close() From 74bfbe1173e8adcce0ab9a350f6d6f91027d6877 Mon Sep 17 00:00:00 2001 From: propanone1006 <1035067916@qq.com> Date: Fri, 15 Aug 2025 16:45:42 +0800 Subject: [PATCH 03/12] [Doc] Add docs for Ucm Mooncake Connector --- .../getting-started/example/Mooncake.md | 84 ++++++++ docs/source/getting-started/example/index.md | 1 + .../getting-started/example/mooncake_conn.md | 180 ++++++++++++++++++ 3 files changed, 265 insertions(+) create mode 100644 docs/source/getting-started/example/Mooncake.md create mode 100644 docs/source/getting-started/example/mooncake_conn.md diff --git a/docs/source/getting-started/example/Mooncake.md b/docs/source/getting-started/example/Mooncake.md new file mode 100644 index 00000000..c9e3755e --- /dev/null +++ b/docs/source/getting-started/example/Mooncake.md @@ -0,0 +1,84 @@ +# How to Use UCM with Mooncake Store +This guide explains how to integrate UCM (Unified Cache Management) with the Mooncake framework. + +## Build Mooncake +Follow the [official guide](https://github.com/kvcache-ai/Mooncake/blob/v0.3.4/doc/en/build.md) to build Mooncake: + +📄 Mooncake Build Instructions + +⚠️ Recommended: Compile inside a Ubuntu container to avoid environment issues. + +## Start Mooncake Services +1. Start Metadata Service +Navigate to the metadata server directory: + +```bash +cd $MOONCAKE_ROOT_DIR/mooncake-transfer-engine/example/http-metadata-server +``` + +Replace `$MOONCAKE_ROOT_DIR` with your Mooncake source root path. + +2. Launch the service: + + - Make sure to unset any HTTP proxies to prevent networking issues. + + - Use appropriate port based on your environment. + +```bash +unset http_proxy https_proxy no_proxy HTTP_PROXY HTTPS_PROXY NO_PROXY +go run . --addr=0.0.0.0:23790 +``` +3. Start Master Service +```bash +unset http_proxy https_proxy no_proxy HTTP_PROXY HTTPS_PROXY NO_PROXY +mooncake_master --port 50001 +``` +Same note as above: ensure no proxies are set, and adjust port as needed. + +## Verify Integration +Run the Example Script +Set the Python working directory. Replace the path accordingly: + +```bash +export UCM_WORK_DIR="/home/xxx/unified-cache-management" && cd $UCM_WORK_DIR +export PYTHONPATH="${PYTHONPATH}:$UCM_WORK_DIR" +``` +Prepare the Mooncake configuration file: + +```bash +vim $UCM_WORK_DIR/test/mooncake.json +``` + +Example config: + +```json +{ + "local_hostname": "127.0.0.1", + "metadata_server": "http://127.0.0.1:23790/metadata", + "protocol": "tcp", + "device_name": "mlx5_1", + "master_server_address": "127.0.0.1:50001" +} +``` +## Reference: Mooncake vLLM RDMA Integration + +### Run the example: + +```bash +unset http_proxy https_proxy no_proxy HTTP_PROXY HTTPS_PROXY NO_PROXY +export MOONCAKE_CONFIG_PATH=$UCM_WORK_DIR/test/mooncake.json +export MC_GID_INDEX=3 +python3 $UCM_WORK_DIR/examples/mooncake_kv_offload.py +``` +- MOONCAKE_CONFIG_PATH: Path to your config file + +- MC_GID_INDEX: (Optional) Required when using RDMA. Should match the RDMA device (rdma link show and ibv_devinfo -v -d mlx5_1 can help inspect). + +### Run Unit Tests + +```bash +unset http_proxy https_proxy no_proxy HTTP_PROXY HTTPS_PROXY NO_PROXY +export MOONCAKE_CONFIG_PATH=$UCM_WORK_DIR/test/mooncake.json +export MC_GID_INDEX=3 +pytest $UCM_WORK_DIR/test/test_mooncake.py +``` \ No newline at end of file diff --git a/docs/source/getting-started/example/index.md b/docs/source/getting-started/example/index.md index 0c0ce5f4..4c188a97 100644 --- a/docs/source/getting-started/example/index.md +++ b/docs/source/getting-started/example/index.md @@ -4,5 +4,6 @@ :maxdepth: 2 nfs_conn.md dram_conn.md +mooncake_conn.md ::: diff --git a/docs/source/getting-started/example/mooncake_conn.md b/docs/source/getting-started/example/mooncake_conn.md new file mode 100644 index 00000000..515b8664 --- /dev/null +++ b/docs/source/getting-started/example/mooncake_conn.md @@ -0,0 +1,180 @@ +# Mooncake Connector + +This document provides a usage example and configuration guide for the **Mooncake Connector**. This connector enables offloading of KV cache from GPU HBM to CPU Mooncake, helping reduce memory pressure and support larger models or batch sizes. + +## Performance + + + + + +## Features + +The Monncake connector supports the following functionalities: + +- `dump`: Offload KV cache blocks from HBM to Mooncake. +- `load`: Load KV cache blocks from Mooncake back to HBM. +- `lookup`: Look up KV blocks stored in Mooncake by block hash. +- `wait`: Ensure that all copy streams between CPU and GPU have completed. + +## Configuration + +### Start Mooncake Services + + + +1. Follow the [Mooncake official guide](https://github.com/kvcache-ai/Mooncake/blob/v0.3.4/doc/en/build.md) to build Mooncake. + +2. Start Mooncake Store Service + + Please change the IP addresses and ports in the following guide according to your env. + +```bash +# Unset HTTP proxies +unset http_proxy https_proxy no_proxy HTTP_PROXY HTTPS_PROXY NO_PROXY +# Navigate to the metadata server directory +cd $MOONCAKE_ROOT_DIR/mooncake-transfer-engine/example/http-metadata-server +# Start Metadata Service +go run . --addr=0.0.0.0:23790 +# Start Master Service +mooncake_master --port 50001 +``` +- Replace `$MOONCAKE_ROOT_DIR` with your Mooncake source root path. +- Make sure to unset any HTTP proxies to prevent networking issues. +- Use appropriate port based on your environment. + + + + +### Required Parameters + +To use the Mooncake connector, you need to configure the `connector_config` dictionary in your model's launch configuration. + +- `max_cache_size` *(optional)*: + Specifies the maximum allowed Mooncake memory usage (in **byte**) for caching in `kv_connector_extra_config["ucm_connector_config"]`. + If not provided, it defaults to **5 GB**. +- `kv_block_size` *(optional)*: + Specifies the memory size (in bytes) of a single key or value cache block used in vLLM’s paged attention mechanism, which is calculated as : `block_size * head_size * total_num_kv_heads * element_size`. +- `local_hostname`: + The IP address of the current node used to communicate with the metadata server. +- `metadata_server`: + The metadata server of the mooncake transfer engine. +- `protocl` *(optional)*: + If not provided, it defaults to **tcp**. +- `device_name` *(optional)*: + The device to be used for data transmission, it is required when “protocol” is set to “rdma”. If multiple NIC devices are used, they can be separated by commas such as “erdma_0,erdma_1”. Please note that there are no spaces between them. +- `master_server_address`: + The IP address and the port of the master daemon process of MooncakeStore. + +### Example: + +```python +# Allocate up to 8GB Mooncake for KV cache +# KV Block size (in byte) is 262144 +kv_connector_extra_config={ + "ucm_connector_name": "UcmMooncake", + "ucm_connector_config":{ + "max_cache_size": 5368709120, + "kv_block_size": 262144, + "local_hostname": "127.0.0.1", + "metadata_server": "http://127.0.0.1:23790/metadata", + "protocol": "tcp", + "device_name": "", + "master_server_address": "127.0.0.1:50001" + } + } +``` + +## Launching Inference + +### Offline Inference + +To start **offline inference** with the Mooncake connector,modify the script `examples/offline_inference.py` to include the `kv_connector_extra_config` for Mooncake connector usage: + +```python +# In examples/offline_inference.py +ktc = KVTransferConfig( + ... + kv_connector_extra_config={ + "ucm_connector_name": "UcmMooncake", + "ucm_connector_config":{ + "max_cache_size": 5368709120, + "kv_block_size": 262144, + "local_hostname": "127.0.0.1", + "metadata_server": "http://127.0.0.1:23790/metadata", + "protocol": "tcp", + "device_name": "", + "master_server_address": "127.0.0.1:50001" + } + } +) +``` + +Then run the script as follows: + +```bash +cd examples/ +python offline_inference.py +``` + +### Online Inference + +For **online inference** , vLLM with our connector can also be deployed as a server that implements the OpenAI API protocol. + +First, specify the python hash seed by: +```bash +export PYTHONHASHSEED=123456 +``` + +Run the following command to start the vLLM server with the Qwen/Qwen2.5-14B-Instruct model: + +```bash +vllm serve /home/models/Qwen2.5-14B-Instruct \ +--max-model-len 20000 \ +--tensor-parallel-size 2 \ +--gpu_memory_utilization 0.87 \ +--trust-remote-code \ +--port 7800 \ +--kv-transfer-config \ +'{ + "kv_connector": "UnifiedCacheConnectorV1", + "kv_connector_module_path": "unifiedcache.integration.vllm.uc_connector", + "kv_role": "kv_both", + "kv_connector_extra_config": { + "ucm_connector_name": "UcmMooncake", + "ucm_connector_config":{ + "max_cache_size": 5368709120, + "kv_block_size": 262144, + "local_hostname": "127.0.0.1", + "metadata_server": "http://127.0.0.1:23790/metadata", + "protocol": "tcp", + "device_name": "", + "master_server_address": "127.0.0.1:50001" + } + } + } +}' +``` + +If you see log as below: + +```bash +INFO: Started server process [321290] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +Congratulations, you have successfully started the vLLM server with Mooncake Connector! + +After successfully started the vLLM server,You can interact with the API as following: + +```bash +curl http://localhost:7800/v1/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "/home/models/Qwen2.5-14B-Instruct", + "prompt": "Shanghai is a", + "max_tokens": 7, + "temperature": 0 + }' +``` From 9f7cee24f262f90dc40de72c167d803b1445baa3 Mon Sep 17 00:00:00 2001 From: propanone1006 <1035067916@qq.com> Date: Wed, 20 Aug 2025 10:03:59 +0800 Subject: [PATCH 04/12] [Feature] Add mooncake to ucm factory --- unifiedcache/ucm_connector/factory.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/unifiedcache/ucm_connector/factory.py b/unifiedcache/ucm_connector/factory.py index 814fb2ff..970594a3 100644 --- a/unifiedcache/ucm_connector/factory.py +++ b/unifiedcache/ucm_connector/factory.py @@ -63,3 +63,6 @@ def create_connector(cls, connector_name: str, config: dict) -> UcmKVStoreBase: UcmConnectorFactory.register_connector( "UcmNfsStore", "unifiedcache.ucm_connector.ucm_nfs_store", "UcmNfsStore" ) +UcmConnectorFactory.register_connector( + "UcmMooncakeStore", "unifiedcache.ucm_connector.ucm_mooncake", "UcmMooncakeStore" +) From 4e8af97eb8e8da02c12c8f67fc0e3ba26d39e11d Mon Sep 17 00:00:00 2001 From: propanone1006 <1035067916@qq.com> Date: Wed, 20 Aug 2025 10:51:00 +0800 Subject: [PATCH 05/12] [Doc][Fix] Modify the description of configuration to match usage. --- .../getting-started/example/Mooncake.md | 2 +- .../getting-started/example/mooncake_conn.md | 35 +++++++------------ 2 files changed, 13 insertions(+), 24 deletions(-) diff --git a/docs/source/getting-started/example/Mooncake.md b/docs/source/getting-started/example/Mooncake.md index c9e3755e..e9319246 100644 --- a/docs/source/getting-started/example/Mooncake.md +++ b/docs/source/getting-started/example/Mooncake.md @@ -12,7 +12,7 @@ Follow the [official guide](https://github.com/kvcache-ai/Mooncake/blob/v0.3.4/d 1. Start Metadata Service Navigate to the metadata server directory: -```bash +```bash cd $MOONCAKE_ROOT_DIR/mooncake-transfer-engine/example/http-metadata-server ``` diff --git a/docs/source/getting-started/example/mooncake_conn.md b/docs/source/getting-started/example/mooncake_conn.md index 515b8664..b76c2df6 100644 --- a/docs/source/getting-started/example/mooncake_conn.md +++ b/docs/source/getting-started/example/mooncake_conn.md @@ -21,8 +21,6 @@ The Monncake connector supports the following functionalities: ### Start Mooncake Services - - 1. Follow the [Mooncake official guide](https://github.com/kvcache-ai/Mooncake/blob/v0.3.4/doc/en/build.md) to build Mooncake. 2. Start Mooncake Store Service @@ -45,37 +43,32 @@ mooncake_master --port 50001 - ### Required Parameters To use the Mooncake connector, you need to configure the `connector_config` dictionary in your model's launch configuration. -- `max_cache_size` *(optional)*: - Specifies the maximum allowed Mooncake memory usage (in **byte**) for caching in `kv_connector_extra_config["ucm_connector_config"]`. - If not provided, it defaults to **5 GB**. -- `kv_block_size` *(optional)*: - Specifies the memory size (in bytes) of a single key or value cache block used in vLLM’s paged attention mechanism, which is calculated as : `block_size * head_size * total_num_kv_heads * element_size`. - `local_hostname`: The IP address of the current node used to communicate with the metadata server. - `metadata_server`: The metadata server of the mooncake transfer engine. +- `master_server_address`: + The IP address and the port of the master daemon process of MooncakeStore. - `protocl` *(optional)*: If not provided, it defaults to **tcp**. - `device_name` *(optional)*: The device to be used for data transmission, it is required when “protocol” is set to “rdma”. If multiple NIC devices are used, they can be separated by commas such as “erdma_0,erdma_1”. Please note that there are no spaces between them. -- `master_server_address`: - The IP address and the port of the master daemon process of MooncakeStore. +- `global_segment_size`*(optional)*: + The size of each global segment in bytes. `DEFAULT_GLOBAL_SEGMENT_SIZE = 3355443200` **3.125 GiB** +- `local_buffer_size`*(optional)*: + The size of the local buffer in bytes. `DEFAULT_LOCAL_BUFFER_SIZE = 1073741824` **1.0 GiB** + ### Example: ```python -# Allocate up to 8GB Mooncake for KV cache -# KV Block size (in byte) is 262144 kv_connector_extra_config={ - "ucm_connector_name": "UcmMooncake", + "ucm_connector_name": "UcmMooncakeStore", "ucm_connector_config":{ - "max_cache_size": 5368709120, - "kv_block_size": 262144, "local_hostname": "127.0.0.1", "metadata_server": "http://127.0.0.1:23790/metadata", "protocol": "tcp", @@ -96,10 +89,8 @@ To start **offline inference** with the Mooncake connector,modify the script ` ktc = KVTransferConfig( ... kv_connector_extra_config={ - "ucm_connector_name": "UcmMooncake", - "ucm_connector_config":{ - "max_cache_size": 5368709120, - "kv_block_size": 262144, + "ucm_connector_name": "UcmMooncakeStore", + "ucm_connector_config":{ "local_hostname": "127.0.0.1", "metadata_server": "http://127.0.0.1:23790/metadata", "protocol": "tcp", @@ -141,10 +132,8 @@ vllm serve /home/models/Qwen2.5-14B-Instruct \ "kv_connector_module_path": "unifiedcache.integration.vllm.uc_connector", "kv_role": "kv_both", "kv_connector_extra_config": { - "ucm_connector_name": "UcmMooncake", - "ucm_connector_config":{ - "max_cache_size": 5368709120, - "kv_block_size": 262144, + "ucm_connector_name": "UcmMooncakeStore", + "ucm_connector_config":{ "local_hostname": "127.0.0.1", "metadata_server": "http://127.0.0.1:23790/metadata", "protocol": "tcp", From 8e873de3717d292703264e4e727c411683b6231d Mon Sep 17 00:00:00 2001 From: propanone1006 <1035067916@qq.com> Date: Wed, 20 Aug 2025 11:01:34 +0800 Subject: [PATCH 06/12] [Feature] [Fix] Load Mooncake config from dict, when lack params, load from env config file. --- .../getting-started/example/mooncake_conn.md | 2 +- unifiedcache/ucm_connector/ucm_mooncake.py | 41 +++++++++++++++---- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/docs/source/getting-started/example/mooncake_conn.md b/docs/source/getting-started/example/mooncake_conn.md index b76c2df6..6aa5192b 100644 --- a/docs/source/getting-started/example/mooncake_conn.md +++ b/docs/source/getting-started/example/mooncake_conn.md @@ -53,7 +53,7 @@ To use the Mooncake connector, you need to configure the `connector_config` dict The metadata server of the mooncake transfer engine. - `master_server_address`: The IP address and the port of the master daemon process of MooncakeStore. -- `protocl` *(optional)*: +- `protocol` *(optional)*: If not provided, it defaults to **tcp**. - `device_name` *(optional)*: The device to be used for data transmission, it is required when “protocol” is set to “rdma”. If multiple NIC devices are used, they can be separated by commas such as “erdma_0,erdma_1”. Please note that there are no spaces between them. diff --git a/unifiedcache/ucm_connector/ucm_mooncake.py b/unifiedcache/ucm_connector/ucm_mooncake.py index b838c59a..cd5be442 100644 --- a/unifiedcache/ucm_connector/ucm_mooncake.py +++ b/unifiedcache/ucm_connector/ucm_mooncake.py @@ -14,8 +14,8 @@ from unifiedcache.ucm_connector import Task, UcmKVStoreBase TIMEOUT_S_THR: int = 60 * 60 -DEFAULT_GLOBAL_SEGMENT_SIZE: int = 1 * 1024 * 1024 * 1024 # 3.125 GiB -DEFAULT_LOCAL_BUFFER_SIZE: int = 1 * 1024 * 1024 * 1024 # 1.0 GiB +DEFAULT_GLOBAL_SEGMENT_SIZE: int = 3355443200 # 3.125 GiB +DEFAULT_LOCAL_BUFFER_SIZE: int = 1073741824 # 1.0 GiB logger = init_logger(__name__) @@ -111,11 +111,10 @@ def __init__(self, config: Dict = {}): try: self.store = MooncakeDistributedStore() - if config != {}: - mooncake_config = MooncakeStoreConfig.load_from_dict(config) - else: - mooncake_config = MooncakeStoreConfig.load_from_env() - logger.info("Mooncake Configuration loaded successfully.") + + mooncake_config = MooncakeStoreConfig.load_from_dict(config) + logger.info("Mooncake Configuration loaded from dict successfully.") + self.store.setup( mooncake_config.local_hostname, mooncake_config.metadata_server, @@ -129,6 +128,34 @@ def __init__(self, config: Dict = {}): except ValueError as e: logger.error("Configuration loading failed: %s", e) raise + except TypeError: + logger.warning( + "Lack of configuration, loading Mooncake configuration from environment variables instead." + ) + try: + mooncake_config = MooncakeStoreConfig.load_from_env() + logger.info("Mooncake Configuration loaded from env successfully.") + self.store.setup( + mooncake_config.local_hostname, + mooncake_config.metadata_server, + mooncake_config.global_segment_size, + mooncake_config.local_buffer_size, + mooncake_config.protocol, + mooncake_config.device_name, + mooncake_config.master_server_address, + ) + except ValueError as e: + logger.error( + "Configuration loading failed: %s \n Please check the dict params or edit the config file and set path.", + e, + ) + raise + except Exception as exc: + logger.error( + "An error occurred while loading the configuration: %s", exc + ) + raise + except Exception as exc: logger.error("An error occurred while loading the configuration: %s", exc) raise From 8d015cb7fc89c9a3cd08e25dec3b2970c0bf61a5 Mon Sep 17 00:00:00 2001 From: propanone1006 <1035067916@qq.com> Date: Wed, 27 Aug 2025 10:01:05 +0800 Subject: [PATCH 07/12] [Doc] update the performance and modify description. --- .../getting-started/example/Mooncake.md | 84 ------------------ .../getting-started/example/mooncake_conn.md | 21 ++++- .../images/mooncake_default_performance.png | Bin 0 -> 41527 bytes docs/source/images/mooncake_performance.png | Bin 0 -> 43663 bytes 4 files changed, 17 insertions(+), 88 deletions(-) delete mode 100644 docs/source/getting-started/example/Mooncake.md create mode 100644 docs/source/images/mooncake_default_performance.png create mode 100644 docs/source/images/mooncake_performance.png diff --git a/docs/source/getting-started/example/Mooncake.md b/docs/source/getting-started/example/Mooncake.md deleted file mode 100644 index e9319246..00000000 --- a/docs/source/getting-started/example/Mooncake.md +++ /dev/null @@ -1,84 +0,0 @@ -# How to Use UCM with Mooncake Store -This guide explains how to integrate UCM (Unified Cache Management) with the Mooncake framework. - -## Build Mooncake -Follow the [official guide](https://github.com/kvcache-ai/Mooncake/blob/v0.3.4/doc/en/build.md) to build Mooncake: - -📄 Mooncake Build Instructions - -⚠️ Recommended: Compile inside a Ubuntu container to avoid environment issues. - -## Start Mooncake Services -1. Start Metadata Service -Navigate to the metadata server directory: - -```bash -cd $MOONCAKE_ROOT_DIR/mooncake-transfer-engine/example/http-metadata-server -``` - -Replace `$MOONCAKE_ROOT_DIR` with your Mooncake source root path. - -2. Launch the service: - - - Make sure to unset any HTTP proxies to prevent networking issues. - - - Use appropriate port based on your environment. - -```bash -unset http_proxy https_proxy no_proxy HTTP_PROXY HTTPS_PROXY NO_PROXY -go run . --addr=0.0.0.0:23790 -``` -3. Start Master Service -```bash -unset http_proxy https_proxy no_proxy HTTP_PROXY HTTPS_PROXY NO_PROXY -mooncake_master --port 50001 -``` -Same note as above: ensure no proxies are set, and adjust port as needed. - -## Verify Integration -Run the Example Script -Set the Python working directory. Replace the path accordingly: - -```bash -export UCM_WORK_DIR="/home/xxx/unified-cache-management" && cd $UCM_WORK_DIR -export PYTHONPATH="${PYTHONPATH}:$UCM_WORK_DIR" -``` -Prepare the Mooncake configuration file: - -```bash -vim $UCM_WORK_DIR/test/mooncake.json -``` - -Example config: - -```json -{ - "local_hostname": "127.0.0.1", - "metadata_server": "http://127.0.0.1:23790/metadata", - "protocol": "tcp", - "device_name": "mlx5_1", - "master_server_address": "127.0.0.1:50001" -} -``` -## Reference: Mooncake vLLM RDMA Integration - -### Run the example: - -```bash -unset http_proxy https_proxy no_proxy HTTP_PROXY HTTPS_PROXY NO_PROXY -export MOONCAKE_CONFIG_PATH=$UCM_WORK_DIR/test/mooncake.json -export MC_GID_INDEX=3 -python3 $UCM_WORK_DIR/examples/mooncake_kv_offload.py -``` -- MOONCAKE_CONFIG_PATH: Path to your config file - -- MC_GID_INDEX: (Optional) Required when using RDMA. Should match the RDMA device (rdma link show and ibv_devinfo -v -d mlx5_1 can help inspect). - -### Run Unit Tests - -```bash -unset http_proxy https_proxy no_proxy HTTP_PROXY HTTPS_PROXY NO_PROXY -export MOONCAKE_CONFIG_PATH=$UCM_WORK_DIR/test/mooncake.json -export MC_GID_INDEX=3 -pytest $UCM_WORK_DIR/test/test_mooncake.py -``` \ No newline at end of file diff --git a/docs/source/getting-started/example/mooncake_conn.md b/docs/source/getting-started/example/mooncake_conn.md index 6aa5192b..4b9f0a55 100644 --- a/docs/source/getting-started/example/mooncake_conn.md +++ b/docs/source/getting-started/example/mooncake_conn.md @@ -4,9 +4,20 @@ This document provides a usage example and configuration guide for the **Mooncak ## Performance - - - +| tokens | mooncake-first | mooncake-second | default | +| ------ | ------------------ | ------------------ | ------------------ | +| 2k | 1.9231491860002279 | 0.8265988459810615 | 0.5419427898712457 | +| 4k | 3.9460434830747544 | 1.5273493870627135 | 0.991630249004811 | +| 8k | 7.577957597002387 | 2.7632693520281464 | 2.0716467570047827 | +| 16k | 16.823639799049126 | 5.515289016952738 | 4.742832682048902 | +| 32k | 81.98759594326839 | 14.217441103421152 | 12.310140203218907 | + +Use mooncake fig && default: +

+ UCM + + UCM +

## Features @@ -23,6 +34,8 @@ The Monncake connector supports the following functionalities: 1. Follow the [Mooncake official guide](https://github.com/kvcache-ai/Mooncake/blob/v0.3.4/doc/en/build.md) to build Mooncake. +> **[Warning]**: Currently, this connector only supports Mooncake v0.3.4, and the updated version is being adapted. + 2. Start Mooncake Store Service Please change the IP addresses and ports in the following guide according to your env. @@ -30,7 +43,7 @@ The Monncake connector supports the following functionalities: ```bash # Unset HTTP proxies unset http_proxy https_proxy no_proxy HTTP_PROXY HTTPS_PROXY NO_PROXY -# Navigate to the metadata server directory +# Navigate to the metadata server directory, http server for example. cd $MOONCAKE_ROOT_DIR/mooncake-transfer-engine/example/http-metadata-server # Start Metadata Service go run . --addr=0.0.0.0:23790 diff --git a/docs/source/images/mooncake_default_performance.png b/docs/source/images/mooncake_default_performance.png new file mode 100644 index 0000000000000000000000000000000000000000..6eff9fd42da1dcf7683d2780000a7f06aaf041df GIT binary patch literal 41527 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 literal 0 HcmV?d00001 From 169167602dbc96b199761509d128eb9b8aa0f686 Mon Sep 17 00:00:00 2001 From: propanone1006 <1035067916@qq.com> Date: Fri, 29 Aug 2025 11:52:28 +0800 Subject: [PATCH 08/12] [Test] Example config file for Mooncake test `test_mooncake_env.py`. --- test/mooncake.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 test/mooncake.json diff --git a/test/mooncake.json b/test/mooncake.json new file mode 100644 index 00000000..43d64b66 --- /dev/null +++ b/test/mooncake.json @@ -0,0 +1,7 @@ +{ + "local_hostname": "127.0.0.1", + "metadata_server": "http://127.0.0.1:23790/metadata", + "protocol": "tcp", + "device_name": "", + "master_server_address": "127.0.0.1:50001" +} From 315af328924fce4e4c96b104fed54d5d4329ed53 Mon Sep 17 00:00:00 2001 From: propanone1006 <1035067916@qq.com> Date: Fri, 29 Aug 2025 14:50:13 +0800 Subject: [PATCH 09/12] [Test] [Del] Removed unnecessary tests that do not match the current functionality --- test/mooncake.json | 7 - test/mooncake_kv_offload.py | 49 ------ ...test_mooncake_dict.py => test_mooncake.py} | 0 test/test_mooncake_env.py | 147 ------------------ 4 files changed, 203 deletions(-) delete mode 100644 test/mooncake.json delete mode 100644 test/mooncake_kv_offload.py rename test/{test_mooncake_dict.py => test_mooncake.py} (100%) delete mode 100644 test/test_mooncake_env.py diff --git a/test/mooncake.json b/test/mooncake.json deleted file mode 100644 index 43d64b66..00000000 --- a/test/mooncake.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "local_hostname": "127.0.0.1", - "metadata_server": "http://127.0.0.1:23790/metadata", - "protocol": "tcp", - "device_name": "", - "master_server_address": "127.0.0.1:50001" -} diff --git a/test/mooncake_kv_offload.py b/test/mooncake_kv_offload.py deleted file mode 100644 index 583887f4..00000000 --- a/test/mooncake_kv_offload.py +++ /dev/null @@ -1,49 +0,0 @@ -import hashlib - -import torch - -from unifiedcache.logger import init_logger -from unifiedcache.ucm_connector.base import Task -from unifiedcache.ucm_connector.ucm_mooncake import UcmMooncakeStore - -logger = init_logger(__name__) - - -def tensor_hash(tensor: torch.Tensor) -> str: - """Calculate the hash value of the tensor.""" - tensor_bytes = tensor.clone().detach().cpu().numpy().tobytes() - hash_object = hashlib.blake2b(tensor_bytes) - hash_hex = hash_object.hexdigest() - return str(int(hash_hex[:16], 16)) - - -store = UcmMooncakeStore() -src_block_data = [torch.randint(0, 1000, (1, 10), dtype=torch.int) for _ in range(5)] -dst_block_data = [torch.empty(data.shape, dtype=data.dtype) for data in src_block_data] -block_ids = [tensor_hash(data) for data in src_block_data] -offset = [0] * len(block_ids) - -mask = store.lookup(block_ids) -logger.info(f"First lookup: {mask=}") - -task: Task = store.dump(block_ids=block_ids, offset=offset, src_tensor=src_block_data) -ret = store.wait(task) -logger.info(f"Dump end: {task=} with return: {ret}") - -mask = store.lookup(block_ids) -logger.info(f"Second lookup: {mask=}") - -task: Task = store.load(block_ids=block_ids, offset=offset, dst_tensor=dst_block_data) -ret = store.wait(task) -logger.info(f"Load end: {task=} with return: {ret}") - -logger.info("原始张量Hash:") -logger.info(f"{block_ids=}") -logger.info("原始张量:") -logger.info(src_block_data) -logger.info("还原后的张量:") -logger.info(dst_block_data) -logger.info("是否一致:") -logger.info( - f"{[torch.equal(src_block_data[i], dst_block_data[i]) for i in range(len(src_block_data))]}" -) diff --git a/test/test_mooncake_dict.py b/test/test_mooncake.py similarity index 100% rename from test/test_mooncake_dict.py rename to test/test_mooncake.py diff --git a/test/test_mooncake_env.py b/test/test_mooncake_env.py deleted file mode 100644 index 1b57d238..00000000 --- a/test/test_mooncake_env.py +++ /dev/null @@ -1,147 +0,0 @@ -import hashlib -import uuid - -import torch - -from unifiedcache.logger import init_logger -from unifiedcache.ucm_connector.base import Task -from unifiedcache.ucm_connector.ucm_mooncake import UcmMooncakeStore - -logger = init_logger(__name__) - - -def tensor_hash(tensor: torch.Tensor) -> str: - """Calculate the hash value of the tensor.""" - tensor_bytes = tensor.clone().detach().cpu().numpy().tobytes() - hash_object = hashlib.blake2b(tensor_bytes) - hash_hex = hash_object.hexdigest() - return str(int(hash_hex[:16], 16)) - - -def test_lookup_not_found(): - """Test that lookup returns False for non-existent block IDs.""" - store = UcmMooncakeStore() - block_ids = [uuid.uuid4().hex for _ in range(10)] - masks = store.lookup(block_ids) - assert all(mask is False for mask in masks) - - -def test_lookup_found(): - """Test that lookup returns True for existing block IDs after dumping data.""" - src_block_data = [ - torch.randint(0, 1000, (1, 100), dtype=torch.int) for _ in range(5) - ] - block_ids = [tensor_hash(data) for data in src_block_data] - offset = [0] * len(block_ids) - - store = UcmMooncakeStore() - task: Task = store.dump( - block_ids=block_ids, offset=offset, src_tensor=src_block_data - ) - ret = store.wait(task) - assert ret == 0 - masks = store.lookup(block_ids) - assert all(mask is True for mask in masks) - - -def test_dump_once(): - """Test dumping data once and verifying it exists in the store.""" - src_block_data = [ - torch.randint(0, 1000, (1, 100), dtype=torch.int) for _ in range(5) - ] - block_ids = [tensor_hash(data) for data in src_block_data] - offset = [0] * len(block_ids) - - store = UcmMooncakeStore() - task: Task = store.dump( - block_ids=block_ids, offset=offset, src_tensor=src_block_data - ) - ret = store.wait(task) - assert ret == 0 - masks = store.lookup(block_ids) - assert all(mask is True for mask in masks) - - -def test_dump_repeated(): - """Test that repeated dumping of the same data doesn't cause errors.""" - src_block_data = [ - torch.randint(0, 1000, (1, 100), dtype=torch.int) for _ in range(5) - ] - block_ids = [tensor_hash(data) for data in src_block_data] - offset = [0] * len(block_ids) - - store = UcmMooncakeStore() - task: Task = store.dump( - block_ids=block_ids, offset=offset, src_tensor=src_block_data - ) - ret = store.wait(task) - assert ret == 0 - masks = store.lookup(block_ids) - assert all(mask is True for mask in masks) - - task: Task = store.dump( - block_ids=block_ids, offset=offset, src_tensor=src_block_data - ) - ret = store.wait(task) - assert ret == 0 - - -def test_load_existing_data(): - """Test loading data that was previously dumped into the store.""" - src_block_data = [ - torch.randint(0, 1000, (1, 100), dtype=torch.int) for _ in range(5) - ] - dst_block_data = [ - torch.empty(data.shape, dtype=data.dtype) for data in src_block_data - ] - block_ids = [tensor_hash(data) for data in src_block_data] - offset = [0] * len(block_ids) - - store = UcmMooncakeStore() - task: Task = store.dump( - block_ids=block_ids, offset=offset, src_tensor=src_block_data - ) - ret = store.wait(task) - assert ret == 0 - - masks = store.lookup(block_ids) - assert all(mask is True for mask in masks) - - task: Task = store.load( - block_ids=block_ids, offset=offset, dst_tensor=dst_block_data - ) - ret = store.wait(task) - assert ret == 0 - assert all( - [ - torch.equal(src_block_data[i], dst_block_data[i]) is True - for i in range(len(src_block_data)) - ] - ) - - -def test_load_non_existent_data(): - """Test loading data that doesn't exist in the store verifies the destination remains unchanged.""" - src_block_data = [ - torch.randint(0, 1000, (1, 100), dtype=torch.int) for _ in range(5) - ] - dst_block_data = [ - torch.empty(data.shape, dtype=data.dtype) for data in src_block_data - ] - block_ids = [tensor_hash(data) for data in src_block_data] - offset = [0] * len(block_ids) - store = UcmMooncakeStore() - masks = store.lookup(block_ids) - assert all(mask is False for mask in masks) - - task: Task = store.load( - block_ids=block_ids, offset=offset, dst_tensor=dst_block_data - ) - ret = store.wait(task) - assert ret != 0 - assert all( - [ - torch.equal(src_block_data[i], dst_block_data[i]) is False - for i in range(len(src_block_data)) - ] - ) From b6372874d005af3f37c37d4f4436fe211fbcbe6c Mon Sep 17 00:00:00 2001 From: propanone1006 <1035067916@qq.com> Date: Fri, 29 Aug 2025 14:56:57 +0800 Subject: [PATCH 10/12] [Feat!] [Del] Adjust the mooncake configuration method, remove the configuration file method, and only retain the parameter transmission method --- unifiedcache/ucm_connector/ucm_mooncake.py | 62 +--------------------- 1 file changed, 1 insertion(+), 61 deletions(-) diff --git a/unifiedcache/ucm_connector/ucm_mooncake.py b/unifiedcache/ucm_connector/ucm_mooncake.py index cd5be442..7adfd350 100644 --- a/unifiedcache/ucm_connector/ucm_mooncake.py +++ b/unifiedcache/ucm_connector/ucm_mooncake.py @@ -48,41 +48,6 @@ def load_from_dict(config: Dict = {}) -> "MooncakeStoreConfig": master_server_address=config.get("master_server_address"), ) - @staticmethod - def from_file(file_path: str) -> "MooncakeStoreConfig": - """ - # NOTE: - # This method currently loads connection information from a file. - # In the future, it should be updated to load configuration from a YAML file, - # as the UC connector plans to support YAML-based config. - """ - """Load the config from a JSON file.""" - with open(file_path) as fin: - config = json.load(fin) - return MooncakeStoreConfig( - local_hostname=config.get("local_hostname"), - metadata_server=config.get("metadata_server"), - global_segment_size=config.get( - "global_segment_size", DEFAULT_GLOBAL_SEGMENT_SIZE - ), - local_buffer_size=config.get( - "local_buffer_size", DEFAULT_LOCAL_BUFFER_SIZE - ), - protocol=config.get("protocol", "tcp"), - device_name=config.get("device_name", ""), - master_server_address=config.get("master_server_address"), - ) - - @staticmethod - def load_from_env() -> "MooncakeStoreConfig": - """Load config from a file specified in the environment variable.""" - config_file_path = os.getenv("MOONCAKE_CONFIG_PATH") - if config_file_path is None: - raise ValueError( - "The environment variable 'MOONCAKE_CONFIG_PATH' is not set." - ) - return MooncakeStoreConfig.from_file(config_file_path) - @dataclass class MooncakeTask(Task): @@ -129,32 +94,7 @@ def __init__(self, config: Dict = {}): logger.error("Configuration loading failed: %s", e) raise except TypeError: - logger.warning( - "Lack of configuration, loading Mooncake configuration from environment variables instead." - ) - try: - mooncake_config = MooncakeStoreConfig.load_from_env() - logger.info("Mooncake Configuration loaded from env successfully.") - self.store.setup( - mooncake_config.local_hostname, - mooncake_config.metadata_server, - mooncake_config.global_segment_size, - mooncake_config.local_buffer_size, - mooncake_config.protocol, - mooncake_config.device_name, - mooncake_config.master_server_address, - ) - except ValueError as e: - logger.error( - "Configuration loading failed: %s \n Please check the dict params or edit the config file and set path.", - e, - ) - raise - except Exception as exc: - logger.error( - "An error occurred while loading the configuration: %s", exc - ) - raise + logger.warning("Lack of configuration, please check the dict params .") except Exception as exc: logger.error("An error occurred while loading the configuration: %s", exc) From 82c418e47a7ba1f0c7fd63d8eaeedde252dd3cd9 Mon Sep 17 00:00:00 2001 From: propanone1006 <1035067916@qq.com> Date: Fri, 29 Aug 2025 15:43:23 +0800 Subject: [PATCH 11/12] [Doc] [Fix] modifiy the performance figure of Mooncake Store. --- .../getting-started/example/mooncake_conn.md | 2 -- .../images/mooncake_default_performance.png | Bin 41527 -> 0 bytes 2 files changed, 2 deletions(-) delete mode 100644 docs/source/images/mooncake_default_performance.png diff --git a/docs/source/getting-started/example/mooncake_conn.md b/docs/source/getting-started/example/mooncake_conn.md index 4b9f0a55..34561fc3 100644 --- a/docs/source/getting-started/example/mooncake_conn.md +++ b/docs/source/getting-started/example/mooncake_conn.md @@ -15,8 +15,6 @@ This document provides a usage example and configuration guide for the **Mooncak Use mooncake fig && default:

UCM - - UCM

## Features diff --git a/docs/source/images/mooncake_default_performance.png b/docs/source/images/mooncake_default_performance.png deleted file mode 100644 index 6eff9fd42da1dcf7683d2780000a7f06aaf041df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41527 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 Date: Fri, 29 Aug 2025 17:05:01 +0800 Subject: [PATCH 12/12] [Feat] add __del__() to shutdown all the mooncake components --- unifiedcache/ucm_connector/ucm_mooncake.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/unifiedcache/ucm_connector/ucm_mooncake.py b/unifiedcache/ucm_connector/ucm_mooncake.py index 7adfd350..34942826 100644 --- a/unifiedcache/ucm_connector/ucm_mooncake.py +++ b/unifiedcache/ucm_connector/ucm_mooncake.py @@ -113,6 +113,13 @@ def __init__(self, config: Dict = {}): self.thread = threading.Thread(target=self._run_event_loop, daemon=True) self.thread.start() + def __del__(self): + """Release resources on garbage collection.""" + try: + self.shutdown() + except Exception: + pass + def _run_event_loop(self): """Run the asyncio event loop in a separate thread.""" asyncio.set_event_loop(self.loop)