jyliuAI's picture
Add files using upload-large-folder tool
784f572 verified
Raw History Blame Contribute Delete
6 kB
"""CLI for atomically evaluating canonical sample JSONL artifacts."""
from __future__ import annotations
import argparse
from dataclasses import asdict
import json
from pathlib import Path
from typing import Sequence
from dlb.io import atomic_json_write, sha256_file, validate_samples
from dlb.schema import SampleRecord
from .generative_perplexity import (
compute_gen_ppl,
load_offline_gpt2_large,
resolve_gpt2_assets,
)
from .self_bleu import SelfBleuConfig, compute_self_bleu
from .unigram_entropy import mean_unigram_entropy
PRODUCTION_SAMPLE_COUNT = 1024
METRIC_NAMES = ("gen_ppl", "entropy", "self_bleu")
DATASET_PADDING_IDS = {"lm1b": frozenset({0}), "owt": frozenset()}
def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--samples", type=Path, required=True)
parser.add_argument("--metrics", default=",".join(METRIC_NAMES))
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--dataset", choices=tuple(DATASET_PADDING_IDS))
parser.add_argument("--special-id", type=int, action="append", default=[])
parser.add_argument("--batch-size", type=int, default=8)
parser.add_argument("--context-length", type=int, default=1024)
parser.add_argument("--device", default="cuda")
parser.add_argument(
"--allow-partial",
action="store_true",
help="allow a non-production sample count and label the artifact partial",
)
return parser.parse_args(argv)
def _metrics(value: str) -> tuple[str, ...]:
names = tuple(part.strip() for part in value.split(",") if part.strip())
if not names:
raise ValueError("at least one metric is required")
unknown = sorted(set(names) - set(METRIC_NAMES))
if unknown:
raise ValueError(f"unknown metrics: {', '.join(unknown)}")
if len(set(names)) != len(names):
raise ValueError("metric names must not be duplicated")
return names
def _load_records(path: Path) -> list[SampleRecord]:
records = []
with path.open("r", encoding="utf-8") as source:
for line in source:
records.append(SampleRecord.model_validate(json.loads(line)))
return records
def _display_path(path: Path, root: Path) -> str:
try:
return path.resolve().relative_to(root.resolve()).as_posix()
except ValueError:
return path.resolve().as_posix()
def evaluate(args: argparse.Namespace) -> dict[str, object]:
root = args.root.resolve()
metric_names = _metrics(args.metrics)
expected = None if args.allow_partial else PRODUCTION_SAMPLE_COUNT
sample_count = validate_samples(args.samples, expected=expected)
if sample_count < 1:
raise ValueError("sample artifact is empty")
partial = sample_count != PRODUCTION_SAMPLE_COUNT
if partial and not args.allow_partial:
raise ValueError("partial input requires --allow-partial")
if not partial and args.special_id:
raise ValueError(
"production evaluation forbids --special-id; dataset padding policy is fixed"
)
if not partial and "entropy" in metric_names and args.dataset is None:
raise ValueError("production entropy evaluation requires --dataset")
records = _load_records(args.samples)
texts = [record.text for record in records]
result_metrics: dict[str, object] = {}
if "gen_ppl" in metric_names:
assets = resolve_gpt2_assets(root)
model, tokenizer = load_offline_gpt2_large(assets, device=args.device)
ppl = compute_gen_ppl(
texts,
model,
tokenizer,
batch_size=args.batch_size,
max_length=args.context_length,
model_revision=assets.model_revision,
tokenizer_revision=assets.tokenizer_revision,
device=args.device,
)
result_metrics["generative_perplexity"] = {
**asdict(ppl),
"model_snapshot": _display_path(assets.model_path, root),
"tokenizer_snapshot": _display_path(assets.tokenizer_path, root),
"offline": True,
}
excluded = set()
exclusion_source = "none_partial"
special_token_policy = "partial_no_token_exclusions"
if args.dataset is not None:
excluded.update(DATASET_PADDING_IDS[args.dataset])
exclusion_source = "dataset_contract"
special_token_policy = "exclude_documented_padding_only_preserve_bos_eos"
if partial and args.special_id:
excluded.update(args.special_id)
exclusion_source = "custom_partial_cli"
special_token_policy = "custom_partial_not_baseline_comparable"
if "entropy" in metric_names:
entropy = mean_unigram_entropy(records, excluded)
result_metrics["unigram_entropy"] = {
**asdict(entropy),
"special_token_policy": special_token_policy,
"exclusion_source": exclusion_source,
}
if "self_bleu" in metric_names:
result_metrics["self_bleu"] = asdict(
compute_self_bleu(texts, SelfBleuConfig())
)
return {
"schema_version": 1,
"samples": _display_path(args.samples, root),
"samples_sha256": sha256_file(args.samples),
"sample_count": sample_count,
"production_sample_count": PRODUCTION_SAMPLE_COUNT,
"partial": partial,
"dataset": args.dataset,
"requested_metrics": list(metric_names),
"metrics": result_metrics,
}
def main(argv: Sequence[str] | None = None) -> int:
args = parse_args(argv)
document = evaluate(args)
atomic_json_write(args.output, document)
print(
f"WROTE {args.output} samples={document['sample_count']} "
f"partial={str(document['partial']).lower()}"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())