Skip to content

Commit 888dffd

Browse files
Add timing observation support for QWS
Signed-off-by: yoshifuminakamura <nakamura@riken.jp>
1 parent e96e7f4 commit 888dffd

14 files changed

Lines changed: 869 additions & 5 deletions

‎.github/workflows/result-server-tests.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ jobs:
7272
bash scripts/tests/test_bk_profiler.sh
7373
bash scripts/tests/test_bk_fetch_source.sh
7474
bash scripts/tests/test_bk_input_info.sh
75+
bash scripts/tests/test_bk_timing_observations.sh
76+
bash scripts/tests/test_qws_timing_artifact.sh
7577
bash scripts/tests/test_build_cache.sh
7678
bash scripts/tests/test_build_environment_snapshot.sh
7779
bash scripts/tests/test_ci_timing_context.sh

‎docs/cx/BENCHKIT_SPEC.md‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -532,7 +532,32 @@ A site-local path is location information, not a durable input identity.
532532
Large datasets and collaboration-derived inputs may still be staged on site-local shared storage, but Result provenance should prefer dataset identity, recipe, manifest, and digest over paths.
533533
Detailed local paths should not be exposed on the public surface unless they are necessary.
534534

535-
### 7.4.1 Public Reuse Packets
535+
### 7.4.1 Detailed Timing Observations
536+
537+
Benchkit は、アプリが出力する詳細 timer table や profiler から得た timing evidence を、任意の `timing_observations` として Result JSON に保持できることが望ましい。
538+
これは `fom_breakdown` とは別の観測レイヤである。
539+
`fom_breakdown.sections` / `fom_breakdown.overlaps` は、推定や reuse に使うと決めた section / overlap 投影を表す。
540+
一方、`timing_observations` は、その前段の詳細測定 artifact と要約を表し、未レビューの nested timer、inclusive timer、overlap window を additive section として扱わない。
541+
542+
`timing_observations` は最初の段階では任意項目であり、存在しない result を ingest failure として扱わない。
543+
同じ job から複数 result が出る場合は、各 observation に `result_exp` などの result scope を添えてよい。
544+
Result JSON には小さな summary と file reference を置き、巨大な profiler report や詳細 table は artifact として保持する。
545+
546+
将来、アプリ変更なしで詳細 timing を採取する場合は、profiler adapter が同じ `timing_observations` 経路へ summary と artifact reference を渡す。
547+
その observation を `fom_breakdown` へ昇格するかどうかは、app / profiler ごとの mapping review によって決める。
548+
549+
Benchkit should also preferably be able to retain detailed timer tables and profiler-derived timing evidence as optional `timing_observations` in Result JSON.
550+
This is an observation layer separate from `fom_breakdown`.
551+
`fom_breakdown.sections` and `fom_breakdown.overlaps` represent section or overlap projections intentionally selected for estimation or reuse, while `timing_observations` records detailed measurement artifacts and summaries before that projection is reviewed.
552+
553+
At the initial stage, `timing_observations` is optional, and results without it are not treated as ingest failures.
554+
When one job emits multiple results, each observation may carry a result scope such as `result_exp`.
555+
Result JSON should keep a compact summary and file reference; large profiler reports or full timer tables should remain artifacts.
556+
557+
For app-change-free collection, profiler adapters may feed summaries and artifact references into the same `timing_observations` path.
558+
Promoting an observation into `fom_breakdown` remains a separate mapping review for the app or profiler output.
559+
560+
### 7.4.2 Public Reuse Packets
536561

537562
Benchkit は、公開可能な Result JSON から public-only reuse packet を生成できることが望ましい。
538563
これは raw Result JSON の代替ではなく、AI agent や人間が再利用可否を判断しやすいように、公開できる evidence だけを Markdown と machine-readable manifest へ投影するものである。

‎docs/guides/developer-reference.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,4 +279,8 @@ Portal quality visibility currently lives in:
279279
- result detail quality rows
280280
- `/results/usage` current-state quality summaries
281281

282-
Treat missing `source_info`, `fom_breakdown`, or artifact references as internal improvement candidates, not upload-time or pull-request gates.
282+
Treat missing `source_info`, `fom_breakdown`, or artifact references as follow-up improvement candidates, not upload-time or pull-request gates.
283+
Detailed timing artifacts may be recorded through `timing_observations` before
284+
they are promoted to `fom_breakdown`; do not treat every detailed timer or
285+
profiler region as an additive estimation section without an app-specific
286+
mapping review.

‎programs/qws/README.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ depend on QWS-local variables or dummy section names.
1010
the section names and the section-package mapping locally. QWS production runs
1111
do not emit section timing metadata until those timings are measured by QWS
1212
itself.
13+
`parse_timing.sh` records the current QWS timing table and optional
14+
`QWS_TIMER_SCHEMA_*` markers as `results/qws_timing_<Exp>.json` artifacts when
15+
they are present, then registers them through Benchkit's common
16+
`timing_observations` manifest. These artifacts are measurement evidence only;
17+
they are not converted into `SECTION:` / `OVERLAP:` records until the QWS-owned
18+
timer IDs and overlap windows are reviewed as reusable section metadata.
1319

1420
Current reference sections are:
1521

@@ -48,3 +54,7 @@ Common Benchkit code should handle:
4854
- section and overlap composition
4955
- current/future system Estimate JSON construction
5056
- result-server artifact upload and portal rendering
57+
58+
Benchkit-local QWS glue may normalize QWS output into app artifacts, but should
59+
not decide that a nested or inclusive timer is an additive section without a
60+
QWS-side mapping.

‎programs/qws/parse_timing.sh‎

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
#!/bin/bash
2+
# parse_timing.sh - Normalize QWS timing output into a small JSON artifact.
3+
4+
set -euo pipefail
5+
6+
qws_extract_fom_from_log() {
7+
local log_file="$1"
8+
9+
awk '
10+
/etime for sovler/ || /etime for solver/ {
11+
count += 1
12+
if (count == 2) {
13+
printf "%.3f\n", $5 + 0
14+
exit
15+
}
16+
}
17+
' "$log_file"
18+
}
19+
20+
qws_extract_timing_table() {
21+
local log_file="$1"
22+
23+
awk '
24+
function is_number(value) {
25+
return value ~ /^[-+]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][-+]?[0-9]+)?$/
26+
}
27+
/^[[:space:]]*rank[[:space:]]+func_id[[:space:]]+calls[[:space:]]+total[(]s[)][[:space:]]+average[(]s[)]/ {
28+
in_table = 1
29+
next
30+
}
31+
in_table && /^[[:space:]]*end[[:space:]]*$/ {
32+
in_table = 0
33+
next
34+
}
35+
in_table && NF >= 5 && $1 ~ /^[0-9]+$/ && $3 ~ /^[0-9]+$/ && is_number($4) && is_number($5) {
36+
printf "%s\t%s\t%s\t%.12g\t%.12g\n", $1, $2, $3, $4 + 0, $5 + 0
37+
}
38+
' "$log_file"
39+
}
40+
41+
qws_timing_schema_json() {
42+
local log_file="$1"
43+
44+
awk '/^QWS_TIMER_SCHEMA/ { print }' "$log_file" | jq -Rsc '
45+
def kv_token:
46+
split(":") as $parts
47+
| select(($parts | length) >= 2)
48+
| {key: $parts[0], value: ($parts[1:] | join(":"))};
49+
[
50+
split("\n")[]
51+
| select(length > 0)
52+
| capture("^(?<record_type>QWS_TIMER_SCHEMA(?:_[A-Z_]+)?)\\s*(?<rest>.*)$")?
53+
| {
54+
record_type: .record_type,
55+
fields: (
56+
.rest
57+
| split(" ")
58+
| map(select(length > 0) | kv_token)
59+
| from_entries
60+
)
61+
}
62+
]
63+
'
64+
}
65+
66+
qws_timing_table_json() {
67+
local log_file="$1"
68+
69+
qws_extract_timing_table "$log_file" | jq -Rnc '
70+
[
71+
inputs
72+
| split("\t")
73+
| select(length == 5)
74+
| {
75+
rank: (.[0] | tonumber),
76+
id: .[1],
77+
calls: (.[2] | tonumber),
78+
total_seconds: (.[3] | tonumber),
79+
average_seconds: (.[4] | tonumber)
80+
}
81+
]
82+
'
83+
}
84+
85+
qws_emit_timing_artifact_json() {
86+
local log_file="$1"
87+
local exp="$2"
88+
local fom="${3:-}"
89+
local schema_json
90+
local timers_json
91+
local source_log_name
92+
93+
if [[ ! -f "$log_file" ]]; then
94+
echo "QWS timing log was not found: ${log_file}" >&2
95+
return 1
96+
fi
97+
98+
source_log_name=$(basename "$log_file")
99+
schema_json=$(qws_timing_schema_json "$log_file")
100+
timers_json=$(qws_timing_table_json "$log_file")
101+
102+
jq -n \
103+
--arg exp "$exp" \
104+
--arg source_log "$source_log_name" \
105+
--arg fom "$fom" \
106+
--argjson schema "$schema_json" \
107+
--argjson timers "$timers_json" '
108+
def number_text:
109+
test("^[-+]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][-+]?[0-9]+)?$");
110+
{
111+
schema_version: 1,
112+
producer: "qws",
113+
kind: "qws_timing_observation",
114+
exp: $exp,
115+
source_log: $source_log,
116+
schema: $schema,
117+
timers: $timers,
118+
summary: {
119+
timer_count: ($timers | length),
120+
schema_record_count: ($schema | length),
121+
has_timing_table: (($timers | length) > 0),
122+
has_overlap_probe_schema: (
123+
any($schema[]?; .record_type == "QWS_TIMER_SCHEMA" and .fields.target == "overlap_probe")
124+
)
125+
}
126+
}
127+
+ (if ($fom | number_text) then {fom_seconds: ($fom | tonumber)} else {} end)
128+
'
129+
}
130+
131+
qws_write_timing_artifact() {
132+
local log_file="$1"
133+
local exp="$2"
134+
local output_file="$3"
135+
local fom="${4:-}"
136+
local tmp_file
137+
138+
if [[ ! -f "$log_file" ]]; then
139+
echo "QWS timing artifact skipped; log was not found: ${log_file}" >&2
140+
return 0
141+
fi
142+
if ! command -v jq >/dev/null 2>&1; then
143+
echo "QWS timing artifact skipped; jq is not available" >&2
144+
return 0
145+
fi
146+
if ! grep -Eq '^[[:space:]]*rank[[:space:]]+func_id[[:space:]]+calls[[:space:]]+total[(]s[)][[:space:]]+average[(]s[)]|^QWS_TIMER_SCHEMA' "$log_file"; then
147+
return 0
148+
fi
149+
150+
mkdir -p "$(dirname "$output_file")"
151+
tmp_file=$(mktemp "${output_file}.tmp.XXXXXX")
152+
if qws_emit_timing_artifact_json "$log_file" "$exp" "$fom" > "$tmp_file"; then
153+
mv "$tmp_file" "$output_file"
154+
else
155+
rm -f "$tmp_file"
156+
echo "QWS timing artifact skipped; failed to parse ${log_file}" >&2
157+
fi
158+
}
159+
160+
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
161+
if [[ $# -lt 2 || $# -gt 4 ]]; then
162+
echo "Usage: $0 <qws-log-file> <exp> [output-json] [fom-seconds]" >&2
163+
exit 2
164+
fi
165+
166+
if [[ $# -ge 3 ]]; then
167+
qws_write_timing_artifact "$1" "$2" "$3" "${4:-}"
168+
else
169+
qws_emit_timing_artifact_json "$1" "$2"
170+
fi
171+
fi

‎programs/qws/run.sh‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ qws_case1_args=(32 6 4 3 1 1 1 2 -1 -1 6 50)
1515
qws_case7_args=(32 6 4 3 1 2 2 2 -1 -1 6 50)
1616

1717
source "${PWD}/scripts/bk_functions.sh"
18+
source "${PWD}/programs/qws/parse_timing.sh"
1819
qws_profiler_tool=$(bk_resolve_profiler_tool fapp QWS_PROFILER_TOOL)
1920
qws_profiler_level=$(bk_resolve_profiler_level detailed QWS_PROFILER_LEVEL)
2021
# QWS synthetic estimation metadata is disabled for production runs. Keep
@@ -23,6 +24,7 @@ qws_profiler_level=$(bk_resolve_profiler_level detailed QWS_PROFILER_LEVEL)
2324
# source "${PWD}/programs/qws/estimate.sh"
2425

2526
mkdir -p results && : > results/result
27+
bk_reset_timing_observations
2628

2729
record_qws_runtime_parameter_inputs() {
2830
bk_reset_input_info
@@ -54,9 +56,21 @@ print_results() {
5456
local outfile=$1
5557
local exp=$2
5658
local np=$3
59+
local artifact_file="../results/qws_timing_${exp}.json"
60+
local artifact_path="results/qws_timing_${exp}.json"
5761
./check.sh "$outfile" "data/$exp"
5862
local fom
59-
fom=$(grep etime "$outfile" | awk 'NR==2{printf("%5.3f\n",$5)}')
63+
fom=$(qws_extract_fom_from_log "$outfile")
64+
qws_write_timing_artifact "$outfile" "$exp" "$artifact_file" "$fom"
65+
if [[ -f "$artifact_file" ]]; then
66+
BK_TIMING_OBSERVATIONS_FILE="../results/timing_observations.json" \
67+
BK_TIMING_OBSERVATION_ITEMS_FILE="../results/.timing_observation_items.jsonl" \
68+
bk_record_timing_observation \
69+
--artifact "$artifact_path" \
70+
--artifact-file "$artifact_file" \
71+
--result-exp "$exp" \
72+
--producer qws
73+
fi
6074
bk_emit_result --fom "$fom" --fom-unit s --fom-version DDSolverJacobi --exp "$exp" --nodes "$nodes" --numproc-node "$np" --nthreads "$nthreads"
6175
# Disabled: this emitted synthetic section timings and dummy estimation
6276
# artifacts. Re-enable only after QWS provides real app-side timings.

‎result_server/templates/result_detail.html‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,10 @@ <h2>Scalar Metrics</h2>
208208
</div>
209209
{% endif %}
210210

211+
{% if timing_observation_rows %}
212+
{{ render_titled_key_value_table("Timing Observations", timing_observation_rows, "meta-table") }}
213+
{% endif %}
214+
211215
{% if build_rows %}
212216
{{ render_titled_key_value_table("Build Information", build_rows, "build-table") }}
213217
{% endif %}

‎result_server/tests/test_result_detail_template.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,28 @@ def app():
119119
"container image SHA-256 matched",
120120
],
121121
},
122+
"timing_observations": {
123+
"schema_version": 1,
124+
"observations": [
125+
{
126+
"id": "qws-case0-timers",
127+
"kind": "detailed-timing",
128+
"producer": "qws",
129+
"format": "qws_timing_observation/v1",
130+
"result_exp": "CASE0",
131+
"artifact": {
132+
"type": "file_reference",
133+
"path": "results/qws_timing_CASE0.json",
134+
},
135+
"summary": {
136+
"timer_count": 14,
137+
"schema_record_count": 3,
138+
"has_overlap_probe_schema": True,
139+
},
140+
"note": "not projected to fom_breakdown",
141+
}
142+
],
143+
},
122144
}
123145

124146
FULL_QUALITY = {
@@ -182,6 +204,12 @@ def test_meta_info_section(self, app):
182204
assert "build inputs hash matched" in html
183205
assert "rccs-cloud" in html
184206
assert "slurm" in html
207+
assert "Timing Observations" in html
208+
assert "qws-case0-timers" in html
209+
assert "producer=qws" in html
210+
assert "artifact=results/qws_timing_CASE0.json" in html
211+
assert "timers=14" in html
212+
assert "overlap probe schema=yes" in html
185213
assert "Build Tools" in html
186214
assert "gcc (GCC) 11.5.0" in html
187215
assert "Back to Results" in html
@@ -205,6 +233,8 @@ def test_public_surface_meta_omits_operator_fields(self, app):
205233
assert "Cached Binary Created At" not in html
206234
assert "Allocation Project ID" not in html
207235
assert "Runner" not in html
236+
assert "Timing Observations" not in html
237+
assert "qws_timing_CASE0" not in html
208238
assert "rccs-cloud" not in html
209239
assert "gh200-runner" not in html
210240

0 commit comments

Comments
 (0)