Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions components/src/dynamo/common/backend/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ class LlmRegistration:
# them so the frontend's PrefillRouter can take its bootstrap path.
bootstrap_host: Optional[str] = None
bootstrap_port: Optional[int] = None
enable_eagle: bool = False


@dataclass
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

from __future__ import annotations

from dataclasses import asdict
from unittest.mock import MagicMock

import pytest
Expand All @@ -35,6 +36,8 @@
reason="dynamo._core.backend not built — run `maturin develop` first",
)

from dynamo.common.backend.engine import LlmRegistration # noqa: E402


def test_module_exposes_expected_classes():
"""The five binding classes must all be importable as top-level
Expand Down Expand Up @@ -71,6 +74,7 @@ def test_engine_config_required_model_only():
assert cfg.llm is None


@pytest.mark.unified
def test_engine_config_full_kwargs_round_trip_through_getters():
cfg = backend.EngineConfig(
model="m2",
Expand All @@ -83,6 +87,7 @@ def test_engine_config_full_kwargs_round_trip_through_getters():
total_kv_blocks=1000,
max_num_seqs=64,
max_num_batched_tokens=2048,
enable_eagle=True,
),
)
assert cfg.model == "m2"
Expand All @@ -95,6 +100,26 @@ def test_engine_config_full_kwargs_round_trip_through_getters():
assert llm.total_kv_blocks == 1000
assert llm.max_num_seqs == 64
assert llm.max_num_batched_tokens == 2048
assert llm.enable_eagle is True


@pytest.mark.unified
def test_llm_registration_preserves_legacy_positional_arguments():
llm = backend.LlmRegistration(2048, 16, 1000, 64, 2048, 2, 1, "host", 9000)
assert llm.bootstrap_host == "host"
assert llm.bootstrap_port == 9000
assert llm.enable_eagle is False


@pytest.mark.parametrize("kwargs", [{}, {"enable_eagle": True}])
@pytest.mark.unified
def test_llm_registration_dataclass_matches_binding(kwargs):
registration = LlmRegistration(**kwargs)
cfg = backend.EngineConfig(
model="eagle-model",
llm=backend.LlmRegistration(**asdict(registration)),
)
assert cfg.llm.enable_eagle is kwargs.get("enable_eagle", False)


def test_worker_config_minimum_args():
Expand Down
1 change: 1 addition & 0 deletions lib/backend-common/examples/mocker/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ impl LLMEngine for MockerBackend {
max_num_batched_tokens: self.engine_args.max_num_batched_tokens.map(|v| v as u64),
data_parallel_size: None,
data_parallel_start_rank: None,
enable_eagle: false,
// Mocker has no real KV transport, so it never advertises a
// bootstrap address.
bootstrap_host: None,
Expand Down
4 changes: 4 additions & 0 deletions lib/backend-common/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ pub struct LlmRegistration {
/// owns a sub-range; the router enumerates
/// `[start, start + data_parallel_size)`.
pub data_parallel_start_rank: Option<u32>,
/// Engine emits bigram-keyed KV events (Eagle speculative decoding).
/// Published as `ModelRuntimeConfig::enable_eagle` so the router hashes
/// prompts the same way.
pub enable_eagle: bool,
/// Bootstrap host advertised to decode peers. Backends with an internal
/// KV-transport handshake leave it `None`. When host+port are set, `Worker`
/// publishes them for the frontend's `PrefillRouter` bootstrap path.
Expand Down
3 changes: 3 additions & 0 deletions lib/backend-common/src/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2053,6 +2053,7 @@ async fn build_local_model(
max_num_batched_tokens: llm.max_num_batched_tokens,
data_parallel_size: llm.data_parallel_size.unwrap_or(1),
data_parallel_start_rank: llm.data_parallel_start_rank.unwrap_or(0),
enable_eagle: llm.enable_eagle,
tool_call_parser: config.tool_call_parser.clone(),
reasoning_parser: config.reasoning_parser.clone(),
exclude_tools_when_tool_choice_none: config.exclude_tools_when_tool_choice_none,
Expand Down Expand Up @@ -2407,6 +2408,7 @@ mod tests {
total_kv_blocks: Some(100),
max_num_seqs: Some(16),
max_num_batched_tokens: Some(8192),
enable_eagle: true,
..Default::default()
}),
..EngineConfig::default()
Expand All @@ -2421,6 +2423,7 @@ mod tests {
assert_eq!(runtime_config.total_kv_blocks, Some(100));
assert_eq!(runtime_config.max_num_seqs, Some(16));
assert_eq!(runtime_config.max_num_batched_tokens, Some(8192));
assert!(runtime_config.enable_eagle);
assert_eq!(runtime_config.tool_call_parser.as_deref(), Some("kimi_k2"));
assert_eq!(runtime_config.reasoning_parser.as_deref(), Some("kimi_k25"));
assert_eq!(
Expand Down
9 changes: 9 additions & 0 deletions lib/bindings/python/rust/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ impl LlmRegistration {
data_parallel_start_rank = None,
bootstrap_host = None,
bootstrap_port = None,
enable_eagle = false,
))]
#[allow(clippy::too_many_arguments)]
fn new(
Expand All @@ -241,6 +242,7 @@ impl LlmRegistration {
data_parallel_start_rank: Option<u32>,
bootstrap_host: Option<String>,
bootstrap_port: Option<u16>,
enable_eagle: bool,
) -> Self {
Self {
inner: RsLlmRegistration {
Expand All @@ -251,6 +253,7 @@ impl LlmRegistration {
max_num_batched_tokens,
data_parallel_size,
data_parallel_start_rank,
enable_eagle,
bootstrap_host,
bootstrap_port,
},
Expand Down Expand Up @@ -293,6 +296,11 @@ impl LlmRegistration {
fn bootstrap_port(&self) -> Option<u16> {
self.inner.bootstrap_port
}

#[getter]
fn enable_eagle(&self) -> bool {
self.inner.enable_eagle
}
}

#[pyclass(module = "dynamo._core.backend", name = "EngineConfig")]
Expand Down Expand Up @@ -928,6 +936,7 @@ impl PyEngineCore {
max_num_batched_tokens: opt_attr::<u64>(&v, "max_num_batched_tokens")?,
data_parallel_size: opt_attr::<u32>(&v, "data_parallel_size")?,
data_parallel_start_rank: opt_attr::<u32>(&v, "data_parallel_start_rank")?,
enable_eagle: opt_attr::<bool>(&v, "enable_eagle")?.unwrap_or(false),
bootstrap_host: opt_attr::<String>(&v, "bootstrap_host")?,
bootstrap_port: opt_attr::<u16>(&v, "bootstrap_port")?,
}),
Expand Down
3 changes: 3 additions & 0 deletions lib/bindings/python/src/dynamo/_core.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -3379,6 +3379,7 @@ class backend:
data_parallel_start_rank: Optional[int] = None,
bootstrap_host: Optional[str] = None,
bootstrap_port: Optional[int] = None,
enable_eagle: bool = False,
) -> None: ...
@property
def context_length(self) -> Optional[int]: ...
Expand All @@ -3398,6 +3399,8 @@ class backend:
def bootstrap_host(self) -> Optional[str]: ...
@property
def bootstrap_port(self) -> Optional[int]: ...
@property
def enable_eagle(self) -> bool: ...

class EngineConfig:
def __init__(
Expand Down
72 changes: 71 additions & 1 deletion lib/sidecar/sglang/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,22 @@ fn kv_event_connect_host(
Ok(bare_host.to_string())
}

/// Same predicate as SGLang's `SpeculativeAlgorithm.is_eagle()`, which is
/// what switches its radix cache (and so its KV events) to bigram keys.
/// `NEXTN` covers older builds that don't normalize it to `EAGLE`.
fn sglang_eagle_enabled(server_info: &Value) -> bool {
server_info
.get("speculative_algorithm")
.and_then(Value::as_str)
.map(str::trim)
.is_some_and(|algorithm| {
matches!(
algorithm.to_ascii_uppercase().as_str(),
"EAGLE" | "EAGLE3" | "FROZEN_KV_MTP" | "NEXTN"
)
})
}

fn is_deepseek_v4_arch(model_info: &Value) -> bool {
model_info
.get("architectures")
Expand Down Expand Up @@ -902,6 +918,8 @@ fn build_engine_config(
));
}

let enable_eagle = sglang_eagle_enabled(&discovery.server_info);

let mut runtime_data = HashMap::new();
runtime_data.insert(
"grpc_service".to_string(),
Expand Down Expand Up @@ -929,6 +947,7 @@ fn build_engine_config(
max_num_batched_tokens,
data_parallel_size,
data_parallel_start_rank,
enable_eagle,
bootstrap_host: mode.is_prefill().then_some(bootstrap_host).flatten(),
bootstrap_port: mode.is_prefill().then_some(bootstrap_port).flatten(),
}),
Expand All @@ -943,7 +962,7 @@ mod tests {
use super::{
DisaggregationMode, DiscoveredKvEventSource, Discovery, build_engine_config,
discover_kv_event_sources, hicache_native_offloading_capacity,
resolve_bootstrap_host_with_local,
resolve_bootstrap_host_with_local, sglang_eagle_enabled,
};

fn discovery(server_info: serde_json::Value) -> Discovery {
Expand All @@ -957,6 +976,57 @@ mod tests {
}
}

#[test]
fn eagle_enabled_tracks_sglang_is_eagle_predicate() {
for algorithm in [
"EAGLE",
"eagle",
"EAGLE3",
"FROZEN_KV_MTP",
"NEXTN",
" Eagle ",
] {
assert!(
sglang_eagle_enabled(&json!({"speculative_algorithm": algorithm})),
"{algorithm:?} keys the radix cache by bigrams and must advertise enable_eagle"
);
}
for server_info in [
json!({}),
json!({"speculative_algorithm": null}),
json!({"speculative_algorithm": "NONE"}),
json!({"speculative_algorithm": "NGRAM"}),
json!({"speculative_algorithm": "STANDALONE"}),
json!({"speculative_algorithm": 3}),
] {
assert!(
!sglang_eagle_enabled(&server_info),
"{server_info} must not advertise enable_eagle"
);
}
}

#[test]
fn registration_advertises_enable_eagle_for_eagle_servers() {
let eagle = build_engine_config(
&discovery(json!({"speculative_algorithm": "EAGLE", "page_size": 256})),
DisaggregationMode::Aggregated,
None,
None,
)
.unwrap();
assert!(eagle.llm.unwrap().enable_eagle);

let plain = build_engine_config(
&discovery(json!({"page_size": 256})),
DisaggregationMode::Aggregated,
None,
None,
)
.unwrap();
assert!(!plain.llm.unwrap().enable_eagle);
}

#[test]
fn explicit_bootstrap_host_takes_precedence() {
let host = resolve_bootstrap_host_with_local(
Expand Down
Loading