Skip to content

Commit 78a4daf

Browse files
authored
Fix flaky test_causal_lm_loss_honors_shift_labels (#50533)
* Fix flaky `test_causal_lm_loss_honors_shift_labels` The first check compared the loss for two random 0/1 `shift_labels` tensors and required them to differ. On tiny random models with nearly flat logits (e.g. Inkling, which divides its hidden states by its muP width multiplier), the per-token loss changes have random signs and can cancel out in the mean below float32 resolution, so the two losses are bit-identical and the test wrongly reports that `shift_labels` is ignored (~1 in 700-2000 runs for Inkling). Use the most and the least likely token at every position as the two `shift_labels` instead: every token loss then moves in the same direction, by the max - min spread of its logits, so the mean is bound to move. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Address review: probe logits without labels, drop skip guard - Reuse the existing no-labels probe forward (moved out of the `else` branch) to pick the most / least likely tokens, instead of an extra forward with labels. - Drop the skip on missing / misaligned logits: the loss would already fail on misaligned targets, and a head without logits should error. - Shorten the comment and point to #50533. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Select the logits the loss is computed on before picking `shift_labels` Git's logits also cover the image patches placed before the text, but its loss only uses the text positions, so `shift_labels` picked from all of the logits had the wrong length. Add a `_logits_scored_by_loss` hook (identity by default) and override it in `GitModelTest` to drop the image patch positions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: ydshieh <ydshieh@users.noreply.github.com>
1 parent a4f3627 commit 78a4daf

2 files changed

Lines changed: 33 additions & 11 deletions

File tree

‎tests/models/git/test_modeling_git.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,12 @@ def _prepare_for_class(self, inputs_dict, model_class, return_labels=False):
391391
)
392392
return inputs_dict
393393

394+
def _logits_scored_by_loss(self, model, logits):
395+
# The logits also cover the image patches placed before the text, `(batch, num_patches + text_length, vocab)`,
396+
# but the loss is only computed on the text positions.
397+
num_image_tokens = model.git.encoder.layer[0].attention.self.image_patch_tokens
398+
return logits[:, num_image_tokens:]
399+
394400
def setUp(self):
395401
self.model_tester = GitModelTester(self)
396402
self.config_tester = ConfigTester(self, config_class=GitConfig, hidden_size=32)

‎tests/test_modeling_common.py‎

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1925,6 +1925,16 @@ def test_encoder_decoder_loss_no_double_shift(self):
19251925
),
19261926
)
19271927

1928+
def _logits_scored_by_loss(self, model, logits):
1929+
"""
1930+
Returns the positions of `logits` that the language modeling loss is computed on, i.e. the ones
1931+
`shift_labels` lines up with. That is all of them for almost every head; override for heads whose logits
1932+
also cover positions the loss skips.
1933+
1934+
Currently only used by `test_causal_lm_loss_honors_shift_labels`, and only overridden by `GitModelTest`.
1935+
"""
1936+
return logits
1937+
19281938
def test_causal_lm_loss_honors_shift_labels(self):
19291939
"""
19301940
Decoder-only LM heads shift ``labels`` by one internally to align them with the logits. Sequence and
@@ -1966,17 +1976,16 @@ def test_causal_lm_loss_honors_shift_labels(self):
19661976
set_seed(42)
19671977
model = model_class(model_config).to(torch_device).eval()
19681978

1979+
set_seed(42)
1980+
with torch.no_grad():
1981+
probe_logits = getattr(model(**self._prepare_for_class(inputs_dict, model_class)), "logits", None)
1982+
19691983
# `_prepare_for_class` builds targets only for the heads it knows about; read the shape off the
19701984
# logits for the others.
19711985
target_shape = self._prepare_for_class(inputs_dict, model_class, return_labels=True).get("labels")
19721986
if isinstance(target_shape, torch.Tensor):
19731987
target_shape = target_shape.shape
19741988
else:
1975-
set_seed(42)
1976-
with torch.no_grad():
1977-
probe_logits = getattr(
1978-
model(**self._prepare_for_class(inputs_dict, model_class)), "logits", None
1979-
)
19801989
if not isinstance(probe_logits, torch.Tensor) or probe_logits.ndim != 3:
19811990
self.skipTest(reason="Tester does not build targets for this head")
19821991
target_shape = probe_logits.shape[:2]
@@ -1987,30 +1996,37 @@ def targets(seed):
19871996
generator = torch.Generator().manual_seed(seed)
19881997
return torch.randint(2, target_shape, generator=generator, dtype=torch.long).to(torch_device)
19891998

1990-
def reported_loss(label_seed, shift_label_seed, model_class=model_class, model=model):
1999+
def reported_loss(labels, shift_labels, model_class=model_class, model=model):
19912000
inputs = self._prepare_for_class(inputs_dict, model_class, return_labels=True)
1992-
inputs["labels"] = targets(label_seed)
1993-
inputs["shift_labels"] = targets(shift_label_seed)
2001+
inputs["labels"] = labels
2002+
inputs["shift_labels"] = shift_labels
19942003
# Some heads run a stochastic front end (audio tokenizers sample); reseed so that the three
19952004
# forward passes below differ only in the targets they are given.
19962005
set_seed(42)
19972006
with torch.no_grad():
19982007
return model(**inputs).loss
19992008

2000-
baseline = reported_loss(0, 0)
2009+
labels = targets(0)
2010+
# Two random `shift_labels` can leave the loss bit-identical: on nearly flat logits, the per-token
2011+
# changes cancel out in the mean (see #50533). The least likely tokens raise every per-token loss
2012+
# above the most likely ones, so the mean has to move.
2013+
scored_logits = self._logits_scored_by_loss(model, probe_logits)
2014+
most_likely, least_likely = scored_logits.argmax(dim=-1), scored_logits.argmin(dim=-1)
2015+
2016+
baseline = reported_loss(labels, most_likely)
20012017
self.assertIsNotNone(
20022018
baseline, msg=f"{model_class.__name__}: no loss is returned when targets are passed"
20032019
)
20042020
self.assertFalse(
2005-
torch.equal(baseline, reported_loss(0, 1)),
2021+
torch.equal(baseline, reported_loss(labels, least_likely)),
20062022
msg=(
20072023
f"{model_class.__name__}: the training loss does not depend on `shift_labels`. Pre-shifted "
20082024
f"targets are the only correct ones under sequence/context parallelism, so the loss must "
20092025
f"be taken against them."
20102026
),
20112027
)
20122028
self.assertTrue(
2013-
torch.equal(baseline, reported_loss(1, 0)),
2029+
torch.equal(baseline, reported_loss(targets(1), most_likely)),
20142030
msg=(
20152031
f"{model_class.__name__}: the training loss still depends on `labels` although "
20162032
f"`shift_labels` was passed. `shift_labels` holds the targets already aligned with the "

0 commit comments

Comments
 (0)