Skip to content

Commit ad7d5bc

Browse files
committed
feat(router): add GMS placement protocol surfaces
Signed-off-by: Maksim Khadkevich <mkhadkevich@nvidia.com>
1 parent 6185adc commit ad7d5bc

22 files changed

Lines changed: 819 additions & 15 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

components/src/dynamo/common/configuration/groups/kv_router_args.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"router_track_output_blocks",
3737
"router_assume_kv_reuse",
3838
"router_track_prefill_tokens",
39+
"router_gms_decode_transfer",
3940
"router_prefill_load_model",
4041
"router_snapshot_threshold",
4142
"router_reset_states",
@@ -116,6 +117,7 @@ class KvRouterConfigBase(ConfigBase):
116117
router_track_output_blocks: bool
117118
router_assume_kv_reuse: bool
118119
router_track_prefill_tokens: bool
120+
router_gms_decode_transfer: bool
119121
router_prefill_load_model: str
120122
router_snapshot_threshold: int
121123
router_reset_states: bool
@@ -296,6 +298,18 @@ def add_arguments(self, parser) -> None:
296298
"prefill-token load, queue pressure, and active_prefill_tokens metrics."
297299
),
298300
)
301+
add_negatable_bool_argument(
302+
g,
303+
flag_name="--router-gms-decode-transfer",
304+
env_var="DYN_ROUTER_GMS_DECODE_TRANSFER",
305+
default=False,
306+
dest="router_gms_decode_transfer",
307+
help=(
308+
"[EXPERIMENTAL] KV Router: Enable GMS decode-to-decode KV transfer "
309+
"orchestration for GMS-capable aggregated decode workers. Disabled "
310+
"by default so vanilla Dynamo routing behavior is unchanged."
311+
),
312+
)
299313
add_argument(
300314
g,
301315
flag_name="--router-prefill-load-model",

components/src/dynamo/common/tests/configuration/test_kv_router_args.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,78 @@ def test_load_aware_frontend_implies_kv_router_mode() -> None:
414414
assert config.router_assume_kv_reuse is False
415415

416416

417+
def test_bulwark_gateway_endpoint_requires_private_namespace() -> None:
418+
parser = argparse.ArgumentParser()
419+
FrontendArgGroup().add_arguments(parser)
420+
421+
args = parser.parse_args(
422+
["--bulwark-gateway-endpoint", "dyn://public.backend.generate"]
423+
)
424+
config = FrontendConfig.from_cli_args(args)
425+
426+
with pytest.raises(ValueError, match="requires --namespace"):
427+
config.validate()
428+
429+
430+
def test_bulwark_gateway_endpoint_rejects_non_endpoint_path() -> None:
431+
parser = argparse.ArgumentParser()
432+
FrontendArgGroup().add_arguments(parser)
433+
434+
args = parser.parse_args(
435+
[
436+
"--bulwark-gateway-endpoint",
437+
"public.backend.generate",
438+
"--namespace",
439+
"private-workers",
440+
]
441+
)
442+
config = FrontendConfig.from_cli_args(args)
443+
444+
with pytest.raises(ValueError, match="dyn://"):
445+
config.validate()
446+
447+
448+
def test_bulwark_gateway_endpoint_validates() -> None:
449+
parser = argparse.ArgumentParser()
450+
FrontendArgGroup().add_arguments(parser)
451+
452+
args = parser.parse_args(
453+
[
454+
"--bulwark-gateway-endpoint",
455+
"dyn://public.backend.generate",
456+
"--namespace-prefix",
457+
"private-workers",
458+
]
459+
)
460+
config = FrontendConfig.from_cli_args(args)
461+
config.validate()
462+
463+
assert config.bulwark_gateway_endpoint == "dyn://public.backend.generate"
464+
assert config.namespace_prefix == "private-workers"
465+
466+
467+
def test_bulwark_gateway_exact_namespace_uses_worker_suffix(
468+
monkeypatch: pytest.MonkeyPatch,
469+
) -> None:
470+
monkeypatch.setenv("DYN_NAMESPACE_WORKER_SUFFIX", "replica-a")
471+
parser = argparse.ArgumentParser()
472+
FrontendArgGroup().add_arguments(parser)
473+
474+
args = parser.parse_args(
475+
[
476+
"--bulwark-gateway-endpoint",
477+
"dyn://public.backend.generate",
478+
"--namespace",
479+
"private-workers",
480+
]
481+
)
482+
config = FrontendConfig.from_cli_args(args)
483+
config.validate()
484+
485+
assert config.namespace == "private-workers-replica-a"
486+
assert config.namespace_prefix is None
487+
488+
417489
def test_frontend_admission_control_defaults_to_none(monkeypatch) -> None:
418490
"""Default --admission-control is 'none': busy thresholds are cleared
419491
even though the underlying threshold flags have non-None defaults."""

components/src/dynamo/frontend/frontend_args.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
add_negatable_bool_argument,
2626
env_or_default,
2727
)
28+
from dynamo.common.utils.namespace import get_worker_namespace
2829

2930
from . import __version__
3031

@@ -95,6 +96,8 @@ def validate(self) -> None:
9596
self.apply_load_aware_preset()
9697

9798
if self.bulwark_gateway_endpoint:
99+
if self.namespace and not self.namespace_prefix:
100+
self.namespace = get_worker_namespace(self.namespace)
98101
if not self.bulwark_gateway_endpoint.startswith("dyn://"):
99102
raise ValueError(
100103
"--bulwark-gateway-endpoint must be a dyn://namespace.component.endpoint path"

lib/bindings/c/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,9 @@ fn kv_router_config_from_env() -> KvRouterConfig {
680680
if let Some(v) = env_bool("DYN_ROUTER_TRACK_PREFILL_TOKENS") {
681681
cfg.router_track_prefill_tokens = v;
682682
}
683+
if let Some(v) = env_bool("DYN_ROUTER_GMS_DECODE_TRANSFER") {
684+
cfg.router_gms_decode_transfer = v;
685+
}
683686
if let Some(v) = env_f64("DYN_ROUTER_QUEUE_THRESHOLD") {
684687
cfg.router_queue_threshold = Some(v);
685688
}
@@ -695,6 +698,7 @@ fn kv_router_config_from_env() -> KvRouterConfig {
695698
router_track_active_blocks = cfg.router_track_active_blocks,
696699
router_track_output_blocks = cfg.router_track_output_blocks,
697700
router_track_prefill_tokens = cfg.router_track_prefill_tokens,
701+
router_gms_decode_transfer = cfg.router_gms_decode_transfer,
698702
router_queue_threshold = ?cfg.router_queue_threshold,
699703
router_predicted_ttl_secs = ?cfg.router_predicted_ttl_secs,
700704
queue_depth_tiers_unbounded = cfg.router_queue_by_incoming_missing_isl.is_unbounded(),

lib/bindings/python/rust/llm/entrypoint.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ impl AicPerfConfig {
175175
#[pymethods]
176176
impl KvRouterConfig {
177177
#[new]
178-
#[pyo3(signature = (overlap_score_weight=None, host_cache_hit_weight=0.75, disk_cache_hit_weight=0.25, router_temperature=0.0, use_kv_events=true, durable_kv_events=false, router_replica_sync=false, router_track_active_blocks=true, router_track_output_blocks=false, router_assume_kv_reuse=true, router_track_prefill_tokens=true, router_prefill_load_model="none", router_snapshot_threshold=1000000, router_reset_states=false, router_ttl_secs=120.0, router_queue_threshold=Some(16.0), router_event_threads=4, router_queue_policy="fcfs", use_remote_indexer=false, serve_indexer=false, shared_cache_multiplier=0.0, shared_cache_type="none", router_predicted_ttl_secs=None, *, overlap_score_credit=1.0, prefill_load_scale=1.0, router_queue_by_incoming_missing_isl=None))]
178+
#[pyo3(signature = (overlap_score_weight=None, host_cache_hit_weight=0.75, disk_cache_hit_weight=0.25, router_temperature=0.0, use_kv_events=true, durable_kv_events=false, router_replica_sync=false, router_track_active_blocks=true, router_track_output_blocks=false, router_assume_kv_reuse=true, router_track_prefill_tokens=true, router_prefill_load_model="none", router_snapshot_threshold=1000000, router_reset_states=false, router_ttl_secs=120.0, router_queue_threshold=Some(16.0), router_event_threads=4, router_queue_policy="fcfs", use_remote_indexer=false, serve_indexer=false, shared_cache_multiplier=0.0, shared_cache_type="none", router_predicted_ttl_secs=None, *, overlap_score_credit=1.0, prefill_load_scale=1.0, router_queue_by_incoming_missing_isl=None, router_gms_decode_transfer=false))]
179179
#[allow(clippy::too_many_arguments)]
180180
fn new(
181181
overlap_score_weight: Option<f64>,
@@ -204,6 +204,7 @@ impl KvRouterConfig {
204204
mut overlap_score_credit: f64,
205205
mut prefill_load_scale: f64,
206206
router_queue_by_incoming_missing_isl: Option<Vec<(usize, usize)>>,
207+
router_gms_decode_transfer: bool,
207208
) -> PyResult<Self> {
208209
if let Some(value) = overlap_score_weight {
209210
apply_deprecated_overlap_score_weight(
@@ -226,6 +227,7 @@ impl KvRouterConfig {
226227
router_track_output_blocks,
227228
router_assume_kv_reuse,
228229
router_track_prefill_tokens,
230+
router_gms_decode_transfer,
229231
router_prefill_load_model: router_prefill_load_model
230232
.parse::<RsRouterPrefillLoadModel>()
231233
.map_err(PyValueError::new_err)?,

lib/bindings/python/rust/llm/kv.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,58 @@ impl KvEventPublisher {
370370
})
371371
}
372372

373+
#[pyo3(signature = (source_nixl_agent_name, source_nixl_agent_metadata_hex, blocks, source_nixl_ip=None, source_nixl_listen_port=None, dp_rank=None))]
374+
#[allow(clippy::too_many_arguments)]
375+
fn publish_gms_placement_stored(
376+
&self,
377+
py: Python,
378+
source_nixl_agent_name: String,
379+
source_nixl_agent_metadata_hex: String,
380+
blocks: Bound<PyAny>,
381+
source_nixl_ip: Option<String>,
382+
source_nixl_listen_port: Option<u16>,
383+
dp_rank: Option<DpRank>,
384+
) -> PyResult<()> {
385+
let blocks: Vec<GmsPlacementBlock> = depythonize(&blocks).map_err(to_pyerr)?;
386+
let inner = self.inner.clone();
387+
let dp_rank = dp_rank.unwrap_or(self.dp_rank);
388+
py.allow_threads(|| {
389+
inner
390+
.publish_gms_placement(
391+
GmsPlacementEventData::Stored(GmsPlacementStoreData {
392+
source_nixl_agent_name,
393+
source_nixl_agent_metadata_hex,
394+
source_nixl_ip,
395+
source_nixl_listen_port,
396+
blocks,
397+
}),
398+
StorageTier::External,
399+
dp_rank,
400+
)
401+
.map_err(to_pyerr)
402+
})
403+
}
404+
405+
#[pyo3(signature = (content_hashes_hex, dp_rank=None))]
406+
fn publish_gms_placement_removed(
407+
&self,
408+
py: Python,
409+
content_hashes_hex: Vec<String>,
410+
dp_rank: Option<DpRank>,
411+
) -> PyResult<()> {
412+
let inner = self.inner.clone();
413+
let dp_rank = dp_rank.unwrap_or(self.dp_rank);
414+
py.allow_threads(|| {
415+
inner
416+
.publish_gms_placement(
417+
GmsPlacementEventData::Removed(GmsPlacementRemoveData { content_hashes_hex }),
418+
StorageTier::External,
419+
dp_rank,
420+
)
421+
.map_err(to_pyerr)
422+
})
423+
}
424+
373425
fn shutdown(&mut self) {
374426
// If no other Arc clones exist, shut down eagerly.
375427
// Otherwise the Drop impl handles cleanup when the last reference is freed.

lib/bindings/python/rust/llm/local_model.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,20 @@ impl ModelRuntimeConfig {
159159
Ok(())
160160
}
161161

162+
fn set_gms_placement_enabled(&mut self) -> PyResult<()> {
163+
self.inner.set_gms_placement_enabled().map_err(to_pyerr)
164+
}
165+
166+
fn set_gms_daemon_socket(&mut self, daemon_socket: String) -> PyResult<()> {
167+
self.inner
168+
.set_gms_daemon_socket(daemon_socket)
169+
.map_err(to_pyerr)
170+
}
171+
172+
fn set_gms_control_enabled(&mut self) -> PyResult<()> {
173+
self.inner.set_gms_control_enabled().map_err(to_pyerr)
174+
}
175+
162176
fn set_tensor_model_config(
163177
&mut self,
164178
_py: Python<'_>,

lib/bindings/python/src/dynamo/_core.pyi

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,18 @@ class ModelRuntimeConfig:
712712
"""Set structural tag schema mode ("auto" or "strict")."""
713713
...
714714

715+
def set_gms_daemon_socket(self, daemon_socket: str) -> None:
716+
"""Advertise this worker's GMS KV daemon socket for router orchestration"""
717+
...
718+
719+
def set_gms_placement_enabled(self) -> None:
720+
"""Advertise that this worker publishes GMS placements on the KV event plane"""
721+
...
722+
723+
def set_gms_control_enabled(self) -> None:
724+
"""Deprecated alias for set_gms_placement_enabled."""
725+
...
726+
715727
def set_disaggregated_endpoint(
716728
self,
717729
bootstrap_host: str | None = None,
@@ -1032,6 +1044,26 @@ class KvEventPublisher:
10321044
"""
10331045
...
10341046

1047+
def publish_gms_placement_stored(
1048+
self,
1049+
source_nixl_agent_name: str,
1050+
source_nixl_agent_metadata_hex: str,
1051+
blocks: List[Dict[str, Any]],
1052+
source_nixl_ip: Optional[str] = None,
1053+
source_nixl_listen_port: Optional[int] = None,
1054+
dp_rank: Optional[int] = None,
1055+
) -> None:
1056+
"""Publish GMS placement descriptors through the KV event plane."""
1057+
...
1058+
1059+
def publish_gms_placement_removed(
1060+
self,
1061+
content_hashes_hex: List[str],
1062+
dp_rank: Optional[int] = None,
1063+
) -> None:
1064+
"""Remove GMS placement descriptors from the router's event-plane index."""
1065+
...
1066+
10351067
def shutdown(self) -> None:
10361068
"""
10371069
Shuts down the event publisher, stopping any background tasks.
@@ -1673,6 +1705,7 @@ class KvRouterConfig:
16731705
overlap_score_credit: float = 1.0,
16741706
prefill_load_scale: float = 1.0,
16751707
router_queue_by_incoming_missing_isl: Optional[list[tuple[int, int]]] = None,
1708+
router_gms_decode_transfer: bool = False,
16761709
) -> None:
16771710
"""
16781711
Create a KV router configuration.
@@ -1697,6 +1730,7 @@ class KvRouterConfig:
16971730
router_assume_kv_reuse: Assume KV cache reuse when tracking active blocks (default: True).
16981731
When True, computes actual block hashes. When False, generates random hashes.
16991732
router_track_prefill_tokens: Include prompt-side prefill tokens in active load accounting (default: True).
1733+
router_gms_decode_transfer: Enable experimental GMS decode-to-decode transfer orchestration (default: False).
17001734
router_prefill_load_model: Prompt-side prefill load model (default: "none").
17011735
"none" keeps static prompt load accounting.
17021736
"aic" decays the oldest active prefill request using AIC-predicted duration.

lib/kv-router/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,5 @@ rstest_reuse = "0.7.0"
6363
serde_json = { workspace = true }
6464
tokio = { workspace = true, features = ["rt", "macros", "time", "test-util"] }
6565
dynamo-tokens = { workspace = true }
66+
bincode = { version = "1" }
6667
tower = { version = "0.5", features = ["util"] }

0 commit comments

Comments
 (0)