Skip to content

[data] feat: support assistant turn loss mask - #89

Open
sugarshaw95 wants to merge 2 commits into
baidu-baige:masterfrom
sugarshaw95:feat/assistant-turn-loss-mask
Open

[data] feat: support assistant turn loss mask#89
sugarshaw95 wants to merge 2 commits into
baidu-baige:masterfrom
sugarshaw95:feat/assistant-turn-loss-mask

Conversation

@sugarshaw95

@sugarshaw95 sugarshaw95 commented Jun 15, 2026

Copy link
Copy Markdown

Summary

  • Add assistant message-level loss_mask support in HFChatTemplate.encode_openai()
  • Define assistant semantics: loss_mask=1 keeps that assistant turn trainable, loss_mask=0 masks it out
  • Preserve existing behavior when no assistant message has explicit loss_mask
  • Allow loss_mask on non-assistant messages for uniform input schemas; these fields are ignored because only assistant messages map to HF generation ranges
  • Validate explicit assistant masks against HuggingFace generation ranges to avoid silent turn/span misalignment
  • Reject conflicts between explicit assistant masks and train_on_prompt / history_mask_loss

Validation

  • compile(..., "loongforge/data/chat_template.py", "exec")
  • CPU helper checks for default assistant behavior, explicit 0/1/string mask parsing, invalid assistant values, ignored non-assistant masks, and alternating generation-span filtering
  • Local CPU tokenizer validation on a ReAct/tool-calling sample confirmed only the final assistant answer remains trainable (assistant_flags=[False, False, True])

@nullnonenilNULL

Copy link
Copy Markdown
Collaborator

@claude review this PR

@claude

claude Bot commented Jun 15, 2026

Copy link
Copy Markdown

Claude finished @nullnonenilNULL's task in 2m 8s —— View job


Verdict: COMMENT

Summary

This PR adds per-assistant-turn loss_mask support to HFChatTemplate.encode_openai(). The core approach is sound: parse the flag per message, align it against HuggingFace generation ranges, and filter out masked spans before building the token-level mask. The validation against train_on_prompt/history_mask_loss conflicts and the count-mismatch guard are good defensive additions. One behavioral edge case around partial labeling warrants discussion before merge.

🟠 Major

  • loongforge/data/chat_template.py:616 — Partial-label behavior is unspecified: when only some assistant messages carry loss_mask, unlabelled turns silently default to True while has_explicit_assistant_loss_mask is set, triggering the validation path. Callers may not realize they're training on unannotated turns. Either enforce all-or-nothing labeling, or document this "missing = train" default explicitly in the mixed case.

🟡 Minor

  • loongforge/data/chat_template.py:575float inputs (e.g. 1.0) fall through to ValueError without a type-aware message. Intentional rejection of floats should be noted in the docstring.
  • loongforge/data/chat_template.py:755 — Conflict guards for train_on_prompt/history_mask_loss run after full tokenization, wasting work on every invalid call. Move them before _tokenize_with_generation_indices.

🟢 Nits

  • loongforge/data/chat_template.py:626 — Docstring for _tokenize_with_generation_indices doesn't mention the new bool third return value.

Tests

No automated tests are in the diff. The PR description mentions manual CPU helper checks and a local tokenizer validation, but these aren't committed. Given the partial-label edge case above, at least one test covering mixed explicit/implicit assistant masks would be valuable.

Checklist

Area Status Notes
A. Correctness COMMENT Partial-label silent behavior
B. Security PASS N/A
C. API design PASS Backward-compatible; encode_openai return shape unchanged
D. Performance LOW Minor: conflict check runs after tokenization
E. Reliability/obs. PASS Count-mismatch error is clear
F. Testing COMMENT No committed tests for new paths
G. Readability PASS Clean helper decomposition
H. Documentation COMMENT _tokenize_with_generation_indices docstring needs updating
I. Conventions PASS Follows existing patterns
J. Repo hygiene PASS Single file, no artifacts

cls._message_loss_mask_to_bool(message["loss_mask"])
)
else:
assistant_loss_flags.append(True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Major: Mixed-explicit semantics create a silent training gap.

When only some assistant messages carry loss_mask (and others don't), the unlabelled turns default to True (append(True) at line 616). This means has_explicit_assistant_loss_mask is set to True, triggering the length-check and the filter — but the unlabelled turns silently inherit "train on this turn", which may not be what the author intended when providing a partial mask.

The PR description says "Preserve existing behavior when no assistant message has explicit loss_mask", but it doesn't address the partial case (some turns labelled, some not). The current behavior is all-or-nothing is not enforced: a dataset with one masked turn and one un-annotated turn will pass validation and silently train on the unannotated turn.

Suggested fix: either (a) require that if any assistant message carries loss_mask, all assistant messages must carry it (raise if the counts differ before the length check), or (b) document this partial-label behavior explicitly in the docstring so callers understand the "missing = True" default applies even in mixed data.

"""
if isinstance(value, bool):
return value
if isinstance(value, int) and value in (0, 1):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Minor: _message_loss_mask_to_bool accepts any int value of 0 or 1, but silently rejects other integers (e.g. 2) by falling through to the ValueError. That is correct, but the isinstance(value, int) branch also matches bool values since bool is a subclass of int in Python — however this is harmless here because the isinstance(value, bool) check comes first.

More importantly, a float like 1.0 or 0.0 will fall through to the ValueError. Whether this is intentional should be documented or validated more explicitly. If floats should be rejected, a note in the docstring would prevent future surprises.

)
)
ori_total_len = len(input_ids)
if has_explicit_loss_mask and train_on_prompt:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Minor: The conflict check fires after _tokenize_with_generation_indices has already done the tokenization work (rendering, aligning masks, etc.). For train_on_prompt / history_mask_loss, that work is wasted. Moving the conflict guard before calling _tokenize_with_generation_indices would save time and make it obvious that these options are mutually exclusive at the call site — not a correctness issue, just a needless cost on every conflicting call.

tools: Optional[Sequence[Dict[str, Any]]] = None,
) -> Tuple[List[int], List[int]]:
) -> Tuple[List[int], List[int], bool]:
"""Render OpenAI chat messages and build assistant-token masks."""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Nit: The returned bool third element of the tuple now has a different meaning from the first two elements (it's a metadata flag, not token data). The return type annotation Tuple[List[int], List[int], bool] at line 625 is correct but the docstring still says only "Render OpenAI chat messages and build assistant-token masks" — it doesn't mention the new third return value. Worth a one-line update for future readers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants