Skip to content

Commit e2bba46

Browse files
committed
feat(regen): make every registry preset usable on-policy
The regen script previously accepted only presets with a bare-prompt column (magpie/ultrachat/gsm8k) — a historical artifact of its Magpie-specific origin, not an architectural limit. Extraction now mirrors the off-policy ingestion via prepare_row(): filter_fn sees the raw row, normalize_fn is merged over it with HF map semantics, and system/user turns are extracted from the result. Every DATASET_CONFIGS preset is therefore a valid --dataset choice, and any dataset that works off-policy works on-policy by construction (nemotron's input/output schema, sharegpt's native conversations, ...). Resume identity is untouched: _primary_identifier still hashes the raw row, so --resume stays valid for outputs from earlier runs. For the three previous presets the extracted turns are unchanged (gsm8k's normalizer yields the same single user turn its prompt_field fallback did); prompt_field remains as a per-row fallback for rows whose conversation is unusable. Known limits deferred to follow-ups rather than gated: sharegpt4v_coco needs an image-capable endpoint and local COCO files (same requirement and failure mode as off-policy), and tool-use datasets get plain-text regeneration until tool-call regen lands. Signed-off-by: Ranran Haoran Zhang <ranzhang@redhat.com>
1 parent c2b8e8d commit e2bba46

3 files changed

Lines changed: 60 additions & 21 deletions

File tree

scripts/response_regeneration/script.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,11 @@
1919
with_retries,
2020
)
2121

22-
# Presets usable for regeneration: those defining a bare-prompt fallback column.
23-
REGEN_DATASETS = [
24-
name for name, config in DATASET_CONFIGS.items() if config.prompt_field
25-
]
26-
2722

2823
def parse_args():
2924
"""Parse command-line arguments for the script."""
3025
parser = argparse.ArgumentParser(
31-
description="Regenerate responses from Magpie instructions via vLLM Chat API."
26+
description="Regenerate dataset responses via a vLLM Chat API endpoint."
3227
)
3328
parser.add_argument(
3429
"--endpoint",
@@ -43,7 +38,7 @@ def parse_args():
4338
parser.add_argument(
4439
"--dataset",
4540
default="ultrachat",
46-
choices=REGEN_DATASETS,
41+
choices=list(DATASET_CONFIGS),
4742
help="Dataset to process",
4843
)
4944
parser.add_argument(
@@ -146,6 +141,19 @@ def extract_turns(row, prompt_field):
146141
return []
147142

148143

144+
def prepare_row(row, config):
145+
"""Extract regeneration turns from a raw dataset row, ``[]`` to skip it.
146+
147+
Mirrors off-policy ingestion: ``filter_fn`` sees the raw row, and
148+
``normalize_fn`` is merged over it (HF ``map`` semantics keep raw columns).
149+
"""
150+
if config.filter_fn is not None and not config.filter_fn(row):
151+
return []
152+
if config.normalize_fn is not None:
153+
row = {**row, **config.normalize_fn(row)}
154+
return extract_turns(row, config.prompt_field)
155+
156+
149157
def _is_present(value: Any) -> bool:
150158
"""Return True for a usable identifier (not None / not empty string)."""
151159
return value not in (None, "")
@@ -412,7 +420,6 @@ async def main():
412420
# Get dataset configuration
413421
dataset_config = DATASET_CONFIGS[args.dataset]
414422
dataset_id = dataset_config.hf_path
415-
prompt_field = dataset_config.prompt_field
416423

417424
# Use dataset-specific defaults if not provided
418425
split = args.split if args.split is not None else dataset_config.split
@@ -431,7 +438,7 @@ async def main():
431438

432439
print(f"Using dataset: {dataset_id}")
433440
print(f"Split: {split}")
434-
print(f"Prompt field: {prompt_field}")
441+
print(f"Prompt field: {dataset_config.prompt_field}")
435442
print(f"Output file: {args.outfile}")
436443
print(f"Error file: {error_outfile}")
437444
print()
@@ -488,8 +495,8 @@ async def main():
488495
if args.language_filter and row.get("language") != args.language_filter:
489496
continue
490497

491-
turns = extract_turns(row, prompt_field)
492-
# extract_turns returns [] when there is no usable user turn.
498+
turns = prepare_row(row, dataset_config)
499+
# prepare_row returns [] for filtered rows and unusable turns.
493500
if not turns:
494501
continue
495502

src/speculators/data_generation/configs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class DatasetConfig:
2020
split: str
2121
filter_fn: Callable[[dict], bool] | None = None
2222
normalize_fn: Callable[[dict], dict] | None = None
23-
# Bare user-prompt column; presets defining it are regen --dataset choices.
23+
# Bare user-prompt fallback column for the response-regeneration script.
2424
prompt_field: str | None = None
2525

2626

tests/unit/scripts/test_response_regeneration.py

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import pytest
1616

1717
from speculators.data_generation import vllm_client
18-
from speculators.data_generation.configs import DATASET_CONFIGS
18+
from speculators.data_generation.configs import DATASET_CONFIGS, DatasetConfig
1919
from speculators.data_generation.preprocessing import _preprocess_batch
2020
from speculators.data_generation.vllm_client import InvalidResponseError
2121

@@ -531,17 +531,49 @@ def test_worker_row_identity_and_all_or_nothing_writes(tmp_path):
531531

532532

533533
# ---------------------------------------------------------------------------
534-
# 6. Dataset presets come from the shared registry.
534+
# 6. Every shared-registry preset works on-policy (off-policy parity).
535535
# ---------------------------------------------------------------------------
536536

537537

538-
def test_regen_presets_come_from_shared_registry():
538+
def test_script_consumes_shared_registry():
539539
# Identity, not equality: a re-declared copy in the script would drift.
540540
assert regen.DATASET_CONFIGS is DATASET_CONFIGS
541-
# The original CLI presets stay regen-eligible, with their fallback columns.
542-
eligible = {
543-
name: DATASET_CONFIGS[name].prompt_field for name in regen.REGEN_DATASETS
541+
542+
543+
def test_prepare_row_normalizes_like_off_policy():
544+
# nemotron rows only become extractable through the preset's normalize_fn.
545+
row = {
546+
"input": [{"role": "user", "content": "Hi"}],
547+
"output": "<original answer to drop>",
544548
}
545-
assert eligible["magpie"] == "instruction"
546-
assert eligible["ultrachat"] == "prompt"
547-
assert eligible["gsm8k"] == "question"
549+
assert regen.prepare_row(row, DATASET_CONFIGS["nemotron"]) == [
550+
{"role": "user", "content": "Hi"}
551+
]
552+
553+
554+
def test_prepare_row_applies_filter_fn():
555+
config = DatasetConfig(
556+
name="t",
557+
hf_path="t",
558+
split="train",
559+
filter_fn=lambda row: row["keep"],
560+
)
561+
row = {"keep": False, "conversations": [{"role": "user", "content": "Hi"}]}
562+
assert regen.prepare_row(row, config) == []
563+
assert regen.prepare_row(row | {"keep": True}, config) == [
564+
{"role": "user", "content": "Hi"}
565+
]
566+
567+
568+
def test_prepare_row_merges_normalize_output_over_raw_row():
569+
# HF map merges columns: normalize output must not clobber the raw fallback.
570+
config = DatasetConfig(
571+
name="t",
572+
hf_path="t",
573+
split="train",
574+
normalize_fn=lambda row: {"conversations": []},
575+
prompt_field="prompt",
576+
)
577+
assert regen.prepare_row({"prompt": "Hi"}, config) == [
578+
{"role": "user", "content": "Hi"}
579+
]

0 commit comments

Comments
 (0)