Skip to content

Commit c31bcdd

Browse files
authored
Report actual decode speed (inverse inter-token latency), not idle-diluted throughput (#2)
The dashboard token rate is rate(vllm:generation_tokens_total[2m]) — tokens over wall-clock, so bursty mostly-idle traffic reads far below the model's real generation speed (~17 tok/s displayed vs ~140 tok/s actual decode). Add decode_tokens_per_sec = rate(count)/rate(sum) of the vllm:inter_token_latency_seconds histogram (inverse mean inter-token latency) — the real per-token generation speed while generating, excluding idle. Existing throughput rates are kept (carbon-per-token math needs them) and shown as secondary context. Idle => no series (0/0), field omitted.
1 parent 00a30d1 commit c31bcdd

2 files changed

Lines changed: 42 additions & 3 deletions

File tree

cmd/static/dashboard.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ <h2>Models</h2>
376376
<div class="metric"><div class="metric-label">GPU Power</div><div class="metric-value">${fmt(m.power_watts)}</div><div class="metric-unit">watts</div></div>
377377
<div class="metric"><div class="metric-label">CO₂ / token</div><div class="metric-value">${m.co2_mg_per_token?fmt(m.co2_mg_per_token,3):'—'}</div><div class="metric-unit">mg · 5 min${m.co2_mg_per_token_avg_24h ? ` · <span style="color:var(--muted)">24h avg: ${fmt(m.co2_mg_per_token_avg_24h,3)}</span>` : ''}</div></div>
378378
<div class="metric"><div class="metric-label">Input tok/s</div><div class="metric-value">${active?fmt(m.prompt_tokens_per_sec):'—'}</div><div class="metric-unit">tok/s prompt</div></div>
379-
<div class="metric"><div class="metric-label">Output tok/s</div><div class="metric-value">${active?fmt(m.generation_tokens_per_sec):'—'}</div><div class="metric-unit">tok/s generated</div></div>
379+
<div class="metric"><div class="metric-label">Decode speed</div><div class="metric-value">${m.decode_tokens_per_sec?fmt(m.decode_tokens_per_sec):(active?fmt(m.generation_tokens_per_sec):'—')}</div><div class="metric-unit">tok/s generating${active?` · <span style="color:var(--muted)">${fmt(m.generation_tokens_per_sec)} throughput</span>`:''}</div></div>
380380
<div class="metric"><div class="metric-label">J / token</div><div class="metric-value">${jPerToken!=null?fmt(jPerToken,3):'—'}</div><div class="metric-unit">joules · 24h avg</div></div>
381381
${frontierMetric}
382382
</div>

internal/scraper/scraper.go

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,13 @@ type ModelMetrics struct {
5858
GPUCount int `json:"gpu_count"`
5959
PowerWatts float64 `json:"power_watts"`
6060
PromptTokensPerSec float64 `json:"prompt_tokens_per_sec"` // input (prefill) token rate
61-
GenerationTokensPerSec float64 `json:"generation_tokens_per_sec"` // output (decode) token rate
62-
TokensPerSec float64 `json:"tokens_per_sec"` // total = prompt + generation
61+
GenerationTokensPerSec float64 `json:"generation_tokens_per_sec"` // output throughput over wall-clock (incl. idle gaps)
62+
TokensPerSec float64 `json:"tokens_per_sec"` // total throughput = prompt + generation
63+
// DecodeTokensPerSec is the ACTUAL generation speed while generating —
64+
// the inverse of vLLM's mean inter-token latency, so it is not diluted by
65+
// idle time between requests (unlike the throughput rates above). This is
66+
// the "N tok/s" figure people usually quote. Omitted when idle.
67+
DecodeTokensPerSec float64 `json:"decode_tokens_per_sec,omitempty"`
6368

6469
// Carbon
6570
CarbonIntensity float64 `json:"carbon_intensity_kg_per_kwh"`
@@ -417,6 +422,11 @@ func (s *Scraper) scrape() {
417422
log.Printf("scraper: MTP acceptance query failed: %v", err)
418423
}
419424

425+
decodeSpeedByKey, err := s.queryDecodeSpeed()
426+
if err != nil {
427+
log.Printf("scraper: decode speed query failed: %v", err)
428+
}
429+
420430
keys := make(map[string]struct{})
421431
for k := range powerByKey {
422432
keys[k] = struct{}{}
@@ -474,6 +484,9 @@ func (s *Scraper) scrape() {
474484
if mtp, ok := mtpAcceptanceByKey[key]; ok {
475485
m.MTPAcceptancePerc = math.Round(mtp*10) / 10
476486
}
487+
if ds, ok := decodeSpeedByKey[key]; ok && ds > 0 {
488+
m.DecodeTokensPerSec = math.Round(ds*10) / 10
489+
}
477490

478491
s.models[key] = m
479492

@@ -647,6 +660,32 @@ func (s *Scraper) queryRequestRate() (map[string]float64, error) {
647660
return rate, nil
648661
}
649662

663+
// queryDecodeSpeed returns the actual generation speed (output tokens/sec
664+
// while generating) keyed by namespace, computed as the inverse of vLLM's
665+
// mean inter-token latency over a 2-minute window: rate(count)/rate(sum) of
666+
// the inter_token_latency_seconds histogram. Unlike the wall-clock token
667+
// rate, this excludes idle time between requests, so it reflects the real
668+
// per-token decode rate (e.g. ~140 tok/s here) rather than a utilization
669+
// average. Idle namespaces produce no series (0/0) and are simply absent.
670+
func (s *Scraper) queryDecodeSpeed() (map[string]float64, error) {
671+
ns := s.cfg.Namespace
672+
results, err := s.client.Query(
673+
fmt.Sprintf(`sum by (namespace) (rate(vllm:inter_token_latency_seconds_count{namespace=%q}[2m]))
674+
/ sum by (namespace) (rate(vllm:inter_token_latency_seconds_sum{namespace=%q}[2m]))`, ns, ns),
675+
)
676+
if err != nil {
677+
return nil, err
678+
}
679+
speed := make(map[string]float64)
680+
for _, r := range results {
681+
if math.IsNaN(r.Value) || math.IsInf(r.Value, 0) {
682+
continue
683+
}
684+
speed[r.Metric["namespace"]] = r.Value
685+
}
686+
return speed, nil
687+
}
688+
650689
// queryMTPAcceptance returns the speculative-decoding (MTP) draft-token
651690
// acceptance rate (%) keyed by namespace.
652691
func (s *Scraper) queryMTPAcceptance() (map[string]float64, error) {

0 commit comments

Comments
 (0)