chore: import upstream snapshot with attribution
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:16 +08:00
commit 94057c3d3e
7152 changed files with 2120455 additions and 0 deletions
@@ -0,0 +1,71 @@
# Using HF3FS as L3 Global KV Cache
This document provides step-by-step instructions for setting up a k8s + 3FS + SGLang runtime environment from scratch, describing how to utilize deepseek-hf3fs as the L3 KV cache for SGLang.
The process consists of five main steps:
## Step 1: Install deepseek-3fs via 3fs-Operator
Refer to the [3fs-operator documentation](https://github.com/aliyun/kvc-3fs-operator/blob/main/README_en.md) to deploy 3FS components in your Kubernetes environment using the Operator with one-click deployment.
## Step 2: Launch SGLang Pod
Start your SGLang Pod while specifying 3FS-related labels in the YAML configuration. Follow the [fuse-client-creation guide](https://github.com/aliyun/kvc-3fs-operator/blob/main/README_en.md#fuse-client-creation).
## Step 3: Configure Usrbio Client in SGLang Pod
The Usrbio client is required for accessing 3FS. Install it in your SGLang Pod using either method below:
**Alternative 1 (Recommend):** Built from the source code, the following provides quick installation commands (refer to [setup_usrbio_client.md](setup_usrbio_client.md))
```
set -e; \
. /etc/os-release; \
case "$VERSION_ID" in \
"22.04") \
CLANG_VERSION="14"; \
GIT_BRANCH=main; \
GIT_COMMIT_ID=6f029c439d0d22995900ca357d51b37975c6ffb5; \
;; \
"24.04") \
CLANG_VERSION="18"; \
GIT_BRANCH="ubuntu24.04"; \
GIT_COMMIT_ID=d0cf83a42395cdb2a66d3ce83cb0a11a46bee9f3; \
;; \
*) \
echo "Unsupported Ubuntu version: $VERSION_ID"; \
exit 1; \
;; \
esac; \
apt-get update && apt-get install -y --no-install-recommends \
clang-format-$CLANG_VERSION clang-$CLANG_VERSION clang-tidy-$CLANG_VERSION lld-$CLANG_VERSION meson google-perftools \
libaio-dev libdouble-conversion-dev libdwarf-dev libgflags-dev libgmock-dev libgoogle-perftools-dev liblz4-dev liblzma-dev libuv1-dev \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean \
&& git clone https://github.com/novitalabs/3FS.git -b $GIT_BRANCH 3fs \
&& cd 3fs \
&& git checkout $GIT_COMMIT_ID \
&& git submodule update --init --recursive \
&& ./patches/apply.sh \
&& CMAKE_BUILD_PARALLEL_LEVEL=32 python3 setup.py bdist_wheel -d dist \
&& pip install dist/*.whl \
&& cd .. \
&& rm -rf 3fs
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib/python3.12/dist-packages
```
**Alternative 2:** Run `pip3 install hf3fs-py-usrbio` (Follow https://pypi.org/project/hf3fs-py-usrbio/#files)
## Step 4: Deploy Model Serving
### Single Node Deployment
```bash
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib/python3.12/dist-packages
python3 -m sglang.launch_server \
--model-path /code/models/Qwen3-32B/ \
--host 0.0.0.0 --port 10000 \
--page-size 64 \
--enable-hierarchical-cache \
--hicache-ratio 2 --hicache-size 0 \
--hicache-write-policy write_through \
--hicache-storage-backend hf3fs
```
### Multi-Node Deployment (Shared KV Cache)
Follow the [deploy_sglang_3fs_multinode.md](deploy_sglang_3fs_multinode.md) guide to deploy SGLang with 3FS across multiple nodes for shared KV caching.
@@ -0,0 +1,65 @@
# 1. Startup 3fs metadata service
```bash
nohup python3 -m sglang.srt.mem_cache.storage.hf3fs.mini_3fs_metadata_server > meta.out &
```
# 2. Startup sglang engine
## HF3fs configures
```bash
vim /sgl-workspace/sglang/benchmark/hf3fs/hf3fs_config.json
{
"file_path_prefix": "/data/hicache",
"file_size": 1099511627776,
"numjobs": 16,
"entries": 8,
"metadata_server_url": "http://metaServerIp:18000"
}
```
## node1
```bash
export SGLANG_HICACHE_HF3FS_CONFIG_PATH=/sgl-workspace/sglang/benchmark/hf3fs/hf3fs_config.json
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib/python3.12/dist-packages
rm -rf instance1.out && \
nohup python3 -m sglang.launch_server \
--model-path /code/models/Qwen3-32B/ \
--host 0.0.0.0 --port 10000 \
--page-size 64 \
--enable-hierarchical-cache \
--hicache-ratio 2 --hicache-size 0 \
--hicache-write-policy write_through \
--hicache-storage-backend hf3fs --tp 2 > instance1.out &
```
## node2
```bash
export SGLANG_HICACHE_HF3FS_CONFIG_PATH=/sgl-workspace/sglang/benchmark/hf3fs/hf3fs_config.json
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib/python3.12/dist-packages
rm -rf instance2.out && \
nohup python3 -m sglang.launch_server \
--model-path /code/models/Qwen3-32B/ \
--host 0.0.0.0 --port 10000 \
--page-size 64 \
--enable-hierarchical-cache \
--hicache-ratio 2 --hicache-size 0 \
--hicache-write-policy write_through \
--hicache-storage-backend hf3fs --tp 2 > instance2.out &
```
# 3. Startup router
```bash
rm -rf router.out && \
nohup python -m sglang_router.launch_router --worker-urls http://node1:10000 http://node2:10000 > router.out &
```
# 4. Startup multiturn benchmark
```bash
rm -rf bench_multiturn.out && \
nohup python3 benchmark/hicache/bench_multiturn.py \
--model-path /code/models/Qwen3-32B \
--dataset-path /code/models/ShareGPT_V3_unfiltered_cleaned_split.json \
--port 30000 \
--request-length 2048 --num-clients 512 --num-rounds 5 --max-parallel 8 \
> bench_multiturn.out &
```
@@ -0,0 +1,68 @@
# HiCacheHF3FS Setup
## Build & Package
### Source Code
https://github.com/deepseek-ai/3FS/blob/main/README.md#check-out-source-code
```sh
git clone https://github.com/deepseek-ai/3fs
cd 3fs
git submodule update --init --recursive
./patches/apply.sh
```
### Build Dev Container
https://github.com/deepseek-ai/3FS/blob/main/dockerfile/dev.dockerfile
```sh
cd 3fs/dockerfile
docker build -t hf3fs:dev -f dev.dockerfile .
```
### Generate Python Wheel
```sh
docker run -it hf3fs:dev bash
# Inside the development container
git clone https://github.com/deepseek-ai/3fs
cd 3fs
git submodule update --init --recursive
./patches/apply.sh
apt-get update \
&& apt-get install -y --no-install-recommends \
python3 python3-pip \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# apt install python3.12 python3.12-venv python3.12-dev
# curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
# python3.12 get-pip.py
# Generated wheel location: dist/hf3fs_py_usrbio-1.2.9+2db69ce-cp310-cp310-linux_x86_64.whl
python3 setup.py bdist_wheel
```
## Installation
```sh
# Install Dependencies
# https://github.com/deepseek-ai/3FS/blob/main/dockerfile/dev.dockerfile
apt update && apt install -y \
libaio-dev \
libboost-all-dev \
libdouble-conversion-dev \
libdwarf-dev \
libgflags-dev \
libgmock-dev \
libgoogle-glog-dev \
libgoogle-perftools-dev \
libgtest-dev \
liblz4-dev \
liblzma-dev \
libssl-dev \
libunwind-dev \
libuv1-dev
# Install Python Package
pip install hf3fs_py_usrbio-1.2.9+394583d-cp312-cp312-linux_x86_64.whl
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib/python3.12/dist-packages
```
@@ -0,0 +1,163 @@
import logging
import os
from abc import ABC, abstractmethod
from typing import List
import torch
class Hf3fsClient(ABC):
"""Abstract interface for HF3FS clients."""
@abstractmethod
def __init__(self, path: str, size: int, bytes_per_page: int, entries: int):
"""Initialize the HF3FS client.
Args:
path: File path for storage
size: Total size of storage file
bytes_per_page: Bytes per page
entries: Number of entries for batch operations
"""
pass
@abstractmethod
def batch_read(self, offsets: List[int], tensors: List[torch.Tensor]) -> List[int]:
"""Batch read from storage."""
pass
@abstractmethod
def batch_write(self, offsets: List[int], tensors: List[torch.Tensor]) -> List[int]:
"""Batch write to storage."""
pass
@abstractmethod
def check(self, offsets: List[int], tensors: List[torch.Tensor]) -> None:
"""Validate batch operation parameters."""
pass
@abstractmethod
def get_size(self) -> int:
"""Get total storage size."""
pass
@abstractmethod
def close(self) -> None:
"""Close the client and cleanup resources."""
pass
@abstractmethod
def flush(self) -> None:
"""Flush data to disk."""
pass
logger = logging.getLogger(__name__)
class Hf3fsMockClient(Hf3fsClient):
"""Mock implementation of Hf3fsClient for CI testing purposes."""
def __init__(self, path: str, size: int, bytes_per_page: int, entries: int):
"""Initialize mock HF3FS client."""
self.path = path
self.size = size
self.bytes_per_page = bytes_per_page
self.entries = entries
# Create directory if it doesn't exist
os.makedirs(os.path.dirname(self.path), exist_ok=True)
# Create and initialize the file
self.file = os.open(self.path, os.O_RDWR | os.O_CREAT)
os.ftruncate(self.file, size)
logger.info(
f"Hf3fsMockClient initialized: path={path}, size={size}, "
f"bytes_per_page={bytes_per_page}, entries={entries}"
)
def batch_read(self, offsets: List[int], tensors: List[torch.Tensor]) -> List[int]:
"""Batch read from mock storage."""
self.check(offsets, tensors)
results = []
for offset, tensor in zip(offsets, tensors):
size = tensor.numel() * tensor.itemsize
try:
os.lseek(self.file, offset, os.SEEK_SET)
bytes_read = os.read(self.file, size)
if len(bytes_read) == size:
# Convert bytes to tensor and copy to target
bytes_tensor = torch.frombuffer(bytes_read, dtype=torch.uint8)
typed_tensor = bytes_tensor.view(tensor.dtype).view(tensor.shape)
tensor.copy_(typed_tensor)
results.append(size)
else:
logger.warning(
f"Short read: expected {size}, got {len(bytes_read)}"
)
results.append(len(bytes_read))
except Exception as e:
logger.error(f"Error reading from offset {offset}: {e}")
results.append(0)
return results
def batch_write(self, offsets: List[int], tensors: List[torch.Tensor]) -> List[int]:
"""Batch write to mock storage."""
self.check(offsets, tensors)
results = []
for offset, tensor in zip(offsets, tensors):
size = tensor.numel() * tensor.itemsize
try:
# Convert tensor to bytes and write directly to file
tensor_bytes = tensor.contiguous().view(torch.uint8).flatten()
data = tensor_bytes.numpy().tobytes()
os.lseek(self.file, offset, os.SEEK_SET)
bytes_written = os.write(self.file, data)
if bytes_written == size:
results.append(size)
else:
logger.warning(f"Short write: expected {size}, got {bytes_written}")
results.append(bytes_written)
except Exception as e:
logger.error(f"Error writing to offset {offset}: {e}")
results.append(0)
return results
def check(self, offsets: List[int], tensors: List[torch.Tensor]) -> None:
"""Validate batch operation parameters."""
pass
def get_size(self) -> int:
"""Get total storage size."""
return self.size
def close(self) -> None:
"""Close the mock client and cleanup resources."""
try:
if hasattr(self, "file") and self.file >= 0:
os.close(self.file)
self.file = -1 # Mark as closed
logger.info(f"MockHf3fsClient closed: {self.path}")
except Exception as e:
logger.error(f"Error closing MockHf3fsClient: {e}")
def flush(self) -> None:
"""Flush data to disk."""
try:
os.fsync(self.file)
except Exception as e:
logger.error(f"Error flushing MockHf3fsClient: {e}")
@@ -0,0 +1,220 @@
import datetime
import logging
import multiprocessing
import os
import threading
from functools import wraps
from pathlib import Path
from typing import List
import torch
from torch.utils.cpp_extension import load
from sglang.srt.mem_cache.storage.hf3fs.hf3fs_client import Hf3fsClient
root = Path(__file__).parent.resolve()
hf3fs_utils = load(name="hf3fs_utils", sources=[f"{root}/hf3fs_utils.cpp"])
logger = logging.getLogger(__name__)
HF3FS_AVAILABLE = True
try:
from hf3fs_fuse.io import (
deregister_fd,
extract_mount_point,
make_ioring,
make_iovec,
register_fd,
)
except ImportError:
HF3FS_AVAILABLE = False
def rsynchronized():
def _decorator(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
with self.rlock:
return func(self, *args, **kwargs)
return wrapper
return _decorator
def wsynchronized():
def _decorator(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
with self.wlock:
return func(self, *args, **kwargs)
return wrapper
return _decorator
class Hf3fsUsrBioClient(Hf3fsClient):
"""HF3FS client implementation using usrbio."""
def __init__(
self,
path: str,
size: int,
bytes_per_page: int,
entries: int,
client_timeout: int,
):
if not HF3FS_AVAILABLE:
raise ImportError(
"hf3fs_fuse.io is not available. Please install the hf3fs_fuse package."
)
self.path = path
self.size = size
self.bytes_per_page = bytes_per_page
self.entries = entries
self.client_timeout = client_timeout
self.file = os.open(self.path, os.O_RDWR | os.O_CREAT)
os.ftruncate(self.file, size)
register_fd(self.file)
self.hf3fs_mount_point = extract_mount_point(path)
self.bs = self.bytes_per_page
self.shm_r = multiprocessing.shared_memory.SharedMemory(
size=self.bs * self.entries, create=True
)
self.shm_w = multiprocessing.shared_memory.SharedMemory(
size=self.bs * self.entries, create=True
)
self.shm_r_tensor = torch.frombuffer(self.shm_r.buf, dtype=torch.uint8)
self.shm_w_tensor = torch.frombuffer(self.shm_w.buf, dtype=torch.uint8)
self.numa = -1
self.ior_r = make_ioring(
self.hf3fs_mount_point,
self.entries,
for_read=True,
timeout=1,
numa=self.numa,
)
self.ior_w = make_ioring(
self.hf3fs_mount_point,
self.entries,
for_read=False,
timeout=1,
numa=self.numa,
)
self.iov_r = make_iovec(self.shm_r, self.hf3fs_mount_point)
self.iov_w = make_iovec(self.shm_w, self.hf3fs_mount_point)
self.shm_r.unlink()
self.shm_w.unlink()
self.rlock = threading.RLock()
self.wlock = threading.RLock()
@rsynchronized()
def batch_read(self, offsets: List[int], tensors: List[torch.Tensor]) -> List[int]:
self.check(offsets, tensors)
results = [0] * len(offsets)
# prepare
current = 0
for offset, tensor in zip(offsets, tensors):
size = tensor.numel() * tensor.itemsize
try:
self.ior_r.prepare(
self.iov_r[current : current + size], True, self.file, offset
)
current += size
except Exception as e:
logger.error(f"Error preparing batch read: {e}")
return results
# submit
ionum = len(offsets)
try:
resv = self.ior_r.submit().wait(
min_results=ionum,
timeout=datetime.timedelta(seconds=self.client_timeout),
)
except Exception as e:
logger.error(f"Error submitting batch read: {e}")
return results
# results
try:
hf3fs_utils.read_shm(self.shm_r_tensor, tensors)
results = [res.result for res in resv]
except Exception as e:
logger.error(f"[Hf3fsUsrBioClient] read_shm failed: {e}", exc_info=True)
return results
return results
@wsynchronized()
def batch_write(self, offsets: List[int], tensors: List[torch.Tensor]) -> List[int]:
self.check(offsets, tensors)
results = [0] * len(offsets)
# prepare
hf3fs_utils.write_shm(tensors, self.shm_w_tensor)
current = 0
for offset, tensor in zip(offsets, tensors):
size = tensor.numel() * tensor.itemsize
try:
self.ior_w.prepare(
self.iov_w[current : current + size], False, self.file, offset
)
current += size
except Exception as e:
logger.error(f"Error preparing batch write: {e}")
return results
# submit
ionum = len(offsets)
try:
resv = self.ior_w.submit().wait(
min_results=ionum,
timeout=datetime.timedelta(seconds=self.client_timeout),
)
except Exception as e:
logger.error(f"Error submitting batch write: {e}")
return results
# results
results = [res.result for res in resv]
return results
def check(self, offsets: List[int], tensors: List[torch.Tensor]) -> None:
sizes = [t.numel() * t.itemsize for t in tensors]
if any(
[
len(offsets) > self.entries,
len(offsets) != len(sizes),
all(
[
offset < 0 or offset + size > self.size
for offset, size in zip(offsets, sizes)
]
),
all([size > self.bytes_per_page for size in sizes]),
]
):
self.close()
raise ValueError(f"Hf3fsClient.check: {offsets=}, {sizes=}")
def get_size(self) -> int:
return self.size
def close(self) -> None:
deregister_fd(self.file)
os.close(self.file)
del self.ior_r
del self.ior_w
del self.iov_r
del self.iov_w
self.shm_r.close()
self.shm_w.close()
def flush(self) -> None:
os.fsync(self.file)
@@ -0,0 +1,35 @@
#include <torch/extension.h>
#include <cstring>
#include <vector>
void read_shm(const torch::Tensor &shm, std::vector<torch::Tensor> dst) {
py::gil_scoped_release release;
char *src_ptr = static_cast<char *>(shm.data_ptr());
size_t current = 0;
for (size_t i = 0; i < dst.size(); ++i) {
auto &t = dst[i];
size_t t_bytes = t.numel() * t.element_size();
char *dst_ptr = static_cast<char *>(t.data_ptr());
std::memcpy(dst_ptr, src_ptr + current, t_bytes);
current += t_bytes;
}
}
void write_shm(const std::vector<torch::Tensor> src, torch::Tensor &shm) {
py::gil_scoped_release release;
char *dst_ptr = static_cast<char *>(shm.data_ptr());
size_t current = 0;
for (size_t i = 0; i < src.size(); ++i) {
auto &t = src[i];
size_t t_bytes = t.numel() * t.element_size();
char *src_ptr = static_cast<char *>(t.data_ptr());
std::memcpy(dst_ptr + current, src_ptr, t_bytes);
current += t_bytes;
}
}
PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
m.def("read_shm", &read_shm, "Read tensors from shared memory");
m.def("write_shm", &write_shm, "Write tensors to shared memory");
}
@@ -0,0 +1,532 @@
import argparse
import atexit
import json
import logging
import threading
from collections import OrderedDict
from pathlib import Path
from typing import Dict, List, Optional, Tuple
import orjson
import requests
from fastapi import FastAPI, HTTPException, Request, Response
from fastapi.responses import ORJSONResponse
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from sglang.srt.mem_cache.hicache_storage import PoolName
from sglang.srt.mem_cache.storage.hf3fs.storage_hf3fs import Hf3fsMetadataInterface
# --- Configuration ---
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
# --- Data Models ---
class RankMetadata:
"""Holds all metadata for a single rank."""
def __init__(self, num_pages: int):
self.lock = threading.Lock()
self.num_pages = num_pages
self.free_pages: List[int] = list(range(num_pages))
self.key_to_index: OrderedDict[str, int] = OrderedDict()
# Todo: Support multi files for HF3FS
def exists_keys(self, keys: List[str]) -> List[bool]:
"""Check if keys exist in metadata."""
with self.lock:
return [key in self.key_to_index for key in keys]
def reserve_and_allocate_page_indices(
self, keys: List[Tuple[str, str]]
) -> List[Tuple[bool, int]]:
"""Reserve and allocate page indices for keys."""
with self.lock:
results = [None] * len(keys)
new_keys_to_process = []
for i, (key, prefix_key) in enumerate(keys):
if key in self.key_to_index:
results[i] = (True, self.key_to_index[key])
self.key_to_index.move_to_end(key)
else:
new_keys_to_process.append((i, key, prefix_key))
# Todo: Implementing data eviction logic after HiCache supports prefix information pass-through
for i, key, prefix_key in new_keys_to_process:
if len(self.free_pages) > 0:
page_index = self.free_pages.pop()
else:
page_index = self.key_to_index.popitem(last=False)[1]
results[i] = (False, page_index)
return results
def confirm_write(
self,
written_keys_to_confirm: List[Tuple[str, int]],
pages_to_release: List[int],
) -> None:
"""Confirm write operations and release pages."""
with self.lock:
for key, page_index in written_keys_to_confirm:
self.key_to_index[key] = page_index
self.key_to_index.move_to_end(key)
for page_index in pages_to_release:
if page_index not in self.free_pages:
self.free_pages.append(page_index)
def delete_keys(self, keys: List[str]) -> int:
"""Delete keys and return count of deleted keys."""
with self.lock:
count = 0
for key in keys:
if key in self.key_to_index:
page_index = self.key_to_index.pop(key)
if page_index not in self.free_pages:
self.free_pages.append(page_index)
count += 1
return count
def clear_all(self) -> None:
"""Clear all metadata."""
with self.lock:
self.free_pages = list(range(self.num_pages))
self.key_to_index.clear()
def get_page_indices(self, keys: List[str]) -> List[Optional[int]]:
"""Get page indices for keys."""
with self.lock:
results = []
for key in keys:
if key in self.key_to_index:
results.append(self.key_to_index[key])
self.key_to_index.move_to_end(key)
else:
results.append(None)
return results
class GlobalMetadataState:
"""Manages the state for all ranks and persistence."""
def __init__(self, persistence_path: Optional[str], save_interval: int):
self.global_lock = threading.RLock()
self.ranks: Dict[str, RankMetadata] = {}
self.persistence_path = Path(persistence_path) if persistence_path else None
self.save_interval = save_interval
self.save_timer: Optional[threading.Timer] = None
self.is_shutting_down = False
def load_from_disk(self):
if not self.persistence_path or not self.persistence_path.exists():
logging.info("Persistence file not found. Starting with a clean state.")
return
logging.info(f"Loading state from {self.persistence_path}")
try:
with open(self.persistence_path, "r") as f:
persisted_data = json.load(f)
with self.global_lock:
for key_str, data in persisted_data.items():
if ":" not in key_str:
key_str = f"{key_str}:kv" # For backward compatibility
num_pages = data["num_pages"]
rank_meta = RankMetadata(num_pages)
rank_meta.free_pages = data["free_pages"]
rank_meta.key_to_index = OrderedDict(data["key_to_index"])
self.ranks[key_str] = rank_meta
logging.info(
f"Successfully loaded metadata for {len(self.ranks)} ranks."
)
except (json.JSONDecodeError, KeyError, TypeError) as e:
logging.error(
f"Failed to load or parse persistence file: {e}. Starting fresh.",
exc_info=True,
)
self.ranks.clear()
def save_to_disk(self):
if not self.persistence_path:
return
logging.info("Persisting metadata to disk...")
with self.global_lock:
serializable_state = {}
for key_str, rank_meta in self.ranks.items():
with rank_meta.lock:
serializable_state[key_str] = {
"num_pages": rank_meta.num_pages,
"free_pages": rank_meta.free_pages,
"key_to_index": list(rank_meta.key_to_index.items()),
}
try:
temp_path = self.persistence_path.with_suffix(".tmp")
with open(temp_path, "w") as f:
json.dump(serializable_state, f, indent=4)
temp_path.rename(self.persistence_path)
logging.info(f"Metadata successfully persisted to {self.persistence_path}")
except Exception as e:
logging.error(f"Failed to save metadata to disk: {e}", exc_info=True)
def schedule_save(self):
if self.is_shutting_down or not self.persistence_path:
return
self.save_to_disk()
self.save_timer = threading.Timer(self.save_interval, self.schedule_save)
self.save_timer.start()
def shutdown(self):
logging.info("Shutting down metadata server...")
self.is_shutting_down = True
if self.save_timer:
self.save_timer.cancel()
self.save_to_disk()
logging.info("Shutdown complete.")
# --- Global MetadataServer implementation ---
class Hf3fsMetadataServer:
"""HF3FS Metadata Server that manages metadata for multiple ranks."""
def __init__(self, persistence_path: Optional[str] = None, save_interval: int = 60):
self.state = GlobalMetadataState(persistence_path, save_interval)
self.app = FastAPI(default_response_class=ORJSONResponse)
self._setup_routes()
def _setup_routes(self):
"""Setup FastAPI routes."""
self.app.post("/{rank}/initialize")(self.initialize)
self.app.post("/{rank}/exists")(self.exists)
self.app.post("/{rank}/reserve_and_allocate_page_indices")(
self.reserve_and_allocate_page_indices
)
self.app.post("/{rank}/confirm_write")(self.confirm_write)
self.app.post("/{rank}/delete_keys")(self.delete_keys)
self.app.post("/{rank}/clear")(self.clear)
self.app.post("/{rank}/get_page_indices")(self.get_page_indices)
def _rank_key(self, rank: int, namespace: str) -> str:
"""Generate the composite key for rank+namespace."""
return f"{rank}:{namespace}"
def get_rank_metadata(self, rank: int, namespace: str = "kv") -> RankMetadata:
"""Get rank metadata with proper error handling."""
key = self._rank_key(rank, namespace)
if key not in self.state.ranks:
raise HTTPException(
status_code=404,
detail=f"Rank {rank} namespace '{namespace}' not initialized. Please call /{rank}/initialize first.",
)
return self.state.ranks[key]
async def _read_json(self, request: Request) -> dict:
"""Parse request JSON using orjson if available."""
body = await request.body()
return orjson.loads(body)
def _json_response(self, content: dict):
"""Return ORJSONResponse when available to bypass jsonable_encoder."""
return ORJSONResponse(content)
async def initialize(self, rank: int, request: Request):
"""Initialize a rank with specified number of pages."""
data = await self._read_json(request)
num_pages = data["num_pages"]
namespace = data.get("namespace", "kv")
key = self._rank_key(rank, namespace)
with self.state.global_lock:
if key in self.state.ranks:
logging.info(
f"Rank {rank} namespace '{namespace}' already exists. Initialization request ignored."
)
if self.state.ranks[key].num_pages != num_pages:
logging.warning(
f"Rank {rank} namespace '{namespace}' initialized with different num_pages. Existing: {self.state.ranks[key].num_pages}, New: {num_pages}"
)
else:
logging.info(
f"Initializing new Rank {rank} namespace '{namespace}' with {num_pages} pages."
)
self.state.ranks[key] = RankMetadata(num_pages)
return Response(status_code=204)
async def exists(self, rank: int, request: Request):
"""Check if keys exist in metadata."""
data = await self._read_json(request)
keys = data["keys"]
namespace = data.get("namespace", "kv")
metadata = self.get_rank_metadata(rank, namespace)
results = metadata.exists_keys(keys)
return self._json_response({"exists": results})
async def reserve_and_allocate_page_indices(self, rank: int, request: Request):
"""Reserve and allocate page indices for keys."""
data = await self._read_json(request)
namespace = data.get("namespace", "kv")
metadata = self.get_rank_metadata(rank, namespace)
keys = data["keys"]
results = metadata.reserve_and_allocate_page_indices(keys)
return self._json_response({"indices": results})
async def confirm_write(self, rank: int, request: Request):
"""Confirm write operations and release pages."""
data = await self._read_json(request)
namespace = data.get("namespace", "kv")
metadata = self.get_rank_metadata(rank, namespace)
success_written_keys = data.get("written_keys_to_confirm", [])
released_pages = data.get("pages_to_release", [])
metadata.confirm_write(success_written_keys, released_pages)
return Response(status_code=204)
async def delete_keys(self, rank: int, request: Request):
"""Delete keys from metadata."""
data = await self._read_json(request)
namespace = data.get("namespace", "kv")
metadata = self.get_rank_metadata(rank, namespace)
count = metadata.delete_keys(data["keys"])
return Response(status_code=204)
async def clear(self, rank: int, request: Request):
"""Clear all metadata for a rank."""
data = await self._read_json(request)
namespace = data.get("namespace", "kv")
metadata = self.get_rank_metadata(rank, namespace)
metadata.clear_all()
return Response(status_code=204)
async def get_page_indices(self, rank: int, request: Request):
"""Get page indices for keys."""
data = await self._read_json(request)
namespace = data.get("namespace", "kv")
metadata = self.get_rank_metadata(rank, namespace)
keys = data["keys"]
results = metadata.get_page_indices(keys)
return self._json_response({"indices": results})
def run(self, host: str = "0.0.0.0", port: int = 18000):
"""Run the metadata server."""
self.state.load_from_disk()
if self.state.persistence_path:
self.state.schedule_save()
atexit.register(self.state.shutdown)
import uvicorn
logging.info(f"Starting metadata server on http://{host}:{port}")
if self.state.persistence_path:
logging.info(
f"Persistence is ENABLED. Saving to '{self.state.persistence_path}' every {self.state.save_interval} seconds."
)
else:
logging.info("Persistence is DISABLED.")
uvicorn.run(self.app, host=host, port=port)
# --- Client implementation ---
class Hf3fsGlobalMetadataClient(Hf3fsMetadataInterface):
"""Global http metadata client for HF3FS."""
def __init__(self, base_url: str, max_retries: int = 3):
self.base_url = base_url.rstrip("/")
self._session = requests.Session()
retry_strategy = Retry(
total=max_retries,
backoff_factor=0.3,
status_forcelist=[500, 502, 503, 504],
allowed_methods=["GET", "POST"],
)
adapter = HTTPAdapter(
max_retries=retry_strategy, pool_connections=256, pool_maxsize=256
)
self._session.mount("http://", adapter)
def _post(self, endpoint: str, json_data: dict) -> dict:
try:
url = f"{self.base_url}/{endpoint}"
headers = {"Content-Type": "application/json"}
payload = orjson.dumps(json_data) # type: ignore[union-attr]
response = self._session.post(url, data=payload, headers=headers)
response.raise_for_status()
if response.status_code == 204 or not response.content:
return {}
return orjson.loads(response.content) # type: ignore[union-attr]
except requests.exceptions.RequestException as e:
logging.error(f"Failed to POST to {endpoint} after retries: {e}")
raise RuntimeError(f"Failed to connect to metadata server: {e}") from e
def initialize(
self, rank: int, num_pages: int, namespace: PoolName = PoolName.KV
) -> None:
self._post(
f"{rank}/initialize", {"num_pages": num_pages, "namespace": str(namespace)}
)
def reserve_and_allocate_page_indices(
self, rank: int, keys: List[Tuple[str, str]], namespace: PoolName = PoolName.KV
) -> List[Tuple[bool, int]]:
response = self._post(
f"{rank}/reserve_and_allocate_page_indices",
{"keys": keys, "namespace": str(namespace)},
)
return [tuple(item) for item in response.get("indices")]
def confirm_write(
self,
rank: int,
written_keys_to_confirm: List[Tuple[str, int]],
pages_to_release: List[int],
namespace: PoolName = PoolName.KV,
) -> None:
self._post(
f"{rank}/confirm_write",
{
"written_keys_to_confirm": written_keys_to_confirm,
"pages_to_release": pages_to_release,
"namespace": str(namespace),
},
)
def delete_keys(
self, rank: int, keys: List[str], namespace: PoolName = PoolName.KV
) -> None:
self._post(f"{rank}/delete_keys", {"keys": keys, "namespace": str(namespace)})
def exists(
self, rank: int, keys: List[str], namespace: PoolName = PoolName.KV
) -> List[bool]:
response = self._post(
f"{rank}/exists", {"keys": keys, "namespace": str(namespace)}
)
return response.get("exists", [])
def clear(self, rank: int, namespace: PoolName = PoolName.KV) -> None:
self._post(f"{rank}/clear", {"namespace": str(namespace)})
def get_page_indices(
self, rank: int, keys: List[str], namespace: PoolName = PoolName.KV
) -> List[Optional[int]]:
response = self._post(
f"{rank}/get_page_indices", {"keys": keys, "namespace": str(namespace)}
)
return response.get("indices")
class Hf3fsLocalMetadataClient(Hf3fsMetadataInterface):
"""Local metadata client that directly operates on RankMetadata in memory without metadata server."""
def __init__(self):
self._metadata: Dict[str, RankMetadata] = {} # key: "rank:namespace"
def _ns_key(self, rank: int, namespace: PoolName) -> str:
return f"{rank}:{namespace}"
def _get_metadata(self, rank: int, namespace) -> RankMetadata:
key = self._ns_key(rank, namespace)
if key not in self._metadata:
raise RuntimeError(
f"Namespace '{namespace}' for rank {rank} not initialized"
)
return self._metadata[key]
def initialize(
self, rank: int, num_pages: int, namespace: PoolName = PoolName.KV
) -> None:
key = self._ns_key(rank, namespace)
if key not in self._metadata:
self._metadata[key] = RankMetadata(num_pages)
def reserve_and_allocate_page_indices(
self, rank: int, keys: List[Tuple[str, str]], namespace: PoolName = PoolName.KV
) -> List[Tuple[bool, int]]:
"""Reserve and allocate page indices for keys."""
return self._get_metadata(rank, namespace).reserve_and_allocate_page_indices(
keys
)
def confirm_write(
self,
rank: int,
written_keys_to_confirm: List[Tuple[str, int]],
pages_to_release: List[int],
namespace: PoolName = PoolName.KV,
) -> None:
"""Confirm write operations."""
self._get_metadata(rank, namespace).confirm_write(
written_keys_to_confirm, pages_to_release
)
def delete_keys(
self, rank: int, keys: List[str], namespace: PoolName = PoolName.KV
) -> None:
"""Delete keys."""
self._get_metadata(rank, namespace).delete_keys(keys)
def exists(
self, rank: int, keys: List[str], namespace: PoolName = PoolName.KV
) -> List[bool]:
"""Check if keys exist."""
return self._get_metadata(rank, namespace).exists_keys(keys)
def clear(self, rank: int, namespace: PoolName = PoolName.KV) -> None:
"""Clear all metadata for rank."""
self._get_metadata(rank, namespace).clear_all()
def get_page_indices(
self, rank: int, keys: List[str], namespace: PoolName = PoolName.KV
) -> List[Optional[int]]:
"""Get page indices for keys."""
return self._get_metadata(rank, namespace).get_page_indices(keys)
def run_metadata_server(
host: str = "0.0.0.0",
port: int = 18000,
persistence_path: Optional[str] = None,
save_interval: int = 60,
):
"""Run the HF3FS metadata server."""
global server
server = Hf3fsMetadataServer(
persistence_path=persistence_path, save_interval=save_interval
)
server.run(host=host, port=port)
# --- Main Execution ---
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="HF3FS Metadata Server")
parser.add_argument(
"--host", type=str, default="0.0.0.0", help="Host to bind the server to."
)
parser.add_argument(
"--port", type=int, default=18000, help="Port to run the server on."
)
parser.add_argument(
"--persistence-path",
type=str,
default=None,
help="Path to the file for persisting metadata. If not provided, persistence is disabled.",
)
parser.add_argument(
"--save-interval",
type=int,
default=60,
help="Interval in seconds for periodically saving metadata to disk.",
)
args = parser.parse_args()
run_metadata_server(args.host, args.port, args.persistence_path, args.save_interval)
@@ -0,0 +1,956 @@
import atexit
import concurrent.futures
import json
import logging
import os
import signal
import threading
import time
from abc import ABC, abstractmethod
from dataclasses import dataclass
from functools import wraps
from typing import Any, List, Optional, Tuple
import torch
from sglang.srt.mem_cache.hicache_storage import (
HiCacheStorage,
HiCacheStorageConfig,
HiCacheStorageExtraInfo,
PoolHitPolicy,
PoolName,
PoolTransfer,
PoolTransferResult,
)
from sglang.srt.mem_cache.pool_host import HostKVCache
from sglang.srt.mem_cache.storage.hf3fs.hf3fs_client import Hf3fsClient
from sglang.srt.observability.metrics_collector import StorageMetrics
logger = logging.getLogger(__name__)
class Hf3fsMetadataInterface(ABC):
"""Interface for HF3FS metadata operations."""
@abstractmethod
def initialize(
self, rank: int, num_pages: int, namespace: PoolName = PoolName.KV
) -> None:
"""Initialize the metadata service with specified number of pages."""
pass
@abstractmethod
def reserve_and_allocate_page_indices(
self,
rank: int,
keys: List[Tuple[str, str]],
namespace: PoolName = PoolName.KV,
) -> List[Tuple[bool, int]]:
"""
Reserve and allocate page indices for the specified keys.
Args:
rank: The rank of the process.
keys: The keys to reserve and allocate page indices for. Each tuple contains a key and the key of its prefix block.
namespace: The namespace (pool type) for the metadata.
Returns:
List[Tuple[bool, int]]: A list of tuples, where each tuple contains a boolean indicating whether the key has existed and an integer indicating the allocated page index.
"""
pass
@abstractmethod
def confirm_write(
self,
rank: int,
written_keys_to_confirm: List[Tuple[str, int]],
pages_to_release: List[int],
namespace: PoolName = PoolName.KV,
) -> None:
"""
Confirm that key-value pairs have been successfully written to storage.
Args:
rank: The rank of the process.
written_keys_to_confirm: A list of tuples, where each tuple contains a key and its corresponding page index.
pages_to_release: A list of page indices to be released.
namespace: The namespace (pool type) for the metadata.
"""
pass
@abstractmethod
def get_page_indices(
self, rank: int, keys: List[str], namespace: PoolName = PoolName.KV
) -> List[Optional[int]]:
"""
Get page indices for the specified keys.
Args:
rank: The rank of the process.
keys: A list of keys.
namespace: The namespace (pool type) for the metadata.
Returns:
List[Optional[int]]: A list of integers representing the page indices for the specified keys.
If a key is not found, the corresponding index will be None.
"""
pass
@abstractmethod
def delete_keys(
self, rank: int, keys: List[str], namespace: PoolName = PoolName.KV
) -> None:
"""Delete specified keys and their associated pages."""
pass
@abstractmethod
def exists(
self, rank: int, keys: List[str], namespace: PoolName = PoolName.KV
) -> List[bool]:
"""Check if the specified keys exist."""
pass
@abstractmethod
def clear(self, rank: int, namespace: PoolName = PoolName.KV) -> None:
"""Clear all key-value pairs and page allocations for the specified rank."""
pass
class AtomicCounter:
def __init__(self, n: int):
assert n > 0
self.n = n
self._value = 0
self._lock = threading.Lock()
def next(self) -> int:
with self._lock:
current = self._value
self._value = (current + 1) % self.n
return current
def synchronized():
def _decorator(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
with self.lock:
return func(self, *args, **kwargs)
return wrapper
return _decorator
def create_hf3fs_client(
path: str,
size: int,
bytes_per_page: int,
entries: int,
client_timeout: int,
use_mock: bool = False,
) -> Hf3fsClient:
"""Factory function to create appropriate HF3FS client.
Args:
path: File path for storage
size: Total size of storage file
bytes_per_page: Bytes per page
entries: Number of entries for batch operations
use_mock: Whether to use mock client instead of real usrbio client
Returns:
"""
if use_mock:
from sglang.srt.mem_cache.storage.hf3fs.hf3fs_client import Hf3fsMockClient
logger.info(f"[Rank Using Hf3fsMockClient for testing")
return Hf3fsMockClient(path, size, bytes_per_page, entries)
else:
from sglang.srt.mem_cache.storage.hf3fs.hf3fs_usrbio_client import (
Hf3fsUsrBioClient,
)
return Hf3fsUsrBioClient(path, size, bytes_per_page, entries, client_timeout)
@dataclass
class _PoolStorageCtx:
"""Per-pool storage context for hybrid KV cache pools."""
pool_name: str
bytes_per_page: int
num_pages: int
namespace: PoolName
clients: List[Hf3fsClient]
gb_per_page: float
class HiCacheHF3FS(HiCacheStorage):
"""HiCache backend that stores KV cache pages in HF3FS files."""
default_env_var: str = "SGLANG_HICACHE_HF3FS_CONFIG_PATH"
def __init__(
self,
rank: int,
file_path: str,
file_size: int,
numjobs: int,
bytes_per_page: int,
entries: int,
client_timeout: int,
dtype: torch.dtype,
metadata_client: Hf3fsMetadataInterface,
is_mla_model: bool = False,
is_page_first_layout: bool = False,
use_mock_client: bool = False,
enable_storage_metrics: bool = False,
):
self.rank = rank
self.file_path = file_path
self.file_size = file_size
self.numjobs = numjobs
self.bytes_per_page = bytes_per_page
self.gb_per_page = bytes_per_page / (1 << 30)
self.entries = entries
self.client_timeout = client_timeout
self.dtype = dtype
self.metadata_client = metadata_client
self.is_mla_model = is_mla_model
self.is_page_first_layout = is_page_first_layout
self.enable_storage_metrics = enable_storage_metrics
self.use_mock_client = use_mock_client
self.numel = self.bytes_per_page // self.dtype.itemsize
self.num_pages = self.file_size // self.bytes_per_page
self.skip_backup = False
if self.is_mla_model and self.rank != 0:
self.skip_backup = True
self.rank = 0
self.is_zero_copy = False
logger.info(
f"[Rank {self.rank}] HiCacheHF3FS Client Initializing: "
f"file_path={self.file_path}, "
f"file_size={self.file_size / (2 ** 30):.2f} GB, "
f"num_pages={self.num_pages}, "
f"is_mla_model={self.is_mla_model}"
)
self.ac = AtomicCounter(self.numjobs)
self.clients = [
create_hf3fs_client(
self.file_path,
self.file_size,
self.bytes_per_page,
self.entries,
self.client_timeout,
use_mock_client,
)
for _ in range(numjobs)
]
self.executor = concurrent.futures.ThreadPoolExecutor(
max_workers=self.numjobs, thread_name_prefix=f"HiCacheHF3FS-Rank{self.rank}"
)
self.metadata_client.initialize(self.rank, self.num_pages)
self.lock = threading.RLock()
self._pool_storage_ctx: dict = {}
atexit.register(self.close)
signal.signal(signal.SIGINT, lambda sig, frame: self.close())
signal.signal(signal.SIGTERM, lambda sig, frame: self.close())
signal.signal(signal.SIGQUIT, lambda sig, frame: self.close())
self.prefetch_pgs = []
self.backup_pgs = []
self.prefetch_bandwidth = []
self.backup_bandwidth = []
@staticmethod
def from_env_config(
bytes_per_page: int,
dtype: torch.dtype,
storage_config: HiCacheStorageConfig = None,
) -> "HiCacheHF3FS":
"""Create a HiCacheHF3FS instance from environment configuration.
Environment:
- Uses env var stored in `HiCacheHF3FS.default_env_var` to locate a JSON config.
- Falls back to a local single-machine config when the env var is not set.
Raises:
ValueError: If MLA Model is requested without global metadata server or required keys are missing.
"""
from sglang.srt.mem_cache.storage.hf3fs.mini_3fs_metadata_server import (
Hf3fsGlobalMetadataClient,
Hf3fsLocalMetadataClient,
)
use_mock_client = False
if storage_config is not None:
rank, is_mla_model, is_page_first_layout = (
storage_config.tp_rank,
storage_config.is_mla_model,
storage_config.is_page_first_layout,
)
if storage_config.extra_config is not None:
use_mock_client = storage_config.extra_config.get(
"use_mock_hf3fs_client", False
)
else:
rank, is_mla_model, is_page_first_layout = (
0,
False,
False,
)
mla_unsupported_msg = f"MLA model is not supported without global metadata server, please refer to https://github.com/sgl-project/sglang/blob/main/python/sglang/srt/mem_cache/storage/hf3fs/docs/deploy_sglang_3fs_multinode.md"
config_path = os.getenv(HiCacheHF3FS.default_env_var)
if not config_path:
if is_mla_model:
raise ValueError(mla_unsupported_msg)
return HiCacheHF3FS(
rank=rank,
file_path=f"/data/hicache.{rank}.bin",
file_size=1 << 40,
numjobs=16,
bytes_per_page=bytes_per_page,
entries=8,
client_timeout=5,
dtype=dtype,
metadata_client=Hf3fsLocalMetadataClient(),
is_page_first_layout=is_page_first_layout,
use_mock_client=use_mock_client,
)
try:
with open(config_path, "r") as f:
config = json.load(f)
except Exception as e:
raise RuntimeError(f"Failed to load config from {config_path}: {str(e)}")
# Check required keys (metadata_server_url is now optional)
required_keys = {
"file_path_prefix",
"file_size",
"numjobs",
"entries",
}
missing_keys = required_keys - set(config.keys())
if missing_keys:
raise ValueError(f"Missing required keys in config: {missing_keys}")
# Choose metadata client based on configuration
if config.get("metadata_server_url"):
# Use global metadata client to connect to metadata server
metadata_server_url = config["metadata_server_url"]
metadata_client = Hf3fsGlobalMetadataClient(metadata_server_url)
logger.info(
f"Using global metadata client with server url: {metadata_server_url}"
)
else:
# Enable MLA optimization only when using the global metadata client
if is_mla_model:
raise ValueError(mla_unsupported_msg)
# Use local metadata client for single-machine deployment
metadata_client = Hf3fsLocalMetadataClient()
rank_for_path = 0 if is_mla_model else rank
return HiCacheHF3FS(
rank=rank,
# Let all ranks use the same file path for MLA model
file_path=f"{config['file_path_prefix']}.{rank_for_path}.bin",
file_size=int(config["file_size"]),
numjobs=int(config["numjobs"]),
bytes_per_page=bytes_per_page,
entries=int(config["entries"]),
client_timeout=config.get("client_timeout", 5),
dtype=dtype,
metadata_client=metadata_client,
is_mla_model=is_mla_model,
is_page_first_layout=is_page_first_layout,
use_mock_client=use_mock_client,
enable_storage_metrics=storage_config.enable_storage_metrics,
)
def _batch_get(
self,
keys: List[str],
values: List[torch.Tensor],
) -> List[bool]:
page_indices = self.metadata_client.get_page_indices(self.rank, keys)
if len(page_indices) != len(keys):
logger.error(
f"[Rank {self.rank}] HiCacheHF3FS get: page_indices length {len(page_indices)} mismatch keys length {len(keys)}."
)
return [False] * len(keys)
batch_indices, file_offsets = [], []
for i, page_index in enumerate(page_indices):
if page_index is not None:
batch_indices.append(i)
file_offsets.append(page_index * self.bytes_per_page)
for target_location in values:
assert target_location.is_contiguous()
file_results = values
start_time = time.perf_counter()
futures = [
self.executor.submit(
self.clients[self.ac.next()].batch_read,
file_offsets[i : i + self.entries],
file_results[i : i + self.entries],
)
for i in range(0, len(batch_indices), self.entries)
]
read_results = [result for future in futures for result in future.result()]
end_time = time.perf_counter()
ionum = len(batch_indices)
if self.enable_storage_metrics:
self.prefetch_pgs.append(ionum)
self.prefetch_bandwidth.append(
ionum / (end_time - start_time) * self.gb_per_page
)
results = [False] * len(keys)
for batch_index, read_result in zip(batch_indices, read_results):
if read_result == self.bytes_per_page:
results[batch_index] = True
else:
logger.error(
f"[Rank {self.rank}] HiCacheHF3FS get {keys[batch_index]} failed"
)
return results
def _batch_set(
self,
keys: List[str],
values: Optional[Any] = None,
) -> List[bool]:
# In MLA backend, only one rank needs to backup the KV cache
if self.skip_backup:
return True
# Todo: Add prefix block's hash key
key_with_prefix = [(key, "") for key in keys]
indices = self.metadata_client.reserve_and_allocate_page_indices(
self.rank, key_with_prefix
)
if len(indices) != len(keys):
logger.error(
f"[Rank {self.rank}] HiCacheHF3FS batch_get: mismatched lengths {len(indices)} != {len(keys)}"
)
# free allocated pages
if indices:
self.metadata_client.confirm_write(
self.rank, [], [index[1] for index in indices]
)
return [False] * len(keys)
batch_indices, file_offsets, file_values = [], [], []
pages_to_release = []
for i, (value, (is_written, page_index)) in enumerate(zip(values, indices)):
if is_written or page_index == -1:
continue
batch_indices.append(i)
file_offsets.append(page_index * self.bytes_per_page)
assert value.is_contiguous()
file_values.append(value)
start_time = time.perf_counter()
futures = [
self.executor.submit(
self.clients[self.ac.next()].batch_write,
file_offsets[i : i + self.entries],
file_values[i : i + self.entries],
)
for i in range(0, len(batch_indices), self.entries)
]
write_results = [
result == self.bytes_per_page
for future in futures
for result in future.result()
]
end_time = time.perf_counter()
ionum = len(batch_indices)
if self.enable_storage_metrics:
self.backup_pgs.append(ionum)
self.backup_bandwidth.append(
ionum / (end_time - start_time) * self.gb_per_page
)
written_keys_to_confirm = []
results = [index[0] for index in indices]
for batch_index, write_result in zip(batch_indices, write_results):
key = keys[batch_index]
page_index = indices[batch_index][1]
if write_result:
written_keys_to_confirm.append((key, page_index))
else:
logger.error(f"[Rank {self.rank}] HiCacheHF3FS set {key} failed")
pages_to_release.append(page_index)
results[batch_index] = write_result
if len(written_keys_to_confirm) > 0 or len(pages_to_release) > 0:
self.metadata_client.confirm_write(
self.rank, written_keys_to_confirm, pages_to_release
)
return results
def delete(self, key: str) -> None:
self.metadata_client.delete_keys(self.rank, [key])
def exists(self, key: str) -> bool:
result = self.metadata_client.exists(self.rank, [key])
return result[0] if result else False
def batch_exists(
self, keys: List[str], extra_info: Optional[HiCacheStorageExtraInfo] = None
) -> int:
factor = 1
if self.mha_zero_copy:
keys = self._get_mha_zero_copy_keys(keys)
factor = 2
results = self.metadata_client.exists(self.rank, keys)
i = 0
while i < len(keys) and results[i]:
i += 1
return i // factor
def clear(self) -> None:
try:
self.metadata_client.clear(self.rank)
for ctx in getattr(self, "_pool_storage_ctx", {}).values():
self.metadata_client.clear(self.rank, namespace=ctx.namespace)
logger.info(f"Cleared HiCacheHF3FS for rank {self.rank}")
except Exception as e:
logger.error(f"Failed to clear HiCacheHF3FS: {e}")
def close(self) -> None:
try:
for c in self.clients:
c.close()
for ctx in getattr(self, "_pool_storage_ctx", {}).values():
for c in ctx.clients:
c.close()
self.executor.shutdown(wait=True)
except Exception as e:
logger.error(f"close HiCacheHF3FS: {e}")
logger.info("close HiCacheHF3FS")
def get_stats(self):
storage_metrics = StorageMetrics()
storage_metrics.prefetch_pgs.extend(self.prefetch_pgs)
storage_metrics.backup_pgs.extend(self.backup_pgs)
storage_metrics.prefetch_bandwidth.extend(self.prefetch_bandwidth)
storage_metrics.backup_bandwidth.extend(self.backup_bandwidth)
self.prefetch_pgs.clear()
self.backup_pgs.clear()
self.prefetch_bandwidth.clear()
self.backup_bandwidth.clear()
return storage_metrics
def register_mem_pool_host(self, mem_pool_host: HostKVCache):
super().register_mem_pool_host(mem_pool_host)
self.is_zero_copy = self.mem_pool_host.layout in [
"page_first",
"page_first_direct",
]
self.mha_zero_copy = self.is_zero_copy and not self.is_mla_model
logger.info(f"{self.is_zero_copy=}, layout={self.mem_pool_host.layout}")
def register_mem_host_pool_v2(self, host_pool: HostKVCache, host_pool_name):
if host_pool_name == PoolName.KV:
return
super().register_mem_host_pool_v2(host_pool, host_pool_name)
pool_page_size = getattr(host_pool, "page_size", 1) or 1
pool_bytes_per_page = host_pool.get_ksize_per_token() * pool_page_size
pool_num_pages = self.file_size // pool_bytes_per_page
pool_file_path = f"{self.file_path}.{host_pool_name}"
namespace = host_pool_name # e.g. PoolName.MAMBA, PoolName.INDEXER
pool_clients = [
create_hf3fs_client(
pool_file_path,
self.file_size,
pool_bytes_per_page,
self.entries,
self.client_timeout,
self.use_mock_client,
)
for _ in range(self.numjobs)
]
self.metadata_client.initialize(self.rank, pool_num_pages, namespace=namespace)
self._pool_storage_ctx[host_pool_name] = _PoolStorageCtx(
pool_name=host_pool_name,
bytes_per_page=pool_bytes_per_page,
num_pages=pool_num_pages,
namespace=namespace,
clients=pool_clients,
gb_per_page=pool_bytes_per_page / (1 << 30),
)
logger.info(
f"[Rank {self.rank}] Registered hybrid pool '{host_pool_name}': "
f"bytes_per_page={pool_bytes_per_page}, num_pages={pool_num_pages}, "
f"namespace={namespace}, file={pool_file_path}"
)
def _get_mha_zero_copy_keys(self, keys: List[str]) -> List[str]:
_keys = []
for k in keys:
_keys.append(f"{k}-k")
_keys.append(f"{k}-v")
return _keys
def _get_mha_zero_copy_values(
self, values: List[torch.Tensor]
) -> List[torch.Tensor]:
_values = []
for value in values:
_values.append(value[0])
_values.append(value[1])
return _values
def _batch_get_preprocess(self, keys, host_indices):
page_num = len(host_indices) // self.mem_pool_host.page_size
# host_indices to kv_buffer
flat = not self.is_zero_copy
values = (
[
self.mem_pool_host.get_data_page(
host_indices[i * self.mem_pool_host.page_size], flat=flat
)
for i in range(page_num)
]
if self.is_zero_copy
else [
self.mem_pool_host.get_dummy_flat_data_page() for _ in range(page_num)
]
)
if self.mha_zero_copy:
keys = self._get_mha_zero_copy_keys(keys)
values = self._get_mha_zero_copy_values(values)
return keys, values
def _batch_get_postprocess(self, host_indices, values, results):
page_num = len(host_indices) // self.mem_pool_host.page_size
if self.is_zero_copy:
if not self.is_mla_model:
results = [
(results[2 * i] and results[2 * i + 1]) for i in range(page_num)
]
results = results[:page_num]
return results
for i in range(page_num):
if not results[i]:
break
self.mem_pool_host.set_from_flat_data_page(
host_indices[i * self.mem_pool_host.page_size], values[i]
)
return results
def batch_exists_v2(
self,
keys: List[str],
pool_transfers: Optional[List[PoolTransfer]] = None,
extra_info: Optional[HiCacheStorageExtraInfo] = None,
) -> PoolTransferResult:
kv_pages = self.batch_exists(keys, extra_info)
hit_count: dict = {PoolName.KV: kv_pages} if kv_pages else {}
final_pages = kv_pages
for transfer in pool_transfers or []:
if final_pages == 0:
break
pool_name = transfer.name
ctx = self._pool_storage_ctx.get(pool_name)
if ctx is None:
final_pages = 0
break
component_keys = [f"{key}_{pool_name}" for key in keys[:kv_pages]]
exists_results = self.metadata_client.exists(
self.rank, component_keys, namespace=ctx.namespace
)
boundary = 0
if transfer.hit_policy == PoolHitPolicy.ALL_PAGES:
try:
boundary = exists_results.index(False)
except ValueError:
boundary = kv_pages
elif transfer.hit_policy == PoolHitPolicy.TRAILING_PAGES:
trailing = max(1, len(transfer.keys) if transfer.keys else 1)
for prefix_len in range(kv_pages, 0, -1):
if all(
exists_results[i]
for i in range(max(0, prefix_len - trailing), prefix_len)
):
boundary = prefix_len
break
if boundary:
hit_count[pool_name] = boundary
final_pages = min(final_pages, boundary)
return PoolTransferResult(final_pages, hit_count)
def _pool_batch_get(self, transfer: PoolTransfer) -> List[bool]:
pool_name = transfer.name
ctx = self._pool_storage_ctx[pool_name]
host_pool = self.registered_pools[pool_name]
keys = transfer.keys
host_indices = transfer.host_indices
page_size = getattr(host_pool, "page_size", 1) or 1
page_num = len(keys)
component_keys = [f"{key}_{pool_name}" for key in keys]
page_indices = self.metadata_client.get_page_indices(
self.rank, component_keys, namespace=ctx.namespace
)
batch_indices, file_offsets, values = [], [], []
for i, page_index in enumerate(page_indices):
if page_index is not None:
batch_indices.append(i)
file_offsets.append(page_index * ctx.bytes_per_page)
values.append(host_pool.get_dummy_flat_data_page())
if not batch_indices:
return [False] * page_num
start_time = time.perf_counter()
futures = [
self.executor.submit(
ctx.clients[self.ac.next()].batch_read,
file_offsets[j : j + self.entries],
values[j : j + self.entries],
)
for j in range(0, len(batch_indices), self.entries)
]
read_results = [r for f in futures for r in f.result()]
end_time = time.perf_counter()
ionum = len(batch_indices)
if self.enable_storage_metrics:
self.prefetch_pgs.append(ionum)
self.prefetch_bandwidth.append(
ionum / (end_time - start_time) * ctx.gb_per_page
)
results = [False] * page_num
for idx, (batch_idx, read_result) in enumerate(
zip(batch_indices, read_results)
):
if read_result == ctx.bytes_per_page:
host_idx = host_indices[batch_idx * page_size].item()
host_pool.set_from_flat_data_page(host_idx, values[idx])
results[batch_idx] = True
else:
logger.error(
f"[Rank {self.rank}][Pool {pool_name.upper()}] HiCacheHF3FS get {keys[batch_idx]} failed"
)
return results
def _pool_batch_set(self, transfer: PoolTransfer) -> List[bool]:
pool_name = transfer.name
ctx = self._pool_storage_ctx[pool_name]
host_pool = self.registered_pools[pool_name]
keys = transfer.keys
host_indices = transfer.host_indices
page_size = getattr(host_pool, "page_size", 1) or 1
page_num = len(keys)
component_keys = [f"{key}_{pool_name}" for key in keys]
key_with_prefix = [(k, "") for k in component_keys]
indices = self.metadata_client.reserve_and_allocate_page_indices(
self.rank, key_with_prefix, namespace=ctx.namespace
)
if len(indices) != page_num:
logger.error(
f"[Rank {self.rank}] Pool {pool_name}: mismatched indices length"
)
if indices:
self.metadata_client.confirm_write(
self.rank, [], [idx[1] for idx in indices], namespace=ctx.namespace
)
return [False] * page_num
batch_indices, file_offsets, file_values = [], [], []
for i, (is_written, page_index) in enumerate(indices):
if is_written or page_index == -1:
continue
batch_indices.append(i)
file_offsets.append(page_index * ctx.bytes_per_page)
host_idx = host_indices[i * page_size].item()
data = host_pool.get_data_page(host_idx, flat=True)
assert data.is_contiguous()
file_values.append(data)
start_time = time.perf_counter()
futures = [
self.executor.submit(
ctx.clients[self.ac.next()].batch_write,
file_offsets[j : j + self.entries],
file_values[j : j + self.entries],
)
for j in range(0, len(batch_indices), self.entries)
]
write_results = [r == ctx.bytes_per_page for f in futures for r in f.result()]
end_time = time.perf_counter()
ionum = len(batch_indices)
if self.enable_storage_metrics:
self.backup_pgs.append(ionum)
self.backup_bandwidth.append(
ionum / (end_time - start_time) * ctx.gb_per_page
)
written_keys_to_confirm = []
pages_to_release = []
results = [idx[0] for idx in indices]
for batch_idx, write_ok in zip(batch_indices, write_results):
key = component_keys[batch_idx]
page_index = indices[batch_idx][1]
if write_ok:
written_keys_to_confirm.append((key, page_index))
else:
logger.error(
f"[Rank {self.rank}][Pool {pool_name.upper()}] HiCacheHF3FS set {keys[batch_idx]} failed"
)
pages_to_release.append(page_index)
results[batch_idx] = write_ok
if written_keys_to_confirm or pages_to_release:
self.metadata_client.confirm_write(
self.rank,
written_keys_to_confirm,
pages_to_release,
namespace=ctx.namespace,
)
return results
def batch_get_v2(
self,
transfers: List[PoolTransfer],
extra_info: Optional[HiCacheStorageExtraInfo] = None,
) -> dict:
results = {}
for transfer in transfers:
results[transfer.name] = self._pool_batch_get(transfer)
return results
def batch_set_v2(
self,
transfers: List[PoolTransfer],
extra_info: Optional[HiCacheStorageExtraInfo] = None,
) -> dict:
results = {}
for transfer in transfers:
results[transfer.name] = self._pool_batch_set(transfer)
return results
def batch_get_v1(
self,
keys: List[str],
host_indices: torch.Tensor,
extra_info: Optional[HiCacheStorageExtraInfo] = None,
) -> List[bool]:
keys, values = self._batch_get_preprocess(keys, host_indices)
results = self._batch_get(keys, values)
return self._batch_get_postprocess(host_indices, values, results)
def _batch_set_preprocess(self, keys, host_indices):
page_num = len(host_indices) // self.mem_pool_host.page_size
# host_indices to kv_buffer
flat = not self.is_zero_copy
values = [
self.mem_pool_host.get_data_page(
host_indices[i * self.mem_pool_host.page_size], flat=flat
)
for i in range(page_num)
]
if self.mha_zero_copy:
keys = self._get_mha_zero_copy_keys(keys)
values = self._get_mha_zero_copy_values(values)
return keys, values
def batch_set_v1(
self,
keys: List[str],
host_indices: torch.Tensor,
extra_info: Optional[HiCacheStorageExtraInfo] = None,
) -> List[bool]:
len_keys = len(keys)
keys, values = self._batch_set_preprocess(keys, host_indices)
results = self._batch_set(keys, values)
return results
# Deprecated
def get(
self,
key: str,
target_location: Optional[Any] = None,
target_sizes: Optional[Any] = None,
) -> torch.Tensor | None:
pass
# Deprecated
def batch_get(
self,
keys: List[str],
target_locations: Optional[Any] = None,
target_sizes: Optional[Any] = None,
) -> List[torch.Tensor | None] | int:
pass
# Deprecated
def set(
self,
key: str,
value: Optional[Any] = None,
target_location: Optional[Any] = None,
target_sizes: Optional[Any] = None,
) -> bool:
pass
# Deprecated
def batch_set(
self,
keys: List[str],
values: Optional[Any] = None,
target_locations: Optional[Any] = None,
target_sizes: Optional[Any] = None,
) -> bool:
pass
@@ -0,0 +1,44 @@
import multiprocessing.shared_memory
import sys
from pathlib import Path
import pytest
import torch
from torch.utils.cpp_extension import load
from tqdm import tqdm
root = Path(__file__).parent.resolve()
hf3fs_utils = load(
name="hf3fs_utils", sources=[f"{root}/hf3fs_utils.cpp"], verbose=True
)
def test_rw_shm():
numel = 8 << 20
dtype = torch.bfloat16
page_num = 128
page_bytes = numel * dtype.itemsize
shm = multiprocessing.shared_memory.SharedMemory(
size=page_num * page_bytes, create=True
)
tshm = torch.frombuffer(shm.buf, dtype=torch.uint8)
a = [
torch.randn(numel, dtype=dtype)
for _ in tqdm(range(page_num), desc="prepare input")
]
b = [
torch.empty(numel, dtype=dtype)
for _ in tqdm(range(page_num), desc="prepare output")
]
hf3fs_utils.write_shm(a, tshm)
hf3fs_utils.read_shm(tshm, b)
for _a, _b in tqdm(zip(a, b), desc="assert_close"):
torch.testing.assert_close(_a, _b)
del tshm
shm.close()
shm.unlink()
if __name__ == "__main__":
sys.exit(pytest.main([__file__]))