Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
File size: 12,893 Bytes
1fb78ac cfd9e58 1fb78ac 62848cc 2475e35 62848cc 2475e35 62848cc 2475e35 62848cc 1fb78ac 3e3bac5 1fb78ac 3e3bac5 1fb78ac 3e3bac5 1fb78ac 3e3bac5 1fb78ac 3e3bac5 1fb78ac 3e3bac5 1fb78ac 3e3bac5 1fb78ac 3e3bac5 1fb78ac 3e3bac5 1fb78ac 3e3bac5 1fb78ac 3e3bac5 1fb78ac 3e3bac5 1fb78ac | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 | """Row math for the Video Benchmark Space.
Pure functions that take ``list[dict]`` rows (a subset of each
benchmark's results schema — projected down to the benchmark's
``keep_keys`` in ``schema.py``) and return JSON-serializable values. The Gradio handlers
in ``app.py`` call these to build the Results table heatmap, the
Leaderboards bar chart, and the three Compare-tab plots.
Nothing here imports Gradio, pandas, or plotly: the module is purely
arithmetic so it stays trivially testable and reusable from notebooks.
"""
from __future__ import annotations
from typing import Any, Iterable
Row = dict[str, Any]
Scale = dict[str, Any] # {"min": float, "max": float, "lower": bool}
# --------------------------------------------------------------------------- #
# Helpers
# --------------------------------------------------------------------------- #
def _is_num(v: Any) -> bool:
"""True for finite ints/floats; False for ``None``, NaN, and bools.
Bools are explicitly rejected so accidental flag columns can't sneak
into numeric aggregations.
"""
if v is None:
return False
if isinstance(v, bool):
return False
if isinstance(v, (int, float)):
# NaN is the only float that's not equal to itself.
return v == v
return False
# --------------------------------------------------------------------------- #
# Per-metric normalization (used by the Results table color ramp and the
# Leaderboards weighted score).
# --------------------------------------------------------------------------- #
def normalize_scale(rows: list[Row], col: dict[str, Any]) -> Scale:
"""Return ``{min, max, lower}`` for a metric column.
Used to map raw values into [0, 1] before feeding the rdylgn color
ramp: ``t = (v - min) / (max - min); if lower: t = 1 - t``. When all
values are missing, returns a degenerate scale tagged with
``empty=True`` so callers can fall back to mid-gray.
"""
vals = [r.get(col["key"]) for r in rows]
nums = [v for v in vals if _is_num(v)]
lower = bool(col.get("lower"))
if not nums:
return {"min": 0.0, "max": 0.0, "lower": lower, "empty": True}
lo = min(nums)
hi = max(nums)
return {"min": lo, "max": hi, "lower": lower}
def _normalize_value(v: Any, scale: Scale) -> float:
"""Map ``v`` into [0, 1] using a scale from :func:`normalize_scale`.
Returns ``0.5`` for empty scales, ``0.0`` for non-numeric values, and
inverts the result when the column is polarity-``lower`` so that
``1.0`` always means "best on this axis".
"""
if scale.get("empty"):
return 0.5
if not _is_num(v):
return 0.0
lo = scale["min"]
hi = scale["max"]
if hi == lo:
return 0.5
t = (v - lo) / (hi - lo)
return (1 - t) if scale["lower"] else t
def dedupe_latest(rows: list[Row], config_keys: Iterable[str]) -> list[Row]:
"""Collapse re-runs of the same config down to a single best row.
Workers append a fresh row every time a configuration is benchmarked,
so the Hub can hold several rows that are identical across every
``config_keys`` field and differ only in their metrics. Grouping by
that key tuple, we keep the most complete row (fewest ``None`` / NaN
fields), so an entry with missing items never wins over one without.
Ties fall back to the most samples (``num_samples``) and then the
latest ``created_at`` (ISO-8601 strings sort chronologically).
"""
keys = tuple(config_keys)
def rank(r: Row) -> tuple[int, int, str]:
missing = sum(v is None or (isinstance(v, float) and v != v)
for v in r.values())
n = r.get("num_samples")
return (-missing,
n if isinstance(n, int) and not isinstance(n, bool) else -1,
str(r.get("created_at") or ""))
best: dict[tuple, Row] = {}
order: list[tuple] = []
for r in rows:
k = tuple(r.get(c) for c in keys)
if k not in best:
order.append(k)
best[k] = r
elif rank(r) >= rank(best[k]):
best[k] = r
return [best[k] for k in order]
def composite_rank(rows: list[Row], metric_cols: list[dict[str, Any]]) -> list[Row]:
"""Sort rows by their composite score across the visible metric columns.
Score is the unweighted average of ``(1 - normalized_value)`` across
``metric_cols``, so *lower* = better. Sort is stable: ties fall back
to original input order so repeated calls on the same input produce
the same ordering.
"""
if not rows:
return []
if not metric_cols:
return list(rows)
scales = [(c, normalize_scale(rows, c)) for c in metric_cols]
def score(r: Row) -> float:
s = 0.0
n = 0
for c, sc in scales:
v = r.get(c["key"])
s += (1 - _normalize_value(v, sc))
n += 1
return s / n if n else 0.0
indexed = list(enumerate(rows))
indexed.sort(key=lambda pair: (score(pair[1]), pair[0]))
return [r for _, r in indexed]
# --------------------------------------------------------------------------- #
# Filtering
# --------------------------------------------------------------------------- #
def filter_rows(rows: list[Row], filters: dict[str, Iterable[str]] | None) -> list[Row]:
"""Keep rows whose column values match the Results-tab chip selections.
``filters`` maps a column key to the set of allowed values. Values
are always compared as strings (the CheckboxGroups emit strings,
even for numeric columns like ``g`` / ``crf``), so row values are
coerced via ``str()`` before lookup. An empty / missing entry for a
key disables that filter.
"""
if not filters:
return list(rows)
cleaned: dict[str, set[str]] = {}
for k, vs in filters.items():
if not vs:
continue
s = {str(v) for v in vs}
if s:
cleaned[k] = s
if not cleaned:
return list(rows)
out: list[Row] = []
for r in rows:
keep = True
for k, allowed in cleaned.items():
if str(r.get(k)) not in allowed:
keep = False
break
if keep:
out.append(r)
return out
def metric_scales(rows: list[Row], cols: list[dict[str, Any]]) -> dict[str, Scale]:
"""Return ``{column_key: scale}`` for every metric column in ``cols``.
Pre-computing scales once per render lets the heatmap colorize each
cell without re-walking the rows for each column.
"""
return {c["key"]: normalize_scale(rows, c) for c in cols if c.get("metric")}
# --------------------------------------------------------------------------- #
# Leaderboards
# --------------------------------------------------------------------------- #
def leaderboard(
rows: list[Row], ts: str, cat: str, *,
axes: list[str],
cats: dict[str, Any],
group_keys: tuple[str, ...],
columns: list[dict[str, Any]],
) -> dict[str, Any]:
"""Rank configs for ``cat`` at access pattern ``ts`` using caller-supplied config.
Keeps rows matching ``ts``, groups by ``group_keys`` (averaging each axis
across repeats), normalizes per axis, and scores by the weighted sum of
``(1 - normalized)`` from ``cats[cat]['weights']`` (lower = better). Returns
``{axes, axis_keys, items}`` with each ``items[i].values`` oriented so 1.0 =
best. Raises ``ValueError`` for an unknown category and ``RuntimeError`` if an
axis is missing from ``columns``.
"""
if cat not in cats:
raise ValueError(f"unknown leaderboard category: {cat!r}")
weights = cats[cat]["weights"]
cols_by_key = {c["key"]: c for c in columns}
axis_cols = [cols_by_key.get(k) for k in axes]
if any(c is None for c in axis_cols):
missing = [k for k, c in zip(axes, axis_cols) if c is None]
raise RuntimeError(f"missing column metadata for axes: {missing}")
scoped = [r for r in rows if r.get("timestamps_mode") == ts]
groups: dict[tuple, list[Row]] = {}
order: list[tuple] = []
for r in scoped:
key = tuple(r.get(k) for k in group_keys)
if key not in groups:
groups[key] = []
order.append(key)
groups[key].append(r)
aggregated: list[Row] = []
for key in order:
rs = groups[key]
ag: Row = dict(rs[0])
for c in axis_cols:
vals = [r.get(c["key"]) for r in rs]
nums = [v for v in vals if _is_num(v)]
ag[c["key"]] = sum(nums) / len(nums) if nums else None
aggregated.append(ag)
if not aggregated:
return {"axes": [c["short"] or c["label"] for c in axis_cols],
"axis_keys": axes, "items": []}
scales = [normalize_scale(aggregated, c) for c in axis_cols]
scored: list[tuple[float, int, Row, list[float]]] = []
for idx, row in enumerate(aggregated):
sum_w = 0.0
wsum = 0.0
values: list[float] = []
for i, c in enumerate(axis_cols):
w = weights.get(c["key"], 1)
n = _normalize_value(row.get(c["key"]), scales[i])
values.append(n)
sum_w += w * (1 - n)
wsum += w
score = sum_w / wsum if wsum else 0.0
scored.append((score, idx, row, values))
scored.sort(key=lambda t: (t[0], t[1]))
items = [
{"row": _slim_row(r, group_keys, axes), "values": vals, "score": s}
for s, _, r, vals in scored
]
return {
"axes": [c["short"] or c["label"] for c in axis_cols],
"axis_keys": axes,
"items": items,
}
def _slim_row(r: Row, group_keys: tuple[str, ...], axes: list[str]) -> Row:
keep = (*group_keys, "timestamps_mode", "repo_id", *axes)
return {k: r.get(k) for k in keep if k in r}
# --------------------------------------------------------------------------- #
# Compare-tab aggregations
# --------------------------------------------------------------------------- #
def compare_bar(rows: list[Row], metric: str, group_by: str) -> list[dict[str, Any]]:
"""Mean of ``metric`` grouped by ``group_by``, sorted descending by value.
Returns ``[{"k": label, "v": mean}, ...]``. The ``lerobot/`` prefix is
stripped from dataset labels so x-axis ticks stay readable when
``group_by="repo_id"``.
"""
g: dict[str, list[float]] = {}
order: list[str] = []
for r in rows:
v = r.get(metric)
if not _is_num(v):
continue
k = str(r.get(group_by))
if k not in g:
g[k] = []
order.append(k)
g[k].append(v)
out = [
{"k": k.replace("lerobot/", ""), "v": sum(g[k]) / len(g[k])}
for k in order
if g[k]
]
out.sort(key=lambda d: d["v"], reverse=True)
return out
def compare_scatter(
rows: list[Row], *, x: str, y: str, label_keys: tuple[str, ...] = ("g", "crf"),
) -> list[dict[str, Any]]:
"""One ``{x, y, c, label}`` point per row, skipping non-numeric ``x``/``y``."""
pts: list[dict[str, Any]] = []
for r in rows:
xv = r.get(x)
yv = r.get(y)
if not _is_num(xv) or not _is_num(yv):
continue
label = " ".join(f"{k}={r.get(k)}" for k in label_keys)
pts.append({"x": xv, "y": yv, "c": r.get("vcodec"), "label": label})
return pts
def compare_stacked(rows: list[Row]) -> dict[str, Any]:
"""Aggregate decoding latency per codec, broken down by access pattern.
Returns ``{codecs, modes, data}`` where ``data[i].segments`` carries
the per-mean-decode-time mean for each access pattern. The codec list
is sorted by total stacked height ascending so the fastest codec sits
at the left of the chart.
"""
codecs: list[str] = []
seen_c: set[str] = set()
modes: list[str] = []
seen_m: set[str] = set()
for r in rows:
c = r.get("vcodec")
if c is not None and c not in seen_c:
seen_c.add(c)
codecs.append(c)
m = r.get("timestamps_mode")
if m is not None and m not in seen_m:
seen_m.add(m)
modes.append(m)
data: list[dict[str, Any]] = []
for codec in codecs:
segs: list[dict[str, Any]] = []
for mode in modes:
vals = [
r["median_load_time_video_ms"]
for r in rows
if r.get("vcodec") == codec
and r.get("timestamps_mode") == mode
and _is_num(r.get("median_load_time_video_ms"))
]
v = sum(vals) / len(vals) if vals else 0.0
segs.append({"key": mode, "v": v})
total = sum(s["v"] for s in segs)
data.append({"k": codec, "segments": segs, "total": total})
data.sort(key=lambda d: d["total"])
return {"codecs": codecs, "modes": modes, "data": data}
|