Skip to content

Commit 1297030

Browse files
committed
Improvements: rename KvCacheTokensTotal, expand help text, use unit label
- Rename KvCacheTokensTotal -> KvCacheTokensCapacity on VariantDecision, Actuator.EmitSaturationMetrics, MetricsEmitter.EmitSaturationMetrics, DeleteSaturationMetrics, and the Prometheus metric name itself (wva_kv_cache_tokens_total -> wva_kv_cache_tokens_capacity). "Total" was confusing — the metric is a gauge of capacity, not a cumulative counter. - Replace the analyzer_version="v1"/"v2" label on wva_required_capacity with a unit="binary"/"continuous" label. The label's purpose is to describe the unit of the metric value (a boolean scale-up signal in V1, a continuous token demand in V2), not the code path that produced it. "binary"/"continuous" remains meaningful after V1 is deprecated, whereas "v1"/"v2" becomes vestigial. Rename VariantDecision.AnalyzerVersion -> RequiredCapacityUnit. Rename constants.LabelAnalyzerVersion -> LabelUnit. Rename constants.AnalyzerVersionV1/V2 -> UnitBinary/UnitContinuous. - Expand help strings on wva_saturation_utilization, wva_spare_capacity, wva_kv_cache_tokens_used, and wva_kv_cache_tokens_capacity to specify what is being measured (KV-cache) and how V1 vs V2 paths differ. - Use constants.LabelUnit, UnitBinary, UnitContinuous in the wva_required_capacity help string via fmt.Sprintf, for consistency with how labels are referenced elsewhere.
1 parent 0707e1c commit 1297030

6 files changed

Lines changed: 197 additions & 89 deletions

File tree

internal/actuator/actuator.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,19 @@ func (a *Actuator) EmitSaturationMetrics(ctx context.Context, decision interface
101101
decision.VariantName,
102102
decision.Namespace,
103103
decision.AcceleratorName,
104-
decision.AnalyzerVersion,
104+
decision.RequiredCapacityUnit,
105105
decision.Utilization,
106106
decision.SpareCapacity,
107107
decision.RequiredCapacity,
108108
decision.KvCacheTokensUsed,
109-
decision.KvCacheTokensTotal,
109+
decision.KvCacheTokensCapacity,
110110
)
111111
}
112+
113+
// DeleteSaturationMetricsForVariant removes all saturation metric series for a
114+
// variant. Call this when the current optimization cycle produced no fresh
115+
// decision for the variant, or when the VA is being deleted — so dashboards
116+
// don't show stale values.
117+
func (a *Actuator) DeleteSaturationMetricsForVariant(variantName, namespace string) {
118+
a.MetricsEmitter.DeleteSaturationMetricsForVariant(variantName, namespace)
119+
}

internal/constants/metrics.go

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -119,19 +119,19 @@ const (
119119

120120
// WVARequiredCapacity is a gauge that tracks model-level required capacity.
121121
// >0 means scale-up needed.
122-
// Units differ by analyzer (use the analyzer_version label to distinguish):
123-
// - V1: binary signal (0.0 = no scale-up, 1.0 = scale-up needed)
124-
// - V2: continuous token-based demand
125-
// Labels: variant_name, namespace, analyzer_version
122+
// Value semantics differ by analyzer (use the "unit" label to distinguish):
123+
// - unit="binary" (V1): 0.0 = no scale-up, 1.0 = scale-up needed
124+
// - unit="continuous" (V2): continuous token-based demand
125+
// Labels: variant_name, namespace, unit
126126
WVARequiredCapacity = "wva_required_capacity"
127127

128128
// WVAKvCacheTokensUsed is a gauge that tracks total KV cache tokens currently in use per variant.
129129
// Labels: variant_name, namespace
130130
WVAKvCacheTokensUsed = "wva_kv_cache_tokens_used"
131131

132-
// WVAKvCacheTokensTotal is a gauge that tracks total KV cache token capacity per variant.
132+
// WVAKvCacheTokensCapacity is a gauge that tracks total KV cache token capacity per variant.
133133
// Labels: variant_name, namespace
134-
WVAKvCacheTokensTotal = "wva_kv_cache_tokens_total"
134+
WVAKvCacheTokensCapacity = "wva_kv_cache_tokens_capacity"
135135
)
136136

137137
// Metric Label Names
@@ -144,11 +144,16 @@ const (
144144
LabelReason = "reason"
145145
LabelAcceleratorType = "accelerator_type"
146146
LabelControllerInstance = "controller_instance"
147-
LabelAnalyzerVersion = "analyzer_version"
147+
// LabelUnit distinguishes the unit of a metric value when a single metric name
148+
// carries values with different semantic units. Currently applied to
149+
// wva_required_capacity, whose value is either a binary scale-up signal (V1)
150+
// or a continuous token-demand value (V2).
151+
LabelUnit = "unit"
148152
)
149153

150-
// Analyzer version label values used in saturation metrics.
154+
// Values for the LabelUnit Prometheus label, describing how to interpret the
155+
// metric value ("binary" 0/1 vs. "continuous" absolute quantity).
151156
const (
152-
AnalyzerVersionV1 = "v1"
153-
AnalyzerVersionV2 = "v2"
157+
UnitBinary = "binary"
158+
UnitContinuous = "continuous"
154159
)

internal/engines/saturation/engine.go

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -764,17 +764,23 @@ func enrichDecisionsFromReplicaMetrics(decisions []interfaces.VariantDecision, r
764764
for i := range decisions {
765765
d := &decisions[i]
766766
d.RequiredCapacity = requiredCapacity
767-
d.AnalyzerVersion = constants.AnalyzerVersionV1
767+
d.RequiredCapacityUnit = constants.UnitBinary
768768
if a, ok := agg[d.VariantName]; ok && a.count > 0 {
769769
d.KvCacheTokensUsed = a.kvUsed
770-
d.KvCacheTokensTotal = a.kvTotal
770+
d.KvCacheTokensCapacity = a.kvTotal
771+
// V1 reasons about saturation per-replica using KvCacheUsage fractions
772+
// (rm.KvCacheUsage is 0.0-1.0), not tokens. Report the mean of those
773+
// per-replica fractions as the variant-level utilization — this
774+
// matches what the V1 analyzer actually evaluates against its
775+
// thresholds. V2 uses a different (token-demand / capacity) formula;
776+
// see the field doc on VariantDecision.Utilization.
771777
d.Utilization = a.kvUsageSum / float64(a.count)
772778
}
773779
}
774780
}
775781

776-
// enrichDecisionsWithKvTokenData sets KvCacheTokensUsed, KvCacheTokensTotal, and
777-
// AnalyzerVersion on decisions from replica metrics aggregated per (model, variant).
782+
// enrichDecisionsWithKvTokenData sets KvCacheTokensUsed, KvCacheTokensCapacity, and
783+
// RequiredCapacityUnit on decisions from replica metrics aggregated per (model, variant).
778784
// Used by V2 path where Utilization and RequiredCapacity are already set from
779785
// AnalyzerResult.
780786
//
@@ -805,10 +811,10 @@ func enrichDecisionsWithKvTokenData(decisions []interfaces.VariantDecision, mode
805811

806812
for i := range decisions {
807813
d := &decisions[i]
808-
d.AnalyzerVersion = constants.AnalyzerVersionV2
814+
d.RequiredCapacityUnit = constants.UnitContinuous
809815
if a, ok := agg[variantKey{modelID: d.ModelID, variant: d.VariantName}]; ok {
810816
d.KvCacheTokensUsed = a.kvUsed
811-
d.KvCacheTokensTotal = a.kvTotal
817+
d.KvCacheTokensCapacity = a.kvTotal
812818
}
813819
}
814820
}
@@ -1157,14 +1163,18 @@ func (e *Engine) applySaturationDecisions(
11571163
}
11581164

11591165
// Emit saturation and capacity metrics for observability.
1160-
// Note: stale time series for deleted VAs are not cleaned up automatically here.
1161-
// The metrics package exposes DeleteSaturationMetrics for callers (e.g., the
1162-
// VariantAutoscaling reconciler's delete handler / finalizer) to remove series
1163-
// when a VA is removed.
1166+
// When this cycle produced no fresh decision for the variant, actively
1167+
// clear the existing series so dashboards show a gap ("no fresh data")
1168+
// rather than stale values that would otherwise persist until Prometheus'
1169+
// 5-minute staleness marker fires. For fully-deleted VAs, additional
1170+
// cleanup via the reconciler's delete handler / finalizer is still
1171+
// required (see DeleteSaturationMetricsForVariant).
11641172
if hasDecision {
11651173
if err := act.EmitSaturationMetrics(ctx, decision); err != nil {
11661174
logger.Error(err, "Failed to emit saturation metrics", "variant", updateVa.Name)
11671175
}
1176+
} else {
1177+
act.DeleteSaturationMetricsForVariant(updateVa.Name, updateVa.Namespace)
11681178
}
11691179

11701180
// Update Shared State and Trigger Reconcile via Channel

internal/interfaces/saturation_analyzer.go

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -195,25 +195,32 @@ type VariantDecision struct {
195195
// V1: threshold-relative spare KV capacity (AvgSpareKvCapacity).
196196
// V2: 1.0 - Utilization (absolute spare).
197197
SpareCapacity float64
198-
// Utilization is the variant-level utilization ratio (0.0-1.0).
199-
// V2: from AnalyzerResult.VariantCapacities[].Utilization.
200-
// V1: average KvCacheUsage across this variant's replicas.
198+
// Utilization is the variant-level utilization ratio (0.0-1.0) reported for
199+
// observability. The exact formula differs by analyzer because V1 and V2
200+
// reason about saturation differently:
201+
// V1: mean of per-replica KvCacheUsage fractions (matches what V1's
202+
// per-replica threshold check operates on).
203+
// V2: TotalDemand / TotalCapacity from AnalyzerResult (token-demand-based).
204+
// For uniform-capacity replicas the two are numerically equivalent; for
205+
// mixed-capacity replicas V2's value is capacity-weighted.
201206
Utilization float64
202207
// KvCacheTokensUsed is the sum of TokensInUse across this variant's replicas.
203208
KvCacheTokensUsed int64
204-
// KvCacheTokensTotal is the sum of TotalKvCapacityTokens across this variant's replicas.
205-
KvCacheTokensTotal int64
209+
// KvCacheTokensCapacity is the sum of TotalKvCapacityTokens across this variant's replicas.
210+
KvCacheTokensCapacity int64
206211
// RequiredCapacity is the model-level required capacity (>0 means scale-up needed).
207212
// Same value for all variants of a model.
208213
// V1: binary (1.0 if shouldScaleUp, else 0.0).
209214
// V2: continuous token-based demand from AnalyzerResult.
210-
// Use AnalyzerVersion to disambiguate the units when consuming this field
215+
// Use RequiredCapacityUnit to disambiguate the units when consuming this field
211216
// (or its corresponding Prometheus metric).
212217
RequiredCapacity float64
213-
// AnalyzerVersion identifies which analyzer produced this decision ("v1" or "v2").
214-
// Exposed as a Prometheus label on saturation metrics so dashboards can filter
215-
// by analyzer to handle the V1/V2 unit difference in RequiredCapacity.
216-
AnalyzerVersion string
218+
// RequiredCapacityUnit describes the unit of RequiredCapacity ("binary" or "continuous").
219+
// Exposed as the `unit` Prometheus label on wva_required_capacity so dashboards
220+
// can filter by semantics rather than by which analyzer produced the value.
221+
// "binary": V1 path, value is 0.0 or 1.0
222+
// "continuous": V2 path, value is a token-demand magnitude
223+
RequiredCapacityUnit string
217224
// ScaleTargetRef references the Deployment/StatefulSet for scheduling constraints
218225
ScaleTargetRef *autoscalingv2.CrossVersionObjectReference
219226

internal/metrics/metrics.go

Lines changed: 62 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ var (
2525
spareCapacity *prometheus.GaugeVec
2626
requiredCapacity *prometheus.GaugeVec
2727
kvCacheTokensUsed *prometheus.GaugeVec
28-
kvCacheTokensTotal *prometheus.GaugeVec
28+
kvCacheTokensCapacity *prometheus.GaugeVec
2929

3030
// controllerInstance stores the optional controller instance identifier.
3131
// When set, it's added as a label to all emitted metrics.
@@ -51,9 +51,9 @@ func InitMetrics(registry prometheus.Registerer) error {
5151
scalingLabels := []string{constants.LabelVariantName, constants.LabelNamespace, constants.LabelDirection, constants.LabelReason}
5252
// modelLabels: variant_name + namespace only (no accelerator_type) for model-level and token metrics
5353
modelLabels := []string{constants.LabelVariantName, constants.LabelNamespace}
54-
// requiredCapacityLabels: model labels + analyzer_version to disambiguate V1 (binary)
55-
// vs V2 (continuous tokens) units of the wva_required_capacity gauge
56-
requiredCapacityLabels := []string{constants.LabelVariantName, constants.LabelNamespace, constants.LabelAnalyzerVersion}
54+
// requiredCapacityLabels: model labels + "unit" to disambiguate V1 (binary 0/1)
55+
// vs V2 (continuous token demand) values of the wva_required_capacity gauge
56+
requiredCapacityLabels := []string{constants.LabelVariantName, constants.LabelNamespace, constants.LabelUnit}
5757

5858
if controllerInstance != "" {
5959
baseLabels = append(baseLabels, constants.LabelControllerInstance)
@@ -93,35 +93,35 @@ func InitMetrics(registry prometheus.Registerer) error {
9393
saturationUtilization = prometheus.NewGaugeVec(
9494
prometheus.GaugeOpts{
9595
Name: constants.WVASaturationUtilization,
96-
Help: "Per-variant utilization ratio (0.0-1.0) from saturation analysis",
96+
Help: "Per-variant utilization ratio (0.0-1.0) from saturation analysis. V1 path: mean of per-replica KV-cache-usage fractions (matches the per-replica threshold V1 checks). V2 path: TotalDemand / TotalCapacity from the analyzer result. Numerically equivalent for uniform-capacity replicas; V2 is capacity-weighted for mixed-capacity cases.",
9797
},
9898
baseLabels,
9999
)
100100
spareCapacity = prometheus.NewGaugeVec(
101101
prometheus.GaugeOpts{
102102
Name: constants.WVASpareCapacity,
103-
Help: "Per-variant spare capacity (0.0-1.0) from saturation analysis",
103+
Help: "Per-variant spare KV-cache capacity (0.0-1.0) from saturation analysis. V1 path: threshold-relative spare (kvCacheThreshold - avg KV usage). V2 path: 1.0 - utilization.",
104104
},
105105
baseLabels,
106106
)
107107
requiredCapacity = prometheus.NewGaugeVec(
108108
prometheus.GaugeOpts{
109109
Name: constants.WVARequiredCapacity,
110-
Help: "Model-level required capacity; >0 indicates scale-up needed. Use the analyzer_version label to distinguish units (V1: binary 0/1, V2: continuous token demand).",
110+
Help: fmt.Sprintf("Model-level required capacity; >0 indicates scale-up needed. Use the %q label to interpret the value: %q → 0/1 scale-up signal (V1), %q → token demand (V2).", constants.LabelUnit, constants.UnitBinary, constants.UnitContinuous),
111111
},
112112
requiredCapacityLabels,
113113
)
114114
kvCacheTokensUsed = prometheus.NewGaugeVec(
115115
prometheus.GaugeOpts{
116116
Name: constants.WVAKvCacheTokensUsed,
117-
Help: "Total KV cache tokens currently in use across all replicas of a variant",
117+
Help: "Total KV cache tokens currently in use across all replicas of a variant (sum of vLLM TokensInUse).",
118118
},
119119
modelLabels,
120120
)
121-
kvCacheTokensTotal = prometheus.NewGaugeVec(
121+
kvCacheTokensCapacity = prometheus.NewGaugeVec(
122122
prometheus.GaugeOpts{
123-
Name: constants.WVAKvCacheTokensTotal,
124-
Help: "Total KV cache token capacity across all replicas of a variant",
123+
Name: constants.WVAKvCacheTokensCapacity,
124+
Help: "Total KV cache token capacity across all replicas of a variant (sum of vLLM TotalKvCapacityTokens).",
125125
},
126126
modelLabels,
127127
)
@@ -151,8 +151,8 @@ func InitMetrics(registry prometheus.Registerer) error {
151151
if err := registry.Register(kvCacheTokensUsed); err != nil {
152152
return fmt.Errorf("failed to register kvCacheTokensUsed metric: %w", err)
153153
}
154-
if err := registry.Register(kvCacheTokensTotal); err != nil {
155-
return fmt.Errorf("failed to register kvCacheTokensTotal metric: %w", err)
154+
if err := registry.Register(kvCacheTokensCapacity); err != nil {
155+
return fmt.Errorf("failed to register kvCacheTokensCapacity metric: %w", err)
156156
}
157157

158158
return nil
@@ -230,16 +230,16 @@ func (m *MetricsEmitter) EmitReplicaMetrics(ctx context.Context, va *llmdOptv1al
230230
}
231231

232232
// EmitSaturationMetrics emits saturation analysis and KV cache capacity metrics.
233-
// analyzerVersion ("v1" or "v2") is used as a label on wva_required_capacity to
234-
// disambiguate the units of the required value (V1: binary, V2: continuous tokens).
233+
// requiredCapacityUnit ("binary" or "continuous") is used as the "unit" label on
234+
// wva_required_capacity to describe how the value should be interpreted.
235235
func (m *MetricsEmitter) EmitSaturationMetrics(
236236
ctx context.Context,
237-
variantName, namespace, acceleratorType, analyzerVersion string,
237+
variantName, namespace, acceleratorType, requiredCapacityUnit string,
238238
utilization, spare, required float64,
239-
kvTokensUsed, kvTokensTotal int64,
239+
kvTokensUsed, kvTokensCapacity int64,
240240
) error {
241241
if saturationUtilization == nil || spareCapacity == nil || requiredCapacity == nil ||
242-
kvCacheTokensUsed == nil || kvCacheTokensTotal == nil {
242+
kvCacheTokensUsed == nil || kvCacheTokensCapacity == nil {
243243
return errors.New("saturation metrics not initialized")
244244
}
245245

@@ -253,9 +253,9 @@ func (m *MetricsEmitter) EmitSaturationMetrics(
253253
constants.LabelNamespace: namespace,
254254
}
255255
requiredLabels := prometheus.Labels{
256-
constants.LabelVariantName: variantName,
257-
constants.LabelNamespace: namespace,
258-
constants.LabelAnalyzerVersion: analyzerVersion,
256+
constants.LabelVariantName: variantName,
257+
constants.LabelNamespace: namespace,
258+
constants.LabelUnit: requiredCapacityUnit,
259259
}
260260

261261
if controllerInstance != "" {
@@ -268,7 +268,7 @@ func (m *MetricsEmitter) EmitSaturationMetrics(
268268
spareCapacity.With(accelLabels).Set(spare)
269269
requiredCapacity.With(requiredLabels).Set(required)
270270
kvCacheTokensUsed.With(modelLabels).Set(float64(kvTokensUsed))
271-
kvCacheTokensTotal.With(modelLabels).Set(float64(kvTokensTotal))
271+
kvCacheTokensCapacity.With(modelLabels).Set(float64(kvTokensCapacity))
272272

273273
return nil
274274
}
@@ -280,8 +280,42 @@ func (m *MetricsEmitter) EmitSaturationMetrics(
280280
// TODO: wire this from the controller's VariantAutoscaling delete handler / finalizer.
281281
// Until that wiring exists, deleted VAs leave their last-emitted metric values in the
282282
// registry indefinitely.
283-
func (m *MetricsEmitter) DeleteSaturationMetrics(variantName, namespace, acceleratorType, analyzerVersion string) {
284-
if saturationUtilization == nil {
283+
// DeleteSaturationMetricsForVariant removes all saturation metric series for the
284+
// given (variant, namespace) regardless of accelerator type or unit label. Uses
285+
// Prometheus DeletePartialMatch so callers don't need to know every label value.
286+
//
287+
// Intended for two scenarios:
288+
// - The optimization cycle produced no decision for this variant — existing
289+
// series would otherwise persist with stale values until Prometheus' default
290+
// 5-minute staleness marker fires. Calling this ensures dashboards show a
291+
// gap (honest "no fresh data") rather than stale values.
292+
// - A VA is being removed (delete handler / finalizer) and the caller doesn't
293+
// want to reconstruct the exact emitted label set.
294+
func (m *MetricsEmitter) DeleteSaturationMetricsForVariant(variantName, namespace string) {
295+
if saturationUtilization == nil || spareCapacity == nil || requiredCapacity == nil ||
296+
kvCacheTokensUsed == nil || kvCacheTokensCapacity == nil {
297+
return
298+
}
299+
match := prometheus.Labels{
300+
constants.LabelVariantName: variantName,
301+
constants.LabelNamespace: namespace,
302+
}
303+
if controllerInstance != "" {
304+
match[constants.LabelControllerInstance] = controllerInstance
305+
}
306+
saturationUtilization.DeletePartialMatch(match)
307+
spareCapacity.DeletePartialMatch(match)
308+
requiredCapacity.DeletePartialMatch(match)
309+
kvCacheTokensUsed.DeletePartialMatch(match)
310+
kvCacheTokensCapacity.DeletePartialMatch(match)
311+
}
312+
313+
func (m *MetricsEmitter) DeleteSaturationMetrics(variantName, namespace, acceleratorType, requiredCapacityUnit string) {
314+
// No-op if any metric hasn't been initialized. InitMetrics registers all
315+
// five together, so either all are non-nil or all are nil — checking every
316+
// var defends against a future code path that partially initializes them.
317+
if saturationUtilization == nil || spareCapacity == nil || requiredCapacity == nil ||
318+
kvCacheTokensUsed == nil || kvCacheTokensCapacity == nil {
285319
return
286320
}
287321
accelLabels := prometheus.Labels{
@@ -294,9 +328,9 @@ func (m *MetricsEmitter) DeleteSaturationMetrics(variantName, namespace, acceler
294328
constants.LabelNamespace: namespace,
295329
}
296330
requiredLabels := prometheus.Labels{
297-
constants.LabelVariantName: variantName,
298-
constants.LabelNamespace: namespace,
299-
constants.LabelAnalyzerVersion: analyzerVersion,
331+
constants.LabelVariantName: variantName,
332+
constants.LabelNamespace: namespace,
333+
constants.LabelUnit: requiredCapacityUnit,
300334
}
301335
if controllerInstance != "" {
302336
accelLabels[constants.LabelControllerInstance] = controllerInstance
@@ -307,5 +341,5 @@ func (m *MetricsEmitter) DeleteSaturationMetrics(variantName, namespace, acceler
307341
spareCapacity.Delete(accelLabels)
308342
requiredCapacity.Delete(requiredLabels)
309343
kvCacheTokensUsed.Delete(modelLabels)
310-
kvCacheTokensTotal.Delete(modelLabels)
344+
kvCacheTokensCapacity.Delete(modelLabels)
311345
}

0 commit comments

Comments
 (0)