# QueueingModelAnalyzer
**Location:** `internal/engines/analyzers/queueingmodel/analyzer.go`
The QueueingModelAnalyzer is a capacity analyzer that uses queueing theory to determine
how many requests each model variant can handle while meeting latency SLOs. It learns
hardware characteristics online via an Extended Kalman Filter and uses a closed-form
queueing model to predict per-replica capacity — no manual calibration needed.
## Table of Contents
1. [Activating the Analyzer](#activating)
2. [ConfigMap Reference](#configmap-reference)
3. [SLO Targeting](#slo-targeting)
4. [How It Works](#how-it-works)
5. [Cold Start Behavior](#cold-start)
6. [Data Flow](#data-flow)
7. [Key Files](#key-files)
8. [Defaults Reference](#defaults-reference)
9. [Theoretical Background](#theory)
---
## 1. Activating the Analyzer
The analyzer is activated by **applying the ConfigMap**
`deploy/configmap-queueing-model.yaml` to the cluster. When this ConfigMap
(`wva-queueing-model-config`) exists and contains a `default` key, the WVA controller uses
the queueing model path.
```bash
kubectl apply -f deploy/configmap-queueing-model.yaml
```
> The ConfigMap is re-read on every reconcile cycle, so changes take effect within one
> reconcile interval — no controller restart required.
---
## 2. ConfigMap Reference
The ConfigMap has two types of entries:
- **`default`** — global settings applied to all models.
- **Per-model entries** — any other key name is a per-model override; the model is
identified by `model_id` + `namespace` inside the value.
### Full annotated example
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: wva-queueing-model-config
namespace: workload-variant-autoscaler-system
labels:
app.kubernetes.io/name: workload-variant-autoscaler
app.kubernetes.io/managed-by: kustomize
data:
# ── Global defaults (required) ─────────────────────────────────────────────
default: |
# sloMultiplier (k) controls the target server utilisation.
# At utilisation rho, mean iteration time = alpha/(1-rho).
# Setting the SLO at T_iter = k×alpha yields target utilisation rho = 1-1/k.
#
# Common values:
# k=2.0 -> rho=0.50 (conservative; lower tail latency)
# k=3.0 -> rho=0.67 (default; good balance for most deployments)
# k=5.0 -> rho=0.80 (aggressive; maximise throughput, higher tail latency)
#
# Constraint: must be > 1.0. Default: 3.0
sloMultiplier: 3.0
# tuningEnabled must always be true. The queueing model requires learned
# (alpha, beta, gamma) parameters to compute capacity; without the Kalman
# filter running, no parameters are ever produced and the analyzer cannot
# make scaling decisions.
tuningEnabled: true
# ── Per-model overrides (optional) ─────────────────────────────────────────
# Key name is arbitrary. model_id + namespace identify the target model.
# Per-model entries override sloMultiplier and can provide explicit SLO targets.
#
# Rules:
# - targetTTFT and targetITL must both be set, or both omitted (0 = infer).
# - If both are set, the explicit SLOs are used directly; the inferred SLO
# path is skipped (tuning still runs to keep parameters fresh).
# - If omitted, SLOs are inferred from learned parameters and sloMultiplier
# (or from observations if tuning hasn't converged yet).
# Example: explicit SLOs for a production Llama model
llama-prod: |
model_id: "meta-llama/Meta-Llama-3.1-8B-Instruct"
namespace: "llm-d-prod"
targetTTFT: 500.0 # ms — time-to-first-token budget
targetITL: 50.0 # ms — per-token decode budget
sloMultiplier: 3.0
# Example: inferred SLOs with a more aggressive utilisation target
mistral-staging: |
model_id: "mistralai/Mistral-7B-Instruct-v0.2"
namespace: "llm-d-staging"
sloMultiplier: 4.0 # rho=0.75; SLO derived from k and learned parameters
```
### Configuration field reference
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `sloMultiplier` | float | `3.0` | Utilisation target multiplier k (must be > 1.0). Target rho = 1 - 1/k. |
| `tuningEnabled` | bool | `true` | Must always be `true`. The queueing model requires learned parameters to function. |
| `model_id` | string | — | Model identifier (per-model entries only). |
| `namespace` | string | — | Kubernetes namespace (per-model entries only). |
| `targetTTFT` | float | `0` | Explicit TTFT SLO in milliseconds. `0` = infer automatically. |
| `targetITL` | float | `0` | Explicit ITL SLO in milliseconds. `0` = infer automatically. |
---
## 3. SLO Targeting
The analyzer determines SLO targets for each model through a three-level priority chain:
### 3.1 Explicit SLOs (highest priority)
If `targetTTFT > 0` and `targetITL > 0` are set in the per-model ConfigMap entry, those
values are used directly as the TTFT and ITL budgets. This is the recommended mode for
production deployments where latency requirements are well-defined.
> **Both must be set together, or neither.** Setting only one is a validation error.
### 3.2 Model-Inferred SLOs (preferred automatic mode)
When explicit targets are absent and at least one variant has learned parameters
`(alpha, beta, gamma)`, the SLO is derived from the queueing model using `sloMultiplier` k.
At the target utilisation rho = 1 − 1/k, the steady-state iteration time equals `k × alpha`.
The SLO is then the latency that corresponds to that iteration time, with token-processing
work added at its true cost (see [Section 9](#theory) for the full derivation):
```
TargetTTFT = k×alpha + (beta + gamma) × avg_input_len
TargetITL = k×alpha + beta + gamma × (avg_input_len + (avg_output_len + 1) / 2)
```
When multiple variants serve the same model, the SLO is taken as the **maximum** across
variants — a single SLO per model, since all variants serve the same traffic.
This mode requires the tuner to have converged, which typically takes 3–10 reconcile cycles
after a variant first receives traffic.
### 3.3 Observation-Based Fallback (cold start)
If no variant has learned parameters yet (the first few cycles after deployment, or after a
variant is freshly added), the SLO is estimated from observed latency metrics with a
headroom multiplier:
```
TargetTTFT = min(avg_observed_TTFT × 1.5, 10000 ms)
TargetITL = min(avg_observed_ITL × 1.5, 500 ms)
```
This is intentionally conservative: the system scales out rather than under-provisioning
while the model is still learning. Once the tuner has converged, SLO resolution
automatically transitions to path 3.2.
> **Summary of SLO resolution order:**
> 1. Explicit `targetTTFT` + `targetITL` from per-model ConfigMap entry.
> 2. Derived from learned parameters using `sloMultiplier` (requires tuner convergence).
> 3. Observed latency × 1.5 headroom (cold start only).
---
## 4. How It Works
### 4.1 Workload Aggregation
All components operate on a single workload summary per variant, aggregated from per-pod
metrics. Only pods with active traffic (`arrival_rate > 0`) contribute. Token lengths,
TTFT, and ITL are averaged **weighted by each pod's arrival rate**:
```
avg_input_len = Σ(arrival_rate_i × input_len_i) / Σ(arrival_rate_i)
avg_output_len = Σ(arrival_rate_i × output_len_i) / Σ(arrival_rate_i)
avg_TTFT = Σ(arrival_rate_i × TTFT_i) / Σ(arrival_rate_i)
avg_ITL = Σ(arrival_rate_i × ITL_i) / Σ(arrival_rate_i)
```
The total arrival rate for capacity sizing is the sum across all busy pods:
`total_arrival_rate = Σ arrival_rate_i`.
### 4.2 The Tuner (Online Parameter Estimator)
**Location:** `internal/engines/analyzers/queueingmodel/tuner/tuner.go`
The Tuner wraps an **Extended Kalman Filter (EKF)** that treats the three hardware
parameters `(alpha, beta, gamma)` as the hidden state and `(AvgTTFT, AvgITL)` as
observations. On each reconcile cycle it:
1. **Restores** the previous state estimate and error covariance from the `ParameterStore`
(or bootstraps from a cold-start guess — see [Section 5](#cold-start)).
2. **Predicts** the next state using an identity transition (parameters are assumed slowly
varying).
3. **Updates** by comparing the TTFT and ITL predicted by the queueing model at the current
parameter estimates against the newly observed values.
4. **Validates** the update using the Normalized Innovation Squared (NIS). Updates with
NIS ≥ 7.378 (95th percentile of χ²₂) are rejected as outliers and the previous state is
restored — the filter never accepts a bad update.
5. **Stores** the accepted `(alpha, beta, gamma)` and covariance back in the `ParameterStore`
for the next cycle.
### 4.3 Capacity Sizing
Once `(alpha, beta, gamma)` are available, `QueueAnalyzer.Size(targetPerf)` binary-searches
for the maximum arrival rate `lambda*` at which both predicted TTFT and ITL remain within
the SLO targets. The required replica count is then:
```
required_replicas = ceil(total_arrival_rate / lambda*)
```
### 4.4 Per-Variant Failure Behavior
If analysis of an individual variant fails at any step — no metrics, no active traffic, no
learned parameters yet, or a queueing model error — the variant is **not dropped**. Instead
it is included in the result with a zero-capacity placeholder:
```
PerReplicaCapacity = 0, TotalCapacity = 0, TotalDemand = 0, Utilization = 0
```
The variant's current and pending replica counts are still reported accurately, so the
optimizer sees the full variant roster and can apply safe-hold behavior.
An error is returned at the model level only if **every** variant fails, in which case no
scaling decision is emitted for that model during that cycle.
---
## 5. Cold Start Behavior
When a variant receives traffic for the first time, the `ParameterStore` has no prior
`(alpha, beta, gamma)`. The analyzer bootstraps an initial estimate analytically from
observed TTFT, ITL, and token lengths (see [Section 9.3](#theory-cold-start) for the
derivation). If the bootstrap produces valid positive values, the EKF starts from that
estimate with tightened bounds to converge quickly. If the bootstrap fails (e.g. due to
unusual metric ratios), the EKF falls back to hardcoded defaults
(`alpha=5.0 ms, beta=0.05 ms, gamma=0.00005 ms`) and converges from there.
During this warm-up period — typically 3–10 reconcile cycles — scaling decisions rely on
the observation-based SLO fallback (Section 3.3), which applies a 1.5× headroom to
observed latencies to avoid under-provisioning.
---
## 6. Data Flow
```
Prometheus / vLLM metrics per pod
(arrival_rate, avg_TTFT, avg_ITL, avg_input_tokens, avg_output_tokens)
│
▼
┌─────────────────────────────────────────────────────────┐
│ Collector (replica_metrics.go) │
│ Groups metrics by model → variant → pod │
└───────────────────────┬─────────────────────────────────┘
│ []ReplicaMetrics
▼
┌─────────────────────────────────────────────────────────┐
│ Workload Aggregation (arrival-rate weighted per variant)│
└───────────────────────┬─────────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌───────────────┐ ┌────────────────────────┐
│ Tuner (EKF) │ │ SLO Resolution │
│ │ │ 1. Explicit config │
│ Predict │ │ 2. k×alpha model │
│ Update obs │ │ 3. 1.5× observed │
│ NIS validate │ └──────────┬─────────────┘
│ Store params │ │ SLOTarget
└───────┬───────┘ │
│ (alpha, beta, gamma) │
└──────────────┬───────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ computeAllVariantCapacities │
│ For each variant: │
│ qa = QueueAnalyzer(alpha, beta, gamma, maxBatchSize) │
│ lambda* = qa.Size(SLOTarget) ← binary search │
│ required = ceil(totalArrival / lambda*) │
└───────────────────────┬─────────────────────────────────┘
│ []VariantCapacity
▼
┌─────────────────────────────────────────────────────────┐
│ Optimizer → VariantDecisions → Enforcer → Apply │
└─────────────────────────────────────────────────────────┘
```
---
## 7. Key Files
| Component | Path |
|-----------------------|------------------------------------------------------------------|
| QueueingModelAnalyzer | `internal/engines/analyzers/queueingmodel/analyzer.go` |
| Config types | `internal/engines/analyzers/queueingmodel/config.go` |
| Parameter Store | `internal/engines/analyzers/queueingmodel/parameters.go` |
| Defaults | `internal/engines/analyzers/queueingmodel/defaults.go` |
| Tuner (EKF) | `internal/engines/analyzers/queueingmodel/tuner/tuner.go` |
| Tuner defaults | `internal/engines/analyzers/queueingmodel/tuner/defaults.go` |
| Tuner configurator | `internal/engines/analyzers/queueingmodel/tuner/configurator.go` |
| Tuner environment | `internal/engines/analyzers/queueingmodel/tuner/environment.go` |
| QueueAnalyzer | `internal/queueing/analyzer/queueanalyzer.go` |
| Engine integration | `internal/engines/saturation/engine_queueing_model.go` |
| ConfigMap interface | `internal/domain/queueing_model_scaling.go` |
| ConfigMap YAML | `deploy/configmap-queueing-model.yaml` |
---
## 8. Defaults Reference
| Constant | Value | Meaning |
|----------|-------|---------|
| `DefaultSLOMultiplier` | `3.0` | k=3 → target utilisation rho=0.67 |
| `DefaultMaxBatchSize` | `256` | Max concurrent requests per replica when not parseable from deployment spec |
| `DefaultMaxQueueSize` | `100` | Queue depth limit in the queueing model |
| `DefaultFallbackHeadroom` | `1.5` | Multiplier on observed latency for cold-start SLO |
| `DefaultMaxFallbackTTFT` | `10000 ms` | Cap on fallback TTFT SLO |
| `DefaultMaxFallbackITL` | `500 ms` | Cap on fallback ITL SLO |
| `DefaultMaxNIS` | `7.378` | χ²₂ at 95th percentile — EKF update rejection threshold |
| `DefaultAlpha` | `5.0 ms` | EKF initial guess when bootstrap fails |
| `DefaultBeta` | `0.05 ms/token` | EKF initial guess when bootstrap fails |
| `DefaultGamma` | `0.00005 ms/token` | EKF initial guess when bootstrap fails |
| `BaseFactor` | `0.9` | Fraction of avg ITL used as initial alpha estimate during bootstrap |
| `TransientDelaySeconds` | `120 s` | Grace period before tuning a freshly scaled-up replica |
---
## 9. Theoretical Background
This section provides the analytical foundations of the queueing model for readers who want
to understand how TTFT, ITL, and capacity are derived.
### 9.1 Hardware Parameters
Three hardware-specific parameters connect abstract work to wall-clock time:
| Parameter | Meaning | Governed by |
|-----------|---------|-------------|
| **alpha** | Baseline iteration overhead (ms) — kernel launch latencies, synchronization barriers, model weight loading — constant regardless of batch composition | — |
| **beta** | Compute time per token (ms/token) — matrix multiplications and nonlinear ops in the forward pass | GPU FLOP throughput |
| **gamma** | KV-cache memory access time per token (ms/token) — attention KV read/write cost | GPU memory bandwidth |
### 9.2 Service Time Model
A request with `i_l` input tokens and `o_l` output tokens participates in `o_l + 1`
iterations (one prefill + `o_l` decodes). The work it contributes per iteration varies
by phase:
- **Prefill** (age=0): compute over `i_l` tokens (`β × i_l`) plus KV-cache read of `i_l`
tokens (`γ × i_l`), giving prefill work `w_prefill = (β + γ) × i_l`.
- **Decode step k** (k=1..o_l): compute on one output token (`β`) plus KV-cache read of
all `i_l + k` cached tokens (`γ × (i_l + k)`), giving `W_k = β + γ(i_l + k)`.
Averaged uniformly over all `o_l + 1` iterations, the marginal work per request per
iteration is:
```
δ = β × (i_l + o_l) / (o_l + 1) + γ × (i_l + o_l / 2)
```
With `n` concurrent requests, the iteration time is:
```
T_iter(n) = α + n × δ
```
### 9.3 Steady-State Analysis
By Little's Law, the average number of concurrent requests is `n = λ × (o_l + 1) × T_iter`.
Substituting into the iteration time equation and solving yields the closed-form result:
```
T_iter = α / (1 - ρ)
where ρ = λ × (o_l + 1) × δ
= λ × ( β(i_l + o_l) + γ(o_l + 1)(i_l + o_l/2) )
```
The system is stable when ρ < 1. As ρ → 1, `T_iter` diverges — this is the fundamental
capacity limit of a single replica.
### 9.4 TTFT and ITL
Because of Poisson arrivals, an incoming request must wait for the current iteration to
finish (residual wait ≈ `T_iter`) before being scheduled. TTFT is that wait plus the
prefill work:
```
TTFT = T_iter + (β + γ) × i_l
```
Each decode step takes one full iteration plus the step's own decode work. Averaging over
all `o_l` decode steps gives:
```
ITL = T_iter + β + γ × (i_l + (o_l + 1) / 2)
```
Setting `T_iter = k × alpha` (i.e. targeting utilisation ρ = 1 − 1/k) yields the SLO
inference formulas in Section 3.2.
### 9.5 Initial Parameter Estimation (Cold Start)
When no prior parameters exist, the analyzer bootstraps `(alpha, beta, gamma)` analytically
by inverting the TTFT and ITL equations under the assumption `T_iter ≈ alpha` (valid at
light load, where ρ ≈ 0):
**Step 1:** Estimate alpha from observed ITL, since at light load ITL ≈ α plus minimal
decode work:
```
alpha ≈ BaseFactor × avg_ITL (BaseFactor = 0.9)
```
**Step 2:** Solve for `(beta + gamma)` from the TTFT equation:
```
(beta + gamma) = (avg_TTFT - alpha) / avg_input_len
```
**Step 3:** Separate beta and gamma from the ITL equation:
```
gamma = ((avg_ITL - alpha) - (beta+gamma)) / (avg_input_len + (avg_output_len+1)/2 - 1)
beta = (beta+gamma) - gamma
```
If any derived value is ≤ 0, the bootstrap fails and the EKF starts from hardcoded
defaults (`alpha=5.0 ms, beta=0.05 ms, gamma=0.00005 ms`), converging over subsequent
cycles.