Skip to content

Commit 0071955

Browse files
committed
fix(data): align GLM Kimi and MiniMax chat masks
Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
1 parent 364e33a commit 0071955

2 files changed

Lines changed: 455 additions & 28 deletions

File tree

‎src/megatron/bridge/data/conversation_processing.py‎

Lines changed: 112 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ class AssistantMaskBoundaryConfig:
7777
``role_end_token_variants`` provides ordered fallback end sequences for
7878
formats whose final turn may omit a trailing delimiter such as a
7979
newline. The primary ``role_end_tokens`` sequence is always preferred.
80+
``rendered_turn_end_roles`` covers formats such as GLM-5.2 whose
81+
assistant turn has no explicit end token. For those roles, the
82+
conversation-prefix render determines the exclusive content boundary.
8083
8184
Example::
8285
@@ -105,6 +108,7 @@ class AssistantMaskBoundaryConfig:
105108
trim_leading_token_ids: Sequence[int] = ()
106109
trim_leading_token_sequences: Sequence[Sequence[int]] = ()
107110
role_end_token_variants: Mapping[str, Sequence[Sequence[int]]] = field(default_factory=dict)
111+
rendered_turn_end_roles: Sequence[str] = ()
108112

109113

110114
@dataclass(frozen=True)
@@ -491,7 +495,9 @@ def resolve_chat_template_kwargs(template_owner: Any, template_kwargs: Mapping[s
491495
492496
MBridge exposes ``truncate_history_thinking`` consistently. Templates that
493497
implement the inverse ``preserve_thinking`` control receive the equivalent
494-
negated value only at the rendering boundary.
498+
negated value only at the rendering boundary, while GLM-style
499+
``clear_thinking`` receives the same value. Custom tokenizers such as
500+
Kimi-K3 use ``thinking`` instead of ``enable_thinking``.
495501
496502
Args:
497503
template_owner: Processor or tokenizer that owns the chat template.
@@ -501,13 +507,29 @@ def resolve_chat_template_kwargs(template_owner: Any, template_kwargs: Mapping[s
501507
A copied mapping adapted to the selected template.
502508
"""
503509
kwargs = dict(template_kwargs)
504-
if "truncate_history_thinking" not in kwargs:
505-
return kwargs
506510
tokenizer = get_processor_tokenizer(template_owner)
507511
template = _get_chat_template(template_owner, tokenizer)
508-
if template is not None and "preserve_thinking" in template and "truncate_history_thinking" not in template:
509-
truncate_history = kwargs.pop("truncate_history_thinking")
510-
kwargs["preserve_thinking"] = not truncate_history
512+
513+
if "truncate_history_thinking" in kwargs and (template is None or "truncate_history_thinking" not in template):
514+
truncate_history = kwargs["truncate_history_thinking"]
515+
if template is not None and "preserve_thinking" in template:
516+
kwargs.pop("truncate_history_thinking")
517+
kwargs["preserve_thinking"] = not truncate_history
518+
elif template is not None and "clear_thinking" in template:
519+
kwargs.pop("truncate_history_thinking")
520+
kwargs["clear_thinking"] = truncate_history
521+
522+
if "enable_thinking" in kwargs and (template is None or "enable_thinking" not in template):
523+
parameters: set[str] = set()
524+
for owner in (template_owner, tokenizer):
525+
apply_chat_template = _get_explicit_attribute(owner, "apply_chat_template")
526+
try:
527+
parameters.update(inspect.signature(apply_chat_template).parameters if apply_chat_template else {})
528+
except (TypeError, ValueError):
529+
continue
530+
if "thinking" in parameters and "enable_thinking" not in parameters:
531+
enable_thinking = kwargs.pop("enable_thinking")
532+
kwargs.setdefault("thinking", enable_thinking)
511533
return kwargs
512534

513535

@@ -852,7 +874,12 @@ def tokenize_chat_example(
852874
)
853875

854876
chat_example: dict[str, Any] = {"conversation": conversation}
855-
chat_example.update(template_kwargs)
877+
mask_template_kwargs = dict(template_kwargs)
878+
mask_tools = mask_template_kwargs.pop("tools", None)
879+
if mask_template_kwargs:
880+
chat_example["chat_template_kwargs"] = mask_template_kwargs
881+
if mask_tools is not None:
882+
chat_example["tools"] = mask_tools
856883
effective_boundary_config = boundary_config or infer_assistant_mask_boundary_config(processor)
857884
if returned_mask is not None and effective_boundary_config is None:
858885
assistant_mask = returned_mask
@@ -1037,9 +1064,42 @@ def infer_assistant_mask_boundary_config(processor: Any) -> AssistantMaskBoundar
10371064
"""Infer assistant role boundaries from known HF chat-template formats."""
10381065
tokenizer = get_processor_tokenizer(processor)
10391066
template = _get_chat_template(tokenizer, processor)
1067+
10401068
if template is None:
1069+
# Kimi-K3 owns rendering in a custom tokenizer rather than a Jinja
1070+
# ``chat_template``. Detect its XTML structural vocabulary instead of a
1071+
# Python class name so local/remote-code wrappers retain the same behavior.
1072+
apply_chat_template = _get_explicit_attribute(tokenizer, "apply_chat_template")
1073+
try:
1074+
template_parameters = inspect.signature(apply_chat_template).parameters if apply_chat_template else {}
1075+
except (TypeError, ValueError):
1076+
template_parameters = {}
1077+
if "thinking" in template_parameters:
1078+
get_vocab = _get_explicit_attribute(tokenizer, "get_vocab")
1079+
try:
1080+
vocab = get_vocab() if callable(get_vocab) else {}
1081+
except (AttributeError, TypeError, ValueError):
1082+
vocab = {}
1083+
kimi_markers = ("<|open|>", "<|close|>", "<|sep|>", "<|end_of_msg|>")
1084+
if isinstance(vocab, Mapping) and all(marker in vocab for marker in kimi_markers):
1085+
return assistant_mask_boundary_config_from_markers(
1086+
processor,
1087+
assistant_start='<|open|>message role="assistant"<|sep|>',
1088+
assistant_end="<|close|>message<|sep|><|end_of_msg|>",
1089+
)
10411090
return None
10421091

1092+
# GLM-5.2 does not terminate completed assistant turns with a structural
1093+
# token. Prefix rendering is therefore the only loss boundary that remains
1094+
# exact for both terminal and historical turns.
1095+
if all(marker in template for marker in ("<|assistant|>", "clear_thinking", "<think>")):
1096+
return assistant_mask_boundary_config_from_markers(
1097+
processor,
1098+
assistant_start="<|assistant|>",
1099+
assistant_end=None,
1100+
rendered_turn_end_roles=(CHATML_ASSISTANT_ROLE,),
1101+
)
1102+
10431103
candidates: tuple[tuple[tuple[str, ...], str, str, tuple[str, ...], Mapping[str, str]], ...] = (
10441104
(
10451105
("<|im_start|>assistant", "<|im_end|>"),
@@ -1086,6 +1146,18 @@ def infer_assistant_mask_boundary_config(processor: Any) -> AssistantMaskBoundar
10861146
("<|end|>", "<|call|>"),
10871147
{role: f"<|start|>{role}" for role in ("system", "developer", "user", "tool")},
10881148
),
1149+
(
1150+
("]~!b[", "]~b]", "[e~[", "<mm:think>"),
1151+
"]~b]ai\n",
1152+
"[e~[\n",
1153+
(),
1154+
{
1155+
"system": "]~!b[]~b]system\n",
1156+
"developer": "]~b]developer\n",
1157+
"user": "]~b]user\n",
1158+
"tool": "]~b]tool",
1159+
},
1160+
),
10891161
)
10901162
for required_markers, assistant_start, assistant_end, assistant_end_fallbacks, other_role_starts in candidates:
10911163
if not all(marker in template for marker in required_markers):
@@ -1118,25 +1190,26 @@ def assistant_mask_boundary_config_from_markers(
11181190
processor: Any,
11191191
*,
11201192
assistant_start: str,
1121-
assistant_end: str,
1193+
assistant_end: str | None,
11221194
assistant_end_fallbacks: Sequence[str] = (),
11231195
role_start_markers: Mapping[str, str] | None = None,
11241196
loss_roles: Sequence[str] = (CHATML_ASSISTANT_ROLE,),
11251197
include_start_tokens_for_roles: Sequence[str] = (),
11261198
include_end_tokens_for_roles: Sequence[str] = (CHATML_ASSISTANT_ROLE,),
11271199
trim_leading_token_ids: Sequence[int] = (),
11281200
trim_leading_token_sequences: Sequence[Sequence[int]] = (),
1201+
rendered_turn_end_roles: Sequence[str] = (),
11291202
) -> AssistantMaskBoundaryConfig:
11301203
"""Build explicit assistant boundary config from model-declared marker strings."""
11311204
tokenizer = get_processor_tokenizer(processor)
11321205
start_tokens = tokenize_text_without_special_tokens(tokenizer, assistant_start)
1133-
end_tokens = tokenize_text_without_special_tokens(tokenizer, assistant_end)
1206+
end_tokens = tokenize_text_without_special_tokens(tokenizer, assistant_end) if assistant_end is not None else []
11341207
end_token_variants = [
11351208
token_ids
11361209
for fallback in assistant_end_fallbacks
11371210
if (token_ids := tokenize_text_without_special_tokens(tokenizer, fallback))
11381211
]
1139-
if not start_tokens or not end_tokens:
1212+
if not start_tokens or (not end_tokens and CHATML_ASSISTANT_ROLE not in rendered_turn_end_roles):
11401213
raise ValueError(
11411214
"Unable to tokenize assistant loss-mask boundary markers. "
11421215
"Check the model collator's assistant_start and assistant_end markers."
@@ -1149,13 +1222,14 @@ def assistant_mask_boundary_config_from_markers(
11491222
role_start_tokens[role] = marker_tokens
11501223
return AssistantMaskBoundaryConfig(
11511224
role_start_tokens=role_start_tokens,
1152-
role_end_tokens={role: end_tokens for role in role_start_tokens},
1225+
role_end_tokens={role: end_tokens for role in role_start_tokens if role not in rendered_turn_end_roles},
11531226
loss_roles=loss_roles,
11541227
include_start_tokens_for_roles=include_start_tokens_for_roles,
11551228
include_end_tokens_for_roles=include_end_tokens_for_roles,
11561229
trim_leading_token_ids=trim_leading_token_ids,
11571230
trim_leading_token_sequences=trim_leading_token_sequences,
11581231
role_end_token_variants={role: end_token_variants for role in role_start_tokens},
1232+
rendered_turn_end_roles=rendered_turn_end_roles,
11591233
)
11601234

11611235

@@ -1308,13 +1382,14 @@ def _assistant_mask_from_conversation_turns(
13081382
}
13091383
include_start_roles = set(boundary_config.include_start_tokens_for_roles)
13101384
include_end_roles = set(boundary_config.include_end_tokens_for_roles)
1385+
rendered_turn_end_roles = set(boundary_config.rendered_turn_end_roles)
13111386
rendered_mask = torch.zeros(len(rendered_ids), dtype=torch.float32)
13121387

13131388
for turn_index, turn in loss_turns:
13141389
role = turn["role"]
13151390
start_tokens = role_start_tokens.get(role)
13161391
end_patterns = [role_end_tokens.get(role, []), *role_end_token_variants.get(role, [])]
1317-
if not start_tokens or not any(end_patterns):
1392+
if not start_tokens or (not any(end_patterns) and role not in rendered_turn_end_roles):
13181393
return None
13191394

13201395
try:
@@ -1344,20 +1419,23 @@ def _assistant_mask_from_conversation_turns(
13441419
if role_start < 0 or content_start > turn_limit:
13451420
return None
13461421

1347-
candidate_ends: list[tuple[int, int, int, list[int]]] = []
1348-
for priority, pattern in enumerate(end_patterns):
1349-
if not pattern:
1350-
continue
1351-
search_start = content_start
1352-
while search_start < turn_limit:
1353-
end_start, after_end = find_token_span(rendered_ids, pattern, search_start)
1354-
if end_start < 0 or after_end > turn_limit:
1355-
break
1356-
candidate_ends.append((end_start, -priority, after_end, pattern))
1357-
search_start = end_start + 1
1358-
if not candidate_ends:
1359-
return None
1360-
content_end, _, after_end, _ = max(candidate_ends)
1422+
if role in rendered_turn_end_roles:
1423+
content_end = after_end = turn_limit
1424+
else:
1425+
candidate_ends: list[tuple[int, int, int, list[int]]] = []
1426+
for priority, pattern in enumerate(end_patterns):
1427+
if not pattern:
1428+
continue
1429+
search_start = content_start
1430+
while search_start < turn_limit:
1431+
end_start, after_end = find_token_span(rendered_ids, pattern, search_start)
1432+
if end_start < 0 or after_end > turn_limit:
1433+
break
1434+
candidate_ends.append((end_start, -priority, after_end, pattern))
1435+
search_start = end_start + 1
1436+
if not candidate_ends:
1437+
return None
1438+
content_end, _, after_end, _ = max(candidate_ends)
13611439

13621440
loss_start = _trim_leading_token_sequences(
13631441
rendered_ids,
@@ -1375,7 +1453,7 @@ def _assistant_mask_from_conversation_turns(
13751453
rendered_mask[loss_start:content_end] = 1.0
13761454
if role in include_start_roles:
13771455
rendered_mask[role_start : role_start + len(start_tokens)] = 1.0
1378-
if role in include_end_roles:
1456+
if role in include_end_roles and role not in rendered_turn_end_roles:
13791457
rendered_mask[content_end:after_end] = 1.0
13801458

13811459
aligned_mask = _align_assistant_mask_to_padded_ids(rendered_ids, rendered_mask.tolist(), ids, tokenizer)
@@ -1535,6 +1613,12 @@ def _assistant_mask_from_boundary_config(
15351613
for role, raw_variants in boundary_config.role_end_token_variants.items()
15361614
}
15371615
loss_roles = set(boundary_config.loss_roles)
1616+
rendered_turn_end_roles = set(boundary_config.rendered_turn_end_roles)
1617+
if rendered_turn_end_roles:
1618+
# A raw token scan cannot distinguish the end of an unterminated turn
1619+
# from following non-loss roles. Only the conversation-prefix renderer
1620+
# can establish this boundary safely.
1621+
return None
15381622
if not role_start_tokens or not role_end_tokens or not loss_roles:
15391623
raise ValueError(
15401624
"AssistantMaskBoundaryConfig must provide non-empty role_start_tokens, role_end_tokens, and loss_roles."
@@ -1641,7 +1725,7 @@ def build_assistant_loss_mask(
16411725

16421726
mask = _assistant_mask_from_hf_chat_template(example_or_conversation, ids, processor, tokenizer)
16431727
if boundary_config is not None:
1644-
boundary_scan_is_safe = (
1728+
boundary_scan_is_safe = not boundary_config.rendered_turn_end_roles and (
16451729
not conversation
16461730
or _conversation_contains_boundary_tokens(example_or_conversation, tokenizer, boundary_config) is False
16471731
)

0 commit comments

Comments
 (0)