Encoder Global Cache Manager (#16137)

Co-authored-by: Zheng Wengang <zwg0606@gmail.com>
Co-authored-by: Teng Ma <sima.mt@alibaba-inc.com>
This commit is contained in:
siyu
2026-02-25 10:05:43 +08:00
committed by GitHub
parent b7af58b9af
commit 245430eaac
5 changed files with 730 additions and 53 deletions

View File

@@ -8,7 +8,7 @@ import pickle
import time
import traceback
from http import HTTPStatus
from typing import Dict, List, Optional, Set, Tuple
from typing import Dict, List, Optional, Set, Tuple, Union
import aiohttp
import numpy as np
@@ -26,6 +26,7 @@ from sglang.srt.configs.model_config import ModelConfig
from sglang.srt.disaggregation.encode_receiver import EmbeddingData
from sglang.srt.distributed.parallel_state import (
get_mooncake_transfer_engine,
get_tp_group,
init_distributed_environment,
initialize_model_parallel,
)
@@ -206,6 +207,22 @@ class MMEncoder:
self.schedule_socket = get_zmq_socket(
self.context, zmq.PULL, schedule_path, True
)
self.background_tasks: Set[asyncio.Task] = set()
if self.server_args.enable_mm_global_cache:
from sglang.srt.mem_cache.storage.mooncake_store.embedding_cache_controller import (
EmbeddingCacheController,
)
self.mm_global_cache = EmbeddingCacheController(
rank,
server_args.tp_size,
hidden_dim=self.model_config.hidden_size,
tp_group=get_tp_group().cpu_group,
all_rank_get=False,
)
else:
self.mm_global_cache = None
if self.rank == 0:
logger.info(
@@ -214,10 +231,10 @@ class MMEncoder:
if self.server_args.encoder_transfer_backend == "mooncake":
self.local_ip = get_local_ip_auto()
self.engine = get_mooncake_transfer_engine()
self.embedding_to_send = dict()
self.background_tasks: Set[asyncio.Task] = set()
logger.info(f"rank {rank} init finish ")
@@ -312,6 +329,233 @@ class MMEncoder:
async_futures = [asyncio.wrap_future(f) for f in futures]
return await asyncio.gather(*async_futures)
def get_num_patches(self, grid: Union[torch.Tensor, List[int]]) -> int:
"""Calculate number of raw patches (before 2x2 merge). Used for pixel_values slicing."""
return int(grid[0] * grid[1] * grid[2])
def get_num_tokens(self, grid: Union[torch.Tensor, List[int]]) -> int:
"""Calculate number of tokens (after 2x2 merge). Used for mm_embedding slicing."""
merge_size = getattr(self.image_processor, "merge_size", 2)
return self.get_num_patches(grid) // (merge_size**2)
def slice_embedding(
self, mm_embedding: torch.Tensor, grid_thw: List
) -> List[torch.Tensor]:
"""Slice a concatenated embedding tensor into individual image embeddings."""
slices, offset = [], 0
for grid in grid_thw:
count = self.get_num_tokens(grid)
slices.append(mm_embedding[offset : offset + count])
offset += count
return slices
def _calculate_hashes_from_features(
self, pixel_values: torch.Tensor, grid_thw: List
) -> List[str]:
"""CPU Task: Compute hashes based on processed feature patches (pixel_values)."""
hashes, offset = [], 0
for grid in grid_thw:
num_patches = self.get_num_patches(grid)
feature_slice = pixel_values[offset : offset + num_patches]
tmp_item = MultimodalDataItem(
modality=Modality.IMAGE, feature=feature_slice
)
tmp_item.set_pad_value()
hashes.append(tmp_item.hash)
offset += num_patches
return hashes
async def _encode_missing(
self, pixel_values: torch.Tensor, images_input: dict, indices: List[int]
) -> List[torch.Tensor]:
"""
GPU Task: Run ViT inference ONLY on the subset of images missing from the cache.
"""
grid_thw = images_input["image_grid_thw"]
# 1. Slice pixel_values to get only the patches for missing images
sub_pixel_list = []
offsets = [0]
curr = 0
for g in grid_thw:
curr += self.get_num_patches(g)
offsets.append(curr)
for idx in indices:
sub_pixel_list.append(pixel_values[offsets[idx] : offsets[idx + 1]])
sub_feature = torch.cat(sub_pixel_list, dim=0)
mm_item = MultimodalDataItem.from_dict(
{
"modality": Modality.IMAGE,
"feature": _convert(sub_feature),
}
)
for k, v in images_input.items():
if k == "pixel_values":
continue
val = _convert(v)
if k in _image_grid_attrs:
mm_item.set(k, val[indices])
else:
mm_item.set(k, val)
with torch.inference_mode():
new_embeddings = self.model.get_image_feature([mm_item]).cpu()
if new_embeddings.ndim != 2:
new_embeddings = new_embeddings.reshape(-1, new_embeddings.shape[-1])
sub_grids = [grid_thw[i] for i in indices]
return self.slice_embedding(new_embeddings, sub_grids)
async def encode_with_global_cache(
self,
mm_items,
req_id: str,
num_parts: int,
part_idx: int,
hashes: Optional[List[str]] = None,
) -> torch.Tensor:
images = await self._flatten_and_load_images(mm_items)
kwargs = {"device": self.device} if self.use_image_processor_gpu else {}
images_input = self.image_processor(images=images, **kwargs)
pixel_values = images_input["pixel_values"]
grid_thw = images_input["image_grid_thw"]
num_images = len(grid_thw)
# Step 1: Rank 0 checks global cache and broadcasts hit/miss mask to all ranks.
if self.rank == 0:
if hashes is None:
image_hashes = self._calculate_hashes_from_features(
pixel_values, grid_thw
)
else:
image_hashes = hashes
exist_mask = await self.mm_global_cache.batch_is_exist(image_hashes)
mask_tensor = torch.tensor(
[1 if e else 0 for e in exist_mask], dtype=torch.int32
)
else:
image_hashes = None
mask_tensor = torch.zeros(num_images, dtype=torch.int32)
if self.server_args.tp_size > 1:
torch.distributed.broadcast(
mask_tensor,
src=0,
group=self.mm_global_cache.prefetch_tp_group,
)
exist_mask = [m.item() == 1 for m in mask_tensor]
missing_indices = [i for i, e in enumerate(exist_mask) if not e]
hit_indices = [i for i, e in enumerate(exist_mask) if e]
# Step 2: All ranks run ViT together on cache-miss images.
new_slices = []
if missing_indices:
new_slices = await self._encode_missing(
pixel_values, images_input, missing_indices
)
# Step 3: Rank 0 prefetches cache-hit embeddings from global cache.
prefetch_status = torch.tensor([1], dtype=torch.int32)
if self.rank == 0:
if hit_indices:
hit_hashes = [image_hashes[i] for i in hit_indices]
hit_tokens = [self.get_num_tokens(grid_thw[i]) for i in hit_indices]
self.mm_global_cache.prefetch(req_id, hit_hashes, hit_tokens)
try:
async def _wait_prefetch():
while not self.mm_global_cache.check_prefetch_progress(req_id):
await asyncio.sleep(0.005)
await asyncio.wait_for(_wait_prefetch(), timeout=60.0)
except (asyncio.TimeoutError, Exception) as e:
logger.error(
f"Prefetch failed for req {req_id}: {e}. "
f"Falling back to ViT for {len(hit_indices)} hit images."
)
prefetch_status[0] = 0
# Step 4: Broadcast prefetch result to all ranks so they stay in sync.
if self.server_args.tp_size > 1:
torch.distributed.broadcast(
prefetch_status,
src=0,
group=self.mm_global_cache.prefetch_tp_group,
)
# Step 5: If prefetch failed, all ranks fallback to ViT for the hit images.
if prefetch_status.item() == 0 and hit_indices:
logger.info(
f"Req {req_id}: Prefetch failed, all ranks running ViT fallback "
f"for {len(hit_indices)} images."
)
fallback_slices = await self._encode_missing(
pixel_values, images_input, hit_indices
)
else:
fallback_slices = None
# Step 6: Rank 0 assembles final embedding and prepares for sending.
if self.rank == 0:
final_slices = [None] * num_images
for i, idx in enumerate(missing_indices):
final_slices[idx] = new_slices[i]
# Fill in cache-hit embeddings (from prefetch or fallback)
if prefetch_status.item() == 1 and hit_indices:
cached_slices = self.mm_global_cache.get_embeddings(
[image_hashes[i] for i in hit_indices]
)
for i, idx in enumerate(hit_indices):
final_slices[idx] = cached_slices[i]
elif fallback_slices is not None:
for i, idx in enumerate(hit_indices):
final_slices[idx] = fallback_slices[i]
mm_embedding = torch.cat(final_slices, dim=0)
# Background insert: store newly computed embeddings into global cache.
# Includes both original misses and fallback-recomputed hits.
all_new_hashes = [image_hashes[i] for i in missing_indices]
all_new_slices = list(new_slices)
if fallback_slices is not None:
all_new_hashes += [image_hashes[i] for i in hit_indices]
all_new_slices += list(fallback_slices)
if all_new_hashes:
async def _background_insert():
await asyncio.to_thread(
self.mm_global_cache.insert_batch,
all_new_hashes,
all_new_slices,
)
task = asyncio.create_task(_background_insert())
self.background_tasks.add(task)
task.add_done_callback(self.background_tasks.discard)
self.embedding_to_send[req_id] = EmbeddingData(
req_id, num_parts, part_idx, grid_thw, mm_embedding
)
return (
mm_embedding.nbytes,
mm_embedding.shape[0],
mm_embedding.shape[1],
None,
None,
)
else:
return (0, 0, 0, None, None)
async def _encode(self, mm_items) -> torch.Tensor:
try:
images = await self._flatten_and_load_images(mm_items)
@@ -421,6 +665,9 @@ class MMEncoder:
await asyncio.get_event_loop().run_in_executor(self.executor, send_with_socket)
async def encode_with_hash(self, mm_items, req_id, num_parts, part_idx, hashes):
images = await self._flatten_and_load_images(mm_items)
async def encode(self, mm_items, req_id, num_parts, part_idx):
try:
image_grid_dim, mm_embedding = await self._encode(mm_items)
@@ -644,12 +891,21 @@ async def run_encoder(
else:
encoder.profiler.stop()
else:
await encoder.encode(
mm_items=request["mm_items"],
req_id=request["req_id"],
num_parts=request["num_parts"],
part_idx=request["part_idx"],
)
if encoder.mm_global_cache is not None:
await encoder.encode_with_global_cache(
mm_items=request["mm_items"],
req_id=request["req_id"],
num_parts=request["num_parts"],
part_idx=request["part_idx"],
hashes=request.get("hashes", None),
)
else:
await encoder.encode(
mm_items=request["mm_items"],
req_id=request["req_id"],
num_parts=request["num_parts"],
part_idx=request["part_idx"],
)
def launch_encoder(server_args, schedule_path, dist_init_method, rank):
@@ -706,15 +962,25 @@ async def handle_encode_request(request: dict):
request.update({"enter_time": time.time()})
for socket in send_sockets:
socket.send_pyobj(request)
nbytes, embedding_len, embedding_dim, error_msg, error_code = (
await encoder.encode(
mm_items=request["mm_items"],
req_id=request["req_id"],
num_parts=request["num_parts"],
part_idx=request["part_idx"],
if encoder.mm_global_cache is not None:
nbytes, embedding_len, embedding_dim, error_msg, error_code = (
await encoder.encode_with_global_cache(
mm_items=request["mm_items"],
req_id=request["req_id"],
num_parts=request["num_parts"],
part_idx=request["part_idx"],
hashes=request.get("hashes", None),
)
)
else:
nbytes, embedding_len, embedding_dim, error_msg, error_code = (
await encoder.encode(
mm_items=request["mm_items"],
req_id=request["req_id"],
num_parts=request["num_parts"],
part_idx=request["part_idx"],
)
)
)
if error_msg:
if encoder.server_args.encoder_transfer_backend == "zmq_to_scheduler":

View File

@@ -0,0 +1,315 @@
import asyncio
import logging
import threading
import time
from queue import Empty, Queue
from typing import List, Optional
import torch
from sglang.srt.mem_cache.storage.mooncake_store.mooncake_embedding_store import (
MooncakeEmbeddingStore,
)
logger = logging.getLogger(__name__)
class ContiguousMemoryAllocator:
"""
A simple allocator to manage variable-sized contiguous blocks
within a large pre-allocated flat buffer.
"""
def __init__(self, total_size_bytes: int):
self.total_size = total_size_bytes
# List of (offset, size) for free blocks
self.free_blocks = [(0, total_size_bytes)]
self.allocated_map = {} # {handle: (offset, size)}
self.lock = threading.Lock()
def allocate(self, size_bytes: int) -> Optional[int]:
with self.lock:
# Simple First-Fit allocation
for i, (offset, block_size) in enumerate(self.free_blocks):
if block_size >= size_bytes:
# Allocate from this block
remaining_size = block_size - size_bytes
if remaining_size > 0:
self.free_blocks[i] = (offset + size_bytes, remaining_size)
else:
self.free_blocks.pop(i)
return offset
return None
def free(self, offset: int, size_bytes: int):
with self.lock:
# Return block and merge adjacent free blocks
self.free_blocks.append((offset, size_bytes))
self.free_blocks.sort()
merged = []
if not self.free_blocks:
return
curr_offset, curr_size = self.free_blocks[0]
for next_offset, next_size in self.free_blocks[1:]:
if curr_offset + curr_size == next_offset:
curr_size += next_size
else:
merged.append((curr_offset, curr_size))
curr_offset, curr_size = next_offset, next_size
merged.append((curr_offset, curr_size))
self.free_blocks = merged
class EmbeddingPrefetchOperation:
"""Groups all missing images of a request for a single batch GET."""
def __init__(self, req_id: str, keys: List[str], ptrs: List[int], sizes: List[int]):
self.req_id = req_id
self.keys = keys
self.ptrs = ptrs
self.sizes = sizes
self.is_finished = False
self.success = False
self._lock = threading.Lock()
def mark_done(self, success: bool):
with self._lock:
self.success = success
self.is_finished = True
class EmbeddingInsertOperation:
"""Groups all newly computed images of a request for a single batch PUT."""
def __init__(self, keys: List[str], ptrs: List[int], sizes: List[int]):
self.keys = keys
self.ptrs = ptrs
self.sizes = sizes
class EmbeddingCacheController:
def __init__(
self,
tp_rank,
tp_size,
max_pool_size_gb=4.0,
hidden_dim=1024,
tp_group=None,
all_rank_get=False,
):
self.tp_world_size = tp_size
self.tp_group = tp_group
self.all_rank_get = all_rank_get
self.hidden_dim = hidden_dim
self.element_size = torch.float32.itemsize
# 1. Mooncake Backend & Pinned Buffer
self.mooncake_store = MooncakeEmbeddingStore()
self.total_pool_size_bytes = int(max_pool_size_gb * 1024**3)
self.cpu_pool = torch.empty(
self.total_pool_size_bytes, dtype=torch.uint8, pin_memory=True
)
self.mooncake_store.register_buffer(self.cpu_pool)
# 2. Variable Size Memory Management
self.allocator = ContiguousMemoryAllocator(self.total_pool_size_bytes)
self.hash_to_metadata = {} # {image_hash: (offset, num_tokens, size_bytes)}
# 3. Task Tracking
self.ongoing_prefetch = {} # {req_id: EmbeddingPrefetchOperation}
self.prefetch_queue = Queue()
self.insert_queue = Queue()
self.lock = threading.Lock()
self.stop_event = threading.Event()
self.io_thread = threading.Thread(target=self._io_loop, daemon=True)
self.io_thread.start()
if self.tp_world_size > 1:
if self.tp_group is None:
raise ValueError("tp_group must be provided when tp_size > 1")
from sglang.srt.distributed.parallel_state import (
create_custom_parallel_group,
)
group_ranks = torch.distributed.get_process_group_ranks(self.tp_group)
self.prefetch_tp_group = create_custom_parallel_group(
group_ranks=group_ranks, backend="gloo"
)
else:
self.prefetch_tp_group = None
def prefetch(
self, req_id: str, image_hashes: List[str], expected_tokens: List[int]
):
"""Issues ONE batch GET for all missing images in the request."""
keys, ptrs, sizes = [], [], []
with self.lock:
for h, num_tokens in zip(image_hashes, expected_tokens):
if h in self.hash_to_metadata:
logger.debug(
f"Req {req_id}: Hash already in local metadata, skipping prefetch."
)
continue
size_bytes = num_tokens * self.hidden_dim * self.element_size
offset = self.allocator.allocate(size_bytes)
if offset is None:
continue
self.hash_to_metadata[h] = (offset, num_tokens, size_bytes)
keys.append(h)
ptrs.append(self.cpu_pool.data_ptr() + offset)
sizes.append(size_bytes)
if not keys:
return
logger.info(
f"Req {req_id}: Starting global fetch for {len(keys)} images from Mooncake."
)
op = EmbeddingPrefetchOperation(req_id, keys, ptrs, sizes)
self.ongoing_prefetch[req_id] = op
self.prefetch_queue.put(op)
def insert_batch(
self, image_hashes: List[str], embedding_tensors: List[torch.Tensor]
):
"""Issues ONE batch PUT for all embeddings computed by this request."""
keys, ptrs, sizes = [], [], []
with self.lock:
for h, tensor in zip(image_hashes, embedding_tensors):
if h in self.hash_to_metadata:
continue
num_tokens = tensor.shape[0]
size_bytes = num_tokens * self.hidden_dim * self.element_size
offset = self.allocator.allocate(size_bytes)
if offset is None:
continue
# Copy to pinned pool for RDMA
self.hash_to_metadata[h] = (offset, num_tokens, size_bytes)
target_view = (
self.cpu_pool[offset : offset + size_bytes]
.view(torch.float32)
.view(num_tokens, self.hidden_dim)
)
target_view.copy_(tensor.cpu())
keys.append(h)
ptrs.append(self.cpu_pool.data_ptr() + offset)
sizes.append(size_bytes)
if keys:
logger.info(
f"Global Cache: Inserting {len(keys)} new embeddings into Mooncake cluster."
)
self.insert_queue.put(EmbeddingInsertOperation(keys, ptrs, sizes))
def _io_loop(self):
"""Asynchronous worker handling both Batch GET and Batch PUT."""
while not self.stop_event.is_set():
processed_any = False
try:
op = self.prefetch_queue.get_nowait()
results = self.mooncake_store.batch_get(op.keys, op.ptrs, op.sizes)
success_count = sum(results)
logger.info(
f"Mooncake GET Finished: Req {op.req_id}, Successfully fetched {success_count}/{len(op.keys)} images."
)
op.mark_done(all(results))
self.prefetch_queue.task_done()
processed_any = True
except Empty:
pass
try:
op = self.insert_queue.get_nowait()
self.mooncake_store.batch_put(op.keys, op.ptrs, op.sizes)
logger.info(
f"Mooncake PUT Finished: Successfully stored {len(op.keys)} keys in cluster."
)
self.insert_queue.task_done()
processed_any = True
except Empty:
pass
if not processed_any:
time.sleep(0.001)
def check_prefetch_progress(self, req_id: str) -> bool:
"""TP-Group barrier: ensures all cards have the request batch ready."""
local_ready = False
with self.lock:
if req_id not in self.ongoing_prefetch:
local_ready = True
else:
op = self.ongoing_prefetch[req_id]
if op.is_finished:
local_ready = op.success
if self.all_rank_get and self.tp_world_size > 1:
ready_tensor = torch.tensor(
[1 if local_ready else 0], dtype=torch.int, device="cpu"
)
torch.distributed.all_reduce(
ready_tensor,
op=torch.distributed.ReduceOp.MIN,
group=self.prefetch_tp_group,
)
local_ready = ready_tensor.item() == 1
if local_ready:
with self.lock:
self.ongoing_prefetch.pop(req_id, None)
return True
return False
def get_embeddings(self, image_hashes: List[str]) -> List[torch.Tensor]:
"""Final reconstruction for model input."""
with self.lock:
tensors = []
for h in image_hashes:
offset, num_tokens, size_bytes = self.hash_to_metadata[h]
tensors.append(
self.cpu_pool[offset : offset + size_bytes]
.view(torch.float32)
.view(num_tokens, self.hidden_dim)
)
return tensors
async def batch_is_exist(self, image_hashes: List[str]) -> List[bool]:
with self.lock:
local_results = [h in self.hash_to_metadata for h in image_hashes]
local_hit_count = sum(local_results)
global_hit_count = 0
if not all(local_results):
missing_indices = [i for i, res in enumerate(local_results) if not res]
missing_hashes = [image_hashes[i] for i in missing_indices]
global_exists = await asyncio.to_thread(
self.mooncake_store.batch_is_exist, missing_hashes
)
global_hit_count = sum(global_exists)
for i, exists in zip(missing_indices, global_exists):
local_results[i] = exists
total = len(image_hashes)
miss_count = total - local_hit_count - global_hit_count
logger.info(
f"=== Multi-Level Cache Check === "
f"Total: {total} | "
f"Local Hits: {local_hit_count} | "
f"Global Hits: {global_hit_count} | "
f"Misses (GPU Work): {miss_count}"
)
return local_results

View File

@@ -0,0 +1,68 @@
import logging
from typing import Any, List
from sglang.srt.mem_cache.storage.mooncake_store.mooncake_store import MooncakeBaseStore
logger = logging.getLogger(__name__)
class MooncakeEmbeddingStore(MooncakeBaseStore):
def __init__(
self,
storage_config: Any = None,
):
super().__init__()
MooncakeDistributedStore = self._import_mooncake_store()
self.store = MooncakeDistributedStore()
self.config = self._load_config(storage_config)
ret_code = self.store.setup(
self.config.local_hostname,
self.config.metadata_server,
self.config.global_segment_size,
16 * 1024 * 1024, # Internal local buffer size
self.config.protocol,
self.config.device_name,
self.config.master_server_address,
)
if ret_code != 0:
raise RuntimeError(f"Failed to setup Mooncake Embedding Store: {ret_code}")
logger.info("Mooncake Embedding Store initialized successfully.")
def get_key(self, image_hash: str) -> str:
return f"emb_{image_hash}"
def batch_get(
self, hashes: List[str], ptrs: List[int], sizes: List[int]
) -> List[bool]:
keys = [self.get_key(h) for h in hashes]
results = self.store.batch_get_into(keys, ptrs, sizes)
return [res > 0 for res in results]
def batch_put(
self, hashes: List[str], ptrs: List[int], sizes: List[int]
) -> List[bool]:
keys = [self.get_key(h) for h in hashes]
exists = self.store.batch_is_exist(keys)
put_keys, put_ptrs, put_sizes, indices = [], [], [], []
success_map = [True] * len(hashes)
for i, status in enumerate(exists):
if status != 1:
put_keys.append(keys[i])
put_ptrs.append(ptrs[i])
put_sizes.append(sizes[i])
indices.append(i)
if put_keys:
results = self.store.batch_put_from(put_keys, put_ptrs, put_sizes)
for i, res in enumerate(results):
success_map[indices[i]] = res == 0
return success_map
def batch_is_exist(self, hashes: List[str]) -> List[bool]:
keys = [self.get_key(h) for h in hashes]
results = self.store.batch_is_exist(keys)
return [res == 1 for res in results]

View File

@@ -222,47 +222,74 @@ class MooncakeStoreConfig:
)
class MooncakeStore(HiCacheStorage):
class MooncakeBaseStore:
def __init__(self):
self.store = None
self.config = None
def _import_mooncake_store(self):
try:
from mooncake.store import MooncakeDistributedStore
return MooncakeDistributedStore
except ImportError as e:
raise ImportError(
"Please install mooncake by following the instructions at "
"https://kvcache-ai.github.io/Mooncake/getting_started/build.html "
"to run SGLang with MooncakeConnector."
) from e
def _load_config(self, storage_config: Any = None):
extra_config = (
getattr(storage_config, "extra_config", None) if storage_config else None
)
if extra_config and (
extra_config.get("master_server_address") is not None
or extra_config.get("client_server_address") is not None
):
config = MooncakeStoreConfig.load_from_extra_config(extra_config)
logger.info("Mooncake Configuration loaded from extra_config successfully.")
elif envs.SGLANG_HICACHE_MOONCAKE_CONFIG_PATH.is_set():
config = MooncakeStoreConfig.from_file()
logger.info("Mooncake Configuration loaded from file successfully.")
else:
config = MooncakeStoreConfig.load_from_env()
logger.info("Mooncake Configuration loaded from env successfully.")
return config
def register_buffer(self, tensor: torch.Tensor):
if self.store is None:
raise RuntimeError("Mooncake store is not initialized.")
ptr = tensor.data_ptr()
size = tensor.numel() * tensor.element_size()
ret_code = self.store.register_buffer(ptr, size)
if ret_code != 0:
logger.error(f"Failed to register buffer, error code: {ret_code}")
raise RuntimeError(
f"Failed to register buffer to Mooncake Store, error code: {ret_code}"
)
class MooncakeStore(HiCacheStorage, MooncakeBaseStore):
def __init__(
self, storage_config: HiCacheStorageConfig = None, mem_pool: HostKVCache = None
):
try:
from mooncake.store import MooncakeDistributedStore
except ImportError as e:
raise ImportError(
"Please install mooncake by following the instructions at "
"https://kvcache-ai.github.io/Mooncake/getting_started/build.html"
"to run SGLang with MooncakeConnector."
) from e
MooncakeBaseStore.__init__(self)
MooncakeDistributedStore = self._import_mooncake_store()
try:
self.store = MooncakeDistributedStore()
self.config = self._load_config(storage_config)
extra_config = (
getattr(storage_config, "extra_config", None)
if storage_config
else None
)
# Load configuration with master_server_address prioritized from extra_config if available
if extra_config is not None and (
extra_config.get("master_server_address") is not None
or extra_config.get("client_server_address") is not None
):
# Load from extra_config
self.config = MooncakeStoreConfig.load_from_extra_config(extra_config)
logger.info(
"Mooncake Configuration loaded from extra_config successfully."
)
elif envs.SGLANG_HICACHE_MOONCAKE_CONFIG_PATH.is_set():
# Load from config file
self.config = MooncakeStoreConfig.from_file()
logger.info("Mooncake Configuration loaded from file successfully.")
else:
# Load from environment variables
self.config = MooncakeStoreConfig.load_from_env()
logger.info("Mooncake Configuration loaded from env successfully.")
tp_scale_factor = 1 if storage_config is None else storage_config.tp_size
per_tp_global_segment_size = (
@@ -442,14 +469,7 @@ class MooncakeStore(HiCacheStorage):
], "mooncake store storage backend only support page first or page first direct layout"
buffer = self.mem_pool_host.kv_buffer
try:
buffer_ptr = buffer.data_ptr()
buffer_size = buffer.numel() * buffer.element_size()
ret_code = self.store.register_buffer(buffer_ptr, buffer_size)
if ret_code:
logger.error(f"Failed to register buffer, error code: {ret_code}")
raise RuntimeError(
f"Failed to register buffer to Mooncake Store, error code: {ret_code}"
)
super().register_buffer(buffer)
except TypeError as err:
logger.error("Failed to register buffer to Mooncake Store: %s", err)
raise TypeError("Mooncake Store Register Buffer Error.") from err

View File

@@ -692,6 +692,7 @@ class ServerArgs:
mm_enable_dp_encoder: bool = False
mm_process_config: Optional[Dict[str, Any]] = None
limit_mm_data_per_request: Optional[Union[str, Dict[str, int]]] = None
enable_mm_global_cache: bool = False
# For checkpoint decryption
decrypted_config_file: Optional[str] = None
@@ -5068,6 +5069,13 @@ class ServerArgs:
help="Enable prefix multimodal cache. Currently only supports mm-only.",
)
parser.add_argument(
"--enable-mm-global-cache",
action="store_true",
default=ServerArgs.enable_mm_global_cache,
help="Enable global multimodal embedding cache to skip redundant ViT inference.",
)
# For registering hooks
parser.add_argument(
"--forward-hooks",