Feat: Add TensorCast storage as a new HiCache backend (#27265)

This commit is contained in:
Yuhan Zhou
2026-09-22 10:19:04 +08:00
committed by GitHub
parent bc30fa1759
commit 15eba3b464
11 changed files with 3773 additions and 1 deletions
@@ -153,7 +153,7 @@ class Memory(msgspec.Struct):
hicache_storage_backend: A[ hicache_storage_backend: A[
Optional[str], Optional[str],
Arg( Arg(
help="The storage backend for hierarchical KV cache. Built-in backends: file, mooncake, npu_memcache, hf3fs, nixl, aibrix. For dynamic backend, use --hicache-storage-backend-extra-config to specify: backend_name (custom name), module_path (Python module path), class_name (backend class name).", help="The storage backend for hierarchical KV cache. Built-in backends: file, mooncake, npu_memcache, hf3fs, nixl, aibrix, tensorcast. For dynamic backend, use --hicache-storage-backend-extra-config to specify: backend_name (custom name), module_path (Python module path), class_name (backend class name).",
choices=[ choices=[
"file", "file",
"sim", "sim",
@@ -167,6 +167,7 @@ class Memory(msgspec.Struct):
"simm", "simm",
"mori", "mori",
"shm", "shm",
"tensorcast",
], ],
), ),
] = None ] = None
@@ -601,6 +601,7 @@ class HiCacheController:
"nixl", "nixl",
"simm", "simm",
"mori", "mori",
"tensorcast",
] ]
) or ( ) or (
self.storage_backend_type == "dynamic" self.storage_backend_type == "dynamic"
@@ -103,6 +103,18 @@ def get_allocator_from_storage(allocator_type):
return HostTensorAllocator() return HostTensorAllocator()
elif allocator_type == "shm": elif allocator_type == "shm":
return ShmHostTensorAllocator() return ShmHostTensorAllocator()
elif allocator_type == "tensorcast":
try:
from sglang.srt.mem_cache.storage.tensorcast_store.host_allocator import (
get_tensorcast_host_allocator_from_runtime,
)
return get_tensorcast_host_allocator_from_runtime()
except ImportError:
logger.warning(
"TensorCast's tensor allocator requires tensorcast >= 0.1.1. Please install TensorCast by 'pip install tensorcast' or build from source by following https://tensorcast.ai/development/build-from-source/. Fallback to use default allocator"
)
return HostTensorAllocator()
else: else:
return HostTensorAllocator() return HostTensorAllocator()
@@ -192,6 +192,8 @@ class StorageBackendFactory:
return backend_class(storage_config, mem_pool_host) return backend_class(storage_config, mem_pool_host)
elif backend_name == "shm": elif backend_name == "shm":
return backend_class(storage_config, mem_pool_host) return backend_class(storage_config, mem_pool_host)
elif backend_name == "tensorcast":
return backend_class(storage_config)
else: else:
raise ValueError(f"Unknown built-in backend: {backend_name}") raise ValueError(f"Unknown built-in backend: {backend_name}")
@@ -258,3 +260,9 @@ StorageBackendFactory.register_backend(
"sglang.srt.mem_cache.storage.shm", "sglang.srt.mem_cache.storage.shm",
"HiCacheShm", "HiCacheShm",
) )
StorageBackendFactory.register_backend(
"tensorcast",
"sglang.srt.mem_cache.storage.tensorcast_store.tensorcast_store",
"TensorcastStore",
)
@@ -0,0 +1,368 @@
# TensorCast as an L3 KV Cache
This document describes how to use TensorCast as the L3 storage backend for
SGLang HiCache. The initial integration targets the Unified Radix Cache FULL
pool and uses TensorCast's public process-scoped
`RegionBackedArtifactSession`; SGLang does not construct daemon requests,
region layouts, or canonical TensorCast artifact IDs.
Related documentation:
- [TensorCast project](https://tensorcast.ai)
- [TensorCast repository](https://github.com/tensorcast-ai/tensorcast)
- [HiCache system design](https://docs.sglang.io/advanced_features/hicache_design.html)
## About TensorCast
TensorCast manages model state, KV caches, checkpoints, and other tensor state
as distributed artifacts. It separates cluster-wide discovery and routing from
host-local memory and data transfer:
- One **Global Store** manages artifact metadata, replica state, and routing.
- One **StoreDaemon** on each serving host owns local memory regions and moves
artifact bytes locally or between hosts.
- Each SGLang rank attaches one process-scoped Session to its node-local
StoreDaemon. The Session owns that rank's regions, RPC health, and transfer
protocol details.
SGLang HiCache continues to decide when pages are published and prefetched.
TensorCast stores and moves the K/V fragments that make up those pages.
### Transfer modes
Both modes use the same artifact identity and HiCache operation path.
1. **Allocator-backed direct mode** is the default and recommended mode.
- The TensorCast Session allocates the CPU tensors used to back the standard
SGLang HostPool.
- L2/L3 operations submit spans in those tensors directly, without an
SGLang-side staging copy.
- Separate HostPool allocations, including asymmetric MHA K and V tensors,
become separate Session-managed regions automatically.
2. **Scratch mode** is the compatibility mode.
- The standard SGLang allocator owns the HostPool tensors.
- The Session lazily creates one fixed-capacity get arena and one
fixed-capacity put arena and copies between them and the HostPool.
- Operators must size `scratch.capacity_bytes` for the largest storage
batch. Registration fails at startup if the configured capacity is too
small.
## Requirements and Initial Scope
The initial integration requires:
- Linux and SGLang's CUDA backend;
- Unified Radix Cache with the primary FULL KV pool;
- `--hicache-host-memory-mode cache`;
- either `page_first` with the `kernel` I/O backend or
`page_first_direct` with the `direct` I/O backend;
- a Global Store and node-local StoreDaemon started and ready before SGLang;
- `engine.cpu_shared_memory.enabled: true` in the StoreDaemon config; and
- a daemon-visible SGLang owner PID. Containers must share a PID namespace
or provide an equivalent PID visibility arrangement.
The FULL path supports standard MHA, asymmetric MHA with separate K/V HostPool
tensors, and MLA. Currently TensorCast L3 does not support `buffer_only`, non-CUDA
platforms, `layer_first`, `page_head`, split-head layouts, DCP/attention CP,
packed MTP draft pools, or storage-v2 sidecar transfers such as Mamba and SWA.
There is no automatic fallback from allocator mode to scratch mode.
## Install TensorCast
Install an ABI-compatible TensorCast SDK and daemon:
```bash
pip install tensorcast
```
If the published wheel's Torch or CUDA build does not match the SGLang
environment, build TensorCast from source instead. See the
[TensorCast build guide](https://github.com/tensorcast-ai/tensorcast/blob/main/docs/development/build-from-source.md).
TensorCast is an optional SGLang dependency. Importing SGLang does not import
TensorCast unless this storage backend is selected.
## Deployment
The recommended deployment is operator-managed services plus SDK attachment:
- The operator starts one Global Store for the TensorCast cluster.
- The operator starts one StoreDaemon on every SGLang host and waits for it to
become ready.
- SGLang ranks attach to the local daemon during HostPool construction.
- SGLang never starts, restarts, supervises, or stops TensorCast services.
### Single-host deployment
Run the following commands from the SGLang repository root. The checked-in
files under `configs/` are starter configurations and must be sized and secured
for the target deployment.
**Step 1: Prepare the environment**
```bash
export TC_CONFIG_DIR="$PWD/python/sglang/srt/mem_cache/storage/tensorcast_store/configs"
export TC_GLOBAL_SESSION=sglang-tensorcast-global
export TC_DAEMON_SESSION=sglang-tensorcast-daemon
# Large KV publishes can retain more than 1,024 artifact memfds.
ulimit -n 65535
```
**Step 2: Start the Global Store**
```bash
tensorcast-cli global start \
--config "${TC_CONFIG_DIR}/global_store_config.yaml" \
--gs-session "${TC_GLOBAL_SESSION}"
```
The checked-in config listens on port `50051`.
**Step 3: Start the StoreDaemon**
```bash
tensorcast-cli daemon start \
--config "${TC_CONFIG_DIR}/store_daemon_config.yaml" \
--global-store-mode connect \
--global-store-address 127.0.0.1:50051 \
--session "${TC_DAEMON_SESSION}"
```
The checked-in daemon config listens for SDK RPCs on port `50052` and uses
port `65090` for P2P transfers.
Its capability-token secret is fixed test material for local validation only.
A production copy must replace
`capability_tokens.active.secret` with independently generated secret material.
The Global Store and every daemon in a deployment must also use a consistent,
deployment-specific cluster identity.
**Step 4: Verify service readiness**
```bash
tensorcast-cli global status --gs-session "${TC_GLOBAL_SESSION}"
tensorcast-cli daemon status --session "${TC_DAEMON_SESSION}"
```
Do not start SGLang until both commands succeed. Session attachment additionally
checks that the daemon is ready, CPU shared memory is enabled, its local handle
service is reachable, and the endpoint is node-local.
**Step 5: Start SGLang in allocator mode**
Allocator mode is selected when `transfer_mode` is omitted:
```bash
python -m sglang.launch_server \
--model-path <model-path> \
--enable-hierarchical-cache \
--hicache-host-memory-mode cache \
--hicache-ratio 1 \
--hicache-mem-layout page_first_direct \
--hicache-io-backend direct \
--hicache-write-policy write_through \
--hicache-storage-backend tensorcast \
--hicache-storage-backend-extra-config '{
"tensorcast": {
"daemon_address": "127.0.0.1:50052"
}
}'
```
`page_first` plus `kernel` is also supported. Transfer mode controls L2/L3
movement; it does not silently select or change the HostPool layout or L1/L2
I/O backend.
**Scratch-mode alternative**
```bash
python -m sglang.launch_server \
--model-path <model-path> \
--enable-hierarchical-cache \
--hicache-host-memory-mode cache \
--hicache-ratio 1 \
--hicache-mem-layout page_first \
--hicache-io-backend kernel \
--hicache-write-policy write_through \
--hicache-storage-backend tensorcast \
--hicache-storage-backend-extra-config '{
"tensorcast": {
"daemon_address": "127.0.0.1:50052",
"transfer_mode": "scratch",
"scratch": {
"capacity_bytes": 4294967296
}
}
}'
```
The 4 GiB value is only an example. Use the sizing rule below for the selected
model, rank topology, dtype, page size, and HostPool capacity.
**Step 6: Shut down in ownership order**
First terminate SGLang normally and allow its storage workers to join. Then
stop the StoreDaemon before the Global Store:
```bash
tensorcast-cli daemon stop --session "${TC_DAEMON_SESSION}"
tensorcast-cli global stop --gs-session "${TC_GLOBAL_SESSION}"
```
Do not stop the daemon while attached SGLang ranks are still running. The
StoreDaemon reclaims a rank's process-pinned stable region backing after that
rank process exits.
### Multi-host and multi-instance deployment
One Global Store may serve many hosts. Each host runs a StoreDaemon, and all
SGLang ranks on that host attach to its node-local endpoint:
```text
Global Store
(one per cluster)
|
+---------------+---------------+
| |
StoreDaemon A <----- P2P -----> StoreDaemon B
/ | \ / \
rank 0 rank 1 rank N rank 0 rank N
```
Multiple ranks and multiple SGLang instances may attach to the same local
daemon. Every rank owns a distinct process Session and one or more distinct
regions. SGLang appends `rank{world_rank}of{world_size}` to the configured
Session and region prefixes, while TensorCast makes concrete region names
PID-unique.
Instances reuse artifacts only when their artifact identity inputs agree,
including namespace, model ID, model version, FULL layout, dtype, page size,
and TP/PP rank topology. Each instance's rank 0 consumes the artifact shard
published by rank 0 of a compatible instance; ranks do not consume one
another's shards.
For cross-host traffic, configure every daemon to register with the same Global
Store and ensure its advertised and P2P addresses are reachable. Enable and
tune RDMA in the daemon config only when the host fabric and drivers support
it; otherwise configure the supported TCP transport.
## Configuration
The full extra-config value is a JSON object, or an `@path` reference to a
JSON, YAML, or TOML file. TensorCast-specific fields must be nested under
`tensorcast`:
```json
{
"prefetch_threshold": 256,
"prefetch_timeout_base": 1.0,
"prefetch_timeout_per_ki_token": 0.25,
"hicache_storage_pass_prefix_keys": false,
"tensorcast": {
"daemon_address": "127.0.0.1:50052",
"namespace": "default",
"transfer_mode": "allocator",
"model_id": null,
"model_version": "unversioned",
"session_name_prefix": "sglang",
"region_name_prefix": "sglang_tensorcast",
"exists_timeout_s": 30.0,
"transfer_timeout_s": null,
"scratch": {
"capacity_bytes": 16777216
}
}
}
```
### TensorCast fields
| Field | Default | Meaning |
|---|---|---|
| `daemon_address` | required | StoreDaemon RPC endpoint passed unchanged to TensorCast, for example `127.0.0.1:50052`. |
| `namespace` | `default` | Byte-artifact namespace. Compatible publishers and consumers must use the same value. |
| `transfer_mode` | `allocator` | Selects `allocator` or `scratch` for the lifetime of the rank process. |
| `model_id` | SGLang storage model name | Optional stable model identity override. |
| `model_version` | `unversioned` | Immutable model/config revision. Production deployments should set a release- or content-specific value to prevent stale KV reuse after a model change. |
| `session_name_prefix` | `sglang` | Diagnostic Session prefix; SGLang appends the world-rank label. |
| `region_name_prefix` | `sglang_tensorcast` | Diagnostic region prefix; SGLang appends the world-rank label and TensorCast makes concrete names PID-unique. |
| `exists_timeout_s` | `30.0` | Positive metadata-only exists deadline in seconds. |
| `transfer_timeout_s` | `null` | Optional positive transfer deadline passed unchanged to TensorCast. Region get/put uses zero transparent retries. |
| `scratch.capacity_bytes` | `16777216` | Positive capacity in bytes for each scratch-direction arena. Ignored by allocator mode. |
### Generic HiCache fields
These optional fields remain at the top level of the extra config:
| Field | Default | Meaning |
|---|---|---|
| `prefetch_threshold` | `256` | Minimum prefix length in tokens before storage prefetch is attempted. |
| `prefetch_timeout_base` | `1.0` | Fixed portion of the Unified prefetch timeout in seconds. |
| `prefetch_timeout_per_ki_token` | `0.25` | Additional timeout in seconds per 1,024 tokens. |
| `hicache_storage_pass_prefix_keys` | `false` | Pass prefix keys to storage backends; TensorCast FULL v1 does not use them. |
### Scratch capacity
The exact minimum is known only after SGLang constructs and registers the
HostPool:
```text
maximum_batch_pages = min(128, host_pool.page_num)
page_bytes = sum(bytes of all registered fragments in one logical page)
minimum_capacity_bytes = maximum_batch_pages * page_bytes
```
For MHA, `page_bytes` includes both K and V. For MLA, it contains the single
combined KV fragment. The configured value applies independently to the lazy
get and put arenas, so budget up to `2 * scratch.capacity_bytes` of additional
host memory after both directions have been used.
The 16 MiB default is syntactically valid but may be too small for a real
model. An insufficient value fails rank startup with the configured and
required byte counts plus the calculated page geometry. Runtime scratch growth
is not supported.
### Supported layout matrix
| Host-memory mode | Transfer mode | Host layout | I/O backend | Result |
|---|---|---|---|---|
| `cache` | `allocator` | `page_first` | `kernel` | Supported; allocator is the default transfer mode. |
| `cache` | `allocator` | `page_first_direct` | `direct` | Supported and recommended for direct L1/L2 I/O. |
| `cache` | `scratch` | `page_first` | `kernel` | Supported with one copy at the L2/L3 boundary. |
| `cache` | `scratch` | `page_first_direct` | `direct` | Supported with one copy at the L2/L3 boundary. |
| `buffer_only` | either | any | any | Rejected. |
| `cache` | either | other layouts | any | Rejected initially. |
## Runtime and Failure Semantics
Session attachment is an early startup operation. A daemon readiness,
configuration, or allocator failure aborts rank startup; SGLang does not fall
back to another transfer mode.
During a supported FULL-v1 exists/get/put operation, a fatal Session or adapter
exception permanently disables TensorCast L3 for that rank. Subsequent calls
report no storage hits or false page results, while the SGLang worker continues
running and may recompute the missing prefix locally. Other ranks retain their own
rank-local Session health.
Transfer mode, daemon endpoint, and Session options cannot be changed at
runtime. A failed or terminated Session cannot reattach in the same rank
process. Restart the rank to establish a new Session.
`TensorcastStore.close()` terminates Session admission but does not directly
release process-pinned allocator or scratch regions. Their mappings remain
valid for the HostPool lifetime, and the daemon reclaims stable backing only
after the SGLang rank exits.
## Troubleshooting
- **Attach fails before model startup:** verify Global Store and StoreDaemon
status, `cpu_shared_memory.enabled`, the configured daemon address, local
handle socket access, and PID visibility.
- **`Too many open files` from `memfd_create`:** raise the StoreDaemon's
inherited `RLIMIT_NOFILE`, for example with `ulimit -n 65535`, and restart
the failed daemon and rank processes.
- **Scratch capacity is insufficient:** use the exact required byte count in
the startup error and increase `scratch.capacity_bytes`; remember that get
and put may allocate two arenas of that size.
@@ -0,0 +1,93 @@
# Config metadata for auditing and cluster safety.
meta:
schema_version: "v1"
description: "Example config file for TensorCast Global Store"
cluster_token: ""
database:
db_file: null
server:
listen:
host: 0.0.0.0
port: 50051
advertise:
host: ""
port: 0
max_workers: 10
grpc:
max_concurrent_streams: 0
keepalive_time: 120s
keepalive_timeout: 0s
max_connection_idle: 0s
max_connection_age: 0s
tcp_nodelay: false
so_reuseport: false
tls:
enabled: false
cert_file: ""
key_file: ""
client_ca_file: ""
metrics_port: 18000
worker_policy:
heartbeat_timeout: 30s
cleanup_interval: 60s
default_heartbeat_interval: 5s
memory_tiers:
snapshot_retention: 600s
snapshot_max_rows: 200
publish_interval: 5s
key_mapping:
alias_cache_ttl: 1s
transport_scheduler:
mode: TRANSPORT_SCHEDULER_MODE_GROUP_DISPATCH
source_balance_weights:
replica_load_weight: 1.0
worker_load_weight: 1.0
recent_assignment_penalty_weight: 1.0
diffusion_bonus_weight: 1.0
group_dispatch:
fairness_floor_ratio: 0.25
completion_bias_weight: 1.0
starvation_aging_threshold: 5s
queue_scan_limit: 128
dispatch_batch_limit: 16
limits:
digest_writes:
max_leaf_writes_per_request: 16384
max_proof_digests_per_request: 16384
max_total_digests_per_request: 32768
max_digest_bytes_per_request: 2097152
operation_leases:
default_ttl: 30s
max_ttl: 300s
operation_writes:
min_status_update_interval: 1s
retention:
operations_ttl: 86400s
assembly_proof_commitments_ttl: 86400s
piece_proof_digests_ttl: 86400s
observability:
logging:
level: INFO
file: ""
otel_context_enabled: false
sink_file: ""
vlog_level: 0
otel:
enabled: false
exporter_otlp_endpoint: http://127.0.0.1:4317
exporter_protocol: grpc
service_name: tensorcast-global-store
sampler: parentbased_traceidratio
sampler_arg: "1.0"
otel_cxx:
sdk_disabled: false
exporter_insecure: false
exporter_headers: {}
console_exporter: false
tracing:
chrome_trace_dir: ""
@@ -0,0 +1,389 @@
# Config metadata for auditing and cluster safety.
meta:
# Schema version label for auditing; keep empty unless you enforce schema checks.
schema_version: ""
# Free-form description for operators and change tracking.
description: "Example config file for TensorCast Store Daemon"
# Cluster identity token to prevent split-brain when talking to Global Store.
cluster_token: ""
# gRPC server settings.
server:
# Control-plane listen socket for the gRPC API.
listen:
# Bind host for gRPC; use 0.0.0.0 to expose all NICs or a specific interface to fence access.
host: 0.0.0.0
# gRPC listen port; this is the port advertised to Global Store regardless of advertise.port.
port: 50052
# Advertised address for Global Store registration; set when auto-detect might pick a non-routable IP.
advertise:
# Routable host that clients and Global Store should dial; avoid loopback and link-local.
# If empty or non-routable, the daemon resolves in order: listen.host (routable),
# route IP toward Global Store, then default interface IP.
host: null # automatically resolve
# Dedicated P2P listen socket; use a separate NIC/port to isolate data-plane traffic.
p2p_listen:
# Bind host for P2P transfers; often set to the RDMA or high-bandwidth interface.
host: 0.0.0.0
# P2P port; must be non-zero when high_availability.enabled is true.
port: 65090
# Optional default root for relative disk_path values.
# When empty, disk materialization accepts absolute disk_path only.
storage_path: ""
# StoreEngine I/O worker threads; tune for disk/P2P parallelism vs CPU contention.
num_threads: 16
# gRPC server channel args and TLS.
grpc:
# Max concurrent HTTP/2 streams; 0 uses gRPC default.
max_concurrent_streams: 0
# Keepalive ping interval; set 0s to disable periodic pings.
keepalive_time: 30s
# Keepalive timeout before terminating a dead connection.
keepalive_timeout: 10s
# Close idle connections after this duration; 0s disables.
max_connection_idle: 10m
# Hard connection max age; 0s disables.
max_connection_age: 0s
# Disable Nagle when true; set true for low-latency RPCs.
tcp_nodelay: false
# Enable SO_REUSEPORT for listener load balancing.
so_reuseport: false
tls:
# Enable TLS for the daemon gRPC server.
enabled: false
# PEM-encoded server certificate.
cert_file: ""
# PEM-encoded private key for cert_file.
key_file: ""
# Optional client CA bundle for mTLS; empty disables client cert checks.
client_ca_file: ""
# StoreEngine and data-plane memory/transfer tuning.
engine:
# UMA chunk granularity; larger chunks reduce metadata but make eviction/leases coarser.
artifact_chunk_bytes: 256MB
# Default depth for StreamingPinnedBuffer instances created by StoreEngine.
streaming_buffer_chunks: 8
# Enable external target verification for MaterializeIntoTarget (off by default).
enable_external_target_verification: false
# Enable memfd-backed UMA CPU allocations for zero-copy CPU tensor materialization.
cpu_shared_memory:
enabled: true
# Memory tiers define stable/preemptible behavior; tune for DRAM pressure and HA telemetry.
memory_tiers:
# When false, CPU chunks stay resident (no preemptible marking); use for stability or debugging.
enable_preemptible: false
# Stable budget in bytes; must fit in host DRAM minus total pinned pools (sum(pinned_memory.classes[].pool_bytes)) or startup fails.
# Sized for the single-GPU local validation host. This leaves room for the
# SGLang worker and its HiCache HostPool while retaining ample L3 capacity.
stable_bytes: 32GB
# Cap for preemptible pool; ignored when enable_preemptible=false (0 means no preemptible budget).
preemptible_limit_bytes: 0
# Reclaim target ratio for preemptible pressure; lower values reclaim more aggressively.
preemptible_low_watermark_ratio: 0.4
# Promotion policy for P2P routing.
promotion:
# Promotion controls whether the daemon will "export" newly materialized
# replicas to become P2P-routable sources in the Global Store (i.e., publish
# `export_state=EXPORTABLE` + transport metadata).
#
# IMPORTANT: Promotion is only attempted when the *client request* asks for
# it via `export_policy` (SDK `GetArtifactOptions.export_policy`):
# - `never` (default): do not attempt promotion for this materialize.
# - `auto`: allow policy-driven promotion (if the daemon policy permits).
# - `force`: request an immediate promotion attempt (still gated by daemon policy).
#
# Policy values:
# - PROMOTION_POLICY_NEVER:
# Never promote; replicas remain presence-only and are not P2P targets.
# - PROMOTION_POLICY_ON_MATERIALIZE:
# Promote after any successful materialize when `export_policy` is `auto` or `force`.
# - PROMOTION_POLICY_ON_HOTNESS:
# Promote only when `export_policy=force` (conservative default; treat "force" as
# the caller's "this is hot" signal; `auto` does not promote).
# - PROMOTION_POLICY_ON_POLICY:
# Currently behaves like ON_MATERIALIZE (promote on `auto`/`force`); reserved for
# future policy-based gating beyond the request signal.
policy: PROMOTION_POLICY_ON_MATERIALIZE
# Require verification metadata before exporting.
require_verified: false
# Bounded drain timeout before forced demotion.
demotion_drain_timeout: 30s
# Unified pinned memory configuration for all daemon-side pinned usage.
pinned_memory:
# Phase 1 fixed-allocation: total pinned bytes is derived as sum(classes[].pool_bytes).
# Startup fail-fast: before allocating pinned pools, the daemon checks current available memory and
# exits early if insufficient. Required bytes = pinned_total + engine.memory_tiers.stable_bytes + headroom,
# where headroom = min(10% * (pinned_total + stable_bytes), 10GiB).
# Deadline for pinned slice acquisition under pressure.
allocation_timeout: 30s
classes:
- name: engine
slice_bytes: 256MB
# 1 GPU * streaming_buffer_chunks(8) = 8 slices required at startup.
# 8 * 256MB = 2GB minimum engine pinned pool.
pool_bytes: 2GB
- name: comm_gpu
slice_bytes: 16MB
# 3GB covers the configured 16 buffers/flow, 8 TCP connections, and
# expected_gpu_channels=8 startup sizing on the local validation host.
pool_bytes: 3GB
rdma_preregister: true
- name: comm_cpu
slice_bytes: 16MB
# 16 buffers/flow require at least 16 slices (256MB).
pool_bytes: 512MB
rdma_preregister: true
# Post-seal policies for assembly -> mi2 alias resolution.
post_seal:
# When true, migrate selected assembly views under the sealed mi2_id.
migrate_views: false
# When true, only migrate views that require transforms (e.g., transpose).
migrate_transpose_only: false
# When true, allow safe reuse of CGID-scoped view replicas after sealing.
reuse_views_if_safe: false
# When true, retire CGID-scoped piece replicas after sealing.
retire_pieces: false
# Capability token keys (daemon-issued capability envelope).
capability_tokens:
active:
# Token version id; rotate by incrementing version and moving old key to previous.
version: 1
# Secret bytes (base64-encoded in YAML/JSON). This is a dummy non-production key as an example; production deployments
# must replace it with independently generated secret material.
secret: "c2dsYW5nLXRlbnNvcmNhc3QtbG9jYWwtdGVzdC1rZXk="
previous: []
# Retention handle settings (control-plane only; requires capability_tokens).
retention_handles:
# When true, daemon issues retention handles for local stable DRAM.
enabled: false
# Default TTL when caller does not specify ttl_ms.
default_ttl: 10m
# Maximum allowed TTL for any handle (issuer clamp).
max_ttl: 24h
# Capability directory publishing (Global Store capability flags).
capability_directory:
enabled: true
# Global Store registration and heartbeat coordination.
high_availability:
# When true, the daemon registers with Global Store and emits heartbeats/sync; requires p2p_listen.port.
enabled: true
# Global Store endpoints; keep the first entry as the primary registration target.
global_store_endpoints:
-
# Host for Global Store gRPC; must be reachable from the daemon.
host: 127.0.0.1
# Port for Global Store gRPC.
port: 50051
# Heartbeat cadence; keep comfortably below Global Store worker_policy.heartbeat_timeout.
heartbeat_interval: 10s
# Chunk inventory sync cadence; set 0s to disable sync thread.
periodic_sync_interval: 30s
# Retry attempts for HA registration/requests; currently uses GlobalStoreClient defaults.
max_retries: 3
# Backoff between registration retries; currently not wired in the daemon.
registration_retry_delay: 500ms
# Background cleanup, eviction, and TTL scheduling.
lifecycle:
# GPU memory fraction that triggers eviction when periodic eviction is enabled; lower values evict earlier.
gpu_memory_limit_fraction: 0.75
# Enable background eviction loop; disable if you want strictly demand-driven eviction.
enable_periodic_eviction: false
# Eviction poll cadence; smaller improves responsiveness but increases background work.
eviction_loop_interval: 1s
# Legacy alias for eviction_loop_interval; 0s keeps eviction_loop_interval in effect.
eviction_check_interval: 0s
# PID liveness polling interval used when pidfd is unavailable; shorter detects dead clients faster.
proc_check_interval: 5s
# Sweep interval for session TTL cleanup; keep below sessions_ttl for timely reclamation.
sessions_sweep_interval: 10s
# Sweep interval for transport lock expiry; keep below locks_ttl to avoid stale locks.
locks_sweep_interval: 10s
# Sweep interval for verification tracker cleanup/timeouts; smaller reduces tail latency on failures.
verification_sweep_interval: 500ms
# Session TTL; must exceed client keepalive cadence to avoid premature eviction.
sessions_ttl: 60s
# Transport lock TTL; long enough for worst-case transfers but short enough to recover from leaks.
locks_ttl: 120s
# Local-only Unix domain socket for handle FD handoff and lease release.
handle_leases:
# Optional override. Leave empty to let the daemon auto-select
# <daemon_state_dir>/local_handle.sock for same-pod/local SDKs.
# daemon_state_dir defaults to:
# $TENSORCAST_HOME/hosts/<host_id>/sessions/<session_id>/session
# (or ~/.tensorcast/hosts/<host_id>/sessions/<session_id>/session)
# Auto-discovery relies on TENSORCAST_INSTANCE (CLI/SDK-launched daemon).
# If TENSORCAST_INSTANCE is not set, the daemon falls back to:
# $TENSORCAST_HOME/hosts/<host_id>/runtime/daemons/<daemon_id>/local_handle.sock
# If the selected socket path exceeds AF_UNIX limits, the daemon falls back to:
# $TENSORCAST_HOME/uds/lh-<hash>.sock
# Set explicitly when daemon and client SDK run in different pods and need a shared path.
# Socket path must have a daemon-owned parent directory (not world-writable); create the directory beforehand.
local_handle_socket_path: ""
# Handle lease TTL as a crash-safety fallback; live clients release leases when tensors are freed.
# Set to 0s to disable TTL (leases rely on explicit release and PID-exit cleanup).
ttl: 0s
# Best-effort guardrail: limit lease-bearing handle mints per second (0 => unlimited).
max_mints_per_second: 0
# Communicator transport configuration (TCP/RDMA).
communicator:
# Enable RDMA transport; requires RDMA-capable NICs and correct kernel/driver setup.
enable_rdma: True
# Example tuning block (defaults shown below); adjust per workload.
stager:
# Stage CPU tensors for RDMA; disable only if you rely on direct GPU RDMA paths.
stage_cpu_for_rdma: true
# Buffers per flow; more buffers increase overlap but require a larger pool.
buffers_per_flow: 16
# Cap concurrent in-flight segments; 0 inherits buffers_per_flow.
max_window_segments: 0
# Cap concurrent GPU channels so pool sizing is predictable (8 for 8-GPU defaults; 0 = auto).
expected_gpu_channels: 8
rdma:
# Outstanding WRs per QP; increase for bandwidth, decrease to reduce CPU/latency.
outstanding_wr: 64
# ACK TTL; increase on high-latency fabrics to avoid premature cleanup.
ack_ttl_ms: 30000
# QP tuning; adjust only if you understand fabric QoS/timeouts.
traffic_class: 186
qp_timeout: 20
qp_retry: 7
# Multi-QP configuration for high-throughput scenarios
qp_count: 1 # Number of QPs per transport (1-16, default: 1)
bonding_balance: false # Enable LAG port balancing (default: false)
# Reuse preregistered stable local MRs for read_plan target windows when available.
# Disable only for A/B or debugging request-scoped MR registration behavior.
enable_stable_local_mr_reuse: true
# Slot-aligned MR reuse chunk size for stable local backing.
stable_local_mr_reuse_chunk_slots: 32
# Async all-rail chunk prewarm. Unset or 0 disables; values >0 enable one
# prewarm job per visible rail on a bounded communicator-local worker pool.
stable_local_mr_reuse_prewarm_workers: 1
transport:
# TCP connections per peer; more lanes help bandwidth but add CPU overhead.
tcp_conn_count: 8
# Connect timeout; increase when handshakes are slow or firewalled.
connect_timeout_sec: 10
# TOS/DSCP value; keep 0 unless fabric QoS is configured.
tcp_tos: 0
# Allow SO_REUSEPORT on listeners; disable if OS load balancing is undesirable.
so_reuseport: true
affinity:
# Reserved for future CPU pinning policies.
enable: false
simple_numa:
# NIC/GPU locality mapping for multi-socket nodes.
enable: false
# Example topology mapping:
# nodes:
# - id: 0
# nics: ["mlx5_0", "mlx5_1"]
# gpus: [0, 1]
# is_default: true
nodes: []
# Logging and tracing configuration (best-effort).
observability:
# Logging output and verbosity controls.
logging:
# Minimum severity emitted; higher levels reduce noise at the cost of debugging detail.
level: INFO
# Optional plain-text logfile; empty logs only to stderr.
file: ""
# When true, include trace/span IDs in logs (if tracing is enabled).
otel_context_enabled: false
# Optional enriched sink file with trace/span IDs; leave empty to disable.
sink_file: ""
# VLOG verbosity level; 0 disables VLOG.
vlog_level: 0
# OpenTelemetry export settings.
otel:
# Enable OTLP export; when false, other OTel settings are ignored.
enabled: false
# Collector endpoint; must be reachable from the daemon host.
exporter_otlp_endpoint: http://127.0.0.1:4317
# Transport protocol for OTLP; aliases like "grpc" or "http/protobuf" are normalized.
exporter_protocol: grpc
# Stable logical service name for grouping traces/metrics across nodes.
service_name: tensorcast-store-daemon
# Sampler name for Python OTel SDK; C++ currently ignores this field.
sampler: parentbased_traceidratio
# Sampler argument (ratio for traceidratio); keep 1.0 for full sampling.
sampler_arg: "1.0"
otel_cxx:
# Disable the C++ SDK when embedding in C++ processes.
sdk_disabled: false
# Force insecure OTLP gRPC; set true when using non-TLS collectors.
exporter_insecure: false
# Static OTLP headers (e.g., auth tokens).
exporter_headers: {}
# Emit spans to stdout for debugging.
console_exporter: false
tracing:
# Chrome trace output directory; empty disables.
chrome_trace_dir: ""
# Compatibility toggles for legacy behavior.
compatibility:
# Require explicit disk_path for confirm RPCs; reserved for legacy clients.
confirm_requires_disk_path: false
# Status to return on verification timeouts; set OK or DEADLINE when wired.
verification_timeout_status: VERIFICATION_TIMEOUT_STATUS_UNSPECIFIED
# Evict replicas on dead PID detection; reserved until wired.
evict_on_dead_pid: false
# Auto-register disk loads; reserved until wired.
auto_register_disk_loads: false
# Force full-digest verification on load; reserved until wired.
force_full_digest_on_load: false
# Debug-only toggles; keep disabled in production unless explicitly needed.
debug:
# CUDA debug toggles used for tests and local runs.
cuda:
# Same-process CUDA IPC fallback for tests; breaks real IPC semantics across processes.
enable_same_process_ipc_fallback: false
# Byte-artifact routing and payload transport defaults. Primarily used for distributed KV store
byte_artifact_routing:
# Number of routing shards used for authority fanout.
# Recommended: 2N shards for N TensorCast daemons in the cluster
shard_count: 8
# How long route resolution can be reused before forcing a refresh.
route_staleness_budget: 5s
# Lease TTL for published byte-artifact routes.
lease_ttl: 30s
# Keepalive cadence for refreshing active route leases.
keepalive_interval: 10s
# Worker-directory cache freshness budget used before refetching active workers.
worker_directory_staleness_budget: 30s
payload_transport:
# Per-transport chunk size limit used when segmenting payloads.
max_chunk_bytes: 1048576
# Batch transport protocol revision.
# v2 uses P2P communicator, v1 uses gRPC developed at early ages. Use v2
batch_transport_protocol_version: 2
# Use communicator/materialize source export paths when available.
communicator_source_enabled: true
# Allow host-memory export for direct-write-capable CPU paths.
host_memory_export_enabled: true
# 0 means no payload-size cap
max_batch_payload_bytes: 0
# 0 means no per-batch item-count cap
max_batch_items: 0
source_publish_prereg:
# Pre-register publish-time exports so later batch_get can reuse them directly.
enabled: true
# Keep preregistered exports alive long enough for the delayed consumer phase.
ttl: 60s
# Cap live preregistered export entries to avoid unbounded retention.
max_live_entries: 4096
# Cap total bytes retained by live preregistered exports.
max_live_bytes: 17179869184
@@ -0,0 +1,476 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to SGLang project
"""Configuration and host-allocation boundary for TensorCast HiCache."""
from __future__ import annotations
import json
import sys
import threading
from collections.abc import Mapping
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import TYPE_CHECKING, Annotated, TypeAlias, cast
import torch
from pydantic import BaseModel, ConfigDict, Field, field_validator
from sglang.srt.mem_cache.pool_host.common import HostTensorAllocator
if TYPE_CHECKING:
from tensorcast.api.store import (
RegionBackedArtifactSession,
RegionBackedArtifactSessionOptions,
)
_SUPPORTED_LAYOUT_IO_PAIRS = frozenset(
{
("page_first", "kernel"),
("page_first_direct", "direct"),
}
)
class TensorcastTransferMode(str, Enum):
"""SGLang's supported TensorCast host-transfer modes.
allocator (default): TensorCast daemon allocates and owns a shared memory slab
with direct RDMA
scratch: uses SGlang-owned common HostTensorAllocator. TensorCast uses a scratch memory slab as staging buffer to copy KV pages into it. More compatible with performance overhead.
"""
ALLOCATOR = "allocator"
SCRATCH = "scratch"
class _FrozenConfig(BaseModel):
model_config = ConfigDict(
extra="forbid",
frozen=True,
validate_default=True,
)
class TensorcastScratchConfig(_FrozenConfig):
"""Fixed per-direction scratch-arena configuration."""
capacity_bytes: Annotated[int, Field(strict=True, gt=0)] = 16 * 1024 * 1024
class TensorcastConfig(_FrozenConfig):
"""Validated TensorCast-specific HiCache configuration."""
daemon_address: Annotated[str, Field(strict=True)]
namespace: Annotated[str, Field(strict=True)] = "default"
transfer_mode: TensorcastTransferMode = TensorcastTransferMode.ALLOCATOR
model_id: Annotated[str, Field(strict=True)] | None = None
model_version: Annotated[str, Field(strict=True)] = "unversioned"
session_name_prefix: Annotated[str, Field(strict=True)] = "sglang"
region_name_prefix: Annotated[str, Field(strict=True)] = "sglang_tensorcast"
exists_timeout_s: Annotated[
float,
Field(gt=0.0, allow_inf_nan=False),
] = 30.0
transfer_timeout_s: (
Annotated[
float,
Field(gt=0.0, allow_inf_nan=False),
]
| None
) = None
scratch: TensorcastScratchConfig = TensorcastScratchConfig()
@field_validator(
"namespace",
"model_version",
"session_name_prefix",
"region_name_prefix",
)
@classmethod
def validate_required_string(cls, value: str) -> str:
return _strip_non_empty(value)
@field_validator("model_id")
@classmethod
def validate_optional_model_id(cls, value: str | None) -> str | None:
if value is None:
return None
return _strip_non_empty(value)
TensorcastConfigSource: TypeAlias = TensorcastConfig | str | Mapping[str, object]
class TensorcastSessionRegistryError(RuntimeError):
"""Raised when process-local TensorCast Session admission is invalid."""
@dataclass(slots=True)
class _TensorcastSessionRegistryState:
session: RegionBackedArtifactSession | None = None
session_options: RegionBackedArtifactSessionOptions | None = None
early_attach_completed: bool = False
active_store_owner: object | None = None
terminal: bool = False
_PROCESS_SESSION_REGISTRY_LOCK = threading.Lock()
_PROCESS_SESSION_REGISTRY = _TensorcastSessionRegistryState()
def normalize_tensorcast_config(source: TensorcastConfigSource) -> TensorcastConfig:
"""Normalize raw or controller-parsed HiCache extra configuration."""
if isinstance(source, TensorcastConfig):
return source
raw = _load_extra_config(source) if isinstance(source, str) else source
if "tensorcast" not in raw:
raise ValueError(
"TensorCast HiCache configuration requires a 'tensorcast' object"
)
return TensorcastConfig.model_validate(raw["tensorcast"])
def format_tensorcast_rank_label(world_rank: int, world_size: int) -> str:
"""Return the stable diagnostic label for one SGLang rank."""
if world_size <= 0:
raise ValueError(f"world_size must be positive, got {world_size}")
if world_rank < 0 or world_rank >= world_size:
raise ValueError(
"world_rank must be in [0, world_size), got "
f"world_rank={world_rank}, world_size={world_size}"
)
return f"rank{world_rank}of{world_size}"
def build_tensorcast_session_options(
config: TensorcastConfig,
*,
world_rank: int | None = None,
world_size: int | None = None,
) -> RegionBackedArtifactSessionOptions:
"""Build public TensorCast Session options without exposing SDK internals."""
try:
from tensorcast.api.store import (
AllocatorTransferOptions,
RegionBackedArtifactSessionOptions,
ScratchTransferOptions,
)
except ImportError as exc:
raise ImportError(
"The TensorCast HiCache backend requires the optional 'tensorcast' "
"Python package. Install TensorCast before selecting "
"--hicache-storage-backend tensorcast."
) from exc
if (world_rank is None) != (world_size is None):
raise ValueError("world_rank and world_size must be provided together")
if world_rank is None:
from sglang.srt.runtime_context import get_parallel
parallel = get_parallel()
world_rank = parallel.world_rank
world_size = parallel.world_size
rank_label = format_tensorcast_rank_label(world_rank, cast(int, world_size))
if config.transfer_mode == TensorcastTransferMode.ALLOCATOR:
transfer = AllocatorTransferOptions()
else:
transfer = ScratchTransferOptions(
capacity_bytes=config.scratch.capacity_bytes,
)
return RegionBackedArtifactSessionOptions(
daemon_address=config.daemon_address,
session_name=f"{config.session_name_prefix}-{rank_label}",
transfer=transfer,
transfer_timeout_s=config.transfer_timeout_s,
exists_timeout_s=config.exists_timeout_s,
region_name_prefix=f"{config.region_name_prefix}-{rank_label}",
)
def _attach_process_session(
options: RegionBackedArtifactSessionOptions,
) -> RegionBackedArtifactSession:
from tensorcast.api.store import RegionBackedArtifactSession
return RegionBackedArtifactSession.attach(options)
def attach_early_process_session(
options: RegionBackedArtifactSessionOptions,
) -> RegionBackedArtifactSession:
"""Attach once before HostPool allocation and retain the process Session."""
with _PROCESS_SESSION_REGISTRY_LOCK:
state = _PROCESS_SESSION_REGISTRY
if state.terminal:
raise TensorcastSessionRegistryError(
"the TensorCast process Session is terminal; restart the rank process"
)
if state.early_attach_completed:
if options != state.session_options:
raise TensorcastSessionRegistryError(
"TensorCast early attach conflicts with the process Session options"
)
if state.session is None:
raise RuntimeError(
"TensorCast Session registry is inconsistent after early attach"
)
return state.session
try:
session = _attach_process_session(options)
except BaseException:
state.terminal = True
raise
state.session = session
state.session_options = options
state.early_attach_completed = True
return session
def claim_tensorcast_store_session(
options: RegionBackedArtifactSessionOptions,
*,
owner: object,
) -> RegionBackedArtifactSession:
"""Give one Store ownership of the already attached process Session."""
if owner is None:
raise ValueError("TensorCast Store owner must not be None")
with _PROCESS_SESSION_REGISTRY_LOCK:
state = _PROCESS_SESSION_REGISTRY
if state.terminal:
raise TensorcastSessionRegistryError(
"the TensorCast process Session is terminal; restart the rank process"
)
if not state.early_attach_completed or state.session is None:
raise TensorcastSessionRegistryError(
"TensorCast Store cannot attach for the first time; the HostPool "
"allocator must complete early Session attach"
)
if options != state.session_options:
raise TensorcastSessionRegistryError(
"TensorCast Store options conflict with the early process Session"
)
if state.active_store_owner is None:
state.active_store_owner = owner
elif state.active_store_owner is not owner:
raise TensorcastSessionRegistryError(
"another TensorCast Store already owns the process Session"
)
return state.session
def terminate_tensorcast_store_session(*, owner: object) -> bool:
"""Make Store admission terminal and terminate the Session exactly once."""
with _PROCESS_SESSION_REGISTRY_LOCK:
state = _PROCESS_SESSION_REGISTRY
if state.active_store_owner is not owner:
raise TensorcastSessionRegistryError(
"only the active TensorCast Store owner may terminate the process Session"
)
if state.terminal:
return False
if state.session is None:
raise RuntimeError(
"TensorCast Session registry has an owner without an attached Session"
)
state.terminal = True
session = state.session
session.terminate_process_session()
return True
class TensorcastHostTensorAllocator(HostTensorAllocator):
"""Delegate exact HostPool tensor allocations to one TensorCast Session."""
def __init__(self, session: RegionBackedArtifactSession) -> None:
super().__init__()
self._session = session
self._allocation_sequence = 0
@property
def session(self) -> RegionBackedArtifactSession:
return self._session
def allocate(
self,
dims: tuple[int, ...],
dtype: torch.dtype,
device: str,
) -> torch.Tensor:
if device != "cpu":
raise ValueError(
f"TensorCast host allocation requires CPU memory, got device={device!r}"
)
self.dims = dims
self.dtype = dtype
self._allocation_sequence += 1
return self._session.allocate_host_tensor(
dims,
dtype,
name=f"host-pool-{self._allocation_sequence}",
)
def create_tensorcast_host_allocator(
source: TensorcastConfigSource,
*,
host_memory_mode: str,
host_layout: str,
io_backend: str,
platform_name: str,
is_cuda_backend: bool,
world_rank: int | None = None,
world_size: int | None = None,
) -> HostTensorAllocator:
"""Validate, attach, and select the configured TensorCast host allocator."""
config = normalize_tensorcast_config(source)
validate_tensorcast_startup_configuration(
host_memory_mode=host_memory_mode,
host_layout=host_layout,
io_backend=io_backend,
platform_name=platform_name,
is_cuda_backend=is_cuda_backend,
)
options = build_tensorcast_session_options(
config,
world_rank=world_rank,
world_size=world_size,
)
session = attach_early_process_session(options)
if config.transfer_mode == TensorcastTransferMode.SCRATCH:
return HostTensorAllocator()
return TensorcastHostTensorAllocator(session)
def get_tensorcast_host_allocator_from_runtime() -> HostTensorAllocator:
"""Select the TensorCast allocator from the published SGLang config."""
from sglang.srt.platforms import current_platform
from sglang.srt.runtime_context import get_memory
memory = get_memory()
source = memory.hicache_storage_backend_extra_config
if source is None:
raise ValueError(
"--hicache-storage-backend tensorcast requires "
"--hicache-storage-backend-extra-config with tensorcast.daemon_address"
)
return create_tensorcast_host_allocator(
source,
host_memory_mode=memory.hicache_host_memory_mode,
host_layout=memory.hicache_mem_layout,
io_backend=memory.hicache_io_backend,
platform_name=sys.platform,
is_cuda_backend=current_platform.is_cuda(),
)
def resolve_tensorcast_model_id(
config: TensorcastConfig,
storage_model_name: str | None,
) -> str:
"""Resolve the artifact model identity at later Store registration."""
if config.model_id is not None:
return config.model_id
if storage_model_name is None:
raise ValueError(
"TensorCast artifact identity requires tensorcast.model_id or an "
"SGLang storage model name"
)
try:
return _strip_non_empty(storage_model_name)
except ValueError as exc:
raise ValueError(
"TensorCast artifact identity requires tensorcast.model_id or a "
"non-empty SGLang storage model name"
) from exc
def validate_tensorcast_startup_configuration(
*,
host_memory_mode: str,
host_layout: str,
io_backend: str,
platform_name: str,
is_cuda_backend: bool,
) -> None:
"""Reject initial-scope runtime combinations before Session attachment."""
if platform_name != "linux":
raise ValueError(
"TensorCast HiCache initially supports Linux only, got "
f"platform={platform_name!r}"
)
if not is_cuda_backend:
raise ValueError("TensorCast HiCache initially supports the CUDA backend only")
if host_memory_mode != "cache":
raise ValueError(
"TensorCast HiCache requires --hicache-host-memory-mode=cache, got "
f"{host_memory_mode!r}"
)
if (host_layout, io_backend) not in _SUPPORTED_LAYOUT_IO_PAIRS:
raise ValueError(
"TensorCast HiCache requires (page_first, kernel) or "
"(page_first_direct, direct), got "
f"layout={host_layout!r}, io_backend={io_backend!r}"
)
def _strip_non_empty(value: str) -> str:
stripped = value.strip()
if not stripped:
raise ValueError("value must not be empty")
return stripped
def _load_extra_config(source: str) -> Mapping[str, object]:
if not source.startswith("@"):
return _require_mapping(json.loads(source), source_name="inline JSON")
path_text = source[1:]
if not path_text:
raise ValueError("TensorCast HiCache config path must not be empty")
path = Path(path_text)
suffix = path.suffix.lower()
if suffix == ".json":
with path.open(encoding="utf-8") as config_file:
parsed = json.load(config_file)
elif suffix == ".toml":
import tomllib
with path.open("rb") as config_file:
parsed = tomllib.load(config_file)
elif suffix in {".yaml", ".yml"}:
import yaml
with path.open(encoding="utf-8") as config_file:
parsed = yaml.safe_load(config_file)
else:
raise ValueError(
f"Unsupported TensorCast HiCache config file extension {suffix!r}"
)
return _require_mapping(parsed, source_name=str(path))
def _require_mapping(value: object, *, source_name: str) -> Mapping[str, object]:
if not isinstance(value, Mapping):
raise ValueError(
f"TensorCast HiCache config from {source_name} must be an object"
)
return cast(Mapping[str, object], value)
@@ -0,0 +1,860 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to SGLang project
"""TensorCast-backed synchronous L3 storage adapter."""
from __future__ import annotations
import logging
import threading
from dataclasses import dataclass
from enum import Enum
from hashlib import sha256
from typing import Any, TypeVar
import torch
from tensorcast.api.store import (
ByteArtifactKeyspace,
ByteArtifactSpec,
HostMemorySpan,
RegionArtifactInputError,
RegionArtifactTransfer,
RegionBackedArtifactSession,
RegionSessionFailedError,
RegionSessionTerminatedError,
)
from sglang.srt.mem_cache.hicache_storage import (
STORAGE_BATCH_SIZE,
HiCacheStorage,
HiCacheStorageConfig,
HiCacheStorageExtraInfo,
PoolTransfer,
PoolTransferResult,
)
from sglang.srt.mem_cache.pool_host import HostKVCache
from sglang.srt.mem_cache.pool_host.mha import (
AsymmetricMHATokenToKVPoolHost,
MHATokenToKVPoolHost,
)
from sglang.srt.mem_cache.pool_host.mla import MLATokenToKVPoolHost
from sglang.srt.mem_cache.storage.tensorcast_store.host_allocator import (
TensorcastConfig,
TensorcastHostTensorAllocator,
TensorcastTransferMode,
build_tensorcast_session_options,
claim_tensorcast_store_session,
normalize_tensorcast_config,
resolve_tensorcast_model_id,
terminate_tensorcast_store_session,
)
logger = logging.getLogger(__name__)
# Schema used for TensorCast byte-artifact key namespace
ARTIFACT_LAYOUT_SCHEMA_VERSION = "full-fragment-v1"
_ARTIFACT_LAYOUT_SCHEMA_TOKEN = "ff1"
_SUPPORTED_FULL_LAYOUTS = frozenset({"page_first", "page_first_direct"})
_FULL_LAYOUT_TOKENS = {
"page_first": "pf",
"page_first_direct": "pfd",
}
_DTYPE_LAYOUT_TOKENS = {
"bfloat16": "bf16",
"float16": "f16",
"float32": "f32",
"float64": "f64",
"float8_e4m3fn": "f8e4m3fn",
"float8_e4m3fnuz": "f8e4m3fnuz",
"float8_e5m2": "f8e5m2",
"uint8": "u8",
}
_ENGINE_KEY_DOMAIN = b"sglang-hicache-engine-key-v1\0"
_ResultT = TypeVar("_ResultT")
class FragmentComponent(str, Enum):
"""Logical contiguous components in one FULL page."""
K = "k"
V = "v"
KV = "kv"
# TODO: support SWA/Mamba in V2
class _PoolFamily(str, Enum):
MHA = "mha"
MLA = "mla"
# TODO: support SWA/Mamba in V2
@dataclass(frozen=True, slots=True)
class FragmentSchema:
"""Immutable byte contract for one component of every logical page."""
component: FragmentComponent
byte_length: int
@dataclass(frozen=True, slots=True)
class PageFragment:
"""One caller-owned identity and optional host range in a page plan."""
logical_page_index: int
component: FragmentComponent
engine_key: bytes
host_address: int | None
byte_length: int
owner: torch.Tensor | None
@dataclass(frozen=True, slots=True)
class _RegisteredPool:
pool: HostKVCache
family: _PoolFamily
fragment_schema: tuple[FragmentSchema, ...]
roots: tuple[torch.Tensor, ...]
keyspace: ByteArtifactKeyspace
rank_suffix: str
layout_id: str
class TensorcastStore(HiCacheStorage):
"""Adapt SGLang KV pages to TensorCast byte-artifact Session calls."""
def __init__(self, storage_config: HiCacheStorageConfig) -> None:
source = storage_config.extra_config
if source is None:
raise ValueError(
"TensorCast HiCache requires storage backend extra configuration"
)
tensorcast_config = normalize_tensorcast_config(source)
session_options = build_tensorcast_session_options(tensorcast_config)
self._storage_config = storage_config
self._tensorcast_config = tensorcast_config
self._session_options = session_options
self._registered: _RegisteredPool | None = None
self._availability_lock = threading.Lock()
self._disabled = False
self._failure_logged = False
# This must remain the final fallible/stateful constructor step so a local
# validation failure cannot strand ownership in the process registry.
self._session = claim_tensorcast_store_session(session_options, owner=self)
@property
def session(self) -> RegionBackedArtifactSession:
return self._session
def register_mem_pool_host(self, mem_pool_host: HostKVCache) -> None:
registered = self._registered
if registered is not None:
if registered.pool is mem_pool_host:
return
raise ValueError(
"TensorCast Store is already registered with a different HostPool"
)
candidate = _build_registered_pool(
mem_pool_host,
storage_config=self._storage_config,
tensorcast_config=self._tensorcast_config,
)
_validate_transfer_mode_registration(
candidate,
config=self._tensorcast_config,
session=self._session,
)
super().register_mem_pool_host(mem_pool_host)
self._registered = candidate
def exists(self, key: str) -> bool:
return self.batch_exists([key]) == 1
def batch_exists(
self,
keys: list[str],
extra_info: HiCacheStorageExtraInfo | None = None,
) -> int:
if not keys:
return 0
if not self._adapter_is_available():
return 0
try:
specs = self._build_artifact_specs(keys)
result = self._session.batch_exists(specs)
registered = self._require_registered()
hit_pages = _leading_complete_page_count(
result.existence_mask,
page_count=len(keys),
fragments_per_page=len(registered.fragment_schema),
)
except Exception as exc:
self._disable_after_runtime_exception("batch_exists", exc)
return 0
return self._publish_if_available(hit_pages, fallback=0)
def _build_page_fragments(self, keys: list[str]) -> tuple[PageFragment, ...]:
registered = self._require_registered()
return _expand_page_fragments(registered, keys)
def _build_artifact_specs(self, keys: list[str]) -> tuple[ByteArtifactSpec, ...]:
registered = self._require_registered()
return _expand_artifact_specs(registered, keys)
def _require_registered(self) -> _RegisteredPool:
if self._registered is None:
raise RuntimeError("TensorCast Store has no registered HostPool")
return self._registered
def get(
self,
key: str,
target_location: Any | None = None,
target_sizes: Any | None = None,
) -> torch.Tensor | None:
raise NotImplementedError("TensorCast does not support value-oriented get()")
def batch_get(
self,
keys: list[str],
target_locations: Any | None = None,
target_sizes: Any | None = None,
) -> list[torch.Tensor | None] | int:
raise NotImplementedError(
"TensorCast does not support value-oriented batch_get()"
)
def set(
self,
key: str,
value: Any | None = None,
target_location: Any | None = None,
target_sizes: Any | None = None,
) -> bool:
raise NotImplementedError("TensorCast does not support value-oriented set()")
def batch_set(
self,
keys: list[str],
values: Any | None = None,
target_locations: Any | None = None,
target_sizes: Any | None = None,
) -> bool:
raise NotImplementedError(
"TensorCast does not support value-oriented batch_set()"
)
def batch_get_v1(
self,
keys: list[str],
host_indices: torch.Tensor,
extra_info: HiCacheStorageExtraInfo | None = None,
) -> list[bool]:
unavailable = [False] * len(keys)
if not keys:
return []
if not self._adapter_is_available():
return unavailable
try:
transfers = self._build_artifact_transfers(keys, host_indices)
result = self._session.batch_get_into(transfers)
registered = self._require_registered()
page_mask = _fold_fragment_mask(
result.success_mask,
page_count=len(keys),
fragments_per_page=len(registered.fragment_schema),
)
result_mask = list(_normalize_leading_page_mask(page_mask))
except Exception as exc:
self._disable_after_runtime_exception("batch_get_v1", exc)
return unavailable
return self._publish_if_available(result_mask, fallback=unavailable)
def batch_set_v1(
self,
keys: list[str],
host_indices: torch.Tensor,
extra_info: HiCacheStorageExtraInfo | None = None,
) -> list[bool]:
unavailable = [False] * len(keys)
if not keys:
return []
if not self._adapter_is_available():
return unavailable
try:
transfers = self._build_artifact_transfers(keys, host_indices)
result = self._session.batch_put_from(transfers)
registered = self._require_registered()
result_mask = list(
_fold_fragment_mask(
result.success_mask,
page_count=len(keys),
fragments_per_page=len(registered.fragment_schema),
)
)
except Exception as exc:
self._disable_after_runtime_exception("batch_set_v1", exc)
return unavailable
return self._publish_if_available(result_mask, fallback=unavailable)
def _adapter_is_available(self) -> bool:
with self._availability_lock:
return not self._disabled
def _publish_if_available(
self,
result: _ResultT,
*,
fallback: _ResultT,
) -> _ResultT:
with self._availability_lock:
if self._disabled:
return fallback
return result
def _disable_after_runtime_exception(
self,
operation: str,
error: Exception,
) -> None:
with self._availability_lock:
if self._disabled:
return
self._disabled = True
should_log = not self._failure_logged
self._failure_logged = True
if not should_log:
return
if isinstance(error, RegionSessionFailedError):
failure = error.failure
logger.error(
"TensorCast L3 unavailable: category=session_failed "
"exception_type=RegionSessionFailedError adapter_operation=%s "
"failure_code=%s session_operation=%s operation_id=%s message=%s",
operation,
failure.code.value,
failure.operation_kind.value,
failure.operation_id,
failure.message,
)
return
if isinstance(error, RegionArtifactInputError):
logger.error(
"TensorCast L3 unavailable: category=adapter_input_failure "
"exception_type=RegionArtifactInputError adapter_operation=%s "
"message=%s",
operation,
error,
)
return
if isinstance(error, RegionSessionTerminatedError):
logger.error(
"TensorCast L3 unavailable: category=unexpected_terminated "
"exception_type=RegionSessionTerminatedError adapter_operation=%s "
"message=%s",
operation,
error,
)
return
logger.exception(
"TensorCast L3 unavailable: category=adapter_exception "
"exception_type=%s adapter_operation=%s message=%s",
type(error).__name__,
operation,
error,
)
def close(self) -> None:
with self._availability_lock:
self._disabled = True
terminate_tensorcast_store_session(owner=self)
def _build_artifact_transfers(
self,
keys: list[str],
host_indices: torch.Tensor,
) -> tuple[RegionArtifactTransfer, ...]:
registered = self._require_registered()
return _expand_artifact_transfers(registered, keys, host_indices)
def batch_exists_v2(
self,
keys: list[str],
pool_transfers: list[PoolTransfer] | None = None,
extra_info: HiCacheStorageExtraInfo | None = None,
) -> PoolTransferResult:
raise NotImplementedError("TensorCast initially supports FULL v1 only")
def batch_get_v2(
self,
transfers: list[PoolTransfer],
extra_info: HiCacheStorageExtraInfo | None = None,
) -> dict[str, list[bool]]:
raise NotImplementedError("TensorCast initially supports FULL v1 only")
def batch_set_v2(
self,
transfers: list[PoolTransfer],
extra_info: HiCacheStorageExtraInfo | None = None,
) -> dict[str, list[bool]]:
raise NotImplementedError("TensorCast initially supports FULL v1 only")
def clear(self) -> None:
raise NotImplementedError("TensorCast has no namespace-wide clear operation")
def _build_registered_pool(
mem_pool_host: HostKVCache,
*,
storage_config: HiCacheStorageConfig,
tensorcast_config: TensorcastConfig,
) -> _RegisteredPool:
family, components, roots = _select_pool_adapter(mem_pool_host)
_validate_storage_config(
mem_pool_host,
family=family,
storage_config=storage_config,
)
fragment_schema = _sample_fragment_schema(mem_pool_host, components=components)
_validate_roots(roots, pool_type=type(mem_pool_host).__name__)
effective_model_id = resolve_tensorcast_model_id(
tensorcast_config,
storage_config.model_name,
)
rank_suffix = _build_rank_suffix(storage_config, family=family)
layout_id = _build_layout_id(mem_pool_host, family=family)
keyspace = ByteArtifactKeyspace(
namespace=tensorcast_config.namespace,
engine="sglang",
model_id=effective_model_id,
model_version=tensorcast_config.model_version,
layout_id=layout_id,
)
return _RegisteredPool(
pool=mem_pool_host,
family=family,
fragment_schema=fragment_schema,
roots=roots,
keyspace=keyspace,
rank_suffix=rank_suffix,
layout_id=layout_id,
)
def _select_pool_adapter(
mem_pool_host: HostKVCache,
) -> tuple[_PoolFamily, tuple[FragmentComponent, ...], tuple[torch.Tensor, ...]]:
pool_type = type(mem_pool_host)
if pool_type is AsymmetricMHATokenToKVPoolHost:
asymmetric_pool = mem_pool_host
return (
_PoolFamily.MHA,
(FragmentComponent.K, FragmentComponent.V),
(asymmetric_pool.k_buffer, asymmetric_pool.v_buffer),
)
if pool_type is MHATokenToKVPoolHost:
mha_pool = mem_pool_host
return (
_PoolFamily.MHA,
(FragmentComponent.K, FragmentComponent.V),
(mha_pool.kv_buffer, mha_pool.kv_buffer),
)
if pool_type is MLATokenToKVPoolHost:
mla_pool = mem_pool_host
return (
_PoolFamily.MLA,
(FragmentComponent.KV,),
(mla_pool.kv_buffer,),
)
raise NotImplementedError(
"TensorCast FULL v1 does not support HostPool type "
f"{pool_type.__module__}.{pool_type.__qualname__}"
)
def _validate_storage_config(
mem_pool_host: HostKVCache,
*,
family: _PoolFamily,
storage_config: HiCacheStorageConfig,
) -> None:
"""Check that the storage configuration matches the implemented data path.
The current checks cover only FULL v1. Extend this validation as support for
additional pool families and storage API versions is implemented.
"""
if mem_pool_host.layout not in _SUPPORTED_FULL_LAYOUTS:
raise NotImplementedError(
"TensorCast FULL v1 supports only page_first and page_first_direct, "
f"got layout={mem_pool_host.layout!r}"
)
if mem_pool_host.mtp_draft_device_pools:
raise NotImplementedError(
"TensorCast FULL v1 does not support packed MTP draft pools"
)
if mem_pool_host.dcp_size != 1 or mem_pool_host.dcp_rank != 0:
raise NotImplementedError(
"TensorCast FULL v1 does not support DCP-aware HostPool layouts"
)
if storage_config.attn_cp_size != 1 or storage_config.attn_cp_rank != 0:
raise NotImplementedError(
"TensorCast FULL v1 requires attention context parallel size 1"
)
if storage_config.tp_lcm_size is not None:
raise NotImplementedError("TensorCast FULL v1 does not support TP-LCM")
if storage_config.should_split_heads:
raise NotImplementedError("TensorCast FULL v1 does not support split heads")
expected_mla = family is _PoolFamily.MLA
if storage_config.is_mla_model is not expected_mla:
raise ValueError(
"TensorCast HostPool family conflicts with is_mla_model: "
f"pool_family={family.value}, is_mla_model={storage_config.is_mla_model}"
)
if mem_pool_host.page_size <= 0 or mem_pool_host.page_num <= 0:
raise ValueError(
"TensorCast FULL HostPool requires positive page_size and page_num"
)
def _sample_fragment_schema(
mem_pool_host: HostKVCache,
*,
components: tuple[FragmentComponent, ...],
) -> tuple[FragmentSchema, ...]:
sample_indices = torch.arange(mem_pool_host.page_size, dtype=torch.int64)
pointers, byte_lengths = mem_pool_host.get_page_buffer_meta(sample_indices)
expected_count = len(components)
if len(pointers) != expected_count or len(byte_lengths) != expected_count:
raise ValueError(
"TensorCast FULL registration metadata must contain exactly one page: "
f"expected_components={expected_count}, pointers={len(pointers)}, "
f"byte_lengths={len(byte_lengths)}"
)
if any(type(pointer) is not int or pointer <= 0 for pointer in pointers):
raise ValueError(
"TensorCast FULL registration metadata contains a non-positive pointer"
)
if any(
type(byte_length) is not int or byte_length <= 0 for byte_length in byte_lengths
):
raise ValueError(
"TensorCast FULL registration metadata contains an invalid byte length"
)
if type(mem_pool_host) is MHATokenToKVPoolHost and (
byte_lengths[0] != byte_lengths[1]
):
raise ValueError(
"TensorCast standard MHA registration requires equal K/V byte lengths"
)
return tuple(
FragmentSchema(component=component, byte_length=byte_length)
for component, byte_length in zip(components, byte_lengths, strict=True)
)
def _validate_roots(roots: tuple[torch.Tensor, ...], *, pool_type: str) -> None:
for root in roots:
if type(root) is not torch.Tensor:
raise TypeError(
f"TensorCast {pool_type} backing roots must be torch.Tensor objects"
)
if root.device.type != "cpu" or not root.is_contiguous():
raise ValueError(
f"TensorCast {pool_type} backing roots must be contiguous CPU tensors"
)
def _validate_transfer_mode_registration(
registered: _RegisteredPool,
*,
config: TensorcastConfig,
session: RegionBackedArtifactSession,
) -> None:
if config.transfer_mode is TensorcastTransferMode.ALLOCATOR:
allocator = registered.pool.allocator
if type(allocator) is not TensorcastHostTensorAllocator:
raise ValueError(
"TensorCast allocator mode requires TensorcastHostTensorAllocator "
"backing for the registered HostPool"
)
if allocator.session is not session:
raise ValueError(
"TensorCast HostPool allocator and Store must use the same Session"
)
return
maximum_logical_batch_pages = min(STORAGE_BATCH_SIZE, registered.pool.page_num)
page_bytes = sum(fragment.byte_length for fragment in registered.fragment_schema)
required_capacity_bytes = maximum_logical_batch_pages * page_bytes
configured_capacity_bytes = config.scratch.capacity_bytes
if configured_capacity_bytes < required_capacity_bytes:
component_lengths = tuple(
fragment.byte_length for fragment in registered.fragment_schema
)
raise ValueError(
"TensorCast scratch capacity is insufficient: "
f"configured_capacity_bytes={configured_capacity_bytes}, "
f"required_capacity_bytes={required_capacity_bytes}, "
f"maximum_logical_batch_pages={maximum_logical_batch_pages}, "
f"page_bytes={page_bytes}, "
f"pool_type={type(registered.pool).__name__}, "
f"layout={registered.pool.layout}, "
f"component_byte_lengths={component_lengths}"
)
def _build_layout_id(mem_pool_host: HostKVCache, *, family: _PoolFamily) -> str:
dtype_name = str(mem_pool_host.dtype)
if dtype_name.startswith("torch."):
dtype_name = dtype_name.removeprefix("torch.")
dtype_token = _DTYPE_LAYOUT_TOKENS.get(dtype_name, dtype_name)
layout_token = _FULL_LAYOUT_TOKENS[mem_pool_host.layout]
return (
f"{_ARTIFACT_LAYOUT_SCHEMA_TOKEN}_{layout_token}_{dtype_token}_"
f"p{mem_pool_host.page_size}_{family.value}"
)
def _build_rank_suffix(
storage_config: HiCacheStorageConfig,
*,
family: _PoolFamily,
) -> str:
if family is _PoolFamily.MLA:
return f"pp{storage_config.pp_rank}of{storage_config.pp_size}"
tp_suffix = f"tp{storage_config.tp_rank}of{storage_config.tp_size}"
if storage_config.pp_size == 1:
return tp_suffix
return f"{tp_suffix}_pp{storage_config.pp_rank}of{storage_config.pp_size}"
def _build_engine_key(
rank_suffix: str,
logical_key: str,
component: FragmentComponent,
) -> bytes:
if type(logical_key) is not str:
raise TypeError(
"TensorCast logical page keys must be exact str values, got "
f"{type(logical_key).__name__}"
)
component_suffix = (
b"k" if component is FragmentComponent.KV else component.value.encode()
)
rank_bytes = rank_suffix.encode("ascii")
logical_key_bytes = logical_key.encode("utf-8")
payload = b"".join(
(
_ENGINE_KEY_DOMAIN,
len(rank_bytes).to_bytes(4, byteorder="big"),
rank_bytes,
len(logical_key_bytes).to_bytes(8, byteorder="big"),
logical_key_bytes,
component_suffix,
)
)
return sha256(payload).digest()
def _expand_page_fragments(
registered: _RegisteredPool,
keys: list[str],
) -> tuple[PageFragment, ...]:
fragments = tuple(
PageFragment(
logical_page_index=page_index,
component=schema.component,
engine_key=_build_engine_key(
registered.rank_suffix,
logical_key,
schema.component,
),
host_address=None,
byte_length=schema.byte_length,
owner=None,
)
for page_index, logical_key in enumerate(keys)
for schema in registered.fragment_schema
)
engine_keys = tuple(fragment.engine_key for fragment in fragments)
if len(set(engine_keys)) != len(engine_keys):
raise ValueError(
"TensorCast logical keys must produce unique fragment identities "
"within one batch"
)
return fragments
def _expand_artifact_specs(
registered: _RegisteredPool,
keys: list[str],
) -> tuple[ByteArtifactSpec, ...]:
return tuple(
ByteArtifactSpec(
keyspace=registered.keyspace,
engine_key=fragment.engine_key,
byte_length=fragment.byte_length,
)
for fragment in _expand_page_fragments(registered, keys)
)
def _expand_transfer_fragments(
registered: _RegisteredPool,
keys: list[str],
host_indices: torch.Tensor,
) -> tuple[PageFragment, ...]:
if type(host_indices) is not torch.Tensor:
raise TypeError(
"TensorCast FULL host_indices must be a torch.Tensor, got "
f"{type(host_indices).__name__}"
)
if host_indices.ndim != 1:
raise ValueError(
"TensorCast FULL host_indices must be one-dimensional, got "
f"shape={tuple(host_indices.shape)}"
)
expected_index_count = len(keys) * registered.pool.page_size
if len(host_indices) != expected_index_count:
raise ValueError(
"TensorCast FULL logical-key/host-index count mismatch: "
f"logical_pages={len(keys)}, page_size={registered.pool.page_size}, "
f"expected_host_indices={expected_index_count}, "
f"actual_host_indices={len(host_indices)}"
)
if not keys:
return ()
pointers, byte_lengths = registered.pool.get_page_buffer_meta(host_indices)
fragments = _expand_page_fragments(registered, keys)
expected_fragment_count = len(fragments)
if (
len(pointers) != expected_fragment_count
or len(byte_lengths) != expected_fragment_count
):
raise ValueError(
"TensorCast FULL runtime metadata count mismatch: "
f"expected_fragments={expected_fragment_count}, "
f"pointers={len(pointers)}, byte_lengths={len(byte_lengths)}"
)
fragments_per_page = len(registered.fragment_schema)
transfer_fragments: list[PageFragment] = []
for fragment_index, (fragment, pointer, byte_length) in enumerate(
zip(fragments, pointers, byte_lengths, strict=True)
):
schema_index = fragment_index % fragments_per_page
schema = registered.fragment_schema[schema_index]
if type(pointer) is not int or pointer <= 0:
raise ValueError(
"TensorCast FULL runtime metadata contains a non-positive pointer: "
f"fragment_index={fragment_index}, pointer={pointer!r}"
)
if type(byte_length) is not int or byte_length <= 0:
raise ValueError(
"TensorCast FULL runtime metadata contains an invalid byte length: "
f"fragment_index={fragment_index}, byte_length={byte_length!r}"
)
if byte_length != schema.byte_length:
raise ValueError(
"TensorCast FULL runtime byte length differs from the registered "
f"schema: fragment_index={fragment_index}, "
f"component={schema.component.value}, expected={schema.byte_length}, "
f"actual={byte_length}"
)
transfer_fragments.append(
PageFragment(
logical_page_index=fragment.logical_page_index,
component=fragment.component,
engine_key=fragment.engine_key,
host_address=pointer,
byte_length=byte_length,
owner=registered.roots[schema_index],
)
)
return tuple(transfer_fragments)
def _expand_artifact_transfers(
registered: _RegisteredPool,
keys: list[str],
host_indices: torch.Tensor,
) -> tuple[RegionArtifactTransfer, ...]:
transfers: list[RegionArtifactTransfer] = []
for fragment in _expand_transfer_fragments(registered, keys, host_indices):
if fragment.host_address is None or fragment.owner is None:
raise RuntimeError("TensorCast transfer fragment has no owned host range")
offset_bytes = fragment.host_address - int(fragment.owner.data_ptr())
span = HostMemorySpan.from_tensor(
fragment.owner,
offset_bytes=offset_bytes,
byte_length=fragment.byte_length,
)
transfers.append(
RegionArtifactTransfer(
artifact=ByteArtifactSpec(
keyspace=registered.keyspace,
engine_key=fragment.engine_key,
byte_length=fragment.byte_length,
),
span=span,
)
)
return tuple(transfers)
def _fold_fragment_mask(
fragment_mask: tuple[bool, ...],
*,
page_count: int,
fragments_per_page: int,
) -> tuple[bool, ...]:
if fragments_per_page <= 0:
raise ValueError("TensorCast fragments_per_page must be positive")
expected_count = page_count * fragments_per_page
if len(fragment_mask) != expected_count:
raise ValueError(
"TensorCast transfer result length does not match the logical page plan: "
f"expected={expected_count}, actual={len(fragment_mask)}"
)
return tuple(
all(
fragment_mask[
page_index * fragments_per_page : (page_index + 1) * fragments_per_page
]
)
for page_index in range(page_count)
)
def _normalize_leading_page_mask(page_mask: tuple[bool, ...]) -> tuple[bool, ...]:
prefix_complete = True
normalized: list[bool] = []
for page_complete in page_mask:
prefix_complete = prefix_complete and page_complete
normalized.append(prefix_complete)
return tuple(normalized)
def _leading_complete_page_count(
fragment_mask: tuple[bool, ...],
*,
page_count: int,
fragments_per_page: int,
) -> int:
page_mask = _fold_fragment_mask(
fragment_mask,
page_count=page_count,
fragments_per_page=fragments_per_page,
)
prefix = 0
for page_complete in page_mask:
if not page_complete:
break
prefix += 1
return prefix
@@ -0,0 +1,301 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to SGLang project
from __future__ import annotations
import subprocess
import sys
import textwrap
def _run_fresh_python(source: str) -> subprocess.CompletedProcess[str]:
"""Run a scenario with pristine import and process-lifecycle state.
TensorCast SDK modules may already be cached by pytest collection, and the
adapter's process-scoped Session registry is intentionally not resettable.
A newly executed interpreter preserves those production invariants while
keeping import-boundary and lifecycle tests independent of test order.
"""
return subprocess.run(
[sys.executable, "-c", textwrap.dedent(source)],
check=False,
capture_output=True,
text=True,
)
# Attach, claim, fail, or terminate the process-scoped Session registry.
# Each complete lifecycle should belong to a fresh interpreter.
def test_allocator_delegates_exact_tensors_in_subprocess() -> None:
result = _run_fresh_python("""
import torch
from sglang.srt.mem_cache.storage.tensorcast_store import host_allocator
class FakeSession:
def __init__(self):
self.calls = []
self.results = [
torch.empty((2, 3), dtype=torch.float32),
torch.empty((5,), dtype=torch.uint8),
torch.empty((1,), dtype=torch.int64),
]
def allocate_host_tensor(self, shape, dtype, *, name):
self.calls.append((shape, dtype, name))
return self.results[len(self.calls) - 1]
def terminate_process_session(self):
raise AssertionError("allocator terminated the Session")
session = FakeSession()
attach_calls = []
def fake_attach(options):
attach_calls.append(options)
return session
host_allocator._attach_process_session = fake_attach
source = {"tensorcast": {"daemon_address": "127.0.0.1:8073"}}
first_allocator = host_allocator.create_tensorcast_host_allocator(
source,
host_memory_mode="cache",
host_layout="page_first",
io_backend="kernel",
platform_name="linux",
is_cuda_backend=True,
world_rank=0,
world_size=1,
)
second_allocator = host_allocator.create_tensorcast_host_allocator(
source,
host_memory_mode="cache",
host_layout="page_first",
io_backend="kernel",
platform_name="linux",
is_cuda_backend=True,
world_rank=0,
world_size=1,
)
assert isinstance(
first_allocator, host_allocator.TensorcastHostTensorAllocator
)
assert isinstance(
second_allocator, host_allocator.TensorcastHostTensorAllocator
)
assert first_allocator.session is session
assert second_allocator.session is session
assert len(attach_calls) == 1
first = first_allocator.allocate((2, 3), torch.float32, "cpu")
second = first_allocator.allocate((5,), torch.uint8, "cpu")
third = second_allocator.allocate((1,), torch.int64, "cpu")
assert first is session.results[0]
assert second is session.results[1]
assert third is session.results[2]
assert session.calls == [
((2, 3), torch.float32, "host-pool-1"),
((5,), torch.uint8, "host-pool-2"),
((1,), torch.int64, "host-pool-1"),
]
assert first_allocator.dims == (5,)
assert first_allocator.dtype is torch.uint8
try:
first_allocator.allocate((1,), torch.float32, "cuda")
except ValueError as exc:
assert "requires CPU memory" in str(exc)
else:
raise AssertionError("non-CPU allocation was accepted")
assert len(session.calls) == 3
""")
assert result.returncode == 0, result.stderr
def test_registry_equal_conflicting_and_store_claims_in_subprocess() -> None:
result = _run_fresh_python("""
from sglang.srt.mem_cache.storage.tensorcast_store import host_allocator
class FakeSession:
def __init__(self):
self.terminate_calls = 0
def terminate_process_session(self):
self.terminate_calls += 1
session = FakeSession()
attach_calls = []
def fake_attach(options):
attach_calls.append(options)
return session
host_allocator._attach_process_session = fake_attach
first_config = host_allocator.TensorcastConfig(
daemon_address="127.0.0.1:8073"
)
first_options = host_allocator.build_tensorcast_session_options(
first_config, world_rank=0, world_size=1
)
conflicting_options = host_allocator.build_tensorcast_session_options(
host_allocator.TensorcastConfig(
daemon_address="127.0.0.1:8073", exists_timeout_s=31.0
),
world_rank=0,
world_size=1,
)
assert host_allocator.attach_early_process_session(first_options) is session
assert host_allocator.attach_early_process_session(first_options) is session
assert len(attach_calls) == 1
try:
host_allocator.attach_early_process_session(conflicting_options)
except host_allocator.TensorcastSessionRegistryError as exc:
assert "conflicts" in str(exc)
else:
raise AssertionError("conflicting early attach was accepted")
owner = object()
other_owner = object()
assert (
host_allocator.claim_tensorcast_store_session(
first_options, owner=owner
)
is session
)
assert (
host_allocator.claim_tensorcast_store_session(
first_options, owner=owner
)
is session
)
try:
host_allocator.claim_tensorcast_store_session(
first_options, owner=other_owner
)
except host_allocator.TensorcastSessionRegistryError as exc:
assert "already owns" in str(exc)
else:
raise AssertionError("a second Store owner was accepted")
assert host_allocator.terminate_tensorcast_store_session(owner=owner) is True
assert host_allocator.terminate_tensorcast_store_session(owner=owner) is False
assert session.terminate_calls == 1
for operation in (
lambda: host_allocator.attach_early_process_session(first_options),
lambda: host_allocator.claim_tensorcast_store_session(
first_options, owner=owner
),
):
try:
operation()
except host_allocator.TensorcastSessionRegistryError as exc:
assert "terminal" in str(exc)
else:
raise AssertionError("terminal registry admitted an operation")
""")
assert result.returncode == 0, result.stderr
def test_store_close_lifecycle_is_terminal_and_retains_allocator_roots_in_subprocess() -> (
None
):
result = _run_fresh_python("""
from types import SimpleNamespace
import torch
import sglang.srt.runtime_context as runtime_context
from sglang.srt.mem_cache.hicache_storage import HiCacheStorageConfig
from sglang.srt.mem_cache.storage.tensorcast_store import host_allocator
from sglang.srt.mem_cache.storage.tensorcast_store.tensorcast_store import (
TensorcastStore,
)
runtime_context.get_parallel = lambda: SimpleNamespace(
world_rank=0,
world_size=1,
)
class FakeSession:
def __init__(self):
self.terminate_calls = 0
def allocate_host_tensor(self, shape, dtype, *, name):
del name
return torch.empty(shape, dtype=dtype)
def terminate_process_session(self):
self.terminate_calls += 1
session = FakeSession()
host_allocator._attach_process_session = lambda options: session
source = {
"tensorcast": {
"daemon_address": "127.0.0.1:8073",
"model_id": "close-lifecycle-model",
}
}
allocator = host_allocator.create_tensorcast_host_allocator(
source,
host_memory_mode="cache",
host_layout="page_first_direct",
io_backend="direct",
platform_name="linux",
is_cuda_backend=True,
world_rank=0,
world_size=1,
)
root = allocator.allocate((8,), torch.float32, "cpu")
root.fill_(7)
config = HiCacheStorageConfig(
tp_rank=0,
tp_size=1,
pp_rank=0,
pp_size=1,
attn_cp_rank=0,
attn_cp_size=1,
is_mla_model=False,
enable_storage_metrics=False,
is_page_first_layout=True,
model_name="unused",
extra_config=source,
)
store = TensorcastStore(config)
store.close()
store.close()
assert session.terminate_calls == 1
assert store.batch_exists(["after-close"]) == 0
indices = torch.tensor([0, 1], dtype=torch.int64)
assert store.batch_get_v1(["after-close"], indices) == [False]
assert store.batch_set_v1(["after-close"], indices) == [False]
assert torch.equal(root, torch.full((8,), 7.0))
root.add_(1)
assert torch.equal(root, torch.full((8,), 8.0))
try:
TensorcastStore(config)
except host_allocator.TensorcastSessionRegistryError as exc:
assert "terminal" in str(exc)
else:
raise AssertionError("terminal registry admitted a second Store")
try:
host_allocator.create_tensorcast_host_allocator(
source,
host_memory_mode="cache",
host_layout="page_first_direct",
io_backend="direct",
platform_name="linux",
is_cuda_backend=True,
world_rank=0,
world_size=1,
)
except host_allocator.TensorcastSessionRegistryError as exc:
assert "terminal" in str(exc)
else:
raise AssertionError("terminal registry admitted allocator reclaim")
""")
assert result.returncode == 0, result.stderr
File diff suppressed because it is too large Load Diff