|
| 1 | +# Response Latency Model |
| 2 | + |
| 3 | +[Wald (Inverse Gaussian)](https://en.wikipedia.org/wiki/Inverse_Gaussian_distribution) |
| 4 | +hierarchical model with Hilbert Space Gaussian Process (HSGP) temporal |
| 5 | +adjustment. Detects mean latency shifts between OGX versions at a 1ms |
| 6 | +threshold. |
| 7 | + |
| 8 | +Response times are strictly positive and right-skewed (occasional slow |
| 9 | +requests). The Wald (Inverse Gaussian) captures this naturally, while a Normal would allow |
| 10 | +impossible negative times. |
| 11 | + |
| 12 | +## Likelihood |
| 13 | + |
| 14 | +```text |
| 15 | +y_i ~ Wald/InvGaussian(mu_i, lambda_i) |
| 16 | +``` |
| 17 | + |
| 18 | +## Mean (Location) Structure |
| 19 | + |
| 20 | +```text |
| 21 | +mu_i = mu_version[g_i] + beta_drift * d_i + delta_run[t_i] + f(x_i) |
| 22 | +``` |
| 23 | + |
| 24 | +## Shape Structure |
| 25 | + |
| 26 | +Lambda is the Wald (Inverse Gaussian) shape parameter: higher values produce a narrower |
| 27 | +peak and lighter tails. Spread scales as mu^3 / lambda, so higher |
| 28 | +lambda means more consistent response times at the same mean latency. |
| 29 | + |
| 30 | +```text |
| 31 | +lambda_i = exp(log_lambda_run[t_i]) |
| 32 | +log_lambda_run[t] ~ Normal(log_lambda_bar, sigma_lambda) |
| 33 | +``` |
| 34 | + |
| 35 | +## Run Intercepts |
| 36 | + |
| 37 | +Each run gets its own intercept (delta_run) to account for |
| 38 | +run-to-run variation (e.g., different thermal state, background load). |
| 39 | +Within each version group, the intercepts are independently constrained |
| 40 | +to sum to zero via [ZeroSumNormal](https://www.pymc.io/projects/docs/en/stable/api/distributions/generated/pymc.ZeroSumNormal.html) |
| 41 | +so that the version mean latency (mu_version) cleanly represents |
| 42 | +each version's average response time. |
| 43 | + |
| 44 | +## Experimental Design (RCBD) |
| 45 | + |
| 46 | +The run order follows a Randomized Complete Block Design. Each block |
| 47 | +contains exactly one run of each version (baseline, comparison, |
| 48 | +comparison_ctrl) in a randomly permuted order. Blocks execute in |
| 49 | +temporal sequence (block 1 first, block N last). |
| 50 | + |
| 51 | +Blocking prevents temporal drift from aliasing with the treatment |
| 52 | +effect: any monotonic trend affects all three versions within a block |
| 53 | +approximately equally. Within-block randomization prevents position |
| 54 | +effects (e.g., cold-start penalty for the first run in a block) |
| 55 | +from systematically favoring one version. |
| 56 | + |
| 57 | +beta_drift operates on the global chronological run position d_i, |
| 58 | +scaled to [0, 1] across the full experiment. It captures smooth |
| 59 | +linear drift that the block structure does not resolve (e.g., |
| 60 | +gradual thermal ramp within a block). The two mechanisms are |
| 61 | +complementary: blocking removes arbitrary between-block shifts, |
| 62 | +beta_drift removes smooth within-experiment trends. |
| 63 | + |
| 64 | +## Gaussian Process Temporal Adjustment |
| 65 | + |
| 66 | +Sequential observations within a run share transient system state |
| 67 | +(GC pressure, connection pool warm-up, event loop saturation), |
| 68 | +producing temporal autocorrelation. The GP captures this within-run |
| 69 | +dependence so that credible intervals on the version effect (beta_v) |
| 70 | +reflect the true effective sample size. Run intercepts (delta_run) |
| 71 | +capture between-run level shifts; the GP captures within-run |
| 72 | +temporal structure by operating on the sequence number of each |
| 73 | +request within a run (1st request, 2nd request, ...). |
| 74 | + |
| 75 | +The [HSGP (Hilbert Space Gaussian Process)](https://www.pymc.io/projects/docs/en/stable/api/gp/generated/pymc.gp.HSGP.html) |
| 76 | +is an approximation to a full Gaussian Process that scales |
| 77 | +linearly in the number of observations, making it practical for |
| 78 | +the large datasets typical of automated performance experiments. |
| 79 | +[Matern 3/2](https://en.wikipedia.org/wiki/Mat%C3%A9rn_covariance_function) |
| 80 | +kernel: nearby observations are correlated, correlation decays |
| 81 | +smoothly with distance. |
| 82 | + |
| 83 | +```text |
| 84 | +f ~ HSGP(Matern32, m=20, c=1.5, noncentered, drop_first=True) |
| 85 | +``` |
| 86 | + |
| 87 | +## Priors |
| 88 | + |
| 89 | +```text |
| 90 | +mu_version[g] ~ Normal(25, 10), for each version group |
| 91 | +beta_drift ~ Normal(0, 2) |
| 92 | +sigma_run ~ Exponential(1) |
| 93 | +delta_run[t] ~ ZeroSumNormal(sigma_run), independently per group |
| 94 | +log_lambda_bar ~ Normal(7, 2) |
| 95 | +sigma_lambda ~ Exponential(1) |
| 96 | +eta_gp ~ HalfNormal(0.25) |
| 97 | +ell_gp ~ InverseGamma(mu=6, sigma=3) |
| 98 | +``` |
| 99 | + |
| 100 | +## Derived Quantities |
| 101 | + |
| 102 | +```text |
| 103 | +beta_v[g] = mu_version[g] - mu_version[baseline] |
| 104 | +``` |
| 105 | + |
| 106 | +## Definitions |
| 107 | + |
| 108 | +A bare index like `g` or `t` ranges over all values (used in priors). |
| 109 | +A subscripted index like `g_i` or `t_i` is a lookup: it maps |
| 110 | +observation `i` to its group or run. |
| 111 | + |
| 112 | +The `_bar` suffix denotes a population average: the center of the |
| 113 | +distribution that individual values are drawn from. |
| 114 | + |
| 115 | +| Symbol | Known/Estimated | Code variable | Description | |
| 116 | +|---|---|---|---| |
| 117 | +| i | index | — | Observation index | |
| 118 | +| g | index | — | Version group index (baseline, comparison, comparison_ctrl) | |
| 119 | +| t | index | — | Run index | |
| 120 | +| y_i | known | `y` | Response time for observation i (ms, positive) | |
| 121 | +| g_i | known | `x_group` | Version group that observation i belongs to | |
| 122 | +| t_i | known | `x_run` | Run that observation i belongs to | |
| 123 | +| d_i | known | `x_drift` | Run's chronological position in the experiment, scaled to [0, 1] | |
| 124 | +| x_i | known | `x_time` | Observation sequence number within a run | |
| 125 | +| mu_version[g] | estimated | `mu_version` | Mean latency for version group g (baseline, comparison, comparison_ctrl) (ms) | |
| 126 | +| beta_drift | estimated | `beta_drift` | Linear drift over run ordering (ms) | |
| 127 | +| sigma_run | estimated | `sigma_run` | Hierarchical standard deviation for run intercepts (ms) | |
| 128 | +| delta_run[t] | estimated | `delta_run_full` | Run-level deviation from group mean (ms), ZeroSumNormal within each group | |
| 129 | +| log_lambda_bar | estimated | `log_lambda_bar` | Population average log shape across runs | |
| 130 | +| sigma_lambda | estimated | `sigma_lambda` | Between-run standard deviation in log shape | |
| 131 | +| log_lambda_run[t] | estimated | `log_lambda_run` | Per-run log shape (centered parameterization) | |
| 132 | +| lambda_i | deterministic | `lambda_obs` | exp(log_lambda_run[t_i]). Wald (Inverse Gaussian) shape parameter | |
| 133 | +| eta_gp | estimated | `eta_gp` | How far the GP can shift latency within a run (ms) | |
| 134 | +| ell_gp | estimated | `ell_gp` | How many observations the GP effect stays correlated over | |
| 135 | +| f(x) | estimated | `f` | HSGP Matern32 within-run temporal adjustment | |
| 136 | +| beta_v[g] | deterministic | `beta_v` | How much slower (positive) or faster (negative) version g is compared to baseline, in ms | |
| 137 | + |
| 138 | +## Data Filtering |
| 139 | + |
| 140 | +First and last observation per run are dropped. The first is a |
| 141 | +Locust client warmup artifact (+20ms). The last is an edge-of-window |
| 142 | +effect. |
| 143 | + |
| 144 | +## Code Reference |
| 145 | + |
| 146 | +| What | File | Function | |
| 147 | +|---|---|---| |
| 148 | +| Model definition and data filtering | `fit_resp_latency_model.py` | `build_model()` | |
| 149 | +| Fitting and LOO | `fit_resp_latency_model.py` | `fit_and_diagnose()` | |
0 commit comments