lmcache--lmcache
232 行
6.8 KiB
Python
232 行
6.8 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
"""
|
|
Plot token cache hit rate vs cache capacity (GiB).
|
|
|
|
Sweeps a logarithmically-spaced range of cache capacities, runs the simulator
|
|
at each point, and produces a matplotlib figure showing how token hit rate
|
|
scales with available memory.
|
|
|
|
Usage::
|
|
|
|
python -m lmcache.tools.cache_simulator.plot_hit_rate \\
|
|
-i /path/to/lookup_hashes/ \\
|
|
--min-capacity-gib 1 \\
|
|
--max-capacity-gib 512 \\
|
|
--points 30 \\
|
|
-o hit_rate_vs_capacity.png
|
|
"""
|
|
|
|
# Standard
|
|
from pathlib import Path
|
|
import argparse
|
|
import math
|
|
import sys
|
|
|
|
# First Party
|
|
from lmcache.tools.cache_simulator.simulator import (
|
|
compute_kv_bytes_per_chunk,
|
|
load_lookup_events,
|
|
simulate,
|
|
)
|
|
|
|
_GIB = 2**30
|
|
|
|
|
|
def capacity_range_bytes(
|
|
min_gib: float,
|
|
max_gib: float,
|
|
num_points: int,
|
|
) -> list[int]:
|
|
"""
|
|
Return *num_points* byte capacities log-spaced between *min_gib* and
|
|
*max_gib* GiB.
|
|
"""
|
|
log_min = math.log10(min_gib * _GIB)
|
|
log_max = math.log10(max_gib * _GIB)
|
|
step = (log_max - log_min) / max(num_points - 1, 1)
|
|
return sorted({round(10 ** (log_min + i * step)) for i in range(num_points)})
|
|
|
|
|
|
def add_sweep_arguments(parser: argparse.ArgumentParser) -> None:
|
|
"""Register all ``sweep`` CLI flags onto *parser*.
|
|
|
|
Called by both the module ``main()`` and by
|
|
:class:`~lmcache.cli.commands.tool.ToolCommand` so that flag definitions
|
|
live in exactly one place.
|
|
|
|
Args:
|
|
parser: The ``ArgumentParser`` (or sub-parser) to add flags to.
|
|
"""
|
|
parser.add_argument(
|
|
"-i",
|
|
"--input",
|
|
nargs="+",
|
|
required=True,
|
|
metavar="PATH",
|
|
help="One or more lookup-hash JSONL files or directories",
|
|
)
|
|
parser.add_argument(
|
|
"-n",
|
|
"--max-samples",
|
|
type=int,
|
|
default=None,
|
|
metavar="N",
|
|
help="Maximum number of events to process (default: all)",
|
|
)
|
|
parser.add_argument(
|
|
"--model",
|
|
default=None,
|
|
metavar="NAME",
|
|
help="Filter events by model_name (exact match)",
|
|
)
|
|
parser.add_argument(
|
|
"--min-capacity-gib",
|
|
type=float,
|
|
default=0.5,
|
|
metavar="GiB",
|
|
help="Minimum cache capacity to sweep (default: 0.5 GiB)",
|
|
)
|
|
parser.add_argument(
|
|
"--max-capacity-gib",
|
|
type=float,
|
|
default=500.0,
|
|
metavar="GiB",
|
|
help="Maximum cache capacity to sweep (default: 500 GiB)",
|
|
)
|
|
parser.add_argument(
|
|
"--points",
|
|
type=int,
|
|
default=30,
|
|
metavar="N",
|
|
help="Number of log-spaced capacity samples (default: 30)",
|
|
)
|
|
parser.add_argument(
|
|
"--linear",
|
|
action="store_true",
|
|
help="Use a linear x-axis (default: log scale)",
|
|
)
|
|
parser.add_argument(
|
|
"--kv-bytes-per-chunk",
|
|
type=int,
|
|
default=None,
|
|
metavar="BYTES",
|
|
help=(
|
|
"Bytes consumed by one cached chunk. "
|
|
"Auto-computed from the first event's shapes/dtypes if omitted."
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"-o",
|
|
"--output",
|
|
default="hit_rate_vs_capacity.png",
|
|
metavar="FILE",
|
|
help="Output image path (default: hit_rate_vs_capacity.png)",
|
|
)
|
|
|
|
|
|
def run_sweep(args: argparse.Namespace) -> None:
|
|
"""Execute the sweep workflow from a parsed argument namespace.
|
|
|
|
Loads events, resolves ``kv_bytes_per_chunk``, sweeps across a log-spaced
|
|
range of cache capacities, prints a results table, and saves a hit-rate vs
|
|
capacity PNG. Called by both the module ``main()`` and by
|
|
:class:`~lmcache.cli.commands.tool.ToolCommand`.
|
|
|
|
Args:
|
|
args: Parsed CLI arguments. Must have the attributes registered by
|
|
:func:`add_sweep_arguments`.
|
|
"""
|
|
paths = [Path(p) for p in args.input]
|
|
print(f"Loading lookup events from {[str(p) for p in paths]} …")
|
|
events = load_lookup_events(paths, model=args.model, max_samples=args.max_samples)
|
|
print(f"Loaded {len(events):,} event(s)\n")
|
|
|
|
if not events:
|
|
print("No events to process.")
|
|
sys.exit(0)
|
|
|
|
kv_bpc = args.kv_bytes_per_chunk
|
|
if kv_bpc is None:
|
|
kv_bpc = compute_kv_bytes_per_chunk(events[0])
|
|
if kv_bpc == 0:
|
|
print(
|
|
"Error: could not determine kv_bytes_per_chunk from the first event "
|
|
"(shapes/dtypes are empty). Pass --kv-bytes-per-chunk explicitly.",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(1)
|
|
print(f"Auto-detected kv_bytes_per_chunk = {kv_bpc:,} bytes")
|
|
|
|
chunk_size = events[0].get("chunk_size", "?")
|
|
model_label = args.model or "all models"
|
|
|
|
capacities_bytes = capacity_range_bytes(
|
|
args.min_capacity_gib, args.max_capacity_gib, args.points
|
|
)
|
|
hit_rates: list[float] = []
|
|
|
|
scale_label = "linear" if args.linear else "log"
|
|
print(
|
|
f"Sweeping {len(capacities_bytes)} capacity values "
|
|
f"({args.min_capacity_gib:.2f} – {args.max_capacity_gib:.2f} GiB), "
|
|
f"chunk_size = {chunk_size} tokens, model = {model_label}\n"
|
|
)
|
|
print(f"{'Capacity (GiB)':>18} {'Hit rate':>10}")
|
|
print("-" * 32)
|
|
|
|
for cap_bytes in capacities_bytes:
|
|
cap_gib = cap_bytes / _GIB
|
|
res = simulate(events, cap_bytes, kv_bpc, fast=True)
|
|
rate = res["token_hit_rate"]
|
|
hit_rates.append(rate)
|
|
print(f"{cap_gib:>18.3f} {rate:>9.2%}")
|
|
|
|
# ── Plot ────────────────────────────────────────────────────────────────
|
|
x_values = [c / _GIB for c in capacities_bytes]
|
|
|
|
# Third Party
|
|
import matplotlib.pyplot as plt # noqa: PLC0415 — lazy import to avoid hard dependency
|
|
|
|
fig, ax = plt.subplots(figsize=(9, 5))
|
|
ax.plot(
|
|
x_values,
|
|
[r * 100 for r in hit_rates],
|
|
marker="o",
|
|
linewidth=2,
|
|
markersize=4,
|
|
)
|
|
|
|
if not args.linear:
|
|
ax.set_xscale("log")
|
|
|
|
ax.set_xlabel("Cache capacity (GiB)", fontsize=12)
|
|
ax.set_ylabel("Token hit rate (%)", fontsize=12)
|
|
ax.set_title(
|
|
f"Token cache hit rate vs capacity\n"
|
|
f"(chunk_size = {chunk_size} tokens, {len(events):,} requests, "
|
|
f"model = {model_label}, {scale_label} scale)",
|
|
fontsize=11,
|
|
)
|
|
ax.set_ylim(0, 100)
|
|
ax.grid(True, which="both", linestyle="--", alpha=0.5)
|
|
ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda y, _: f"{y:.0f}%"))
|
|
|
|
fig.tight_layout()
|
|
fig.savefig(args.output, dpi=150)
|
|
print(f"\nPlot saved to '{args.output}'")
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(
|
|
description=(
|
|
"Plot token cache hit rate vs cache capacity from lookup-hash JSONL logs"
|
|
)
|
|
)
|
|
add_sweep_arguments(parser)
|
|
args = parser.parse_args()
|
|
run_sweep(args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|