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
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:
@@ -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__]))
|
||||
Reference in New Issue
Block a user