Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
c796c28
add taints
michaelfeil May 14, 2026
5cd49a9
Merge remote-tracking branch 'upstream/main' into mf/taints
michaelfeil May 14, 2026
bcb124c
add taints
michaelfeil May 14, 2026
9bc8f64
add schema
michaelfeil May 14, 2026
c164501
make taints simplified
michaelfeil May 14, 2026
70c5c71
do not, commit the codexx file ...
michaelfeil May 14, 2026
304a5b6
fix the tests
michaelfeil May 14, 2026
848070a
fix the fmt
michaelfeil May 14, 2026
044328e
Merge branch 'main' into mf/taints
michaelfeil May 14, 2026
6e934bf
fix the fmt
michaelfeil May 14, 2026
bd2a681
fix the fmt
michaelfeil May 14, 2026
7d25ea9
adding a single good regression test and the complaint about the c bi…
michaelfeil May 14, 2026
7f731f8
added taints to queue, and push-router
michaelfeil May 14, 2026
da3bce6
added tests to make sure the queue is passing
michaelfeil May 14, 2026
dd566cc
rename from Taints to RoutingContraints
michaelfeil May 14, 2026
7c3911a
rename from Taints to RoutingContraints
michaelfeil May 14, 2026
04e78c8
rename required to required_taints
michaelfeil May 14, 2026
28e853f
add routing contraints to RouterRequest which is the whole reason we …
michaelfeil May 14, 2026
179203b
taints are now hashsets
michaelfeil May 14, 2026
5a4050d
python api should be hashset
michaelfeil May 14, 2026
250562b
python api should be hashset -> now also in core.pyi
michaelfeil May 14, 2026
bffdef8
pinned worker + required taints -> error instead of warning
michaelfeil May 14, 2026
ccd5d3b
pinned worker + required taints -> error instead of warning FMT
michaelfeil May 14, 2026
93f702e
add a prefered taint bias
michaelfeil May 14, 2026
3414556
add a prefered taint bias as hashmap, with individual scores
michaelfeil May 15, 2026
5c122a9
address comments
michaelfeil May 15, 2026
8b7487f
address comments
michaelfeil May 15, 2026
bf2571b
verify routing constraints with selector
michaelfeil May 15, 2026
7469e89
add a comment
michaelfeil May 15, 2026
32b141c
fmt
michaelfeil May 15, 2026
65596fe
add improved heuristics support around preferred routing targets
michaelfeil May 15, 2026
ce236a3
add eligibility helper to bundle checks with usually inline
michaelfeil May 15, 2026
d45ead4
add re-export and test utils import fix
michaelfeil May 15, 2026
0b263d5
Merge branch 'main' into mf/taints
michaelfeil May 15, 2026
b471aff
make api more clear to only multiply if useful
michaelfeil May 15, 2026
1922ae4
make api more clear to only multiply if useful
michaelfeil May 15, 2026
f802890
ove pinned worker in the api, scheduled context helper to make sure q…
michaelfeil May 15, 2026
64ccf6d
add comment why heuristic is choosen
michaelfeil May 15, 2026
6aca329
add comment why heuristic is choosen
michaelfeil May 15, 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
43 changes: 31 additions & 12 deletions lib/bindings/c/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,7 @@ impl RouterHandles {
///
/// When `allowed_worker_ids` is Some, only workers in that set are considered.
/// Returns worker_id on success.
#[expect(clippy::too_many_arguments)]
async fn query_prefill_worker(
&self,
tokens: &[u32],
Expand All @@ -458,6 +459,7 @@ impl RouterHandles {
lora_name: Option<String>,
priority_jump: f64,
allowed_worker_ids: Option<HashSet<WorkerId>>,
routing_constraints: RoutingConstraints,
) -> Result<(u64, Option<u32>), QueryRouterResult> {
if let Some(ref ids) = allowed_worker_ids {
self.prefill_router.register_workers(ids);
Expand All @@ -471,6 +473,7 @@ impl RouterHandles {
lora_name,
priority_jump,
allowed_worker_ids,
routing_constraints,
)
.await
.map_err(|e| {
Expand Down Expand Up @@ -500,6 +503,7 @@ impl RouterHandles {
is_disaggregated: bool,
priority_jump: f64,
allowed_worker_ids: Option<HashSet<WorkerId>>,
routing_constraints: RoutingConstraints,
) -> Result<(WorkerWithDpRank, u32), QueryRouterResult> {
if let Some(ref ids) = allowed_worker_ids {
self.decode_router.register_workers(ids);
Expand Down Expand Up @@ -529,6 +533,7 @@ impl RouterHandles {
priority_jump,
None,
allowed_worker_ids,
routing_constraints,
)
.await
.map_err(|e| {
Expand Down Expand Up @@ -1124,15 +1129,15 @@ pub unsafe extern "C" fn free_routing_result(result: *mut CRoutingResult) {
/// Parse a JSON request string, apply the chat template, tokenize, and lift
/// the router-relevant `priority_jump` out of `nvext.agent_hints.priority`.
///
/// Returns `(token_ids, priority_jump)` on success, or a `QueryRouterResult`
/// Returns `(token_ids, priority_jump, routing_constraints)` on success, or a `QueryRouterResult`
/// error code. `priority_jump` is `0.0` when no hint is present. This mirrors
/// the standalone Dynamo preprocessor lift in `lib/llm/src/preprocessor.rs`
/// so the GAIE/EPP path produces the same queue ordering as a non-EPP
/// deployment.
unsafe fn preprocess_request(
handles: &RouterHandles,
request_json: *const c_char,
) -> Result<(Vec<u32>, f64), QueryRouterResult> {
) -> Result<(Vec<u32>, f64, RoutingConstraints), QueryRouterResult> {
let preprocessor = match &handles.preprocessor {
Some(p) => p,
None => {
Expand All @@ -1156,6 +1161,11 @@ unsafe fn preprocess_request(
};

let priority_jump = extract_priority_jump(&request);
let routing_constraints = request
.nvext
.as_ref()
.and_then(|nvext| nvext.routing_constraints.clone())
.unwrap_or_default();
Comment thread
coderabbitai[bot] marked this conversation as resolved.

let formatted_prompt = match preprocessor.apply_template(&request) {
Ok(Some(prompt)) => prompt,
Expand All @@ -1182,7 +1192,7 @@ unsafe fn preprocess_request(
"[EPP-TOKENIZE] Tokenized prompt in C bindings (this is the ONLY tokenization)"
);

Ok((token_ids, priority_jump))
Ok((token_ids, priority_jump, routing_constraints))
}

/// Parse pods JSON into an optional set of allowed worker IDs.
Expand Down Expand Up @@ -1268,10 +1278,11 @@ pub unsafe extern "C" fn route_prefill_request(

let handles = unsafe { &*handle };

let (tokens, priority_jump) = match unsafe { preprocess_request(handles, request_json) } {
Ok(t) => t,
Err(code) => return code,
};
let (tokens, priority_jump, routing_constraints) =
match unsafe { preprocess_request(handles, request_json) } {
Ok(t) => t,
Err(code) => return code,
};

let allowed_worker_ids = unsafe { parse_pods_filter(pods_json) };

Expand All @@ -1284,6 +1295,7 @@ pub unsafe extern "C" fn route_prefill_request(
None,
priority_jump,
allowed_worker_ids,
routing_constraints,
)
.await?;

Expand Down Expand Up @@ -1346,16 +1358,23 @@ pub unsafe extern "C" fn route_decode_request(

let handles = unsafe { &*handle };

let (tokens, priority_jump) = match unsafe { preprocess_request(handles, request_json) } {
Ok(t) => t,
Err(code) => return code,
};
let (tokens, priority_jump, routing_constraints) =
match unsafe { preprocess_request(handles, request_json) } {
Ok(t) => t,
Err(code) => return code,
};

let allowed_worker_ids = unsafe { parse_pods_filter(pods_json) };

let result = handles.runtime.secondary().block_on(async {
let (decode_worker, _overlap_blocks) = handles
.query_decode_worker(&tokens, is_disaggregated, priority_jump, allowed_worker_ids)
.query_decode_worker(
&tokens,
is_disaggregated,
priority_jump,
allowed_worker_ids,
routing_constraints,
)
.await?;

tracing::info!(
Expand Down
3 changes: 2 additions & 1 deletion lib/bindings/python/rust/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ use dynamo_llm::{self as llm_rs};

use crate::llm::entrypoint::RouterConfig as PyRouterConfig;

use crate::llm::local_model::ModelRuntimeConfig;
use crate::llm::local_model::{ModelRuntimeConfig, RoutingConstraints};
use crate::llm::preprocessor::{MediaDecoder, MediaFetcher};

#[pyclass(eq, eq_int)]
Expand Down Expand Up @@ -183,6 +183,7 @@ fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<llm::kv::WorkerMetricsPublisher>()?;
m.add_class::<llm::model_card::ModelDeploymentCard>()?; // Internal: only in _internal, not public API
m.add_class::<llm::local_model::ModelRuntimeConfig>()?;
m.add_class::<RoutingConstraints>()?;
m.add_class::<llm::preprocessor::MediaDecoder>()?;
m.add_class::<llm::preprocessor::MediaFetcher>()?;
m.add_class::<llm::kv::OverlapScores>()?;
Expand Down
11 changes: 8 additions & 3 deletions lib/bindings/python/rust/llm/kv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use std::sync::atomic::AtomicU32;
use std::sync::mpsc;
use tokio_stream::StreamExt;

use super::local_model::RoutingConstraints;
use super::*;
use crate::Endpoint;
#[cfg(feature = "kv-indexer")]
Expand Down Expand Up @@ -957,7 +958,7 @@ impl KvRouter {
}

#[allow(clippy::too_many_arguments)]
#[pyo3(signature = (token_ids, model, stop_conditions=None, sampling_options=None, output_options=None, router_config_override=None, worker_id=None, dp_rank=None, extra_args=None, block_mm_infos=None, multi_modal_data=None, mm_routing_info=None))]
#[pyo3(signature = (token_ids, model, stop_conditions=None, sampling_options=None, output_options=None, router_config_override=None, worker_id=None, dp_rank=None, extra_args=None, block_mm_infos=None, multi_modal_data=None, mm_routing_info=None, routing_constraints=None))]
fn generate<'p>(
&self,
py: Python<'p>,
Expand All @@ -973,6 +974,7 @@ impl KvRouter {
block_mm_infos: Option<PyObject>,
multi_modal_data: Option<PyObject>,
mm_routing_info: Option<PyObject>,
routing_constraints: Option<RoutingConstraints>,
) -> PyResult<Bound<'p, PyAny>> {
// Depythonize the options with defaults
let stop_conditions: StopConditions = if let Some(obj) = stop_conditions {
Expand Down Expand Up @@ -1048,10 +1050,11 @@ impl KvRouter {
.tracker(Some(tracker.clone()));

// Set routing hints if worker_id or dp_rank is provided
if worker_id.is_some() || dp_rank.is_some() {
if worker_id.is_some() || dp_rank.is_some() || routing_constraints.is_some() {
let routing = llm_rs::protocols::common::preprocessor::RoutingHints {
backend_instance_id: worker_id,
dp_rank,
routing_constraints: routing_constraints.map(Into::into),
..Default::default()
};
request_builder.routing(Some(routing));
Expand Down Expand Up @@ -1087,7 +1090,7 @@ impl KvRouter {
}

#[allow(clippy::too_many_arguments)]
#[pyo3(signature = (token_ids, router_config_override=None, request_id=None, update_indexer=false, block_mm_infos=None, lora_name=None))]
#[pyo3(signature = (token_ids, router_config_override=None, request_id=None, update_indexer=false, block_mm_infos=None, lora_name=None, routing_constraints=None))]
fn best_worker<'p>(
&self,
py: Python<'p>,
Expand All @@ -1097,6 +1100,7 @@ impl KvRouter {
update_indexer: bool,
block_mm_infos: Option<PyObject>,
lora_name: Option<String>,
routing_constraints: Option<RoutingConstraints>,
) -> PyResult<Bound<'p, PyAny>> {
let router_config_override = if let Some(obj) = router_config_override {
let override_config: RouterConfigOverride =
Expand Down Expand Up @@ -1125,6 +1129,7 @@ impl KvRouter {
0.0,
None,
None, // allowed_worker_ids: pass via RoutingHints in PreprocessedRequest path
routing_constraints.map(Into::into).unwrap_or_default(),
)
.await
.map_err(to_pyerr)?;
Expand Down
55 changes: 55 additions & 0 deletions lib/bindings/python/rust/llm/local_model.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,55 @@
// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

use std::collections::{HashMap, HashSet};

use super::*;
use dynamo_kv_router::protocols::RoutingConstraints as RsRoutingConstraints;
use llm_rs::local_model::runtime_config::DisaggregatedEndpoint as RsDisaggregatedEndpoint;
use llm_rs::local_model::runtime_config::ModelRuntimeConfig as RsModelRuntimeConfig;

#[pyclass]
#[derive(Clone, Debug, Default)]
pub struct RoutingConstraints {
Comment thread
michaelfeil marked this conversation as resolved.
#[pyo3(get, set)]
pub required_taints: HashSet<String>,
#[pyo3(get, set)]
pub preferred_taints: HashMap<String, f32>,
}

#[pymethods]
impl RoutingConstraints {
#[new]
#[pyo3(signature = (required_taints=None, preferred_taints=None))]
fn new(
required_taints: Option<HashSet<String>>,
preferred_taints: Option<HashMap<String, f32>>,
) -> Self {
Self {
required_taints: required_taints.unwrap_or_default(),
preferred_taints: preferred_taints.unwrap_or_default(),
}
}
}

impl From<RoutingConstraints> for RsRoutingConstraints {
fn from(value: RoutingConstraints) -> Self {
Self {
required_taints: value.required_taints,
preferred_taints: value.preferred_taints,
}
}
}

impl From<RsRoutingConstraints> for RoutingConstraints {
fn from(value: RsRoutingConstraints) -> Self {
Self {
required_taints: value.required_taints,
preferred_taints: value.preferred_taints,
}
}
}

#[pyclass]
#[derive(Clone, Debug, Default)]
pub struct ModelRuntimeConfig {
Expand Down Expand Up @@ -73,6 +118,11 @@ impl ModelRuntimeConfig {
self.inner.enable_eagle = enable_eagle;
}

#[setter]
fn set_taints(&mut self, taints: HashSet<String>) {
self.inner.taints = taints;
}

fn set_engine_specific(&mut self, key: &str, value: String) -> PyResult<()> {
let value: serde_json::Value = serde_json::from_str(&value).map_err(to_pyerr)?;
self.inner
Expand Down Expand Up @@ -182,4 +232,9 @@ impl ModelRuntimeConfig {
fn enable_eagle(&self) -> bool {
self.inner.enable_eagle
}

#[getter]
fn taints(&self) -> HashSet<String> {
self.inner.taints.clone()
}
}
25 changes: 25 additions & 0 deletions lib/bindings/python/src/dynamo/_core.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ from typing import (
List,
Literal,
Optional,
Set,
Tuple,
)

Expand Down Expand Up @@ -514,6 +515,7 @@ class ModelRuntimeConfig:
data_parallel_size: int
enable_local_indexer: bool
enable_eagle: bool
taints: Set[str]
runtime_data: dict[str, Any]
tensor_model_config: Any | None
bootstrap_host: str | None
Expand Down Expand Up @@ -545,6 +547,26 @@ class ModelRuntimeConfig:
"""Get the tensor model configuration."""
...

class RoutingConstraints:
Comment thread
michaelfeil marked this conversation as resolved.
"""
Request-side routing constraints.

``required_taints`` is a hard eligibility filter.
``preferred_taints`` maps taint -> signed weight.
Positive weights prefer matching workers, negative weights avoid them,
and ``0.0`` is neutral. Matching weights are summed and squashed with
``tanh``, so opposite preferences cancel before Dynamo converts the
bounded bias into a strictly positive score multiplier.
"""
required_taints: Set[str]
preferred_taints: Dict[str, float]

def __init__(
self,
required_taints: Optional[Set[str]] = None,
preferred_taints: Optional[Dict[str, float]] = None,
) -> None: ...

class OverlapScores:
"""
A collection of prefix matching scores of workers for a given token ids.
Expand Down Expand Up @@ -1998,6 +2020,7 @@ class KvRouter:
block_mm_infos: Optional[List[Optional[Dict[str, Any]]]] = None,
multi_modal_data: Optional[JsonLike] = None,
mm_routing_info: Optional[JsonLike] = None,
routing_constraints: Optional[RoutingConstraints] = None,
) -> AsyncIterator[JsonLike]:
"""
Generate text using the KV-aware router.
Expand Down Expand Up @@ -2026,6 +2049,7 @@ class KvRouter:
mm_routing_info: Optional structured routing-only multimodal payload
(e.g., {"routing_token_ids": [...], "block_mm_infos": [...]})
used by router selection without changing execution token_ids.
routing_constraints: Optional request routing constraints used to constrain or prefer tainted workers.

Returns:
An async iterator yielding generation responses
Expand Down Expand Up @@ -2059,6 +2083,7 @@ class KvRouter:
update_indexer: bool = False,
block_mm_infos: Optional[List[Optional[Dict[str, Any]]]] = None,
lora_name: Optional[str] = None,
routing_constraints: Optional[RoutingConstraints] = None,
) -> Tuple[int, int, int]:
"""
Find the best matching worker for the given tokens.
Expand Down
1 change: 1 addition & 0 deletions lib/bindings/python/src/dynamo/llm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from dynamo._core import ReasoningConfig as ReasoningConfig
from dynamo._core import RouterConfig as RouterConfig
from dynamo._core import RouterMode as RouterMode
from dynamo._core import RoutingConstraints as RoutingConstraints
from dynamo._core import SglangArgs as SglangArgs
from dynamo._core import WorkerMetricsPublisher as WorkerMetricsPublisher
from dynamo._core import compute_block_hash_for_seq as compute_block_hash_for_seq
Expand Down
Loading
Loading