CI / test (push) Has been cancelled
Comments, doc comments, test names, error and log messages, CLI help, README, Makefile, CI step names and the design document are now all in English. No behaviour changes; the full suite passes under the race detector. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
143 lines
5.0 KiB
Go
143 lines
5.0 KiB
Go
package sim
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
// ttftBuckets and tpotBuckets use second boundaries close to vLLM's own.
|
|
var (
|
|
ttftBuckets = []float64{0.001, 0.005, 0.01, 0.02, 0.04, 0.06, 0.08, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10, 20, 40, 80}
|
|
tpotBuckets = []float64{0.01, 0.025, 0.05, 0.075, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.75, 1, 2.5}
|
|
)
|
|
|
|
// histogram is a minimal Prometheus-compatible cumulative histogram.
|
|
type histogram struct {
|
|
bounds []float64
|
|
counts []uint64
|
|
sum float64
|
|
count uint64
|
|
}
|
|
|
|
func newHistogram(bounds []float64) *histogram {
|
|
return &histogram{bounds: bounds, counts: make([]uint64, len(bounds))}
|
|
}
|
|
|
|
func (h *histogram) observe(v float64) {
|
|
h.sum += v
|
|
h.count++
|
|
for i, b := range h.bounds {
|
|
if v <= b {
|
|
h.counts[i]++
|
|
}
|
|
}
|
|
}
|
|
|
|
// write renders the histogram in Prometheus text format (cumulative le buckets).
|
|
func (h *histogram) write(w io.Writer, name, labels string) {
|
|
var cum uint64
|
|
for i, b := range h.bounds {
|
|
cum = h.counts[i]
|
|
fmt.Fprintf(w, "%s_bucket{%s,le=\"%g\"} %d\n", name, labels, b, cum)
|
|
}
|
|
fmt.Fprintf(w, "%s_bucket{%s,le=\"+Inf\"} %d\n", name, labels, h.count)
|
|
fmt.Fprintf(w, "%s_sum{%s} %g\n", name, labels, h.sum)
|
|
fmt.Fprintf(w, "%s_count{%s} %d\n", name, labels, h.count)
|
|
}
|
|
|
|
type metrics struct {
|
|
received atomic.Int64
|
|
finished atomic.Int64
|
|
failed atomic.Int64
|
|
cancelled atomic.Int64
|
|
|
|
promptTokens atomic.Int64
|
|
genTokens atomic.Int64
|
|
|
|
mu sync.Mutex
|
|
ttft *histogram
|
|
tpot *histogram
|
|
}
|
|
|
|
func newMetrics() *metrics {
|
|
return &metrics{
|
|
ttft: newHistogram(ttftBuckets),
|
|
tpot: newHistogram(tpotBuckets),
|
|
}
|
|
}
|
|
|
|
func (m *metrics) observeTTFT(d time.Duration) {
|
|
m.mu.Lock()
|
|
m.ttft.observe(d.Seconds())
|
|
m.mu.Unlock()
|
|
}
|
|
|
|
func (m *metrics) observeTPOT(d time.Duration) {
|
|
m.mu.Lock()
|
|
m.tpot.observe(d.Seconds())
|
|
m.mu.Unlock()
|
|
}
|
|
|
|
// WriteMetrics renders the engine's state in Prometheus text format using
|
|
// vLLM's metric names.
|
|
//
|
|
// Those names are the engine contract from plan §7.6; the router parses
|
|
// against them. Matching real vLLM names exactly is the point: sim-engine
|
|
// exists to stand in for a real engine as far as the router can tell.
|
|
func (e *Engine) WriteMetrics(w io.Writer) {
|
|
s := e.Stats()
|
|
lbl := fmt.Sprintf("model_name=%q", e.cfg.Model)
|
|
|
|
fmt.Fprintf(w, "# HELP vllm:num_requests_running Number of requests currently running on GPU.\n")
|
|
fmt.Fprintf(w, "# TYPE vllm:num_requests_running gauge\n")
|
|
fmt.Fprintf(w, "vllm:num_requests_running{%s} %d\n", lbl, s.Running)
|
|
|
|
fmt.Fprintf(w, "# HELP vllm:num_requests_waiting Number of requests waiting to be processed.\n")
|
|
fmt.Fprintf(w, "# TYPE vllm:num_requests_waiting gauge\n")
|
|
fmt.Fprintf(w, "vllm:num_requests_waiting{%s} %d\n", lbl, s.Waiting)
|
|
|
|
fmt.Fprintf(w, "# HELP vllm:kv_cache_usage_perc KV-cache usage. 1 means 100 percent usage.\n")
|
|
fmt.Fprintf(w, "# TYPE vllm:kv_cache_usage_perc gauge\n")
|
|
fmt.Fprintf(w, "vllm:kv_cache_usage_perc{%s} %g\n", lbl, s.KVCacheUsage)
|
|
|
|
fmt.Fprintf(w, "# HELP vllm:cache_config_info Information of the LLMEngine CacheConfig.\n")
|
|
fmt.Fprintf(w, "# TYPE vllm:cache_config_info gauge\n")
|
|
fmt.Fprintf(w, "vllm:cache_config_info{block_size=\"%d\",num_gpu_blocks=\"%d\"} 1\n", s.BlockSize, s.NumGPUBlocks)
|
|
|
|
fmt.Fprintf(w, "# HELP vllm:prefix_cache_queries_total Prefix cache queries, in terms of number of queried blocks.\n")
|
|
fmt.Fprintf(w, "# TYPE vllm:prefix_cache_queries_total counter\n")
|
|
fmt.Fprintf(w, "vllm:prefix_cache_queries_total{%s} %d\n", lbl, s.PrefixQueries)
|
|
|
|
fmt.Fprintf(w, "# HELP vllm:prefix_cache_hits_total Prefix cache hits, in terms of number of cached blocks.\n")
|
|
fmt.Fprintf(w, "# TYPE vllm:prefix_cache_hits_total counter\n")
|
|
fmt.Fprintf(w, "vllm:prefix_cache_hits_total{%s} %d\n", lbl, s.PrefixHits)
|
|
|
|
fmt.Fprintf(w, "# HELP vllm:prompt_tokens_total Number of prefill tokens processed.\n")
|
|
fmt.Fprintf(w, "# TYPE vllm:prompt_tokens_total counter\n")
|
|
fmt.Fprintf(w, "vllm:prompt_tokens_total{%s} %d\n", lbl, e.m.promptTokens.Load())
|
|
|
|
fmt.Fprintf(w, "# HELP vllm:generation_tokens_total Number of generation tokens processed.\n")
|
|
fmt.Fprintf(w, "# TYPE vllm:generation_tokens_total counter\n")
|
|
fmt.Fprintf(w, "vllm:generation_tokens_total{%s} %d\n", lbl, e.m.genTokens.Load())
|
|
|
|
fmt.Fprintf(w, "# HELP vllm:request_success_total Count of successfully processed requests.\n")
|
|
fmt.Fprintf(w, "# TYPE vllm:request_success_total counter\n")
|
|
fmt.Fprintf(w, "vllm:request_success_total{%s,finished_reason=\"length\"} %d\n", lbl, e.m.finished.Load())
|
|
|
|
// Simulation-specific (absent in real vLLM; useful for chaos tests).
|
|
fmt.Fprintf(w, "# TYPE sim:requests_cancelled_total counter\n")
|
|
fmt.Fprintf(w, "sim:requests_cancelled_total{%s} %d\n", lbl, e.m.cancelled.Load())
|
|
fmt.Fprintf(w, "# TYPE sim:requests_failed_total counter\n")
|
|
fmt.Fprintf(w, "sim:requests_failed_total{%s} %d\n", lbl, e.m.failed.Load())
|
|
|
|
e.m.mu.Lock()
|
|
defer e.m.mu.Unlock()
|
|
fmt.Fprintf(w, "# TYPE vllm:time_to_first_token_seconds histogram\n")
|
|
e.m.ttft.write(w, "vllm:time_to_first_token_seconds", lbl)
|
|
fmt.Fprintf(w, "# TYPE vllm:time_per_output_token_seconds histogram\n")
|
|
e.m.tpot.write(w, "vllm:time_per_output_token_seconds", lbl)
|
|
}
|