-
Notifications
You must be signed in to change notification settings - Fork 155
feat(radix-index): shared radix membership index service (WIP experiment) #2394
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
0810020
5d17b9f
086dac8
cb39dca
ed0e1a5
5d55070
e823316
458ed3a
010aaca
cb79109
ba9561a
6c5786b
f427b42
c942850
5b6b6b7
c843813
3e20f2f
f22b312
bdf51f4
9f189b9
eb6a53b
ec8de8c
a84c635
0beaa02
b2ea503
1e4f5f1
be4fd90
8d4a0a9
2083d94
a8ef003
36dbcbf
7d987af
e0850ea
0a82bfd
0005ef1
7fb9a8a
caf68a6
73c3411
357c640
1d48ffc
71324f1
b2515a8
b601194
609fb50
419ef0a
c43700a
286fc0d
94a1ce9
5c49989
28a11c1
81158b7
5eb4625
46cfe0e
cfd0b25
991d6f6
a52a639
61f1046
7aeefb2
e9268a8
b416637
e95cf67
87f0689
b88b9aa
79d36ad
50f14c8
41b2020
d05f720
f0585dc
ef6450d
b9ec522
a228b4f
a559a2c
2e84db8
687a441
90f13a9
9295a7c
6185b95
833ba14
3ad8e00
afb1dc7
885c4bb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| [package] | ||
| name = "radix-index" | ||
| version = "0.1.0" | ||
| edition = "2021" | ||
| license = "Apache-2.0" | ||
| description = "Generic radix membership index service: block-quantized prefix chains, per-holder overlap scoring, pub in / sub out" | ||
|
|
||
| [lib] | ||
| name = "radix_index" | ||
| path = "src/lib.rs" | ||
|
|
||
| [[bin]] | ||
| name = "radix-index-service" | ||
| path = "src/bin/service.rs" | ||
|
|
||
| [[bin]] | ||
| name = "radix-index-bridge" | ||
| path = "src/bin/bridge.rs" | ||
|
|
||
| [[bin]] | ||
| name = "radix-index-bench" | ||
| path = "src/bin/bench.rs" | ||
|
|
||
| [[bin]] | ||
| name = "radix-index-loadbench" | ||
| path = "src/bin/loadbench.rs" | ||
|
|
||
| [dependencies] | ||
| radix-tree = { path = "../radix_tree" } | ||
| xxhash-rust = { version = "0.8", features = ["xxh3"] } | ||
| smg-grpc-client = { path = "../grpc_client" } | ||
| tonic.workspace = true | ||
| tonic-prost.workspace = true | ||
| prost.workspace = true | ||
| tokio = { workspace = true, features = ["full"] } | ||
| tokio-stream = "0.1" | ||
| futures.workspace = true | ||
| tracing.workspace = true | ||
| tracing-subscriber = { workspace = true } | ||
| rustc-hash = "2" | ||
|
|
||
| [dev-dependencies] | ||
| kv-index = { path = "../kv_index" } | ||
| rand = { workspace = true } | ||
| mock-worker = { path = "../mock_worker" } | ||
| portpicker = "0.1" | ||
|
|
||
| [build-dependencies] | ||
| tonic-prost-build = "0.14.6" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| # radix-index | ||
|
|
||
| A shared radix membership index for cache-aware routing: gateways ask | ||
| "which worker already holds the longest prefix of this request?" without | ||
| each gateway building and syncing its own tree. | ||
|
|
||
| The data structure is SMG `kv_index`'s block-quantized positional | ||
| index (a direct import — whether the service should instead get a | ||
| ground-up generic radix tree is an open evaluation). This crate wraps | ||
| it in a keyspace-partitioned engine, a gRPC surface, and the | ||
| client/bridge pieces that feed and query it. | ||
|
|
||
| ## Interface: two verbs | ||
|
|
||
| - **Publish** (client-streamed `Update`s, acked): a holder's block-hash | ||
| chain changed. Two feeds share the verb: | ||
| - **Event feed** — a bridge subscribes to engine KV events | ||
| (`SubscribeKvEvents`) and forwards Stored/Removed/Cleared batches, | ||
| sequenced per holder with epoch bumps on gap or backend loss. | ||
| Eviction is *observed*, so index state tracks engine truth. | ||
| - **Placement feed** — a gateway publishes "this request's chain now | ||
| (probably) resides on that worker" after each completed request | ||
| (`seq=0`, content-idempotent). No engine cooperation needed — this | ||
| is the path for engines/modes with no KV event stream. Inferred | ||
| state is bounded by idle TTL + per-holder capacity with tail-first | ||
| (prefix-closed) eviction. | ||
| The first sequenced or Removed-bearing update marks a holder | ||
| *event-fed*: placements for it are ignored from then on, so the | ||
| precise feed always wins. | ||
| - **Subscribe** (bidirectional query stream): content-hash chain in, | ||
| per-holder matched-block counts out. The gateway enforces its own | ||
| deadline (2 ms in SMG) and falls back to its local policy on a miss — | ||
| answers are advisory, never load-bearing for correctness. | ||
|
|
||
| `Pull` streams the whole state as synthetic `Update`s; a starting | ||
| replica bootstraps from a sibling with it before declaring ready. | ||
|
|
||
| ## Replication: copy, don't agree | ||
|
|
||
| No consensus. Writes are per-holder sequenced (event feed) or | ||
| content-idempotent (placement feed), so replicas converge by applying | ||
| the same updates in any interleaving. A replica relays each accepted | ||
| `Publish` to its `--peers` best-effort; a wedged peer drops relayed | ||
| updates rather than wedging ingest, and TTL plus re-placement plus | ||
| bootstrap bound the divergence. Gateways stay stupid: one endpoint, no | ||
| fan-out, reconnect on failure. | ||
|
|
||
| ## Keyspaces | ||
|
|
||
| State is partitioned by `(model, symbol_kind, block_size)`. TOKENS at | ||
| the engine page size is what SMG uses today; BYTES exists for text-mode | ||
| (HTTP) feeds that hash normalized bytes instead of token ids. | ||
|
|
||
| ## Binaries | ||
|
|
||
| - `radix-index-service` — the server. Flags: | ||
|
|
||
| | flag | default | meaning | | ||
| |---|---|---| | ||
| | `--bind` | `127.0.0.1` | listen address (set `0.0.0.0` in k8s) | | ||
| | `--port` | `40000` | gRPC port | | ||
| | `--metrics-port` | off | admin plane: `/metrics`, `/healthz`, `/readyz` | | ||
| | `--peers` | none | sibling replicas to relay Publishes to (comma-separated URLs) | | ||
| | `--bootstrap-from` | none | sibling to Pull state from before serving | | ||
| | `--inferred-ttl-secs` | `180` | idle TTL for placement-fed holders | | ||
| | `--event-ttl-secs` | `1800` | liveness backstop for EVENT-fed holders: silence past this soft-retires the holder (a lost departure signal must not leak it); `0` disables | | ||
| | `--default-capacity-blocks` | unbounded | RUNAWAY PROTECTION for holders that never sent `Added` — the index truncates only past 2x this value. Leave unbounded or set well above worker KV size: the placement feed carries no removal signal, so an index that races the worker's own eviction under-matches (measured); idle TTL is the freshness bound. | | ||
| | `--sweep-interval-secs` | `5` | idle-sweep cadence | | ||
| | `--apply-delay-stored-ms` / `--apply-delay-removed-ms` | `0` | staleness injection (experiments only) | | ||
|
|
||
|
Comment on lines
+64
to
+70
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Nit: The flag table is missing
Similarly, |
||
| Stops gracefully on SIGTERM/ctrl-c. `/readyz` answers 503 until the | ||
| bootstrap pull completes — point the k8s readiness probe at it. | ||
|
|
||
| - `radix-index-bridge` — per-fleet event bridge: engine KV event | ||
| streams in, index Updates out. Flags: `--workers` (comma-separated | ||
| worker URLs), `--index` (service URL), `--model`, `--block-size` | ||
| (MUST match the gateway's `--kv-indexer-block-size` — the keyspace | ||
| key includes it, and a mismatch silently splits the fleet's state | ||
| into two keyspaces; both default to the same shared value). | ||
| - `radix-index-bench` — apply/query throughput and memory-per-entry | ||
| microbench. | ||
|
|
||
| A reference StatefulSet lives in [`deploy/statefulset.yaml`](deploy/statefulset.yaml). | ||
|
|
||
| ## Gateway side (SMG) | ||
|
|
||
| `--kv-indexer-url` + `--kv-indexer-block-size` on the gateway enable | ||
| the remote path: a routing-time overlap prefetch (2 ms deadline, | ||
| fast-fail while disconnected) feeding the cache-aware policy, and a | ||
| placement publish of the prompt⊕output chain after each completed | ||
| request. Flag off = every code path byte-identical to local behavior. | ||
|
|
||
| ## Metrics | ||
|
|
||
| `/metrics` (Prometheus text): `radix_index_{keyspaces,holders,event_fed_holders,dropped_holders,blocks}` | ||
| gauges and `radix_index_{applies,queries,relay_dropped}_total` counters. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| fn main() -> Result<(), Box<dyn std::error::Error>> { | ||
| println!("cargo:rerun-if-changed=proto/radix_index.proto"); | ||
| tonic_prost_build::configure() | ||
| .build_server(true) | ||
| .build_client(true) | ||
| .protoc_arg("--experimental_allow_proto3_optional") | ||
| .compile_protos(&["proto/radix_index.proto"], &["proto"])?; | ||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Nit: This paragraph describes the pre-rewrite state and contradicts the crate's actual dependency — worth fixing in the same doc-hygiene pass as the
R3-DESIGN/VERIFICATIONreference cleanup.Cargo.toml:29hasradix-tree = { path = "../radix_tree" }as a real dependency, andkv_indexis only a dev-dependency test oracle. So "the data structure is SMGkv_index's block-quantized positional index (a direct import)" is no longer true, and "whether the service should instead get a ground-up generic radix tree is an open evaluation" describes a question this PR answered — the ground-up tree is what ships.This is a reader's first paragraph on the service crate, and it currently points them at the wrong data structure.