Skip to content

Commit d30829c

Browse files
committed
use new mooncake API and enable zero-copy
Signed-off-by: 0oshowero0 <o0shower0o@outlook.com> try: use new mooncake API and enable zero-copy Signed-off-by: 0oshowero0 <o0shower0o@outlook.com> update Signed-off-by: 0oshowero0 <o0shower0o@outlook.com> update Signed-off-by: 0oshowero0 <o0shower0o@outlook.com> fix Signed-off-by: 0oshowero0 <o0shower0o@outlook.com>
1 parent 5f05587 commit d30829c

2 files changed

Lines changed: 239 additions & 38 deletions

File tree

transfer_queue/storage/clients/mooncake_client.py

Lines changed: 67 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,15 @@
2323

2424
from transfer_queue.storage.clients.base import TransferQueueStorageKVClient
2525
from transfer_queue.storage.clients.factory import StorageClientFactory
26+
from transfer_queue.utils.tensor_utils import allocate_empty_tensors, get_nbytes, merge_continues_memory
2627

2728
logger = logging.getLogger(__name__)
2829
logger.setLevel(os.getenv("TQ_LOGGING_LEVEL", logging.WARNING))
2930

3031
MOONCAKE_STORE_IMPORTED: bool = True
3132
try:
32-
from mooncake.store import MooncakeDistributedStore
33+
from mooncake.store import MooncakeDistributedStore, ReplicateConfig
34+
3335
except ImportError:
3436
MOONCAKE_STORE_IMPORTED = False
3537

@@ -78,10 +80,9 @@ def __init__(self, config: dict[str, Any]):
7880
if not self.metadata_server.startswith("etcd://") and not self.metadata_server.endswith("/metadata"):
7981
self.metadata_server = self.metadata_server + "/metadata"
8082

81-
if self.metadata_server is None:
82-
raise ValueError("Missing 'metadata_server' in config")
83-
if self.master_server_address is None:
84-
raise ValueError("Missing 'master_server_address' in config")
83+
self.replica_config = ReplicateConfig()
84+
# FIXME: hard_pin is not supported yet
85+
# self.replica_config.with_hard_pin = True
8586

8687
self._store = MooncakeDistributedStore()
8788
ret = self._store.setup(
@@ -116,12 +117,8 @@ def put(self, keys: list[str], values: list[Any]) -> Optional[list[Any]]:
116117

117118
for key, value in zip(keys, values, strict=True):
118119
if isinstance(value, torch.Tensor):
119-
tensor = value.contiguous()
120-
# TODO: use gpu direct rdma instead
121-
if tensor.device.type == "cuda":
122-
tensor = tensor.cpu()
123120
tensor_keys.append(key)
124-
tensor_values.append(tensor)
121+
tensor_values.append(value)
125122
else:
126123
non_tensor_keys.append(key)
127124
non_tensor_values.append(pickle.dumps(value))
@@ -139,38 +136,50 @@ def _batch_put_tensors(self, keys: list[str], tensors: list[Tensor]):
139136
batch_keys = keys[i : i + BATCH_SIZE_LIMIT]
140137
batch_tensors = tensors[i : i + BATCH_SIZE_LIMIT]
141138

142-
results = self._store.batch_put_tensor(batch_keys, batch_tensors)
139+
batch_ptrs, batch_sizes = self._preprocess_tensors_for_put(batch_tensors)
140+
batch_ptr_reduced, batch_sizes_reduced = merge_continues_memory(batch_ptrs, batch_sizes)
141+
self._register_all_buffers(batch_ptr_reduced, batch_sizes_reduced)
142+
143+
results = self._store.batch_upsert_from(batch_keys, batch_ptrs, batch_sizes, config=self.replica_config)
143144
if not all(r == 0 for r in results):
144145
failed_indices = [j for j, r in enumerate(results) if r != 0]
145146
error_codes = [results[j] for j in failed_indices]
146147
raise RuntimeError(
147148
f"batch_put_tensor failed for indices {failed_indices} with error codes: {error_codes}"
148149
)
149150

151+
self._unregister_all_buffers(batch_ptr_reduced)
152+
150153
def _batch_put_bytes(self, keys: list[str], values: list[bytes]):
151154
for i in range(0, len(keys), BATCH_SIZE_LIMIT):
152155
batch_keys = keys[i : i + BATCH_SIZE_LIMIT]
153156
batch_values = values[i : i + BATCH_SIZE_LIMIT]
154157

155-
ret = self._store.put_batch(batch_keys, batch_values)
158+
ret = self._store.upsert_batch(batch_keys, batch_values, self.replica_config)
156159
if ret != 0:
157160
raise RuntimeError(f"put_batch failed with error code: {ret}")
158161

159-
def get(self, keys: list[str], shapes=None, dtypes=None, custom_backend_meta=None) -> list[Any]:
162+
def get(
163+
self,
164+
keys: list[str],
165+
shapes: Optional[list[Any]] = None,
166+
dtypes: Optional[list[Any]] = None,
167+
custom_backend_meta: Optional[list[str]] = None,
168+
) -> list[Any]:
160169
"""Get multiple key-value pairs from MooncakeStore.
161170
162171
Args:
163-
keys (List[str]): Keys to fetch.
164-
shapes (List[List[int]]): Expected tensor shapes (use [] for scalars).
165-
dtypes (List[Optional[torch.dtype]]): Expected dtypes; use None for non-tensor data.
166-
custom_backend_meta (List[str], optional): ...
172+
keys: Keys to fetch.
173+
shapes: Expected tensor shapes (use [] for scalars).
174+
dtypes: Expected dtypes; use None for non-tensor data.
175+
custom_backend_meta: Optional custom backend metadata.
167176
168177
Returns:
169-
List[Any]: Retrieved values in the same order as input keys.
178+
Retrieved values in the same order as input keys.
170179
"""
171180

172181
if shapes is None or dtypes is None:
173-
raise ValueError("MooncakeStoreClient needs shapes and dtypes")
182+
raise ValueError("MooncakeStoreClient needs shapes and dtypes for zero-copy transfer.")
174183
if not (len(keys) == len(shapes) == len(dtypes)):
175184
raise ValueError("Lengths of keys, shapes, dtypes must match")
176185

@@ -210,14 +219,25 @@ def _batch_get_tensors(self, keys: list[str], shapes: list, dtypes: list) -> lis
210219
batch_shapes = shapes[i : i + BATCH_SIZE_LIMIT]
211220
batch_dtypes = dtypes[i : i + BATCH_SIZE_LIMIT]
212221

213-
batch_results = self._store.batch_get_tensor(batch_keys)
222+
batch_nbytes = get_nbytes(batch_dtypes, batch_shapes)
223+
batch_buffer_tensors, batch_buffer_ptrs = allocate_empty_tensors(batch_dtypes, batch_shapes)
214224

215-
if len(batch_results) != len(batch_keys):
216-
raise RuntimeError(f"batch_get_tensor returned {len(batch_results)} items, expected {len(batch_keys)}")
225+
batch_ptrs = batch_buffer_ptrs
217226

218-
for j, (tensor, shape, dtype) in enumerate(zip(batch_results, batch_shapes, batch_dtypes, strict=True)):
219-
if tensor is None:
220-
raise RuntimeError(f"batch_get_tensor returned None for key '{batch_keys[j]}'")
227+
self._register_all_buffers(batch_ptrs, batch_nbytes)
228+
ret_codes = self._store.batch_get_into(batch_keys, batch_ptrs, batch_nbytes)
229+
self._unregister_all_buffers(batch_ptrs)
230+
231+
if len(ret_codes) != len(batch_keys):
232+
raise RuntimeError(f"batch_get_into returned {len(ret_codes)} results, expected {len(batch_keys)}")
233+
234+
# Check result codes and validate tensors
235+
# Note: Positive values indicate success (bytes read), negative values indicate error
236+
for j, (tensor, shape, dtype, ret_code) in enumerate(
237+
zip(batch_buffer_tensors, batch_shapes, batch_dtypes, ret_codes, strict=True)
238+
):
239+
if ret_code < 0:
240+
raise RuntimeError(f"batch_get_into failed for key '{batch_keys[j]}' with error code: {ret_code}")
221241
if tensor.shape != torch.Size(shape):
222242
raise RuntimeError(
223243
f"Shape mismatch for key '{batch_keys[j]}': expected {shape}, got {tensor.shape}"
@@ -243,26 +263,35 @@ def _batch_get_bytes(self, keys: list[str]) -> list[bytes]:
243263
def clear(self, keys: list[str], custom_backend_meta=None):
244264
"""Deletes multiple keys from MooncakeStore.
245265
246-
247266
Args:
248267
keys (List[str]): List of keys to remove.
249268
custom_backend_meta (List[Any], optional): ...
250269
"""
251-
global_indexes_patterns = {key.split("@")[0] + "@.*" for key in keys}
252-
for p in global_indexes_patterns:
253-
ret = self._store.remove_by_regex(p, force=True)
254-
if ret < 0:
255-
logger.warning(f"remove failed for key '{p}' with error code: {ret}")
256-
257-
# FIXME: controller returned BatchMeta may have mismatched fields in some case, preventing
258-
# key-value based backends to accurately clear all existing keys..
259-
# for key in keys:
260-
# ret = self._store.remove(key)
261-
# if not (ret == 0 or ret == -704):
262-
# logger.warning(f"remove failed for key '{key}' with error code: {ret}")
270+
rets = self._store.batch_remove(keys, force=True)
271+
for i, ret in enumerate(rets):
272+
if not (ret == 0 or ret == -704):
273+
logger.error(f"remove failed for key '{keys[i]}' with error code: {ret}")
263274

264275
def close(self):
265276
"""Closes MooncakeStore."""
266277
if self._store:
267278
self._store.close()
268279
self._store = None
280+
281+
@staticmethod
282+
def _preprocess_tensors_for_put(values: list[Tensor]) -> tuple[list[Any], list[Any]]:
283+
ptr_list = []
284+
size_list = []
285+
for t in values:
286+
t = t.contiguous()
287+
ptr_list.append(t.data_ptr())
288+
size_list.append(t.nbytes)
289+
return ptr_list, size_list
290+
291+
def _register_all_buffers(self, ptrs, sizes):
292+
for ptr, size in zip(ptrs, sizes, strict=False):
293+
self._store.register_buffer(ptr, size)
294+
295+
def _unregister_all_buffers(self, ptrs):
296+
for ptr in ptrs:
297+
self._store.unregister_buffer(ptr)
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# Copyright 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
2+
# Copyright 2025 The TransferQueue Team
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
import logging
17+
import operator
18+
import os
19+
from functools import reduce
20+
21+
import torch
22+
from torch import Tensor
23+
24+
logger = logging.getLogger(__name__)
25+
logger.setLevel(os.getenv("TQ_LOGGING_LEVEL", logging.WARNING))
26+
27+
28+
def allocate_empty_tensors(dtypes: list[torch.dtype], shapes: list[tuple]) -> tuple[list[Tensor], list[int]]:
29+
"""Allocate empty tensors, grouping same dtypes into shared memory blocks.
30+
31+
Instead of allocating each tensor separately, this function groups tensors
32+
by their dtype and allocates one large contiguous memory block per dtype.
33+
Each tensor is then created as a view into this shared memory.
34+
35+
Args:
36+
dtypes: List of torch dtypes for each tensor.
37+
shapes: List of shapes (tuples) for each tensor.
38+
39+
Returns:
40+
A tuple containing:
41+
- List of tensors sharing memory within their dtype groups.
42+
- List of memory pointers (data_ptr) for each tensor.
43+
44+
Example:
45+
>>> dtypes = [torch.float32, torch.float32, torch.int32, torch.float32]
46+
>>> shapes = [(10,), (20,), (5,), (15,)]
47+
>>> tensors, ptrs = allocate_empty_tensors(dtypes, shapes)
48+
>>> # tensors[0], [1], [3] share the same dtype and memory block
49+
"""
50+
assert len(dtypes) == len(shapes), "dtypes and shapes must have the same length"
51+
52+
if len(dtypes) == 0:
53+
return [], []
54+
55+
# Group indices by dtype
56+
dtype_groups: dict[torch.dtype, list[int]] = {}
57+
for i, dtype in enumerate(dtypes):
58+
if dtype not in dtype_groups:
59+
dtype_groups[dtype] = []
60+
dtype_groups[dtype].append(i)
61+
62+
tensor_list = [torch.empty(()) for _ in range(len(dtypes))]
63+
ptr_list = [0] * len(dtypes)
64+
65+
# For each dtype group, allocate one big tensor and create views
66+
for dtype, indices in dtype_groups.items():
67+
# Calculate total number of elements needed for this dtype
68+
total_elements = 0
69+
shape_info = [] # Store (index, shape, num_elements, offset)
70+
71+
for idx in indices:
72+
shape = shapes[idx]
73+
num_elements = reduce(operator.mul, shape)
74+
shape_info.append((idx, shape, num_elements, total_elements))
75+
total_elements += num_elements
76+
77+
# Allocate one big contiguous memory block for this dtype
78+
big_tensor = torch.empty(total_elements, dtype=dtype)
79+
80+
# Create views into the big tensor for each small tensor
81+
for idx, shape, num_elements, offset in shape_info:
82+
# Use as_strided to create a view with the correct shape
83+
small_tensor = big_tensor.as_strided(size=shape, stride=compute_stride(shape), storage_offset=offset)
84+
tensor_list[idx] = small_tensor
85+
ptr_list[idx] = small_tensor.data_ptr()
86+
87+
return tensor_list, ptr_list
88+
89+
90+
def compute_stride(shape: tuple[int, ...]) -> tuple[int, ...]:
91+
"""Compute stride for a contiguous row-major (C-style) tensor.
92+
93+
Args:
94+
shape: The shape of the tensor.
95+
96+
Returns:
97+
Stride tuple for contiguous storage.
98+
99+
Example:
100+
>>> compute_stride((2, 3, 4))
101+
(12, 4, 1)
102+
"""
103+
stride = []
104+
cumulative = 1
105+
# Iterate from last dimension to first
106+
for dim in reversed(shape):
107+
stride.append(cumulative)
108+
cumulative *= dim
109+
return tuple(reversed(stride))
110+
111+
112+
def get_nbytes(dtypes, shapes) -> list[int]:
113+
assert len(dtypes) == len(shapes)
114+
nbytes = []
115+
for i in range(len(dtypes)):
116+
elem_size = torch.tensor([], dtype=dtypes[i]).element_size()
117+
numel = reduce(operator.mul, shapes[i])
118+
nbytes.append(elem_size * numel)
119+
120+
return nbytes
121+
122+
123+
def merge_continues_memory(ptrs: list[int], sizes: list[int]) -> tuple[list[int], list[int]]:
124+
"""Merge continuous memory regions to reduce register_buffer overhead
125+
126+
Args:
127+
ptrs: List of memory pointers (starting addresses).
128+
sizes: List of memory region sizes corresponding to each pointer.
129+
130+
Returns:
131+
A tuple of (merged_ptrs, merged_sizes) where continuous regions
132+
have been merged into single regions.
133+
134+
Example:
135+
>>> merge_continues_memory([0, 10, 30], [10, 20, 10])
136+
([0, 30], [30, 10])
137+
138+
>>> merge_continues_memory([0, 5, 20], [5, 5, 10])
139+
([0, 20], [10, 10])
140+
"""
141+
if not ptrs or not sizes:
142+
return [], []
143+
144+
if len(ptrs) != len(sizes):
145+
raise ValueError("ptrs and sizes must have the same length")
146+
147+
# Create list of (ptr, size) pairs and sort by pointer address
148+
regions = sorted(zip(ptrs, sizes, strict=False), key=lambda x: x[0])
149+
150+
merged_ptrs = []
151+
merged_sizes = []
152+
153+
# Initialize with the first region
154+
current_ptr, current_size = regions[0]
155+
156+
for ptr, size in regions[1:]:
157+
# Check if current region is contiguous with the next one
158+
# A region is contiguous if: ptr == current_ptr + current_size
159+
if ptr == current_ptr + current_size:
160+
# Merge: extend the current region
161+
current_size += size
162+
else:
163+
# Not contiguous: save the current region and start a new one
164+
merged_ptrs.append(current_ptr)
165+
merged_sizes.append(current_size)
166+
current_ptr, current_size = ptr, size
167+
168+
# Add the last region
169+
merged_ptrs.append(current_ptr)
170+
merged_sizes.append(current_size)
171+
172+
return merged_ptrs, merged_sizes

0 commit comments

Comments
 (0)