[Rust Frontend] Record Mooncake/NIXL KV-connector metrics (#52755)

Co-authored-by: Bugen Zhao <i@bugenzhao.com>
Signed-off-by: Markov Ilya <markovilya197@gmail.com>
Signed-off-by: Bugen Zhao <i@bugenzhao.com>
This commit is contained in:
Ilya Markov
2026-09-04 06:21:54 +00:00
committed by GitHub
co-authored by Bugen Zhao
parent 8a728663c1
commit ae2d1ca96f
10 changed files with 600 additions and 62 deletions
+181 -3
View File
@@ -7,11 +7,14 @@ use std::time::{SystemTime, UNIX_EPOCH};
use vllm_metrics::{
EngineLabels, EnginePositionLabels, F64Gauge, Family, HistogramMetric, LoraAdapterNames,
LoraInfoLabels, SchedulerLogStatsAccumulator, SchedulerMetrics, U64Counter, U64Gauge,
LoraInfoLabels, MooncakeOperationCounterFamily, MooncakeOperationHistogramFamily,
MooncakeOperationLabels, SchedulerLogStatsAccumulator, SchedulerMetrics, U64Counter, U64Gauge,
WaitingReasonLabels,
};
use crate::protocol::stats::SchedulerStats;
use crate::protocol::stats::{
KvConnectorStats, MooncakeStats, MultiConnectorStats, NixlStats, SchedulerStats,
};
use crate::transport::ConnectedEngine;
const WAITING_REASON_CAPACITY: &str = "capacity";
@@ -57,6 +60,24 @@ struct SchedulerStatsHandles {
kv_block_idle_before_evict_seconds: HistogramMetric,
kv_block_reuse_gap_seconds: HistogramMetric,
// Mooncake store connector telemetry, decoded from `kv_connector_stats`.
// Kept as `Family` (not pre-resolved) because `operation`/`status` are
// dynamic per-record labels.
mooncake_operation_time_seconds: MooncakeOperationHistogramFamily,
mooncake_operation_total: MooncakeOperationCounterFamily,
mooncake_operation_keys_total: MooncakeOperationCounterFamily,
mooncake_operation_bytes_total: MooncakeOperationCounterFamily,
mooncake_operation_failed_keys_total: MooncakeOperationCounterFamily,
// NIXL connector telemetry, decoded from `kv_connector_stats`.
nixl_xfer_time_seconds: HistogramMetric,
nixl_post_time_seconds: HistogramMetric,
nixl_bytes_transferred: HistogramMetric,
nixl_num_descriptors: HistogramMetric,
nixl_num_failed_transfers: U64Counter,
nixl_num_failed_notifications: U64Counter,
nixl_num_kv_expired_reqs: U64Counter,
// Non-Prometheus interval accumulator for periodic text-log helpers.
log_stats: SchedulerLogStatsAccumulator,
}
@@ -150,6 +171,20 @@ fn resolve_scheduler_stats_handles(
.kv_block_idle_before_evict_seconds
.get_or_create_owned(&labels),
kv_block_reuse_gap_seconds: metrics.kv_block_reuse_gap_seconds.get_or_create_owned(&labels),
mooncake_operation_time_seconds: metrics.mooncake_operation_time_seconds.clone(),
mooncake_operation_total: metrics.mooncake_operation_total.clone(),
mooncake_operation_keys_total: metrics.mooncake_operation_keys_total.clone(),
mooncake_operation_bytes_total: metrics.mooncake_operation_bytes_total.clone(),
mooncake_operation_failed_keys_total: metrics.mooncake_operation_failed_keys_total.clone(),
nixl_xfer_time_seconds: metrics.nixl_xfer_time_seconds.get_or_create_owned(&labels),
nixl_post_time_seconds: metrics.nixl_post_time_seconds.get_or_create_owned(&labels),
nixl_bytes_transferred: metrics.nixl_bytes_transferred.get_or_create_owned(&labels),
nixl_num_descriptors: metrics.nixl_num_descriptors.get_or_create_owned(&labels),
nixl_num_failed_transfers: metrics.nixl_num_failed_transfers.get_or_create_owned(&labels),
nixl_num_failed_notifications: metrics
.nixl_num_failed_notifications
.get_or_create_owned(&labels),
nixl_num_kv_expired_reqs: metrics.nixl_num_kv_expired_reqs.get_or_create_owned(&labels),
labels,
}
}
@@ -236,6 +271,79 @@ fn record_scheduler_stats_with_handles(handles: &SchedulerStatsHandles, stats: &
}
}
}
// Connector-specific KV transfer stats. A bare connector reports its own
// flat payload; MultiConnector reports connector class name -> flat child
// payload.
if let Some(kv_connector_stats) = &stats.kv_connector_stats {
match kv_connector_stats {
KvConnectorStats::Nixl(stats) => record_nixl_stats(handles, stats),
KvConnectorStats::Mooncake(stats) => record_mooncake_stats(handles, stats),
KvConnectorStats::Multi(stats) => record_multi_connector_stats(handles, stats),
KvConnectorStats::Other(_) => {}
}
}
}
fn record_multi_connector_stats(handles: &SchedulerStatsHandles, stats: &MultiConnectorStats) {
for nixl in [&stats.nixl, &stats.nixl_pull, &stats.nixl_push].into_iter().flatten() {
record_nixl_stats(handles, nixl);
}
if let Some(mooncake) = &stats.mooncake {
record_mooncake_stats(handles, mooncake);
}
}
fn record_mooncake_stats(handles: &SchedulerStatsHandles, stats: &MooncakeStats) {
for (operation, records) in &stats.0 {
for record in records {
let labels = MooncakeOperationLabels {
model_name: handles.labels.model_name.clone(),
engine: handles.labels.engine,
operation: operation.as_str().to_string(),
status: record.status.as_str().to_string(),
};
handles
.mooncake_operation_time_seconds
.get_or_create(&labels)
.observe(record.duration_seconds);
handles.mooncake_operation_total.get_or_create(&labels).inc();
handles
.mooncake_operation_keys_total
.get_or_create(&labels)
.inc_by(record.num_keys);
handles
.mooncake_operation_bytes_total
.get_or_create(&labels)
.inc_by(record.num_bytes);
handles
.mooncake_operation_failed_keys_total
.get_or_create(&labels)
.inc_by(record.num_failed_keys);
}
}
}
fn record_nixl_stats(handles: &SchedulerStatsHandles, stats: &NixlStats) {
for value in &stats.transfer_duration {
handles.nixl_xfer_time_seconds.observe(*value);
}
for value in &stats.post_duration {
handles.nixl_post_time_seconds.observe(*value);
}
for value in &stats.bytes_transferred {
handles.nixl_bytes_transferred.observe(*value as f64);
}
for value in &stats.num_descriptors {
handles.nixl_num_descriptors.observe(*value as f64);
}
handles
.nixl_num_failed_transfers
.inc_by(stats.num_failed_transfers.iter().sum());
handles
.nixl_num_failed_notifications
.inc_by(stats.num_failed_notifications.iter().sum());
handles.nixl_num_kv_expired_reqs.inc_by(stats.num_kv_expired_reqs.iter().sum());
}
/// Exports `vllm:lora_requests_info` as a single series covering all LoRA
@@ -286,12 +394,16 @@ fn now_unix_secs() -> f64 {
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use std::collections::{BTreeMap, BTreeSet};
use expect_test::expect;
use vllm_metrics::Metrics;
use crate::metrics::LoraInfoExporter;
use crate::protocol::stats::{
KvConnectorStats, MooncakeOperation, MooncakeRecord, MooncakeStats, MooncakeStatus,
MultiConnectorStats, NixlStats, SchedulerStats,
};
fn names(values: &[&str]) -> BTreeSet<String> {
values.iter().map(|name| (*name).to_string()).collect()
@@ -345,4 +457,70 @@ mod tests {
exporter.update(&metrics.scheduler, names(&[]), names(&[]));
expect![[""]].assert_eq(&lora_series(&metrics.render().unwrap()));
}
fn nixl_stats() -> NixlStats {
NixlStats {
transfer_duration: vec![0.01, 0.02],
post_duration: vec![0.001, 0.002],
bytes_transferred: vec![4096, 8192],
num_descriptors: vec![2, 4],
num_failed_transfers: vec![],
num_failed_notifications: vec![],
num_kv_expired_reqs: vec![1],
}
}
fn mooncake_stats() -> MooncakeStats {
MooncakeStats(BTreeMap::from([(
MooncakeOperation::LoadGet,
vec![MooncakeRecord {
duration_seconds: 0.05,
num_keys: 3,
num_bytes: 1024,
status: MooncakeStatus::Ok,
num_failed_keys: 0,
}],
)]))
}
/// Records one MultiConnector payload into Mooncake and NIXL metrics.
#[test]
fn kv_connector_stats_are_recorded_into_mooncake_and_nixl_metrics() {
let metrics = Metrics::new();
let handles = super::resolve_scheduler_stats_handles(&metrics.scheduler, "model", 0);
let stats = SchedulerStats {
kv_connector_stats: Some(KvConnectorStats::Multi(Box::new(MultiConnectorStats {
nixl: Some(nixl_stats()),
mooncake: Some(mooncake_stats()),
..Default::default()
}))),
..Default::default()
};
super::record_scheduler_stats_with_handles(&handles, &stats);
let rendered = metrics.render().unwrap();
assert!(rendered.contains(
"vllm:mooncake_store_operation_total{model_name=\"model\",engine=\"0\",\
operation=\"load_get\",status=\"ok\"} 1"
));
assert!(rendered.contains(
"vllm:mooncake_store_operation_keys_total{model_name=\"model\",engine=\"0\",\
operation=\"load_get\",status=\"ok\"} 3"
));
assert!(rendered.contains(
"vllm:mooncake_store_operation_bytes_total{model_name=\"model\",engine=\"0\",\
operation=\"load_get\",status=\"ok\"} 1024"
));
assert!(
rendered.contains(
"vllm:nixl_num_kv_expired_reqs_total{model_name=\"model\",engine=\"0\"} 1"
)
);
assert!(
rendered
.contains("vllm:nixl_xfer_time_seconds_count{model_name=\"model\",engine=\"0\"} 2")
);
}
}
@@ -5,8 +5,6 @@ use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::protocol::OpaqueValue;
/// Stores cache hit statistics.
///
/// Original Python definition:
@@ -158,6 +156,120 @@ pub struct CudagraphStats {
pub runtime_mode: String,
}
/// KV connector telemetry DTOs carried by [`SchedulerStats`].
pub mod kv_connector {
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::protocol::OpaqueValue;
/// NIXL connector transfer telemetry.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct NixlStats {
/// Time spent transferring each successful request.
pub transfer_duration: Vec<f64>,
/// Time spent posting each successful transfer.
pub post_duration: Vec<f64>,
/// Bytes transferred by each successful request.
pub bytes_transferred: Vec<u64>,
/// Descriptor count for each successful request.
pub num_descriptors: Vec<u64>,
/// Failure counter increments collected since the previous update.
pub num_failed_transfers: Vec<u64>,
/// Notification failure counter increments collected since the previous update.
pub num_failed_notifications: Vec<u64>,
/// Expired-request counter increments collected since the previous update.
pub num_kv_expired_reqs: Vec<u64>,
}
/// Mooncake store operation name.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MooncakeOperation {
SaveExists,
SavePut,
LoadGet,
LookupExists,
}
impl MooncakeOperation {
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::SaveExists => "save_exists",
Self::SavePut => "save_put",
Self::LoadGet => "load_get",
Self::LookupExists => "lookup_exists",
}
}
}
/// Mooncake store operation status.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MooncakeStatus {
Ok,
Error,
PartialFailure,
}
impl MooncakeStatus {
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::Ok => "ok",
Self::Error => "error",
Self::PartialFailure => "partial_failure",
}
}
}
/// One Mooncake store operation sample.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MooncakeRecord {
pub duration_seconds: f64,
pub num_keys: u64,
pub num_bytes: u64,
pub status: MooncakeStatus,
pub num_failed_keys: u64,
}
/// Mooncake store telemetry grouped by operation.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct MooncakeStats(pub BTreeMap<MooncakeOperation, Vec<MooncakeRecord>>);
/// Telemetry emitted by a `MultiConnector` with a flat child payload map.
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct MultiConnectorStats {
#[serde(rename = "NixlConnector")]
pub nixl: Option<NixlStats>,
#[serde(rename = "NixlPullConnector")]
pub nixl_pull: Option<NixlStats>,
#[serde(rename = "NixlPushConnector")]
pub nixl_push: Option<NixlStats>,
#[serde(rename = "MooncakeStoreConnector")]
pub mooncake: Option<MooncakeStats>,
/// Child connector payloads without a Rust telemetry implementation.
#[serde(flatten)]
pub other: BTreeMap<String, OpaqueValue>,
}
/// Connector-specific scheduler telemetry.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum KvConnectorStats {
Nixl(NixlStats),
Mooncake(MooncakeStats),
Multi(Box<MultiConnectorStats>),
Other(BTreeMap<String, OpaqueValue>),
}
}
pub use kv_connector::*;
/// Stats associated with the scheduler.
///
/// Original Python definition:
@@ -185,8 +297,8 @@ pub struct SchedulerStats {
pub kv_cache_eviction_events: Vec<KvCacheEvictionEvent>,
/// Speculative decoding scheduler stats, when enabled.
pub spec_decoding_stats: Option<SpecDecodingStats>,
/// Connector-specific KV transfer stats, kept opaque for now.
pub kv_connector_stats: Option<BTreeMap<String, OpaqueValue>>,
/// Connector-specific KV transfer stats.
pub kv_connector_stats: Option<KvConnectorStats>,
/// CUDA graph runtime stats when graph metrics are enabled.
pub cudagraph_stats: Option<CudagraphStats>,
/// Estimated MFU/performance stats, when enabled.
@@ -31,7 +31,7 @@ use crate::protocol::output::{
};
use crate::protocol::request::{EngineCoreRequest, EngineCoreRequestType};
use crate::protocol::sampling::EngineCoreSamplingParams;
use crate::protocol::stats::SchedulerStats;
use crate::protocol::stats::{KvConnectorStats, MooncakeOperation, SchedulerStats};
use crate::protocol::tensor::{WireArrayData, WireTensor};
use crate::protocol::utility::{UtilityOutput, UtilityResultEnvelope};
use crate::test_utils::{
@@ -2635,6 +2635,10 @@ fn python_msgpack_fixtures_match_rust_encoding() {
let inline_prompt_frames = lines.next().expect("missing inline prompt logprobs fixture line");
let multipart_prompt_frames =
lines.next().expect("missing multipart prompt logprobs fixture line");
let nixl_stats_hex = lines.next().expect("missing NIXL stats fixture line");
let mooncake_stats_hex = lines.next().expect("missing Mooncake stats fixture line");
let multi_connector_stats_hex =
lines.next().expect("missing MultiConnector stats fixture line");
let ready_response_hex = lines.next().expect("missing ready response fixture line");
let request_bytes = hex::decode(request_hex).unwrap();
@@ -2799,6 +2803,28 @@ fn python_msgpack_fixtures_match_rust_encoding() {
.expect("multipart prompt logprobs decoded"),
);
let nixl_stats: KvConnectorStats =
rmp_serde::from_slice(&hex::decode(nixl_stats_hex).unwrap()).unwrap();
assert!(matches!(nixl_stats, KvConnectorStats::Nixl(_)));
let mooncake_stats: KvConnectorStats =
rmp_serde::from_slice(&hex::decode(mooncake_stats_hex).unwrap()).unwrap();
assert!(matches!(
mooncake_stats,
KvConnectorStats::Mooncake(stats)
if stats.0.contains_key(&MooncakeOperation::LoadGet)
));
let multi_connector_stats: KvConnectorStats =
rmp_serde::from_slice(&hex::decode(multi_connector_stats_hex).unwrap()).unwrap();
assert!(matches!(
multi_connector_stats,
KvConnectorStats::Multi(stats)
if stats.nixl.is_some()
&& stats.mooncake.is_some()
&& stats.other.contains_key("UnsupportedConnector")
));
let map_keys = |bytes: &[u8]| -> BTreeSet<String> {
match decode_value(bytes) {
Value::Map(entries) => entries
@@ -456,6 +456,32 @@ ready_response = EngineCoreReadyResponse(
),
)
nixl_stats = {
"transfer_duration": [0.01, 0.02],
"post_duration": [0.001, 0.002],
"bytes_transferred": [4096, 8192],
"num_descriptors": [2, 4],
"num_failed_transfers": [],
"num_failed_notifications": [],
"num_kv_expired_reqs": [1],
}
mooncake_stats = {
"load_get": [
{
"duration_seconds": 0.05,
"num_keys": 3,
"num_bytes": 1024,
"status": "ok",
"num_failed_keys": 0,
}
]
}
multi_connector_stats = {
"NixlConnector": nixl_stats,
"MooncakeStoreConnector": mooncake_stats,
"UnsupportedConnector": {"sample_count": 1},
}
print(msgspec.msgpack.encode(request).hex())
print(msgspec.msgpack.encode(defaults_request).hex())
print(msgpack.packb(multimodal_request_wire, use_bin_type=True).hex())
@@ -475,4 +501,7 @@ print(
for frame in encode_output_frames(multipart_prompt_logprobs, size_threshold=1)
)
)
print(msgspec.msgpack.encode(nixl_stats).hex())
print(msgspec.msgpack.encode(mooncake_stats).hex())
print(msgspec.msgpack.encode(multi_connector_stats).hex())
print(msgspec.msgpack.encode(ready_response).hex())
+195
View File
@@ -29,6 +29,58 @@ fn kv_block_reuse_gap_histogram() -> Histogram {
Histogram::new(KV_CACHE_RESIDENCY_BUCKETS.iter().copied())
}
// Buckets copied verbatim from the Python `MooncakeStorePromMetrics` /
// `NixlPromMetrics` classes so dashboards built against either frontend are
// interchangeable.
const MOONCAKE_OPERATION_TIME_BUCKETS: [f64; 15] = [
1e-3, 5e-3, 1e-2, 5e-2, 1e-1, 2e-1, 3e-1, 4e-1, 5e-1, 7.5e-1, 1.0, 1.5, 2.0, 3.0, 4.0,
];
const NIXL_XFER_TIME_BUCKETS: [f64; 12] = [
0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.2, 0.3, 0.5, 0.75, 1.0, 5.0,
];
const NIXL_POST_TIME_BUCKETS: [f64; 13] = [
0.001, 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.2, 0.3, 0.5, 0.75, 1.0, 5.0,
];
// Uniform 2KB to 8GB range: 2**(10+i) for i in range(1, 25, 2).
const NIXL_BYTES_TRANSFERRED_BUCKETS: [f64; 12] = [
2048.0,
8192.0,
32768.0,
131072.0,
524288.0,
2097152.0,
8388608.0,
33554432.0,
134217728.0,
536870912.0,
2147483648.0,
8589934592.0,
];
const NIXL_NUM_DESCRIPTORS_BUCKETS: [f64; 14] = [
10.0, 20.0, 30.0, 50.0, 75.0, 100.0, 200.0, 400.0, 1000.0, 2000.0, 4000.0, 10000.0, 20000.0,
50000.0,
];
fn mooncake_operation_time_histogram() -> Histogram {
Histogram::new(MOONCAKE_OPERATION_TIME_BUCKETS.iter().copied())
}
fn nixl_xfer_time_histogram() -> Histogram {
Histogram::new(NIXL_XFER_TIME_BUCKETS.iter().copied())
}
fn nixl_post_time_histogram() -> Histogram {
Histogram::new(NIXL_POST_TIME_BUCKETS.iter().copied())
}
fn nixl_bytes_transferred_histogram() -> Histogram {
Histogram::new(NIXL_BYTES_TRANSFERRED_BUCKETS.iter().copied())
}
fn nixl_num_descriptors_histogram() -> Histogram {
Histogram::new(NIXL_NUM_DESCRIPTORS_BUCKETS.iter().copied())
}
#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)]
pub struct EngineLabels {
pub model_name: String,
@@ -49,6 +101,24 @@ pub struct WaitingReasonLabels {
pub reason: &'static str,
}
/// Labels for per-operation Mooncake store connector telemetry. `operation`
/// and `status` are dynamic (not known ahead of time), unlike the other
/// fixed-label families in this module.
#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)]
pub struct MooncakeOperationLabels {
pub model_name: String,
pub engine: u32,
pub operation: String,
pub status: String,
}
/// Family type aliases for the Mooncake connector telemetry, exposed so
/// `engine-core-client` can hold onto the `Family` directly (rather than a
/// pre-resolved handle) since `operation`/`status` vary per record.
pub type MooncakeOperationHistogramFamily =
Family<MooncakeOperationLabels, Histogram, fn() -> Histogram>;
pub type MooncakeOperationCounterFamily = Family<MooncakeOperationLabels, U64Counter>;
/// Adapter names encoded as a deterministic comma-joined Prometheus label value.
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct LoraAdapterNames(pub BTreeSet<String>);
@@ -184,6 +254,22 @@ pub struct SchedulerMetrics {
pub kv_block_idle_before_evict_seconds: HistogramFamily,
pub kv_block_reuse_gap_seconds: HistogramFamily,
// Mooncake store connector telemetry. Mirrors `MooncakeStorePromMetrics`.
pub mooncake_operation_time_seconds: MooncakeOperationHistogramFamily,
pub mooncake_operation_total: MooncakeOperationCounterFamily,
pub mooncake_operation_keys_total: MooncakeOperationCounterFamily,
pub mooncake_operation_bytes_total: MooncakeOperationCounterFamily,
pub mooncake_operation_failed_keys_total: MooncakeOperationCounterFamily,
// NIXL connector telemetry. Mirrors `NixlPromMetrics`.
pub nixl_xfer_time_seconds: HistogramFamily,
pub nixl_post_time_seconds: HistogramFamily,
pub nixl_bytes_transferred: HistogramFamily,
pub nixl_num_descriptors: HistogramFamily,
pub nixl_num_failed_transfers: Family<EngineLabels, U64Counter>,
pub nixl_num_failed_notifications: Family<EngineLabels, U64Counter>,
pub nixl_num_kv_expired_reqs: Family<EngineLabels, U64Counter>,
/// Non-Prometheus interval accumulators for periodic text-log helpers.
pub log_stats: Family<EngineLabels, SchedulerLogStatsAccumulator>,
}
@@ -336,6 +422,103 @@ impl SchedulerMetrics {
kv_block_reuse_gap_seconds.clone(),
);
// Mooncake store connector telemetry.
let mooncake_operation_time_seconds =
Family::new_with_constructor(mooncake_operation_time_histogram as fn() -> Histogram);
registry.register(
"vllm:mooncake_store_operation_time_seconds",
"Histogram of Mooncake store communication time.",
mooncake_operation_time_seconds.clone(),
);
// NOTE: registration names below omit the `_total` suffix that the
// Python metric names carry (e.g. `vllm:mooncake_store_operation_total`)
// because this crate's `prometheus-client` (unlike Python's, which
// strips a user-supplied `_total` before re-adding it) appends
// `_total` unconditionally for counters; see the note in `lib.rs`.
let mooncake_operation_total = Family::default();
registry.register(
"vllm:mooncake_store_operation",
"Number of Mooncake store communication operations.",
mooncake_operation_total.clone(),
);
let mooncake_operation_keys_total = Family::default();
registry.register(
"vllm:mooncake_store_operation_keys",
"Number of Mooncake store keys touched by operations.",
mooncake_operation_keys_total.clone(),
);
let mooncake_operation_bytes_total = Family::default();
registry.register(
"vllm:mooncake_store_operation_bytes",
"Number of bytes transferred by Mooncake store operations.",
mooncake_operation_bytes_total.clone(),
);
let mooncake_operation_failed_keys_total = Family::default();
registry.register(
"vllm:mooncake_store_operation_failed_keys",
"Number of Mooncake store keys that failed in operations.",
mooncake_operation_failed_keys_total.clone(),
);
// NIXL connector telemetry.
let nixl_xfer_time_seconds =
Family::new_with_constructor(nixl_xfer_time_histogram as fn() -> Histogram);
registry.register(
"vllm:nixl_xfer_time_seconds",
"Histogram of transfer duration for NIXL KV Cache transfers.",
nixl_xfer_time_seconds.clone(),
);
let nixl_post_time_seconds =
Family::new_with_constructor(nixl_post_time_histogram as fn() -> Histogram);
registry.register(
"vllm:nixl_post_time_seconds",
"Histogram of transfer post time for NIXL KV Cache transfers.",
nixl_post_time_seconds.clone(),
);
let nixl_bytes_transferred =
Family::new_with_constructor(nixl_bytes_transferred_histogram as fn() -> Histogram);
registry.register(
"vllm:nixl_bytes_transferred",
"Histogram of bytes transferred per NIXL KV Cache transfers.",
nixl_bytes_transferred.clone(),
);
let nixl_num_descriptors =
Family::new_with_constructor(nixl_num_descriptors_histogram as fn() -> Histogram);
registry.register(
"vllm:nixl_num_descriptors",
"Histogram of number of descriptors per NIXL KV Cache transfers.",
nixl_num_descriptors.clone(),
);
let nixl_num_failed_transfers = Family::default();
registry.register(
"vllm:nixl_num_failed_transfers",
"Number of failed NIXL KV Cache transfers.",
nixl_num_failed_transfers.clone(),
);
let nixl_num_failed_notifications = Family::default();
registry.register(
"vllm:nixl_num_failed_notifications",
"Number of failed NIXL KV Cache notifications.",
nixl_num_failed_notifications.clone(),
);
let nixl_num_kv_expired_reqs = Family::default();
registry.register(
"vllm:nixl_num_kv_expired_reqs",
"Number of requests that had their KV expire. \
NOTE: This metric is tracked on the P instance.",
nixl_num_kv_expired_reqs.clone(),
);
Self {
scheduler_running,
scheduler_waiting,
@@ -356,6 +539,18 @@ impl SchedulerMetrics {
kv_block_lifetime_seconds,
kv_block_idle_before_evict_seconds,
kv_block_reuse_gap_seconds,
mooncake_operation_time_seconds,
mooncake_operation_total,
mooncake_operation_keys_total,
mooncake_operation_bytes_total,
mooncake_operation_failed_keys_total,
nixl_xfer_time_seconds,
nixl_post_time_seconds,
nixl_bytes_transferred,
nixl_num_descriptors,
nixl_num_failed_transfers,
nixl_num_failed_notifications,
nixl_num_kv_expired_reqs,
log_stats: Family::default(),
}
}
@@ -23,6 +23,7 @@ from vllm.distributed.kv_transfer.kv_connector.v1.base import (
from vllm.distributed.kv_transfer.kv_connector.v1.metrics import KVConnectorStats
from vllm.distributed.kv_transfer.kv_connector.v1.multi_connector import (
MultiConnector,
MultiKVConnectorPromMetrics,
MultiKVConnectorStats,
MultiKVConnectorWorkerMetadata,
)
@@ -559,14 +560,12 @@ class TestMultiConnectorStats:
correct data."""
serialized_data = {
"NixlConnector": {
"data": {
"transfer_duration": [1.5, 2.3],
"post_duration": [0.1, 0.2],
"bytes_transferred": [1024, 2048],
"num_descriptors": [10, 20],
"num_failed_transfers": [],
"num_failed_notifications": [],
}
"transfer_duration": [1.5, 2.3],
"post_duration": [0.1, 0.2],
"bytes_transferred": [1024, 2048],
"num_descriptors": [10, 20],
"num_failed_transfers": [],
"num_failed_notifications": [],
}
}
@@ -584,16 +583,14 @@ class TestMultiConnectorStats:
"""Test reconstruction with multiple connector types that have custom stats."""
serialized_data = {
"NixlConnector": {
"data": {
"transfer_duration": [1.5],
"post_duration": [0.1],
"bytes_transferred": [1024],
"num_descriptors": [10],
"num_failed_transfers": [],
"num_failed_notifications": [],
}
"transfer_duration": [1.5],
"post_duration": [0.1],
"bytes_transferred": [1024],
"num_descriptors": [10],
"num_failed_transfers": [],
"num_failed_notifications": [],
},
"MockConnector": {"data": {"mock_field": [1, 2, 3]}},
"MockConnector": {"mock_field": [1, 2, 3]},
}
stats = MultiConnector.build_kv_connector_stats(data=serialized_data)
@@ -608,20 +605,19 @@ class TestMultiConnectorStats:
assert isinstance(stats.data["MockConnector"], MockConnectorStats)
# Verify data is preserved
assert stats.data["MockConnector"].data == {"mock_field": [1, 2, 3]}
assert stats.to_dict() == serialized_data
def test_build_kv_connector_stats_raises_error_for_unknown_connector(self):
"""Test that unknown connectors raise an error."""
serialized_data = {
"UnknownConnector": {"data": {"some_field": [1, 2, 3]}},
"UnknownConnector": {"some_field": [1, 2, 3]},
"NixlConnector": {
"data": {
"transfer_duration": [1.5],
"post_duration": [0.1],
"bytes_transferred": [1024],
"num_descriptors": [10],
"num_failed_transfers": [],
"num_failed_notifications": [],
}
"transfer_duration": [1.5],
"post_duration": [0.1],
"bytes_transferred": [1024],
"num_descriptors": [10],
"num_failed_transfers": [],
"num_failed_notifications": [],
},
}
@@ -675,7 +671,7 @@ class TestMultiConnectorStats:
mixed_data = {
"NixlConnector": nixl_stats, # Already instantiated
"MockConnector": {"data": {"mock_field": [1, 2, 3]}}, # Serialized
"MockConnector": {"mock_field": [1, 2, 3]}, # Serialized
}
stats = MultiConnector.build_kv_connector_stats(data=mixed_data)
@@ -695,16 +691,14 @@ class TestMultiConnectorStats:
# so it returns None and should be skipped
serialized_data = {
"NixlConnector": {
"data": {
"transfer_duration": [1.5],
"post_duration": [0.1],
"bytes_transferred": [1024],
"num_descriptors": [10],
"num_failed_transfers": [],
"num_failed_notifications": [],
}
"transfer_duration": [1.5],
"post_duration": [0.1],
"bytes_transferred": [1024],
"num_descriptors": [10],
"num_failed_transfers": [],
"num_failed_notifications": [],
},
"ExampleConnector": {"data": {"some_field": [1, 2, 3]}},
"ExampleConnector": {"some_field": [1, 2, 3]},
}
stats = MultiConnector.build_kv_connector_stats(data=serialized_data)
@@ -718,14 +712,15 @@ class TestMultiConnectorStats:
# ExampleConnector should be skipped (returns None)
assert "ExampleConnector" not in stats.data
def test_build_kv_connector_stats_handles_malformed_data(self):
"""Test that malformed data raises appropriate errors."""
serialized_data = {
"NixlConnector": {"wrong_field": {"transfer_duration": [1.5]}}
}
def test_prom_metrics_passes_flat_data_to_children(self):
child_metrics = MagicMock()
metrics = object.__new__(MultiKVConnectorPromMetrics)
metrics._prom_metrics = {"NixlConnector": child_metrics}
payload = {"NixlConnector": {"transfer_duration": [1.5]}}
with pytest.raises(AssertionError, match="Expected a dict with a 'data' field"):
MultiConnector.build_kv_connector_stats(data=serialized_data)
metrics.observe(payload, engine_idx=2)
child_metrics.observe.assert_called_once_with({"transfer_duration": [1.5]}, 2)
def test_aggregate_same_connector(self):
"""Test aggregating stats from the same connector type."""
@@ -1709,8 +1709,9 @@ def test_scheduler_kv_connector_stats_aggregation():
final_stats = next(
iter(engine_core_outputs.values())
).scheduler_stats.kv_connector_stats
# The scheduler stats payload carries serialized per-connector dicts.
nixl_stats = final_stats["NixlConnector"]
assert nixl_stats.num_successful_transfers == 2
assert len(nixl_stats["transfer_duration"]) == 2
@pytest.mark.parametrize("distributed_executor_backend", ["ray", None])
@@ -26,6 +26,10 @@ class KVConnectorStats:
data: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
"""Return the serializable connector stats payload."""
return self.data
def reset(self):
"""Reset the stats, clear the state."""
raise NotImplementedError
@@ -75,6 +75,11 @@ class MultiKVConnectorStats(KVConnectorStats):
This is used to aggregate the stats from all connectors separately.
"""
def to_dict(self) -> dict[str, Any]:
return {
connector_id: stats.to_dict() for connector_id, stats in self.data.items()
}
def aggregate(self, other: KVConnectorStats) -> KVConnectorStats:
for connector_id, stats in other.data.items():
if connector_id not in self.data:
@@ -122,7 +127,7 @@ class MultiKVConnectorPromMetrics(KVConnectorPromMetrics):
f"{connector_id} is not contained in the list of registered connectors "
f"with Prometheus metrics support: {self._prom_metrics.keys()}"
)
self._prom_metrics[connector_id].observe(stats_data["data"], engine_idx)
self._prom_metrics[connector_id].observe(stats_data, engine_idx)
class MultiConnector(KVConnectorBase_V1, SupportsHMA):
@@ -617,16 +622,9 @@ class MultiConnector(KVConnectorBase_V1, SupportsHMA):
connector_name
)
# stats_value is the serialized dataclass which contains {'data': {...}}
# We need to extract the inner 'data' field to avoid double-nesting
assert isinstance(stats_value, dict) and "data" in stats_value, (
f"Expected a dict with a 'data' field, got {stats_value}"
)
inner_data = stats_value["data"]
# Use the connector's build_kv_connector_stats to reconstruct
if reconstructed_stats := connector_cls.build_kv_connector_stats(
data=inner_data
data=stats_value
):
reconstructed_data[connector_name] = reconstructed_stats
+1 -1
View File
@@ -2704,7 +2704,7 @@ class Scheduler(SchedulerInterface):
)
spec_stats = spec_decoding_stats
connector_stats_payload = (
kv_connector_stats.data if kv_connector_stats else None
kv_connector_stats.to_dict() if kv_connector_stats else None
)
return SchedulerStats(
num_running_reqs=len(self.running),