Skip to content

Commit 45fe1b7

Browse files
fix(asap-tools): score p99 runs by rank error in the summary
summarize compared the p99 runs' relative value error with the rank-error target, and showed the recommended config's prediction on default rows. It now computes rank error against the replayed trace values (shared with kll_rank_error.py) and leaves predicted_error empty for default rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 49bb479 commit 45fe1b7

3 files changed

Lines changed: 130 additions & 33 deletions

File tree

‎asap-tools/experiments/recommended_sketch_configs/kll_rank_error.py‎

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
"""
88

99
import argparse
10-
import gzip
1110
import os
1211
import sys
1312

@@ -19,16 +18,12 @@
1918
0, os.path.join(HERE, "../../../asap-common/dependencies/py/promql_utilities")
2019
)
2120
from post_experiment.lib.results_loader import load_results # noqa: E402
22-
23-
24-
def replayed_values(trace: str, cutoff_us: int) -> np.ndarray:
25-
values = []
26-
with gzip.open(trace, "rt") as f:
27-
for line in f:
28-
c = line.rstrip("\n").split(",")
29-
if int(c[0]) <= cutoff_us and c[18] in ("", "0") and c[5] != "":
30-
values.append(float(c[5]))
31-
return np.sort(values)
21+
from recommended_sketch_configs import ( # noqa: E402
22+
P99,
23+
P99_REPLAY_CUTOFF_US,
24+
rank_errors,
25+
replayed_google_cpu_values,
26+
)
3227

3328

3429
def main() -> None:
@@ -39,16 +34,16 @@ def main() -> None:
3934
parser.add_argument(
4035
"--outputs", required=True, help="experiment_outputs directory of the runs"
4136
)
42-
parser.add_argument("--cutoff-us", type=int, default=615_000_000)
37+
parser.add_argument("--cutoff-us", type=int, default=P99_REPLAY_CUTOFF_US)
4338
parser.add_argument(
4439
"--runs",
4540
default="recommended,default,k500",
4641
help="suffixes of google_2011_cpu_p99_instant_<run>",
4742
)
4843
args = parser.parse_args()
4944

50-
v = replayed_values(args.trace, args.cutoff_us)
51-
print(f"{len(v)} values, exact p99 {np.quantile(v, 0.99):.6f}")
45+
v = replayed_google_cpu_values(args.trace, args.cutoff_us)
46+
print(f"{len(v)} values, exact p99 {np.quantile(v, P99):.6f}")
5247
for name in args.runs.split(","):
5348
out = os.path.join(
5449
args.outputs,
@@ -62,7 +57,7 @@ def main() -> None:
6257
for q in results["sketchdb"][0].query_results
6358
if q.result
6459
]
65-
err = np.abs(np.searchsorted(v, est, side="right") / len(v) - 0.99)
60+
err = rank_errors(v, est, P99)
6661
print(
6762
f"{name}: n={len(est)} rank_err median={np.median(err):.4f} "
6863
f"mean={np.mean(err):.4f} max={np.max(err):.4f}"

‎asap-tools/experiments/recommended_sketch_configs/recommended_sketch_configs.py‎

Lines changed: 71 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
1010
`summarize` reads finished experiments and prints, per query and config, the
1111
measured error against Prometheus (ARE over the 100 largest keys for key
12-
queries, the metric sketch-bench's CMS estimate uses; relative value error for
13-
quantile queries; ARE over all keys as a second column) next to the predicted
14-
error, plus query latencies.
12+
queries, the metric sketch-bench's CMS estimate uses, with ARE over all keys as
13+
a second column; rank error against the replayed trace values for the p99
14+
query) next to the predicted error, plus query latencies.
1515
1616
Usage (from asap-tools/experiments):
1717
python recommended_sketch_configs/recommended_sketch_configs.py generate \
@@ -22,6 +22,7 @@
2222

2323
import argparse
2424
import csv
25+
import gzip
2526
import json
2627
import os
2728
import re
@@ -128,6 +129,12 @@
128129
# remote_monitor.py keyword of the containerized query engine.
129130
QUERY_ENGINE_MONITOR_KEYWORD = "sketchdb-queryengine-rust"
130131

132+
# The only quantile query is cpu_p99. A run replays Google at 1/10 speed, so by
133+
# its queries the exporter has exported the rows starting by 615 s (the
134+
# 5-minute window starting at 600 s).
135+
P99 = 0.99
136+
P99_REPLAY_CUTOFF_US = 615_000_000
137+
131138

132139
def parse_duration_s(text: str) -> int:
133140
"""'5m' -> 300."""
@@ -327,15 +334,42 @@ def are_top_keys(exact: Dict, estimate: Dict, num_keys: int = 100) -> float:
327334
return float(np.mean(errors)) if errors else float("nan")
328335

329336

337+
def replayed_google_cpu_values(trace: str, cutoff_us: int) -> np.ndarray:
338+
"""Sorted mean CPU usage of the task_usage rows the exporter replayed.
339+
340+
Rows with start_time <= cutoff_us and aggregation_type 0 (the series the
341+
p99 query reads).
342+
"""
343+
values = []
344+
with gzip.open(trace, "rt") as f:
345+
for line in f:
346+
c = line.rstrip("\n").split(",")
347+
if int(c[0]) <= cutoff_us and c[18] in ("", "0") and c[5] != "":
348+
values.append(float(c[5]))
349+
return np.sort(values)
350+
351+
352+
def rank_errors(sorted_values: np.ndarray, estimates: List[float], q: float):
353+
"""|F(estimate) - q| per estimate, F the empirical CDF of sorted_values."""
354+
ranks = np.searchsorted(sorted_values, estimates, side="right")
355+
return np.abs(ranks / len(sorted_values) - q)
356+
357+
330358
def monitor_output_path(experiment_dir: str) -> str:
331359
"""Written by remote_monitor.py when the run finishes."""
332360
return os.path.join(
333361
experiment_dir, "sketchdb", "remote_monitor_output", "monitor_output.json"
334362
)
335363

336364

337-
def summarize_experiment(experiment_dir: str) -> dict:
338-
"""Measured error and latency of one finished experiment."""
365+
def summarize_experiment(
366+
experiment_dir: str, quantile_values: Optional[np.ndarray] = None
367+
) -> dict:
368+
"""Measured error and latency of one finished experiment.
369+
370+
With quantile_values (sorted replayed values), the measured error is the
371+
p99 rank error instead of the ARE, and there is no all-keys column.
372+
"""
339373
sys.path.insert(0, EXPERIMENTS_DIR)
340374
from post_experiment.lib.results_loader import load_results
341375

@@ -347,13 +381,16 @@ def summarize_experiment(experiment_dir: str) -> dict:
347381
errors = []
348382
errors_all_keys = []
349383
for exact_rep, estimate_rep in zip(exact, estimate):
350-
if exact_rep.result and estimate_rep.result:
351-
errors.append(are_top_keys(exact_rep.result, estimate_rep.result))
352-
errors_all_keys.append(
353-
are_top_keys(
354-
exact_rep.result, estimate_rep.result, len(exact_rep.result)
355-
)
356-
)
384+
if not (exact_rep.result and estimate_rep.result):
385+
continue
386+
if quantile_values is not None:
387+
(value,) = estimate_rep.result.values()
388+
errors.append(float(rank_errors(quantile_values, [value], P99)[0]))
389+
continue
390+
errors.append(are_top_keys(exact_rep.result, estimate_rep.result))
391+
errors_all_keys.append(
392+
are_top_keys(exact_rep.result, estimate_rep.result, len(exact_rep.result))
393+
)
357394
latencies = {
358395
server: [r.latency for r in results[server][0].query_results if r.latency]
359396
for server in ("prometheus", "sketchdb")
@@ -367,7 +404,7 @@ def summarize_experiment(experiment_dir: str) -> dict:
367404
return {
368405
"measured_error": float(np.nanmedian(errors)) if errors else float("nan"),
369406
"measured_error_all_keys": (
370-
float(np.nanmedian(errors_all_keys)) if errors else float("nan")
407+
float(np.nanmedian(errors_all_keys)) if errors_all_keys else ""
371408
),
372409
"answered": f"{len(errors)}/{len(exact)}",
373410
"asap_latency_ms": 1000 * float(np.median(latencies["sketchdb"])),
@@ -376,9 +413,12 @@ def summarize_experiment(experiment_dir: str) -> dict:
376413
}
377414

378415

379-
def summarize(recommendations: List[dict], experiments_dir: str) -> List[dict]:
416+
def summarize(
417+
recommendations: List[dict], experiments_dir: str, google_trace: str
418+
) -> List[dict]:
380419
"""One row per finished recommended or default experiment."""
381420
rows = []
421+
quantile_values = None
382422
for row in recommendations:
383423
if row["family"] not in PLANNER_FAMILIES:
384424
continue
@@ -392,13 +432,21 @@ def summarize(recommendations: List[dict], experiments_dir: str) -> List[dict]:
392432
if not os.path.exists(monitor_output_path(experiment_dir)):
393433
print(f"skipped unfinished {name}", file=sys.stderr)
394434
continue
395-
summary = summarize_experiment(experiment_dir)
435+
if row["family"] == "kll" and quantile_values is None:
436+
quantile_values = replayed_google_cpu_values(
437+
google_trace, P99_REPLAY_CUTOFF_US
438+
)
439+
summary = summarize_experiment(
440+
experiment_dir, quantile_values if row["family"] == "kll" else None
441+
)
442+
recommended = variant == "recommended"
396443
rows.append(
397444
{
398445
"experiment": name,
399446
"family": row["family"],
400-
"config": row["config"] if variant == "recommended" else "default",
401-
"predicted_error": float(row["est_error"]),
447+
"config": row["config"] if recommended else "default",
448+
# Only the recommended config has a prediction.
449+
"predicted_error": float(row["est_error"]) if recommended else "",
402450
"target": float(row["target"]),
403451
**summary,
404452
"meets_target": summary["measured_error"] <= float(row["target"]),
@@ -418,6 +466,11 @@ def main() -> None:
418466
summ.add_argument("--recommendations", required=True)
419467
summ.add_argument("--experiments-dir", required=True)
420468
summ.add_argument("--output-csv")
469+
summ.add_argument(
470+
"--google-trace",
471+
default="/data/cluster_traces/google/part-00000-of-00500.csv.gz",
472+
help="task_usage part the p99 runs replayed, for their rank error",
473+
)
421474
args = parser.parse_args()
422475

423476
recommendations = load_recommendations(args.recommendations)
@@ -432,7 +485,7 @@ def main() -> None:
432485
print(f"skipped {note}")
433486
return
434487

435-
rows = summarize(recommendations, args.experiments_dir)
488+
rows = summarize(recommendations, args.experiments_dir, args.google_trace)
436489
if not rows:
437490
print("No finished experiments found")
438491
return

‎asap-tools/experiments/tests/test_recommended_sketch_configs.py‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
"""Tests for recommended_sketch_configs.py (config generation and error metric)."""
22

33
import os
4+
import tempfile
45
import unittest
6+
from unittest import mock
57

8+
import numpy as np
69
from hydra import compose, initialize_config_dir
710

811
from recommended_sketch_configs import recommended_sketch_configs as rsc
@@ -202,5 +205,51 @@ def test_missing_estimate_counts_as_zero(self):
202205
self.assertEqual(rsc.are_top_keys({"a": 4.0}, {}), 1.0)
203206

204207

208+
class RankErrorsTest(unittest.TestCase):
209+
def test_distance_of_estimate_rank_from_quantile(self):
210+
values = np.arange(1.0, 101.0)
211+
np.testing.assert_allclose(
212+
rsc.rank_errors(values, [99.0, 95.0, 1000.0], 0.99), [0.0, 0.04, 0.01]
213+
)
214+
215+
216+
class SummarizeTest(unittest.TestCase):
217+
# The p99 runs were once scored by relative value error against the
218+
# rank-error target, and default rows showed the recommended prediction.
219+
def summarize(self, family):
220+
rec = recommendation("google_2011", "cpu_p99", "instant", family, "k=200")
221+
with tempfile.TemporaryDirectory() as tmp:
222+
for variant in ("recommended", "default"):
223+
name = rsc.experiment_name("google_2011", "cpu_p99", "instant", variant)
224+
os.makedirs(os.path.dirname(rsc.monitor_output_path(f"{tmp}/{name}")))
225+
open(rsc.monitor_output_path(f"{tmp}/{name}"), "w").close()
226+
with mock.patch.object(
227+
rsc, "summarize_experiment", return_value={"measured_error": 0.002}
228+
) as summarize_experiment, mock.patch.object(
229+
rsc, "replayed_google_cpu_values", return_value="values"
230+
) as replayed:
231+
rows = rsc.summarize([rec], tmp, "trace.csv.gz")
232+
return rows, summarize_experiment, replayed
233+
234+
def test_kll_is_scored_on_replayed_values(self):
235+
rows, summarize_experiment, replayed = self.summarize("kll")
236+
replayed.assert_called_once_with("trace.csv.gz", rsc.P99_REPLAY_CUTOFF_US)
237+
for call in summarize_experiment.call_args_list:
238+
self.assertEqual(call.args[1], "values")
239+
240+
def test_cms_does_not_read_the_trace(self):
241+
_, summarize_experiment, replayed = self.summarize("cms")
242+
replayed.assert_not_called()
243+
for call in summarize_experiment.call_args_list:
244+
self.assertIsNone(call.args[1])
245+
246+
def test_only_recommended_row_has_a_prediction(self):
247+
rows, _, _ = self.summarize("kll")
248+
self.assertEqual(
249+
[(r["config"], r["predicted_error"]) for r in rows],
250+
[("k=200", 0.04), ("default", "")],
251+
)
252+
253+
205254
if __name__ == "__main__":
206255
unittest.main()

0 commit comments

Comments
 (0)