-
Notifications
You must be signed in to change notification settings - Fork 165
feat(radix-tree): chain-native prefix-membership index #2436
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,44 @@ | ||||||||||||||||||||||||||||||
| name: radix-tree fuzz campaign | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # The always-on `fuzz_quick` in the unit-test lane is a triage gate (8+3 | ||||||||||||||||||||||||||||||
| # seeds, debug build). The differential campaign — seeded runs against | ||||||||||||||||||||||||||||||
| # the reference model, every 4th at the wide H<=64 sharing width, plus | ||||||||||||||||||||||||||||||
| # the out-of-contract chaos runs — is release-only and runs here nightly | ||||||||||||||||||||||||||||||
| # so the data structure's correctness gate does not depend on someone | ||||||||||||||||||||||||||||||
| # remembering to run it by hand. Budget: 1000 seeds took 52 min in | ||||||||||||||||||||||||||||||
| # release on an M-series laptop; 600 fits a 2x slower hosted runner | ||||||||||||||||||||||||||||||
| # inside the 120-minute job limit with headroom. Dispatch for more. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| on: | ||||||||||||||||||||||||||||||
| schedule: | ||||||||||||||||||||||||||||||
| - cron: "17 6 * * *" | ||||||||||||||||||||||||||||||
| workflow_dispatch: | ||||||||||||||||||||||||||||||
| inputs: | ||||||||||||||||||||||||||||||
| seeds: | ||||||||||||||||||||||||||||||
| description: "RADIX_FUZZ_SEEDS" | ||||||||||||||||||||||||||||||
| default: "600" | ||||||||||||||||||||||||||||||
| start: | ||||||||||||||||||||||||||||||
| description: "RADIX_FUZZ_START" | ||||||||||||||||||||||||||||||
| default: "1000" | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| concurrency: | ||||||||||||||||||||||||||||||
| group: radix-tree-fuzz | ||||||||||||||||||||||||||||||
| cancel-in-progress: true | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| jobs: | ||||||||||||||||||||||||||||||
| campaign: | ||||||||||||||||||||||||||||||
| runs-on: ubuntu-latest | ||||||||||||||||||||||||||||||
| timeout-minutes: 120 | ||||||||||||||||||||||||||||||
| steps: | ||||||||||||||||||||||||||||||
| - uses: actions/checkout@v7 | ||||||||||||||||||||||||||||||
|
Comment on lines
+29
to
+33
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. 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win Set read-only token permissions and disable checkout credential persistence. The job runs repository code after 🔒️ Proposed fix jobs:
campaign:
runs-on: ubuntu-latest
timeout-minutes: 120
+ permissions:
+ contents: read
steps:
- uses: actions/checkout@v7
+ with:
+ persist-credentials: false📝 Committable suggestion
Suggested change
🧰 Tools🪛 zizmor (1.29.0)[warning] 33-33: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false (artipacked) [warning] 29-45: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block (excessive-permissions) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| - uses: dtolnay/rust-toolchain@stable | ||||||||||||||||||||||||||||||
| - uses: Swatinem/rust-cache@v2 | ||||||||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||||||||
| key: radix-tree-fuzz | ||||||||||||||||||||||||||||||
| - name: Run the differential campaign (release) | ||||||||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||||||||
| RADIX_FUZZ_SEEDS: ${{ github.event.inputs.seeds || '600' }} | ||||||||||||||||||||||||||||||
| RADIX_FUZZ_START: ${{ github.event.inputs.start || '1000' }} | ||||||||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||||||||
| cargo test -p smg-radix-tree --release --test fuzz_differential \ | ||||||||||||||||||||||||||||||
| -- --ignored --nocapture fuzz_campaign | ||||||||||||||||||||||||||||||
|
Comment on lines
+42
to
+44
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: That's the same failure mode
Suggested change
( |
||||||||||||||||||||||||||||||
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,31 @@ | ||
| [package] | ||
| name = "smg-radix-tree" | ||
| version = "0.1.0" | ||
| edition = "2021" | ||
| description = "Generic prefix-membership index: chain-native radix tree answering which holders share the longest prefix of a block chain, and how deep" | ||
| license = "Apache-2.0" | ||
| repository = "https://github.com/smg-project/smg" | ||
| readme = "README.md" | ||
| authors = ["Simo Lin <linsimo.mark@gmail.com>"] | ||
| keywords = ["radix-tree", "prefix-matching", "cache-routing", "kv-cache", "llm"] | ||
| categories = ["data-structures", "caching"] | ||
|
|
||
| # The Rust import path stays `radix_tree`; only the crates.io package name | ||
| # carries the `smg-` prefix (the bare `radix-tree` name is taken). | ||
| [lib] | ||
| name = "radix_tree" | ||
| path = "src/lib.rs" | ||
|
|
||
| [dependencies] | ||
| # Zero SMG dependencies. External utility crates only. | ||
| rustc-hash = "2" | ||
|
|
||
|
|
||
| [dev-dependencies] | ||
| # The differential oracle. Dev-only AND path-only (no version): cargo | ||
| # drops a version-less path dev-dependency from the published manifest, | ||
| # so the crate on crates.io carries no SMG dependency at all and can be | ||
| # published in tier 1 without waiting on a kv-index release (a | ||
| # versioned dev-dep would have to exist on the registry first — a race | ||
| # whenever both crates bump in one release). | ||
| kv-index = { path = "../kv_index" } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| # radix-tree | ||
|
|
||
| Published on crates.io as **`smg-radix-tree`** (the bare `radix-tree` name | ||
| belongs to an unrelated router crate); the Rust import path is `radix_tree`. | ||
| No SMG dependencies — it is a tier-1 crate in the release workflow. | ||
|
|
||
| A generic prefix-membership index: given per-holder chains of | ||
| content-addressed blocks, answer *"which holders already hold the | ||
| longest prefix of this chain, and how deep?"* — plus the write and | ||
| lifecycle operations a long-lived, multi-tenant index service needs | ||
| as first-class API. Zero SMG dependencies; everything it sees is a | ||
| hash. | ||
|
|
||
| Built as the ground-up replacement for the gateway's | ||
| `kv_index::PositionalIndexer` as a fleet-wide index core; the shared | ||
| radix-index service (`smg-radix-index`, a separate crate) is built on | ||
| it. | ||
|
|
||
| ## API in one glance | ||
|
|
||
| ```rust | ||
| let mut tree = RadixTree::new(Config::default()); | ||
| let w = tree.create_holder("worker-7"); // generational id | ||
| tree.store(w, None, &[(key, content), ...])?; // anchor a chain | ||
| tree.store(w, Some(parent_key), &more)?; // extend it | ||
| tree.remove(w, &[key]); // event-feed eviction | ||
| tree.truncate_tail(w, keep); // prefix-closed capacity cut | ||
| tree.clear(w); // epoch bump | ||
| tree.retire_holder(w); // frees everything, id recycled | ||
|
|
||
| let mut scratch = OverlapScratch::default(); | ||
| let mut out = Vec::new(); | ||
| tree.overlap(&chain_hashes, &mut scratch, &mut out); | ||
| // out: [{ holder, depth, total_blocks }] | ||
| tree.enumerate(w); // (pos, key, content) for snapshots | ||
| ``` | ||
|
|
||
| The contract — exact matching semantics, convergence scope, and the | ||
| §4 alias rules — is what `tests/differential.rs` enforces: every core | ||
| must equal the reference model on every run. | ||
|
|
||
| ## Structure | ||
|
|
||
| Prefixes form a trie of **chains**. A chain's contents are one | ||
| contiguous array, stored once no matter how many holders cover it; | ||
| membership is a short list of maximal runs pointing at interned | ||
| (hash-consed) holder sets; a query is one hash probe to the root | ||
| chain, a linear scan of contiguous contents to the divergence point, | ||
|
Comment on lines
+47
to
+48
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. 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -euo pipefail
ast-grep outline crates/radix_tree/src/chain.rs --items all --type function --match 'overlap|audit'
rg -n -C 8 '\broots\b|root_lineage|\.push\(' crates/radix_tree/src/chain.rsRepository: smg-project/smg Length of output: 18461 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- README context ---'
sed -n '38,54p' crates/radix_tree/README.md
printf '%s\n' '--- root lookup and lineage definitions ---'
sed -n '1,115p' crates/radix_tree/src/chain.rs
sed -n '235,275p' crates/radix_tree/src/chain.rs
sed -n '548,570p' crates/radix_tree/src/chain.rs
sed -n '1218,1238p' crates/radix_tree/src/chain.rsRepository: smg-project/smg Length of output: 8058 🤖 get_repo_knowledge executed:
Length of output: 35804 🟡 Nit: Include the root-candidate scan in the query complexity.
🤖 Prompt for AI Agents |
||
| a child-fork hop if needed, and a handful of span reads. Matching is | ||
| exact and content-verified — fingerprint collisions cannot | ||
| cross-credit. | ||
|
|
||
| `FlatTree` is the first-generation flat layout, kept as a second, | ||
| independently verified implementation: the test harness asserts BOTH | ||
| cores equal the reference model on every run. | ||
|
|
||
| ## Verification | ||
|
|
||
| The crate was built harness-first: | ||
|
|
||
| - `tests/differential.rs` — both cores vs a representationally | ||
| complete reference model AND the production `kv_index` oracle | ||
| (dev-dependency), with full-state `audit()` at every checkpoint. | ||
| - `tests/fuzz_differential.rs` — wide-config fuzz plus a CHAOS mode | ||
| that violates every contract precondition under a no-panic, | ||
| audit-green, model-equal, deterministic-replay contract. | ||
| `RADIX_FUZZ_SEEDS=10000` ran green. | ||
| - `tests/api.rs` — lifecycle, boundary, and exactness cases the model | ||
| deliberately doesn't express. | ||
| - `tests/alloc_gate.rs` — counting-allocator gate: single-holder | ||
| stores amortize to zero allocations. | ||
| - `tests/pinned_bench.rs` — the normative performance workload | ||
| (`RADIX_BENCH_SIDE=oracle|r1|r3`, `RADIX_BENCH_SCALE=large`, | ||
| `RADIX_BENCH_SOAK_SECS=n`; `RADIX_BENCH_PROFILE=agentic|churn|fleet| | ||
| fragmented` are diagnostics, not gates). | ||
|
|
||
| ## Measured (pinned workload: 12.8M holder-blocks, 256 holders) | ||
|
|
||
| | | old `PositionalIndexer` + glue | `RadixTree` | | ||
| |---|---|---| | ||
| | bytes / holder-block | 166.7 | **26.9** | | ||
| | worst cell (d78, 64 holders) cold p99 | 6.0 µs (unsound skip) | **7.6 µs exact** | | ||
| | single-holder query p50 | 917 ns | **292 ns** | | ||
| | writes, mixed stream | 5.5M blocks/s | 4.6M blocks/s | | ||
| | at 128M blocks: worst-cell p99 | 29 µs | **8.0 µs** | | ||
|
|
||
| Known sensitivity: interior holes fragment a holder's chain into | ||
| segments, and the query walk pays per segment where the positional | ||
| map does not. On the `fragmented` profile (the pinned shape with 2% | ||
| of every instance's interior blocks evicted at random, ~10 holes in a | ||
| 512-block prefix) the worst cell's p99 is 18.5 µs against the | ||
| incumbent's 8.3 µs, at the same 27.5 B/block. Production eviction is | ||
| tail-heavy (the pinned shape); a workload with dense interior holes | ||
| should be measured on this profile before the tree is adopted for it. | ||
|
|
||
| No timestamps live in the tree: freshness policy (idle TTL per | ||
| holder) belongs to the consumer; chain data is freed by reference | ||
| counting. | ||
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 is the only scheduled workflow in the repo that runs on a GitHub-hosted runner without a fork guard. Every other cron job that uses a bare hosted runner has one —
benchmark-manual-policy.yml:38,benchmark-request-processing.yml:42,benchmark-tokenizer.yml:38,benchmark-tool-parser.yml:38all carryif: github.repository == 'smg-project/smg' || vars.SMG_RUN_BENCHMARKS == 'true', andnightly-engine-docker.yml:22/stale.yml:11carry the plaingithub.repository ==form. The ones without a guard (nightly-triage,engine-version-watch, …) are all onvars.SMG_RUNNER_CPU || 'k8s-runner-cpu', which simply never picks up in a fork. That guard was added deliberately in #2324 ("let forks opt into the benchmark workflows").As written, every fork of the repo starts running a
timeout-minutes: 120release fuzz campaign at 06:17 UTC nightly, on the fork owner's Actions minutes, with no way to opt out short of disabling the workflow.Separately,
benchmark-radix-tree.yml:42puts the comparable Rust workload on${{ vars.SMG_RUNNER_CPU || 'k8s-runner-cpu' }}rather thanubuntu-latest— worth matching, since a 2-core hosted runner is where the 120-minute ceiling is most likely to bite on a 2000-seed release campaign.