Skip to content

Commit 59c94a8

Browse files
authored
Merge pull request #48 from dhruvdcoder/fix/mlm-seq2seq-suffix-window
Fix MLM seq2seq suffix window train/inference mismatch
2 parents b10f1e6 + 2cd520f commit 59c94a8

5 files changed

Lines changed: 202 additions & 55 deletions

File tree

docs/models/mlm.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ The packed FlexAttention variant uses `PackedFlexMLMBatch` (subset of the above)
6969
| Class | Input | Output batch | Special behavior |
7070
|---|---|---|---|
7171
| `DefaultMLMCollator` | `BaseCollatorInput` | `MLMBatch` | Pad-right to `block_size`, BOS/EOS optional, random MLM masking. |
72-
| `MLMSeq2SeqTrainCollator` | `Seq2SeqCollatorInput` | `MLMBatch` | Concatenates `[prompt][BOS][target][EOS]` with right padding; masks only suffix positions. |
72+
| `MLMSeq2SeqTrainCollator` | `Seq2SeqCollatorInput` | `MLMBatch` | Concatenates `[prompt][BOS][target][EOS]`; only `block_size` suffix positions after the prompt are visible and MLM-eligible (tail hidden via `attention_mask=0`). |
7373
| `MLMSeq2SeqCollator` | `Seq2SeqCollatorInput` | `MLMBatch` | Left-pads prompt and right-pads target separately (padding on both sides). |
7474
| `_MLMSeq2SeqPredCollator` | `Seq2SeqCollatorInput` | `MLMBatch` | Same as `MLMSeq2SeqCollator` but masks **all** suffix tokens (`mask_all=True`); used for exact-match eval. |
7575
| `MLMSeq2SeqPredCollator` | `Seq2SeqCollatorInput` | `MLMBatch` | `input_ids = left-padded prompt only`; `target_ids = right-padded target` (used for seq2seq prediction). |
@@ -140,8 +140,9 @@ Task dataset and preprocessing: [TinyGSM](../tasks/tinygsm.md). GSM8K and code-e
140140
| Tokenizer | Qwen2-0.5B (`Qwen/Qwen2-0.5B`) with added `<|mask|>` |
141141
| `block_size` | 512 |
142142
| `input_block_size` | 0 |
143+
| `model.max_length` | 2048 (prompt + `block_size` suffix; overrides default 516) |
143144
| Batching | Per-device 32; global 512 |
144-
| Collators | STAR seq2seq (`seq2seq_*` / `seq2seq_pred_*`); no BOS between question and code |
145+
| Collators | STAR seq2seq (`seq2seq_*` / `seq2seq_pred_*`); no BOS; train/val `truncate: null` |
145146
| Val / test prediction | Post-hoc `code_exec_accuracy` (`Gsm8kCodeEval`); token EM disabled |
146147
| Monitored metric | `val/lm/accumulated_loss` |
147148
| Training schedule | Up to 1M steps; validation every 50k steps; checkpoint every 2.5k steps (keep every 100k) |

tests/models/mlm/test_collator_mlm.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
DefaultMLMCollator,
88
MLMSeq2SeqPredCollator,
99
prepare_prefix_ids,
10+
prepare_prefix_suffix_ids,
1011
)
1112
from tests.models._base import BaseCollatorTests
1213

@@ -143,3 +144,83 @@ def test_left_pads_to_max_seq_len(self, simple_tokenizer):
143144
assert (
144145
out["attention_mask"].sum(dim=1).tolist() == [3, 2]
145146
)
147+
148+
149+
class TestPreparePrefixSuffixIds:
150+
"""Suffix window cap for seq2seq training (STAR and TinyGSM layouts)."""
151+
152+
def test_star_hidden_tail_and_suffix_slot(self, simple_tokenizer):
153+
torch.manual_seed(0)
154+
pad = simple_tokenizer.pad_token_id
155+
bos = simple_tokenizer.bos_token_id
156+
eos = simple_tokenizer.eos_token_id
157+
mask = simple_tokenizer.mask_token_id
158+
input_block_size = 16
159+
block_size = 8
160+
max_seq_len = input_block_size + block_size
161+
162+
batch = prepare_prefix_suffix_ids(
163+
prefix_ids=[[10, 11, 12], [13, 14]],
164+
suffix_ids=[[20, 21], [22, 23, 24]],
165+
pad_token_id=pad,
166+
mask_token_id=mask,
167+
eos_token_id=eos,
168+
bos_token_id=bos,
169+
max_seq_len=max_seq_len,
170+
truncate="block",
171+
suffix_block_size=block_size,
172+
)
173+
174+
assert batch["input_ids"].shape == (2, max_seq_len)
175+
# Example 0: P=3, BOS, suffix slot 8 -> visible_len=12
176+
visible_len = 3 + 1 + block_size
177+
assert batch["attention_mask"][0, :visible_len].all()
178+
assert not batch["attention_mask"][0, visible_len:].any()
179+
assert (batch["target_ids"][0, visible_len:] == -100).all()
180+
# Prefix + BOS never MLM-masked
181+
assert batch["input_ids"][0, 0] == 10
182+
assert batch["input_ids"][0, 3] == bos
183+
assert (batch["input_ids"][0, :4] != mask).all()
184+
# Suffix slot layout in targets (input_ids may replace some with [MASK])
185+
assert batch["target_ids"][0, 4] == 20
186+
assert batch["target_ids"][0, 5] == 21
187+
assert batch["target_ids"][0, 6] == eos
188+
assert batch["target_ids"][0, 7] == pad
189+
# MLM masks only in suffix slot
190+
suffix_region = batch["input_ids"][0, 4:visible_len]
191+
assert (suffix_region == mask).any()
192+
193+
def test_tinygsm_variable_prefix_no_shared_block(self, simple_tokenizer):
194+
torch.manual_seed(0)
195+
pad = simple_tokenizer.pad_token_id
196+
eos = simple_tokenizer.eos_token_id
197+
mask = simple_tokenizer.mask_token_id
198+
block_size = 8
199+
200+
batch = prepare_prefix_suffix_ids(
201+
prefix_ids=[[10, 11, 12, 13, 14], [20, 21]],
202+
suffix_ids=[[30, 31], [40, 41, 42]],
203+
pad_token_id=pad,
204+
mask_token_id=mask,
205+
eos_token_id=eos,
206+
bos_token_id=None,
207+
max_seq_len=None,
208+
truncate=None,
209+
suffix_block_size=block_size,
210+
)
211+
212+
# Batch padded to max visible: 5 + 8 = 13 vs 2 + 8 = 10
213+
assert batch["input_ids"].shape == (2, 13)
214+
visible_len_0 = 5 + block_size
215+
visible_len_1 = 2 + block_size
216+
assert batch["attention_mask"][0, :visible_len_0].all()
217+
assert not batch["attention_mask"][0, visible_len_0:].any()
218+
assert batch["attention_mask"][1, :visible_len_1].all()
219+
assert not batch["attention_mask"][1, visible_len_1:].any()
220+
assert (batch["target_ids"][1, visible_len_1:] == -100).all()
221+
# Suffix slot starts right after prompt (no BOS); check targets for layout
222+
assert batch["target_ids"][0, 5] == 30
223+
assert batch["target_ids"][0, 6] == 31
224+
assert batch["target_ids"][0, 7] == eos
225+
assert (batch["input_ids"][0, 5:visible_len_0] == mask).any()
226+
assert (batch["input_ids"][0, :5] != mask).all()

xlm-models/mlm/configs/datamodule/tinygsm_mlm.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ datamodule:
1414
lm:
1515
collator:
1616
add_bos: false
17+
truncate: null
1718
val:
1819
lm:
1920
collator:
2021
add_bos: false
22+
truncate: null
2123
prediction:
2224
collator:
2325
add_bos: false

xlm-models/mlm/configs/experiment/tinygsm_mlm.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ block_size: 512
1414
input_block_size: 0
1515
monitored_metric: val/lm/accumulated_loss
1616

17+
# prompt + block_size suffix can exceed default rotary max_length (516)
18+
model:
19+
max_length: 2048
20+
1721
global_components:
1822
tokenizer:
1923
_target_: xlm.datamodule.load_auto_tokenizer

xlm-models/mlm/datamodule_mlm.py

Lines changed: 112 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -263,13 +263,18 @@ def prepare_prefix_suffix_ids(
263263
max_seq_len: Optional[int] = None,
264264
truncate: Literal["max", "block", None] = "block",
265265
loss_on_padding: bool = True,
266+
suffix_block_size: Optional[int] = None,
266267
) -> MLMBatch:
267268
"""Prepare concatenated prefix and suffix ids for seq2seq tasks with padding on the right only
268269
269270
Args:
270271
loss_on_padding: bool
271272
- If true, pad token is treated as a normal token: it has attention on it, it is predicted as a target token.
272273
- If false, it has no attention on it, it is not predicted as a target token (-100)
274+
suffix_block_size: When set, reserve exactly this many suffix tokens after the prefix
275+
(suffix + EOS padded/truncated to fit). Positions beyond ``prefix + BOS +
276+
suffix_block_size`` are attention-hidden and excluded from MLM masking and loss,
277+
matching inference ``max_new_tokens``.
273278
"""
274279
input_ids: List[TT] = []
275280
attention_mask: List[TT] = []
@@ -280,10 +285,16 @@ def prepare_prefix_suffix_ids(
280285
bos_token_id is not None
281286
) # always add bos before the suffix. Otherwise it is not needed.
282287
if truncate in ["max", None]:
283-
max_len = max(
284-
len(_prefix_ids) + len(_suffix_ids) + add_eos + add_bos
285-
for _prefix_ids, _suffix_ids in zip(prefix_ids, suffix_ids)
286-
)
288+
if suffix_block_size is not None:
289+
max_len = max(
290+
len(_prefix_ids) + add_bos + suffix_block_size
291+
for _prefix_ids, _suffix_ids in zip(prefix_ids, suffix_ids)
292+
)
293+
else:
294+
max_len = max(
295+
len(_prefix_ids) + len(_suffix_ids) + add_eos + add_bos
296+
for _prefix_ids, _suffix_ids in zip(prefix_ids, suffix_ids)
297+
)
287298
if truncate == "max" and max_seq_len is not None:
288299
max_len = max(max_len, max_seq_len)
289300
elif truncate == "block" and max_seq_len is not None:
@@ -296,58 +307,99 @@ def prepare_prefix_suffix_ids(
296307
for i, (_prefix_ids, _suffix_ids) in enumerate(
297308
zip(prefix_ids, suffix_ids)
298309
):
299-
# bos should not be masked
300-
suffix_mask = pad_truncate_list(
301-
[0] * (len(_prefix_ids) + add_bos)
302-
+ [1] * (len(_suffix_ids) + add_eos),
303-
max_len,
304-
1,
305-
pad_left=False,
306-
)
307-
temp = pad_truncate_list(
308-
_prefix_ids
309-
+ ([bos_token_id] * add_bos)
310-
+ _suffix_ids
311-
+ ([eos_token_id] * add_eos),
312-
max_len,
313-
pad_token_id,
314-
pad_left=False,
315-
)
316-
_mask = (torch.rand(len(temp)) < t[i]).logical_and(
317-
torch.tensor(suffix_mask, dtype=torch.bool)
318-
)
319-
_input_ids = torch.tensor(temp, dtype=torch.long)
320-
input_ids.append(_input_ids)
321-
if loss_on_padding:
322-
attention_mask.append(
323-
torch.tensor([1] * len(temp), dtype=torch.bool)
310+
if suffix_block_size is not None:
311+
suffix_slot = pad_truncate_list(
312+
_suffix_ids + [eos_token_id] * add_eos,
313+
suffix_block_size,
314+
pad_token_id,
315+
pad_left=False,
324316
)
325-
target_ids.append(_input_ids.clone())
317+
content = (
318+
_prefix_ids + ([bos_token_id] * add_bos) + suffix_slot
319+
)
320+
visible_len = len(_prefix_ids) + add_bos + suffix_block_size
321+
temp = pad_truncate_list(
322+
content, max_len, pad_token_id, pad_left=False
323+
)
324+
effective_visible = min(visible_len, len(temp))
325+
suffix_mask = pad_truncate_list(
326+
[0] * (len(_prefix_ids) + add_bos)
327+
+ [1] * suffix_block_size,
328+
max_len,
329+
0,
330+
pad_left=False,
331+
)
332+
_mask = (torch.rand(len(temp)) < t[i]).logical_and(
333+
torch.tensor(suffix_mask, dtype=torch.bool)
334+
)
335+
_input_ids = torch.tensor(temp, dtype=torch.long)
336+
input_ids.append(_input_ids)
337+
_attn = torch.zeros(len(temp), dtype=torch.bool)
338+
_attn[:effective_visible] = True
339+
attention_mask.append(_attn)
340+
_target_ids = _input_ids.clone()
341+
_target_ids[effective_visible:] = -100
342+
if not loss_on_padding:
343+
content_len = (
344+
len(_prefix_ids) + add_bos + len(_suffix_ids) + add_eos
345+
)
346+
for j in range(min(content_len, effective_visible), effective_visible):
347+
_target_ids[j] = -100
348+
target_ids.append(_target_ids)
326349
mask.append(_mask)
327350
else:
328-
attention_mask.append(
329-
torch.tensor(
330-
pad_truncate_list(
331-
[1]
332-
* (
333-
len(_prefix_ids)
334-
+ len(_suffix_ids)
335-
+ add_eos
336-
+ add_bos
351+
# bos should not be masked
352+
suffix_mask = pad_truncate_list(
353+
[0] * (len(_prefix_ids) + add_bos)
354+
+ [1] * (len(_suffix_ids) + add_eos),
355+
max_len,
356+
1,
357+
pad_left=False,
358+
)
359+
temp = pad_truncate_list(
360+
_prefix_ids
361+
+ ([bos_token_id] * add_bos)
362+
+ _suffix_ids
363+
+ ([eos_token_id] * add_eos),
364+
max_len,
365+
pad_token_id,
366+
pad_left=False,
367+
)
368+
_mask = (torch.rand(len(temp)) < t[i]).logical_and(
369+
torch.tensor(suffix_mask, dtype=torch.bool)
370+
)
371+
_input_ids = torch.tensor(temp, dtype=torch.long)
372+
input_ids.append(_input_ids)
373+
if loss_on_padding:
374+
attention_mask.append(
375+
torch.tensor([1] * len(temp), dtype=torch.bool)
376+
)
377+
target_ids.append(_input_ids.clone())
378+
mask.append(_mask)
379+
else:
380+
attention_mask.append(
381+
torch.tensor(
382+
pad_truncate_list(
383+
[1]
384+
* (
385+
len(_prefix_ids)
386+
+ len(_suffix_ids)
387+
+ add_eos
388+
+ add_bos
389+
),
390+
max_len,
391+
0,
392+
pad_left=False,
337393
),
338-
max_len,
339-
0,
340-
pad_left=False,
341-
),
342-
dtype=torch.bool,
394+
dtype=torch.bool,
395+
)
343396
)
344-
)
345-
mask.append(
346-
_mask.logical_and(attention_mask[-1])
347-
) # no input masks in padding
348-
_target_ids = _input_ids.clone()
349-
_target_ids[~attention_mask[-1]] = -100 # no loss on padding
350-
target_ids.append(_target_ids)
397+
mask.append(
398+
_mask.logical_and(attention_mask[-1])
399+
) # no input masks in padding
400+
_target_ids = _input_ids.clone()
401+
_target_ids[~attention_mask[-1]] = -100 # no loss on padding
402+
target_ids.append(_target_ids)
351403
target_ids = torch.stack(target_ids, dim=0)
352404
attention_mask = torch.stack(attention_mask, dim=0)
353405
input_ids = torch.stack(input_ids, dim=0)
@@ -452,16 +504,23 @@ def __call__(
452504
suffix_lists = [
453505
seq2seq_suffix_ids(e, self.target_field) for e in examples
454506
]
507+
if self.input_block_size == 0:
508+
max_seq_len = None
509+
truncate = None
510+
else:
511+
max_seq_len = self.input_block_size + self.block_size
512+
truncate = self.truncate
455513
prefix_suffix = prepare_prefix_suffix_ids(
456514
[e["prompt_ids"] for e in examples],
457515
suffix_lists,
458516
self.tokenizer.pad_token_id,
459517
self.tokenizer.mask_token_id,
460518
eos_token_id=self.tokenizer.eos_token_id if self.add_eos else None,
461519
bos_token_id=self.tokenizer.bos_token_id if self.add_bos else None,
462-
max_seq_len=(self.input_block_size + self.block_size),
463-
truncate=self.truncate,
520+
max_seq_len=max_seq_len,
521+
truncate=truncate,
464522
loss_on_padding=self.loss_on_padding,
523+
suffix_block_size=self.block_size,
465524
)
466525
return prefix_suffix
467526

0 commit comments

Comments
 (0)