Skip to content
Merged
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
12 changes: 6 additions & 6 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,12 +236,12 @@ services:

Human-readable sizes use MiB below 1 GiB and GiB otherwise. DaseR receives the
derived L1 byte size directly. In evict runs, DaseR also receives the derived
L2 byte size. LMCache's L1 CLI accepts only integer GiB and starts eviction at
an 80% trigger watermark, so evict runs configure `lmcache_l1_gb` as
`ceil(derived_l1 / 0.8)` in GiB and pass `--eviction-trigger-watermark 0.8`.
For small workloads, LMCache's integer-GiB granularity can still make its
effective L1 slightly larger than DaseR's byte-exact L1. In no-evict runs,
DaseR starts with `--skip-l2` and no `--l2-size`, and `lmcache_l2_gb` is
L2 byte size. LMCache's L1 CLI accepts only integer GiB, so runs configure
`lmcache_l1_gb` as `ceil(derived_l1)` in GiB and still pass
`--eviction-trigger-watermark 0.8`. For small workloads, LMCache's integer-GiB
granularity can still make its configured L1 slightly larger than DaseR's
byte-exact L1. In no-evict runs, DaseR starts with `--skip-l2` and no
`--l2-size`, and `lmcache_l2_gb` is
`null` because LMCache has no L2 adapter. In evict runs, LMCache's current FS
L2 adapter CLI does not expose an L2 capacity limit, so the report should treat
LMCache L2 as bounded by the filesystem free space rather than by the derived
Expand Down
9 changes: 7 additions & 2 deletions benchmarks/run_bench.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@
COMPARISON_IOURING_MEM,
slot_size_for_block_tokens,
)
from benchmarks.utils.loadgen import backend_server_hit_rate, collect_phase_metrics
from benchmarks.utils.loadgen import (
_wait_lmcache_quiescent,
backend_server_hit_rate,
collect_phase_metrics,
)
from benchmarks.utils.servers import BenchmarkManifest, stop_from_pid_file
from benchmarks.utils.sizing import (
BenchmarkCapacityLimits,
Expand Down Expand Up @@ -597,7 +601,8 @@ def _run_vllm_bench_load(
cold_raw,
)
if backend_run.backend == "lmcache":
_wait_with_message("lmcache_warm_settle_s", 10.0)
_print_kv("lmcache_warm_wait", "quiescent")
asyncio.run(_wait_lmcache_quiescent(manifest, settle_seconds=0.0))
elif backend_run.backend == "daser":
_drain_daser(manifest)
warm_metrics, warm_hit_rate = _run_vllm_bench_phase(
Expand Down
16 changes: 16 additions & 0 deletions benchmarks/utils/lmcache_connector_shim.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# SPDX-License-Identifier: Apache-2.0
"""Benchmark-only alias for LMCache's external MP connector."""

from __future__ import annotations

from lmcache.integration.vllm.lmcache_mp_connector import (
LMCacheMPConnector as _ExternalLMCacheMPConnector,
)


class DaseRBenchLMCacheMPConnector(_ExternalLMCacheMPConnector):
"""Alias that bypasses vLLM's built-in ``LMCacheMPConnector`` registry entry.

Thread-safety:
Same as ``lmcache.integration.vllm.lmcache_mp_connector.LMCacheMPConnector``.
"""
13 changes: 10 additions & 3 deletions benchmarks/utils/loadgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
from benchmarks.utils.prompts import build_prompt_payloads
from benchmarks.utils.servers import LMCACHE_HTTP_PORT, BenchmarkManifest

_LMCACHE_QUIESCENCE_TIMEOUT_SECONDS = 600.0


@dataclass
class RequestResult:
Expand Down Expand Up @@ -220,7 +222,7 @@ async def run_lmcache(
max_inflight: int,
gen_params: dict[str, Any],
timeout: float,
settle_seconds: float = 10.0,
settle_seconds: float = 0.0,
chunk_aligned_prompts: bool = False,
) -> dict[str, Any]:
"""Run LMCache cold and warm full-prompt phases."""
Expand Down Expand Up @@ -469,10 +471,15 @@ async def _wait_lmcache_quiescent(
manifest: BenchmarkManifest, settle_seconds: float
) -> None:
del manifest
deadline = time.monotonic() + max(1.0, settle_seconds)
timeout_seconds = max(_LMCACHE_QUIESCENCE_TIMEOUT_SECONDS, float(settle_seconds))
deadline = time.monotonic() + timeout_seconds
stable = 0
async with httpx.AsyncClient(timeout=httpx.Timeout(5.0)) as client:
while time.monotonic() < deadline:
while True:
if time.monotonic() >= deadline:
raise TimeoutError(
f"LMCache did not become quiescent within {timeout_seconds:.1f}s"
)
status = await _get_json(
client, f"http://127.0.0.1:{LMCACHE_HTTP_PORT}/status"
)
Expand Down
59 changes: 46 additions & 13 deletions benchmarks/utils/servers.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@
from benchmarks.utils.sizing import (
LMCACHE_EVICTION_TRIGGER_WATERMARK,
bytes_to_lmcache_gb,
bytes_to_lmcache_gb_for_effective_l1,
)

LMCACHE_MP_HOST = "tcp://localhost"
LMCACHE_MP_PORT = 5555
LMCACHE_HTTP_PORT = 8080
LMCACHE_MP_CONNECTOR_NAME = "DaseRBenchLMCacheMPConnector"
LMCACHE_MP_CONNECTOR_MODULE = "benchmarks.utils.lmcache_connector_shim"
REPO_ROOT = Path(__file__).resolve().parents[2]
LMCACHE_REPO_ROOT = REPO_ROOT.parent / "LMCache"


@dataclass(frozen=True)
Expand Down Expand Up @@ -226,7 +228,11 @@ def manifest(self) -> BenchmarkManifest:
async def start_lmcache_mp_server(self) -> None:
"""Start the LMCache MP server."""
cmd = self._lmcache_mp_server_command()
proc = self._start(cmd, "lmcache_mp_server.log")
proc = self._start(
cmd,
"lmcache_mp_server.log",
extra_env=self.lmcache_process_env(),
)
await self._wait_healthy(
f"http://127.0.0.1:{LMCACHE_HTTP_PORT}",
"/healthcheck",
Expand All @@ -245,11 +251,7 @@ def _lmcache_mp_server_command(self) -> list[str]:
Pure helper except for creating the L2 scratch directory when the
adapter is enabled.
"""
l1_gb = (
bytes_to_lmcache_gb(self.l1_size_bytes)
if self.skip_l2
else bytes_to_lmcache_gb_for_effective_l1(self.l1_size_bytes)
)
l1_gb = bytes_to_lmcache_gb(self.l1_size_bytes)
cmd = [
"lmcache",
"server",
Expand Down Expand Up @@ -287,19 +289,50 @@ async def start_vllm_only(self) -> None:

async def start_vllm_lmcache(self) -> None:
"""Start vLLM with LMCache MP connector."""
await self._start_vllm(
"vllm_lmcache.log",
self.lmcache_kv_transfer_config(),
extra_env={
**self.lmcache_process_env(),
"PYTHONHASHSEED": "42",
},
)

def lmcache_kv_transfer_config(self) -> dict[str, Any]:
"""Return vLLM KV transfer config for the sibling LMCache checkout.

Returns:
JSON-serialisable vLLM ``--kv-transfer-config`` payload.

Thread-safety:
Pure calculation over module constants and port configuration.
"""
kv_config = {
"kv_connector": "LMCacheMPConnector",
"kv_connector": LMCACHE_MP_CONNECTOR_NAME,
"kv_connector_module_path": LMCACHE_MP_CONNECTOR_MODULE,
"kv_role": "kv_both",
"kv_connector_extra_config": {
"lmcache.mp.host": LMCACHE_MP_HOST,
"lmcache.mp.port": LMCACHE_MP_PORT,
},
}
await self._start_vllm(
"vllm_lmcache.log",
kv_config,
extra_env={"PYTHONHASHSEED": "42"},
)
return kv_config

def lmcache_process_env(self) -> dict[str, str]:
"""Return environment overrides for LMCache benchmark subprocesses.

Returns:
Environment entries that make the sibling ``../LMCache`` checkout
take precedence over installed LMCache packages and vLLM built-ins.

Thread-safety:
Reads the current process environment without mutating it.
"""
pythonpath = os.environ.get("PYTHONPATH")
entries = [str(LMCACHE_REPO_ROOT), str(REPO_ROOT)]
if pythonpath:
entries.append(pythonpath)
return {"PYTHONPATH": os.pathsep.join(entries)}

async def start_vllm_daser(self) -> None:
"""Start vLLM with DaseR connector."""
Expand Down
29 changes: 1 addition & 28 deletions benchmarks/utils/sizing.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,9 +238,7 @@ def derive_benchmark_sizing(
daser_l2_bytes=daser_l2_bytes,
daser_l1_bytes=daser_l1_bytes,
lmcache_disk_gb=None,
lmcache_cpu_gb=bytes_to_lmcache_gb_for_effective_l1(daser_l1_bytes)
if evict
else bytes_to_lmcache_gb(daser_l1_bytes),
lmcache_cpu_gb=bytes_to_lmcache_gb(daser_l1_bytes),
capacity_capped=capacity_capped,
)

Expand Down Expand Up @@ -310,31 +308,6 @@ def bytes_to_lmcache_gb(nbytes: int) -> int:
return math.ceil(nbytes / BYTES_PER_GIB)


def bytes_to_lmcache_gb_for_effective_l1(
nbytes: int,
watermark: float = LMCACHE_EVICTION_TRIGGER_WATERMARK,
) -> int:
"""Convert effective L1 bytes to LMCache's configured GiB size.

Args:
nbytes: Desired effective L1 capacity in bytes.
watermark: LMCache eviction trigger watermark. LMCache starts evicting
at this fraction of ``--l1-size-gb``.

Returns:
LMCache ``--l1-size-gb`` value whose watermark capacity is at least
``nbytes``.

Thread-safety:
Pure function.
"""
if nbytes <= 0:
return 0
if watermark <= 0:
raise ValueError("watermark must be positive")
return bytes_to_lmcache_gb(math.ceil(nbytes / watermark))


def format_capacity(nbytes: int) -> str:
"""Format a byte capacity for human-readable benchmark output.

Expand Down
11 changes: 5 additions & 6 deletions daser/server/ipc/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
from daser.transfer import TransferLayer
from daser.transfer.cuda_ipc import open_cuda_ipc_buffer
from daser.transfer.iouring import TieredIOUringTransferLayer
from daser.transfer.memory import L1OnlyTransferLayer

logger = init_logger(__name__)

Expand Down Expand Up @@ -638,11 +637,10 @@ def _ensure_transfer(self) -> TransferLayer:
return self._transfer
mode = str(self._runtime_config.get("transfer_mode", "gds"))
path = str(self._runtime_config.get("store_path", ""))
if bool(self._runtime_config.get("skip_l2", False)):
self._transfer = L1OnlyTransferLayer(
l1_bytes=int(self._runtime_config.get("l1_size_bytes", 0)),
)
elif mode == "gds":
skip_l2 = bool(self._runtime_config.get("skip_l2", False))
if mode == "gds":
if skip_l2:
raise ValueError("skip_l2 is incompatible with gds transfer")
from daser.transfer.gds import GDSTransferLayer

self._transfer = GDSTransferLayer(path)
Expand All @@ -661,6 +659,7 @@ def _ensure_transfer(self) -> TransferLayer:
path=path,
l1_bytes=int(self._runtime_config.get("l1_size_bytes", l2_bytes)),
l2_bytes=l2_bytes,
skip_l2=skip_l2,
)
else:
raise ValueError(f"unknown transfer_mode: {mode}")
Expand Down
2 changes: 0 additions & 2 deletions daser/transfer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@

from daser.transfer.base import TransferLayer, TransferMode, TransferStats
from daser.transfer.iouring import TieredIOUringTransferLayer
from daser.transfer.memory import L1OnlyTransferLayer

__all__ = [
"GDSTransferLayer",
"L1OnlyTransferLayer",
"TieredIOUringTransferLayer",
"TransferBackend",
"TransferLayer",
Expand Down
Loading
Loading