Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions asap-tools/experiments/CONFIG_PARAMETERS_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,10 @@ table with `sketch_bench_e2e/sketch_bench_e2e_configs.py`.
- **Validation**: Must be valid PromQL syntax
- **SCHEMA ISSUE**: Inconsistent time ranges ([1m] vs [10m]) across configs

#### `experiment_params.query_groups[].sql_file` (dict, required; `experiment_run_clickhouse.py`)
- **Description**: Mode name -> SQL file (`;`-separated statements) the mode runs for this group, in place of `queries`. Every mode but `sketchdb` needs an entry in every group. `sketchdb` runs its own entry (the SQL ASAPQuery receives, and the planner plans) and skips groups without one, but only trailing groups: results pair across modes by global query index, so `sketchdb`'s queries must be the first of baseline's
- **Example**: `{baseline: /path/to/q_baseline.sql, sketchdb: /path/to/q_asap.sql}`

#### `experiment_params.query_groups[].repetition_delay` (int, required)
- **Description**: Delay in seconds between query repetitions
- **Range**: 1-3600 seconds
Expand Down
12 changes: 8 additions & 4 deletions asap-tools/experiments/HYDRA_CONFIG_USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ cat > config/experiment_type/clickhouse.yaml << 'EOF'
# providers.cloudlab.hostname_suffix=myexp.cloudlab.us \
# experiment_params.dataset.name=clickbench \
# experiment_params.dataset.local_data_file=/path/to/hits.json \
# 'experiment_params.query_groups[0].sql_file=/path/to/queries.sql'
# 'experiment_params.query_groups[0].sql_file={baseline:/path/to/queries.sql}'

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The template body below (~line 177) still shows sql_file: ??? # path to SQL file as a plain string. _mode_sql_files calls sql_file.get(mode) on it and fails with AttributeError: 'str' object has no attribute 'get' instead of a clear error. The checked-in clickhouse.yaml and quantile_demo.yaml also use a string sql_file. Suggest updating the template to the dict form, and either rejecting a string with a clear message or accepting it as {baseline: path}.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bbcdac9: a plain-string sql_file now fails with a clear error (test: test_plain_string_sql_file_is_refused_clearly), and the docs template uses the dict form. No checked-in YAML sets sql_file, and there is no clickhouse.yaml or quantile_demo.yaml in the repo.


experiment_params:
monitoring:
Expand All @@ -174,7 +174,8 @@ experiment_params:
max_rows: 0 # 0 = load all rows
query_groups:
- name: default_queries
sql_file: ??? # path to SQL file generated by benchmark/generate_queries.py
sql_file: # mode -> SQL file generated by benchmark/generate_queries.py
baseline: ??? # a sketchdb mode needs its own entry (may be the same file)
repetition_delay: 0
client_options:
starting_delay: 0
Expand All @@ -199,9 +200,12 @@ python experiment_run_clickhouse.py \
controller.punting=false \
experiment_params.dataset.name=clickbench \
experiment_params.dataset.local_data_file=/path/to/hits.json \
'experiment_params.query_groups[0].sql_file=/path/to/queries.sql'
'experiment_params.query_groups[0].sql_file={baseline:/path/to/queries.sql}'

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This passes only {baseline:...}. Under the new rule, any sketchdb mode in experiment_params.experiment (as in the repo's clickhouse.yaml) needs its own entry, so this command now fails with 'No query group has a sql_file entry for mode sketchdb'. Before, sketchdb fell back to baseline's SQL.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That example runs baseline only, so it still works. bbcdac9 adds a note below it: with a sketchdb mode, give sketchdb its own entry, e.g. sql_file={baseline:...,sketchdb:...}.

```

With a `sketchdb` mode in `experiment_params.experiment`, give it its own
entry, e.g. `sql_file={baseline:/path/to/queries.sql,sketchdb:/path/to/queries.sql}`.

Optional overrides:
```bash
# Skip data loading step, keep ClickHouse running after experiment
Expand Down Expand Up @@ -239,7 +243,7 @@ python experiment_run_clickhouse.py \
experiment_type=clickhouse \
experiment.name=frozen_plan_eval \
controller.prebuilt_output_dir=/path/to/frozen-sql-plan \
'experiment_params.query_groups[0].sql_file=/path/to/queries.sql'
'experiment_params.query_groups[0].sql_file={baseline:/path/to/queries.sql,sketchdb:/path/to/queries.sql}'
```

### experiment_run_e2e.py (Full E2E Pipeline)
Expand Down
8 changes: 4 additions & 4 deletions asap-tools/experiments/experiment_run_clickhouse.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
providers.cloudlab.username=myuser \\
providers.cloudlab.hostname_suffix=myexp.cloudlab.us \\
experiment_params.dataset.local_data_file=/path/to/hits.json \\
'experiment_params.query_groups[0].sql_file=/path/to/clickbench.sql'
'experiment_params.query_groups[0].sql_file={baseline:/path/to/clickbench.sql}'

Baseline + sketchdb:
python experiment_run_clickhouse.py \\
Expand All @@ -83,7 +83,7 @@
providers.cloudlab.username=myuser \\
providers.cloudlab.hostname_suffix=myexp.cloudlab.us \\
experiment_params.dataset.local_data_file=/path/to/hits.json \\
'experiment_params.query_groups[0].sql_file=/path/to/clickbench.sql'
'experiment_params.query_groups[0].sql_file={baseline:/path/to/clickbench.sql,sketchdb:/path/to/clickbench.sql}'

Sketchdb mode runs when experiment_params.experiment contains an entry
with mode: sketchdb (mirrors the Prometheus experiment config structure).
Expand Down Expand Up @@ -246,8 +246,8 @@ def _expand_groups(groups):
sql_file = group.get("sql_file")
if not sql_file or "queries" in group:
continue
# sql_file is per-mode (issue #491 precompute arms); label with
# baseline's text, matching the query text sketchdb also plans against.
# sql_file is per-mode; label with baseline's text, since every
# mode's query i answers baseline's query i.
sql_file = sql_file.get(constants.BASELINE_EXPERIMENT_NAME)
if not sql_file:
continue
Expand Down
130 changes: 82 additions & 48 deletions asap-tools/experiments/experiment_utils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,27 +153,58 @@ def _is_clickhouse_experiment(experiment_params: DictConfig) -> bool:
return "dataset" in experiment_params


def _resolve_sql_file(sql_file: Any, mode: str, idx: int, group_name: str) -> str:
"""Resolve the sql_file path for one query group and mode.

``sql_file`` is a dict keyed by mode name (e.g.
baseline/baseline_sketch/baseline_mv/...). sketchdb always resolves
through baseline's entry — ASAP's SQL-mode query engine must see the
exact same SQL text as baseline to pattern-match it against available
sketches, so it never gets its own dict entry.
def _mode_sql_files(query_groups: Any, mode: str) -> List[Tuple[int, Any, str]]:
"""(index, group, sql_file path) of each query group `mode` runs.

``sql_file`` is a dict keyed by mode name (e.g. baseline/baseline_mv/
sketchdb). Every mode but sketchdb needs an entry in every group.
sketchdb skips the groups without one (e.g. queries ASAPQuery has no SQL
for), but only trailing groups: results are matched across modes by
global query index, so sketchdb's queries must be a prefix of baseline's,
and each file it runs must hold as many statements as baseline's.
"""
lookup_mode = (
constants.BASELINE_EXPERIMENT_NAME
if mode == constants.SKETCHDB_EXPERIMENT_NAME
else mode
)
path = sql_file.get(lookup_mode)
if not path:
raise ValueError(
f"Query group {idx} ({group_name!r}) missing sql_file entry for "
f"mode {lookup_mode!r}"
)
return path
runs = []
for idx, group in enumerate(query_groups):
name = group.get("name", str(idx))
sql_file = group.get("sql_file")
if not sql_file:
raise ValueError(f"Query group {idx} ({name!r}) missing 'sql_file'")
if not hasattr(sql_file, "get"):
raise ValueError(
f"Query group {idx} ({name!r}) sql_file must map mode name to "
f"SQL file, e.g. {{baseline: {sql_file}}}; got {sql_file!r}"
)
path = sql_file.get(mode)
if path:
if len(runs) != idx:
raise ValueError(
f"Query group {idx} ({name!r}) has a sql_file for mode "
f"{mode!r} after a group without one; a mode may skip "
"only trailing groups, so its query indices match baseline's"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_mode_sql_files checks that sketchdb's groups are a prefix of baseline's, but not that each sketchdb sql_file has as many statements as baseline's file for the same group. Results are paired by global query index, so a count mismatch shifts every later index.

Example: group 0 has baseline {q0,q1,q2} and sketchdb {q0,q2}, and group 1 is the same for both. Sketchdb's query 1 is then scored against baseline's q1, and all of its group-1 queries against the wrong baseline queries. calculate_fidelity truncates to len(estimate_results) and prints scores with no error. Suggest checking the statement counts per group here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bbcdac9: _mode_sql_files now fails when a mode's SQL file holds a different number of statements than baseline's for the same group (test: test_statement_count_mismatch_is_refused).

)
baseline_path = sql_file.get(constants.BASELINE_EXPERIMENT_NAME)
if (
mode != constants.BASELINE_EXPERIMENT_NAME
and baseline_path
and os.path.exists(path)
and os.path.exists(baseline_path)
and len(_load_sql_queries(path))
!= len(_load_sql_queries(baseline_path))
):
raise ValueError(
f"Query group {idx} ({name!r}): mode {mode!r}'s {path!r} and "
f"baseline's {baseline_path!r} hold different numbers of "
"statements, so their query indices would not match"
)
runs.append((idx, group, path))
elif mode != constants.SKETCHDB_EXPERIMENT_NAME:
raise ValueError(
f"Query group {idx} ({name!r}) missing sql_file entry for "
f"mode {mode!r}"
)
if not runs:
raise ValueError(f"No query group has a sql_file entry for mode {mode!r}")
return runs


def _validate_clickhouse_experiment_config(experiment_params: DictConfig) -> None:
Expand Down Expand Up @@ -239,7 +270,6 @@ def _validate_clickhouse_experiment_config(experiment_params: DictConfig) -> Non
"At least one mode must be defined in experiment_params.experiment."
)
experiment_modes = {m["mode"] for m in experiment_params.experiment}
experiment_modes.discard(constants.SKETCHDB_EXPERIMENT_NAME)

for i, group in enumerate(experiment_params.query_groups):
sql_file = group.get("sql_file")
Expand All @@ -248,9 +278,8 @@ def _validate_clickhouse_experiment_config(experiment_params: DictConfig) -> Non
f"Query group {i} missing 'sql_file'. "
"Generate SQL files with benchmark/generate_queries.py first."
)
group_name = group.get("name", str(i))
for mode in experiment_modes:
path = _resolve_sql_file(sql_file, mode, i, group_name)
for mode in sorted(experiment_modes):
for i, _, path in _mode_sql_files(experiment_params.query_groups, mode):
if not os.path.exists(path):
raise ValueError(
f"Query group {i} sql_file for mode {mode!r} "
Expand Down Expand Up @@ -700,6 +729,22 @@ def _is_planned(query, planned):
return re.fullmatch(r"[\s()+\-*/0-9.]*", query) is not None


# A quoted time literal the query client renders, e.g.
# '{{ end_datetime[:17] }}{{ "%02d" | format(end_time % 60 // 60 * 60) }}'.
_SQL_TEMPLATED_TIME_LITERAL = re.compile(r"'\{\{[^']*\}\}[^']*'")


def _comparable_sql(sql):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_comparable_sql rewrites every quoted templated literal to NOW(), so queries that differ only in their time literals compare equal. Examples: SQL floored to different intervals ('{{ end_datetime[:17] }}...' vs '{{ end_datetime[:14] }}...:00'), or BETWEEN '{{start}}' AND '{{end}}' against a plan that uses NOW() for both bounds.

copy_prebuilt_controller_output can then accept a saved plan for different windows, and the mismatch shows up only at query time.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Known limitation, kept as is. The saved plan holds NOW(), so the flooring interval can't be recovered from its text. The ponytail: comment on _comparable_sql notes this. The check catches a wrong plan; the engine matches queries to the plan structurally at query time.

"""`sql` without comment lines or a trailing ';', with each templated time
literal as NOW(): a saved plan holds the workload's SQL on NOW(), the
client config the same SQL on the literal the client fills in."""
# ponytail: textual; any templated literal reads as NOW(). The engine
# matches queries to the plan structurally, this only catches a wrong plan.
lines = [line for line in sql.splitlines() if not line.lstrip().startswith("--")]
sql = _SQL_TEMPLATED_TIME_LITERAL.sub("NOW()", "\n".join(lines))
return sql.strip().rstrip(";").strip()


def _validate_sql_inference_config(inference_config):
"""Raise a clear error unless `inference_config` has the SQL schema shape."""
if not isinstance(inference_config, dict):
Expand Down Expand Up @@ -797,11 +842,15 @@ def copy_prebuilt_controller_output(
"controller.prebuilt_output_dir inference_config.yaml has invalid "
"queries"
)
planned = {q["query"] for q in inference_config["queries"]}
comparable = _comparable_sql if query_language == QUERY_LANGUAGE_SQL else str
planned = {comparable(q["query"]) for q in inference_config["queries"]}
with open(controller_input_config) as f:
groups = yaml.safe_load(f)["query_groups"]
unplanned = [
q for g in groups for q in g["queries"] if not _is_planned(q, planned)
q
for g in groups
for q in g["queries"]
if not _is_planned(comparable(q), planned)
]
if unplanned:
raise ValueError(
Expand Down Expand Up @@ -1088,18 +1137,10 @@ def generate_clickhouse_client_configs(
modes = list(mode_server_urls.keys())
for mode, url in mode_server_urls.items():
# Each mode may target a different sql_file (e.g. baseline_mv queries a
# materialized view, baseline_sketch swaps in a sketch function) — see
# _resolve_sql_file for the sketchdb-falls-back-to-baseline rule.
# materialized view) and sketchdb may skip trailing groups — see
# _mode_sql_files.
built_groups = []
for idx, group in enumerate(query_groups_list):
sql_file = group.get("sql_file")
if not sql_file:
name = group.get("name", str(idx))
raise ValueError(f"Query group {idx!r} ({name!r}) missing 'sql_file'")
sql_file = _resolve_sql_file(
sql_file, mode, idx, group.get("name", str(idx))
)

for idx, group, sql_file in _mode_sql_files(query_groups_list, mode):
queries = _load_sql_queries(sql_file)
if not queries:
raise ValueError(f"No SQL statements found in {sql_file!r}")
Expand Down Expand Up @@ -1214,7 +1255,10 @@ def generate_sql_planner_input(
groups_list = list(query_groups)

planner_query_groups = []
for idx, group in enumerate(groups_list):
# The planner plans the SQL text the engine receives, i.e. sketchdb's.
for idx, group, sql_file in _mode_sql_files(
groups_list, constants.SKETCHDB_EXPERIMENT_NAME
):
ctrl_opts = dict(group.get("controller_options") or {})
# Dropping an unknown key here would hide it from the planner's strict parse.
unknown = set(ctrl_opts) - CONTROLLER_OPTION_KEYS
Expand All @@ -1224,16 +1268,6 @@ def generate_sql_planner_input(
f"{sorted(unknown)}; allowed: {sorted(CONTROLLER_OPTION_KEYS)}"
)

sql_file = group.get("sql_file")
if not sql_file:
raise ValueError(f"query_groups[{idx}] missing 'sql_file'")
# sketchdb always plans against baseline's SQL text (see _resolve_sql_file).
sql_file = _resolve_sql_file(
sql_file,
constants.SKETCHDB_EXPERIMENT_NAME,
idx,
group.get("name", str(idx)),
)
queries = _load_sql_queries(sql_file)
if not queries:
raise ValueError(f"No SQL statements found in {sql_file!r}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,15 @@ def get_timeseries_similarity_scores(
similarity_functions,
verbose: bool,
):
# The estimate mode may run only the first queries (sketchdb skips
# trailing query groups it has no SQL for); a gap among them still fails
# on the lookup below.
if len(estimate_results) < len(queries):
print(
f"Scoring the first {len(estimate_results)} of {len(queries)} "
"queries: the estimate mode ran no others"
)
queries = queries[: len(estimate_results)]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This truncation assumes the estimate mode ran a prefix of baseline's queries, but nothing ties the scored queries to the groups the mode actually ran; pairing goes only by count. A misconfigured sql_file or a statement-count mismatch is scored against the first N baseline labels, and the only sign is the 'Scoring the first N of M' message.

Both client configs keep 'id': idx, so pairing by group id plus per-group query index would remove the need for both the prefix-only rule and this truncation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declined for now. With the prefix rule and the per-group statement-count check (bbcdac9), the estimate's queries are exactly baseline's first N, so truncating is safe. A gap among them still fails on the lookup. Pairing by (group id, index) would change the client output and every analysis script that indexes by position.

similarity_scores = {
f.__name__: {q: 0 for q in queries} for f in similarity_functions
}
Expand Down
26 changes: 26 additions & 0 deletions asap-tools/experiments/tests/test_calculate_fidelity.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,32 @@ def test_sql_exact_matches_prometheus_json_estimate(self):

self.assertAlmostEqual(scores["mape"][GROUPED_QUERY], 20.0)

def test_only_the_estimate_modes_queries_are_scored(self):
# sketchdb may skip trailing query groups; baseline ran them all.
exact = _sql_results("clickhouse", ["api\ta:1\t10\n"])
estimate = _sql_results("asap", ["api\ta:1\t11\n"])

scores = get_timeseries_similarity_scores(
{0: exact, 1: exact},
{0: estimate},
[GROUPED_QUERY, "SELECT 'skipped'"],
[mape],
verbose=False,
)

self.assertEqual(scores["mape"], {GROUPED_QUERY: 10.0})

def test_a_query_missing_among_the_estimate_modes_fails(self):
exact = _sql_results("clickhouse", ["api\ta:1\t10\n"])
with self.assertRaises(KeyError):
get_timeseries_similarity_scores(
{0: exact, 1: exact},
{1: exact},
[GROUPED_QUERY, GROUPED_QUERY + " "],
[mape],
verbose=False,
)


if __name__ == "__main__":
unittest.main()
Loading
Loading