feat: append EOS to response in IFD evaluation

- Add EOS token at end of response in both conditional and unconditional passes so model also predicts when response should end
- New --append_eos/--no-append_eos CLI flag (default: enabled) with graceful fallback when tokenizer has no EOS
This commit is contained in:
2026-07-28 22:22:59 +08:00
parent a95794d3db
commit db487ab48b
+35 -3
View File
@@ -9,10 +9,16 @@ v2 changelog:
- Same token set: unconditional pass prefixes resp with a plain-text sentinel
(default ``\\n``; use ``--sentinel_text ""`` for bos/pad fallback).
Both branches predict the identical N resp tokens.
Single-token answers (rl=1) are now supported.
- Single-token answers (rl=1) are now supported.
- ctx_len tracked in output
- skip_reason for None samples (no more silent None)
- --per_token for per-token IFD breakdown
v3 changelog:
- Append EOS at the end of response in both conditional and unconditional
passes (``--append_eos`` / ``--no-append_eos``, default: enabled).
The model now also predicts when the response should end, which is part
of instruction following. Falls back gracefully when tokenizer has no EOS.
"""
import argparse
@@ -237,6 +243,7 @@ def process_file(
sentinel_ids=None,
per_token=False,
max_samples=None,
eos_ids=None,
):
"""Score a single file, write per-sample JSONL, return summary stats."""
if device is None:
@@ -245,6 +252,11 @@ def process_file(
if sentinel_ids is None:
sentinel_ids = _resolve_sentinel_ids(tokenizer, "\n")
if eos_ids is None:
eos_ids = []
eos_len = len(eos_ids)
data = _load_items(input_file)
if max_samples and len(data) > max_samples:
@@ -267,7 +279,9 @@ def process_file(
ctx_text = "\n\n".join(m["content"] for m in item["messages"][:i])
ctx_ids = tokenizer.encode(ctx_text)
resp_ids = tokenizer.encode(msg["content"], add_special_tokens=False)
ctx_ids, resp_ids = _trim(ctx_ids, resp_ids, max_len)
ctx_ids, resp_ids = _trim(ctx_ids, resp_ids, max_len - eos_len)
if eos_ids and resp_ids and resp_ids[-1:] != eos_ids:
resp_ids = resp_ids + eos_ids
if ctx_ids and resp_ids:
turns.append((ctx_ids, resp_ids))
if not turns:
@@ -284,7 +298,7 @@ def process_file(
else:
ctx_ids = tokenizer.encode(item[instr_key], add_special_tokens=False)
resp_ids = tokenizer.encode(item[resp_key], add_special_tokens=False)
ctx_ids, resp_ids = _trim(ctx_ids, resp_ids, max_len)
ctx_ids, resp_ids = _trim(ctx_ids, resp_ids, max_len - eos_len)
if not ctx_ids or not resp_ids:
results.append(
{
@@ -294,6 +308,8 @@ def process_file(
}
)
continue
if eos_ids and resp_ids[-1:] != eos_ids:
resp_ids = resp_ids + eos_ids
buffer.append((item, [(ctx_ids, resp_ids)], "plain"))
if len(buffer) >= batch_size:
@@ -452,6 +468,11 @@ def main():
default=None,
help="Maximum number of samples per file (random subsample). Default: all.",
)
parser.add_argument(
"--append_eos/--no-append_eos",
default=True,
help="Append EOS token at the end of response in both passes (default: enabled).",
)
args = parser.parse_args()
if args.device is None:
@@ -466,6 +487,16 @@ def main():
sentinel_ids = _resolve_sentinel_ids(tokenizer, args.sentinel_text)
eos_ids = []
if args.append_eos:
eos_token_id = getattr(tokenizer, "eos_token_id", None)
if eos_token_id is not None:
eos_ids = [eos_token_id]
else:
print(
"Warning: --append_eos enabled but tokenizer has no EOS token; skipping."
)
input_files = _collect_input_files(args.input_path)
if not input_files:
print(f"No input files found at {args.input_path}")
@@ -493,6 +524,7 @@ def main():
sentinel_ids=sentinel_ids,
per_token=args.per_token,
max_samples=args.max_samples,
eos_ids=eos_ids,
)
all_stats[label] = stats