层2/U3: SFTCollator 放开 prompt-only 模式(供 on-policy 生成)

data.py(对应 docs/03 §5 U3):
- 加 prompt_only 开关:True 时输出 prompts/prompt_attention_mask(不产 labels,
  由 U4 生成后重建);False 时 SFT 双预算路径逐字不变
- max_length 改可选:prompt-only 无总预算;SFT 模式缺它构造即报错
- 兑现 T3 为 on-policy 生成预留的口子;生成用左 padding(右边界对齐)

test_data.py:
- 新增 prompt_only 模式:返回 prompt 张量/不报错、左 padding、截断、剥末轮 assistant
- 回归守卫:SFT 模式仍拒绝 prompt-only 行("SFT 路径行为不变")

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-19 05:02:24 -04:00
parent de7f36828a
commit e5a28e8e77
2 changed files with 138 additions and 11 deletions
+59 -1
View File
@@ -233,11 +233,20 @@ def test_nothink模板注入空思考块():
assert text.endswith("<T></T>")
def test_prompt_only行显式报错():
def test_sft模式仍拒绝prompt_only行():
# 回归守卫(docs/03 §5 U3"SFT 路径行为不变"):SFT 模式下 prompt-only 行
# 仍是静默空训练闸门,必须报错——放开只发生在显式 prompt_only=True 模式
with pytest.raises(ValueError, match="prompt-only"):
make_collator()([row("没有答案的题")])
def test_sft模式缺max_length构造即报错():
with pytest.raises(ValueError, match="max_length"):
SFTCollator(
ToyTokenizer(), max_prompt_length=50
) # 非 prompt_only 却无 max_length
def test_左padding对齐():
collator = make_collator()
batch = collator([row("ab", "cd"), row("a", "c")])
@@ -258,3 +267,52 @@ def test_pad回退到eos():
max_prompt_length=50,
)
assert collator.pad_token_id == 7
# ---------------------------------------------------------------------------
# SFTCollatorprompt_only 模式(层 2 / U3
# ---------------------------------------------------------------------------
def make_prompt_collator(**kw):
defaults = dict(max_prompt_length=100, enable_thinking=False, prompt_only=True)
defaults.update(kw)
return SFTCollator(ToyTokenizer(), **defaults)
def test_prompt_only模式返回prompt张量且不报错():
# 层 2prompt-only 行是常态,不再报错;渲染带生成引导符供 generate 续写
collator = make_prompt_collator()
batch = collator([row("ab")])
expected = "[user]ab[assistant]<T></T>"
assert set(batch.keys()) == {"prompts", "prompt_attention_mask"}
assert batch["prompts"][0].tolist() == ids_of(expected)
assert batch["prompt_attention_mask"][0].tolist() == [1] * len(expected)
def test_prompt_only模式左padding对齐():
# 生成要求左 padding:短 prompt 在左侧补 pad,右边界对齐
collator = make_prompt_collator()
batch = collator([row("abc"), row("a")])
t = batch["prompts"].shape[1]
short_mask = batch["prompt_attention_mask"][1].tolist()
n_pad = t - short_mask.count(1)
assert n_pad > 0
assert short_mask[:n_pad] == [0] * n_pad # padding 在左
assert batch["prompts"][1].tolist()[:n_pad] == [0] * n_pad # pad_token_id=0
def test_prompt_only模式截断到prompt预算():
collator = make_prompt_collator(max_prompt_length=8)
batch = collator([row("很长的题目" * 20)])
assert batch["prompts"].shape[1] == 8 # 恰截到 max_prompt_length
def test_prompt_only模式剥掉末轮assistant():
# 若数据碰巧带了 assistant 轮,取生成前上下文(剥掉它再加生成引导符)
collator = make_prompt_collator()
batch = collator([row("q", "已有答案")])
expected = "[user]q[assistant]<T></T>" # 不含"已有答案"
assert batch["prompts"][0].tolist() == ids_of(expected)