Skip to content
Merged
Show file tree
Hide file tree
Changes from 106 commits
Commits
Show all changes
111 commits
Select commit Hold shift + click to select a range
ab668d8
feat(kv-router): host-supplied inputs for SelectionService parity wit…
ishandhanani Sep 4, 2026
888548e
fix(router): preserve current session input context contract
ishandhanani Sep 9, 2026
042dc29
feat(kv-router): approximate, remote, and hint-aware indexing in Sele…
ishandhanani Sep 4, 2026
e7ef305
fix(router): use typed KV transfer hints in standalone selection
ishandhanani Sep 9, 2026
8dfeecb
fix(bindings): compile selection service tests with remote indexing
ishandhanani Sep 9, 2026
2d77426
feat(router): re-host KvRouter scheduling on an embedded selection pa…
ishandhanani Sep 3, 2026
fc8a67c
fix(router): feed scheduler load from the embedded selection partition
ishandhanani Sep 3, 2026
ad7e918
feat(router): make embedded selection the default scheduler
ishandhanani Sep 3, 2026
616bcef
feat(router): replica sync for the embedded selection partition
ishandhanani Sep 3, 2026
f417e1c
feat(router): resolve linked worker-selection policies on the embedde…
ishandhanani Sep 3, 2026
b2644f8
fix(router): project current KV transfer metadata into embedded selec…
ishandhanani Sep 9, 2026
14aad43
refactor(router): delete the runtime-bound KV router scheduler
ishandhanani Sep 3, 2026
2c3a1e6
style(router): remove duplicate test configuration attribute
ishandhanani Sep 9, 2026
81cc6b6
refactor(router): remove the ignored router_embedded_selection flag
ishandhanani Sep 3, 2026
6c2e7d4
refactor(router): drop the selector type parameter from the frontend …
ishandhanani Sep 3, 2026
f9f1259
refactor(kv-router): remove the delegated worker-selector adapter
ishandhanani Sep 3, 2026
e7713c8
refactor(kv-router): group host-supplied selection inputs into Select…
ishandhanani Sep 3, 2026
9cb836b
fix(router): update callers after selector type erasure
ishandhanani Sep 9, 2026
cd1a793
feat(epp): data-parallel worker registration in the standalone reflector
ishandhanani Sep 3, 2026
d140d9c
feat(kv-router): WorkerCatalogSource and CatalogReconciler for worker…
ishandhanani Sep 3, 2026
2efe000
refactor(router): feed the embedded partition's catalog through Catal…
ishandhanani Sep 3, 2026
dab4d2b
refactor(epp): expose the pod reflector as a WorkerCatalogSource
ishandhanani Sep 3, 2026
945b4ae
fix(router): update membership tests to reconciler API
ishandhanani Sep 9, 2026
1990241
refactor(kv-router): HostCache.index names where a partition's KV ind…
ishandhanani Sep 3, 2026
8772ed0
refactor(kv-router): one Indexer type for the frontend and the select…
ishandhanani Sep 3, 2026
d17aa47
refactor(kv-router): the frontend's index is the partition's index
ishandhanani Sep 3, 2026
e263341
feat(kv-router): KvEventIngress feeds core-owned indexes
ishandhanani Sep 4, 2026
9aa0971
feat(router): RuntimeIngress feeds the partition's index for the fron…
ishandhanani Sep 4, 2026
f413da4
refactor(kv-router): session-affinity table shared by every selection…
ishandhanani Sep 4, 2026
12d0610
feat(kv-router): session affinity for request-shaped selection hosts
ishandhanani Sep 4, 2026
f1d2668
feat(epp): session affinity in the standalone EPP
ishandhanani Sep 4, 2026
f8432b3
docs(router): clarify partition session affinity ownership
ishandhanani Sep 9, 2026
cc4d756
fix(router): preserve partition and reservation lifecycle in selectio…
ishandhanani Sep 9, 2026
b91015f
fix(bindings): reject invalid selection affinity TTLs
ishandhanani Sep 9, 2026
8071b3e
Merge origin/main (464ea4f82a) into idhanani/dyn-14310-selection-core…
ishandhanani Sep 10, 2026
efb4c7e
fix(router): drop the reservation-index guard before the partition lo…
ishandhanani Sep 10, 2026
533ff45
fix(router): consume worker completion marks without replica sync
ishandhanani Sep 10, 2026
ab4c959
fix(kv-router): compile the slot tracker without the selection feature
ishandhanani Sep 10, 2026
0c0096d
fix(router): default unreported worker capacity for discovered workers
ishandhanani Sep 10, 2026
76c4374
fix(router): restore per-worker load gauges on the embedded partition
ishandhanani Sep 10, 2026
e5fe3f0
fix(router): restore router queue metrics and the prefill overlap obs…
ishandhanani Sep 10, 2026
ca0a0c6
fix(router): route without affinity when the session table is full
ishandhanani Sep 10, 2026
9493e06
fix(router): report remote indexer outages as not-ready
ishandhanani Sep 10, 2026
17c73cc
fix(epp): surface selection client errors as 400
ishandhanani Sep 10, 2026
20c307d
chore(router): gate affinity test hooks and drop dead re-exports
ishandhanani Sep 10, 2026
f81f949
fix(router): reject cache_salt on tiered indexer queries and skip no-…
ishandhanani Sep 10, 2026
7741152
fix(router): reject unsupported data-parallel shapes and correct stan…
ishandhanani Sep 10, 2026
960500e
fix(router): free core bookings the caller never consumes
ishandhanani Sep 10, 2026
3003cb8
fix(router): hold the rebound session lease on concurrent affinity fa…
ishandhanani Sep 10, 2026
92ea625
fix(router): re-register KV listeners a cancelled worker update removed
ishandhanani Sep 10, 2026
b4a81a5
fix(router): purge index blocks for a rank whose listener is already …
ishandhanani Sep 10, 2026
e2e4e61
feat(router): guard core bookings with the lifecycle lease and claim …
ishandhanani Sep 10, 2026
fe816fc
feat(router): share the session affinity commit and configure its mode
ishandhanani Sep 10, 2026
9740a38
refactor(router): route every selection through one core operation
ishandhanani Sep 10, 2026
d6b1c8f
perf(router): trim the core hint and readiness hot paths and carry th…
ishandhanani Sep 10, 2026
6616406
feat(router): wire the frontend to the shared selection core without …
ishandhanani Sep 10, 2026
c670214
refactor(router): route the frontend through the shared selection core
ishandhanani Sep 10, 2026
1172f06
refactor(router): commit session affinity through the shared table
ishandhanani Sep 11, 2026
fabac4b
refactor(router): split the selection core into one module per concern
ishandhanani Sep 11, 2026
9c4b334
chore(router): delete the selection paths the core cutover left unused
ishandhanani Sep 11, 2026
89785a3
refactor(router): start replica ingress after the lease manager is in…
ishandhanani Sep 11, 2026
25aea16
fix(router): hold the routed selection future once on the cleanup path
ishandhanani Sep 11, 2026
e22fab5
fix(router): apply the review round on the selection core
ishandhanani Sep 11, 2026
317eb30
chore(router): clear the small self-review findings
ishandhanani Sep 11, 2026
ea037f3
test(router): pin the booking lifecycle invariants
ishandhanani Sep 11, 2026
c95307e
fix(router): gate the LoRA narrowing helper behind standalone-selection
ishandhanani Sep 11, 2026
1f91e53
refactor(router): hand bookings to hosts as one BookingHandle
ishandhanani Sep 11, 2026
bbd8412
refactor(router): wire the request lease manager directly as the part…
ishandhanani Sep 11, 2026
45383b4
Merge branch 'main' into idhanani/dyn-14310-selection-core-full
ishandhanani Sep 12, 2026
5c2eb04
chore(router): cleanup round one, drop test-only shims and duplicate …
ishandhanani Sep 12, 2026
63f7ac4
chore(router): cleanup round two, one result shape and fewer wrappers
ishandhanani Sep 12, 2026
99f8574
chore(router): cleanup round three, verbatim moves and shorter tests
ishandhanani Sep 12, 2026
37cfba9
test(router): restore two pins the cutover dropped
ishandhanani Sep 12, 2026
7d637ba
ci: refresh the kvbm bindings lockfile and fix a moved docs link
ishandhanani Sep 13, 2026
29a05b4
chore(router): finish the deferred mechanical cleanups
ishandhanani Sep 13, 2026
7b2f6c3
fix(router): track hint capability per partition and clear stale rank…
ishandhanani Sep 13, 2026
9a18ffb
fix(router): reconcile a catalog snapshot as one batch, retry a faile…
ishandhanani Sep 13, 2026
3cecd2a
fix(router): read hint capability from the published partition snapshot
ishandhanani Sep 13, 2026
29b2b9c
fix(router): construct the worker-selection policy once per partition
ishandhanani Sep 13, 2026
4cc6f49
fix(router): publish a completed partition move before the batch cont…
ishandhanani Sep 13, 2026
9e1e92a
fix(router): keep a lease when a session commit lands behind a compet…
ishandhanani Sep 13, 2026
2dfc529
fix(router): derive the embedded partition key in one place
ishandhanani Sep 13, 2026
d4be244
fix(router): uncount a joined lease released before its initializer c…
ishandhanani Sep 13, 2026
02165d7
feat(router): instrument the active-sequence ingress funnel
ishandhanani Sep 13, 2026
27c03ee
fix(router): label ingress metrics by worker type and pin the queued …
ishandhanani Sep 13, 2026
461fff6
fix(epp): address EPP-specific bugs and config layering issues in sel…
tmonty12 Sep 13, 2026
41d1e68
fix(router): address PR review comments on session affinity
tmonty12 Sep 13, 2026
3b660db
refactor(router): unify session affinity config into KvRouterConfig
tmonty12 Sep 13, 2026
6b6d5c2
docs(router): restore canonical doc comment on session affinity fields
tmonty12 Sep 13, 2026
ea7ab6e
fix(epp): address EPP-specific bugs and config layering issues in sel…
ishandhanani Sep 14, 2026
39da83d
Merge origin/main into idhanani/dyn-14310-selection-core-full
ishandhanani Sep 15, 2026
fd09b4b
revert(router): back out EPP affinity follow-up
ishandhanani Sep 15, 2026
71335cf
fix(router): validate session affinity DP ranks during failover
ishandhanani Sep 15, 2026
54c2076
perf(router): keep LoRA candidate filtering linear
ishandhanani Sep 15, 2026
800070d
perf(router): move the committed booking request ID
ishandhanani Sep 15, 2026
928321e
docs(router): clarify default lookup retention
ishandhanani Sep 15, 2026
0a477b1
fix(router): preserve affinity ownership across concurrent failover
ishandhanani Sep 15, 2026
0049ca3
perf(router): omit unused metadata from frontend leases
ishandhanani Sep 15, 2026
c71f3d3
refactor(router): defer standalone HTTP indexer integration
ishandhanani Sep 15, 2026
90557bb
test(bindings): update selection service lifecycle constructor call
ishandhanani Sep 15, 2026
3b40f4d
fix(router): preserve frontend accounting before discovery
ishandhanani Sep 16, 2026
9d1859a
fix(router): fence pending reservation claims by identity
ishandhanani Sep 16, 2026
6bef1c4
fix(router): yield during repeated affinity invalidation
ishandhanani Sep 16, 2026
e1878f6
Merge main into selection-core branch
ishandhanani Sep 16, 2026
0d80f5c
Merge branch 'main' into idhanani/dyn-14310-selection-core-full
ishandhanani Sep 16, 2026
8827c61
fix(router): address selection core review feedback
ishandhanani Sep 16, 2026
3335e4b
refactor(router): remove unused indexer lookup wrappers
ishandhanani Sep 16, 2026
3165ff2
Merge main into idhanani/dyn-14310-selection-core-full
ishandhanani Sep 16, 2026
5eaea24
Merge main into idhanani/dyn-14310-selection-core-full
ishandhanani Sep 17, 2026
c66159b
Merge latest main into idhanani/dyn-14310-selection-core-full
ishandhanani Sep 17, 2026
1b82fe2
perf(router): borrow membership snapshots during reconciliation
ishandhanani Sep 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/pre-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,13 @@ jobs:
working-directory: ${{ matrix.dir }}
# NOTE: --all-targets doesn't run doc tests
run: cargo test --locked --all-targets
- name: Check isolated kv-router feature builds
if: matrix.dir == '.'
working-directory: ${{ matrix.dir }}
# Workspace feature unification hides these; check each on its own.
run: |
cargo check --locked -p dynamo-kv-router --no-default-features --features standalone-slot-tracker
cargo check --locked -p dynamo-kv-router --no-default-features --features standalone-indexer
Comment thread
ishandhanani marked this conversation as resolved.
Resolved
- name: Run standalone slot-tracker feature tests
if: matrix.dir == '.'
working-directory: ${{ matrix.dir }}
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions deploy/inference-gateway/ext-proc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ name = "dynamo-ext-proc"
path = "src/bin/dynamo-ext-proc.rs"

[dependencies]
async-trait = { workspace = true }
dynamo-kv-router = { workspace = true, features = [
"metrics",
"runtime-protocols",
Expand Down
31 changes: 9 additions & 22 deletions deploy/inference-gateway/ext-proc/src/epp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -483,10 +483,6 @@ impl Router {
allowed_worker_ids: Option<HashSet<u64>>,
routing_constraints: RoutingConstraints,
) -> Result<PrefillReservation> {
if let Some(ref ids) = allowed_worker_ids {
self.prefill_router.register_workers(ids);
}

self.prefill_router
.reserve_prefill_worker(
reservation_id,
Expand Down Expand Up @@ -527,10 +523,6 @@ impl Router {
allowed_worker_ids: Option<HashSet<u64>>,
routing_constraints: RoutingConstraints,
) -> Result<(WorkerWithDpRank, u32)> {
if let Some(ref ids) = allowed_worker_ids {
self.decode_router.register_workers(ids);
}

let config_override = decode_router_config_override(is_disaggregated);

let outcome = self
Expand Down Expand Up @@ -934,8 +926,9 @@ fn indexed_endpoint_address(endpoint: &Endpoint) -> Option<String> {
/// The mode is exclusive, and so are the identities. A worker process picks one
/// `KubeDiscoveryTarget` from its own mode, so under container discovery
/// nothing registers under the bare pod identity — emitting it there would
/// invent a worker that `register_workers` upserts at zero load and zero KV
/// overlap, making it the most attractive candidate the scheduler sees.
/// put a worker id in `allowed_worker_ids` that no backend registered. The
/// scheduler filters unknown ids out, so the pod contributes nothing, and a
/// subset made only of such ids selects no worker at all.
/// `"main"` hashes to the pod identity (`hash_container_name`), so a pod whose
/// main container is Ready still contributes that id through the container
/// path; one whose main container is *not* Ready correctly contributes nothing
Expand Down Expand Up @@ -1392,11 +1385,9 @@ impl EndpointPicker for Router {
// Only pod discovery registers a worker under its pod identity.
// Under container discovery each engine container registers under
// its own (`KubeDiscoveryTarget::Container`), so a hand-built pod
// hash names a worker present in no registry: `register_workers`
// would upsert it at zero load and zero KV overlap, making it the
// scheduler's most attractive candidate, and the reverse lookup
// below would then fail to match and silently forward to
// `endpoints[0]`. The index is the one place that knows which
// hash names a worker present in no registry, which the scheduler
// filters out, leaving the subset short one candidate or empty.
// The index is the one place that knows which
// identity scheme is in effect (see `pod_worker_ids`), so ask it.
let wm: Vec<(u64, &Endpoint)> = {
let index = read_index(&self.worker_index);
Expand Down Expand Up @@ -2146,10 +2137,7 @@ mod tests {
}

/// Under pod discovery a worker registers under its pod identity alone, so
/// a pod's ready sidecars must contribute no worker ids. Emitting them
/// would invent workers no backend registered under: they miss
/// `register_workers`' discovery lookup, default to `(0, 1)` with no load
/// and no KV overlap, and so look maximally attractive to the scheduler.
/// a pod's ready sidecars must contribute no worker ids.
#[test]
fn pod_worker_ids_ignores_containers_under_pod_discovery() {
let pod = pod_mode_worker_pod();
Expand Down Expand Up @@ -2215,7 +2203,7 @@ mod tests {
}

/// End of the chain that made this matter: the index feeds
/// `subset_to_worker_ids` -> `allowed_worker_ids` -> `register_workers`,
/// `subset_to_worker_ids` -> `allowed_worker_ids` -> the scheduler,
/// so one backend pod must contribute exactly one worker id under pod
/// discovery rather than one per ready container.
#[test]
Expand Down Expand Up @@ -2450,8 +2438,7 @@ mod tests {
/// An externally supplied endpoint must resolve to the identity the
/// reflector actually holds. Under container discovery that is the engine
/// container's id, never `hash_pod_name` -- deriving the latter names a
/// worker no registry contains, which `register_workers` then upserts at
/// zero load as the scheduler's most attractive candidate.
/// worker no registry contains.
#[test]
fn external_endpoint_resolves_to_the_indexed_container_identity() {
let mut index = WorkerEndpointIndex::new(true);
Expand Down
7 changes: 6 additions & 1 deletion deploy/inference-gateway/ext-proc/src/epp_router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ use std::time::Duration;
use anyhow::Result;
use tokio::sync::Semaphore;

use dynamo_kv_router::services::selection::WorkerSelectionPolicyRegistry;
use dynamo_kv_router::services::selection::{SelectionError, WorkerSelectionPolicyRegistry};
use dynamo_llm::http::service::metadata::extract_metadata_from_header_pairs;
use dynamo_llm::protocols::agents::HEADER_DYNAMO_SESSION_ID;
use dynamo_llm::protocols::common::extensions::{
AgentHints, HEADER_REQUEST_PRIORITY, HEADER_REQUEST_STRICT_PRIORITY, resolve_request_priority,
};
Expand Down Expand Up @@ -368,6 +369,7 @@ impl EndpointPicker for EppRouter {
model_name: self.model_name.clone(),
reservation_id: reservation_id.clone(),
token_ids: tokens,
session_id: first_header(&req.headers, HEADER_DYNAMO_SESSION_ID).map(str::to_owned),
// `None` on the ordinary path: the selector schedules over its
// catalog; `Some` only carries an Envoy subset constraint.
allowed_worker_ids: allowed,
Expand All @@ -383,6 +385,9 @@ impl EndpointPicker for EppRouter {

let resp = match self.selector.select_and_reserve(select_req).await {
Ok(resp) => resp,
Err(SelectionError::BadRequest(message)) => {
return Err(PickError::InvalidRequest(message));
}
Err(e) => return Err(PickError::RoutingFailed(e.to_string())),
};

Expand Down
39 changes: 39 additions & 0 deletions deploy/inference-gateway/ext-proc/src/epp_standalone_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,17 @@ pub struct EppStandaloneConfig {
/// KV-cache block size; MUST equal the inference engine block size.
#[validate(range(min = 1, message = "DYN_KV_CACHE_BLOCK_SIZE must be >= 1"))]
pub block_size: u32,
/// Data-parallel ranks per worker pod. Only `1` is accepted: the EPP routes
/// by worker and cannot convey the selected rank to the pod.
#[validate(range(
min = 1,
max = 1,
message = "DYN_EPP_DATA_PARALLEL_SIZE must be 1; multi-rank standalone serving is unsupported"
))]
pub data_parallel_size: u32,
/// Port distance between consecutive data-parallel ranks' KV event ports.
#[validate(range(min = 1, message = "DYN_EPP_KV_EVENT_PORT_STRIDE must be >= 1"))]
pub kv_event_port_stride: u16,
/// KV zmq event port.
#[validate(range(min = 1))]
pub kv_event_port: u16,
Expand All @@ -163,6 +174,15 @@ pub struct EppStandaloneConfig {
/// throughput throttle). Excess requests are shed with a 503, not queued.
#[validate(range(min = 1, message = "DYN_EPP_MAX_INFLIGHT_REQUESTS must be >= 1"))]
pub max_inflight_requests: usize,
/// Pin each `x-dynamo-session-id` to the worker that served it for this
Comment thread
ishandhanani marked this conversation as resolved.
Resolved
/// long after its last request (`DYN_EPP_SESSION_AFFINITY_TTL_SECS`).
/// `None` disables session affinity.
#[validate(range(
min = 1.0,
max = 31536000.0,
message = "DYN_EPP_SESSION_AFFINITY_TTL_SECS must be between 1 and 31536000 seconds"
))]
pub session_affinity_ttl_secs: Option<f64>,
}

impl EppStandaloneConfig {
Expand Down Expand Up @@ -216,13 +236,17 @@ impl EppStandaloneConfig {
)?
.unwrap_or(DEFAULT_TOKENIZER_MAX_RESPONSE_BYTES),
block_size: opt_parse::<u32>(get, "DYN_KV_CACHE_BLOCK_SIZE")?.unwrap_or(0),
data_parallel_size: opt_parse::<u32>(get, "DYN_EPP_DATA_PARALLEL_SIZE")?.unwrap_or(1),
kv_event_port_stride: opt_parse::<u16>(get, "DYN_EPP_KV_EVENT_PORT_STRIDE")?
.unwrap_or(1),
kv_event_port: opt_parse::<u16>(get, "DYN_EPP_KV_EVENT_PORT")?
.unwrap_or(DEFAULT_KV_EVENT_PORT),
replay_port: opt_parse::<u16>(get, "DYN_EPP_KV_EVENT_REPLAY_PORT")?,
total_kv_blocks: opt_parse::<u64>(get, "DYN_EPP_TOTAL_KV_BLOCKS")?,
max_num_batched_tokens: opt_parse::<u64>(get, "DYN_EPP_MAX_NUM_BATCHED_TOKENS")?,
max_inflight_requests: opt_parse::<usize>(get, "DYN_EPP_MAX_INFLIGHT_REQUESTS")?
.unwrap_or(DEFAULT_MAX_INFLIGHT_REQUESTS),
session_affinity_ttl_secs: opt_parse::<f64>(get, "DYN_EPP_SESSION_AFFINITY_TTL_SECS")?,
})
}

Expand Down Expand Up @@ -543,6 +567,21 @@ mod tests {
);
}

#[test]
fn data_parallel_size_above_one_fails() {
assert!(
parse_cfg(&[
("DYN_EPP_INFERENCE_POOL_NAME", "vllm-qwen-pool"),
("POD_NAMESPACE", "inference"),
("DYN_MODEL_NAME", "Qwen/Qwen3-0.6B"),
("DYN_EPP_TOKENIZER_SERVICE_URL", "http://vllm-render:8000"),
("DYN_EPP_TOKENIZER_PROTOCOL", "vllm-render"),
("DYN_EPP_DATA_PARALLEL_SIZE", "2"),
])
.is_err()
);
}

#[test]
fn replay_port_is_optional_and_uses_the_kv_event_name() {
let cfg = parse_cfg(&[
Expand Down
2 changes: 1 addition & 1 deletion deploy/inference-gateway/ext-proc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ pub use picker::{Endpoint, EndpointPicker, PickResult, RequestInfo, ResponseUsag
pub use pod_discovery::{PodDiscovery, RawWorker};
pub use render_http::RenderError;
pub use runner::run;
pub use selector::{OverlapSummary, SelectRequest, SelectResponse, Selector, WorkerRegistration};
pub use selector::{OverlapSummary, SelectRequest, SelectResponse, Selector};
pub use server::ExtProcServer;
pub use sglang_renderer_client::SglangRendererClient;
pub use topology_adapter::{RegistrationDefaults, TopologyAdapter};
Expand Down
Loading
Loading