Skip to content

[feat] Add gather_across_devices parameter to some contrastive losses#3442

Merged
tomaarsen merged 17 commits into
huggingface:masterfrom
tomaarsen:feat/gather_across_devices
Jul 17, 2025
Merged

[feat] Add gather_across_devices parameter to some contrastive losses#3442
tomaarsen merged 17 commits into
huggingface:masterfrom
tomaarsen:feat/gather_across_devices

Conversation

@tomaarsen

@tomaarsen tomaarsen commented Jul 11, 2025

Copy link
Copy Markdown
Member

Hello!

Pull Request overview

  • Add gather_across_devices to MNRL and CMNRL, etc.
  • Add contrast_anchors and contrast_positives to GIST and CGIST losses
  • Add utility functions for distributed all-gathers

Details

This is a work-in-progress. The goal of this PR is to introduce gathering across devices, as @NohTow showed that it was still worthwhile despite the same also being possible with GradCache. Also thanks to @NohTow for writing the code that this PR is based on & for helping debugging.

TODOs:

  • Update all other contrastive losses that benefit from in-batch negatives.
    • For Dense
      • Implement and test for MNRL
      • Implement and test for CMNRL
      • Implement and test for MNSRL
      • Implement and test for CMNSRL
      • Implement and test for GIST
      • Implement and test for CGIST
    • For Reranker: Rerankers don't benefit from cross-device gathering as they require computing per pair.
    • For Sparse
      • Implement for SparseMNRL
  • Turn util.py into a util folder with files for separate components, e.g. negative_mining.py, distributed.py, similarity_functions.py, huggingface.py, etc. It's getting too big.

  • Tom Aarsen

@tomaarsen
tomaarsen requested a review from Copilot July 11, 2025 14:45
@tomaarsen
tomaarsen marked this pull request as draft July 11, 2025 14:45

This comment was marked as outdated.

@tomaarsen
tomaarsen requested a review from Copilot July 15, 2025 21:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds distributed training support to contrastive losses by implementing gather_across_devices functionality and adds utility functions for distributed all-gathers. The main purpose is to enable larger effective batch sizes across multiple GPUs during contrastive learning by gathering embeddings from all devices before computing the loss.

Key changes include:

  • Addition of distributed all-gather utility functions (all_gather, all_gather_with_grad)
  • Implementation of gather_across_devices parameter across multiple contrastive loss classes
  • Addition of contrast_anchors and contrast_positives parameters to GIST losses for more flexible training

Reviewed Changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.

File Description
sentence_transformers/util.py Adds distributed all-gather utility functions and removes unused logging utilities
sentence_transformers/trainer.py Simplifies evaluation logic by removing conditional logging wrapper
sentence_transformers/losses/*.py Implements gather_across_devices functionality across 8 loss classes
sentence_transformers/sparse_encoder/losses/*.py Adds gather_across_devices support to sparse encoder losses

Comment thread sentence_transformers/losses/MultipleNegativesSymmetricRankingLoss.py Outdated
Comment thread sentence_transformers/losses/GISTEmbedLoss.py
Comment thread sentence_transformers/util.py
@tomaarsen
tomaarsen marked this pull request as ready for review July 15, 2025 21:59
@tomaarsen

tomaarsen commented Jul 17, 2025

Copy link
Copy Markdown
Member Author

I've decided to leave this item for a follow-up PR to avoid expanding the scope of this PR too much:

  • Turn util.py into a util folder with files for separate components, e.g. negative_mining.py, distributed.py, similarity_functions.py, huggingface.py, etc. It's getting too big.

With all of these losses tested (at least, checked that they perform better with gathering, ran without crashes, etc.), I'm ready to merge this. The test failures are related to the datasets v4 release, and the issues were fixed in #3445. I'll merge those into this branch first.

  • Tom Aarsen

@tomaarsen
tomaarsen merged commit 98cf7dc into huggingface:master Jul 17, 2025
7 of 9 checks passed
@Batwu

Batwu commented Jul 21, 2025

Copy link
Copy Markdown

Hi, thank you for the great work on this project.

While reviewing the implementation of MultipleNegativesRankingLoss in a distributed setting, I noticed a potential issue with how the labels are computed when using all_gather on the candidates.

In particular, the code computes labels as:

if torch.distributed.is_initialized():
    rank = torch.distributed.get_rank()
    offset = rank * batch_size
labels = torch.arange(offset, offset + batch_size, device=anchors.device)

However, if candidates are constructed using torch.cat(embeddings[1:]) where [positives, negatives] are concatenated along dim=0, the actual positions of the positives in the gathered candidates tensor will not necessarily align with offset + i. This is because torch.cat first places all positives, then all negatives from each rank, so the true index of the positive may differ from what offset = rank * batch_size assumes.
I'm still new to distributed training and PyTorch, so please forgive me if I misunderstood the logic!

@NohTow

NohTow commented Jul 21, 2025

Copy link
Copy Markdown
Contributor

Not sure I get you correctly, but essentially since we are only gathering the queries, this offset is meant to align those with their corresponding positive documents, which as you pointed out are always the first n documents (because we concatenate the positive and then the negatives).
Among those n documents, the positives for a query are at [rank * batch_size: rank* batch_size + 1] (considering we concatenate following the rank order). It is just meant to skip the first positives that corresponds to the positives of previous rank.

@tomaarsen

Copy link
Copy Markdown
Member Author

I think @Batwu might be on to something though, although I'm not 100% sure yet. If we have e.g. 1 positive and 8 negatives with 4 devices, then the candidates that are gathered are structured a bit like PNNNNNNNNPNNNNNNNNPNNNNNNNNPNNNNNNNN.

But the offsetting with labels only offsets by batch_size, resulting in:

PNNNNNNNNPNNNNNNNNPNNNNNNNNPNNNNNNNN
1
 2
  3
   4

instead of offsetting by batch_size * (1 + num_negatives), resulting in:

PNNNNNNNNPNNNNNNNNPNNNNNNNNPNNNNNNNN
1
         2
                  3
                           4

I'll have to do some more tests, but I think it's likely that the tests I ran only used an anchor and positive, and no negatives. Otherwise I would have noticed a degradation in performance presumably.

  • Tom Aarsen

@NohTow

NohTow commented Jul 21, 2025

Copy link
Copy Markdown
Contributor

That is actually totally true, I am very sorry I had the PyLate implementation in mind, where you can see that we do not concatenate the positives/negatives before gathering the elements (the "concatenation" is done at the score level), which in this case create PPPNNNNNNNN.
I believe you could simply move the concat operation to after the gathering to fix the issue no?

Again, deeply sorry @Batwu, should have checked the code of Tom instead of answering based on the code used as base.

tomaarsen added a commit to tomaarsen/sentence-transformers that referenced this pull request Jul 24, 2025
tomaarsen added a commit that referenced this pull request Jul 25, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants