Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
fd0d572
fix(mm-routing): MM-aware KV routing for Phi-3, Qwen2-VL, Qwen2.5-VL
krishung5 May 12, 2026
d52d504
perf(mm-routing): pass formatted_prompt by reference to avoid per-req…
krishung5 May 13, 2026
b216723
test(mm-routing): strong-gate agg_router profiles on lightseek init +…
krishung5 May 14, 2026
253f73e
test(mm-routing): settle kv-router index between repeats to fix Phi-3…
krishung5 May 27, 2026
688b675
Revert "test(mm-routing): settle kv-router index between repeats to f…
krishung5 May 27, 2026
5032de3
fix(mm-routing): mirror vLLM decode-roundtrip for Phi-3 routing tokens
krishung5 May 27, 2026
0af2150
fix(mm-routing): disable MM-aware routing when DYN_TOKENIZER=fastokens
krishung5 May 27, 2026
324fb0f
chore(mm-routing): cargo fmt fixup
krishung5 May 27, 2026
261a871
test(mm-routing): metric-based router_kv_hit_rate gate for MM agg_rou…
krishung5 May 27, 2026
e50b044
Merge remote-tracking branch 'origin/main' into krish/phi3-mm-routing…
krishung5 May 28, 2026
ab42746
Merge remote-tracking branch 'origin/main' into krish/phi3-mm-routing…
krishung5 May 29, 2026
646fe91
Merge remote-tracking branch 'origin/main' into krish/phi3-mm-routing…
krishung5 Jun 1, 2026
19d263d
test(mm-routing): rename require_lightseek_init -> require_rust_proce…
krishung5 Jun 1, 2026
211d975
test(mm-routing): drop lightseek wording from PR-added log strings
krishung5 Jun 1, 2026
523c02b
refactor(mm-routing): centralize HF config parsing in lightseek_mm.rs
krishung5 Jun 1, 2026
79d2057
refactor(mm-routing): collapse redundant image-token fields
krishung5 Jun 1, 2026
4024fdc
refactor(mm-routing): unify BOS handling + Phi-3 family guard in spli…
krishung5 Jun 1, 2026
8c02d52
test(mm-routing): compose router_kv_hit_rate metric name from canonic…
krishung5 Jun 1, 2026
d6a21c4
test(mm-routing): simplify router-histogram scraper to return (sum, c…
krishung5 Jun 1, 2026
d2bef12
test(tokenizers): add merge_special_tokens_from_config gate test + wa…
krishung5 Jun 1, 2026
21f02de
test(mm-routing): narrow _scrape_router_kv_hit_rate except to Request…
krishung5 Jun 1, 2026
b759d58
docs(mm-routing): fix stale Qwen2-VL placeholder-token comment
krishung5 Jun 1, 2026
098de96
fix(mm-routing): apply tokenizer_config.json merge before extracting …
krishung5 Jun 1, 2026
8f6d7a6
Merge remote-tracking branch 'origin/main' into krish/phi3-mm-routing…
krishung5 Jun 1, 2026
ced13a9
style(mm-routing): cargo fmt + clippy fixes for pre-merge gates
krishung5 Jun 1, 2026
2d7a88a
docs(mm-routing): add TODO marker for RouterMetricsAssertion mixin fo…
krishung5 Jun 1, 2026
769f6ea
fix(test): align rust mm-router init-log assertion with renamed log line
krishung5 Jun 2, 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
2 changes: 1 addition & 1 deletion examples/backends/vllm/launch/agg_multimodal_router.sh
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ NATS_SERVER="${NATS_SERVER:-nats://127.0.0.1:4222}"
ETCD_ENDPOINTS="${ETCD_ENDPOINTS:-http://127.0.0.1:2379}"
VLLM_SYSTEM_PORT_BASE="${VLLM_SYSTEM_PORT_BASE:-18081}"
KV_EVENTS_PORT_BASE="${KV_EVENTS_PORT_BASE:-5557}"
DYN_LOG_VAL="${DYN_LOG:-info,lightseek_mm=debug,dynamo_kv_router::scheduling=debug}"
DYN_LOG_VAL="${DYN_LOG:-info,lightseek_mm=debug,dynamo_kv_router::scheduling=debug,dynamo_llm::kv_router=debug}"

# Pass-through extra args for `python -m dynamo.vllm`.
VLLM_EXTRA_ARGS="${VLLM_EXTRA_ARGS:-}"
Expand Down
25 changes: 18 additions & 7 deletions lib/llm/src/model_card.rs
Original file line number Diff line number Diff line change
Expand Up @@ -969,7 +969,7 @@ impl ModelDeploymentCard {
// extracting special-token strings. `FastTokenizer` does not re-expose
// `get_added_tokens_decoder`, so we must capture specials from the raw
// HF tokenizer before any swap.
let hf = HfTokenizer::from_file(p)
let mut hf = HfTokenizer::from_file(p)
.inspect_err(|err| {
if let Some(serde_err) = err.downcast_ref::<serde_json::Error>()
&& let Ok(contents) = std::fs::read_to_string(p)
Expand All @@ -979,14 +979,27 @@ impl ModelDeploymentCard {
})
.map_err(anyhow::Error::msg)
.with_context(|| p.display().to_string())?;

// Apply the tokenizer_config.json special-token merge eagerly so
// `extract_hf_special_tokens` below sees the same specials the
// wrapped tokenizer will use. Without this the L1 prefix cache's
// boundary list would diverge from the actual tokenizer
// (e.g. Qwen2-VL's `<|image_pad|>` would be in the tokenizer
// but missing from the cache specials), letting chat prefixes
// straddle a special-token boundary and reducing hit rate.
if let Some(model_dir) = p.parent() {
crate::tokenizers::hf::merge_special_tokens_from_config(&mut hf, model_dir);
}
// Hold onto specials before any move of `hf`.
let specials: Vec<String> = if cache_enabled {
extract_hf_special_tokens(&hf)
} else {
Vec::new()
};

// Merge already applied above; just wrap.
let wrap_hf =
|hf: HfTokenizer| crate::tokenizers::HuggingFaceTokenizer::from_tokenizer(hf);

// Pick the inner backend.
let raw: Arc<dyn crate::tokenizers::traits::Tokenizer> = if use_fast {
if let Some(path_str) = p.to_str() {
Expand All @@ -1000,20 +1013,18 @@ impl ModelDeploymentCard {
%e,
"Failed to load fastokens, falling back to HuggingFace"
);
Arc::new(crate::tokenizers::HuggingFaceTokenizer::from_tokenizer(
hf,
))
Arc::new(wrap_hf(hf))
}
}
} else {
tracing::warn!(
path = %p.display(),
"Tokenizer path contains non-UTF-8 characters, skipping fastokens; falling back to HuggingFace"
);
Arc::new(crate::tokenizers::HuggingFaceTokenizer::from_tokenizer(hf))
Arc::new(wrap_hf(hf))
}
} else {
Arc::new(crate::tokenizers::HuggingFaceTokenizer::from_tokenizer(hf))
Arc::new(wrap_hf(hf))
};

if cache_enabled {
Expand Down
469 changes: 296 additions & 173 deletions lib/llm/src/preprocessor.rs

Large diffs are not rendered by default.

155 changes: 130 additions & 25 deletions lib/llm/src/preprocessor/lightseek_mm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,20 @@ impl LightseekMmCounter {
/// - `tokenizer.json` or `config.json` is missing or unparseable, or
/// - no `ModelProcessorSpec` matches the model (caller should fall back to
/// text-prefix routing).
///
/// Standalone wrapper around [`resolve_image_token_id_with_config`]. Prefer
/// [`resolve_routing_tokens`] when also fetching the chat-template placeholder
/// or BOS token (one config-parse pass instead of two).
pub fn resolve_image_token_id(model_id: &str, model_dir: &Path) -> Option<TokenIdType> {
let config = read_json(model_dir, "config.json")?;
resolve_image_token_id_with_config(model_id, model_dir, &config)
}

fn resolve_image_token_id_with_config(
model_id: &str,
model_dir: &Path,
config: &serde_json::Value,
) -> Option<TokenIdType> {
// Try the HuggingFace fast tokenizer first; fall back to a no-op
// tokenizer when `tokenizer.json` is missing (Kimi-K2.5 ships only
// `tiktoken.model`, for example). Specs that read the placeholder
Expand Down Expand Up @@ -174,34 +187,10 @@ pub fn resolve_image_token_id(model_id: &str, model_dir: &Path) -> Option<TokenI
None => &null_tokenizer,
};

let config_path = model_dir.join("config.json");
let config_json = std::fs::read_to_string(&config_path)
.map_err(|e| {
tracing::warn!(
target: "mm_routing",
config = %config_path.display(),
err = %e,
"lightseek: failed to read config.json"
);
e
})
.ok()?;
let config: serde_json::Value = serde_json::from_str(&config_json)
.map_err(|e| {
tracing::warn!(
target: "mm_routing",
config = %config_path.display(),
err = %e,
"lightseek: failed to parse config.json"
);
e
})
.ok()?;

let metadata = ModelMetadata {
model_id,
tokenizer,
config: &config,
config,
};

let spec = MODEL_REGISTRY.lookup(&metadata)?;
Expand All @@ -227,6 +216,122 @@ pub fn resolve_image_token_id(model_id: &str, model_dir: &Path) -> Option<TokenI
Some(id as TokenIdType)
}

/// Bundle of routing-side token info resolved from a model's HF JSON
/// configs. All fields default to `None` when the corresponding lookup
/// fails — callers disable the respective routing path without erroring.
///
/// Built by [`resolve_routing_tokens`]; reads `config.json` and
/// `tokenizer_config.json` at most once each.
pub struct RoutingTokens {
/// Image-placeholder token id resolved via `ModelProcessorSpec`
/// (per-family `config.json` field). `None` disables MM-aware routing.
pub image_token_id: Option<TokenIdType>,
/// Token id the chat template emits per image. Read from `config.json`'s
/// literal `image_token_id` field, falling back to `image_token_id`
/// above. Equals `image_token_id` for most VLMs; Qwen2-VL / Qwen2.5-VL
/// emit `<|image_pad|>` here while the per-patch id is `<|vision_pad|>`.
pub chat_placeholder_token_id: Option<TokenIdType>,
/// `bos_token` string from `tokenizer_config.json` when
/// `add_bos_token: true`. Caller encodes via its model tokenizer to
/// produce the routing-side prepend id. `None` for models that don't
/// prepend BOS.
pub bos_token_string: Option<String>,
}

/// Resolve all routing-side token info from a model directory in a single
/// pass. Reads `config.json` once for the per-spec image id + chat-template
/// placeholder, and `tokenizer_config.json` once for BOS. Replaces the
/// in-`preprocessor.rs` `read_image_token_id_from_config` /
/// `read_bos_token_from_config` helpers so config parsing lives next to
/// the rest of the MM-routing token resolution.
pub fn resolve_routing_tokens(model_id: &str, model_dir: &Path) -> RoutingTokens {
let config = read_json(model_dir, "config.json");
let tokenizer_config = read_json(model_dir, "tokenizer_config.json");

let image_token_id = config
.as_ref()
.and_then(|c| resolve_image_token_id_with_config(model_id, model_dir, c));
let chat_placeholder_token_id = config
.as_ref()
.and_then(extract_chat_placeholder_from_config)
.or(image_token_id);
let bos_token_string = tokenizer_config
.as_ref()
.and_then(extract_bos_token_from_tokenizer_config);

RoutingTokens {
image_token_id,
chat_placeholder_token_id,
bos_token_string,
}
}

/// Read + parse a JSON file under `model_dir`. Warns on read or parse
/// failure (missing files are silent — many models legitimately lack
/// `tokenizer_config.json`). Returns `None` on any error.
fn read_json(model_dir: &Path, filename: &str) -> Option<serde_json::Value> {
let path = model_dir.join(filename);
let raw = match std::fs::read_to_string(&path) {
Ok(s) => s,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return None,
Err(e) => {
tracing::warn!(
target: "mm_routing",
path = %path.display(),
err = %e,
"lightseek: failed to read {filename}"
);
return None;
}
};
match serde_json::from_str(&raw) {
Ok(v) => Some(v),
Err(e) => {
tracing::warn!(
target: "mm_routing",
path = %path.display(),
err = %e,
"lightseek: failed to parse {filename}"
);
None
}
}
}

/// Read the literal `image_token_id` field from a pre-parsed `config.json`.
/// Used by Qwen2-VL / Qwen2.5-VL where the chat-template-emitted placeholder
/// differs from the per-patch expansion token returned by the spec.
fn extract_chat_placeholder_from_config(config: &serde_json::Value) -> Option<TokenIdType> {
config
.get("image_token_id")
.and_then(|x| x.as_u64())
.and_then(|id| u32::try_from(id).ok())
}

/// Return the `bos_token` string from a pre-parsed `tokenizer_config.json`
/// when `add_bos_token: true`. The routing-side sequence must prepend it to
/// match the backend's HF-processor output (Phi-3-vision and other
/// `LlamaTokenizer`-family models). Returns `None` otherwise.
fn extract_bos_token_from_tokenizer_config(cfg: &serde_json::Value) -> Option<String> {
if !cfg
.get("add_bos_token")
.and_then(|x| x.as_bool())
.unwrap_or(false)
{
return None;
}
// `bos_token` is usually a plain string ("<s>") but the HF schema also
// allows it to be an `AddedToken` dict — handle both.
cfg.get("bos_token").and_then(|x| match x {
serde_json::Value::String(s) => Some(s.clone()),
serde_json::Value::Object(o) => o
.get("content")
.and_then(|c| c.as_str())
.map(|s| s.to_owned()),
_ => None,
})
}

#[cfg(test)]
mod tests {
//! Contract tests against the upstream lightseek registry. Pin the
Expand Down
Loading
Loading