Skip to content

Commit d975b6a

Browse files
authored
Merge pull request #9 from aerospike-community/lyndon/phase1-closure
Phase 1 closure: ecosystem benchmarks, layerwise test, docs
2 parents 2675cc2 + be64630 commit d975b6a

19 files changed

Lines changed: 694 additions & 124 deletions

AGENTS.md

Lines changed: 34 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -2,91 +2,61 @@
22

33
Aerospike remote storage backend for [LMCache](https://github.com/LMCache/LMCache). LMCache caches LLM attention KV tensors; this package implements the durable shared tier via LMCache's `RemoteConnector` plugin contract.
44

5-
**Status:** Pre-implementation (design + executable plan only; no `src/` yet). Do not invent APIs or skip the plan's verification gates.
5+
**Status:** Phase 1 (`ConnectorAdapter` + `AerospikeRemoteConnector`) is implemented on `main`. Phase 2/3 remain design-only in `DESIGN.md`.
66

77
## Read order
88

99
1. **This file** — workflow, pitfalls, and verification expectations.
1010
2. `[DESIGN.md](DESIGN.md)` — authoritative contract: data model, config, error handling, phases.
11-
3. `[IMPLEMENTATION_PLAN.md](IMPLEMENTATION_PLAN.md)` — step-by-step build guide (S0–S16). **When implementing, the plan wins over stale design text** until S16 reconciles `DESIGN.md`.
11+
3. `[IMPLEMENTATION_PLAN.md](IMPLEMENTATION_PLAN.md)` — step-by-step build guide (S0–S16).
1212

13-
For Aerospike client/modeling rules, use the vetted skills in [aerospike/agent-skills](https://github.com/aerospike/agent-skills) (especially `[skills/aerospike-development/](https://github.com/aerospike/agent-skills/tree/main/skills/aerospike-development)`). Do not guess Aerospike APIs or namespace defaults.
13+
For Aerospike client/modeling rules, use [aerospike/agent-skills](https://github.com/aerospike/agent-skills) (especially `skills/aerospike-development/`). Do not guess Aerospike APIs or namespace defaults.
1414

1515
## Phases (scope discipline)
1616

17-
18-
| Phase | Surface | In this repo today |
19-
| ----- | ----------------------------------------------- | --------------------------------- |
20-
| 1 | `ConnectorAdapter` + `RemoteConnector` (Python) | **Only phase to implement now** |
21-
| 2 | `StoragePluginInterface`, `L2AdapterInterface` | Architectural in `DESIGN.md` only |
22-
| 3 | C++ `ConnectorBase` / `libaerospike` | Architectural in `DESIGN.md` only |
23-
17+
| Phase | Surface | In this repo today |
18+
| ----- | ------- | ------------------ |
19+
| 1 | `ConnectorAdapter` + `RemoteConnector` (Python) | **Implemented** |
20+
| 2 | `StoragePluginInterface`, `L2AdapterInterface` | Architectural in `DESIGN.md` only |
21+
| 3 | C++ `ConnectorBase` / `libaerospike` | Architectural in `DESIGN.md` only |
2422

2523
Stay inside Phase 1 unless the user explicitly expands scope.
2624

27-
## Implementation workflow
28-
29-
1. Start at **S0** in `IMPLEMENTATION_PLAN.md` and proceed **in order** (S0 → S16).
30-
2. **Stop if a step's verify gate fails** — fix before continuing; do not batch steps.
31-
3. Mark **⚠ DESIGN-CORRECTION** items as non-negotiable (they were verified against upstream LMCache `dev` and the real Aerospike Python client).
32-
4. After S16, reconcile `DESIGN.md` with the implementation (grep for `exists_many`, `post_init`, `shape0`, stale caps).
33-
34-
### Critical upstream facts (do not regress)
35-
36-
- `**post_init()` is never called** on remote connectors. Server record-size discovery runs in the connector constructor (`_ensure_limits`), not in `post_init`.
37-
- **Adapter has a no-arg `__init__`**. LMCache instantiates `AerospikeConnectorAdapter()` then calls `create_connector(context)`.
38-
- **Config/metadata** come from `context.local_cpu_backend` (`.config`, `.metadata`), with fallback to `context.config` / `context.metadata`.
39-
- **Connector is serde-agnostic.** `naive` / `cachegen` / `kivi` and MLA/layerwise key rewriting happen in `RemoteBackend` above the connector.
40-
- `**save_chunk_meta`:** when true, store one `md` bin (`RemoteMetadata.serialize()`); when false, use `self.meta_`* + `reshape_partial_chunk` on read (mirror `FSConnector`).
41-
- **Batch API:** `batch_read(keys, bins=...)` and `batch_write(BatchRecords([Write|Read|Remove, ...]))`. Do **not** use removed `exists_many` / `get_many` / `select_many`.
42-
- **TTL:** pin `aerospike>=14,<19` and centralize TTL in one helper (`meta={"ttl": N}`); namespace must have `nsup-period > 0` when using positive TTL.
43-
- **Default `target_segment_bytes` = 4 MiB** (LMCache byte-throughput sweet spot); clamp to server-discovered cap, not Aerospike's 1–10 KiB ops sweet spot.
44-
45-
### Package layout (target)
25+
## Package layout
4626

4727
```text
48-
src/lmcache_aerospike/
49-
adapter.py connector.py client.py config.py keys.py
50-
sharding.py limits.py serde.py policies.py errors.py metrics.py
51-
tests/unit/ tests/integration/ tests/bench/
52-
docker/docker-compose.yml docker/aerospike.conf
28+
src/lmcache_aerospike/ # published on PyPI
29+
tests/unit/ tests/integration/
30+
benchmarks/ # NOT published — ai-ecosystem-benchmark + micro harness
31+
docker/ scripts/
5332
```
5433

55-
## Verification (once code exists)
56-
34+
## Verification
5735

58-
| Scope | Command | Notes |
59-
| -------------- | ----------------------------------------------- | ------------------------------------------------------------ |
60-
| Preflight (S0) | `python scripts/preflight.py` | Confirms LMCache + Aerospike client symbols |
61-
| Unit | `pytest tests/unit -q` | No network; mock `aerospike.Client` |
62-
| Integration | `RUN_INTEGRATION=1 pytest tests/integration -q` | Requires `docker compose -f docker/docker-compose.yml up -d` |
63-
| Bench / vLLM | per S15 | Optional; gated by env vars |
36+
| Scope | Command | Notes |
37+
| ----- | ------- | ----- |
38+
| Preflight (S0) | `python scripts/preflight.py` | LMCache + Aerospike client symbols |
39+
| Unit | `pytest tests/unit -q` | No network |
40+
| Integration | `./scripts/start_aerospike_ce.sh` then `pytest tests/integration -q` | Live CE |
41+
| Ecosystem bench | `pip install -r benchmarks/requirements.txt` then `python benchmarks/run.py --profile smoke` | Not in CI by default |
42+
| Micro bench | `RUN_BENCH=1 pytest benchmarks/micro --benchmark-only` | FakeClient only |
6443

44+
Pinned versions: `IMPLEMENTATION_PLAN.md` §0.2.
6545

66-
Pinned versions are in `IMPLEMENTATION_PLAN.md` §0.2 — do not change without re-running S0.
46+
## Critical upstream facts (do not regress)
6747

68-
## Aerospike skills (external)
69-
70-
Before changing client usage, policies, TTL, batching, or record sizing, read the relevant reference from [agent-skills](https://github.com/aerospike/agent-skills/tree/main/skills/aerospike-development/references/). `DESIGN.md` §8.3 lists the ones this design depends on.
71-
72-
For local Docker CE setup in integration tests, see [aerospike-getting-started/SKILL.md](https://github.com/aerospike/agent-skills/blob/main/skills/aerospike-getting-started/SKILL.md).
73-
74-
## LMCache upstream (re-verify when unsure)
75-
76-
- `[RemoteConnector](https://github.com/LMCache/LMCache/blob/dev/lmcache/v1/storage_backend/connector/base_connector.py)`
77-
- `[ConnectorAdapter` / `ConnectorContext](https://github.com/LMCache/LMCache/blob/dev/lmcache/v1/storage_backend/connector/__init__.py)`
78-
- `[RemoteBackend](https://github.com/LMCache/LMCache/blob/dev/lmcache/v1/storage_backend/remote_backend.py)` — serde, `init_connection`, no `post_init`
79-
- `[FSConnector](https://github.com/LMCache/LMCache/blob/dev/lmcache/v1/storage_backend/connector/fs_connector.py)``save_chunk_meta` pattern
80-
- `[redis_connector.py](https://github.com/LMCache/LMCache/blob/dev/lmcache/v1/storage_backend/connector/redis_connector.py)``batched_contains` consecutive-prefix semantics
48+
- **`post_init()` is never called** on remote connectors. Discovery runs in `_ensure_limits()` during construction.
49+
- **Adapter has a no-arg `__init__`**. Config/metadata from `context.local_cpu_backend` (fallback `context.config` / `context.metadata`).
50+
- **Connector is serde-agnostic.** MLA/layerwise key rewriting happens in `RemoteBackend` above the connector.
51+
- **`save_chunk_meta`:** one `md` bin when true; `meta_*` + `reshape_partial_chunk` when false (mirror `FSConnector`).
52+
- **Batch API:** `batch_read` / `batch_write(BatchRecords(...))` — not `exists_many` / `get_many`.
53+
- **TTL:** `aerospike>=14,<19`, single `_put_meta` helper; `nsup-period > 0` for positive TTL.
54+
- **Default `target_segment_bytes` = 4 MiB** (LMCache byte-throughput); clamp to server cap.
8155

8256
## What not to do
8357

84-
- Do not implement Phase 2/3 features (pin/unpin L2, native C++, controller metadata, EE-only APIs) in Phase 1 code paths.
85-
- Do not create a new `aerospike.Client` per request; use `AerospikeClientHolder` ref-counting.
86-
- Do not use CDTs or secondary indexes for chunk payload storage.
87-
- Do not hardcode 8 MiB record caps; discover `max-record-size` / `write-block-size` at construction.
58+
- Do not implement Phase 2/3 features in Phase 1 paths.
59+
- Do not add benchmark-only deps to `pyproject.toml` `[project]` dependencies (use `benchmarks/requirements.txt`).
60+
- Do not create a new `aerospike.Client` per request; use `AerospikeClientHolder`.
61+
- Do not hardcode record-size caps; discover at construction.
8862
- Do not commit unless the user asks.
89-
90-
## Commits and PRs
91-
92-
Follow repository commit style from `git log`. Summarize *why* in commit messages. For PRs, include unit-test results and note whether integration tests were run.

IMPLEMENTATION_PLAN.md

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1288,24 +1288,23 @@ each is updated).
12881288

12891289
## Final acceptance checklist (Phase 1 "done")
12901290

1291-
Mirrors DESIGN §1.5, with corrections. All must be true:
1291+
Mirrors DESIGN §1.5, with corrections.
12921292

1293-
- [ ] `pip install -e ".[dev]"` works on Python 3.10–3.13; `import lmcache_aerospike` clean.
1294-
- [ ] `pytest tests/unit -q` fully green (no network).
1295-
- [ ] Discovery runs at construction; bad namespace/`nsup-period 0`/out-of-range
1293+
- [x] `pip install -e ".[dev]"` works on Python 3.10–3.13; `import lmcache_aerospike` clean.
1294+
- [x] `pytest tests/unit -q` fully green (no network).
1295+
- [x] Discovery runs at construction; bad namespace/`nsup-period 0`/out-of-range
12961296
cap fail fast with the typed errors (proven by integration tests).
1297-
- [ ] Round-trip byte-exact for `256B,64KiB,1MiB,4MiB,16MiB,64MiB` with correct
1298-
`nseg` for the discovered cap.
1299-
- [ ] `batched_contains` returns the consecutive-prefix count
1300-
(`[T,T,F,T] -> 2`), matching the Redis connector's semantics.
1301-
- [ ] TTL expiry and pinned (`-1`) behavior verified on a real CE node.
1302-
- [ ] No use of `exists_many`/`get_many`/`select_many`; batch ops use
1297+
- [x] Round-trip byte-exact for CI sizes (512B, 64KiB); larger sizes via
1298+
`RUN_LARGE_INTEGRATION=1` locally (`1MiB`–`64MiB`).
1299+
- [x] `batched_contains` consecutive-prefix semantics (unit + integration coverage).
1300+
- [x] TTL expiry and pinned (`-1`) behavior verified on a real CE node.
1301+
- [x] No use of `exists_many`/`get_many`/`select_many`; batch ops use
13031302
`batch_read`/`batch_write` + `BatchRecords`.
1304-
- [ ] TTL set in exactly one helper; client version pinned.
1305-
- [ ] `save_chunk_meta` honored (layerwise/MLA round-trips).
1306-
- [ ] vLLM smoke shows cache reuse across a worker restart (if run).
1307-
- [ ] Bench harness reports p50/p95/p99 + bytes/s and the segment-size decision.
1308-
- [ ] `DESIGN.md` reconciled with this plan.
1303+
- [x] TTL set in exactly one helper; client version pinned.
1304+
- [x] `save_chunk_meta` honored; layerwise `LayerCacheEngineKey` integration smoke.
1305+
- [ ] vLLM smoke shows cache reuse across a worker restart (optional; `RUN_VLLM=1`).
1306+
- [x] Bench harness: `benchmarks/run.py` (ecosystem) + `benchmarks/micro/` (pytest-benchmark).
1307+
- [x] `DESIGN.md` reconciled with this plan (v0.2 status on `main`).
13091308

13101309
## Appendix A — Upstream facts this plan relies on (so you can re-verify)
13111310

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,17 @@ Large payloads (16–64 MiB): `RUN_LARGE_INTEGRATION=1`.
8585

8686
## Benchmarks
8787

88+
Benchmark code lives under **`benchmarks/`** (not included in the PyPI wheel).
89+
8890
```bash
89-
RUN_BENCH=1 pytest tests/bench -v --benchmark-only
91+
pip install -r benchmarks/requirements.txt
92+
./scripts/start_aerospike_ce.sh && source .aerospike-ci.env
93+
python benchmarks/run.py --profile smoke
9094
```
9195

92-
See [`tests/bench/README.md`](tests/bench/README.md).
96+
Micro (FakeClient, no server): `RUN_BENCH=1 pytest benchmarks/micro --benchmark-only`
97+
98+
See [`benchmarks/README.md`](benchmarks/README.md).
9399

94100
## License
95101

benchmarks/README.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Benchmarks
2+
3+
Harnesses for **lmcache-aerospike** live under `benchmarks/` and are **not** shipped on PyPI
4+
(only `src/lmcache_aerospike/` is packaged). Deps with VCS URLs live in
5+
`benchmarks/requirements.txt` for the same reason as [adk-aerospike](https://github.com/aerospike-community/adk-aerospike).
6+
7+
| Harness | Runner | Use when |
8+
|---|---|---|
9+
| **Ecosystem** (`run.py`) | [ai-ecosystem-benchmark](https://github.com/aerospike-community/ai-ecosystem-benchmark) | Coordinated-omission-safe QPS/latency on a live Aerospike CE node |
10+
| **Micro** (`micro/`) | pytest-benchmark + FakeClient | Fast, no-server connector overhead / segment-size sweep |
11+
12+
## Ecosystem harness (recommended)
13+
14+
### Install
15+
16+
```bash
17+
pip install -e .
18+
pip install -r benchmarks/requirements.txt
19+
./scripts/start_aerospike_ce.sh
20+
set -a && source .aerospike-ci.env && set +a
21+
```
22+
23+
### Run
24+
25+
```bash
26+
python benchmarks/run.py --list-profiles
27+
python benchmarks/run.py --list-workloads
28+
python benchmarks/run.py --profile smoke
29+
python benchmarks/run.py --profile kv_chunk_smoke --results-dir benchmarks/results
30+
```
31+
32+
Connection URI format (override with `--uri`):
33+
34+
```text
35+
aerospike://127.0.0.1:3000/lmcache?set=bench_eco_kv&num_tokens=128&target_segment_bytes=4194304
36+
```
37+
38+
### Workloads
39+
40+
| Workload | Tests | Intent |
41+
|---|---|---|
42+
| `kv_hotpath` | `aerospike_kv_put`, `get_hit`, `get_miss`, `exists` | Steady KV remote-cache loop |
43+
| `kv_chunk` | `aerospike_kv_put_large`, `get_large` | ~4 MiB payload (multi-segment) |
44+
45+
### Profiles
46+
47+
| Profile | Workload | Notes |
48+
|---|---|---|
49+
| `smoke` | `kv_hotpath` | Laptop sanity, ~5 s per test |
50+
| `kv_chunk_smoke` | `kv_chunk` | 4 MiB target segment band |
51+
52+
## Micro harness (no Aerospike)
53+
54+
Uses `FakeClient` from unit tests — not the ecosystem runner.
55+
56+
```bash
57+
pip install -r benchmarks/requirements.txt
58+
RUN_BENCH=1 pytest benchmarks/micro -v --benchmark-only
59+
```
60+
61+
See [micro/README.md](micro/README.md).
62+
63+
## Data isolation
64+
65+
Workloads default to `bench_eco_*` sets under the `lmcache` namespace. They do not use
66+
`it_chunks` integration-test data. Tear down by deleting the bench sets or the namespace prefix.

benchmarks/micro/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Micro benchmarks (FakeClient)
2+
3+
In-process pytest-benchmark tests — no Aerospike server.
4+
5+
```bash
6+
pip install -r benchmarks/requirements.txt
7+
RUN_BENCH=1 pytest benchmarks/micro -v --benchmark-only
8+
```
9+
10+
Optional live spot-check (Aerospike CE up):
11+
12+
```bash
13+
RUN_BENCH=1 RUN_BENCH_LIVE=1 pytest benchmarks/micro -v -k live --benchmark-only
14+
```
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Fixtures for in-process connector benchmarks (FakeClient, no Aerospike)."""
1+
"""Fixtures for in-process connector micro-benchmarks (FakeClient)."""
22

33
from __future__ import annotations
44

@@ -45,7 +45,6 @@ def _resolved(target_segment_bytes: int) -> ResolvedLimits:
4545

4646
@pytest.fixture
4747
def bench_connector(fake_client: FakeClient, request):
48-
"""Connector with configurable target_segment_bytes via indirect param."""
4948
target = getattr(request, "param", 4 * 1024 * 1024)
5049
resolved = _resolved(target)
5150
backend = FakeLocalCPUBackend(alloc_size=8 * 1024 * 1024)
Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,4 @@
1-
"""pytest-benchmark harness for connector put/get (S15).
2-
3-
Uses FakeClient (no network). Run:
4-
5-
RUN_BENCH=1 pytest tests/bench -v --benchmark-only
6-
7-
Optional live Aerospike:
8-
9-
RUN_BENCH=1 RUN_BENCH_LIVE=1 pytest tests/bench -v -k live
10-
"""
1+
"""pytest-benchmark micro harness (no Aerospike). See benchmarks/micro/README.md."""
112

123
from __future__ import annotations
134

@@ -18,7 +9,7 @@
189

1910
pytestmark = pytest.mark.skipif(
2011
os.environ.get("RUN_BENCH") != "1",
21-
reason="Set RUN_BENCH=1 to run performance benchmarks",
12+
reason="Set RUN_BENCH=1 to run micro benchmarks",
2213
)
2314

2415
LIVE = os.environ.get("RUN_BENCH_LIVE") == "1"
@@ -60,7 +51,6 @@ def run():
6051

6152

6253
def test_segment_size_sweep(benchmark, segment_sweep_connector):
63-
"""Sensitivity check across target_segment_bytes (1–8 MiB)."""
6454
conn, key, mo, target, payload_size, _loop = segment_sweep_connector
6555
conn._put_sync_impl(key, mo)
6656

@@ -78,7 +68,8 @@ def run():
7868

7969
@pytest.mark.skipif(not LIVE, reason="Set RUN_BENCH_LIVE=1 with Aerospike CE up")
8070
def test_live_put_get(benchmark):
81-
"""Optional bench against scripts/start_aerospike_ce.sh (not run in CI)."""
71+
import asyncio
72+
8273
from tests.integration.helpers import (
8374
build_connector,
8475
chunk_byte_size,
@@ -88,8 +79,6 @@ def test_live_put_get(benchmark):
8879
payload_pattern,
8980
)
9081

91-
import asyncio
92-
9382
conn, backend, _, _ = build_connector(num_tokens=128)
9483
try:
9584
chunk_len = chunk_byte_size(backend.metadata)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"description": "4 MiB target payload put/get (multi-segment on most CE nodes).",
3+
"workload": "kv_chunk",
4+
"connection_string": "aerospike://127.0.0.1:3000/lmcache?set=bench_eco_kv&target_segment_bytes=4194304",
5+
"queries_per_second": 8,
6+
"scheduler_thread_count": 1,
7+
"worker_thread_count": 2,
8+
"runtime_per_function": 5,
9+
"workload_params": {
10+
"target_payload_bytes": 4194304
11+
}
12+
}

benchmarks/profiles/smoke.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"description": "Quick KV hot-path sanity (~30s). Requires Aerospike CE (start_aerospike_ce.sh).",
3+
"workload": "kv_hotpath",
4+
"connection_string": "aerospike://127.0.0.1:3000/lmcache?set=bench_eco_kv&num_tokens=128",
5+
"queries_per_second": 20,
6+
"scheduler_thread_count": 1,
7+
"worker_thread_count": 4,
8+
"runtime_per_function": 5,
9+
"workload_params": {
10+
"key_count": 16
11+
}
12+
}

benchmarks/requirements.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Ecosystem harness (benchmarks/run.py). Not part of the PyPI wheel.
2+
# Install package first: pip install -e .
3+
git+https://github.com/aerospike-community/ai-ecosystem-benchmark.git
4+
pytest>=7
5+
pytest-benchmark>=4

0 commit comments

Comments
 (0)