Skip to content
This repository was archived by the owner on Jan 21, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
cad2af5
support mooncake-store as tq backend
Dec 21, 2025
cb90030
fix
Dec 21, 2025
dd35034
add performance_test
Dec 21, 2025
7dbc81d
fix
Dec 21, 2025
51df082
fix
Dec 21, 2025
f465d9b
fix
Dec 21, 2025
347364c
mc batch put/get
Dec 21, 2025
46d6889
use batch api
Dec 22, 2025
e6a3939
add mooncake_config.yaml
Dec 28, 2025
4c2df53
profiling put
Dec 28, 2025
bc2b590
fix
Dec 28, 2025
14d5916
fix
Dec 28, 2025
1ca1d0f
fix
Dec 28, 2025
4da986d
fix
Dec 28, 2025
b03e457
profile other overhead
Dec 28, 2025
26d244b
profile get overhead
Dec 28, 2025
7b710e4
optmize get_batch
Dec 28, 2025
6bb4cdd
optmize get_batch
Dec 28, 2025
18bbe29
fix
Dec 28, 2025
52040cc
add mooncake_store_bench_python.py
Dec 28, 2025
a844d34
fix
Dec 28, 2025
6898ad3
fix
Dec 28, 2025
1f8eba3
fix
Dec 28, 2025
74bdaa3
use put_batch for get op
Dec 28, 2025
e19ff0e
get no dup
Dec 28, 2025
2d7575f
fix get
Dec 28, 2025
f0c45c3
fix get
Dec 28, 2025
96a8d38
fix get
Dec 28, 2025
17c9d0e
fix get
Dec 28, 2025
d24bc4a
fix get
Dec 28, 2025
50a30e6
rm bytearray convert
Dec 28, 2025
963b326
reuse shape
Dec 28, 2025
5eca2ff
profile tensor convertion overhead
Dec 28, 2025
5cecbed
fix get
Dec 28, 2025
23b8aa0
profile tensor view
Dec 28, 2025
f9c1483
profile tensor view
Dec 28, 2025
96e52b3
profile get
Dec 28, 2025
f3e799f
profile validate group
Dec 28, 2025
a645c45
fix
Dec 28, 2025
a6d8a41
profile other overhead
Dec 28, 2025
3daa7b6
optimize group
Dec 28, 2025
f567907
profile other overhead
Dec 28, 2025
6be620c
zero copy
Dec 28, 2025
31783fd
fix
Dec 28, 2025
0e59756
zero copy
Dec 28, 2025
8b810bd
fix
Dec 28, 2025
e883c12
refactor mooncake_client.py
Dec 28, 2025
21035cd
fix
Dec 28, 2025
8f996c8
use put_batch/get_batch
Dec 28, 2025
810dbcd
opt de-serialization
Dec 28, 2025
b1b316f
opt get
Dec 28, 2025
c6b49ba
opt put
Dec 28, 2025
1108fb7
opt get
Dec 28, 2025
0d02b63
profile put/get
Dec 28, 2025
b5778e6
fix
Dec 28, 2025
89e15ca
fix
Dec 28, 2025
d7d13fe
profile put
Dec 28, 2025
ddfa3f3
profile put
Dec 28, 2025
10e64ad
chore clean
Dec 28, 2025
2385155
Merge remote-tracking branch 'origin/dev' into mcr
Dec 28, 2025
20e3946
fix
Dec 28, 2025
fc4310f
fix
Dec 28, 2025
d16de59
fix
Dec 29, 2025
d3c52db
address comments
Dec 29, 2025
29a0cc0
patch after-put loop optimization
Dec 29, 2025
663a5b5
address comments
Dec 29, 2025
c8b5662
address comments
Dec 29, 2025
12ab773
diff cpu and gpu
Dec 29, 2025
f798310
address comments
Dec 29, 2025
41ac007
address comments
Dec 29, 2025
7bcadee
address comments
Jan 2, 2026
1b07b03
replace put_batch/get_batch with batch_put_tensor/batch_get_tensor
Jan 2, 2026
9a3fc7a
address comments
Jan 2, 2026
9832a13
Merge branch 'dev' into mcr
zhaohaidao Jan 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion transfer_queue/storage/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from .managers import AsyncSimpleStorageManager, TransferQueueStorageManager, TransferQueueStorageManagerFactory
from .managers import (
AsyncSimpleStorageManager,
MooncakeStorageManager,
TransferQueueStorageManager,
TransferQueueStorageManagerFactory,
YuanrongStorageManager,
)
from .simple_backend import SimpleStorageUnit, StorageMetaGroup, StorageUnitData

__all__ = [
Expand All @@ -23,4 +29,6 @@
"TransferQueueStorageManager",
"TransferQueueStorageManagerFactory",
"AsyncSimpleStorageManager",
"MooncakeStorageManager",
"YuanrongStorageManager",
]
2 changes: 2 additions & 0 deletions transfer_queue/storage/clients/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
# This module is currently empty but reserved for future client implementations
from .base import TransferQueueStorageKVClient
from .factory import StorageClientFactory
from .mooncake_client import MooncakeStorageClient
from .yuanrong_client import YuanrongStorageClient

__all__ = [
"TransferQueueStorageKVClient",
"StorageClientFactory",
"MooncakeStorageClient",
"YuanrongStorageClient",
]
188 changes: 188 additions & 0 deletions transfer_queue/storage/clients/mooncake_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import logging
import os
import pickle
from typing import Any

import torch
from torch import Tensor

from transfer_queue.storage.clients.base import TransferQueueStorageKVClient
from transfer_queue.storage.clients.factory import StorageClientFactory

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

MOONCAKE_STORE_IMPORTED: bool = True
try:
from mooncake.store import MooncakeDistributedStore
except ImportError:
MOONCAKE_STORE_IMPORTED = False

BATCH_SIZE_LIMIT: int = 500


@StorageClientFactory.register("MooncakeStorageClient")
class MooncakeStorageClient(TransferQueueStorageKVClient):
def __init__(self, config: dict[str, Any]):
if not MOONCAKE_STORE_IMPORTED:
raise ImportError("Mooncake Store not installed. Please install via: pip install mooncake-transfer-engine")

Copilot AI Dec 29, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message could be more helpful by including the installation command. Consider updating to: "Mooncake Store not installed. Please install it using: pip install mooncake-transfer-engine" (note the period at the end and rephrasing for clarity).

Suggested change
raise ImportError("Mooncake Store not installed. Please install via: pip install mooncake-transfer-engine")
raise ImportError(
"Mooncake Store not installed. Please install it using: pip install mooncake-transfer-engine."
)

Copilot uses AI. Check for mistakes.

self.local_hostname = config.get("local_hostname", "localhost")
self.metadata_server = config.get("metadata_server")
self.global_segment_size = config.get("global_segment_size", 512 * 1024 * 1024)
self.local_buffer_size = config.get("local_buffer_size", 128 * 1024 * 1024)
self.protocol = config.get("protocol", "tcp")
self.device_name = config.get("device_name", "")
self.master_server_address = config.get("master_server_address")

if self.metadata_server is None:
raise ValueError("Missing 'metadata_server' in config")
if self.master_server_address is None:
raise ValueError("Missing 'master_server_address' in config")

self._store = MooncakeDistributedStore()
ret = self._store.setup(
self.local_hostname,
self.metadata_server,
self.global_segment_size,
self.local_buffer_size,
self.protocol,
self.device_name,
self.master_server_address,
)
if ret != 0:
raise RuntimeError(f"Mooncake store setup failed with error code: {ret}")

def put(self, keys: list[str], values: list[Any]):
if not isinstance(keys, list) or not isinstance(values, list):
raise ValueError("keys and values must be lists")
if len(keys) != len(values):
raise ValueError("Number of keys must match number of values")

tensor_keys = []
tensor_values = []
non_tensor_keys = []
non_tensor_values = []

for key, value in zip(keys, values, strict=True):
if isinstance(value, torch.Tensor):
tensor = value.contiguous()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe put all of these tensor related operations into _batch_put_tensors?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might not have get your point. Could you elaborate? This is just a categorization.

# TODO: use gpu direct rdma instead
if tensor.device.type == "cuda":

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mooncake store supports GPUDirect transfer (tensor in gpu -> host mem). Is it possble to support this feature in TQ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theoretically, it's supported, but I haven't put in the effort to investigate yet. Maybe I can make it a to-do?

tensor = tensor.cpu()
tensor_keys.append(key)
tensor_values.append(tensor)
else:
non_tensor_keys.append(key)
non_tensor_values.append(pickle.dumps(value))

if tensor_keys:
self._batch_put_tensors(tensor_keys, tensor_values)

if non_tensor_keys:
self._batch_put_bytes(non_tensor_keys, non_tensor_values)

def _batch_put_tensors(self, keys: list[str], tensors: list[Tensor]):
for i in range(0, len(keys), BATCH_SIZE_LIMIT):
batch_keys = keys[i : i + BATCH_SIZE_LIMIT]
batch_tensors = tensors[i : i + BATCH_SIZE_LIMIT]

results = self._store.batch_put_tensor(batch_keys, batch_tensors)
if not all(r == 0 for r in results):
failed_indices = [j for j, r in enumerate(results) if r != 0]
error_codes = [results[j] for j in failed_indices]
raise RuntimeError(
f"batch_put_tensor failed for indices {failed_indices} with error codes: {error_codes}"
)

def _batch_put_bytes(self, keys: list[str], values: list[bytes]):
for i in range(0, len(keys), BATCH_SIZE_LIMIT):
batch_keys = keys[i : i + BATCH_SIZE_LIMIT]
batch_values = values[i : i + BATCH_SIZE_LIMIT]

ret = self._store.put_batch(batch_keys, batch_values)
if ret != 0:
raise RuntimeError(f"put_batch failed with error code: {ret}")

def get(self, keys: list[str], shapes=None, dtypes=None) -> list[Any]:
if shapes is None or dtypes is None:
raise ValueError("MooncakeStorageClient needs shapes and dtypes")
if not (len(keys) == len(shapes) == len(dtypes)):
raise ValueError("Lengths of keys, shapes, dtypes must match")

tensor_indices = []
non_tensor_indices = []

for i, dtype in enumerate(dtypes):
if dtype is not None:
tensor_indices.append(i)
else:
non_tensor_indices.append(i)

results = [None] * len(keys)

if tensor_indices:
tensor_keys = [keys[i] for i in tensor_indices]
tensor_shapes = [shapes[i] for i in tensor_indices]
tensor_dtypes = [dtypes[i] for i in tensor_indices]
tensor_results = self._batch_get_tensors(tensor_keys, tensor_shapes, tensor_dtypes)
# TODO: optimize these for loops
for idx, tensor in zip(tensor_indices, tensor_results, strict=True):
Comment thread
zhaohaidao marked this conversation as resolved.
results[idx] = tensor

if non_tensor_indices:
non_tensor_keys = [keys[i] for i in non_tensor_indices]
non_tensor_results = self._batch_get_bytes(non_tensor_keys)
for idx, data in zip(non_tensor_indices, non_tensor_results, strict=True):
results[idx] = pickle.loads(data)

return results

def _batch_get_tensors(self, keys: list[str], shapes: list, dtypes: list) -> list[Tensor]:
tensors = [None] * len(keys)

for i in range(0, len(keys), BATCH_SIZE_LIMIT):
batch_keys = keys[i : i + BATCH_SIZE_LIMIT]
batch_shapes = shapes[i : i + BATCH_SIZE_LIMIT]
batch_dtypes = dtypes[i : i + BATCH_SIZE_LIMIT]

batch_results = self._store.batch_get_tensor(batch_keys)

if len(batch_results) != len(batch_keys):
raise RuntimeError(f"batch_get_tensor returned {len(batch_results)} items, expected {len(batch_keys)}")

for j, (tensor, shape, dtype) in enumerate(zip(batch_results, batch_shapes, batch_dtypes, strict=True)):
if tensor is None:
raise RuntimeError(f"batch_get_tensor returned None for key '{batch_keys[j]}'")
if tensor.shape != torch.Size(shape):
raise RuntimeError(
f"Shape mismatch for key '{batch_keys[j]}': expected {shape}, got {tensor.shape}"
)
if tensor.dtype != dtype:
raise RuntimeError(
f"Dtype mismatch for key '{batch_keys[j]}': expected {dtype}, got {tensor.dtype}"
)
tensors[i + j] = tensor

return tensors

def _batch_get_bytes(self, keys: list[str]) -> list[bytes]:
results = []
for i in range(0, len(keys), BATCH_SIZE_LIMIT):
batch_keys = keys[i : i + BATCH_SIZE_LIMIT]
batch_results = self._store.get_batch(batch_keys)
if len(batch_results) != len(batch_keys):
raise RuntimeError(f"get_batch returned {len(batch_results)} items, expected {len(batch_keys)}")
results.extend(batch_results)
return results

def clear(self, keys: list[str]):
for key in keys:
ret = self._store.remove(key)
if ret != 0:
logger.warning(f"remove failed for key '{key}' with error code: {ret}")
Comment on lines +179 to +183

Copilot AI Dec 29, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clear operation removes keys one at a time in a loop, which could be inefficient for large numbers of keys. If the MooncakeDistributedStore supports batch removal, consider implementing a batch operation similar to how put and get are batched. If not, at least document why individual removal is necessary or consider using asyncio for concurrent removals when this is called from an async context.

Copilot uses AI. Check for mistakes.

def close(self):
if self._store:
self._store.close()
self._store = None
2 changes: 2 additions & 0 deletions transfer_queue/storage/managers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from .base import TransferQueueStorageManager
from .factory import TransferQueueStorageManagerFactory
from .mooncake_manager import MooncakeStorageManager
from .simple_backend_manager import AsyncSimpleStorageManager
from .yuanrong_manager import YuanrongStorageManager

Expand All @@ -23,4 +24,5 @@
"TransferQueueStorageManagerFactory",
"AsyncSimpleStorageManager",
"YuanrongStorageManager",
"MooncakeStorageManager",
]
17 changes: 12 additions & 5 deletions transfer_queue/storage/managers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import asyncio
import itertools
import logging
import os
Expand Down Expand Up @@ -432,9 +432,16 @@ async def put_data(self, data: TensorDict, metadata: BatchMeta) -> None:
if not metadata.field_names:
logger.warning("Attempted to put data, but metadata contains no fields.")
return

# For each field, extract dtype and shape for each sample
num_samples = len(metadata.global_indexes)
if num_samples == 0:
return

keys = self._generate_keys(data.keys(), metadata.global_indexes)
values = self._generate_values(data)
self.storage_client.put(keys=keys, values=values)
loop = asyncio.get_event_loop()

Copilot AI Dec 29, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use asyncio.get_running_loop() instead of asyncio.get_event_loop(). The latter is deprecated in Python 3.10+ and can raise errors if there's no running event loop. Since this code is inside an async function, a loop is guaranteed to be running, so get_running_loop() is more appropriate and efficient.

Suggested change
loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()

Copilot uses AI. Check for mistakes.
await loop.run_in_executor(None, self.storage_client.put, keys, values)

per_field_dtypes = {}
per_field_shapes = {}
Expand All @@ -444,9 +451,9 @@ async def put_data(self, data: TensorDict, metadata: BatchMeta) -> None:
per_field_dtypes[global_idx] = {}
per_field_shapes[global_idx] = {}

# For each field, extract dtype and shape for each sample
for field_name, field_data in data.items():
for i, data_item in enumerate(field_data):
for i in range(num_samples):
data_item = field_data[i]
global_idx = metadata.global_indexes[i]
per_field_dtypes[global_idx][field_name] = (
getattr(data_item, "dtype", None) if isinstance(data_item, Tensor) else None
Expand Down Expand Up @@ -484,5 +491,5 @@ async def clear_data(self, metadata: BatchMeta) -> None:
if not metadata.field_names:
logger.warning("Attempted to clear data, but metadata contains no fields.")
return
keys = self._generate_keys(metadata)
keys = self._generate_keys(metadata.field_names, metadata.global_indexes)
self.storage_client.clear(keys=keys)
Comment thread
0oshowero0 marked this conversation as resolved.
31 changes: 31 additions & 0 deletions transfer_queue/storage/managers/mooncake_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import logging
import os
from typing import Any

from transfer_queue.storage.managers.base import KVStorageManager
from transfer_queue.storage.managers.factory import TransferQueueStorageManagerFactory

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


@TransferQueueStorageManagerFactory.register("MooncakeStorageManager")
class MooncakeStorageManager(KVStorageManager):
def __init__(self, config: dict[str, Any]):
# Required: Address of the HTTP metadata server (e.g., "localhost:8080")
metadata_server = config.get("metadata_server", None)
# Required: Address of the master server RPC endpoint (e.g., "localhost:8081")
master_server_address = config.get("master_server_address", None)
# Optional: Name of the storage client, defaults to "MooncakeStorageClient" if not provided
client_name = config.get("client_name", None)

if metadata_server is None or not isinstance(metadata_server, str):
raise ValueError("Missing or invalid 'metadata_server' in config")
if master_server_address is None or not isinstance(master_server_address, str):
raise ValueError("Missing or invalid 'master_server_address' in config")
if client_name is None:
logger.info("Missing 'client_name' in config, using default value('MooncakeStorageClient')")
config["client_name"] = "MooncakeStorageClient"
elif client_name != "MooncakeStorageClient":
raise ValueError(f"Invalid 'client_name': {client_name} in config. Expecting 'MooncakeStorageClient'")
Comment thread
0oshowero0 marked this conversation as resolved.
super().__init__(config)