Skip to content

fix(epp): record standalone selections in the approximate KV index - #13836

Open
flpanbin wants to merge 1 commit into
ai-dynamo:mainfrom
flpanbin:record-selections-approximate
Open

flpanbin wants to merge 1 commit into
ai-dynamo:mainfrom
flpanbin:record-selections-approximate

Conversation

@flpanbin

@flpanbin flpanbin commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Overview:

When DYN_USE_KV_EVENTS=false, the standalone embedded EPP routing path had no way to learn about KV blocks produced by workers. As a result, repeated prefixes were scheduled purely load-aware and never saw KV overlap.

This change records the selected worker and prompt-block decision after each successful select_and_reserve into a local approximate KV index with TTL pruning, so later requests can exploit estimated overlap without worker KV-event ingestion.

Details:

  • Enable standalone approximate retention

    • WorkerRegistry now carries an optional ApproximateRetentionConfig; in standalone mode it is set to TTL using router_ttl_secs.
    • create_indexer_with_retention() passes the retention into the primary indexer (KvIndexer or ThreadPoolIndexer).
    • Indexer::record_routing_decision() exposes the recording operation to callers.
  • Record successful routing decisions

    • SelectionCore::schedule_selection() captures block_hashes / sequence_hashes from the prepared inputs and records them against the selected worker after a successful select_and_reserve.
    • Recording is gated by !use_kv_events && overlap_score_credit > 0, leaving the event-driven path unchanged.
  • Tests

    • repeated_prefix_without_kv_events_uses_approximate_overlap
    • approximate_overlap_decisions_expire_after_ttl
    • approximate_routing_is_isolated_by_cache_namespace
    • approximate_routing_is_disabled_when_kv_events_enabled
    • approximate_r_is_disabled_when_overlap_credit_zero

Where should the reviewer start?

  1. lib/kv-router/src/services/selection/core/mod.rs — standalone_approximate_retention_config() and the schedule_selection() recording logic.
  2. lib/kv-router/src/services/indexer/backend.rs — Indexer::record_routing_decision() and create_indexer_with_retention().
  3. lib/kv-router/src/services/selection/tests.rs — focused tests for the new standalone approximate behavior.

Validation

Unit tests

cargo test -p dynamo-kv-router --features standalone-selection
cargo clippy -p dynamo-kv-router --features standalone-selection
cargo check -p dynamo-ext-proc

End-to-end test (agentgateway + standalone EPP + vLLM worker)

Deployed with DYN_USE_KV_EVENTS=false and default router_ttl_secs=120. Sent the same prompt three times:

  1. First request

    • overlap_credit_blocks: 0.000
    • effective cached blocks: 0.00
    • Establishes the baseline: no prior decision in the indexer.
  2. request (same prompt, a few seconds later)

    • overlap_credit_blocks: 5.000
    • effective cached blocks: 5.00
    • The first select_and_reserve decision was recorded; the second request matches it.
  3. Third request (same prompt, ~3 minutes later)

    • overlap_credit_blocks: 0.000
    • effective cached blocks: 0.00
    • TTL pruning removed stale decision; routing fell back to load-aware.

End-to-end cache_salt

configs:

DYN_ROUTER_USE_KV_EVENTS=false
DYN_ROUTER_TTL_SECS=30
DYN_KV_CACHE_BLOCK_SIZE=16
DYN_EPP_MODE=standalone
DYN_MODEL_NAME=Qwen/Qwen3-0.6B

Results:

# Scenario Expected Observed (logs)
1 tenant-a, first request with a new prefix No overlap: index has no entry for the prefix overlap_credit_blocks: 0.000, effective cached blocks 0.00
2 tenant-a, second request, same prefix Hit: the decision recorded by (1) is matched overlap_credit_blocks > 0 , effective cached blocks > 0
3 tenant-b, same prefix as (1)/(2) No hit: cache-namespace isolation — tenant-b hashes never match tenant-a entries overlap_credit_blocks: 0.000
4 tenant-a, entirely different prefix No hit: unrelated content must not match overlap_credit_blocks: 0.000
5 tenant-a, same prefix as (2), 30+ s later (TTL=30s) No hit: decision-derived entries expired via TTL pruning overlap_credit_blocks: 0.000

Related Issues

⚠️ This section is required. Choose one path below and delete the other.

🔗 This PR is linked to an issue:

🚫 This PR is NOT linked to an issue:

  • Confirmed — no related issue

Open in Devin Review

Summary by CodeRabbit

  • New Features

    • Added approximate routing support for standalone selection when KV events are disabled.
    • Routing decisions can now be retained for a configurable period and used to improve overlap scoring.
    • Added support for recording routing decisions and configuring retention during indexer setup.
  • Bug Fixes

    • Ensured routing data expires according to its configured retention period.
    • Isolated cached routing data between namespaces.
    • Preserved existing behavior when KV events are enabled or overlap scoring is disabled.

@flpanbin
flpanbin requested a review from a team as a code owner August 26, 2026 15:50
@copy-pr-bot

copy-pr-bot Bot commented Aug 26, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 34ba5fb9-ce51-4753-9e67-1409e34900a9

📥 Commits

Reviewing files that changed from the base of the PR and between ce48476 and 1e7be98.

📒 Files selected for processing (4)
  • lib/kv-router/src/services/indexer/backend.rs
  • lib/kv-router/src/services/indexer/registry.rs
  • lib/kv-router/src/services/selection/core/mod.rs
  • lib/kv-router/src/services/selection/tests.rs

Included review availability: Your plan provides up to 12 included reviews per hour; 11 remain after this review.


Walkthrough

The indexer and worker registry now support optional approximate-retention settings. Standalone selection records successful routing decisions when KV events are disabled and overlap scoring is enabled. Tests cover overlap, expiry, namespace isolation, and disabled paths.

Changes

Standalone approximate routing

Layer / File(s) Summary
Indexer retention and routing API
lib/kv-router/src/services/indexer/backend.rs
The indexer accepts optional approximate-retention settings and records routing decisions through single-threaded or concurrent primary backends.
Worker registry retention propagation
lib/kv-router/src/services/indexer/registry.rs
The registry stores retention settings and applies them when creating or recovering worker indexers.
Standalone selection recording and validation
lib/kv-router/src/services/selection/core/mod.rs, lib/kv-router/src/services/selection/tests.rs
Standalone selection configures TTL retention and records booked decisions when KV events are disabled and overlap scoring is enabled. Tests cover overlap, expiry, namespace isolation, event-driven routing, and zero overlap credit.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: ⚪ Minimal · up to 1e7be

This change adds standalone approximate KV recording with focused expiration and isolation tests; no actionable merge-blocking risk remains beyond normal checks and review.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 22 functions across 4 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes satisfy issue #13425. They enable standalone approximate routing, record successful selections, apply TTL retention, preserve cache-namespace isolation, keep event-driven behavior unchange…
Out of Scope Changes check ✅ Passed The changes remain within the linked issue scope. They do not add worker KV-event ingestion, shared cache lookup, new routing policies, or unrelated event-driven index changes.
Title check ✅ Passed The title clearly identifies the main change: recording standalone selections in the approximate KV index.
Description check ✅ Passed The description includes the required Overview, Details, reviewer guidance, and Related Issues sections. It explains the implementation, scope, tests, and validation results. The alternate no-issue pa…
  • Fix all pre-merge checks with AI

Comment @coderabbitai help to get the list of available commands.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

Open in Devin Review

Comment on lines +807 to +808
let routing_decision_hashes =
book.then(|| (block_hashes.clone(), sequence_hashes.to_vec()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔍 Unconditional hash clone on the booked hot path

routing_decision_hashes clones block_hashes and sequence_hashes whenever book is true, before the feature gate at mod.rs. In the default use_kv_events=true deployment those clones are never used, so each booked selection pays two Vec<u64> allocations for nothing. Folding the gate into the .then() avoids the clone when standalone recording is disabled.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Adopted — moved the gate into .then() so the clones only happen when standalone approximate recording is actually enabled.

Comment on lines +258 to +269
fn standalone_approximate_retention_config(
kv_router_config: &crate::config::KvRouterConfig,
) -> Option<ApproximateRetentionConfig> {
if kv_router_config.use_kv_events {
return None;
}
if kv_router_config.overlap_score_credit <= 0.0 {
return None;
}
let ttl = std::time::Duration::from_secs_f64(kv_router_config.router_ttl_secs.max(0.0));
Some(ApproximateRetentionConfig::Ttl(PruneConfig { ttl }))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔍 LRU approximate policy ignored in standalone retention

standalone_approximate_retention_config always builds ApproximateRetentionConfig::Ttl, ignoring router_approximate_cache_policy. lib/kv-router/src/scheduling/config.rs:1043-1047 documents that the lru policy is valid only when use_kv_events=false, yet an operator selecting it here silently gets TTL. This may be an intentional TTL-only first step; confirm.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Standalone mode lacks request-guard lifecycles, so LRU is explicitly downgraded to TTL. Added an info!log when the policy is LRU to make the downgrade visible to operators.

let response_isl_tokens = book.then_some(isl_tokens);
let response_track_prefill_tokens = book.then_some(track_prefill_tokens);

let routing_decision_hashes =

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Gating routing_decision_hashes on book records approximate KV state only for /select_and_reserve, so the public /select + /create_reservation flow books KV on a worker without ever adding its prefix to the standalone index. Fix: carry the selected block hashes through PendingSelection/ReservationBooking and record after cached reservation booking succeeds.

🤖 AI Fix

In lib/kv-router/src/services/selection/core/mod.rs and lib/kv-router/src/services/selection/pending.rs, add block_hashes: Vec<LocalBlockHash> to PendingSelection and ReservationBooking, populate it from schedule_selection, and after finalize_reservation successfully books a cached selection call entry.indexer.record_routing_decision(pending.worker, pending.block_hashes.clone(), pending.sequence_hashes.clone()).await? when standalone approximate routing is enabled.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. /select now carries block hashes into PendingSelection; /reservations replays them through ReservationBooking, and finalize_reservation records the decision after a successful cached booking.

@github-actions github-actions Bot added the fix label Aug 26, 2026
@flpanbin
flpanbin temporarily deployed to external_collaborator August 26, 2026 16:12 — with GitHub Actions Inactive
@flpanbin
flpanbin temporarily deployed to external_collaborator August 26, 2026 16:12 — with GitHub Actions Inactive
@github-actions

Copy link
Copy Markdown
Contributor

👋 Hi flpanbin! Thank you for contributing to ai-dynamo/dynamo.

Just a reminder: The NVIDIA Test Github Validation CI runs an essential subset of the testing framework to quickly catch errors.Your PR reviewers may elect to test the changes comprehensively before approving your changes.

🚀

@github-actions github-actions Bot added the external-contribution Pull request is from an external contributor label Aug 26, 2026
@flpanbin
flpanbin temporarily deployed to external_collaborator August 28, 2026 14:42 — with GitHub Actions Inactive
@flpanbin
flpanbin force-pushed the record-selections-approximate branch from b0a91f2 to a8e979f Compare August 28, 2026 14:48
@flpanbin
flpanbin temporarily deployed to external_collaborator August 28, 2026 14:48 — with GitHub Actions Inactive
@flpanbin

Copy link
Copy Markdown
Contributor Author

The cache_salt propagation through the OpenAI-compatible path requires #13714; full end-to-end cache-namespace validation is pending that PR.

@tmonty12 tmonty12 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

SelectionService approximate-routing review.

}
// Standalone mode has no request-guard lifecycle, so LRU is intentionally
// downgraded to TTL. Log the downgrade so operators are not surprised.
if kv_router_config.router_approximate_cache_policy == ApproximateCachePolicyKind::Lru {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] router_approximate_cache_policy=lru is an explicit capacity-aware policy, but this silently replaces it with TTL. An info log does not prevent an operator from believing LRU is active.

Please reject this combination in SelectionServiceBuilder::build() before constructing the core: !use_kv_events && overlap_score_credit > 0 && policy == Lru. Keep this validation SelectionService-specific: LRU remains valid for the integrated frontend path.

The reason is not that SelectionService lacks lifecycle callbacks—it has prefill_complete, add_output_block, and free_reservation—but that it does not currently wire them to an ApproximateLruLease and canonical output-block materialization lifecycle.

@flpanbin flpanbin Sep 7, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. SelectionServiceBuilder::build() rejects the combination before constructing the core, deriving approximate-mode activation from the shared approximate_retention_config helper so validation can't drift from construction.

/// (worker-event-disabled) routing. Returns `None` when KV events are enabled
/// or when overlap scoring is disabled, preserving the existing event-driven
/// path.
fn standalone_approximate_retention_config(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[nit] standalone_approximate_retention_config describes the current caller/constraint rather than the value this function derives. approximate_retention_config would be clearer and avoids baking “standalone” into a general configuration helper.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Renamed to approximate_retention_config; doc comment now describes the derived value, not the caller.

let response_isl_tokens = book.then_some(isl_tokens);
let response_track_prefill_tokens = book.then_some(track_prefill_tokens);

let should_record_routing_decision = book

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] This repeats the same !use_kv_events && overlap_score_credit > 0 decision used when constructing approximate retention. The integrated KvRouter constructs the indexer once, then gates request-side writes on indexer.records_routing_decisions().

Could this path make the constructed indexer’s recording capability authoritative as well? A config helper can centralize construction intent, but the actual recording decision should follow the indexer that was built so the two paths cannot drift.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. The service Indexer now carries a records_routing_decisions flag, exposed as can_record_routing_decisions(). Both write paths gate on entry.indexer.can_record_routing_decisions() instead of re-reading config. Construction intent stays in approximate_retention_config, so the two can't drift.

@flpanbin
flpanbin force-pushed the record-selections-approximate branch from a8e979f to da31b86 Compare September 7, 2026 07:26
@pull-request-size pull-request-size Bot removed the size/L label Sep 7, 2026
@flpanbin
flpanbin temporarily deployed to external_collaborator September 7, 2026 07:26 — with GitHub Actions Inactive
@flpanbin
flpanbin force-pushed the record-selections-approximate branch from da31b86 to 32349e1 Compare September 7, 2026 08:09
@flpanbin
flpanbin temporarily deployed to external_collaborator September 7, 2026 08:09 — with GitHub Actions Inactive
@flpanbin
flpanbin force-pushed the record-selections-approximate branch from 32349e1 to bc4c0e4 Compare September 7, 2026 08:27
@flpanbin
flpanbin temporarily deployed to external_collaborator September 7, 2026 08:27 — with GitHub Actions Inactive
@flpanbin
flpanbin force-pushed the record-selections-approximate branch from bc4c0e4 to ae94cb3 Compare September 16, 2026 07:15
@flpanbin
flpanbin deployed to external_collaborator September 16, 2026 07:15 — with GitHub Actions Active
@flpanbin
flpanbin force-pushed the record-selections-approximate branch from ae94cb3 to 33d36be Compare September 16, 2026 07:30
@flpanbin
flpanbin deployed to external_collaborator September 16, 2026 07:30 — with GitHub Actions Active
@flpanbin
flpanbin force-pushed the record-selections-approximate branch from 33d36be to 6e724cb Compare September 17, 2026 02:32
@flpanbin
flpanbin deployed to external_collaborator September 17, 2026 02:32 — with GitHub Actions Active

This branch was successfully deployed

1 active deployment
external_collaborator — 6e724cbf Deployed Sep 17, 2026 by flpanbin via ok-to-test #19161
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external-contribution Pull request is from an external contributor fix size/XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(epp): record standalone selections in the approximate KV index

2 participants