All checks were successful
CI / Format (push) Successful in 30s
CI / CUDA type-check (push) Successful in 1m30s
CI / Clippy (push) Successful in 2m18s
CI / Test (push) Successful in 4m17s
CI / Build cortex SRPM (push) Has been skipped
CI / Publish cortex to COPR (push) Has been skipped
CI / Build neuron SRPM (push) Has been skipped
CI / Publish neuron to COPR (push) Has been skipped
CI / Bump version in source (push) Has been skipped
build-prerelease / Resolve version stamps + change detection (push) Successful in 33s
build-prerelease / Build neuron-blackwell (push) Successful in 1m42s
build-prerelease / Build neuron-ampere (push) Successful in 2m18s
build-prerelease / Build neuron-ada (push) Successful in 2m19s
build-prerelease / Build helexa-bench binary (push) Successful in 2m18s
build-prerelease / Lint (fmt + clippy) (push) Successful in 2m27s
build-prerelease / Build cortex binary (push) Successful in 2m45s
build-prerelease / Package helexa-neuron-ada RPM (push) Successful in 2m2s
build-prerelease / Test (push) Successful in 4m50s
build-prerelease / Package helexa-bench RPM (push) Successful in 1m18s
build-prerelease / Package cortex RPM (push) Successful in 1m22s
build-prerelease / Package helexa-neuron-ampere RPM (push) Successful in 1m37s
build-prerelease / Package helexa-neuron-blackwell RPM (push) Successful in 1m43s
build-prerelease / Publish to rpm.lair.cafe (unstable) (push) Successful in 56s
Completes #53: the bounded scheduler's lock-free counters are now visible to the fleet, which is what cortex's load-aware router (#55) consumes to spread traffic across replicas and propagate honest backpressure. - cortex-core::discovery: HealthResponse gains `models: Vec<ModelLoad>` (#[serde(default)] — back-compatible; older gateways/neurons interop). ModelLoad { id, in_flight, queue_depth }. - LoadedHandle::load() → (in_flight, queue_depth), lock-free for both single-GPU and TP; CandleHarness::load_snapshot() enumerates resident models; the /health handler overlays it from the candle harness. Tests: /health always exposes a models array (api integration test); a pre-#53 payload without `models` still deserializes, and ModelLoad round-trips (cortex-core serde tests). Local fmt/clippy/test green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
80 lines
2.6 KiB
Rust
80 lines
2.6 KiB
Rust
//! Cached GPU health monitoring via periodic nvidia-smi polling.
|
|
|
|
use cortex_core::discovery::HealthResponse;
|
|
use std::time::{Duration, Instant};
|
|
use tokio::sync::RwLock;
|
|
|
|
const POLL_INTERVAL: Duration = Duration::from_secs(5);
|
|
|
|
/// Thread-safe cache for the latest GPU health reading.
|
|
pub struct HealthCache {
|
|
inner: RwLock<HealthResponse>,
|
|
has_gpus: RwLock<bool>,
|
|
}
|
|
|
|
impl Default for HealthCache {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl HealthCache {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
inner: RwLock::new(HealthResponse {
|
|
uptime_secs: 0,
|
|
devices: vec![],
|
|
// The cache only owns the device-state half of /health;
|
|
// the api handler overlays activation from the tracker.
|
|
// Initialise with the default (Ready, empty lists) so a
|
|
// direct read from the cache stays a well-typed
|
|
// HealthResponse on the wire.
|
|
activation: Default::default(),
|
|
// Per-model admission load is overlaid by the api handler
|
|
// from the candle harness (#53); the cache doesn't own it.
|
|
models: Vec::new(),
|
|
}),
|
|
has_gpus: RwLock::new(false),
|
|
}
|
|
}
|
|
|
|
/// Mark whether this node has GPUs (set after discovery).
|
|
pub async fn set_has_gpus(&self, has_gpus: bool) {
|
|
*self.has_gpus.write().await = has_gpus;
|
|
}
|
|
|
|
/// Get a snapshot of the current health state.
|
|
pub async fn snapshot(&self) -> HealthResponse {
|
|
self.inner.read().await.clone()
|
|
}
|
|
|
|
/// Run forever, polling nvidia-smi every 5 seconds and updating the cache.
|
|
pub async fn poll_loop(&self, start_time: Instant) {
|
|
loop {
|
|
tokio::time::sleep(POLL_INTERVAL).await;
|
|
|
|
let uptime = start_time.elapsed().as_secs();
|
|
|
|
if !*self.has_gpus.read().await {
|
|
let mut health = self.inner.write().await;
|
|
health.uptime_secs = uptime;
|
|
continue;
|
|
}
|
|
|
|
match crate::discovery::query_health().await {
|
|
Ok(devices) => {
|
|
let mut health = self.inner.write().await;
|
|
health.uptime_secs = uptime;
|
|
health.devices = devices;
|
|
}
|
|
Err(e) => {
|
|
tracing::warn!(error = %e, "failed to poll GPU health");
|
|
// Keep last known reading, just update uptime.
|
|
let mut health = self.inner.write().await;
|
|
health.uptime_secs = uptime;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|