You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
| Integration |`./scripts/ci_integration_install.sh` then `./scripts/start_aerospike_ce.sh` and `pytest tests/integration -q`| Live CE + LMCache `dev` for L2 E2E (mirrors CI)|
40
+
| Integration |`./scripts/start_aerospike_ce.sh` then `pytest tests/integration -q`| Live CE |
41
41
| Ecosystem bench |`pip install -r benchmarks/requirements.txt` then `python benchmarks/run.py --profile smoke`| Not in CI by default |
42
-
| L2 bench |`./scripts/setup_l2_bench.sh` then `./benchmarks/l2/run.sh` (LMCache `dev` + live CE) | Not in CI by default |
42
+
| L2 bench |`./scripts/setup_l2_bench.sh` (builds `.deps/` C client + `_native`); `./benchmarks/l2/compare.sh`| Not in CI by default |
43
+
| Native integration |`RUN_NATIVE=1 pytest tests/integration/test_native_connector_e2e.py -q`| After `build_libaerospike.sh` + CE |
43
44
| Micro bench |`RUN_BENCH=1 pytest benchmarks/micro --benchmark-only`| FakeClient only |
Copy file name to clipboardExpand all lines: DESIGN.md
+29-15Lines changed: 29 additions & 15 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,16 +1,17 @@
1
1
# LMCache Aerospike Backend - Design Document
2
2
3
-
**Status:** v0.2 — Phase 1 implemented (see `main` and `IMPLEMENTATION_PLAN.md`)
3
+
**Status:** v0.3 — Phase 1 and Phase 2 implemented; Phase 3 native connector in progress (see `main` and `IMPLEMENTATION_PLAN.md`)
4
4
**Audience:** Engineers implementing and reviewing an Aerospike storage backend for [LMCache](https://github.com/LMCache/LMCache).
5
5
**Scope:** A multi-phase plan delivering an Aerospike-backed remote KV-cache tier for LMCache, anchored to LMCache's `RemoteConnector` plugin contract and a CE-only, adaptive-sharded Aerospike data model tuned for ~4 MiB chunks.
6
6
7
-
> **v0.2 reconciliation (verified against upstream LMCache `dev`and the official Aerospike Python client).** This revision corrects the design against the actual contracts before implementation. The companion build guide is `IMPLEMENTATION_PLAN.md`. Key changes:
7
+
> **v0.3 reconciliation (verified against upstream LMCache `dev`, LMCache native RESP, and the official Aerospike clients).** This revision corrects the design against the actual contracts before implementation. The companion build guide is `IMPLEMENTATION_PLAN.md`. Key changes:
8
8
> 1.**Server-side limit discovery runs at connector construction, not `post_init()`** — upstream `RemoteBackend` never calls `post_init()` on a remote connector ([Section 4.3.6](#436-server-side-record-size-discovery), [Section 4.4.0](#440-construction-time-limit-discovery-_ensure_limits-and-the-post_init-override)).
9
9
> 2.**Batch API corrected** to `batch_write(BatchRecords([...]))` and `batch_read(keys, [])`; the removed `exists_many`/`get_many`/`select_many` helpers are not used ([Section 4.4.4](#444-async-def-putself-key-cacheenginekey-memory_obj-memoryobj), [Section 4.4.7](#447-def-support_batched_containsself---bool---true-and-def-batched_containsself-keys-listcacheenginekey---int)).
10
10
> 3.**Metadata is one serialized `RemoteMetadata` blob (`md` bin), gated on `save_chunk_meta`**, mirroring `FSConnector` — replacing the rigid `shape0..shape3`/`dtype`/`fmt` bins; reads allocate accordingly ([Section 4.3.1](#431-meta-record), [Section 4.4.3](#443-async-def-getself-key-cacheenginekey---optionalmemoryobj)).
11
11
> 4.**The connector is serde-agnostic**; `naive`/`cachegen`/`kivi` serde and MLA/layerwise key rewriting happen in `RemoteBackend` above it ([Section 4.4](#44-method-by-method-implementation-spec)).
12
12
> 5.**Aerospike client pinned to `>=14,<19`** because `meta={"ttl": N}` is deprecated from `19.1.0`; per-record cap is server-governed (7.1+ `max-record-size` default 1 MiB), and the ops sweet spot is restated as **1-10 KiB** ([Section 2.2](#22-aerospike-just-enough-for-this-design), [Section 4.1](#41-package-layout)).
13
13
> 6.**4 MiB `target_segment_bytes` retained** and now cited to the LMCache paper ([arXiv:2510.09665](https://arxiv.org/abs/2510.09665)); the Aerospike ops sweet spot and the LMCache byte-throughput sweet spot are explicitly distinguished ([Section 4.3.4](#434-adaptive-shard-planner)).
14
+
> 7.**Phase 3 follows Redis' native mechanics, not its schema by default**: C++ workers, GIL-free pybind submissions, eventfd completions, and direct buffer copies are adopted immediately, while the Phase 1/2 meta+segment schema remains the first native layout. A raw Redis-like schema is reserved for a benchmark-proven follow-up, either as a separate native mode or as a coordinated migration of Phase 1 and Phase 2.
|`phase 3: native C++ connector`|`ConnectorBase`-style C++/pybind11 against `libaerospike` via LMCache `native_plugin`| Implementation in progress|
67
68
68
69
69
70
### 1.5 Success criteria for Phase 1
@@ -111,7 +112,7 @@ This is the road map for the rest of the doc:
111
112
|`ConnectorAdapter` + `RemoteConnector` (Python, single-process worker) |`aerospike` Python client wrapped behind `loop.run_in_executor`; adaptive sharded data model | 1 |
112
113
|`StoragePluginInterface` (Python, full backend, non-multiprocess) | Same data model; takes ownership of `LocalCPUBackend` interactions for richer admission control | 2 |
113
114
|`L2AdapterInterface` (Python `plugin` and `native_plugin`, multiprocess) | Python L2 wraps Phase 1/2; `native_plugin` exposes a C++ adapter with `eventfd` completions | 2 / 3 |
114
-
| C++ `ConnectorBase` (highest throughput, RESP-style) | pybind11 binding over `libaerospike` with `as_event_loop` and zero-copy buffers | 3 |
@@ -791,22 +792,34 @@ Until any of these triggers, Phase 1 is the recommended path and Phase 2 stays a
791
792
792
793
---
793
794
794
-
## 6. Phase 3 - Native C++ connector (architectural)
795
+
## 6. Phase 3 - Native C++ connector (implementation-ready direction)
795
796
796
-
Phase 3 replaces the Python connector hot path with a C++ implementation modeled after LMCache's native RESP connector ([`resp_client.py`](https://github.com/LMCache/LMCache/blob/dev/lmcache/v1/storage_backend/native_clients/resp_client.py)and the `ConnectorBase` base class it pairs with).
797
+
Phase 3 replaces the Python L2 hot path with a C++ implementation modeled after LMCache's native RESP connector ([`resp_client.py`](https://github.com/LMCache/LMCache/blob/dev/lmcache/v1/storage_backend/native_clients/resp_client.py), `NativeConnectorL2Adapter`, and the `ConnectorBase` protocol it pairs with). Redis' winning techniques are the native mechanics: C++ worker tiling, GIL-free pybind submissions, one eventfd-backed completion stream, and direct copies into LMCache-provided buffers. Phase 3 adopts those techniques first while preserving the Phase 1/2 Aerospike schema.
797
798
798
799
### 6.1 Why we want it
799
800
800
801
The Python connector ceiling isset by GIL contention on the executor pool, copy overhead through `memoryview`, and the synchronous Aerospike Python client's per-call C extension setup. For sustained multi-GB/s per worker (Llama 70B class at large TP, or multiple concurrent inference requests), this overhead becomes the bottleneck. A native connector closes that gap.
801
802
802
803
### 6.2 Design sketch
803
804
804
-
-**Language and bindings.** C++17 implementation; pybind11 binding exposed as`lmcache_aerospike._native`and registered through the LMCache `native_plugin` slot.
805
-
-**Client.** Official Aerospike C client (`libaerospike`) using `as_event_loop`for asynchronous operations.
806
-
-**Threading.** A dedicated event-loop thread (or thread pool tuned to NIC queues); the Python side never holds the GIL during fetch.
807
-
-**Buffers.** Zero-copy: writes consume LMCache-supplied `void*` buffers directly; reads write into LMCache-supplied buffers without an intermediate `bytes` copy.
808
-
-**Completion model.**`eventfd`-based; matches the existing LMCache `native_plugin` completion contract.
809
-
-**Data model.** Identical to Phase 1 (meta + segments). The native connector reads records written by Phase 1/2and vice versa.
805
+
-**Language and bindings.** C++17 implementation; pybind11 binding exposed as`lmcache_aerospike._native`and loaded through LMCache's `native_plugin` L2 adapter.
-**Client.** Official Aerospike C client (`libaerospike`) with one shared cluster client per native connector instance; workers issue key operations against that client with read/write policies matching Phase 1/2 defaults.
808
+
-**Threading.** Use the same worker tiling model as LMCache Redis' native connector: each submitted batch is split across C++ worker threads, and one completion is emitted when all tiles finish. The Python side never holds the GIL after pybind has extracted key strings and memoryview pointers.
809
+
-**Buffers.** Writes wrap LMCache-supplied buffers with Aerospike C client bytes values where the API allows; reads copy Aerospike bytes directly into LMCache's preallocated `MemoryObj` buffers without a Python `bytes` hop.
810
+
-**Completion model.** Eventfd-based, with per-key result bits for lookup/load/delete and one completion per submitted batch.
811
+
-**Data model.** Default native layout is the Phase 1/2 meta+segment schema: inline payload bin`b`for single-record objects, segment records for larger objects, and meta-last publish semantics with`state`, `nseg`, `seg_b`, and`tot_b`. Because L2 loads are preallocated, native code does not need to add new LMCache shape/dtype metadata; it only preserves the existing bins required for compatibility and sharding correctness.
812
+
813
+
### 6.2.1 Schema evolution policy
814
+
815
+
Phase 3 does **not** begin by switching to a Redis-like raw one-record schema. That schema can reduce bins and branching, but it would break compatibility with Phase 1/2 records unless every Aerospike path migrates together.
816
+
817
+
The allowed future paths are:
818
+
819
+
1. **Separate raw native mode:** keep Phase 1/2 compatible schema as the default, and add an opt-in raw native schema if benchmarks prove schema overhead is a top bottleneck.
820
+
2. **Coordinated schema migration:** change Phase 1, Phase 2, and Phase 3 to the faster schema together, with explicit migration or dual-read support.
821
+
822
+
Do not make a schema-breaking change on guesswork. The benchmark loop must first show that the compatible schema, rather than Python overhead, Aerospike policy choices, worker count, network/device bandwidth, or batch shape, is one of the top bottlenecks.
810
823
811
824
### 6.3 Build and distribution
812
825
@@ -821,6 +834,7 @@ The Python connector ceiling is set by GIL contention on the executor pool, copy
821
834
-**Build matrix cost.** manylinux wheels, ABI compatibility across `libaerospike` releases, debug story (gdb on the native side, py-spy on the Python side, correlating them).
822
835
-**Upstream tracking.** LMCache's `ConnectorBase` is the youngest surface; tracking changes will be ongoing work.
823
836
-**Operational surface.** Customers debugging will need both Python and C++ familiarity.
837
+
-**Schema pressure.** Preserving Phase 1/2 schema may leave some performance on the table versus Redis' raw key/value storage. Treat this as a measured optimization decision, not a Phase 3 prerequisite.
0 commit comments