feat(mem_cache): semantic KV cache reuse via a pluggable fuzzy-match radix backend by zbennett10 · Pull Request #31057 · sgl-project/sglang · GitHub
Skip to content

feat(mem_cache): semantic KV cache reuse via a pluggable fuzzy-match radix backend - #31057

Open
zbennett10 wants to merge 31 commits into
sgl-project:mainfrom
WorldFlowAI:feat/semantic-radix-backend
Open

feat(mem_cache): semantic KV cache reuse via a pluggable fuzzy-match radix backend#31057
zbennett10 wants to merge 31 commits into
sgl-project:mainfrom
WorldFlowAI:feat/semantic-radix-backend

Conversation

@zbennett10

@zbennett10 zbennett10 commented Jul 13, 2026

Copy link
Copy Markdown

Motivation

Exact prefix matching cannot reuse KV across prompts that share meaning but
not leading tokens: paraphrased questions over the same document, RAG
prompts with reordered context, multi-user workloads hitting the same
knowledge base. This PR adds opt-in semantic KV cache reuse following
the |exact|fuzzy|miss| prompt decomposition from the fuzzy prefix
matching design discussion (Draft_Prefix_Matching.md, @ibifrost): when the
radix tree covers only a prefix, a pluggable FuzzyMatchProvider may
nominate donor KV from a previously finished request, which is copied into
recipient-owned slots with RoPE position correction before the forward
pass.

Design goals, per maintainer guidance (@hzh0425): keep the default path
untouched, register the feature as a radix-cache backend, keep
model_runner changes minimal.

Modifications

  • New FuzzyRadixCache(RadixCache) registered as radix-cache backend
    fuzzy_match (flexkv pattern). All fuzzy behavior lives in the backend:
    provider match on exact-miss, donor validation + pinning (lock refs via a
    TreeNode-id registry), realization-slot pre-allocation with clean
    exact-only fallback, donor registration on request finish.
  • Core RadixCache/BasePrefixCache changes = two seams:
    MatchResult.fuzzy_matched_len (new trailing field) and a no-op
    _on_finished_insert hook between the finished-request insert and
    duplicate-slot freeing.
  • FuzzyKVRealizer collaborator does pre-forward donor-KV realization
    (V copy; K reverse-RoPE at donor positions + apply at target positions;
    per-layer zero-out mask). model_runner.py gains only orchestration: a
    gated maybe_init_fuzzy_kv_realizer() and one delegate call in
    _forward_raw beside the deferred-mamba hook (outside CUDA-graph
    capture/replay, covers all extend branches).
  • ReverseRotaryEmbedding / get_reverse_rope / reverse_rotary_emb
    additions to layers/rotary_embedding (native path; no CUDA kernel
    exists for the reverse direction).
  • SemanticEmbedding provider (default): implemented by the
    semblend package
    (PyPI, Apache-2.0), installed via
    the new extra pip install "sglang[fuzzy-semantic]". SemBlend is an
    open-source
    library for semantic KV-cache donor discovery and reuse planning: it
    embeds prompts (MiniLM ONNX), maintains the donor index, aligns donor
    and recipient token chunks (N:M), scores reuse quality against
    configurable gates, and emits the reuse plan — donor span, positions,
    and the per-layer mask — that this PR's realization path consumes.
    Keeping that pipeline in a package keeps SGLang's tree free of the
    embedding/ANN stack while the reuse-planning algorithms iterate. Lazy
    import + hard version gate at startup with an actionable error. The
    FuzzyMatchProvider interface is public so custom providers can plug
    in.
  • Observability: prefill batch log shows #fuzzy-token: N when fuzzy
    reuse fired (previously realized reuse showed as #cached-token: 0).
  • Config surface: selecting the backend enables the feature; five
    --fuzzy-* flags (provider, threshold, min-reuse-ratio,
    min-match-length, model-arch). Provider-internal tuning stays in config
    defaults.
  • Docs: end-to-end call chain with explicit lock/alloc/RoPE ownership
    in python/sglang/srt/mem_cache/fuzzy_match/README.md (requested in
    review of the experimental branch).
  • Tests: 21 CPU unit tests (test_fuzzy_match_providers.py,
    test_fuzzy_radix_cache.py), semblend faked via sys.modules — no new CI
    dependency. Cover pool-accounting failure modes: stale-donor and
    kv-length-mismatch fallbacks (leak-checked), donor pin/release lifecycle,
    donor-registration ordering vs duplicate freeing, node-registry
    maintenance, registry selection + unsupported-config rejection, and
    concurrent recipients pinning one donor.

Not supported yet: MLA pools, EAGLE,
multi-region reuse, hierarchical-cache interaction.

Follow-on work

This PR deliberately ships the minimal |exact|fuzzy|miss| pattern — one
contiguous fuzzy span re-anchored at the exact-prefix boundary — because it
reuses the existing inference pipeline with no structural changes. Planned
follow-ups, in rough order:

  • Multi-segment / multi-region reuse (|exact|miss|fuzzy|miss|fuzzy|...),
    the realistic shape of RAG and multi-document workloads: the scheduler
    builds a segmented prefill plan of fresh spans plus donor-KV spans, and
    the attention backend consumes it via FlashInfer's
    VariableBlockSparseAttentionWrapper (per the design discussion, a
    better fit than custom_mask). Early prototypes on 64K multi-donor
    prompts show mean 7.65x TTFT speedup versus cold prefill with good
    quality — worth its own RFC once this foundation lands.
  • True per-layer recompute for masked layers (the current
    layer_recompute_mask is a zero-out signal); recompute is the cleaner
    long-term answer for quality.
  • Donor-registration budgeting (rate/dedup limits on donor indexing)
    to shrink the remaining enabled-but-idle overhead on dense
    short-request workloads.
  • MLA-pool support in the realization path.
  • Hierarchical-cache (HiCache) interaction — fuzzy donors resident in
    host tiers.

Accuracy Tests

All runs on THIS branch (A10G, Qwen2.5-7B-Instruct-AWQ, fresh server per
configuration, 2026-07-13).

  • GSM8K few-shot (n=1000), fuzzy on vs off: baseline accuracy 0.862
    (invalid 0.001) vs fuzzy-on 0.865 (invalid 0.001) — no degradation
    (delta within batching noise). The provider's gates produced zero
    fuzzy fires across all 1000 questions
    (short, semantically dissimilar
    prompts never meet the reuse-ratio gate; the few-shot prefix hits the
    exact radix path), and zero pool leaks. This is the negative control:
    enabling the backend does not perturb workloads without semantic
    overlap.

  • Warm-vs-cold A/B at 8K/16K (cnn_dailymail + wikihow clusters, at
    default flags). ROUGE-L / token-F1 compare the warm (reused) answer
    against the cold answer for the same prompt; the measurement scale:
    identical-prompt reruns without any reuse score ~0.88/0.90
    (batching-noise floor), exact-prefix radix reuse scores 0.94/0.96.

    Overlap type cache frac cold→warm TTFT ROUGE-L token-F1
    exact (control) — (radix) 3264→44ms 0.941 0.955
    partial_60 0.88 3254→646ms (5.0x) 0.547 0.587
    partial_80 0.58 3257→1700ms (1.9x) 0.449 0.490
    paraphrase 0.76 3262→1034ms (3.2x) 0.478 0.525
    diverse (negative) 0.00 3262→3318ms (no fire) 0.878 0.898

    A sweep at looser settings (threshold 0.50, min-match 1) produced the
    same hit rates and the same drift — on genuinely similar pairs the
    threshold does not change what a hit does, it changes which pairs hit
    (on GSM8K, defaults fire on 1 in 1000 requests; the diverse control
    never fires). Read: a fuzzy hit trades TTFT for output drift beyond
    the rerun-noise floor — reuse is quality-gated but not lossless;
    answers remain on-topic and grounded (samples in the raw results), and
    per-layer recompute (follow-on) is the path to closing the gap. Zero
    pool-consistency failures across all sweeps and both GSM8K runs.
    Experimental-branch reference @16k (n=1): PPL(warm)/PPL(cold) 0.9587.

  • Statement of semantics: reuse is quality-gated but not lossless; the
    layer mask is a zero-out signal, not per-layer recompute.

Speed Tests and Profiling

  • Reuse speedup (this branch, e2e smoke): paraphrase variant realized
    104/119 donor tokens with RoPE correction; only 15 tokens prefilled.
    Experimental-branch reference at 16K: cold TTFT 4548ms → warm 230ms
    (19.8x); donor registration runs off the hot path (async embed).
    [OPTIONAL: re-measure 16K TTFT pair on this branch]
  • No-hit overhead (GSM8K n=1000, parallel 32, default flags): 1249 →
    1164 tok/s output throughput (~7%) and 127.3 → 136.8s wall on a
    worst-case-dense workload (a thousand short requests, every one
    indexed as a donor). The synchronous per-miss lookup cost is bounded
    by fuzzy_min_suffix_tokens (default 256): suffixes too short to
    amortize a semantic lookup never pay for one. The residual is the
    asynchronous donor-registration embedding, which amortizes on
    long-context workloads (fewer, larger requests); donor-registration
    budgeting is listed as follow-on. Without the suffix gate the same
    workload measured ~10-11%. The feature is opt-in per deployment.

Reproduction

pip install "sglang[fuzzy-semantic]"
python -m sglang.launch_server \
  --model-path Qwen/Qwen2.5-7B-Instruct-AWQ \
  --radix-cache-backend fuzzy_match \
  --fuzzy-model-arch qwen2.5-7b \
  --mem-fraction-static 0.70 2>&1 | tee server.log
# paired paraphrase prompts -> look for:
#   "fuzzy match success", "[FUZZY] Realized", "#fuzzy-token:"
grep -c "pool memory leak detected" server.log   # expect 0

Checklist / credits

Big thanks to @ibifrost and @hzh0425 for all of the hard work/advice/reviewing over the past several months.


CI States

Latest PR Test (Base): ❌ Run #33812107578
Latest PR Test (Extra): ❌ Run #33812107456
Latest PR Test (AMD ROCm 7.2): ❌ Run #33812107729

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@github-actions github-actions Bot added documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file labels Jul 13, 2026
…radix backend

Adds opt-in semantic KV cache reuse following the |exact|fuzzy|miss|
prompt decomposition: when exact prefix matching covers only a prefix, a
pluggable FuzzyMatchProvider may nominate donor KV from a previously
finished request, which is realized into recipient-owned slots with RoPE
position correction before the forward pass.

- FuzzyRadixCache(RadixCache) registered as radix-cache backend
  "fuzzy_match"; all fuzzy behavior (provider match, donor validation and
  lock-ref pinning, realization-slot pre-allocation with exact-only
  fallbacks, donor registration) lives in the backend.
- Core RadixCache/BasePrefixCache changes are two seams:
  MatchResult.fuzzy_matched_len and a no-op _on_finished_insert hook.
- FuzzyKVRealizer collaborator performs pre-forward donor-KV realization;
  model_runner.py gains only orchestration (a gated maybe_init and one
  delegate call beside the deferred-mamba hook).
- ReverseRotaryEmbedding / get_reverse_rope / reverse_rotary_emb added to
  layers/rotary_embedding (native path).
- Default SemanticEmbedding provider implemented by the semblend package
  (Apache-2.0), installed via the sglang[fuzzy-semantic] extra; lazy
  import with a hard version gate. Semantic lookups are gated on a
  minimum miss-suffix length to bound no-hit overhead.
- Prefill batch log shows #fuzzy-token when fuzzy reuse fired.
- End-to-end call-chain doc in mem_cache/fuzzy_match/README.md; 26 CPU
  unit tests with semblend faked via sys.modules.

Signed-off-by: Zach Bennett <zach@worldflowai.com>
Co-authored-by: Chenxin Wu <ibifrostki@gmail.com>
hzh0425 and others added 2 commits July 14, 2026 11:05
return torch.stack((o1, o2), dim=-1).flatten(-2)


def reverse_rotary_emb(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a numerical test for reverse_rotary_emb — i.e. applying RoPE and then reverse RoPE to an input x returns x' ≈ x — for both is_neox_style=True and False?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most definitely

Requested in review: applying RoPE then reverse RoPE returns the input
(orthonormal rotation, both Neox and GPT-J layouts), plus the relocation
identity the KV realization path relies on: reverse at donor position p
then re-apply at target position q equals rotating at q directly. A
non-degenerate guard asserts the forward rotation actually changes the
input so the roundtrip cannot pass for no-op helpers.

Signed-off-by: Zach Bennett <zach@worldflowai.com>
…-backend

Conflicts:
	python/sglang/srt/model_executor/model_runner.py
	  Both sides added a pre-forward hook after the deferred-mamba step:
	  kept fuzzy donor-KV realization (pool write, grouped with the mamba
	  hook) followed by upstream's new DWDP first-layer prefetch.

Semantic adaptations to upstream changes in this range:
	python/sglang/srt/mem_cache/fuzzy_match/fuzzy_radix_cache.py
	  cache_finished_req gained the required keyword-only kv_len_to_handle
	  parameter (sgl-project#29428) and forwards it to super(), matching the other
	  backend overrides.
	test/registered/unit/mem_cache/test_fuzzy_radix_cache.py
	  Pass kv_len_to_handle at the three finish call sites and drop the
	  removed pop_committed_kv_cache stub.
	python/sglang/srt/layers/rotary_embedding/base.py
	  ReverseRotaryEmbedding keeps its cos/sin cache dtype condition
	  identical to RotaryEmbedding after the XPU/FP32 cache change.

Signed-off-by: Zach Bennett <zach@worldflowai.com>

mask_len = len(layer_recompute_mask) if layer_recompute_mask else 0
for layer_id in range(pool.layer_num):
if layer_id < mask_len and layer_recompute_mask[layer_id]:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mask comes from the provider, so it's not visible here which layers get zeroed — worth stating explicitly which layers / what fraction on Qwen2.5-7B. More importantly, the reported ROUGE 0.45–0.55 conflates zeroing with reuse, so we can't attribute the drift. An ablation would help here — e.g., rerunning the same three overlap types with zeroing disabled (copy all) to see how much of the drift comes from zeroing vs. reuse itself. (Note a zeroed K still draws softmax weight but contributes V=0 — worth comparing against reusing the donor KV.)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ran the ablation you suggested. Same probe that produced the PR table, same 8 clusters, Qwen2.5-7B AWQ on an A10G, one arm with the provider mask as shipped and one arm with zeroing disabled so every layer is copied from the donor. The same matches fired in both arms so the comparison is paired.

Warm vs cold on the pairs where a fuzzy match fired:

arm ROUGE-L token-F1
mask on (zeroing) 0.384 0.442
mask off (all copy) 0.861 0.871

Per overlap type, mean over all pairs:

type fire rate mask on mask off
partial_80 0.50 0.632 0.908
partial_60 0.25 0.778 0.929
paraphrase 0.38 0.825 1.000
diverse (negative) 0.00 1.000 1.000

So you called it. The zeroing is the dominant source of the drift on these workloads, not the reuse itself. A zeroed key still takes softmax mass at a zero logit while contributing nothing, which dilutes attention over the reused span exactly as you suspected. We're changing the provider default to copy all layers in the next semblend release and the engine side keeps the mask as an optional provider hook. On visibility, the mask is adaptive per request rather than a fixed list. On this preset it flags the edge layers, typically the first two and the last five of 28, capped around a quarter of the layers.

One caveat for reading the numbers against the June table in the PR description. Realized reuse mass per hit is smaller on the current merged stack than in those runs, so this ablation is the quality attribution and the June table remains the TTFT reference. I also reworked the engine docstring so it describes only the contract and the actual zeroing semantics.

@@ -0,0 +1,64 @@
---

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, when fuzzy match fires, the quality drop is substantial — ROUGE-L 0.45–0.55 vs ~0.88 for the diverse/rerun control. Given that, it'd be good to clarify the intended application scenarios: what workloads is this aimed at, where the TTFT win justifies this much output drift? (Related: the README's "bound the drift" reads a bit strong against these numbers — might be worth qualifying.)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair challenge. I qualified the wording in both places. The README now says the gates and the mask are intended to reduce the drift rather than bound it and the docs page states the measured range inline next to the control number so nobody reads the quality claim as stronger than the data.

On intended scenarios, the target is latency sensitive resend heavy traffic where the shared content does not sit at a common prefix. Think a shared document re-asked under different task instructions, session content behind a shifted header, or templated enterprise flows. Exact prefix caching recovers nothing there and time to first token is what the operator cares about. Workloads that need byte stable outputs for identical inputs should keep the default backend and the docs page now says that explicitly.

Also worth noting from the ablation in the thread above, most of the measured drift came from the per layer zeroing rather than from reuse. With all layers copied the hit-only aggregate is 0.86 ROUGE against cold and per overlap type it lands between 0.91 and 1.0. We're defaulting the mask off in the next provider release, which closes most of that gap.

Used to apply RoPE in the recipient's reference frame.
layer_recompute_mask: Optional list of bools; when ``mask[i]`` is
True, layer ``i`` is zeroed instead of copied (the prefill
pass that bookends the block will produce fresh K/V for those

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says a "prefill pass that bookends the block will produce fresh K/V for those layers," but there's no such pass in this PR — the fuzzy span is in the matched prefix and the extend only computes the miss tail, so zeroed layers stay zeroed. Either point to the code that refills them or update the comment to match the actual behavior.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, there is no refill pass in this PR and the comment was wrong. I rewrote it to describe what actually happens. The zeros survive the forward pass since the extend only computes the missed tokens, and at request finish the realized slots are inserted into the radix tree, so they stay visible to later exact prefix matches and to later fuzzy matches that pick this request as a donor. I also added a unit test pinning the mask semantics, masked layers zeroed, unmasked layers position corrected, and short masks leaving trailing layers copied.

_REVERSE_ROPE_DICT: Dict[Tuple, ReverseRotaryEmbedding] = {}


def get_reverse_rope(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_reverse_rope doesn't appear to have any call site — the realization path uses copy_kv_with_rope_correction, which pulls reverse_rotary_emb from utils and the model's own cos_sin_cache.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed. The ReverseRotaryEmbedding class behind it had no other caller so it went too, which puts layers/rotary_embedding back to the upstream tree except for the reverse helper in utils.py that the realization path actually uses. The numerical round trip test from the earlier thread lives in test/registered/unit/layers/test_rotary_embedding_utils.py and covers both neox styles.

import torch


def as_long_tensor(obj, device) -> torch.Tensor:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as_long_tensor is also defined but never used.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed as well, thanks for the catch.

Remove get_reverse_rope and the ReverseRotaryEmbedding class, which
had no call site (the realization path uses reverse_rotary_emb from
utils directly), restoring layers/rotary_embedding to the upstream
tree. Remove the unused as_long_tensor helper. Correct the layer mask
documentation, which described a refill pass that does not exist:
zeroed layers stay zeroed through the forward pass and the realized
slots are inserted into the radix tree, and at a zeroed layer reused
positions score zero logits that take softmax mass while contributing
zero V. Add a unit test pinning the mask semantics. Qualify the drift
wording in the module README and docs page and state the measured
quality range explicitly.

Signed-off-by: Zach Bennett <zach@worldflowai.com>
Upstream now threads cache_salt from the request into RadixKey during
finished-request insertion, so the stub carries the field.

Signed-off-by: Zach Bennett <zach@worldflowai.com>
Correctness. Donor registration now skips inserts that deduplicated
against existing tree content, because the captured kv indices in the
deduplicated range are freed right after the callback and the provider
would retain dangling slots. The cache salt joins the provider
namespace on both the registration and match paths so donors never
serve across salts. RadixKey.limit caps the tokens the provider sees.
Empty segment lists normalize to None so every consumer has one
no-segments sentinel.

Startup guards. The backend now rejects MLA-style pools and non-RoPE
models at model load instead of silently serving donor KV without
position correction, requires page_size 1 and tp_size 1 and pp_size 1,
validates the fuzzy flag ranges at argument parsing, and forces the
overlap scheduler off: realization mutates request KV state on the
forward stream and is not ordered against overlapped batch
preparation. The realizer's silent degrade path is now a loud error.

API surface. Removed dead provider API nothing consumed: the
set_min_match_length method, the cached_token_ids result field, the
provider-internal match_entry handle, the segment NodeRef addressing
fields that no consumer implemented, and the rejection_reason quality
field. Renamed layer_recompute_mask to layer_zero_mask to say what it
does. The provider's fallback tokenizer now comes from the served
model path instead of an undocumented environment variable.

Observability and docs. The fuzzy match success log line now fires
after validation so it marks an accepted match as documented, per
request provider logs drop to debug, the metrics comment states that
fuzzy tokens are a subset of the hit counter, and the README and docs
page claims are aligned with measured behavior, including the client
visibility of reused tokens and the startup rejections. The semblend
extra takes an upper version bound.

Tests. End to end realization math on real tensors for both the
contiguous and scattered segment paths, mask zeroing semantics, and
cache salt namespacing.

Signed-off-by: Zach Bennett <zach@worldflowai.com>
@zbennett10

Copy link
Copy Markdown
Author

Pushed the review fixes along with a fresh merge from main. Summary of what changed since the review. The dead code is gone so the rotary embedding package is back to upstream apart from the utils helper the realization path uses. The layer mask documentation now matches the real behavior and there is a new unit test pinning the mask semantics. The drift wording is qualified with the measured numbers stated inline in the docs. The ablation on the zeroing question is in the thread above, short version is that the zeroing caused most of the drift and the next provider release defaults it off.

Beyond the review items I did a hardening pass on the backend. Donor registration now skips inserts that deduplicated against existing tree content so the provider does not retain freed slots. The cache salt joins the provider namespace on both registration and match so donors never serve across salts. Unsupported stacks are rejected at startup with clear errors instead of degrading silently, that covers MLA style pools, models without rotary embeddings, page size above 1, and multi GPU. The overlap scheduler is forced off with a warning. Flag values are validated at argument parsing. The provider interface also slimmed down, the fields nothing was consuming are gone and the mask field is renamed to say what it does. New tests cover the realization math end to end on real tensors for both the contiguous and scattered segment paths plus cache salt namespacing.

Signed-off-by: Zach Bennett <zach@worldflowai.com>
Upstream moved per-request KV lifecycle state onto a ReqKvInfo record,
so the fuzzy reuse fields move with it: they travel through detach_kv
and clear on retraction, where a stale realization plan would otherwise
point at slots the request no longer owns. Server-argument resolution
moved to per-domain hooks, so the fuzzy backend's envelope checks move
to the KV cache hook and register in the resolution pipeline. The
cache-backend name now comes from the published memory namespace.

Signed-off-by: Zach Bennett <zach@worldflowai.com>
The provider's per layer zeroing measurably degrades output on reused
spans, and 0.3.17 is the first release that defaults to copying all of
them. Raise both floors together so the runtime guard cannot admit an
older build that the extra no longer installs.

Signed-off-by: Zach Bennett <zach@worldflowai.com>
Reading model_path off the supplied server_args record returns the raw
CLI input, so the provider's fallback tokenizer would miss a path that
resolution or a later weight update rewrote. The backend factory already
holds the resolved model config, so it passes the effective path in.

Signed-off-by: Zach Bennett <zach@worldflowai.com>
Realization slots came from the free pool only, so once a few long
donors and their recipients were resident every later provider match
fell back to exact-only in silence: on an A10G with a 61K-token pool,
3 of 24 wrapper-shift items served at 8K tokens and 1 of 24 at 16K,
exactly the number that fit before the pool filled. The donor is now
pinned first so making room can never free it, then unlocked entries
are evicted the way prefill admission does; a genuine capacity failure
still degrades cleanly and releases the pin.

Signed-off-by: Zach Bennett <zach@worldflowai.com>
A provider result whose span starts at the exact-matched length was
dropped, and the contiguous realizer skipped it, on the assumption that
token-and-position-aligned content belongs to the exact tree. A verified
paraphrase served whole is different tokens at the same positions: it
needs its own slots (delta 0 copy) or the recipient reads donor-owned
slots and later inserts them into its own branch. The paraphrase_verified
tier now admits the span and copies it; exact-tier aligned spans keep
the old behavior.

Signed-off-by: Zach Bennett <zach@worldflowai.com>
The adapter in this release serves verified paraphrases as a
position-aligned contiguous span after the exact prefix, which the
backend now realizes with a zero delta; earlier releases emitted a shape
the backend drops.

Signed-off-by: Zach Bennett <zach@worldflowai.com>
@zbennett10

Copy link
Copy Markdown
Author

Three follow-up commits from long-context testing (8K to 24K token prompts on an A10G):

  • Realization slots are now obtained with evict-to-fit (donor pinned first), instead of a plain alloc that silently fell back to exact-only once the pool filled.
  • A verified paraphrase served whole is position-aligned with the exact prefix; it is now copied into fresh slots with a zero delta rather than dropped as exact-tree content.
  • Minimum semblend raised to 0.3.19, which carries the matching adapter changes.

Unit tests cover all three (fuzzy radix, realizer, provider suites).

A request that consumed donor KV holds approximate content for its fuzzy
span and for everything computed after it. cache_finished_req inserted
its whole sequence into the radix tree, so the next request sharing its
token prefix received the donor's KV through a trusted exact match
(observed: answers about one service naming another service's report).
Served requests are no longer inserted or registered as donors; their
slots beyond the exact prefix are freed as before.

Signed-off-by: Zach Bennett <zach@worldflowai.com>
Signed-off-by: Zach Bennett <zach@worldflowai.com>
@zbennett10

Copy link
Copy Markdown
Author

Two more follow-ups from long-context testing:

  • A request that consumed donor KV is no longer inserted into the exact radix tree or registered as a donor. Its fuzzy span is approximate content, and inserting it let a later request that shared its token prefix pick up the donor KV through an exact match.
  • semblend floor raised to 0.3.20, whose adapter embeds donors with the same canonicalization as queries (identical documents under different wrappers were missing the similarity floor).

Both covered by unit tests in the fuzzy radix suite.

…napshot

A provider registers a donor with a snapshot of the request's slot
mapping, which includes prefix slots shared with earlier tree nodes. If
such a node is evicted and its slots reused before a recipient matches,
the served span starts inside another request's content (observed:
answers about one service opening with another service's report).
Served slots are now resolved from the pinned donor's tree path at the
same positions, for contiguous results and NodeRef segments; a span the
path does not cover is dropped.

Signed-off-by: Zach Bennett <zach@worldflowai.com>
0.3.21 aligns the leading donor candidates instead of only the top fast
estimate, which keeps wrapper-shifted prompts on the position-corrected
copy path among near-duplicate donors, and its fact gate compares
identifiers as well as numbers.

Signed-off-by: Zach Bennett <zach@worldflowai.com>
@zbennett10

Copy link
Copy Markdown
Author

Head is now 2bcb005: the semblend floor moves to 0.3.21.

What changed in 0.3.21 and why it matters here: among many near-duplicate donors, the fast reuse estimate could rank a decoy above the donor that actually shares the prompt's tokens, and only the top estimate was ever aligned, so wrapper-shifted prompts fell through to the paraphrase tier. The store now aligns the leading candidates and keeps the best alignment, and the fact gate also compares identifiers (service ids, incident codes), not just numbers.

Re-measured on this head with 0.3.21 (A10G, Qwen2.5-7B fp16, 24 prompts per length, same document under a different instruction wrapper, each prompt paired with its own cold run):

Prompt length Served TTFT cold p50 with reuse p50 speedup p50 (range)
8K 24 of 24 2.05 s 0.23 s 9.1x (2.0x to 11.6x)
16K 24 of 24 4.50 s 0.44 s 9.9x (3.9x to 15.0x)
24K 24 of 24 7.38 s 0.63 s 11.0x (4.0x to 17.1x)

All served on the exact tier (aligned copy, reuse 0.995 to 0.999). On LongBench multi-document QA (2K to 17K tokens) all 24 prompts were served whole; gold-answer pass count 12 served vs 13 cold.

@zbennett10

Copy link
Copy Markdown
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation run-ci run-ci-extra unified-radix-cache

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants