whileai 0.55__tar.gz → 0.56__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {whileai-0.55/whileai.egg-info → whileai-0.56}/PKG-INFO +1 -1
- {whileai-0.55 → whileai-0.56}/pyproject.toml +1 -1
- {whileai-0.55 → whileai-0.56}/whileai/simulations/__init__.py +6 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/ingest/platform.py +33 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/score/delta.py +37 -1
- {whileai-0.55 → whileai-0.56}/whileai/simulations/score/optimize.py +127 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/score/stats.py +110 -0
- {whileai-0.55 → whileai-0.56/whileai.egg-info}/PKG-INFO +1 -1
- {whileai-0.55 → whileai-0.56}/LICENSE +0 -0
- {whileai-0.55 → whileai-0.56}/MANIFEST.in +0 -0
- {whileai-0.55 → whileai-0.56}/README.md +0 -0
- {whileai-0.55 → whileai-0.56}/setup.cfg +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/__init__.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/_env.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/auth.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/cli.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/ingest.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/py.typed +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/__main__.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/data.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/environment.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/export.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/generate/__init__.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/generate/actionspace.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/generate/adapters.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/generate/agents.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/generate/coverage.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/generate/diversity.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/generate/embeddings.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/generate/explore.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/generate/generator.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/generate/offline_agent.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/generate/scenarios.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/generate/usage_meter.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/ingest/__init__.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/ingest/otel.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/ingest/traces.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/monitor.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/py.typed +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/run/__init__.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/run/config.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/run/engine.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/run/rows.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/run/spec.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/schema.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/schemas/row-v1.json +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/score/__init__.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/score/agreement.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/score/checklist.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/score/curriculum.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/score/grade_llm.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/score/grading.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/score/grounding.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/score/hack_scan.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/score/hygiene.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/score/judge_trust.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/score/judging.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/score/labels.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/score/llm_judge.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/score/logprobs.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/score/markers.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/score/pairwise.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/score/passat.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/score/preflight.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/score/privileged.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/score/publish_gate.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/score/quality.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/score/reference.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/score/rubric.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/score/spec.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/score/stage.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/score/style.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/score/trace.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/simulation.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/training.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/verify/__init__.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/verify/base.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/verify/code.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/verify/math.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/verify/structured.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/verify/text.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/world/__init__.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai/simulations/world/sandbox.py +0 -0
- {whileai-0.55 → whileai-0.56}/whileai.egg-info/SOURCES.txt +0 -0
- {whileai-0.55 → whileai-0.56}/whileai.egg-info/dependency_links.txt +0 -0
- {whileai-0.55 → whileai-0.56}/whileai.egg-info/entry_points.txt +0 -0
- {whileai-0.55 → whileai-0.56}/whileai.egg-info/requires.txt +0 -0
- {whileai-0.55 → whileai-0.56}/whileai.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "whileai"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.56"
|
|
8
8
|
description = "While Python SDK: trace ingestion and verification client, plus agent simulations grounded in your tools and system prompt."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "Apache-2.0"}
|
|
@@ -135,6 +135,7 @@ from .score.optimize import (
|
|
|
135
135
|
DEFAULT_BAND,
|
|
136
136
|
filter_rl_rows,
|
|
137
137
|
group_signal,
|
|
138
|
+
next_round,
|
|
138
139
|
optimize,
|
|
139
140
|
recommend,
|
|
140
141
|
select_for_rl,
|
|
@@ -162,7 +163,9 @@ from .score.stage import STAGES, format_stages, stage_of, stage_report, stamp_st
|
|
|
162
163
|
from .score.stats import (
|
|
163
164
|
compare_runs,
|
|
164
165
|
decontaminate,
|
|
166
|
+
detectable_effect,
|
|
165
167
|
eval_variance,
|
|
168
|
+
holdout_size,
|
|
166
169
|
marker_summary,
|
|
167
170
|
metric_summary,
|
|
168
171
|
task_key,
|
|
@@ -258,6 +261,7 @@ __all__ = [
|
|
|
258
261
|
"delete_model",
|
|
259
262
|
"delete_run",
|
|
260
263
|
"delta_report",
|
|
264
|
+
"detectable_effect",
|
|
261
265
|
"dimensions_from_traces",
|
|
262
266
|
"drop_leaky_rows",
|
|
263
267
|
"eval_variance",
|
|
@@ -290,6 +294,7 @@ __all__ = [
|
|
|
290
294
|
"hf_publish",
|
|
291
295
|
"hf_publish_run",
|
|
292
296
|
"hf_status",
|
|
297
|
+
"holdout_size",
|
|
293
298
|
"hosted_model",
|
|
294
299
|
"import_hf",
|
|
295
300
|
"inspect",
|
|
@@ -317,6 +322,7 @@ __all__ = [
|
|
|
317
322
|
"mine_traces",
|
|
318
323
|
"models",
|
|
319
324
|
"near_duplicate_prompts",
|
|
325
|
+
"next_round",
|
|
320
326
|
"normalize_judge_result",
|
|
321
327
|
"novelty",
|
|
322
328
|
"open_ended_probes",
|
|
@@ -22,6 +22,7 @@ import time
|
|
|
22
22
|
import urllib.error
|
|
23
23
|
import urllib.parse
|
|
24
24
|
import urllib.request
|
|
25
|
+
import warnings as _warnings
|
|
25
26
|
from collections.abc import Sequence
|
|
26
27
|
from typing import Any
|
|
27
28
|
|
|
@@ -242,6 +243,36 @@ def _meta_body(
|
|
|
242
243
|
return body
|
|
243
244
|
|
|
244
245
|
|
|
246
|
+
HOLDOUT_PROVE_EFFECT = 0.05
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _warn_small_holdout(rows: Sequence[dict]) -> None:
|
|
250
|
+
"""A holdout too small to prove a 5-point gain reads every round as
|
|
251
|
+
``no_change_detected``; say so at push time, not after training (#257)."""
|
|
252
|
+
from ..score.stats import holdout_size, task_key
|
|
253
|
+
|
|
254
|
+
groups: dict[str, int] = {}
|
|
255
|
+
for row in rows:
|
|
256
|
+
if isinstance(row, dict):
|
|
257
|
+
key = task_key(row)
|
|
258
|
+
groups[key] = groups.get(key, 0) + 1
|
|
259
|
+
n_tasks = len(groups)
|
|
260
|
+
if not n_tasks:
|
|
261
|
+
return
|
|
262
|
+
try:
|
|
263
|
+
need = holdout_size(HOLDOUT_PROVE_EFFECT, rows=rows)
|
|
264
|
+
except ValueError:
|
|
265
|
+
need = holdout_size(HOLDOUT_PROVE_EFFECT, k=min(groups.values()))
|
|
266
|
+
if n_tasks < need["n_tasks"]:
|
|
267
|
+
_warnings.warn(
|
|
268
|
+
f"holdout has {n_tasks} tasks at k={need['k']}; proving a "
|
|
269
|
+
f"{HOLDOUT_PROVE_EFFECT:.0%} gain at 80% power needs about {need['n_tasks']} "
|
|
270
|
+
"(holdout_size). A smaller holdout reads a real gain that size as "
|
|
271
|
+
"no_change_detected.",
|
|
272
|
+
stacklevel=3,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
245
276
|
def push_rows(
|
|
246
277
|
rows: list[dict],
|
|
247
278
|
name: str,
|
|
@@ -279,6 +310,8 @@ def push_rows(
|
|
|
279
310
|
|
|
280
311
|
gate_report = publish_gate(rows, mode=mode, endorsed=endorsed, strict_hacks=strict_hacks)
|
|
281
312
|
check(rows, where="push_rows")
|
|
313
|
+
if purpose == "holdout":
|
|
314
|
+
_warn_small_holdout(rows)
|
|
282
315
|
body: dict = {
|
|
283
316
|
"name": name,
|
|
284
317
|
**_meta_body(purpose, mode if mode in MODES else None, agent, description),
|
|
@@ -26,7 +26,15 @@ from collections.abc import Callable, Sequence
|
|
|
26
26
|
from typing import Any
|
|
27
27
|
|
|
28
28
|
from .passat import pass_at
|
|
29
|
-
from .stats import
|
|
29
|
+
from .stats import (
|
|
30
|
+
DEFAULT_BOOT,
|
|
31
|
+
compare_runs,
|
|
32
|
+
detectable_effect,
|
|
33
|
+
eval_variance,
|
|
34
|
+
holdout_size,
|
|
35
|
+
marker_names,
|
|
36
|
+
task_means,
|
|
37
|
+
)
|
|
30
38
|
|
|
31
39
|
GROUP_KEYS = ("delta", "ci95", "verdict", "mean_a", "mean_b", "n_used", "n_paired", "paired")
|
|
32
40
|
|
|
@@ -327,6 +335,32 @@ def delta_report(
|
|
|
327
335
|
headline_key = target_key if target_result else "pass_at_1"
|
|
328
336
|
if headline.get("note"):
|
|
329
337
|
warnings.append(f"{headline_key}: {headline['note']}")
|
|
338
|
+
# Eval size: a no-change verdict is only as strong as the band the
|
|
339
|
+
# task count allows. Say what this holdout can prove and what the
|
|
340
|
+
# delta seen here would have needed (#257).
|
|
341
|
+
n_paired = int(headline.get("n_paired") or 0)
|
|
342
|
+
k_eval = int(pass_at(before).config.get("k") or 1)
|
|
343
|
+
base_rate = float(mean_a) if mean_a is not None else 0.6
|
|
344
|
+
can_prove = detectable_effect(n_paired, base=base_rate, k=k_eval) if n_paired >= 2 else None
|
|
345
|
+
tasks_needed: int | None = None
|
|
346
|
+
delta_seen: float | None = None
|
|
347
|
+
raw_delta = headline.get("delta")
|
|
348
|
+
if isinstance(raw_delta, (int, float)) and 0 < raw_delta < 1:
|
|
349
|
+
delta_seen = float(raw_delta)
|
|
350
|
+
tasks_needed = holdout_size(delta_seen, base=base_rate, k=k_eval)["n_tasks"]
|
|
351
|
+
verdict_word = (
|
|
352
|
+
target_verdict if target_result else _verdict_word(results["pass_at_1"], replicated)
|
|
353
|
+
)
|
|
354
|
+
if verdict_word == "no_change_detected" and can_prove is not None:
|
|
355
|
+
line = (
|
|
356
|
+
f"{n_paired} paired tasks at k={k_eval} can prove a gain of about "
|
|
357
|
+
f"+{can_prove:.2f} at 80% power"
|
|
358
|
+
)
|
|
359
|
+
if tasks_needed is not None and delta_seen is not None:
|
|
360
|
+
line += (
|
|
361
|
+
f"; to prove the {delta_seen:+.3f} seen here you need about {tasks_needed} tasks"
|
|
362
|
+
)
|
|
363
|
+
warnings.append(line + " (holdout_size).")
|
|
330
364
|
if target_verdict == "target_not_measured":
|
|
331
365
|
warnings.append(f"target {target!r} is not on both row sets")
|
|
332
366
|
groups: dict[str, dict[str, Any]] | None = None
|
|
@@ -398,6 +432,8 @@ def delta_report(
|
|
|
398
432
|
"eval_runs": eval_runs,
|
|
399
433
|
"replicated": replicated,
|
|
400
434
|
"ceiling": ceiling,
|
|
435
|
+
"detectable_effect": can_prove,
|
|
436
|
+
"tasks_needed": tasks_needed,
|
|
401
437
|
"proxy": proxy_key,
|
|
402
438
|
"proxy_verdict": proxy_verdict,
|
|
403
439
|
"proxy_delta": proxy_result["delta"] if proxy_result else None,
|
|
@@ -614,6 +614,105 @@ def _spread_by(prompts: list[str], key) -> list[str]:
|
|
|
614
614
|
return out
|
|
615
615
|
|
|
616
616
|
|
|
617
|
+
def _task_keys_of(tasks: Sequence[Any]) -> list[str]:
|
|
618
|
+
out: list[str] = []
|
|
619
|
+
for t in tasks:
|
|
620
|
+
if isinstance(t, dict):
|
|
621
|
+
out.append(task_key(t))
|
|
622
|
+
else:
|
|
623
|
+
out.append(task_key({"prompt": str(t)}))
|
|
624
|
+
return out
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def next_round(
|
|
628
|
+
prior: Sequence[dict],
|
|
629
|
+
*,
|
|
630
|
+
tasks: Sequence[Any] | None = None,
|
|
631
|
+
lo: float = DEFAULT_BAND[0],
|
|
632
|
+
hi: float = DEFAULT_BAND[1],
|
|
633
|
+
) -> dict[str, Any]:
|
|
634
|
+
"""The prompt set for the next round, from the last round's graded
|
|
635
|
+
rollouts.
|
|
636
|
+
|
|
637
|
+
A round trained on the file it started from keeps paying for groups
|
|
638
|
+
that give no gradient: at a 0.65 training reward about half the
|
|
639
|
+
groups are all-pass or all-fail. The band is the fix the book already
|
|
640
|
+
names (rlhf-book ch. 7: filter to the 20-80% band; ch. 6, DAPO's
|
|
641
|
+
dynamic sampling drops groups with no contrast), applied to what the
|
|
642
|
+
*current* policy does rather than what the base did. ``prior`` is
|
|
643
|
+
round N's graded rollouts (``simulate(tasks=..., repeats=k)`` on the
|
|
644
|
+
round-N policy, or the trainer's own sampled rows); each task's pass
|
|
645
|
+
rate over them decides: inside ``[lo, hi]`` it is kept, above ``hi``
|
|
646
|
+
it is solved and dropped, below ``lo`` it is unsolved and dropped.
|
|
647
|
+
``tasks`` restricts the candidates (rows, task dicts with a
|
|
648
|
+
``prompt``, or prompt strings); a task with no prior rollouts is
|
|
649
|
+
``unknown`` and kept, since nothing says it is flat.
|
|
650
|
+
|
|
651
|
+
Returns ``tasks`` (one representative row per kept task: the prior
|
|
652
|
+
row, with ``calibration.pass_rate`` and the band), the counts
|
|
653
|
+
``kept``, ``dropped_solved``, ``dropped_unsolved``, ``unknown``,
|
|
654
|
+
``pass_rates`` per task, ``band``, ``from_policy`` (the policy
|
|
655
|
+
versions the prior rows came from) and ``prompt_set_sha``: the
|
|
656
|
+
identity of the kept set, for lineage on the run. Push the kept rows
|
|
657
|
+
as the next train set with ``parent=`` the last one.
|
|
658
|
+
"""
|
|
659
|
+
if not 0 <= lo < hi <= 1:
|
|
660
|
+
raise ValueError("band is 0 <= lo < hi <= 1")
|
|
661
|
+
labels = _group_label_lists(prior)
|
|
662
|
+
rates = {key: sum(v) / len(v) for key, v in labels.items() if v}
|
|
663
|
+
first: dict[str, dict] = {}
|
|
664
|
+
policies: set[str] = set()
|
|
665
|
+
for row in prior:
|
|
666
|
+
if not isinstance(row, dict):
|
|
667
|
+
continue
|
|
668
|
+
first.setdefault(task_key(row), row)
|
|
669
|
+
if row.get("policy_version"):
|
|
670
|
+
policies.add(str(row["policy_version"]))
|
|
671
|
+
if tasks is None:
|
|
672
|
+
candidates = list(rates)
|
|
673
|
+
given: dict[str, Any] = {}
|
|
674
|
+
else:
|
|
675
|
+
given = {}
|
|
676
|
+
for t in tasks:
|
|
677
|
+
key = task_key(t) if isinstance(t, dict) else task_key({"prompt": str(t)})
|
|
678
|
+
given.setdefault(key, t)
|
|
679
|
+
candidates = list(given)
|
|
680
|
+
kept: list[dict] = []
|
|
681
|
+
solved = unsolved = unknown = 0
|
|
682
|
+
for key in candidates:
|
|
683
|
+
rate = rates.get(key)
|
|
684
|
+
if rate is None:
|
|
685
|
+
unknown += 1
|
|
686
|
+
rep = given.get(key)
|
|
687
|
+
rep = dict(rep) if isinstance(rep, dict) else {"prompt": str(rep)}
|
|
688
|
+
kept.append(rep)
|
|
689
|
+
continue
|
|
690
|
+
if rate > hi:
|
|
691
|
+
solved += 1
|
|
692
|
+
continue
|
|
693
|
+
if rate < lo:
|
|
694
|
+
unsolved += 1
|
|
695
|
+
continue
|
|
696
|
+
rep = dict(first.get(key) or given.get(key) or {"prompt": key})
|
|
697
|
+
cal = dict(rep.get("calibration") or {})
|
|
698
|
+
cal.update({"pass_rate": round(rate, 4), "n": len(labels[key]), "band": [lo, hi]})
|
|
699
|
+
rep["calibration"] = cal
|
|
700
|
+
kept.append(rep)
|
|
701
|
+
sha = hashlib.sha256("\n".join(sorted(task_key(r) for r in kept)).encode()).hexdigest()[:16]
|
|
702
|
+
return {
|
|
703
|
+
"tasks": kept,
|
|
704
|
+
"kept": len(kept) - unknown,
|
|
705
|
+
"dropped_solved": solved,
|
|
706
|
+
"dropped_unsolved": unsolved,
|
|
707
|
+
"unknown": unknown,
|
|
708
|
+
"n_prior_tasks": len(rates),
|
|
709
|
+
"pass_rates": {k: round(v, 4) for k, v in rates.items()},
|
|
710
|
+
"band": [lo, hi],
|
|
711
|
+
"from_policy": sorted(policies),
|
|
712
|
+
"prompt_set_sha": sha,
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
|
|
617
716
|
def select_for_rl(
|
|
618
717
|
rows: Sequence[dict],
|
|
619
718
|
*,
|
|
@@ -627,9 +726,18 @@ def select_for_rl(
|
|
|
627
726
|
endorsed: Sequence[str] = (),
|
|
628
727
|
truncated: str = "drop",
|
|
629
728
|
order: str = "spread",
|
|
729
|
+
prior: Sequence[dict] | None = None,
|
|
630
730
|
) -> tuple[list[dict], dict[str, Any]]:
|
|
631
731
|
"""Whole mixed groups up to roughly ``target`` rows. Groups never split.
|
|
632
732
|
|
|
733
|
+
``prior`` is the previous round's graded rollouts: tasks the round-N
|
|
734
|
+
policy already solves (pass rate above ``hi`` on ``prior``) or never
|
|
735
|
+
solves (below ``lo``) are dropped before anything else, so round N+1
|
|
736
|
+
trains on what that policy gets right 20-80% of the time rather than
|
|
737
|
+
on the file round 1 started from (``next_round``; rlhf-book ch. 7).
|
|
738
|
+
The report's ``prior`` block counts kept, dropped_solved,
|
|
739
|
+
dropped_unsolved and unknown.
|
|
740
|
+
|
|
633
741
|
``truncated`` says what happens to a rollout cut at the token cap
|
|
634
742
|
(rlhf-book ch. 6, DAPO's overlong handling; ch. 7 overlong filtering):
|
|
635
743
|
``"drop"`` removes it (the default; ``drop_truncated=False`` is the old
|
|
@@ -691,6 +799,24 @@ def select_for_rl(
|
|
|
691
799
|
raise ValueError(f"order must be one of {', '.join(RL_ORDERS)}; got {order!r}")
|
|
692
800
|
if not drop_truncated and truncated == "drop":
|
|
693
801
|
truncated = "keep"
|
|
802
|
+
prior_report: dict[str, Any] | None = None
|
|
803
|
+
if prior is not None:
|
|
804
|
+
plan = next_round(prior, lo=lo, hi=hi)
|
|
805
|
+
rates = plan["pass_rates"]
|
|
806
|
+
before_n = len(rows)
|
|
807
|
+
rows = [
|
|
808
|
+
r
|
|
809
|
+
for r in rows
|
|
810
|
+
if not isinstance(r, dict)
|
|
811
|
+
or rates.get(task_key(r)) is None
|
|
812
|
+
or lo <= rates[task_key(r)] <= hi
|
|
813
|
+
]
|
|
814
|
+
prior_report = {
|
|
815
|
+
k: plan[k]
|
|
816
|
+
for k in ("kept", "dropped_solved", "dropped_unsolved", "unknown", "from_policy")
|
|
817
|
+
}
|
|
818
|
+
prior_report["rows_dropped"] = before_n - len(rows)
|
|
819
|
+
prior_report["prompt_set_sha"] = plan["prompt_set_sha"]
|
|
694
820
|
penalized = kept_overlong = 0
|
|
695
821
|
if truncated != "drop":
|
|
696
822
|
marked: list[dict] = []
|
|
@@ -838,6 +964,7 @@ def select_for_rl(
|
|
|
838
964
|
"enforce_band": bool(enforce_band),
|
|
839
965
|
"band_groups_dropped": band_report["n_groups_dropped"],
|
|
840
966
|
"band_dropped": {"too_easy": band_report["too_easy"], "too_hard": band_report["too_hard"]},
|
|
967
|
+
"prior": prior_report,
|
|
841
968
|
"duplicates": dup_report,
|
|
842
969
|
"truncated_dropped": trunc_report["n_dropped"],
|
|
843
970
|
"truncated_policy": truncated,
|
|
@@ -57,6 +57,116 @@ def wilson_interval(successes: int, n: int, *, z: float = 1.96) -> tuple[float,
|
|
|
57
57
|
return (max(0.0, center - half), min(1.0, center + half))
|
|
58
58
|
|
|
59
59
|
|
|
60
|
+
def _z(p: float) -> float:
|
|
61
|
+
from statistics import NormalDist
|
|
62
|
+
|
|
63
|
+
return NormalDist().inv_cdf(p)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _paired_task_sd(base: float, effect: float, k: int) -> float:
|
|
67
|
+
"""Standard deviation of one task's paired difference (after minus
|
|
68
|
+
before pass rate over ``k`` rollouts each side) when the gain lands
|
|
69
|
+
uniformly: before at ``base``, after at ``base + effect``."""
|
|
70
|
+
p = min(1.0, max(0.0, float(base)))
|
|
71
|
+
q = min(1.0, max(0.0, p + float(effect)))
|
|
72
|
+
kk = max(1, int(k))
|
|
73
|
+
return math.sqrt((p * (1 - p) + q * (1 - q)) / kk)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _rows_base_and_k(rows: Sequence[dict]) -> tuple[float, int]:
|
|
77
|
+
"""Mean per-task pass rate and the smallest rollouts-per-task on graded
|
|
78
|
+
rows: what ``delta_report`` would pair on."""
|
|
79
|
+
groups: dict[str, list[float]] = {}
|
|
80
|
+
for row in rows:
|
|
81
|
+
if not isinstance(row, dict):
|
|
82
|
+
continue
|
|
83
|
+
value = _binary(row)
|
|
84
|
+
if value is None:
|
|
85
|
+
continue
|
|
86
|
+
groups.setdefault(task_key(row), []).append(value)
|
|
87
|
+
if not groups:
|
|
88
|
+
raise ValueError("rows carry no 0/1 rewards; grade them first, or pass base= and k=")
|
|
89
|
+
base = _mean([_mean(v) for v in groups.values()])
|
|
90
|
+
k = min(len(v) for v in groups.values())
|
|
91
|
+
return base, k
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def holdout_size(
|
|
95
|
+
effect: float,
|
|
96
|
+
*,
|
|
97
|
+
base: float = 0.6,
|
|
98
|
+
k: int = 4,
|
|
99
|
+
power: float = 0.8,
|
|
100
|
+
alpha: float = 0.05,
|
|
101
|
+
rows: Sequence[dict] | None = None,
|
|
102
|
+
) -> dict[str, Any]:
|
|
103
|
+
"""How many paired tasks a holdout needs to prove a gain of ``effect``.
|
|
104
|
+
|
|
105
|
+
Models the test ``delta_report`` runs: each task's pass rate over
|
|
106
|
+
``k`` rollouts on each side, the delta as the mean of the paired
|
|
107
|
+
differences, the interval from a bootstrap over tasks. A task's
|
|
108
|
+
difference then has standard deviation
|
|
109
|
+
``sqrt((p(1-p) + q(1-q)) / k)`` with ``p = base`` and ``q = base +
|
|
110
|
+
effect``, and the usual two-sided power calculation gives
|
|
111
|
+
``n = ((z_{1-alpha/2} + z_power) * sd / effect) ** 2`` (rlhf-book ch. 16,
|
|
112
|
+
appendix C: the eval's own variance decides what a delta can mean).
|
|
113
|
+
It assumes the gain lands uniformly across tasks; a gain concentrated
|
|
114
|
+
on a few tasks needs more.
|
|
115
|
+
|
|
116
|
+
``rows`` (graded before-side rows) reads ``base`` and ``k`` off the
|
|
117
|
+
data instead. Returns ``n_tasks`` plus the inputs, ``sd_task``, and
|
|
118
|
+
``half_width``: the 95% band on the delta at that ``n``.
|
|
119
|
+
|
|
120
|
+
The recipe that asked for this had 140 tasks at k=4 around 0.6: a
|
|
121
|
+
band of about +-0.06, so a real 3-point gain reads
|
|
122
|
+
``no_change_detected`` every round. This says so before training.
|
|
123
|
+
"""
|
|
124
|
+
if not 0 < float(effect) < 1:
|
|
125
|
+
raise ValueError(
|
|
126
|
+
"effect is the gain in pass rate to prove, between 0 and 1 (0.05 = 5 points)"
|
|
127
|
+
)
|
|
128
|
+
if not 0 < power < 1 or not 0 < alpha < 1:
|
|
129
|
+
raise ValueError("power and alpha are probabilities strictly between 0 and 1")
|
|
130
|
+
if rows is not None:
|
|
131
|
+
base, k = _rows_base_and_k(rows)
|
|
132
|
+
sd = _paired_task_sd(base, effect, k)
|
|
133
|
+
z = _z(1 - alpha / 2) + _z(power)
|
|
134
|
+
n = math.ceil((z * sd / float(effect)) ** 2) if sd > 0 else 1
|
|
135
|
+
n = max(n, 2)
|
|
136
|
+
return {
|
|
137
|
+
"n_tasks": n,
|
|
138
|
+
"effect": float(effect),
|
|
139
|
+
"base": float(base),
|
|
140
|
+
"k": int(k),
|
|
141
|
+
"power": float(power),
|
|
142
|
+
"alpha": float(alpha),
|
|
143
|
+
"sd_task": round(sd, 4),
|
|
144
|
+
"half_width": round(_z(1 - alpha / 2) * sd / math.sqrt(n), 4),
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def detectable_effect(
|
|
149
|
+
n_tasks: int,
|
|
150
|
+
*,
|
|
151
|
+
base: float = 0.6,
|
|
152
|
+
k: int = 4,
|
|
153
|
+
power: float = 0.8,
|
|
154
|
+
alpha: float = 0.05,
|
|
155
|
+
) -> float | None:
|
|
156
|
+
"""The smallest gain ``n_tasks`` paired tasks can prove at ``power``:
|
|
157
|
+
``holdout_size`` solved for the effect (a few fixed-point steps, since
|
|
158
|
+
the after-side variance depends on it). ``None`` below two tasks."""
|
|
159
|
+
n = int(n_tasks)
|
|
160
|
+
if n < 2:
|
|
161
|
+
return None
|
|
162
|
+
z = _z(1 - alpha / 2) + _z(power)
|
|
163
|
+
effect = 0.0
|
|
164
|
+
for _ in range(12):
|
|
165
|
+
sd = _paired_task_sd(base, effect, k)
|
|
166
|
+
effect = z * sd / math.sqrt(n)
|
|
167
|
+
return round(min(1.0, effect), 4)
|
|
168
|
+
|
|
169
|
+
|
|
60
170
|
def bootstrap_ci(
|
|
61
171
|
values: Sequence[float],
|
|
62
172
|
*,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|