[fix] Exclude padding from the Plackett-Luce normalizer in ListMLE/PListMLE losses#3827
Conversation
…istMLE losses Padded list positions are filled with a logit of 1e-16, so exp(1e-16) ~= 1 and they enter the reverse-cumsum normalizer with ~unit mass. The mask was applied only after the cumsum, so a query's loss and gradients depended on the batch's padding width (loss inflates, and a document's gradient can flip sign). Zero the padded scores before the cumsum using the sorted-order mask; no-op for single-length lists.
There was a problem hiding this comment.
Pull request overview
Fixes an error in the Plackett–Luce normalizer used by PListMLELoss / ListMLELoss where padded entries contributed non-zero mass to the cumulative-sum denominator, making the loss/gradients depend on max_docs (i.e., other queries’ list lengths in the batch).
Changes:
- Exclude padded positions from the Plackett–Luce normalizer by masking
exp(logits)before the reverse cumulative sum. - Add a regression test asserting padding-invariance for both
PListMLELossandListMLELoss, acrossrespect_input_order=True/False.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
sentence_transformers/cross_encoder/losses/plist_mle.py |
Masks padded entries out of the Plackett–Luce normalizer to make listwise losses invariant to batch padding width. |
tests/cross_encoder/losses/test_misc.py |
Adds a padding-invariance regression test for the affected listwise losses. |
Comments suppressed due to low confidence (2)
tests/cross_encoder/losses/test_misc.py:68
- This test compares losses across multiple forward passes; if the underlying HF model is in train mode, dropout will make the per-query and batched computations consume different random masks and can cause flaky failures unrelated to padding invariance. Put the model into eval mode (and optionally run under no_grad) for a deterministic regression test.
tests/cross_encoder/losses/test_misc.py:62 - For consistency with the other tests in this file (and to avoid line-length/lint issues), format the test signature across multiple lines and add a return type annotation.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Hello! Thanks for tracking this down and for the clean padding-invariance test! I wanted to see how much it actually moves the needle in real training, so I finetuned a few NanoBEIR R100 mean ndcg@10 (best checkpoint):
(Small note: The ListMLE 'no fix' was based on older ST/transformers versions, I didn't rerun that baseline)
I also pushed two tiny follow-ups to your branch: trimmed the inline comment and folded the regression test into
|
|
Nice, bigger ListMLE bump than I'd have guessed! Thanks for benchmarking both and publishing the runs 😄 test_misc.py works for me |
Summary
PListMLELoss(andListMLELoss, which subclasses it) scatter a list's logits into a(batch_size, max_docs)matrix and pad shorter lists with a logit of1e-16. The Plackett-Lucelog-likelihood uses a reverse cumulative sum of
scores = logits.exp()as the per-positionnormalizer. Because
exp(1e-16) ~= 1, every padded slot enters that normalizer with roughly unitmass, inflating the denominator of every real document's log-probability. The padding mask is only
applied after the cumulative sum (
log_probs[~mask] = 0.0), so it removes the padded positions'own terms but not their pollution of the real positions' normalizers.
As a result, a query's loss and gradients depend on
max_docs— i.e. on how many documents thelongest other query in the batch happens to have. For a single list (real labels
[2, 1],logits
[1.5, 0.3]):max_docs[-0.2315, +0.2315][-0.3440, -0.2280]The same list's loss grows with padding width and the second document's gradient flips sign, so a
variable-length-list batch (the common case for reranker training) trains on a corrupted signal.
This excludes padded entries from the normalizer by zeroing their scores before the cumulative sum,
using a mask in the same (sorted) order as the logits:
This matches how reference ListMLE implementations handle padding (e.g. allRank sets padded
predictions to
-infbefore the exp, which is the same as zero mass in the normalizer). For a listwith no padding the change is a no-op, so existing single-length behavior is unchanged.
Tests
Added a deterministic padding-invariance regression test for
ListMLELossandPListMLELoss,parametrized over
respect_input_order(True/False): it batches a 2-document query with a3-document query and asserts the batched loss equals the mean of the two per-query losses. It fails
on current
mainand passes with this change: