This section is about a second way to train an image encoder and a text encoder together, with a simpler loss. CLIP, the model of the previous sections, asks each image to pick its own caption out of all the captions in the batch, a multiple-choice question. The sigmoid loss instead asks one yes-or-no question about every image and every caption in the batch. Do these two belong together? That one change makes training on many devices cheaper, works better at small batches, and gives each score a meaning on its own. Models trained this way, SigLIP and SigLIP 2, are the image encoders inside many open vision-language models, PaliGemma and Gemma 3 among them, so interviewers expect you to know the loss exactly.
Here is the setup of §30.2 in brief. A batch holds B matching pairs, each an image and its caption. An image encoder and a text encoder, called the two towers, turn each input into a vector of the same width. Both vectors are scaled to length one, so their dot product is their cosine. Each cosine times a learned scale is a logit, a raw score that a softmax or a sigmoid turns into a probability. The logits of every image with every caption fill a B×B matrix, with the matching pairs on its diagonal. CLIP's loss reads each row as a softmax classification of one image over the B captions, and each column as one of a caption over the B images (§7.2).
A softmax divides by a sum over its whole row, called its normalizer, and that has three costs. First, every logit in a row is tied to every other, so adding an unrelated pair to the batch changes the loss of every pair already there. Second, the normalizer needs a whole row and a whole column in one place. Training on many devices must therefore copy every embedding onto every device. Third, the loss means something different at each batch size. It starts near logB. And §7.2 showed that it can certify at most logB of mutual information, the measure of how much an image tells you about its caption.
The sigmoid loss of Zhai et al. (2023), called SigLIP (sigmoid loss for language-image pretraining), removes the normalizer. It asks of every entry of the matrix whether it is a real pair or a fake one, and scores each answer with logistic regression. §7.2 previewed this loss in one line, with its starting bias of −10 and scale of 10, and left the reasons for this section.
The first half of the section builds the loss and proves what it does. We derive the loss and its gradient, and compare how hard each wrong caption is pushed under the sigmoid and under the softmax. We prove why the bias must start near −logB, and say exactly what it means that the loss "decouples the batch size". Then we build the ring computation that never gathers the batch. The second half covers other ways to get better encoders. They are freezing a pretrained image tower, adding a captioning loss, combining both as SigLIP 2 does, and distilling a large encoder into a small one. Near its end the section surveys the encoders that compete with SigLIP 2 as of late 2026, then closes with when to choose each loss.
One yes-or-no question per pair
Here is the idea before the symbols. A batch of B image-caption pairs offers B×B ways to put an image next to a caption. Exactly B of them are real pairs. The rest are fakes, made by mismatching. Treat each of the B2 combinations as its own yes-or-no question, "is this a real pair?". Score each answer with the log-loss of logistic regression, the loss of a classifier that outputs one probability. No question depends on any other.
Now the symbols. A batch holds B matching pairs (xi,yi) for i=1,…,B, an image xi and its caption yi. The image tower ends in a pooled feature vector hiI, and a projection matrix WI maps it into the shared space, ui=WI⊤hiI∈Rd. Here d is the width of the shared space. The text tower likewise gives vj=WT⊤hjT∈Rd. Their unit-length versions are u^i=ui/∥ui∥ and v^j=vj/∥vj∥, exactly as in §30.2. Stacked as rows, they form U^,V^∈RB×d. In this chapter V^ holds text embeddings, never attention values. Throughout, σ(t)=1/(1+e−t) is the logistic sigmoid of §0.4, which turns any real number into a probability.
In plain words, every entry of the logit matrix is its own yes-or-no question. The diagonal entries should answer yes and all the others no. The loss is the total log-loss of those B2 answers, divided by the number of images. We call a matching pair a positive and a mismatched one a negative.
Three details of the definition each have a reason.
Why divide by B and not by B2. Write the loss as B1∑iℓi, where ℓi=∑jlog(1+e−zijsij) is one image's loss against all B captions. The loss is then an average over images, like CLIP's, and this is what the paper's pseudocode does. It also gives each positive logit the gradient (σ(sii)−1)/B. That is the same size as the gradient (Aii−1)/B of CLIP's image-to-caption loss, where Aii is the softmax probability of the right caption. Dividing by B2 would shrink every gradient by another factor of B. Under plain gradient descent the effective learning rate would then fall as the batch grows. Adam divides each gradient by its own running size and so mostly hides a constant factor. But the factor still changes what a gradient-clipping threshold means, how much ϵ matters, and the weight of this loss against any other loss added to it.
Why a bias. At a batch of 32,768, each positive faces 32,767 negatives. A classifier that has learned nothing should already say "no" to almost every pair. That needs a large negative logit before any cosine has been learned. The scale cannot supply it, because for random embeddings the scaled cosine eαu^⊤v^ is centered at zero whatever α is. The bias b0 supplies that prior. A later part of this section shows why −10 is the right size.
Why one matrix is enough. The label matrix is symmetric. Swapping images and texts transposes the logit matrix S and leaves the loss unchanged. So there is no separate image-to-text and text-to-image loss to average. There is also no pass over each row to subtract its maximum, which a softmax needs to avoid overflow. Every term is computed from its own pair alone.
You have seen this loss before, on words. §1.5 trained word vectors by negative sampling. Each real (word, neighbor) pair was contrasted with k randomly drawn fake neighbors, and each pair was judged by its own sigmoid. The sigmoid loss is the same idea laid out over a batch. Its fakes cost nothing to draw, since they are the other captions already in the batch, and each image gets B−1 of them.
A two-by-two example by hand
Two images and two captions are enough to see every piece of the loss. We reuse the cosines of the InfoNCE hand example in §7.2, so you can compare the numbers with the softmax's, and we fix the scale eα at 10.
Python · cell 1
The cell also prints the log-scale gradient ∂L/∂α at b0=−5, which is −0.411. A descent step therefore raises eα and makes every answer more confident. §30.2 found the same pressure toward a large scale for CLIP's learned temperature, which is why CLIP clips it at 100.
The gradient, pair by pair
The loss is a sum of separate logistic losses. So its gradient is the familiar logistic gradient, "prediction minus label" (§0.2), one entry at a time.
∂ Derive it, one logistic term at a time
For one term ℓ(s,z)=log(1+e−zs), the chain rule gives ∂ℓ/∂s=−ze−zs/(1+e−zs)=−zσ(−zs). If z=+1 this is −σ(−s)=σ(s)−1. If z=−1 it is σ(s). Both cases are σ(s)−y with y=(1+z)/2. Each sij appears in exactly one term of the loss, and the loss carries a factor 1/B, so Gij=(σ(sij)−yij)/B.
Next, sij=eαu^i⊤v^j+b0 gives ∂sij/∂u^i=eαv^j and ∂sij/∂v^j=eαu^i. The vector u^i appears in the B logits of row i, so its gradient is eα∑jGijv^j, which is row i of eαGV^. The vector v^j appears in column j, which gives row j of eαG⊤U^.
Finally, ∂sij/∂α=eαu^i⊤v^j=sij−b0, not sij itself, because the bias is not scaled. And ∂sij/∂b0=1 for every entry. Summing Gij times these derivatives over all entries gives the last two formulas.
In plain words, negative j pushes image i away with weight σ(sij). That weight is the model's current belief that the pair matches, and it depends on nothing else. The positive pulls with weight 1−σ(sii). A negative whose logit sits well below zero gets almost no gradient, and its cosine is then well below −b0/eα. So the bias sets one fixed bar for what counts as a hard negative, a wrong caption that the model still finds plausible.
Now compare the softmax. In CLIP's row loss, negative j pushes image i with weight Aij, its probability in the row's softmax. That weight depends on every logit in the row. One identity makes the comparison exact.
∂ Proof of the identity and its consequences
Divide the numerator and the denominator of Aij by esij. That gives Aij=1/(1+eλij−sij)=σ(sij−λij).
For (i), the softmax Jacobian from §4.3 is ∂Aij/∂sik=Aij(1[j=k]−Aik). For k=j that is −AijAik=0, while σ(sij) is a function of sij alone. For (ii), each row of A sums to 1, while each σ(sij) can approach 1 on its own. For (iii), adding c to every logit adds c to both sij and λij, so their difference does not change. □
In plain words, both losses compare each negative with a threshold. For the softmax, the threshold of entry j is the log-sum-exp of everything else in its row. That is a statistic of the whole batch, and it moves whenever any pair moves. For the sigmoid, the threshold is −b0, one learned number shared by every pair. Both thresholds are measured on the scaled cosines eαu^i⊤v^j, and a bias added inside a softmax would cancel, which is why CLIP has none. That is the whole difference, and it is why the sigmoid never needs to see the whole batch at once.
The cell below makes this concrete for one image. Its positive has logit 6, its six negatives run from 4 down to −6, and the sigmoid uses a bias of −5.
Python · cell 2
Raising negative 1 from 4 to 7 drops the softmax weight on negative 2 from 0.016 to 0.005. The hard negative now takes 0.727 of the row's total push of 0.733, and that total can never exceed 1. The sigmoid weight on negative 2 stays at 0.047. Shifting every logit up by 3 changes nothing for the softmax. The sigmoid's total push grows almost fivefold, from 0.32 to 1.52.
In practice, this means a model trained with the sigmoid is punished for scoring all captions too high, even when it ranks them correctly. A model trained with the softmax is not. An interviewer who asks how the two losses differ wants this threshold picture.
How hard is each negative pushed under the softmax and under the sigmoid loss?
One image against its caption and six others. Drag a dot. Bars show the push, or the positive's pull.
Loading interactive figure…
Interview
SigLIP loss and gradients
Implement siglip_loss_grad(Un, Vn, alpha, bias), the forward and the backward pass of the sigmoid loss defined above.
Un and Vn are (B, d) arrays of image and text embeddings. Their rows already have length one, so use them as given. Their gradients are taken with respect to these arrays as plain inputs, without differentiating through any normalization.
alpha is the logarithm of the logit scale, and bias is b0.
Return (loss, dUn, dVn, dalpha, dbias). That is the loss, with its 1/B, followed by its gradients with respect to the four inputs.
Compute −logσ(t) in a form that cannot overflow, and use no Python loops.
import numpy as np
def siglip_loss_grad(Un, Vn, alpha, bias):
"""Un, Vn: (B, d) unit rows. alpha: log of the logit scale. bias: the logit bias b0.
Returns (loss, dUn, dVn, dalpha, dbias)."""
raise NotImplementedError
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
Core
Softmax and sigmoid weights on the negatives
For one image's row of logits, compute how hard each negative is pushed under each loss, and check the identity that links the two.
negative_weights(s, pos, bias, mode) takes s, a (B,) array of scaled cosines eαu^i⊤v^j without the bias, and pos, the index of the positive. It returns a (B,) array. Each entry is the derivative of that row's loss with respect to one negative logit, and the entry at pos is 0.
With mode='softmax' the row's loss is −logsoftmax(s)pos, and bias is ignored. With mode='sigmoid' it is the row's part of the sigmoid loss with bias bias, without the 1/B.
softmax_as_sigmoid_bias(s, j) returns the number βj for which σ(sj+βj) equals the softmax weight of entry j.
Both functions must stay finite for logits in the thousands.
import numpy as np
def negative_weights(s, pos, bias, mode):
"""s: (B,) one image's logits e^alpha * cosine, without the bias. pos: index of its positive.
Returns a (B,) array, the derivative of that row's loss with respect to each negative logit,
and 0 at pos. mode 'softmax' uses the row softmax (bias ignored), 'sigmoid' the SigLIP terms."""
w = np.full(len(s), 1.0 / len(s))
w[pos] = 0.0
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
Why the bias starts near minus log B
The starting value of the bias decides how the first steps of training go, and the best value has an exact formula. At the start of training the two towers are random and know nothing about each other. Two random unit vectors in many dimensions are nearly perpendicular (§1.3). So every cosine u^i⊤v^j is close to 0, and every logit is close to b0. Each row then holds one positive and B−1 negatives that all look the same. We want the b0 that makes this starting point least wrong.
The best bias for four pairs made each pair's probability equal to the share of pairs that match, one in four. The same holds at every batch size, and the theorem below proves it by writing one image's starting loss as a function of b0 alone.
∂ Proof of the optimal initial bias
Differentiate term by term, using dtdlog(1+et)=σ(t),
The second derivative is f′′(b0)=σ(b0)σ(−b0)+(B−1)σ(b0)σ(−b0)=Bσ(b0)(1−σ(b0))>0. So f is strictly convex, and its stationary point is its minimum.
Setting f′(b0)=0 gives σ(b0)/(1−σ(b0))=eb0=1/(B−1). So b0⋆=−log(B−1) and σ(b0⋆)=1/B.
Substituting, log(1+e−b0⋆)=logB and (B−1)log(1+eb0⋆)=(B−1)logB−1B. Since log(1+x)=x−x2/2+…, the second term is (B−1)log(1+B−11)=1−2(B−1)1+…, which gives the approximation. □
In plain words, the best guess before learning anything is that each pair matches with probability 1/B, the fraction of pairs that do. The derivative has a second reading. Its first term, (B−1)σ(b0), is the total push on a row's B−1 negatives. Its second term, 1−σ(b0), is the pull on the positive. So b0⋆ is exactly where the push balances the pull. At initialization the ratio of push to pull is (B−1)eb0.
Python · cell 3
The minimum moves left by about log2≈0.69 each time the batch doubles. The curves are also lopsided. Above the optimum the slope f′(b0) climbs toward B−1, since every negative is then wrong. Below it the slope only approaches −1, since only the positive is wrong.
SigLIP's fixed −10 comes within a tenth of a nat of the best starting loss for batches from about 14,000 to 33,000. (A nat is the unit of a loss measured with natural logarithms.) It comes within half a nat for batches from about 7,000 to 52,000. At smaller batches it sits below the optimum, on the gentle side.
What goes wrong at b0=0 is the gradient, not only the loss value. The first updates are dominated by B−1 negatives per row, all saying "lower me". That push carries no information about which caption belongs to which image. The paper describes large early steps that over-correct for the imbalance, and its ablation shows the cost. It trained a Base-size Vision Transformer (86 million parameters, §27.2) at a batch of 8,192 for 900 million examples, with the scale starting at 10. Zero-shot ImageNet accuracy was 62.0 percent with no bias, 61.7 with b0=0, and 63.0 with b0=−10. Starting the scale at 1 instead hurt the run with b0=0 far more (53.7 percent) than the run with b0=−10 (61.0).
The same effect shows up in a small training run that you can execute here. A hidden "concept" of 8 numbers is seen through two noisy views of 24 features each, which stand in for an image and its caption. Two linear towers map each view into a shared space of width 64. The loop trains them with Adam and learns the scale and the bias too. It sets β2=0.95, the decay rate of Adam's running average of squared gradients, for a reason given later. The gradients are those of the theorem above, carried back through the normalization.
The same code also trains CLIP's softmax loss, which we use later to compare the two. We score each run by recall@1 on 500 held-out pairs. That is the fraction of images whose top-scoring caption is their own, averaged with the same fraction for captions.
Python · cell 4
Python · cell 5
Starting at b0=0 wrecks the run, with recall@1 below 1 percent after 40 steps. Starting at −3 reaches only 0.23, because the first updates spend themselves pushing every pair apart. The optimal value reaches 0.72. And −10, which starts below the optimum, reaches 0.81.
Two facts explain why erring low is safe. First, when the push of the negatives is weaker than the pull of the positive, the first steps pull matched pairs together. That is exactly the information the model needs, and the bias then rises on its own as the negatives start to matter. Second, the theorem assumed that every cosine is exactly 0. Here, random 64-dimensional embeddings give cosines with a standard deviation of about 1/64 (§1.3). So the logits spread around b0 by about eα/64=1.25. That spread moves the best starting bias down, to about −6.3 here, as the box below derives.
In practice, start at −10 for batches in the tens of thousands, or at −log(B−1) for other sizes, and lean lower rather than higher. An interviewer will ask you to derive −log(B−1) and to say why 0 is a bad start.
Core
The right initial bias, with and without spread
Implement two functions that give the starting bias for a batch of B pairs. The second one uses the condition derived in the box above.
init_bias(B) returns the bias that minimizes the loss per image when every logit equals b0.
init_bias_spread(B, logit_std) returns the bias at which the expected starting loss per image is stationary, when every logit, positive or negative, is b0 plus independent Gaussian noise with standard deviation logit_std.
Compute the expectation with Gauss-Hermite quadrature, np.polynomial.hermite_e.hermegauss. It gives a fixed set of points and weights for integrating against a Gaussian. Find the root with a bracketed one-dimensional search such as bisection.
The second function must reduce to init_bias(B) at logit_std = 0. It must stay finite and accurate for B up to 32,768 and logit_std up to 3. Do not use the approximation −log(B−1)−v/2, which fails at small B and large spread.
import numpy as np
def init_bias(B):
"""The bias that minimizes the initial loss per image when every logit equals it."""
return -10.0
def init_bias_spread(B, logit_std):
"""The bias b0 at which the expected initial loss per image is stationary when every logit is
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
What a perfectly trained score measures
The bias result is a special case of a cleaner fact. That fact says what a perfectly trained sigmoid score measures. The answer is how much more often an image and a text occur together than they would by chance.
That quantity is the pointwise mutual information, PMI(x,y)=logp(x)p(y)p(x,y), which §1.4 met for words. Here is an example. Suppose a photo of a beach and the caption "waves at sunset" appear together four times as often as independent draws would pair them. Their PMI is log4=1.39. If they appear together exactly as often as chance predicts, their PMI is 0.
To state the result, let the pairs of a batch be drawn independently from a joint distribution p(x,y) of images and texts, with marginals p(x) and p(y). Let s(x,y) be any scoring function, not only a scaled cosine plus a bias.
∂ Proof that the optimum is PMI minus log(B - 1)
Row i contributes log(1+e−s(xi,yi))+∑j=ilog(1+es(xi,yj)). The pair (xi,yi) is drawn from p(x,y). For j=i, yj belongs to a different, independently drawn pair, so (xi,yj) is distributed as p(x)p(y). By linearity every row has the same expected value, the right-hand side above, and the B1∑i averages B copies of it. Unlike the softmax loss's expectation, this one is exact at every B, because no normalizer couples the terms.
For discrete images and texts the expectation is ∑x,y[p(x,y)log(1+e−s)+(B−1)p(x)p(y)log(1+es)]. Each s(x,y) appears in one term only, so we can minimize term by term. The derivative of the (x,y) term is −p(x,y)σ(−s)+(B−1)p(x)p(y)σ(s). It increases with s and vanishes when es=(B−1)p(x)p(y)p(x,y), that is, at s=PMI(x,y)−log(B−1). If p(x,y)=0 but p(x)p(y)>0, the derivative is positive everywhere, and the optimal score runs to −∞. □
In plain words, a perfectly trained sigmoid logit is the PMI of the image and the text, shifted down by log(B−1). That shift is the log-odds of the prior that a random pair in the batch matches, one chance in B. (The log-odds of a probability p is log1−pp, and for p=1/B it is −log(B−1).) Write the logit as s=eαu^⊤v^+b0 with b0=−log(B−1). Then the scaled cosine is the PMI itself. At initialization the towers know nothing, so the model acts as if PMI were 0 everywhere, and the theorem gives back b0⋆=−log(B−1).
You have met this result twice before. Negative sampling with k fake neighbors per real one is optimized by PMI−logk (§1.5), and here k=B−1. CLIP's softmax loss is optimized by PMI plus a constant (§30.1). But the softmax never sees that constant, because adding one number to every logit leaves every softmax unchanged. So its value is arbitrary. The sigmoid pins it at −log(B−1).
As a result, a sigmoid logit has an absolute meaning, and one threshold works across all queries. A CLIP logit can be compared with other logits, but no value of it means "match" on its own. That is the difference an interviewer is probing when they ask whether a CLIP score is a probability.
Python · cell 6
The cell builds a random joint distribution over six images and six texts. It then minimizes the expected loss numerically, with one free score per (image, text) cell. The minimizer differs from PMI by exactly −log(B−1) in every cell, at every batch size.
What the sigmoid of a trained score means
It is tempting to read σ(s) as the probability that the pair matches. That is almost right, and the gap is a favorite follow-up question. Substitute the optimum into the sigmoid. Since es⋆=(B−1)p(x)p(y)p(x,y),
The last form is Bayes' rule for one specific question. Draw a pair the way a batch does. With probability 1/B it is a positive, drawn from the joint p(x,y). With probability (B−1)/B it is a negative, an image and a text drawn independently from p(x)p(y). Then σ(s⋆) is the posterior probability that the pair was a positive. So it is a match probability under the training prior 1/B, and that prior is a property of the batch, not of the world.
∂ Proof of the re-biasing rule
By Bayes' rule the posterior is πp(x,y)/(πp(x,y)+(1−π)p(x)p(y)). Divide the numerator and the denominator by πp(x,y). That gives 1/(1+e−s′) with e−s′=π1−π⋅p(x,y)p(x)p(y). So s′=PMI+log1−ππ, and PMI=s⋆+log(B−1) by the theorem above. □
In plain words, to use a sigmoid score as a probability in a new setting, add back the log(B−1) that training subtracted, then add the log-odds of the new base rate. Only the bias changes, so one threshold still serves every query once it is re-set. With π=1/B the correction vanishes, as it should.
Three cautions keep this honest.
A trained model is not at the population optimum. Its logits approximate s⋆ only roughly, and the learned bias drifts away from −log(B−1) during training (the paper saw it move when negatives were removed). So fit or check any threshold on held-out labeled pairs, as with the calibration of §30.3.
The derivation assumed that non-matches at deployment look like random pairs. A shortlist from a first-stage retriever is full of near misses, whose PMI is far above that of a random pair, so the same score means less there.
Shift the logit, not the probability, because Bayes' rule adds in log-odds.
Python · cell 7
The same raw score can mean near certainty or a long shot. That is why a production system sets its threshold for the base rate it actually faces, and why "is σ(s) a probability?" is a good interview question.
What decoupling means, and what it does not
Now we can say precisely what the paper means when it says that the sigmoid loss "decouples the batch size from the definition of the task".
What the batch size does, measured
How large a batch does each loss need? The paper answers in two settings, both with Vision Transformers. Their names give a size and a patch width (§27.2). B/16 is the Base size, 86 million parameters, cutting images into 16-pixel patches. g/14 is the giant size, about a billion parameters, with 14-pixel patches.
The first setting freezes a pretrained giant image tower, stores its image embeddings once, and trains only a Base-size text tower from scratch. This is locked-image tuning, which a later part of this section covers, and with the sigmoid loss the paper calls it SigLiT. Its batches ran from 512 to about one million, for 18 billion examples seen. The second setting trains both towers, at Base size, from scratch for 9 billion examples, at batches up to 307,000.
Our small run can show the small-batch half of that result. The cell trains both losses on the same synthetic pairs, with the same number of pairs seen, once with a batch of 16 and once with 256.
Python · cell 8
At a batch of 16 the sigmoid wins clearly, 0.78 against 0.71. At 256 the softmax is slightly ahead in this toy, 0.82 against 0.81. One plausible reading of the small-batch gap is that a softmax over 16 candidates is a noisy normalizer. Its gradient on each negative depends on which 15 others happened to be drawn, while the sigmoid's gradient does not. Do not read more into the large-batch numbers than the paper's own result, which is that the two losses converge. In practice, if your hardware caps you at a few thousand pairs per step, the sigmoid is the safer choice.
Three more findings from the paper complete the picture.
The imbalance is not the problem it looks like. At a batch of 16,384 there are about 268 million negative pairs for 16,384 positives. In the frozen-image setting, the paper masked out negatives to rebalance the batch, down to 1.6 negatives per positive, and it did not help. Dropping negatives at random hurt. Keeping only the easiest ones failed completely. Keeping only the hardest ones nearly matched the full batch, and slightly beat it when trained longer to see as many pairs. So most of what the negatives teach comes from the hard ones. The learned bias also rose as negatives were removed, from about −12.5 to about −3.5, absorbing the changed prior as the theorem says it should.
Robustness to noisy data. The paper corrupted a rising fraction of the training data. It replaced images with noise, replaced texts with random tokens, or shuffled which caption went with which image. Models trained with the sigmoid kept their lead over models trained with the softmax at every level of corruption. Earlier work had found the same for sigmoid losses in classification. Web alt-text is noisy, so this matters.
Spikes at large batches. Larger batches made training less stable. Spikes in the gradient norm caused large jumps in the parameters. Lowering β2 from 0.999 to 0.95 fixed it, in both Adam and Adafactor (a memory-saving variant of Adam). β2 sets how long the optimizer's running average of squared gradients remembers. At 0.95 it averages over about 20 steps instead of 1,000, so it recovers from a spike quickly. Language model pretraining uses the same remedy, for the same reason (§8.6, §8.8).
Computing the loss on a ring of devices
A large batch is split across many accelerators. The loss must then be computed without any one of them holding the whole batch. Let D=ndp be the number of data-parallel devices, each holding b=B/D pairs, its shard of the batch.
Recall how CLIP does it (§30.2). An all-gather, a collective operation that gives every device a copy of every device's data, puts all B image and text embeddings on every device. Each device then computes its b×B rows and B×b columns of logits.
The sigmoid loss needs neither the gather nor the slices. Every term depends on one pair, so the B×B matrix can be computed one b×b block at a time, in any order.
Picture four devices around a table, each holding its own images and its own captions. First, each device scores its images against its own captions. That is the only block with positives. Then every device passes its captions to its neighbor around the table, all in the same direction. It scores its images against the captions it just received and adds that all-negative block to its running total. After three passes every device has seen every caption once, and nobody ever held more than one block. The widget runs this schedule.
How can every device see every negative without ever gathering the batch?
Each device keeps its own images and passes its texts around the ring, scoring one block per step.
Loading interactive figure…
That is the chunked implementation of Zhai et al. (2023). Device r keeps its own images U^r and starts with its own texts V^r. At step 0 it scores the block (r,r). Then every device sends the text shard it holds to device r−1. This step is a collective permute, in which every device sends to one fixed partner and receives from another at the same time. So device r receives what device r+1 held, scores the new block, and adds it to its sum. After D−1 permutes, a final sum across devices and a division by B give the loss.
∂ Proof that the ring visits every pair once
Pair (i,j) lies in block (r,c) with i∈Ir and j∈Ic. For a fixed r, the map k↦(r+k)modD is a bijection from {0,…,D−1} onto itself. So block (r,c) is scored at exactly one step, k=(c−r)modD. Positives have i=j, so they lie in blocks with c=r, which are scored at k=0, and within such a block they are its diagonal. Every other block holds only negatives. The double sum therefore visits each of the B2 terms of Lsig exactly once, with the right label. □
The schedule fixes the costs, since each device holds one block at a time and passes one text shard per step.
Memory. One b×b block of logits at a time, plus a second text buffer if the next permute overlaps the current block's compute. The sharded softmax holds 2bB logits.
Communication in the forward pass.D−1 permutes of a b×d text shard, (D−1)bd numbers per device. That is half of the 2(D−1)bd of the two all-gathers, using the ring cost of §10.2. Each permute talks only to a neighbor and can overlap with the block's compute. The paper reports that D collective permutes are typically faster than two all-gathers.
The backward pass. Each block's logit gradient (σ(sij)−yij)/B is known the moment the block is computed, so the backward pass can run inside the same loop. The image gradient eαGblkV^c stays on its device and accumulates there. The text gradient eαGblk⊤U^r accumulates in a buffer that travels around the ring with its text shard and returns to the shard's owner.
Python · cell 9
So the ring turns a batch size that is impossible for the softmax into a routine one. In a systems interview, the expected answer to "how would you train a contrastive model at a batch of a million?" is this ring, with its memory and its traffic.
Interview
The chunked ring
Implement ring_siglip_loss(U_shards, V_shards, alpha, bias), a simulation of the ring on one machine.
The inputs are lists of D arrays of shape (b, d), the unit image and text embeddings on each device.
Device r starts with text shard r. After every step, each device receives the shard held by device (r + 1) % D.
Compute every block's summed pair losses with the provided block_loss(U_blk, V_blk, alpha, bias, has_pos). It records the shape of every block it is called with, and no block larger than (b, b) may be formed.
Return (total, visits, schedule). Here total is the SigLIP loss of the whole batch, and visits is a (B, B) integer array counting how many times each (image, text) pair was scored. schedule[k][r] is the index of the text shard that device r holds at step k.
import numpy as np
def ring_siglip_loss(U_shards, V_shards, alpha, bias):
"""U_shards, V_shards: lists of D arrays of shape (b, d), device r's unit image and text embeddings.
Returns (total, visits, schedule): the SigLIP loss of the whole batch, a (B, B) int array counting
how often each (image, text) pair was scored, and schedule[k][r], the text shard device r holds at step k."""
D = len(U_shards)
total = sum(block_loss(U_shards[r], V_shards[r], alpha, bias, True) for r in range(D)) # local blocks only
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
In PyTorch
Production code computes the loss with F.logsigmoid, a log of the sigmoid that never overflows, and lets autograd compute the gradients. The block below checks autograd against the closed-form gradients of the theorem. Then it computes the same loss block by block over three simulated devices.
🔥 In PyTorchfor a machine with PyTorch
On real devices the permute is a point-to-point exchange between processes. It is wrapped in an autograd function whose backward pass sends the gradient the opposite way around the ring. OpenCLIP, the widely used open-source library for training CLIP models, implements its SigLIP loss this way. The code below cannot run here, since it needs several processes.
The last line is worth a second look, because it hides a classic bug. Each rank (one process, driving one device) divides by its own b. Data parallelism then averages the parameter gradients over the D ranks, so the effective normalizer is bD=B, matching the definition. Dividing by B on each rank would make the gradients D times too small.
Locking the image tower
Everything so far trained both towers from scratch. Suppose instead you already own an excellent image model, trained on far more images than you have captions. A cheaper and often better option is to freeze it and train only a text tower that learns to land on its embeddings. Zhai et al. (2022) studied this choice and named the options with two letters, the first for the image tower and the second for the text tower.
Locking has engineering benefits that are easy to see. No gradients flow through the image tower, so training is faster and needs less memory. Without image augmentation, the image embeddings can be computed once for the whole dataset and stored. After that, each step costs only the text tower and the logits.
The SigLIP paper shows what that buys. SigLiT on four TPUv4 chips reached 79.7 percent zero-shot ImageNet accuracy in one day. It used a frozen B/8 image model and a text tower of Large width cut to 12 layers instead of 24. With a frozen g/14 image tower and a Large text tower, it reached 84.5 percent in two days.
The surprising result is that locked also beats unlocked. In the LiT study, Lu almost always beat Uu and uu across training lengths. On a dataset of 4 billion image-text pairs, Lu reached 70.1 percent zero-shot ImageNet, against 57.2 for Uu and 50.6 for uu. A LiT model on a g/14 image tower reached 85.2 percent zero-shot on ImageNet. It reached 82.5 percent on ObjectNet, a test set of objects in unusual poses and backgrounds.
The paper explains this with a measured trade-off. Unlocking the image tower reaches a lower contrastive loss on the caption data it trains on. But it reaches a higher loss on other caption sets. And its features do worse in a few-shot linear probe, a linear classifier trained on a few labeled images per class on top of the frozen features. Contrastive fine-tuning narrows a general representation to one dataset. Locking keeps the general representation and teaches the text side to match it. A pretrained text tower helped only a little, and locking the text tower did not work.
Locking has limits, and each one is a good interview follow-up.
The image tower cannot learn what its pretraining missed. Concepts that are rare in its pretraining data, such as those of specialized domains, stay poorly separated. The text tower can only point at what is already there.
The LiT paper reports that on cross-modal retrieval, unlike classification, Lu's advantage is less clear. With very long schedules, Uu or UU sometimes overtakes it.
If you do unlock a pretrained tower, protect it. The SigLIP paper fine-tuned a public B/16 image checkpoint with its learning rate scaled by 0.1. The default weight decay degraded the pretrained weights. On a 10-shot ImageNet probe the fine-tuned tower ended barely better than one trained from scratch. Turning off weight decay on the pretrained weights, while keeping it on the new text tower, gave 71.0 percent zero-shot on 16 TPUv4 chips in three days.
The idea is alive in current work. dino.txt (Jose et al. 2024) trains a text encoder from scratch against a frozen DINOv2, an image encoder trained without any text (§28.4). It also trains two new transformer blocks on top of the frozen backbone. It concatenates the average of the patch tokens with the class token, so that dense, pixel-level tasks line up with text too. The same recipe can give a language interface to any of the self-supervised image encoders of Chapter 28.
Research
Locked-image tuning on frozen features
The setup builds a small world of eight classes, with only 64 image-caption pairs to train on.
Raw images are a class pattern buried in heavy pixel noise.
Captions are bags of words, the class word plus three filler words.
W_pre is a pretrained image tower that already removes the noise. It stands in for a backbone trained on far more data.
Implement lit_train(Zi, Ht, Wt0, steps, lr, alpha, bias), which trains a linear text tower against frozen image embeddings.
Zi is (B, k), the frozen unit image embeddings unit(Xtr @ W_pre).
Ht is (B, V), the caption features, and Wt0 is (V, k), the initial linear text tower.
Train only Wt, with steps steps of plain full-batch gradient descent, Wt = Wt - lr * dL/dWt. The loss is the SigLIP loss of Zi against normalize(Ht @ Wt), at a fixed scale eα and a fixed bias.
Return Wt, and do not modify Zi or Wt0.
The tests check that your steps match gradient descent exactly and that the loss falls. Then they classify 400 held-out images zero-shot, with the prompt "a photo of a" plus each class word. Your model must beat one whose two towers were both trained from scratch on the same 64 pairs with the same budget.
import numpy as np
def lit_train(Zi, Ht, Wt0, steps, lr, alpha, bias):
"""Zi: (B, k) frozen unit image embeddings. Ht: (B, V) text features. Wt0: (V, k) initial text tower.
Train only Wt with `steps` steps of full-batch gradient descent, Wt <- Wt - lr * dL/dWt, on the SigLIP
loss of Zi against normalize(Ht @ Wt) at fixed scale e^alpha and bias. Return Wt."""
return Wt0
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
With the default seeds, LiT reaches about 89 percent zero-shot accuracy on the held-out images, against about 59 percent for the two towers trained from scratch on the same 64 pairs. Both models share a handicap. With eight classes, a batch of 64 holds about seven other captions of each image's class, and all of them are labeled as negatives. These false negatives, wrong labels on pairs that actually fit, are a fact of contrastive training on web data. §30.5 shows how curation reduces them, and the distillation part of this section shows how soft targets soften them.
Adding a captioning loss
A contrastive loss teaches an encoder only what it needs to tell captions apart within a batch. If no other caption in the batch mentions a red hat, the encoder never has to notice the hat. A captioning loss asks for more. To predict every word of the caption, the image features must carry everything the words mention. For an image x and caption tokens y1,…,yT, a decoder that reads the image features is trained on
Lcap=−t=1∑Tlogpθ(yt∣y<t,x),
the next-token loss of §3.1, conditioned on the image.
CoCa (Yu et al. 2022) adds this loss to a contrastive one in a single forward pass. Its text decoder is split in half.
The bottom half has causal self-attention only and never sees the image. Its output at a learned token appended to the end of the caption is a pure text embedding for the contrastive loss.
The top half adds cross-attention to the image encoder's output tokens and predicts the caption.
The total loss is LCoCa=λconLcon+λcapLcap with λcap=2 and λcon=1, since a larger captioning weight was better in their ablation. The image side is pooled by attention, with 1 learned query for the contrastive embedding and 256 for the decoder.
CoCa trained with a softmax contrastive loss at a batch of 65,536, for about 5 days on 2,048 TPUv4 chips. Its data were Google's internal JFT images, with their class labels used as text, plus the web alt-text collected for ALIGN, an earlier model trained like CLIP (§30.2). It reached 86.3 percent zero-shot on ImageNet, 90.6 percent with a frozen encoder and a learned classification head, and 91.0 percent fine-tuned.
Can captioning stand alone? Tschannen et al. (2023) compared captioning and contrastive pretraining with matched data, compute, and model size. Captioning alone gave vision encoders that were competitive with contrastive ones on classification, better on vision-language tasks, and scaled at least as well. Their variant CapPa predicts the whole caption at once from mask tokens, without the causal mask, for 75 percent of training examples. That forces the decoder to rely on the image rather than on the previous words.
The price is zero-shot classification. A captioner must score every class name with its decoder for every image. A contrastive model compares one stored embedding per class.
The practical lesson is that a captioning loss buys richer patch features at the cost of cheap zero-shot classification. That trade decides whether you want a contrastive, a generative, or a combined encoder. The combined one is the next design.
SigLIP 2 combines the pieces
SigLIP 2 (Tschannen et al. 2025) keeps SigLIP's architecture, so its weights drop into existing systems. On top of the sigmoid loss it stacks several losses and data techniques that were developed separately. Here is the recipe, with each piece said in plain words and credited.
The gains on zero-shot ImageNet are 1 to 2 points at each size. Why does a captioning decoder help an encoder that will be used without it? The decoder's gradient reaches the patch tokens, not just the pooled embedding. And it asks them to support text about specific regions and boxes. The contrastive loss alone shapes the pooled vector only enough to rank captions within a batch.
So the paper's largest gains are on localization and dense prediction, such as finding the region a phrase describes, segmentation, and depth. These are the weaknesses that §30.6 examines. Inside a vision-language model (VLM), SigLIP 2 also beats SigLIP as the image encoder. In practice, if you need an off-the-shelf image tower for a VLM or a dense task, SigLIP 2 is a strong default.
Distilling a joint encoder
Everything above makes encoders better. Serving often needs them smaller. A phone app that embeds each photo as it is taken cannot afford a large image tower, and neither can a search service answering thousands of image queries a second. On an iPhone 12 Pro Max, a ViT-L/14 image tower (the Large size, about 304 million parameters) takes 57.9 ms per image. The small MobileCLIP2-S2 tower takes 3.6 ms (Faghri et al. 2025). A small model trained from scratch on web pairs also leaves much accuracy unclaimed, as the numbers below show.
The standard fix is knowledge distillation, training a small student model to copy a large teacher's outputs (§20.4). Here it is applied to the whole batch's similarity matrix instead of one output distribution.
Here is the idea. The teacher, a large trained CLIP or SigLIP model, scores the same batch the student sees. Its row softmax says, for each image, how its probability is spread over the batch's captions. Its column softmax does the same for each caption. A one-hot label says "this caption and no other". The teacher's rows say "mostly this caption, a little of that near-duplicate, none of the rest". Training the student to reproduce those rows and columns passes on what the teacher knows about which captions are close.
In plain words, for every image, make the student's distribution over the batch's captions match the teacher's. For every caption, do the same for its distribution over the images. Each mismatch is measured by the KL divergence from the teacher. The factor τKD2 keeps the gradient from shrinking as the temperature rises, as §20.4 derives. It comes from the original distillation paper (Hinton et al. 2015).
∂ Proof of the distillation gradient
For one row, KL(p∥q)=∑jpjlogpj−∑jpjlogqj, and the first sum does not depend on the student. With qj=softmax(s/τKD)j, we have logqj=sj/τKD−log∑kesk/τKD. Using ∑jpj=1, that gives ∂(−∑jpjlogqj)/∂sk=−pk/τKD+qk/τKD. This is the softmax cross-entropy gradient of §0.2 with a soft target.
Each logit sijst appears in row i and in column j, so the two terms add. Multiplying by τKD2/(2B) gives the formula. For a one-hot teacher, ∑jpjlogpj=0 and −∑jpjlogqj=−logAiist, which is the CLIP row loss, and likewise for the columns. □
In plain words, CLIP is distillation from a teacher that knows only which caption came with which image. A real teacher replaces the identity matrix with its own soft matrices.
That replacement fixes a problem that hard labels cannot. When two captions in a batch are near duplicates, a one-hot label calls one of them a negative. The loss then keeps pushing it away, however good the model is. The teacher puts probability on both, so the student is never pushed away from a caption that also fits. The cell below checks this on six pairs, in which images 1 and 2 are two photos of dogs with the same caption.
Python · cell 10
At real scale, three lines of work set the recipes that interviewers expect you to know. The first matches similarities or features. The second removes the teacher's cost from training. The third tunes the teacher. The box below gives their numbers.
The MobileCLIP ablation is a reminder, as in §30.5, that what a model is trained on matters as much as how. A distilled student also inherits its teacher's blind spots along with its knowledge, so it is no better on concepts the teacher gets wrong.
Both inputs are (B, B) logit matrices for the same batch, with rows for images and columns for texts. tau is the distillation temperature.
Return (loss, dS), the loss LKD of the definition and its gradient with respect to S_student. The loss covers rows and columns, puts the teacher first in each KL, and includes the factor τKD2 and the 1/(2B).
Use stable log-softmaxes, since logits can be in the thousands, and no Python loops over entries.
import numpy as np
def clip_kd_loss(S_teacher, S_student, tau):
"""S_teacher, S_student: (B, B) logits, rows images and columns texts. tau: distillation temperature.
Returns (loss, dS), the contrastive distillation loss over rows and columns and its gradient
with respect to S_student."""
B = len(S_student)
return 0.0, np.zeros((B, B))
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
In PyTorch the KL is F.kl_div with log-probabilities for both arguments. Mind its argument order. The student's log-probabilities come first and the teacher's second, and the result is KL(teacher ‖ student).
🔥 In PyTorchfor a machine with PyTorch
The frontier encoders as of late 2026
Two families now compete for the job of embedding images. The first is the pair of towers this chapter has built, trained with a softmax or a sigmoid loss and made better by data, captioning losses, and distillation. Its strongest members in late 2026 are SigLIP 2 and Meta's Perception Encoder, both near 85 percent zero-shot ImageNet. The second family turns a whole vision-language model into an embedder. It feeds an image and a text through one large model and takes a hidden state as the embedding. The box lists the main releases with their dates and numbers.
The VLM-based embedders are where the field moved in 2025 and 2026. Such an embedder is the decoder-LLM embedder of §7.3 with images among its inputs, and it inherits that section's habits. It pools the last token and takes an instruction written in front of each query. It often trains its embeddings so that a prefix of the coordinates works on its own, a property called Matryoshka embeddings. It also pays that section's price. Every embedding costs a forward pass through a model of billions of parameters, against about 0.4 billion for a SigLIP 2 So400m image tower.
In return it reads text inside images and follows instructions. It accepts an image and a text together as one query ("this jacket, in green"). And it handles long, compositional queries that a 64-token text tower truncates. It has become the usual choice for retrieval over documents, screenshots, and mixed media. Pairs of towers remain the usual choice for high-volume photo search, zero-shot classification, and the vision side of VLMs.
The Perception Encoder's recipe ablation also bears on this section's main question. On a fixed set of 2.3 billion pairs, it used compute saved elsewhere to double the softmax batch from 32,768 to 65,536, which also doubled the samples seen. That added 0.6 points of ImageNet accuracy and 1.1 points of average robustness. The gain mixes a larger batch with more data, but it shows the softmax still improving past 32,768. With enough memory and a large enough batch, the softmax is not a handicap. That leads to the practical choice.
Softmax or sigmoid
Softmax (CLIP, CoCa, PE)
Sigmoid (SigLIP, SigLIP 2)
What each logit answers
which of the B candidates is mine
does this one pair match
Normalizer
log-sum-exp over every row and every column
none, one learned bias
Logits per device, sharded
2bB (row and column slices)
b2 (one block at a time)
Communication per step, forward
two all-gathers, 2(D−1)bd numbers
D−1 neighbor permutes, (D−1)bd
Quality below a batch of 16k
worse
better
Quality at 32k and above
about equal, keeps improving to 64k in PE's setting
about equal, saturates near 32k
Extra state and tuning
learned scale
learned scale and bias, bias must start near −log(B−1)
Score meaning
PMI up to an unknown constant, not a probability
PMI minus log(B−1), a match probability under the training prior 1/B, re-biased for other base rates
Label noise
less robust in SigLIP's tests
more robust
Choose the sigmoid when memory per device limits the batch, or when there are many devices and a global gather is expensive. Choose it too when you want one threshold on match scores, or when training is small or noisy. Choose the softmax when you have the memory for very large batches and an existing softmax pipeline, as the Perception Encoder did. At large batches the evidence says that the choice of data (§30.5) matters far more than the choice of loss.
Read more
Zhai et al. (2023). Sigmoid Loss for Language Image Pre-Training. The loss, its bias, the chunked implementation in Section 3.3, the batch-size study up to one million, and the ablations on bias, negatives, noise, and β2.
🎬 Lucas Beyer - Sigmoid Loss for Language Image Pre-Training · CohereOne of the paper's authors explains the sigmoid loss, the bias, the chunked implementation, and the batch-size study himself.🎬 Coding a Multimodal (Vision) Language Model from scratch in PyTorch with full explanation · Umar Jamil · starts at 23:00From 23:00, a clear walkthrough of why SigLIP replaces CLIP's softmax with a sigmoid, then the SigLIP vision tower coded from scratch as PaliGemma uses it.