3v324v23 commited on
Commit
13d819f
·
1 Parent(s): 585fe63

fix score trend

Browse files
Files changed (3) hide show
  1. README.md +6 -2
  2. scripts/build_data.py +122 -11
  3. server.py +38 -0
README.md CHANGED
@@ -47,5 +47,9 @@ qualification and full-evaluation state for the original-model baseline and each
47
  miner. The public dashboard shows only hotkeys and stage states; prompts,
48
  answers, and rejection reasons are excluded.
49
 
50
- After a validator restart, the dashboard uses the newest run containing valid
51
- miner scores. A newer empty run no longer hides an older completed evaluation.
 
 
 
 
 
47
  miner. The public dashboard shows only hotkeys and stage states; prompts,
48
  answers, and rejection reasons are excluded.
49
 
50
+ The dashboard only reads W&B runs whose run state is `running`; stopped, crashed,
51
+ failed, or finished runs are ignored.
52
+
53
+ Among running validator runs, the dashboard uses the newest run containing valid
54
+ miner scores. A newer empty running run no longer hides an older scored running
55
+ run.
scripts/build_data.py CHANGED
@@ -129,11 +129,110 @@ def extract_progress(summary: dict) -> dict | None:
129
  }
130
 
131
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  def progress_for_run(run, summary: dict) -> dict | None:
133
  progress = extract_progress(summary)
134
- if progress is not None and progress["status"] == "running":
135
- if getattr(run, "state", None) in {"crashed", "failed", "killed"}:
136
- progress["status"] = "failed"
 
 
 
 
 
 
 
 
 
 
137
  return progress
138
 
139
 
@@ -147,32 +246,36 @@ def extract_history(run) -> dict:
147
  epoch = _as_float(row.get("epoch"))
148
  if epoch is None:
149
  continue
150
- bucket = rows_by_epoch.setdefault(epoch, {})
151
  for key, value in row.items():
152
  hk = _miner_hotkey_from_score_key(key)
153
  if hk is not None:
154
- bucket[("miner", hk, "score")] = _as_float(value)
155
  miner_keys.add(hk)
156
  elif key.startswith(_SCORE_PREFIX) and key.endswith(_LEN_SUFFIX):
157
  hk2 = key[len(_SCORE_PREFIX) : -len(_LEN_SUFFIX)]
158
  if hk2:
159
- bucket[("miner", hk2, "completion_len")] = _as_float(value)
160
  miner_keys.add(hk2)
161
  elif key.startswith(_SCORE_PREFIX) and key.endswith(_CORRECTNESS_SUFFIX):
162
  hk2 = key[len(_SCORE_PREFIX) : -len(_CORRECTNESS_SUFFIX)]
163
  if hk2:
164
- bucket[("miner", hk2, "correctness_score")] = _as_float(value)
165
  miner_keys.add(hk2)
166
  elif key == _ORIG_SCORE:
167
- bucket[("orig", "score")] = _as_float(value)
168
- bucket.setdefault(("orig", "correctness_score"), _score_to_signed_correctness(value))
 
 
169
  saw_orig_score = True
170
  elif key == _ORIG_CORRECTNESS:
171
- bucket[("orig", "correctness_score")] = _as_float(value)
172
  saw_orig_score = True
173
  elif key == _ORIG_LEN:
174
- bucket[("orig", "completion_len")] = _as_float(value)
175
  saw_orig_len = True
 
 
176
 
177
  epochs = sorted(rows_by_epoch)
178
  miners: dict[str, dict] = {}
@@ -300,6 +403,10 @@ def wandb_path(project: str, entity: str | None) -> str:
300
  return f"{entity}/{project}" if entity else project
301
 
302
 
 
 
 
 
303
  def build(project: str, entity: str | None, api=None) -> dict:
304
  if api is None:
305
  import wandb
@@ -311,6 +418,8 @@ def build(project: str, entity: str | None, api=None) -> dict:
311
  fallbacks: dict[str, dict] = {}
312
  selected: dict[str, dict] = {}
313
  for run in api.runs(path, order="-created_at"):
 
 
314
  hotkey = dict(run.config).get("validator_hotkey")
315
  if not isinstance(hotkey, str) or not hotkey or hotkey in selected:
316
  continue
@@ -350,6 +459,8 @@ def build_progress(
350
  seen = set()
351
  runs = api.runs(path, order="-created_at")
352
  for run in islice(runs, max(1, run_limit)):
 
 
353
  hotkey = dict(run.config).get("validator_hotkey")
354
  if not isinstance(hotkey, str) or not hotkey or hotkey in seen:
355
  continue
 
129
  }
130
 
131
 
132
+ def _parse_iso(value) -> datetime | None:
133
+ if not isinstance(value, str):
134
+ return None
135
+ try:
136
+ return datetime.fromisoformat(value)
137
+ except ValueError:
138
+ return None
139
+
140
+
141
+ def _snapshot_for_epoch(raw, epoch: float) -> dict | None:
142
+ if isinstance(raw, str):
143
+ try:
144
+ raw = json.loads(raw)
145
+ except json.JSONDecodeError:
146
+ return None
147
+ if not isinstance(raw, dict):
148
+ return None
149
+ row_epoch = _as_float(raw.get("epoch"))
150
+ if row_epoch is None or row_epoch != epoch:
151
+ return None
152
+ return raw
153
+
154
+
155
+ def extract_stage_timings(run, epoch) -> dict[str, dict]:
156
+ """Replay a run's progress history to time how long each stage's baseline
157
+ evaluation actually took for the given epoch.
158
+
159
+ The live progress snapshot (summary) only ever holds the *latest* stage
160
+ state, so a fast baseline/miner pass can flip from "evaluating" to
161
+ "finished" between two dashboard polls without ever being observed live.
162
+ Scanning history lets the dashboard show concrete proof ("base done in
163
+ 4.2s") instead of silently losing that transition.
164
+ """
165
+ epoch_value = _as_float(epoch)
166
+ if epoch_value is None:
167
+ return {}
168
+
169
+ active_stage: dict[str, datetime] = {}
170
+ active_baseline: dict[str, datetime] = {}
171
+ timings: dict[str, dict] = {}
172
+
173
+ try:
174
+ rows = run.scan_history()
175
+ except Exception:
176
+ return {}
177
+
178
+ for row in rows:
179
+ snapshot = _snapshot_for_epoch(row.get("progress/snapshot"), epoch_value)
180
+ if snapshot is None:
181
+ continue
182
+ ts = _parse_iso(snapshot.get("updated_at"))
183
+ raw_stages = snapshot.get("stages")
184
+ if not isinstance(raw_stages, dict):
185
+ continue
186
+ for stage_name in _PROGRESS_STAGES:
187
+ stage = raw_stages.get(stage_name)
188
+ if not isinstance(stage, dict):
189
+ continue
190
+ status = stage.get("status")
191
+ if status == "evaluating" and stage_name not in active_stage:
192
+ active_stage[stage_name] = ts
193
+ elif (
194
+ status in {"completed", "skipped", "failed"}
195
+ and stage_name in active_stage
196
+ and "duration_seconds" not in timings.get(stage_name, {})
197
+ ):
198
+ start = active_stage[stage_name]
199
+ if start is not None and ts is not None:
200
+ timings.setdefault(stage_name, {})["duration_seconds"] = (
201
+ ts - start
202
+ ).total_seconds()
203
+
204
+ baseline = stage.get("baseline")
205
+ if baseline == "evaluating" and stage_name not in active_baseline:
206
+ active_baseline[stage_name] = ts
207
+ elif (
208
+ baseline in {"finished", "skipped", "failed"}
209
+ and stage_name in active_baseline
210
+ and "baseline_seconds" not in timings.get(stage_name, {})
211
+ ):
212
+ start = active_baseline[stage_name]
213
+ if start is not None and ts is not None:
214
+ timings.setdefault(stage_name, {})["baseline_seconds"] = (
215
+ ts - start
216
+ ).total_seconds()
217
+
218
+ return timings
219
+
220
+
221
  def progress_for_run(run, summary: dict) -> dict | None:
222
  progress = extract_progress(summary)
223
+ if progress is None:
224
+ return None
225
+ if progress["status"] == "running" and getattr(run, "state", None) in {
226
+ "crashed",
227
+ "failed",
228
+ "killed",
229
+ }:
230
+ progress["status"] = "failed"
231
+ timings = extract_stage_timings(run, progress["epoch"])
232
+ for stage_name, timing in timings.items():
233
+ stage = progress["stages"].get(stage_name)
234
+ if isinstance(stage, dict):
235
+ stage.update(timing)
236
  return progress
237
 
238
 
 
246
  epoch = _as_float(row.get("epoch"))
247
  if epoch is None:
248
  continue
249
+ row_metrics = {}
250
  for key, value in row.items():
251
  hk = _miner_hotkey_from_score_key(key)
252
  if hk is not None:
253
+ row_metrics[("miner", hk, "score")] = _as_float(value)
254
  miner_keys.add(hk)
255
  elif key.startswith(_SCORE_PREFIX) and key.endswith(_LEN_SUFFIX):
256
  hk2 = key[len(_SCORE_PREFIX) : -len(_LEN_SUFFIX)]
257
  if hk2:
258
+ row_metrics[("miner", hk2, "completion_len")] = _as_float(value)
259
  miner_keys.add(hk2)
260
  elif key.startswith(_SCORE_PREFIX) and key.endswith(_CORRECTNESS_SUFFIX):
261
  hk2 = key[len(_SCORE_PREFIX) : -len(_CORRECTNESS_SUFFIX)]
262
  if hk2:
263
+ row_metrics[("miner", hk2, "correctness_score")] = _as_float(value)
264
  miner_keys.add(hk2)
265
  elif key == _ORIG_SCORE:
266
+ row_metrics[("orig", "score")] = _as_float(value)
267
+ row_metrics.setdefault(
268
+ ("orig", "correctness_score"), _score_to_signed_correctness(value)
269
+ )
270
  saw_orig_score = True
271
  elif key == _ORIG_CORRECTNESS:
272
+ row_metrics[("orig", "correctness_score")] = _as_float(value)
273
  saw_orig_score = True
274
  elif key == _ORIG_LEN:
275
+ row_metrics[("orig", "completion_len")] = _as_float(value)
276
  saw_orig_len = True
277
+ if any(value is not None for value in row_metrics.values()):
278
+ rows_by_epoch.setdefault(epoch, {}).update(row_metrics)
279
 
280
  epochs = sorted(rows_by_epoch)
281
  miners: dict[str, dict] = {}
 
403
  return f"{entity}/{project}" if entity else project
404
 
405
 
406
+ def _is_running_run(run) -> bool:
407
+ return getattr(run, "state", None) == "running"
408
+
409
+
410
  def build(project: str, entity: str | None, api=None) -> dict:
411
  if api is None:
412
  import wandb
 
418
  fallbacks: dict[str, dict] = {}
419
  selected: dict[str, dict] = {}
420
  for run in api.runs(path, order="-created_at"):
421
+ if not _is_running_run(run):
422
+ continue
423
  hotkey = dict(run.config).get("validator_hotkey")
424
  if not isinstance(hotkey, str) or not hotkey or hotkey in selected:
425
  continue
 
459
  seen = set()
460
  runs = api.runs(path, order="-created_at")
461
  for run in islice(runs, max(1, run_limit)):
462
+ if not _is_running_run(run):
463
+ continue
464
  hotkey = dict(run.config).get("validator_hotkey")
465
  if not isinstance(hotkey, str) or not hotkey or hotkey in seen:
466
  continue
server.py CHANGED
@@ -70,6 +70,40 @@ def _progress_from_payload(payload: dict) -> dict[str, dict]:
70
  }
71
 
72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  def _progress_requires_data_refresh(
74
  payload: dict, progress_by_validator: dict[str, dict]
75
  ) -> bool:
@@ -91,6 +125,10 @@ def _progress_requires_data_refresh(
91
  history_epoch is None or progress_epoch > history_epoch
92
  ):
93
  return True
 
 
 
 
94
  return False
95
 
96
 
 
70
  }
71
 
72
 
73
+ def _has_value(values, index: int) -> bool:
74
+ if not isinstance(values, list) or index >= len(values):
75
+ return False
76
+ value = values[index]
77
+ return isinstance(value, (int, float)) and value == value
78
+
79
+
80
+ def _history_has_scored_epoch(validator: dict, epoch) -> bool:
81
+ history = validator.get("history", {})
82
+ epochs = history.get("epochs", [])
83
+ if not isinstance(history, dict) or not isinstance(epochs, list):
84
+ return False
85
+ try:
86
+ index = epochs.index(epoch)
87
+ except ValueError:
88
+ return False
89
+
90
+ original = history.get("original", {})
91
+ if isinstance(original, dict):
92
+ for field in ("correctness_score", "completion_len", "score"):
93
+ if _has_value(original.get(field), index):
94
+ return True
95
+
96
+ miners = history.get("miners", {})
97
+ if isinstance(miners, dict):
98
+ for series in miners.values():
99
+ if not isinstance(series, dict):
100
+ continue
101
+ for field in ("correctness_score", "completion_len", "score"):
102
+ if _has_value(series.get(field), index):
103
+ return True
104
+ return False
105
+
106
+
107
  def _progress_requires_data_refresh(
108
  payload: dict, progress_by_validator: dict[str, dict]
109
  ) -> bool:
 
125
  history_epoch is None or progress_epoch > history_epoch
126
  ):
127
  return True
128
+ if isinstance(progress_epoch, (int, float)) and not _history_has_scored_epoch(
129
+ validator, progress_epoch
130
+ ):
131
+ return True
132
  return False
133
 
134