Skip to content

Commit 5898845

Browse files
committed
fix(data): support GLM-4.5V assistant masking
Signed-off-by: Chen Cui <chcui@nvidia.com>
1 parent bb7870f commit 5898845

2 files changed

Lines changed: 114 additions & 8 deletions

File tree

src/megatron/bridge/models/glm_vl/data/collate_fn.py

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,76 @@
1414

1515
"""GLM VL collator implementations."""
1616

17+
from typing import Any
18+
1719
import torch
1820

1921
from megatron.bridge.data.collators.sequence import prepare_sequence_batch
2022
from megatron.bridge.data.collators.sequence_padding import use_processor_right_padding
2123
from megatron.bridge.data.collators.visual import THW_GRID_VISUAL_KEYS
2224
from megatron.bridge.data.conversation_processing import (
25+
AssistantMaskBoundaryConfig,
26+
assistant_mask_boundary_config_from_markers,
2327
build_assistant_loss_mask,
2428
chat_template_kwargs_from_example,
25-
infer_assistant_mask_boundary_config,
29+
get_processor_tokenizer,
2630
shared_chat_template_kwargs_from_examples,
31+
tokenize_text_without_special_tokens,
2732
)
2833
from megatron.bridge.data.datasets.utils import IGNORE_INDEX
2934
from megatron.bridge.data.packing.in_batch import build_mcore_thd_sequence_batch_from_rows
3035
from megatron.bridge.data.token_utils import extract_skipped_token_ids
3136
from megatron.bridge.training.utils.visual_inputs import GenericVisualInputs
3237

3338

39+
GLM4V_ASSISTANT_START = "<|assistant|>\n"
40+
GLM4V_ASSISTANT_END = "<|endoftext|>"
41+
GLM4V_EMPTY_THINK = "<think></think>\n"
42+
GLM4V_NEXT_ROLE_MARKERS = ("<|system|>\n", "<|user|>\n", "<|observation|>\n")
43+
44+
45+
def _glm4v_assistant_mask_boundary_config(processor: Any) -> AssistantMaskBoundaryConfig:
46+
"""Build GLM-4.5V role boundaries and exclude its empty thinking prefix."""
47+
tokenizer = get_processor_tokenizer(processor)
48+
empty_think_tokens = tokenize_text_without_special_tokens(tokenizer, GLM4V_EMPTY_THINK)
49+
trim_leading_token_sequences = (empty_think_tokens,) if empty_think_tokens else ()
50+
51+
return assistant_mask_boundary_config_from_markers(
52+
processor,
53+
assistant_start=GLM4V_ASSISTANT_START,
54+
assistant_end=GLM4V_ASSISTANT_END,
55+
assistant_end_fallbacks=GLM4V_NEXT_ROLE_MARKERS,
56+
include_end_tokens_for_roles=(),
57+
trim_leading_token_sequences=trim_leading_token_sequences,
58+
)
59+
60+
61+
def _build_glm4v_assistant_loss_mask(
62+
example: dict,
63+
input_ids: torch.Tensor,
64+
processor: Any,
65+
skipped_tokens: torch.Tensor,
66+
boundary_config: AssistantMaskBoundaryConfig,
67+
) -> torch.Tensor:
68+
"""Build GLM-4.5V loss spans using a virtual final turn terminator."""
69+
tokenizer = get_processor_tokenizer(processor)
70+
eos_token_id = getattr(tokenizer, "eos_token_id", None)
71+
if eos_token_id is None:
72+
raise ValueError("GLM-4.5V assistant masking requires tokenizer.eos_token_id.")
73+
74+
# GLM role markers terminate intermediate turns, but its final assistant
75+
# turn ends directly at the sequence boundary. Add EOS only to delimit that
76+
# final span for masking; it is neither returned nor trained as a target.
77+
terminated_input_ids = torch.cat([input_ids, input_ids.new_tensor([int(eos_token_id)])])
78+
return build_assistant_loss_mask(
79+
example,
80+
terminated_input_ids,
81+
processor,
82+
skipped_tokens,
83+
boundary_config=boundary_config,
84+
)[:-1]
85+
86+
3487
def glm4v_collate_fn(
3588
examples: list,
3689
processor,
@@ -55,7 +108,7 @@ def glm4v_collate_fn(
55108
del visual_keys, min_pixels, max_pixels
56109

57110
skipped_tokens = extract_skipped_token_ids(processor)
58-
boundary_config = infer_assistant_mask_boundary_config(processor)
111+
boundary_config = _glm4v_assistant_mask_boundary_config(processor)
59112

60113
if enable_in_batch_packing:
61114
sequence_rows = []
@@ -80,12 +133,12 @@ def glm4v_collate_fn(
80133
if position_ids is not None
81134
else torch.arange(input_ids.numel(), device=input_ids.device, dtype=torch.long)
82135
)
83-
loss_mask = build_assistant_loss_mask(
136+
loss_mask = _build_glm4v_assistant_loss_mask(
84137
example,
85138
input_ids,
86139
processor,
87140
skipped_tokens,
88-
boundary_config=boundary_config,
141+
boundary_config,
89142
).to(device=input_ids.device, dtype=torch.float32)
90143
labels = torch.cat([input_ids[1:], input_ids.new_full((1,), IGNORE_INDEX)])
91144
if skipped_tokens.numel() > 0:
@@ -144,12 +197,12 @@ def glm4v_collate_fn(
144197

145198
loss_mask = torch.stack(
146199
[
147-
build_assistant_loss_mask(
200+
_build_glm4v_assistant_loss_mask(
148201
example,
149202
input_ids,
150203
processor,
151204
skipped_tokens,
152-
boundary_config=boundary_config,
205+
boundary_config,
153206
)
154207
for example, input_ids in zip(examples, batch["input_ids"])
155208
]

tests/unit_tests/data/collators/test_model_collators.py

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -880,10 +880,10 @@ def apply_chat_template(self, conversations, **kwargs):
880880
monkeypatch.setattr(
881881
glm_vl_collate, "extract_skipped_token_ids", lambda processor: torch.empty(0, dtype=torch.long)
882882
)
883-
monkeypatch.setattr(glm_vl_collate, "infer_assistant_mask_boundary_config", lambda processor: None)
883+
monkeypatch.setattr(glm_vl_collate, "_glm4v_assistant_mask_boundary_config", lambda processor: None)
884884
monkeypatch.setattr(
885885
glm_vl_collate,
886-
"build_assistant_loss_mask",
886+
"_build_glm4v_assistant_loss_mask",
887887
lambda example, input_ids, *args, **kwargs: (input_ids != 0).to(dtype=torch.float32),
888888
)
889889
examples = [
@@ -906,6 +906,59 @@ def apply_chat_template(self, conversations, **kwargs):
906906
assert processor.tokenizer.padding_side == "left"
907907

908908

909+
@pytest.mark.parametrize(
910+
("input_ids", "expected_mask"),
911+
[
912+
(
913+
[100, 1, 102, 55, 56, 15, 3, 4, 100, 2, 102, 55, 56, 15, 5, 6],
914+
[0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1],
915+
),
916+
(
917+
[100, 1, 102, 55, 56, 15, 3, 4, 99, 99],
918+
[0, 0, 0, 0, 0, 0, 1, 1, 0, 0],
919+
),
920+
(
921+
[100, 1, 102, 55, 56, 15, 70, 71, 107, 8, 102, 55, 56, 15, 3, 4],
922+
[0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1],
923+
),
924+
],
925+
)
926+
def test_glm4v_assistant_mask_uses_role_boundaries_and_virtual_final_terminator(input_ids, expected_mask):
927+
class _GlmProcessor:
928+
class _Tokenizer:
929+
chat_template = "<|user|>...<|assistant|>...<|observation|>"
930+
eos_token_id = 99
931+
932+
def encode(self, text, add_special_tokens=False):
933+
return self(text, add_special_tokens=add_special_tokens)["input_ids"]
934+
935+
def __call__(self, text, add_special_tokens=False):
936+
mapping = {
937+
"<|assistant|>\n": [102],
938+
"<|endoftext|>": [99],
939+
"<|system|>\n": [105],
940+
"<|user|>\n": [100],
941+
"<|observation|>\n": [107],
942+
"<think></think>\n": [55, 56, 15],
943+
}
944+
return {"input_ids": mapping[text]}
945+
946+
tokenizer = _Tokenizer()
947+
948+
processor = _GlmProcessor()
949+
boundary_config = glm_vl_collate._glm4v_assistant_mask_boundary_config(processor)
950+
951+
mask = glm_vl_collate._build_glm4v_assistant_loss_mask(
952+
{"conversation": []},
953+
torch.tensor(input_ids),
954+
processor,
955+
torch.empty(0, dtype=torch.long),
956+
boundary_config,
957+
)
958+
959+
assert mask.tolist() == expected_mask
960+
961+
909962
def test_expand_image_tokens_handles_multiple_images_and_temporal_grids():
910963
image_token_id = 163605
911964
input_ids = torch.tensor([11, image_token_id, 22, image_token_id, 33])

0 commit comments

Comments
 (0)