Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
123 changes: 123 additions & 0 deletions src/kvcache_upper_bound/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@
convert_conversation_dataset,
load_request_records,
)
from kvcache_upper_bound.ingest.normalizer import build_effective_requests
from kvcache_upper_bound.oracle import (
analyze_capacity_upper_bound,
analyze_lfu_baseline,
analyze_lru_baseline,
analyze_prefix_aware,
)
from kvcache_upper_bound.reporting import (
analyze_bucket_deployments,
build_bucket_input_summaries,
Expand Down Expand Up @@ -185,6 +192,20 @@ def main() -> int:
help="Allow replay-only synthetic hash_ids when benchmark records do not provide them",
)

compare_parser = subparsers.add_parser(
"compare-policies",
help="Run all eviction policies (Belady, LRU, LFU, Prefix-Aware) and output comparison",
)
compare_parser.add_argument("--trace", required=True, help="Local JSONL path or http(s) URL")
compare_parser.add_argument("--config", required=True, help="Bucket analysis JSON config")
compare_parser.add_argument("--output-dir", required=True, help="Directory for comparison JSON output")
compare_parser.add_argument(
"--max-records",
type=int,
default=None,
help="Optional cap for quick iteration",
)

args = parser.parse_args()
if args.command == "list-datasets":
return _run_list_datasets()
Expand All @@ -200,6 +221,8 @@ def main() -> int:
return _run_convert_conversation_dataset(args)
if args.command == "convert-benchmark-results":
return _run_convert_benchmark_results(args)
if args.command == "compare-policies":
return _run_compare_policies(args)
raise ValueError(f"unsupported command: {args.command}")


Expand Down Expand Up @@ -487,6 +510,106 @@ def _run_convert_benchmark_results(args: argparse.Namespace) -> int:
return 0


def _run_compare_policies(args: argparse.Namespace) -> int:
config = load_bucket_analysis_config(args.config)
trace_result = load_request_records(args.trace, max_records=args.max_records)
records = trace_result.records

# Convert RequestRecords to EffectiveRequests (same pattern as analyze_bucket_deployments)
# Use the first bucket deployment's window_tokens, or fall back to max input_length
deployment = config.bucket_deployments[0]
window_tokens = deployment.resolved_window_tokens(records)
normalized = build_effective_requests(
records,
window_tokens=window_tokens,
scope=config.scope,
block_size=config.block_size,
)

# Compute HBM budget from first deployment
hbm_kv_gb_per_card = deployment.resolved_hbm_kv_gb_per_card(config.model_profile)
hbm_kv_total_gb = deployment.accelerator_count * hbm_kv_gb_per_card
budget_bytes = int(hbm_kv_total_gb * (1024**3))

# Run all 4 policies
belady_result = analyze_capacity_upper_bound(
normalized.requests,
model_profile=config.model_profile,
budget_bytes=budget_bytes,
block_size=config.block_size,
include_output_kvcache=config.include_output_kvcache,
)
lru_result = analyze_lru_baseline(
normalized.requests,
model_profile=config.model_profile,
budget_bytes=budget_bytes,
block_size=config.block_size,
include_output_kvcache=config.include_output_kvcache,
)
lfu_result = analyze_lfu_baseline(
normalized.requests,
model_profile=config.model_profile,
budget_bytes=budget_bytes,
block_size=config.block_size,
include_output_kvcache=config.include_output_kvcache,
)
prefix_aware_result = analyze_prefix_aware(
normalized.requests,
model_profile=config.model_profile,
budget_bytes=budget_bytes,
block_size=config.block_size,
include_output_kvcache=config.include_output_kvcache,
)

comparison = {
"trace": args.trace,
"config": str(Path(args.config).resolve()),
"budget_bytes": budget_bytes,
"budget_gb": hbm_kv_total_gb,
"total_requests": len(normalized.requests),
"policies": {
"belady": {
"total_blocks": belady_result.summary.total_blocks,
"hit_blocks": belady_result.summary.hit_blocks,
"block_hit_rate": belady_result.summary.block_hit_rate,
"strict_prefix_block_hit_rate": belady_result.summary.strict_prefix_block_hit_rate,
"token_hit_rate_est": belady_result.summary.token_hit_rate_est,
},
"lru": {
"total_blocks": lru_result.summary.total_blocks,
"hit_blocks": lru_result.summary.hit_blocks,
"block_hit_rate": lru_result.summary.block_hit_rate,
"strict_prefix_block_hit_rate": lru_result.summary.strict_prefix_block_hit_rate,
"token_hit_rate_est": lru_result.summary.token_hit_rate_est,
},
"lfu": {
"total_blocks": lfu_result.summary.total_blocks,
"hit_blocks": lfu_result.summary.hit_blocks,
"block_hit_rate": lfu_result.summary.block_hit_rate,
"strict_prefix_block_hit_rate": lfu_result.summary.strict_prefix_block_hit_rate,
"token_hit_rate_est": lfu_result.summary.token_hit_rate_est,
},
"prefix_aware": {
"total_blocks": prefix_aware_result.summary.total_blocks,
"hit_blocks": prefix_aware_result.summary.hit_blocks,
"block_hit_rate": prefix_aware_result.summary.block_hit_rate,
"strict_prefix_block_hit_rate": prefix_aware_result.summary.strict_prefix_block_hit_rate,
"token_hit_rate_est": prefix_aware_result.summary.token_hit_rate_est,
},
},
}

output_dir = Path(args.output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
output_path = output_dir / "policy_comparison.json"
output_path.write_text(
json.dumps(comparison, ensure_ascii=False, indent=2),
encoding="utf-8",
)
print(json.dumps(comparison, ensure_ascii=False, indent=2))
return 0


def _build_analysis_metadata_payload(
trace: str,
config_path: str,
Expand Down
6 changes: 6 additions & 0 deletions src/kvcache_upper_bound/oracle/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
ContentSummary,
analyze_content_upper_bound,
)
from .lfu import LFUSimulationResult, analyze_lfu_baseline
from .lru import LRUSimulationResult, analyze_lru_baseline
from .prefix_aware import PrefixAwareSimulationResult, analyze_prefix_aware
from .prefix_trie import PrefixTrie, PrefixTrieNode
from .strict_prefix import (
StrictPrefixAnalysisResult,
Expand All @@ -28,14 +30,18 @@
"ContentAnalysisResult",
"ContentRequestMetric",
"ContentSummary",
"LFUSimulationResult",
"LRUSimulationResult",
"PrefixAwareSimulationResult",
"PrefixTrie",
"PrefixTrieNode",
"StrictPrefixAnalysisResult",
"StrictPrefixRequestMetric",
"StrictPrefixSummary",
"analyze_capacity_upper_bound",
"analyze_content_upper_bound",
"analyze_lfu_baseline",
"analyze_lru_baseline",
"analyze_prefix_aware",
"analyze_strict_prefix_capacity_upper_bound",
]
165 changes: 165 additions & 0 deletions src/kvcache_upper_bound/oracle/lfu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import Iterable

from kvcache_upper_bound.core.models import EffectiveRequest, ModelProfile
from kvcache_upper_bound.oracle.capacity import (
CapacityRequestMetric,
CapacitySummary,
_build_access_trace,
_count_prefix_hits,
)
from kvcache_upper_bound.oracle.content import _estimate_hit_tokens, _safe_ratio


@dataclass(frozen=True)
class LFUSimulationResult:
request_metrics: list[CapacityRequestMetric]
summary: CapacitySummary


def analyze_lfu_baseline(
requests: Iterable[EffectiveRequest],
model_profile: ModelProfile,
budget_bytes: int,
block_size: int = 16,
include_output_kvcache: bool = False,
) -> LFUSimulationResult:
if budget_bytes < 0:
raise ValueError("budget_bytes must be non-negative")
if block_size <= 0:
raise ValueError("block_size must be positive")
if model_profile.block_size != block_size:
raise ValueError("model_profile.block_size must match analysis block_size")

bytes_per_block = model_profile.kv_bytes_per_block()
resident_block_capacity = budget_bytes // bytes_per_block if bytes_per_block > 0 else 0
access_trace = _build_access_trace(requests, include_output_kvcache=include_output_kvcache)
event_hits = _run_lfu(access_trace.access_events, resident_block_capacity)

request_metrics: list[CapacityRequestMetric] = []
total_blocks = 0
hit_blocks = 0
strict_prefix_hit_blocks = 0
total_tokens = 0
hit_tokens_est = 0
strict_prefix_hit_tokens_est = 0

for request, (start, end) in zip(access_trace.requests, access_trace.request_ranges):
request_hit_blocks = sum(event_hits[start:end])
request_strict_prefix_hit_blocks = _count_prefix_hits(event_hits, start, end)
request_total_blocks = request.effective_blocks
request_miss_blocks = request_total_blocks - request_hit_blocks
request_hit_tokens = _estimate_hit_tokens(
effective_blocks=request_total_blocks,
effective_tokens=request.effective_tokens,
hit_blocks=request_hit_blocks,
block_size=block_size,
)
request_strict_prefix_hit_tokens = _estimate_hit_tokens(
effective_blocks=request_total_blocks,
effective_tokens=request.effective_tokens,
hit_blocks=request_strict_prefix_hit_blocks,
block_size=block_size,
)
request_miss_tokens = request.effective_tokens - request_hit_tokens
request_total_kv_bytes = request_total_blocks * bytes_per_block
request_hit_kv_bytes = request_hit_blocks * bytes_per_block
request_strict_prefix_hit_kv_bytes = request_strict_prefix_hit_blocks * bytes_per_block
request_miss_kv_bytes = request_miss_blocks * bytes_per_block

request_metrics.append(
CapacityRequestMetric(
request_id=request.request_id,
scope_root_id=request.scope_root_id,
request_type=request.request_type,
turn=request.turn,
total_blocks=request_total_blocks,
hit_blocks=request_hit_blocks,
strict_prefix_hit_blocks=request_strict_prefix_hit_blocks,
miss_blocks=request_miss_blocks,
total_tokens=request.effective_tokens,
hit_tokens_est=request_hit_tokens,
strict_prefix_hit_tokens_est=request_strict_prefix_hit_tokens,
miss_tokens_est=request_miss_tokens,
total_kv_bytes=request_total_kv_bytes,
hit_kv_bytes=request_hit_kv_bytes,
strict_prefix_hit_kv_bytes=request_strict_prefix_hit_kv_bytes,
miss_kv_bytes=request_miss_kv_bytes,
)
)

total_blocks += request_total_blocks
hit_blocks += request_hit_blocks
strict_prefix_hit_blocks += request_strict_prefix_hit_blocks
total_tokens += request.effective_tokens
hit_tokens_est += request_hit_tokens
strict_prefix_hit_tokens_est += request_strict_prefix_hit_tokens

miss_blocks = total_blocks - hit_blocks
miss_tokens_est = total_tokens - hit_tokens_est
total_kv_bytes = total_blocks * bytes_per_block
hit_kv_bytes = hit_blocks * bytes_per_block
strict_prefix_hit_kv_bytes = strict_prefix_hit_blocks * bytes_per_block
miss_kv_bytes = miss_blocks * bytes_per_block

summary = CapacitySummary(
budget_bytes=budget_bytes,
resident_block_capacity=resident_block_capacity,
total_requests=len(access_trace.requests),
total_blocks=total_blocks,
hit_blocks=hit_blocks,
strict_prefix_hit_blocks=strict_prefix_hit_blocks,
miss_blocks=miss_blocks,
block_hit_rate=_safe_ratio(hit_blocks, total_blocks),
strict_prefix_block_hit_rate=_safe_ratio(strict_prefix_hit_blocks, total_blocks),
total_tokens=total_tokens,
hit_tokens_est=hit_tokens_est,
strict_prefix_hit_tokens_est=strict_prefix_hit_tokens_est,
miss_tokens_est=miss_tokens_est,
token_hit_rate_est=_safe_ratio(hit_tokens_est, total_tokens),
strict_prefix_token_hit_rate_est=_safe_ratio(strict_prefix_hit_tokens_est, total_tokens),
total_kv_bytes=total_kv_bytes,
hit_kv_bytes=hit_kv_bytes,
strict_prefix_hit_kv_bytes=strict_prefix_hit_kv_bytes,
miss_kv_bytes=miss_kv_bytes,
kv_byte_hit_rate=_safe_ratio(hit_kv_bytes, total_kv_bytes),
strict_prefix_kv_byte_hit_rate=_safe_ratio(strict_prefix_hit_kv_bytes, total_kv_bytes),
)
return LFUSimulationResult(request_metrics=request_metrics, summary=summary)


def _run_lfu(access_events: Iterable[int], resident_block_capacity: int) -> bytearray:
"""LFU eviction: evict block with lowest frequency; ties broken by earliest insertion."""
access_list = list(access_events)
event_hits = bytearray(len(access_list))
if resident_block_capacity <= 0 or not access_list:
return event_hits

# freq[node_id] = access count while resident
freq: dict[int, int] = {}
# insertion_order[node_id] = order of first insertion (for tie-breaking)
insertion_order: dict[int, int] = {}
resident: set[int] = set()
next_insertion_order = 0

for index, node_id in enumerate(access_list):
if node_id in resident:
event_hits[index] = 1
freq[node_id] += 1
continue

if len(resident) >= resident_block_capacity:
# Evict block with lowest frequency; ties broken by earliest insertion
victim = min(resident, key=lambda nid: (freq[nid], insertion_order[nid]))
resident.remove(victim)
del freq[victim]
del insertion_order[victim]

resident.add(node_id)
freq[node_id] = 1
insertion_order[node_id] = next_insertion_order
next_insertion_order += 1

return event_hits
Loading