Skip to content

fix: represent unbounded metric bounds as the JSON string "Infinity"#207

Open
borgr wants to merge 1 commit into
evaleval:mainfrom
borgr:fix/inf-serialization
Open

fix: represent unbounded metric bounds as the JSON string "Infinity"#207
borgr wants to merge 1 commit into
evaleval:mainfrom
borgr:fix/inf-serialization

Conversation

@borgr

@borgr borgr commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Problem

An unbounded continuous bound (PSNR, perplexity, TER, …) is float('inf')/-inf, but pydantic's default serialization wrote it to JSON null — which then failed the existing continuous requires min/max_score validator. So inf could be read but not written: unbounded metrics could only be dropped, faked, or made invalid.

Fix

Core — a field serializer on MetricConfig.min_score/max_score emits inf/-inf as the JSON string "Infinity"/"-Infinity":

  • valid RFC-8259 JSON (parses in JS / Go / jq / orjson, not just Python) that pydantic reads back to float('inf');
  • chosen over model-level ser_json_inf_nan because that only affects model_dump_json() — the codebase also writes via json.dump(model_dump(mode='json')) (cli.py, instance writers), which would emit the bare, invalid Infinity token. field_serializer(when_used='json') covers both paths; mode='python' keeps the native float;
  • the field stays typed float | None, so "convertible to a number" is enforced by the type; the schema pins the string form to exactly {"Infinity","-Infinity"}.

Semantics (kept strict on purpose)

  • null stays invalid for a continuous metric: missing ≠ unbounded, so null cannot masquerade as inf; unbounded uses inf, genuinely-missing bounds stay invalid.
  • NaN bounds are rejected (never meaningful; NaN != NaN breaks comparisons).

Convention applied in-repo (converters/lm_eval) so nothing is left emitting null-for-unbounded: ter bound (0.0, None)(0.0, inf), and a metric with no entry in KNOWN_METRIC_BOUNDS is now skipped with a warning instead of building an invalid null-bounded continuous config. Supersedes #206.

Regen-safe: post_codegen.py re-applies the validator + serializer to MetricConfig.

Tests

tests/test_inf_serialization.py (both dump paths incl. the mode='json' regression, mode='python' float, strict-JSON validity, float typing/round-trip, null-still-invalid, NaN-rejected) + a new lm_eval case. Full suite green (207 passed / 20 skipped), ruff clean.

Portability

"Infinity"/"-Infinity" is valid JSON everywhere. A statically-typed consumer (Go float64) needs a small custom unmarshaller for the number | "Infinity" union — the schema enum documents exactly what to expect.

Follow-ups (separate — not required for correctness here)

  • eval-card-registry (separate repo): reclassify the ~9 canonical metrics whose max_score: null means unbounded (elo, cost-per-task, latency-*, rank, …) to inf; leave the genuinely-vague ones (score, *.mean placeholders) null.
  • EEE_datastore: re-run / patch affected records in parallel.

An unbounded continuous bound (PSNR, perplexity, TER, ...) is float('inf')/-inf,
but pydantic's default serialization wrote it to JSON null -- which then failed
the "continuous requires min/max_score" validator. inf could be read but not
written, so unbounded metrics could only be dropped, faked, or made invalid.

Core fix (every_eval_ever/eval_types.py):
- Field serializer on MetricConfig.min_score/max_score emits inf/-inf as the JSON
  string "Infinity"/"-Infinity": valid RFC-8259 JSON (parses in JS/Go/jq/orjson,
  not just Python) that pydantic reads back to float('inf').
- Chosen over model config `ser_json_inf_nan` because that only affects
  model_dump_json(); the codebase also writes via json.dump(model_dump(mode='json'))
  (cli.py, instance writers), which would emit the bare, INVALID Infinity token. The
  field serializer (when_used='json') covers BOTH paths; mode='python' keeps the
  native float. It also targets exactly the two bound fields -- no blast radius.
- The field stays typed float|None, so "convertible to a number" is enforced by the
  type; the schema pins the string form to exactly {"Infinity","-Infinity"}.

Semantics kept strict on purpose:
- null stays INVALID for a continuous metric: missing != unbounded, so null cannot
  masquerade as inf; unbounded uses inf, genuinely-missing bounds stay invalid.
- NaN bounds are rejected (never meaningful; NaN != NaN breaks comparisons).

Apply the convention in-repo so nothing is left emitting null-for-unbounded
(converters/lm_eval): 'ter' bound changes from (0.0, None) -> (0.0, inf), and a
metric with no entry in KNOWN_METRIC_BOUNDS is now skipped with a warning rather
than building an invalid null-bounded continuous config. Supersedes evaleval#206.

Regen-safe: post_codegen re-applies the validator + serializer to MetricConfig.
Tests: tests/test_inf_serialization.py and a new lm_eval case.

Follow-ups (out of this repo / parallel, not required for correctness here):
- eval-card-registry (separate repo): reclassify the ~9 canonical metrics whose
  max_score:null means "unbounded" (elo, cost-per-task, latency-*, rank, ...) to
  inf; leave the genuinely-vague ones (score, *.mean placeholders) null.
- EEE_datastore: re-run/patch affected records in parallel.
borgr added a commit to borgr/eval-card-registry that referenced this pull request Jul 21, 2026
EEE now distinguishes inf (unbounded) from null (missing) -- see
evaleval/every_eval_ever#207. Reclassify the canonical metrics whose null
max_score meant "unbounded":

- unbounded above (finite min, open top) -> max_score: .inf:
  cost-per-task, total-cost, avg-attempts, rank, mean-response-time,
  p95-latency, mean-latency, latency-stddev
- unbounded both ways -> (-.inf, .inf): elo (ratings have no fixed bounds)

Left as null on purpose (genuinely undefined/vague, not "unbounded"): score,
mean-score, and the *.mean placeholders.

.inf/-.inf parse to float('inf')/-inf via yaml.safe_load and store as float64;
seed tests pass. No JSON path serializes these bounds (they are float columns),
so no inf->null regression.
borgr added a commit that referenced this pull request Jul 21, 2026
Convert PwC PostgreSQL dumps from the HF bucket nielsr/paperswithcode-backups
into EEE aggregate EvaluationLogs (one log per model; source_type=documentation).

Metric bounds/direction resolve against a vendored snapshot of the
eval-card-registry canonical metrics, with a three-tier policy:
  - resolved   -> canonical bounds/id; scores rescaled to the canonical scale;
                  unbounded (null) bounds emitted as +/-inf, serialized as the
                  JSON string "Infinity"/"-Infinity" (depends on every_eval_ever#207)
  - unresolved -> FAIL CLOSED by default (never ship un-vetted bounds); an
                  --allow-unresolved opt-out emits observed-range bounds

Read with pgdumplib (declared as the 'paperswithcode' extra; pure-python, no
PostgreSQL server). Includes offline fixture tests, a README, and a snapshot
regenerator. Stacked on every_eval_ever#207 for the "Infinity" bound
serialization; rebase onto main once #207 merges.
borgr added a commit that referenced this pull request Jul 21, 2026
Convert PwC PostgreSQL dumps from the HF bucket nielsr/paperswithcode-backups
into EEE aggregate EvaluationLogs (one log per model; source_type=documentation).

Metric bounds/direction resolve against a vendored snapshot of the
eval-card-registry canonical metrics, with a three-tier policy:
  - resolved   -> canonical bounds/id; scores rescaled to the canonical scale;
                  unbounded (null) bounds emitted as +/-inf, serialized as the
                  JSON string "Infinity"/"-Infinity" (depends on every_eval_ever#207)
  - unresolved -> FAIL CLOSED by default (never ship un-vetted bounds); an
                  --allow-unresolved opt-out emits observed-range bounds

Read with pgdumplib (declared as the 'paperswithcode' extra; pure-python, no
PostgreSQL server). Includes offline fixture tests, a README, and a snapshot
regenerator. Stacked on every_eval_ever#207 for the "Infinity" bound
serialization; rebase onto main once #207 merges.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant