Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
97783fc
Implement prefix reuse functionality in HostPagedKVWorkerView and rel…
lausannel Feb 27, 2026
b4a2ae3
feat: enhance prefix cache with decode token support and capacity def…
lausannel Mar 2, 2026
23debbc
feat: enhance prefix cache logging for reused and skipped tokens
lausannel Mar 3, 2026
9490730
feat: add decode token handling in BatchGenWorker for improved sequen…
lausannel Mar 3, 2026
01bbfec
Merge remote-tracking branch 'origin' into feature/prefix-cache
lausannel Mar 4, 2026
32f8dcb
feat: update sequence handling in HostPagedKVWorkerView for improved …
lausannel Mar 5, 2026
ed65a7c
Merge origin/main into feature/prefix-cache
lausannel Mar 6, 2026
9955bd8
fix: restore namespace scope in host paged kv backend
lausannel Mar 6, 2026
8311d06
feat: allow prefix reuse via env
lausannel Mar 6, 2026
db58e50
feat: add prefix cache server flag
lausannel Mar 6, 2026
680762a
fix: use venv pip in install script
lausannel Mar 6, 2026
a50ac5c
revert: restore install script
lausannel Mar 6, 2026
42c997e
fix: lazily import server worker entrypoint
lausannel Mar 6, 2026
05d33f4
Merge remote-tracking branch 'origin/main' into feature/prefix-cache
lausannel Mar 10, 2026
fde3312
fix: initialize empty gpu page table on idle ranks
lausannel Mar 10, 2026
927d892
fix: keep prefix cache runtime config in sync
lausannel Mar 10, 2026
2d58cd8
fix: persist worker host kv config for runtime sync
lausannel Mar 10, 2026
1a51939
test: expand prefix cache coverage
lausannel Mar 10, 2026
7adf834
test: add multiprocess prefix reuse coverage
lausannel Mar 10, 2026
551d05a
test: fix multiprocess prefix stats assertions
lausannel Mar 10, 2026
67f730f
Merge remote-tracking branch 'origin' into feature/prefix-cache
lausannel Mar 16, 2026
865a726
merge
lausannel Apr 24, 2026
d0163ab
merge
lausannel Apr 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
log_*.log
log
*.log
tmp/

*.pt
runner.sh
Expand Down
271 changes: 256 additions & 15 deletions batchgen/batchgen_worker.py

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions batchgen/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,45 @@ class HostPagedKVConfig:
num_v_heads: int = 0 # Zero for MLA.
v_head_dim: int = 0
kv_dtype: str = "bfloat16" # "bfloat16 or float8_e4m3fn"
enable_prefix_reuse: bool = False
prefix_min_reuse_pages: int = 1
prefix_min_store_pages: int = 2
# Capacity knobs for prefix cache internals.
# 0 means "auto": values are derived from num_pages_per_layer at runtime.
sequence_page_node_capacity: int = 0
radix_node_capacity: int = 0
radix_edge_capacity: int = 0
prefix_entry_capacity: int = 0
prefix_page_ref_capacity: int = 0
prefix_page_budget: int = 0

def __post_init__(self) -> None:
self.apply_runtime_defaults()

def apply_runtime_defaults(self) -> None:
"""Fill auto (0) capacity fields using num_pages_per_layer."""
num_pages = max(int(self.num_pages_per_layer), 0)
if num_pages == 0:
return

if self.sequence_page_node_capacity <= 0:
self.sequence_page_node_capacity = max(num_pages, num_pages * 4)
if self.radix_node_capacity <= 0:
self.radix_node_capacity = max(4096, num_pages // 2)
if self.radix_edge_capacity <= 0:
self.radix_edge_capacity = max(
self.radix_node_capacity * 2,
self.radix_node_capacity + 1,
)
if self.prefix_entry_capacity <= 0:
self.prefix_entry_capacity = max(1024, num_pages // 64)
if self.prefix_page_budget <= 0:
self.prefix_page_budget = max(128, num_pages // 2)
if self.prefix_page_ref_capacity <= 0:
self.prefix_page_ref_capacity = max(
num_pages,
self.prefix_entry_capacity * 8,
)

@dataclass
class DevicePagedKVConfig:
Expand Down
1 change: 1 addition & 0 deletions batchgen/config/engine_config_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ def _parse_host_paged_kv_config(
if key not in valid_fields:
raise ValueError(f"Unknown key in Host_Paged_KV_Config: {key}")
setattr(host_config, key, value)
host_config.apply_runtime_defaults()


def _parse_device_paged_kv_config(
Expand Down
17 changes: 12 additions & 5 deletions batchgen/kv_cache/dual_host_kv_coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ def _try_set_logger_name(config, name: str) -> bool:
return False


def _build_host_config_from_profile(profile, shm_name: str, num_pages: int) -> Any:
def _build_host_config_from_profile(
profile, shm_name: str, num_pages: int, enable_prefix_reuse: bool = True
) -> Any:
"""Build a bg_lib.HostPagedKVConfig from a _HostKVModelProfile."""
from batchgen.kv_cache.host_kv_mananger_config import _dtype_size_bytes

Expand All @@ -55,6 +57,9 @@ def _build_host_config_from_profile(profile, shm_name: str, num_pages: int) -> A
profile.sequence_table_capacity or config.num_pages
)
config.alignment_bytes = profile.alignment_bytes
config.enable_prefix_reuse = bool(enable_prefix_reuse)
config.prefix_min_reuse_pages = 1
config.prefix_min_store_pages = 2
return config


Expand Down Expand Up @@ -107,6 +112,7 @@ def from_budget(
model_name: str,
host_kv_cache_size: int,
core_engine_module,
enable_prefix_reuse: bool = True,
enable_memfd: bool = False,
memfd_creator_pid: int = -1,
memfd_fd: int = -1,
Expand All @@ -127,10 +133,10 @@ def from_budget(
)

primary_config = _build_host_config_from_profile(
primary_profile, HOST_KV_SHM_NAME, num_pages,
primary_profile, HOST_KV_SHM_NAME, num_pages, enable_prefix_reuse,
)
aux_config = _build_host_config_from_profile(
aux_profile, HOST_KV_AUX_SHM_NAME, num_pages,
aux_profile, HOST_KV_AUX_SHM_NAME, num_pages, enable_prefix_reuse,
)

# Set distinct logger names to avoid C++ logger name collision
Expand Down Expand Up @@ -177,6 +183,7 @@ def create_managers(
cls,
model_name: str,
host_kv_cache_size: int,
enable_prefix_reuse: bool = True,
enable_memfd: bool = False,
) -> Optional[Tuple[Any, Any]]:
"""Server-side factory: create and initialize both host KV managers.
Expand All @@ -194,10 +201,10 @@ def create_managers(
)

primary_config = _build_host_config_from_profile(
primary_profile, HOST_KV_SHM_NAME, num_pages,
primary_profile, HOST_KV_SHM_NAME, num_pages, enable_prefix_reuse,
)
aux_config = _build_host_config_from_profile(
aux_profile, HOST_KV_AUX_SHM_NAME, num_pages,
aux_profile, HOST_KV_AUX_SHM_NAME, num_pages, enable_prefix_reuse,
)

# Set distinct logger names to avoid C++ logger name collision
Expand Down
46 changes: 44 additions & 2 deletions batchgen/kv_cache/host_kv_mananger_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,37 @@ def _resolve_profile(model_name: str) -> _HostKVModelProfile:
return _PROFILE_REGISTRY[_PROFILE_ALIASES[alias]]


def build_host_kv_config(model_name: str, host_kv_cache_size: int) -> Any:
def _apply_capacity_defaults(config: Any) -> None:
"""Populate HostPagedKVConfig capacity fields when they are zero."""
num_pages = int(config.num_pages)
if num_pages <= 0:
return

if int(config.sequence_page_node_capacity) <= 0:
config.sequence_page_node_capacity = max(num_pages, num_pages * 4)
if int(config.radix_node_capacity) <= 0:
config.radix_node_capacity = max(4096, num_pages // 2)
if int(config.radix_edge_capacity) <= 0:
config.radix_edge_capacity = max(
int(config.radix_node_capacity) * 2,
int(config.radix_node_capacity) + 1,
)
if int(config.prefix_entry_capacity) <= 0:
config.prefix_entry_capacity = max(1024, num_pages // 64)
if int(config.prefix_page_budget) <= 0:
config.prefix_page_budget = max(128, num_pages // 2)
if int(config.prefix_page_ref_capacity) <= 0:
config.prefix_page_ref_capacity = max(
num_pages,
int(config.prefix_entry_capacity) * 8,
)


def build_host_kv_config(
model_name: str,
host_kv_cache_size: int,
enable_prefix_reuse: bool = True,
) -> Any:
"""Builds a core HostPagedKVConfig for the given model and host budget."""

if host_kv_cache_size is None:
Expand Down Expand Up @@ -271,6 +301,10 @@ def build_host_kv_config(model_name: str, host_kv_cache_size: int) -> Any:
profile.sequence_table_capacity or config.num_pages
)
config.alignment_bytes = profile.alignment_bytes
config.enable_prefix_reuse = bool(enable_prefix_reuse)
config.prefix_min_reuse_pages = 1
config.prefix_min_store_pages = 2
_apply_capacity_defaults(config)
return config


Expand Down Expand Up @@ -356,7 +390,11 @@ def build_gpu_kv_config_aux(
)


def build_host_kv_config_aux(model_name: str, host_kv_cache_size: int) -> Any | None:
def build_host_kv_config_aux(
model_name: str,
host_kv_cache_size: int,
enable_prefix_reuse: bool = True,
) -> Any | None:
"""Builds a HostPagedKVConfig for the DSA indexer host cache, or None."""

profile = _resolve_indexer_profile(model_name)
Expand Down Expand Up @@ -386,6 +424,10 @@ def build_host_kv_config_aux(model_name: str, host_kv_cache_size: int) -> Any |
profile.sequence_table_capacity or config.num_pages
)
config.alignment_bytes = profile.alignment_bytes
config.enable_prefix_reuse = bool(enable_prefix_reuse)
config.prefix_min_reuse_pages = 1
config.prefix_min_store_pages = 2
_apply_capacity_defaults(config)
return config


Expand Down
15 changes: 15 additions & 0 deletions batchgen/server/server_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class ServerArgs:
disable_cuda_graphs: bool = True # Disable CUDA graph capture for decode attention (128K+ crash: corrupted num_tokens_per_rank)
cuda_graph_max_bucket_size: int = 128 # Max batch size per rank for CUDA graph capture
cuda_graph_num_buckets: int = 16 # Number of CUDA graph bucket sizes
enable_prefix_cache: bool = True # Enable host KV prefix cache reuse
detokenization_include_special_tokens: bool = False # When True, include special tokens in detokenized output
# Dynamic host KV reservation settings
host_kv_chunk_size: int = 8192 # Initial host KV chunk size in tokens (default: 8K)
Expand Down Expand Up @@ -349,6 +350,19 @@ def _build_parser() -> argparse.ArgumentParser:
default=16,
help="Maximum number of CUDA graph bucket sizes (default: 16). More buckets = longer capture time but less padding waste.",
)
parser.set_defaults(enable_prefix_cache=True)
parser.add_argument(
"--enable-prefix-cache",
dest="enable_prefix_cache",
action="store_true",
help="Enable host KV prefix cache reuse (default: enabled)",
)
parser.add_argument(
"--disable-prefix-cache",
dest="enable_prefix_cache",
action="store_false",
help="Disable host KV prefix cache reuse",
)
parser.add_argument(
"--detokenization-include-special-tokens",
action="store_true",
Expand Down Expand Up @@ -546,6 +560,7 @@ def prepare_server_args(argv: Optional[list[str]] = None) -> ServerArgs:
cuda_graph_max_bucket_size=parsed.cuda_graph_max_bucket_size,
cuda_graph_num_buckets=parsed.cuda_graph_num_buckets,
detokenization_include_special_tokens=parsed.detokenization_include_special_tokens,
enable_prefix_cache=parsed.enable_prefix_cache,
host_kv_chunk_size=parsed.host_kv_chunk_size,
host_kv_eviction_watermark=parsed.host_kv_eviction_watermark,
enable_host_kv_eviction=parsed.enable_host_kv_eviction,
Expand Down
15 changes: 14 additions & 1 deletion batchgen/server/worker_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@
PARAMETER_SERVER_ENDPOINT_ENV = "BATCHGEN_PARAMETER_SERVER_ENDPOINT"


def _load_server_worker_main():
# Delay this import to avoid a package-init cycle when worker subprocesses
# import `batchgen.server.process_utils` via `batchgen.server_worker_main_loop`.
from batchgen.server_worker_main_loop import server_worker_main

return server_worker_main


def _validate_shmem_enabled() -> None:
"""Check that THP shmem is enabled for --fast-init. Raises RuntimeError if not."""
import re
Expand Down Expand Up @@ -204,6 +212,7 @@ def _diag(msg):
_diag(">>> allocate_host_kv_cache")
result = self.allocate_host_kv_cache(
self.args.host_kv_cache_size, self.args.model,
enable_prefix_cache=self.args.enable_prefix_cache,
enable_memfd=self.args.fast_init,
)
_diag("<<< allocate_host_kv_cache")
Expand Down Expand Up @@ -629,6 +638,7 @@ def _spawn_workers(self) -> None:
disable_cuda_graphs=self.args.disable_cuda_graphs,
cuda_graph_max_bucket_size=self.args.cuda_graph_max_bucket_size,
cuda_graph_num_buckets=self.args.cuda_graph_num_buckets,
enable_prefix_cache=self.args.enable_prefix_cache,
detokenization_include_special_tokens=self.args.detokenization_include_special_tokens,
host_kv_chunk_size=self.args.host_kv_chunk_size,
enable_host_kv_eviction=self.args.enable_host_kv_eviction,
Expand All @@ -648,7 +658,7 @@ def _spawn_workers(self) -> None:
)
from batchgen.server_worker_main_loop import server_worker_main
self.worker_process = mp.spawn(
server_worker_main,
_load_server_worker_main(),
args=(
self.request_queue,
self.response_queue,
Expand Down Expand Up @@ -1005,6 +1015,7 @@ def _configure_host_kv_cache_budget(self) -> None:
@staticmethod
def allocate_host_kv_cache(
host_kv_cache_size_gb: int, model_name: str,
enable_prefix_cache: bool = True,
enable_memfd: bool = False,
) -> Any:
from batchgen.kv_cache.dual_host_kv_coordinator import DualHostKVCoordinator
Expand All @@ -1013,6 +1024,7 @@ def allocate_host_kv_cache(
dual = DualHostKVCoordinator.create_managers(
model_name=model_name,
host_kv_cache_size=int(host_kv_cache_size_gb * (1024**3)),
enable_prefix_reuse=enable_prefix_cache,
enable_memfd=enable_memfd,
)
if dual is not None:
Expand All @@ -1025,6 +1037,7 @@ def allocate_host_kv_cache(
config = build_host_kv_config(
host_kv_cache_size=host_kv_cache_size_gb * (1024**3),
model_name=model_name,
enable_prefix_reuse=enable_prefix_cache,
)
if enable_memfd:
config.enable_memfd = True
Expand Down
Loading
Loading