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
4 changes: 4 additions & 0 deletions .github/codeowners/areas.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ areas:
- docs/fern/pages/reference/components/pool-relay-configuration.md
- lib/bindings/python/rust/llm/pool_relay.rs
- lib/global-discovery-client/
- lib/global-discovery-service/
- lib/global-ingress/
- lib/global-router/
- lib/global-router-shared/
- lib/llm/src/pool_relay.rs
- lib/llm/src/pool_relay/
- lib/llm/tests/fixtures/relay-sources-canonical.json
Expand Down
6 changes: 5 additions & 1 deletion CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,13 @@
/components/src/dynamo/thunderagent_router/ @ai-dynamo/dynamo-router-codeowners

# === global-router (@ai-dynamo/dynamo-global-router-codeowners) ===
/examples/global-router/cdk/ @ai-dynamo/dynamo-global-router-codeowners
/lib/global-router/ @ai-dynamo/dynamo-global-router-codeowners
/lib/global-ingress/ @ai-dynamo/dynamo-global-router-codeowners
/lib/global-router-shared/ @ai-dynamo/dynamo-global-router-codeowners
/lib/global-view-receiver/ @ai-dynamo/dynamo-global-router-codeowners
/examples/global-router/cdk/ @ai-dynamo/dynamo-global-router-codeowners
/lib/global-discovery-client/ @ai-dynamo/dynamo-global-router-codeowners
/lib/global-discovery-service/ @ai-dynamo/dynamo-global-router-codeowners
/components/src/dynamo/pool_relay/ @ai-dynamo/dynamo-global-router-codeowners

# === backend-vllm (@ai-dynamo/dynamo-backend-vllm-codeowners) ===
Expand Down
42 changes: 42 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ members = [
"lib/kv-router",
"lib/global-discovery-client",
"lib/global-discovery-service",
"lib/global-ingress",
"lib/global-router",
"lib/global-router-shared",
"lib/router-plugins/catalog",
"lib/router-plugins/builtin",
Expand Down
37 changes: 29 additions & 8 deletions lib/global-discovery-client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
use std::time::Duration;

use reqwest::StatusCode;
use serde::de::DeserializeOwned;
use thiserror::Error;
use url::Url;

use crate::replicas::ReplicaEndpoint;
use crate::wire::{PoolRegistration, RoutersResponse};
use crate::watch::{Completeness, Snapshot};
use crate::wire::{PoolListing, PoolRegistration, PoolsResponse, RoutersResponse};

#[derive(Debug, Error)]
#[non_exhaustive]
Expand Down Expand Up @@ -74,7 +76,28 @@ impl GlobalDiscoveryClient {
/// Each call returns the full current set. If any entry does not parse,
/// the whole call fails, so a caller never acts on a partial list.
pub async fn routers(&self) -> Result<Vec<ReplicaEndpoint>, DiscoveryClientError> {
let url = &self.routers_url;
let body: RoutersResponse = self.get_json(&self.routers_url).await?;
Ok(body.routers)
}

/// Calls `GET /pools` and returns every pool the service lists. A Global
/// Router calls this to learn the pools and their frontends.
///
/// Like [`Self::routers`], one entry that does not parse fails the whole
/// call.
pub async fn pools(&self) -> Result<Snapshot<PoolListing>, DiscoveryClientError> {
let body: PoolsResponse = self.get_json(&self.pools_url).await?;
Ok(Snapshot {
items: body.pools,
completeness: if body.complete {
Completeness::Exact
} else {
Completeness::Estimated
},
})
}

async fn get_json<T: DeserializeOwned>(&self, url: &Url) -> Result<T, DiscoveryClientError> {
let response = self.send(self.http.get(url.clone()), url).await?;
let body = response
.bytes()
Expand All @@ -83,12 +106,10 @@ impl GlobalDiscoveryClient {
url: url.clone(),
source,
})?;
let body: RoutersResponse =
serde_json::from_slice(&body).map_err(|source| DiscoveryClientError::Decode {
url: url.clone(),
source,
})?;
Ok(body.routers)
serde_json::from_slice(&body).map_err(|source| DiscoveryClientError::Decode {
url: url.clone(),
source,
})
}

/// Calls `POST /pools` to register a pool or refresh its registration.
Expand Down
10 changes: 5 additions & 5 deletions lib/global-discovery-client/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

//! Client for the two calls a PoolRelay makes to the Global Discovery
//! Service: listing every Global Router replica
//! ([`GlobalDiscoveryClient::routers`]) and registering its pool
//! ([`GlobalDiscoveryClient::register_pool`]).
//! Client for the Global Discovery Service. A PoolRelay lists every Global
//! Router replica ([`GlobalDiscoveryClient::routers`]) and registers its pool
//! ([`GlobalDiscoveryClient::register_pool`]). A Global Router lists every
//! pool ([`GlobalDiscoveryClient::pools`]).
//!
//! The crate also defines the read-side types that the client and the service
//! share: the router replica types and the list-and-watch events.
Expand All @@ -17,4 +17,4 @@ mod wire;
pub use client::{DiscoveryClientError, GlobalDiscoveryClient};
pub use replicas::{ReplicaEndpoint, ReplicaEvent, ReplicaId};
pub use watch::{Completeness, Snapshot, WatchEvent};
pub use wire::{Capacity, PoolKey, PoolLocation, PoolRegistration};
pub use wire::{Capacity, PoolKey, PoolListing, PoolLocation, PoolRegistration};
27 changes: 27 additions & 0 deletions lib/global-discovery-client/src/wire.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,33 @@ pub(crate) struct RoutersResponse {
pub routers: Vec<ReplicaEndpoint>,
}

#[derive(Debug, Deserialize)]
pub(crate) struct PoolsResponse {
pub pools: Vec<PoolListing>,
/// True when the service lists every pool.
pub complete: bool,
}

/// One pool in the `GET /pools` response.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
pub struct PoolListing {
pub key: PoolKey,
pub location: PoolLocation,
/// `None` when the service knows only the pool's name, as with MCS.
pub model: Option<String>,
#[serde(default)]
pub capabilities: Vec<String>,
/// Local frontends the Global Router dispatches requests to.
#[serde(default)]
pub frontends: Vec<Url>,
#[serde(default)]
pub capacity: Vec<Capacity>,
/// The only Relay allowed to stream this pool's state, when the service
/// knows it.
#[serde(default)]
pub relay_identity: Option<String>,
}

/// `POST /pools` request body. A PoolRelay sends it on every registration.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PoolRegistration {
Expand Down
128 changes: 128 additions & 0 deletions lib/global-discovery-client/tests/router_calls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

use std::time::Duration;

use axum::http::header;
use axum::routing::get;
use dynamo_global_discovery_client::{
Capacity, Completeness, DiscoveryClientError, GlobalDiscoveryClient, PoolKey, PoolListing,
PoolLocation,
};
use tokio::net::TcpListener;
use url::Url;

/// A client for a service that answers `GET /discovery/pools` with `body`.
async fn client(body: &'static str) -> GlobalDiscoveryClient {
let app = axum::Router::new().route(
"/discovery/pools",
get(move || async move { ([(header::CONTENT_TYPE, "application/json")], body) }),
);
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap();
tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
let base_url = Url::parse(&format!("http://{address}/discovery")).unwrap();
GlobalDiscoveryClient::new(base_url, Duration::from_secs(2)).unwrap()
}

#[tokio::test]
async fn lists_every_pool() {
// The second pool is what the Kubernetes stores report: a name and a
// Relay, but no model or frontends.
let client = client(
r#"{ "complete": true, "pools": [
{
"key": { "site_id": "aws-west", "namespace": "dynamo", "dgd_name": "mocker-west-1" },
"location": { "region": "us-west-2", "availability_zone": null, "cluster": "aws-west", "datacenter": null },
"model": "Qwen/Qwen3-0.6B",
"capabilities": ["aggregated"],
"frontends": ["http://10.1.22.20:8000/"],
"capacity": [{ "sku": "H100", "count": 8 }],
"relay_identity": null,
"added_later": "ignored"
},
{
"key": { "site_id": "aws-ohio", "namespace": "dynamo", "dgd_name": "mocker-ohio-1" },
"location": { "region": "us-east-2", "availability_zone": null, "cluster": null, "datacenter": null },
"model": null,
"relay_identity": "10.0.3.4"
}
] }"#,
)
.await;

let snapshot = client.pools().await.unwrap();
assert_eq!(snapshot.completeness, Completeness::Exact);
assert_eq!(
snapshot.items,
[
PoolListing {
key: PoolKey {
site_id: "aws-west".into(),
namespace: "dynamo".into(),
dgd_name: "mocker-west-1".into(),
},
location: PoolLocation {
region: "us-west-2".into(),
availability_zone: None,
cluster: Some("aws-west".into()),
datacenter: None,
},
model: Some("Qwen/Qwen3-0.6B".into()),
capabilities: vec!["aggregated".into()],
frontends: vec!["http://10.1.22.20:8000/".parse().unwrap()],
capacity: vec![Capacity {
sku: "H100".into(),
count: 8,
}],
relay_identity: None,
},
PoolListing {
key: PoolKey {
site_id: "aws-ohio".into(),
namespace: "dynamo".into(),
dgd_name: "mocker-ohio-1".into(),
},
location: PoolLocation {
region: "us-east-2".into(),
availability_zone: None,
cluster: None,
datacenter: None,
},
model: None,
capabilities: Vec::new(),
frontends: Vec::new(),
capacity: Vec::new(),
relay_identity: Some("10.0.3.4".into()),
},
]
);
}

#[tokio::test]
async fn a_partial_list_is_estimated() {
let client = client(r#"{ "complete": false, "pools": [] }"#).await;

let snapshot = client.pools().await.unwrap();
assert_eq!(snapshot.completeness, Completeness::Estimated);
assert!(snapshot.items.is_empty());
}

#[tokio::test]
async fn one_bad_frontend_fails_the_whole_list() {
let client = client(
r#"{ "complete": true, "pools": [ {
"key": { "site_id": "aws-west", "namespace": "dynamo", "dgd_name": "mocker-west-1" },
"location": { "region": "us-west-2", "availability_zone": null, "cluster": null, "datacenter": null },
"model": "Qwen/Qwen3-0.6B",
"frontends": ["http://10.1.22.20:99999/"]
} ] }"#,
)
.await;

let error = client.pools().await.unwrap_err();
assert!(
matches!(error, DiscoveryClientError::Decode { .. }),
"{error:?}"
);
}
44 changes: 44 additions & 0 deletions lib/global-ingress/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

[package]
name = "dynamo-global-ingress"
description = "Global Frontend: serves the OpenAI API and forwards each request to a pool frontend"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true

[[bin]]
name = "global-ingress"
path = "src/main.rs"

[dependencies]
dynamo-global-discovery-client = { path = "../global-discovery-client" }
dynamo-global-router = { path = "../global-router" }
dynamo-global-router-shared = { path = "../global-router-shared" }
# The block manager serves workers. A frontend that only forwards requests
# never uses it.
dynamo-llm = { path = "../llm", default-features = false }
dynamo-protocols = { workspace = true }
dynamo-runtime = { workspace = true }

anyhow = { workspace = true }
async-stream = { workspace = true }
async-trait = { workspace = true }
clap = { version = "4.5.49", features = ["derive"] }
futures = { workspace = true }
reqwest = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
tokio-util = { workspace = true }
tracing = { workspace = true }
url = { workspace = true }

[dev-dependencies]
dynamo-global-discovery-service = { path = "../global-discovery-service" }

axum = { workspace = true }
Loading
Loading