Personalize this lesson
Adapt explanations and teaching visuals to your background and preferred voice.
One inspection photo can raise two very different questions. A technician uploads a rack picture and types "blocked vent on rack door." The archive should find similar inspections, but the same technician might then ask for the printed asset tag or for a box around the hazard. A plausible match isn't enough evidence to close a safety ticket, so the system needs a cheap way to find candidates and a deliberate way to inspect them.
Contrastive Language-Image Pre-training (CLIP) gives us the cheap first pass: it places images and captions in one embedding space, so photos can be indexed offline and searched with text.[1] The rest of this design follows one rack photo from that score to the evidence a reviewer can trust.
Product brief: data-center visual inspection
The service lets technicians upload rack photos, search past inspections in natural language, and route uncertain hazards to a reviewer. CLIP or Sigmoid Loss for Language-Image Pre-training (SigLIP) powers first-pass retrieval. Optical character recognition (OCR), grounding, or a generative vision-language model (VLM) handles questions that need label text, regions, or explanations. That split keeps a similarity score from pretending to be a complete inspection.
Requirements and API
POST /v1/inspection-imagesaccepts an immutable image reference, site, rack, capture time, and idempotency key. It returnsimage_id, ingestion state, and encoder version.POST /v1/inspection-searchacceptsquery, requested site filters, andlimit. The server intersects those filters with authenticated permissions. It returns rankedimage_idvalues, similarity scores, thumbnails, and review state. Scores are ranking signals, not probabilities; clients can't lower a safety-review threshold.POST /v1/inspection-analysisacceptsimage_id,question, and requested evidence type. It returnsanswer, OCR spans or grounded regions, model versions, andanswered,abstained, orreview.- Illustrative service targets are p95 below 300ms for search and below 3 seconds for analysis, not measured results. Access filters apply before retrieval, and a hazard never auto-closes from an unreviewed model result.
Data flow and sizing
Ingestion has one job: turn an authorized image into a searchable, versioned record. It follows upload -> malware and format checks -> metadata authorization -> image encoder -> normalized embedding -> versioned vector index. A failed check stops before the image becomes searchable.
Search has a different job. It follows authorized query -> text encoder -> site-filtered approximate nearest neighbor (ANN) search -> optional reranker -> response, with access filters applied before candidates are returned. Analysis starts only after the selected image is authorized, then follows crop or tile policy -> OCR or VLM -> grounding check -> review policy. Protect search capacity from bursts of expensive analysis. Separate GPU pools are one option; sharing needs admission limits and scheduling that meet the search target under mixed load.

Start with the vector bytes. For 10 million images and 768-dimensional 16-bit floating-point (FP16) embeddings, raw storage is 10,000,000 x 768 x 2 = 15.36 GB. That number excludes index structures, metadata, replicas, and old encoder versions.
The selected hierarchical navigable small-world (HNSW) graph or inverted-file (IVF) partitions determine the working set. An assumed total multiplier of 2 to 4 times raw storage would mean roughly 31 to 61 GB per generation, not a measured index requirement. Some indexes retain FP32 vectors despite FP16 inputs. Measure the actual vectors, graph, IDs, filters, replicas, and simultaneous rollout generations before sizing hosts.
At 100 queries per second (QPS), give the text encoder and ANN query path their own pool. Graph probes and encoder work shouldn't compete with the separately sized generative-analysis pool.
Recovery, rollout, and evaluation
Bind each idempotency key to the authenticated tenant and immutable request fingerprint; a content hash supports deduplication, not permission sharing. Reject a reused key with different image or metadata. Failed images enter retry or dead-letter state without becoming searchable. An encoder upgrade writes a new index generation; switch the paired text encoder, image preprocessing, normalization, and index generation together after validation. Equal vector dimensions don't make different models compatible. Retain the previous complete generation for rollback.
Treat uploads and image text as untrusted data. Bound decoded dimensions and tile counts as well as compressed bytes; resolve only authorized object references instead of fetching arbitrary user URLs. Strip unnecessary location metadata, restrict thumbnails and OCR artifacts, and apply deletion to indexes, crops, caches, and source images. Printed instructions such as "ignore safety policy" remain pixels or OCR evidence, not authority for tool calls. Recheck authorization before analysis and before returning cached results.
This in-memory example demonstrates the access boundary, not an ANN implementation. permitted comes from authenticated server state, not the request body. A requested site can't expand it, and a previously returned ID doesn't retain access after revocation.
def candidate_ids(records: dict, requested: set[str], permitted: set[str], generation: str) -> list[str]:
visible_sites = requested & permitted
return [image_id for image_id, row in records.items()
if row["site"] in visible_sites and row["generation"] == generation]
def read_image(records: dict, image_id: str, permitted: set[str]) -> str:
row = records.get(image_id)
if row is None or row["site"] not in permitted:
raise PermissionError("image unavailable")
return row["object_version"]
records = {
"a": {"site": "east", "generation": "clip-v1", "object_version": "a:v3"},
"b": {"site": "west", "generation": "clip-v1", "object_version": "b:v1"},
"c": {"site": "east", "generation": "clip-v2", "object_version": "c:v1"},
}
print("visible candidates:", candidate_ids(records, {"east", "west"}, {"east"}, "clip-v1"))
print("authorized object:", read_image(records, "a", {"east"}))
try:
read_image(records, "a", set()) # permission was revoked after search
except PermissionError as error:
print("after revocation:", error)visible candidates: ['a']
authorized object: a:v3
after revocation: image unavailableAnalysis needs its own safe failure. If OCR or grounding fails, return abstained or review rather than a caption presented as verified evidence. A fluent sentence is still a model output, not a measurement.
Roll out with a frozen, technician-reviewed set, shadow traffic, a site-limited canary, and staged index switching. Block expansion when asset retrieval recall@10, hazard false-negative rate, asset-tag OCR accuracy, grounding IoU, p95 latency, review rate, or cross-site access tests miss their route-specific bars. Monitor drift by site, camera, lighting, rack type, and prompt template instead of relying on one aggregate benchmark.
Many supervised computer vision systems expose a fixed label inventory: recognizing a new category generally requires labeled examples and another training step. CLIP showed that image-text matching at web scale can produce representations that transfer to new text-defined classes without training a new classifier head.[1] That made practical open-vocabulary retrieval possible and provided useful vision features for later generative systems.
That gives us a routing plan, not yet a trustworthy score. Follow the uploaded rack photo into the shared space first; then we can see which questions need a different model.
Score the rack photo with text first
Embeddings are the first inspection index
Suppose the search box contains "blocked vent on rack door." Each stored photo has already become a 768-number embedding, and the query becomes another vector of the same size. The index doesn't need to compare pixels with words at request time; it ranks stored image vectors against the query vector.
If the model has learned the right visual signal, matching image and text vectors point in roughly the same direction. Cosine similarity measures that alignment with a score between -1 and 1. Within one trained model, a higher score ranks a pair as more aligned than a lower-scoring candidate.
The number still isn't a calibrated probability, and it doesn't prove that the image contains every detail in the caption. CLIP's contract is narrower: rank matching image-text pairs above mismatches.
What does cosine similarity measure in CLIP-style systems?
Answer
It measures whether an image embedding and a text embedding point in a similar learned direction. Use it to rank candidate matches; don't treat it as a calibrated probability or proof that a caption is correct.
Why rack_hazard is a bad first index
A fixed label list can teach one class called rack_hazard, but that class says little about how blocked airflow looks across lighting, camera angles, cables, and rack labels. What happens when a technician adds a new phrase next week?
Learning from many image-text pairs gives the model a broader signal. It learns to align a visual concept with a natural-language description, rather than memorizing only the names in one classifier head.
The shared space lets the system score text prompts that weren't fixed training labels. Transfer still depends on whether pre-training learned the relevant evidence and whether the new domain resembles its data. Validate prompts on the team's racks, lighting, cabling, and review policy before turning a score into an operational decision.
Why is open-vocabulary classification different from fixed-label classification?
Answer
Fixed-label classifiers choose from labels baked into training. CLIP-style models can score new text-defined classes without training a new head; launching a new class still requires validation and a threshold or review policy.
Shared image-text space
The shared map is the handoff between two different inputs. One encoder starts with pixels and the other with words, but both are trained so a matched pair receives a higher similarity than a mismatch. The original model learned this from 400 million image-text pairs.[1]
That makes a prompt such as "blocked airflow behind rack panel" possible even when the phrase wasn't a training label. It remains a transfer hypothesis: test it on the racks that matter before trusting the rank.
What is the shared image-text space buying you?
Answer
It gives image and text encoders a common coordinate system for ranking pairs. That enables retrieval or text-defined classification without retraining a new label head for every candidate task, subject to domain validation.
Two encoders, one rack-photo score
Architecture[1]
What has to happen inside the model for that shared map to exist? CLIP jointly trains an image encoder and a text encoder, so a rack photo and "blocked vent on rack door" land in one coordinate system. The original paper used either a ResNet (Residual Network) or a ViT (Vision Transformer) for images and a Transformer for text, producing one representation for each caption.[1]
Before the figure, predict the matching point: do the two encoders exchange pixels and words layer by layer, or do they meet after encoding? They meet after encoding. A contrastive objective pulls matched representations together and pushes mismatches apart.

The two lanes stay separate until their outputs are normalized and compared in the shared map. That separation is what lets us encode the image once, store it, and answer many text queries later.
Implementation details remain checkpoint-specific. The official CLIP repository's encode_image and encode_text return unnormalized features. Normalize both before using their dot product as cosine similarity; the model's full forward does this internally and applies its learned logit scale. The README's separate zero-shot example uses a fixed scale of 100, so distinguish that example from the learned-scale implementation.[2]
Its tokenizer defaults to a 77-token sequence including start/end tokens. Overlong input raises by default; setting truncate=True cuts it and restores the end token, which can discard a decisive phrase. Changing context_length alone doesn't extend a checkpoint's learned positional embeddings. Use the matching preprocessing returned by clip.load, and bind it, both encoders, normalization, and the similarity metric to the index generation. These source details were checked October 10, 2026; no pretrained model was run here.[2]
Why is CLIP called a dual-encoder model?
Answer
It has one encoder for images and one encoder for text. Both produce embeddings in the same dimension, and the model scores image-text pairs by similarity rather than by decoding text.
Training objective
Training turns the shared map into a ranking game. For every image, the model must pick its paired caption from the other captions in the batch. The reverse direction asks whether each caption can find its paired image.
Use a tiny batch so the game stays visible. Suppose you have 3 images and 3 captions:
| Image | Caption |
|---|---|
| A | "cable label torn near port" |
| B | "asset tag partly covered by tape" |
| C | "warning label visible on power unit" |
There are possible pairings. Only the 3 diagonal pairs (A+A, B+B, C+C) are correct. The other 6 are mismatches. Before reading the labels in the figure, predict what training should do: raise the diagonal scores and lower the six off-diagonal scores.

The real batch in the original paper contains 32,768 pairs.[1] Each image therefore sees thousands of candidate captions, which supplies many negatives for the ranking game. The loss is symmetric: image-to-text and text-to-image each contribute a cross-entropy term. It's often described as InfoNCE-style (Information Noise Contrastive Estimation).
CLIP trains this setup from scratch with a linear projection from each encoder into the shared space. It also learns a temperature, initialized to the equivalent of 0.07 and clipped so logits aren't scaled by more than 100.[1] The temperature controls how sharply the model separates close scores.
Let and be the L2-normalized image and text embeddings. CLIP computes logits
where is a learned logit-scale parameter. You'll also see this written as division by a temperature , where . The symmetric objective is:
The first equation asks: "Given image , what's the probability that caption is the right one?" It compares the matching score with every caption score in row . The text-to-image equation does the same for column .
The final loss averages both directions. Both contribute gradients, but an average can still hide a regression in one direction, so evaluate image-to-text and text-to-image retrieval separately. Diagonal labels assume the batch's pairs are aligned; related off-diagonal pairs may be false negatives.
In a batch of 3 images and 3 captions, which pairs should the contrastive loss reward?
Answer
Only the 3 diagonal image-caption pairs are positives. The other 6 off-diagonal pairs are negatives. The loss raises diagonal similarity and lowers mismatched similarities in both image-to-text and text-to-image directions.
The next implementation keeps the same symmetric pattern with a fixed temperature. It leaves out distributed batching and the learned scale so the matrix and its output stay inspectable.
import math
def normalize(vector: list[float]) -> list[float]:
if not vector or not all(math.isfinite(value) for value in vector):
raise ValueError("expected a nonempty finite vector")
length = math.hypot(*vector)
if length == 0 or not math.isfinite(length):
raise ValueError("vector norm must be finite and nonzero")
return [value / length for value in vector]
def dot(left: list[float], right: list[float]) -> float:
return sum(a * b for a, b in zip(left, right, strict=True))
def cross_entropy(logits: list[float], target: int) -> float:
if not logits or not all(math.isfinite(x) for x in logits):
raise ValueError("logits must be nonempty and finite")
if type(target) is not int or not 0 <= target < len(logits):
raise ValueError("target is out of range")
max_logit = max(logits)
return math.log(sum(math.exp(x - max_logit) for x in logits)) - (logits[target] - max_logit)
def clip_loss(
image_embeds: list[list[float]],
text_embeds: list[list[float]],
temperature: float = 0.2,
) -> tuple[float, list[list[float]]]:
if not image_embeds or len(image_embeds) != len(text_embeds):
raise ValueError("require equal nonempty batches of paired embeddings")
if not math.isfinite(temperature) or temperature <= 0:
raise ValueError("temperature must be finite and positive")
if len({len(v) for v in image_embeds + text_embeds}) != 1:
raise ValueError("all embeddings must share one dimension")
images = [normalize(vector) for vector in image_embeds]
texts = [normalize(vector) for vector in text_embeds]
logits = [
[dot(image, text) / temperature for text in texts]
for image in images
]
loss_i2t = sum(cross_entropy(row, i) for i, row in enumerate(logits)) / len(logits)
columns = [list(column) for column in zip(*logits, strict=True)]
loss_t2i = sum(cross_entropy(column, i) for i, column in enumerate(columns)) / len(columns)
return (loss_i2t + loss_t2i) / 2, logits
image_embeds = [
[0.95, 0.05, 0.00],
[0.05, 0.92, 0.03],
[0.00, 0.07, 0.94],
]
text_embeds = [
[0.91, 0.08, 0.01],
[0.06, 0.89, 0.04],
[0.02, 0.09, 0.90],
]
loss, logits = clip_loss(image_embeds, text_embeds)
print("logit matrix:")
for row in logits:
print([round(value, 2) for value in row])
print(f"symmetric_loss: {loss:.3f}")logit matrix:
[5.0, 0.6, 0.14]
[0.71, 5.0, 0.66]
[0.09, 0.59, 5.0]
symmetric_loss: 0.022Read the output as a matrix, not as three independent scores. The diagonal entries are all 5.0, while most off-diagonal entries are close to zero, so the paired rows and columns win. A random or poorly aligned batch would look flatter and produce a higher loss.
That matrix also explains the systems cost. Softmax-based cross-entropy needs normalization over each row and column of the effective batch. The simple implementation materializes logits; an optimized one can accumulate normalization in blocks instead. Large distributed batches still require embedding/gradient communication. CLIP used 32,768 pairs, supplying many in-batch negatives.[1]
Why does original CLIP benefit from very large batch sizes?
Answer
The contrastive loss uses in-batch negatives. Larger batches give each image many more wrong captions to push away, but they also require a large similarity matrix and cross-device synchronization.
An off-diagonal pair is treated as negative, but it isn't automatically a clean semantic negative. Two photographs of the same rack hazard can be useful matches even when they arrived with different captions. Before scaling the batch, audit for those collisions so the loss doesn't push related examples apart.
from dataclasses import dataclass
@dataclass(frozen=True)
class Pair:
image_id: str
caption: str
concept: str
pairs = [
Pair("rack-front", "blocked vent on rack door", "blocked-airflow"),
Pair("rack-side", "airflow blocked by loose cabling", "blocked-airflow"),
Pair("asset-tag", "barcode obscured by cable tie", "blocked-barcode"),
]
potential_false_negatives = [
(left.image_id, right.image_id, left.concept)
for index, left in enumerate(pairs)
for right in pairs[index + 1:]
if left.concept == right.concept
]
print("pairs_treated_as_negative_but_related:", potential_false_negatives)pairs_treated_as_negative_but_related: [('rack-front', 'rack-side', 'blocked-airflow')]Training data
The original paper names its constructed corpus WebImageText (WIT). It contains 400 million (image, text) pairs collected from the internet, with search queries chosen to cover a broad vocabulary rather than a closed label set.[1]
Unlike ImageNet-style datasets built around manual class labels, CLIP learns from natural language attached to images on the web. That supervision is broader and noisier. A pair of photos showing the same blocked vent can still become in-batch negatives.
For a domain dataset, record source rights, collection purpose, caption provenance, and retention rules. Split near-duplicates and related views by asset/site/time before evaluating; otherwise the same rack can leak into train and test. Captions may omit hazards or contain asset identifiers that let the model memorize shortcuts. Audit those shortcuts and stratify evaluation by camera, site, lighting, and rare defects.
Zero-shot classification
Zero-shot classification reuses the same ranking interface. Instead of a final layer with fixed class weights, the model compares one image embedding with text descriptions of the candidate classes.
Suppose a data-center rack photo must land in one of three categories. Before looking at the result, predict what the highest similarity will tell you, and what it still won't tell you:
| Class | Prompt | Illustrative similarity |
|---|---|---|
| rack hazard | "a photo of a rack hazard" | 0.87 |
| safe rack | "a photo of a safe rack" | 0.12 |
| blocked asset tag | "a photo of a blocked asset tag" | 0.41 |
These invented scores explain ranking, not a pretrained model's performance. The highest score is the top candidate, not a verdict. Calibrate a score/margin policy on development data, then evaluate it on a separate held-out set. A softmax over candidate classes is conditional on that class list, not a calibrated hazard probability or an open-set detector.
The same mechanism produced a striking research result. The CLIP paper's best model, ViT-L/14@336px, reaches 76.2% zero-shot top-1 on ImageNet and matches the original ResNet-50 without using ImageNet's 1.28 million crowd-labeled training examples.[1] A data-center prompt such as "cable label partially obscured by a tie" can be scored the same way, but reliability on your racks still has to be measured.
What changes between training a new classifier head and doing CLIP zero-shot classification?
Answer
A classifier head learns fixed class weights from labeled examples. CLIP zero-shot classification embeds natural-language class prompts and compares them to the image embedding, so the class set can change at inference time.
The function below reuses normalize and dot from training-objective.py. It scores hand-written vectors, not encoded rack photos. It returns both cosine similarities and candidate-relative softmax weights. No model weights are downloaded or evaluated in these exercises.
import json
def softmax(values: list[float]) -> list[float]:
if not values or not all(math.isfinite(x) for x in values):
raise ValueError("softmax requires nonempty finite values")
max_value = max(values)
exp_values = [math.exp(value - max_value) for value in values]
total = sum(exp_values)
return [value / total for value in exp_values]
def zero_shot_classify(
image_embed: list[float],
class_names: list[str],
text_embeds: dict[str, list[float]],
logit_scale: float = 12.0,
) -> dict[str, object]:
if (not class_names or any(not name.strip() for name in class_names)
or len(set(class_names)) != len(class_names)):
raise ValueError("class names must be nonempty and unique")
if not math.isfinite(logit_scale) or logit_scale <= 0:
raise ValueError("logit scale must be finite and positive")
image = normalize(image_embed)
prompts = [f"an image showing {name}" for name in class_names]
similarities = [dot(image, normalize(text_embeds[prompt])) for prompt in prompts]
logits = [logit_scale * similarity for similarity in similarities]
probs = softmax(logits)
best_index = max(range(len(probs)), key=probs.__getitem__)
return {
"predicted_class": class_names[best_index],
"cosines": dict(zip(class_names, (round(x, 3) for x in similarities), strict=True)),
"relative_softmax": {
class_name: round(probability, 3)
for class_name, probability in zip(class_names, probs, strict=True)
},
}
text_embeds = {
"an image showing rack hazard": [0.92, 0.20, 0.05],
"an image showing safe rack": [0.18, 0.88, 0.08],
"an image showing blocked asset tag": [0.48, 0.31, 0.81],
}
result = zero_shot_classify(
image_embed=[0.89, 0.24, 0.08],
class_names=["rack hazard", "safe rack", "blocked asset tag"],
text_embeds=text_embeds,
)
print(json.dumps(result, indent=2))
print("singleton softmax:", softmax([-10.0])) # 1.0 even for a poor match
# An invented norm difference reverses raw-dot ranking, not cosine ranking.
probe_image = [1.0, 0.0]
probe_features = {"caption A": [0.8, 0.6], "caption B": [9.0, 12.0]}
raw_scores = {name: dot(probe_image, vector) for name, vector in probe_features.items()}
cosine_scores = {name: round(dot(normalize(probe_image), normalize(vector)), 3)
for name, vector in probe_features.items()}
assert max(raw_scores, key=raw_scores.__getitem__) == "caption B"
assert max(cosine_scores, key=cosine_scores.__getitem__) == "caption A"
print("raw dot:", raw_scores)
print("normalized cosine:", cosine_scores){
"predicted_class": "rack hazard",
"cosines": {
"rack hazard": 0.998,
"safe rack": 0.453,
"blocked asset tag": 0.618
},
"relative_softmax": {
"rack hazard": 0.988,
"safe rack": 0.001,
"blocked asset tag": 0.01
}
}
singleton softmax: [1.0]
raw dot: {'caption A': 0.8, 'caption B': 9.0}
normalized cosine: {'caption A': 0.8, 'caption B': 0.6}The second probe isolates the API trap. Caption B's feature norm is 15, so raw dot product rewards its magnitude and picks it over A. Normalizing removes that magnitude advantage and ranks A's 0.8 cosine above B's 0.6. These are invented features, not caption-quality measurements. Bind normalization and the index metric to the generation. Inner product and cosine rank unit vectors identically; unnormalized inner product can change their order as it did here.
Prompt engineering matters
The words around a class change the text vector. For a rack photo, compare a bare rack hazard label with "a photo of a rack hazard." The latter matches the sentence-like text CLIP saw during pre-training; on ImageNet, that template alone added 1.3 percentage points of top-1 accuracy.[1]
One wording can still be brittle. CLIP can encode several templates, average their normalized text embeddings, and cache one class vector. The original paper ensembled 80 context prompts and gained another 3.5 percentage points over the single default template; prompt engineering plus ensembling improved ImageNet accuracy by almost 5 percentage points.[1]
The code makes that averaging visible with a hardcoded list of prompt vectors. No single phrasing gets to decide the class representation by itself.
Why does prompt ensembling improve zero-shot classification?
Answer
Different templates emphasize slightly different contexts. Averaging normalized embeddings across templates reduces sensitivity to one brittle phrase and creates a more stable class representation.
def mean_vector(vectors: list[list[float]]) -> list[float]:
if not vectors or len({len(v) for v in vectors}) != 1:
raise ValueError("require nonempty equal-width vectors")
return [
sum(vector[i] for vector in vectors) / len(vectors)
for i in range(len(vectors[0]))
]
prompt_vectors = {
"a photo of a rack hazard": [0.92, 0.20, 0.05],
"a picture of a rack hazard": [0.88, 0.24, 0.07],
"an image showing rack hazard": [0.86, 0.28, 0.08],
"a rack hazard in the wild": [0.82, 0.31, 0.12],
}
ensemble = normalize(mean_vector([
normalize(vector)
for vector in prompt_vectors.values()
]))
print(json.dumps({
"class": "rack hazard",
"templates": len(prompt_vectors),
"ensemble_embedding": [round(value, 3) for value in ensemble],
}, indent=2)){
"class": "rack hazard",
"templates": 4,
"ensemble_embedding": [
0.955,
0.284,
0.088
]
}Run the ensemble cell after the classification cell. Averaging may reduce wording sensitivity; it doesn't guarantee better transfer. Opposing template vectors can even cancel to a zero vector, which normalization should reject. The next policy illustrates threshold mechanics with invented cosine scores and thresholds, not a calibration experiment. Tune thresholds on development data and measure risk/coverage on a separate test set.
import math
def decide(scores: dict[str, float], min_score: float, min_margin: float) -> str:
if len(scores) < 2 or any(not math.isfinite(v) or not -1 <= v <= 1 for v in scores.values()):
raise ValueError("require at least two finite cosine scores")
if not -1 <= min_score <= 1 or not 0 <= min_margin <= 2:
raise ValueError("invalid cosine threshold or margin")
ranked = sorted(scores.items(), key=lambda item: item[1], reverse=True)
(best_label, best_score), (_, second_score) = ranked[:2]
if best_score == second_score or best_score < min_score or best_score - second_score < min_margin:
return "review"
return best_label
cases = {
"clear_hazard": {"rack_hazard": 0.87, "safe_rack": 0.12, "tag_blocked": 0.31},
"ambiguous_panel": {"rack_hazard": 0.56, "safe_rack": 0.53, "tag_blocked": 0.18},
}
for name, scores in cases.items():
print(name, decide(scores, min_score=0.60, min_margin=0.10))clear_hazard rack_hazard
ambiguous_panel reviewLimitations of CLIP
Here is the boundary to test before wiring the score into an action: can one pooled similarity tell you how many vents are blocked or where each one is? No. Standard CLIP inference exposes a pooled image representation optimized for image-text matching, not calibrated counts or boxes. For counting and grounded localization, validate a detector such as Grounding DINO[3] or a grounding-aware VLM instead of interpreting global similarity as a region prediction.
That missing output is the key boundary. Zero-shot scoring compares one pooled image representation with one text representation. It returns a match score, not region coordinates or count evidence, so a concept-level hit can still come from the wrong component for an action that needs spatial proof.
Why is CLIP bad at counting and exact localization?
Answer
Its standard zero-shot interface scores one pooled image representation against one text representation. That output isn't supervised to return region coordinates, counts, or relations that detection or grounding models preserve.
Ask CLIP to "describe this image" and it returns similarity scores, not a sentence. A dual encoder has no language-generation head. Use a generative VLM such as LLaVA (Large Language-and-Vision Assistant) for captioning, or keep CLIP on retrieval and add a separate LLM for generation.
The same pooled contract can confuse relationships. Include paired probes such as "a red toolbox beside a blue safety cone" versus "a blue toolbox beside a red safety cone" in your inspection evaluation. These are proposed tests, not measured outcomes here. Grounding DINO is a separate open-set detector, not a drop-in CLIP detection head; boxes still need relation-specific evaluation and don't guarantee spatial reasoning.[3]
Counting needs its own evidence path. If a team maps the top similarity score to "three rack hazards" and a labeled audit finds five, the score has been asked to do count-supervised work it never outputs. Use region-based models or an explicit counting module instead.
Printed text is another boundary. CLIP learns useful OCR features on digitally rendered words, which are common in its pre-training data, but the paper reports highly variable transfer across domains and formats.[1] Zero-shot CLIP reaches 88% on MNIST and is beaten there by logistic regression on raw pixels; full-number SVHN is only 51%, with failures on repeated characters, blur, and low resolution.[1] A pooled similarity score isn't a document-OCR interface, so route rack labels through a specialized OCR stage.
Finally, web-crawled supervision carries social bias. CLIP can associate visual concepts with stereotypes.[4] Audit embeddings on representative data before deployment, and treat debiasing or domain-specific fine-tuning as separate interventions rather than assuming a prompt fixes the problem.
When is CLIP the wrong tool even if it recognizes the concept?
Answer
Use a different tool when you need generated captions, object counts, bounding boxes, exact OCR, spatial relations, or safety-critical decisions that require calibrated, audited behavior.
Vision encoder architectures
The pooled vector is the wrong contract for counting vents or drawing boxes. To understand what a generative VLM can preserve, look one step earlier. A ViT emits a sequence of patch features before pooling; a CNN tower emits a spatial feature map that a connector can flatten into tokens. That feature count becomes a serving input even when CLIP later reduces it to one vector.
Vision Transformer (ViT)[5]
Many published VLMs use Vision Transformers (ViT), while earlier systems used Convolutional Neural Networks (CNNs) such as ResNet. ViT treats an image as a sequence of patches and runs a Transformer encoder over them, much like Large Language Models (LLMs) process text tokens. Each patch becomes an embedding, and the stack returns contextualized visual tokens. CLIP pools those tokens into one image vector; a generative VLM may project many of them into the language prefix.

Patch tokenization
Before you count, predict the shape: a 224×224 image with 16×16 patches has 14 patches along each side. That gives visual tokens, plus a special [CLS] token used by many implementations to aggregate the global image representation.
How many patch tokens does a 224x224 image produce with 16x16 ViT patches?
Answer
patches per side, so patch tokens, plus an optional [CLS] token depending on the implementation.
Patch size is a token-budget choice. The figure uses 16x16 patches, while the high-resolution calculation below uses 14x14 patches. Compute the budget from the actual tower instead of copying a memorized number.
def patch_tokens(height: int, width: int, patch_size: int, crops: int = 1) -> int:
if any(type(x) is not int or x <= 0 for x in (height, width, patch_size, crops)):
raise ValueError("dimensions, patch size, and crops must be positive integers")
if height % patch_size or width % patch_size:
raise ValueError("this example requires exact patches; specify resize/padding separately")
return (height // patch_size) * (width // patch_size) * crops
for label, size, crops in [("thumbnail", 224, 1), ("page_crop", 336, 1), ("tiled_page", 336, 4)]:
print(label, patch_tokens(size, size, patch_size=14, crops=crops))thumbnail 256
page_crop 576
tiled_page 2304The arithmetic tells us how many tokens exist. Architecture determines how those tokens acquire context. The comparison below keeps that distinction visible before we choose a connector.
ViT vs. ResNet
| Feature | Vision Transformer (ViT) | ResNet (CNN) |
|---|---|---|
| Scaling | Scales well with sufficient data; attention cost grows with patch-token count. | Strong convolutional baseline with different compute and inductive-bias trade-offs. |
| Context Window | Global context from the first layer via self-attention across all patches. | Local context initially, building to global context only in deep layers. |
| Inductive Bias | Low (treats image as sequence of patches), requiring more data to learn structure. | High (locality and translation-equivariance biases), often requiring less data to train from scratch. |
| Architecture | Transformer-shaped token sequence is convenient for many connectors. | Also usable as a vision tower; connector still maps its features to the downstream task. |
SigLIP[6]
Standard CLIP pays for a softmax over image-text candidates in the effective batch. Original CLIP used a batch size of 32,768 in its distributed training run; smaller implementations can still train, but they change the negative-diversity and accuracy trade-off.[1]
SigLIP (Sigmoid Loss for Language-Image Pre-training) changes the question. Rather than asking "Which of these 32,000 captions is the best match?", it asks "Is this specific image and caption a good match, yes or no?" It replaces the global softmax competition with a pairwise sigmoid loss:
Here is the batch size, for matching pairs and otherwise. This is a label, unlike CLIP's earlier logit notation. and are normalized embeddings; is a learned logit scale (inverse temperature), a learned bias, and the logistic sigmoid.
The paper parametrizes , initializes , and initializes . This favors negative predictions initially, matching a batch dominated by mismatches.[6] Algorithm 1 sums all pair losses and divides by , not . Averaging every pair instead would shrink the loss and gradients by another factor of .
Advantages over CLIP
- Decoupled computation: No global normalization is needed. Each pair is scored independently, allowing the similarity matrix to be computed in chunks across devices.[6]
- Efficiency: The sigmoid loss removes global softmax normalization. The SigLIP paper reports a large gap versus softmax when the batch is smaller than 16k, a smaller gap as the batch grows, and diminishing returns past a more reasonable 32k. Pushing SigLiT to a 1 million batch was possible; it didn't keep helping.[6]
- Memory and partitioning: The loss is easier to chunk across devices because it doesn't require a single global softmax over the full batch.
SigLIP's pairwise loss changes the computation contract: positive and negative pair terms can be evaluated without a batch-wide probability distribution. That makes partitioning easier, but it doesn't decide what accelerator, data volume, or accuracy a data-center matcher will need.
What does SigLIP change relative to CLIP's softmax loss?
Answer
It scores image-text pairs with independent sigmoid terms instead of one global softmax over the batch. That reduces global synchronization pressure and works better at smaller batches, though large batches can still help.
import math
def softplus(value: float) -> float:
return max(value, 0.0) + math.log1p(math.exp(-abs(value)))
def pair_loss(score: float, is_match: bool) -> float:
if not math.isfinite(score) or type(is_match) is not bool:
raise ValueError("require a finite logit and a boolean match label")
label = 1.0 if is_match else -1.0
return softplus(-label * score)
scores = [
("same_asset", 3.2, True),
("wrong_caption", -2.1, False),
("hard_negative", 0.4, False),
]
for name, score, match in scores:
print(name, round(pair_loss(score, match), 3))
# Four pairs, two examples. At zero logits each pair costs log(2).
batch_logits = [[0.0, 0.0], [0.0, 0.0]]
n = len(batch_logits)
total = sum(pair_loss(score, i == j)
for i, row in enumerate(batch_logits) for j, score in enumerate(row))
print("paper reduction:", round(total / n, 3))
print("pair mean:", round(total / (n * n), 3))same_asset 0.04
wrong_caption 0.116
hard_negative 0.913
paper reduction: 1.386
pair mean: 0.693Why SigLIP matters in newer stacks
Some published VLMs use SigLIP-family towers when moderate-batch training or partitioned pair scoring matters. SigLIP 2 adds caption-based pretraining, self-distillation, masked prediction, and online data curation; its paper reports better transfer than original SigLIP across model scales when used as a VLM vision backbone.[7] Its NaFlex variants support multiple resolutions while preserving native aspect ratio. Standard fixed-resolution checkpoints don't acquire that behavior simply by being SigLIP 2: NaFlex needs its matching vision implementation and adapted preprocessing.[8]
For a wide rack panorama, choose and validate a supported checkpoint/preprocessing pair before counting patches. “Native aspect ratio” doesn't mean the original image keeps every pixel or that an arbitrary token budget is safe. The foundation remains image-text alignment, and sigmoid match scores still need target-domain calibration rather than being treated as hazard probabilities.
EVA-CLIP: scaling vision backbones with masked pretraining[9]
Training a large Vision Transformer directly from scratch with contrastive loss is expensive, and scaling can become unstable. EVA-CLIP initializes its vision tower from an EVA masked image modeling (MIM) model, then uses that representation to make contrastive scaling more stable and efficient.[9] The tower therefore starts with features learned by reconstructing masked visual features from a teacher, rather than learning every visual pattern from image-text pairs alone.
The hybrid recipe also changes the sample-efficiency result. The paper's EVA-02-CLIP-L/14+ has about 430M total model parameters and reaches 80.4% zero-shot top-1 on ImageNet-1K; EVA-02-CLIP-E/14+ has 5.0B total parameters and reaches 82.0%.[9] Those totals include both towers, not only the vision encoder. The results describe ImageNet evaluation, not a guarantee that a data-center tower will read small asset tags.
Why does EVA-CLIP initialize its vision tower with masked image modeling?
Answer
Training massive vision transformers purely with contrastive loss is unstable and sample-inefficient. Initializing from an EVA masked image modeling checkpoint provides strong representations early, accelerating contrastive convergence and enabling stable scaling to billions of parameters.
Modern VLMs: from alignment to generation
The similarity score has reached its boundary. CLIP and SigLIP can say whether an image and caption belong together, but a technician asking "what's covering the barcode?" needs generated text and perhaps a box.
That question introduces fusion: the point where visual evidence and text interact to produce an answer. Before choosing a connector, predict the serving trade-off. Passing every patch to the language model preserves detail but lengthens its input; compressing patches protects the input budget but may discard the barcode.
Three published architectures illustrate the connector patterns. These dated examples aren't a current leaderboard or a claim that newer checkpoints retain the same input limits:
- simple projectors that map vision features into the LLM space
- query-based compressors that reduce many visual patches into a fixed token budget
- resamplers and adapters that preserve more spatial detail for high-resolution inputs
These patterns are enough to reason about token flow without guessing at proprietary internals.

LLaVA: the straightforward projector path[10]
LLaVA is the clearest example of projecting visual features into a language model. Original LLaVA takes a pretrained CLIP ViT-L/14 encoder and Vicuna (an instruction-tuned LLaMA chat model), then learns a linear projection matrix that maps encoder grid features into the LLM's word-embedding dimension.[10] The projector changes representation space; it doesn't decide whether a barcode is present.
The original recipe separates two learning problems:[10]
- Feature alignment: freeze the vision encoder and LLM, train only on a filtered slice of Conceptual Captions 3M (595K image-text pairs after filtering).
- Visual instruction tuning: fine-tune the projector and LLM on 158K GPT-4-generated multimodal instructions.
The data-generation teacher was text-only: GPT-4 received captions and object-box annotations, not image pixels. It synthesized conversations about that symbolic evidence.[10] Missing or incorrect annotations can therefore reach the training answers; fluent synthetic instructions still need data-quality checks.
LLaVA-1.5 keeps that two-stage shape and changes two serving-relevant pieces: a two-layer MLP connector replaces the linear layer, and CLIP-ViT-L-336px replaces the 224px input. That resolution yields a grid, or 576 visual prefix tokens per image.[11] The pattern stays simple: a pretrained vision encoder, an LLM, and a learned projector can support multimodal instruction following without a new vision backbone.
What is the core LLaVA architecture recipe?
Answer
Use a pretrained vision encoder, project its visual features into the LLM hidden dimension, concatenate projected visual tokens with text tokens, then train first for feature alignment and then for multimodal instruction following. Original LLaVA uses a linear projector; LLaVA-1.5 switches to an MLP and a 336px CLIP encoder.
Common multimodal training ladder
The LLaVA split illustrates a common training ladder. This staged recipe separates representation learning, connector alignment, and answer behavior. The rows below are common options, not a required sequence for every VLM; which components are frozen or trained jointly depends on the architecture and recipe.
| Stage | Typical objective | What it teaches |
|---|---|---|
| Contrastive pretraining | image-text matching as in CLIP or SigLIP | shared semantic space for retrieval and zero-shot recognition |
| Connector or projector alignment | freeze big backbones, train connector | map vision features into language-model space cheaply |
| Multimodal instruction tuning | image + prompt -> assistant response | teach task behavior, dialogue format, OCR usage, and grounded answers |
| Preference or safety post-training | chosen vs rejected responses, critiques, or policy filters | reduce harmful or low-quality multimodal behavior |
CLIP and SigLIP mostly cover the first row.[1][6] LLaVA makes the second and third rows explicit with feature alignment followed by visual instruction tuning.[10] A chat-oriented system needs training beyond contrastive alignment because matching doesn't teach long-form answer behavior.
Use the observed failure to locate the missing stage:
- contrastive pretraining teaches matching
- instruction tuning teaches how to answer
- post-training teaches which answers are preferred or allowed
A response-format failure suggests checking instruction data and the chat template. A safety failure needs policy enforcement and evaluation, with post-training as one possible intervention. Neither symptom uniquely identifies a training stage: bad features, prompts, or data can also cause failures. BLIP-2's Q-Former addresses a separate question: how much visual information reaches the language path?
BLIP-2 and the Q-Former: compress before the LLM[12]
BLIP-2 (Bootstrapping Language-Image Pre-training, version 2) addresses the token bottleneck directly. Instead of forwarding every visual feature into the LLM, it introduces a Q-Former that learns a fixed set of query vectors. The queries pull task-relevant information from a frozen vision encoder.
The published setup uses 32 queries of dimension 768. That bottleneck is much smaller than frozen ViT-L/14 features (, including the class token), and the query count stays fixed when input resolution rises.[12] The paper reports that BLIP-2 outperforms Flamingo-80B by 8.7 points on zero-shot VQAv2 while using fewer trainable parameters.[12] That's a research comparison, not a serving SLA for rack photos.
The serving consequence is straightforward: tokens admitted to the language path affect prefill and cache cost. A smaller fixed budget wins only if it preserves task-critical evidence. If 32 queries drop characters on an asset tag, lower prefix cost is the wrong result.
Why does BLIP-2 put a Q-Former before the LLM?
Answer
The Q-Former uses learned queries to extract a bounded set of useful visual tokens from a frozen vision encoder. That keeps visual evidence useful without sending every patch token into the expensive language model.
Flamingo-style resampling: preserve more context without exploding token count[13]
Flamingo takes a different route to fusion. It first compresses variable-length visual features with a Perceiver Resampler, then lets language tokens attend to that compact bank through gated cross-attention blocks inserted into a frozen LM.[13]
The published resampler emits 64 visual tokens per image/video item, independent of its resolution or frame count.[13] Multiple items still create multiple banks. Flamingo uses a frozen contrastively pretrained NFNet-F6, not a CLIP ViT. Its cross-attention mask directly exposes the most recent preceding image/video to a text token; older visual information can flow indirectly through text self-attention. A gate initialized at zero initially preserves the frozen LM's behavior.[13]
Predict the workload where this boundary helps most. A single low-resolution photo may not justify cross-attention machinery, but the trade-off changes when a prompt contains:
- multiple images appear in one prompt
- video or long visual sequences are involved
- the language model needs to revisit visual evidence later in the answer
Resamplers and cross-attention can preserve a revisitable visual bank, but they add more moving parts than the simple LLaVA prefix. That complexity belongs in the design only when multiple images, long visual sequences, or repeated visual access pay it back.
When is a Flamingo-style resampler more attractive than simple projection?
Answer
Use it when prompts include multiple images, video frames, or long visual sequences where the language model needs to revisit visual evidence without paying for every raw patch token in the main prefix.
Qwen-VL: grounding and text reading beyond caption matching[14]
Qwen-VL (Qwen Vision-Language) shows what changes when grounding and text reading are first-class tasks. Its paper describes a visual receptor, an input-output interface, and a multi-stage training pipeline for OCR-heavy and box-grounded work in addition to generic image understanding.[14]
Those tasks need spatial signal. A tiny asset tag or a box around a vent can disappear when a high-resolution image is reduced to one pooled representation.
High-resolution token math
For a separate, generic budget example, suppose the tower uses 14×14 patches. One 336×336 crop becomes patch tokens; four crops become 2,304. This isn't Qwen-VL's exact preprocessing contract. More crops preserve more pixel detail, but quality needn't improve: seams, repeated regions, and missing global context can introduce errors. The projected language prefix still gets longer.
In a projected-prefix VLM, those admitted visual tokens cost twice. They lengthen prefill, and they enlarge the KV cache that must remain resident during decode. PagedAttention can pack and recycle that memory more efficiently, but it doesn't reduce the prefix token count.[15] Cross-attention designs keep a separate visual bank, so measure their memory and latency path separately.

Why can high-resolution document images hurt projected-prefix VLM serving twice?
Answer
They can create many prefix tokens, which makes prefill slower, and those same tokens enlarge the KV cache that must stay resident during decoding. Memory managers help pack the cache, but they don't erase token count. Cross-attention designs need separate measurement because they expose visual evidence through a different path.
In a projector path, the connector decides how many encoder features become autoregressive prefix tokens. Other architectures expose a separate cross-attention memory bank instead. The comparison below uses raw tiles and a fixed-size compression path to count tokens; it doesn't claim that compression preserves enough OCR evidence.
def prefix_tokens(text_tokens: int, crops: int, patch_tokens_per_crop: int, compressed_tokens: int | None) -> int:
if (type(text_tokens) is not int or text_tokens < 0
or any(type(x) is not int or x <= 0 for x in (crops, patch_tokens_per_crop))):
raise ValueError("invalid text, crop, or patch count")
visual_tokens = crops * patch_tokens_per_crop
if compressed_tokens is not None:
if type(compressed_tokens) is not int or not 0 < compressed_tokens <= visual_tokens:
raise ValueError("compression must retain a positive bounded token count")
visual_tokens = compressed_tokens
return text_tokens + visual_tokens
raw_prefix = prefix_tokens(text_tokens=180, crops=4, patch_tokens_per_crop=576, compressed_tokens=None)
compressed_prefix = prefix_tokens(text_tokens=180, crops=4, patch_tokens_per_crop=576, compressed_tokens=64)
print("raw_prefix_tokens:", raw_prefix)
print("compressed_prefix_tokens:", compressed_prefix)
print("tokens_avoided_if_quality_holds:", raw_prefix - compressed_prefix)raw_prefix_tokens: 2484
compressed_prefix_tokens: 244
tokens_avoided_if_quality_holds: 2240The 64-token case is a hypothetical prefix compressor over the complete crop set, not Flamingo's cross-attention architecture. These totals omit image separators, chat framing, and generated output; reserve those separately before admitting the request. If a compressor emits 64 tokens per crop instead, four crops contribute 256, not 64.
When detail is expendable, resize. When small objects or text matter, tile or crop the image and retain crop coordinates so OCR and boxes can map back to the original image. Compression can reduce language-model cost, but a fixed query count doesn't make arbitrary input resolutions compatible with a pretrained vision tower.
Whichever path you pick, measure the evidence path with the budget. Check resolution, tiling, OCR, connector compression, and the model's measured failure slices together. BLIP-2's Q-Former and Flamingo's resampler show ways to bound or mediate visual features before language reasoning, but neither removes the need to test the asset-tag slice.[12][13]
Adaptive pruning and merging under a latency budget
A fixed visual-token budget makes capacity predictable, but it spends the same allowance on a blank panel and a dense wiring diagram. An adaptive reducer estimates which tokens carry useful information for each image, then drops or merges the rest.
Pruning and merging are different operations:
- Pruning removes low-importance tokens. It's cheap, but discarded evidence can't be recovered.
- Merging combines redundant tokens into retained representatives. It can preserve more aggregate context, but similarity mistakes can blend distinct objects or text regions.
PruMerge is one researched example for projected-prefix VLMs.[16] It uses sparse attention between the vision encoder's class token and spatial tokens to select important tokens per image. It then clusters tokens by key similarity and folds related pruned tokens into retained representatives with weighted averaging. The paper evaluates it on LLaVA-1.5 and Video-LLaVA. That makes it a candidate reducer, not a guarantee for every encoder, OCR route, or domain.
The reducer still needs a hard serving contract. If a dense label panel generates more candidates than the route can afford, the cap must win:
The route should also reserve minimum coverage for task-critical evidence. A dashboard policy might keep tokens that overlap OCR boxes, small-object proposals, or selected crops before spending the remaining budget on saliency. A global spatial sample protects against an importance scorer that focuses on one object.
| Control | Why it matters |
|---|---|
| Hard maximum tokens | Protects p95 prefill, memory, and concurrency from dense outliers |
| Task-specific token floor | Prevents an easy latency win from erasing text, defects, or small objects |
| Spatial coverage | Keeps neglected regions represented when saliency is concentrated |
| Token-count buckets | Reduces padding waste when adaptive requests are batched together |
| Full-token shadow sample | Measures whether reduction changes answers, OCR, or grounding |
Placement determines which cost disappears. Reducing tokens after the vision encoder saves connector and language-model work, but it doesn't save the completed vision-encoder pass. A reducer inside the vision tower can save later encoder work, but it changes more of the model and needs separate evaluation.
Track the admitted-token distribution, reduction overhead, vision latency, prefill time, time to first token, peak memory, and quality by slice. For this inspection product, OCR exact match, small-hazard recall, and grounding IoU matter more than average VQA alone. If the hard cap would remove protected evidence, use a slower route or return review instead of silently forcing the image through the fast path.
Make that conflict visible with four invented token IDs already ranked by saliency. The warning label ranks last. Predict whether taking the first two IDs preserves the evidence needed by the technician.
def admit_tokens(ranked: list[str], protected: set[str], cap: int) -> list[str] | None:
if type(cap) is not int or cap <= 0 or len(set(ranked)) != len(ranked):
raise ValueError("positive cap and unique token IDs required")
if not protected <= set(ranked):
raise ValueError("protected evidence is absent from the candidates")
if len(protected) > cap:
return None # Policy cannot satisfy both contracts on this route.
selected = set(protected)
for token in ranked:
if len(selected) >= cap:
break
selected.add(token)
# Keep original token order separately in the model's actual input.
return [token for token in ranked if token in selected]
ranked = ["rack-door", "logo", "cable", "warning-text"]
protected = {"rack-door", "warning-text"} # Trusted task policy, not model permission.
print("saliency_only:", ranked[:2])
print("protected_selection:", admit_tokens(ranked, protected, cap=2))
print("cap_one:", "review" if admit_tokens(ranked, protected, cap=1) is None else "admitted")
assert admit_tokens(ranked, protected, 2) == ["rack-door", "warning-text"]
assert admit_tokens(ranked, protected, 3) == ["rack-door", "logo", "warning-text"]
try:
admit_tokens(ranked, {"unseen-tag"}, 2)
except ValueError as error:
assert str(error) == "protected evidence is absent from the candidates"
else:
raise AssertionError("missing evidence must not look like successful admission")saliency_only: ['rack-door', 'logo']
protected_selection: ['rack-door', 'warning-text']
cap_one: reviewBoth two-token results meet the cap, but only the protected selection includes the warning. At cap one, no valid fast-path subset exists, so the policy returns review instead of deleting required evidence. This fixture selects IDs; it doesn't detect text or implement PruMerge. A real reducer must preserve selected positions, spatial relationships, and the trained input contract, then measure whether its protected regions actually retain legible evidence.
Why does an adaptive token reducer still need a hard maximum and protected evidence floors?
Answer
Adaptive counts can spike on dense inputs, so a hard maximum protects the latency and memory contract. Protected floors keep the reducer from meeting that contract by deleting OCR regions, small objects, or other evidence the task requires.
Connector design controls a serving trade-off
The component between the vision encoder and the LLM answers two serving questions: what visual evidence reaches generation, and how many visual tokens the language path must handle. A connector can save context and still fail the barcode task if it compresses the wrong region.
Compare four connector contracts:
- Linear projection: original LLaVA's . Changes feature width without changing token count.[10]
- MLP projector: LLaVA-1.5's two-layer connector. Adds a nonlinear mapping; its benefit is an empirical model/training result, not a universal guarantee.[11]
- Q-Former / query transformer: compresses many visual features into a fixed learned token set.[12]
- Convolutional or locality-aware abstractors: preserve more spatial structure before handing features to the LLM.[17]
An overcompressed connector can remove OCR or grounding evidence. A large or expensive connector can increase latency or language-prefix cost. Measure both outcomes by task slice.
There isn't one best adapter. Choose against the task you have to protect:
- raw simplicity
- token efficiency
- spatial fidelity
- video or multi-image support
How do you choose between a linear projector, MLP, Q-Former, and locality-aware adapter?
Answer
Use linear or MLP projectors for simple, cheap alignment. Use Q-Former-style compression when token budget matters. Use locality-aware adapters when spatial detail, OCR, or grounding matters enough to spend more compute.
Designing around unpublished internals
Closed vendors may publish capability demos and benchmark numbers without enough architecture detail to justify an internals claim. Treat that missing detail as a boundary, not an invitation to guess.
Treat the model as an evaluated dependency. Measure supported input resolution, visual-token billing if exposed, latency by image count, OCR and grounding slices, refusal behavior, and output contract. Don't infer a projector type, token budget, or training curriculum unless the provider publishes it.
What is the safe way to discuss closed VLM architectures?
Answer
Describe observed API behavior and measured task performance. Don't claim projector types, token budgets, or training recipes unless the vendor published them.
Evaluation
The connector decision is only a hypothesis until evaluation shows which evidence survives. A VLM needs more than one benchmark because retrieval, captioning, OCR, grounding, and reasoning stress different abilities.
| Inspection task | Metric | Failure the metric can miss |
|---|---|---|
| Find related inspections | Recall@k over labeled relevant images | Unauthorized candidates or stale image versions |
| Copy the asset tag | Exact match and character error rate | Low coverage if the system abstains on difficult tags |
| Locate a blocked vent | Box IoU with defined matching, detection recall | Missing hazards if IoU is averaged only over detected objects |
| Explain the hazard | Human-verified claim support and unsupported-claim rate | Fluent but incorrect descriptions |
| Route uncertain cases | Risk versus coverage and review rate | A threshold that works at one site but fails at another |
Read the table as separate failure probes. A CLIP-like model can retrieve well and still fail counting. Reference-caption overlap can look fine while spatial reasoning is weak, and OCR-heavy tasks expose errors that general image description hides.
For the inspection product, keep these capability slices separate and set a pass bar for each one. A strong aggregate score can't rescue a route that loses asset-tag characters.
Why is one VLM benchmark insufficient for deployment?
Answer
Retrieval, captioning, VQA, OCR, expert reasoning, and prompt variation stress different capabilities. A model can look strong on one benchmark while failing counting, layout, text reading, or prompt-variation cases.
The small evaluation below computes metrics from explicit synthetic predictions. It doesn't call a vision model. Retrieval recall counts relevant IDs present in the top ten, OCR uses exact string equality, and box IoU is intersection area divided by union area in normalized image coordinates. One OCR character is wrong despite a relevant retrieved photo and an overlapping box.
import math
def box_iou(left: tuple, right: tuple) -> float:
for box in (left, right):
if (len(box) != 4 or any(not math.isfinite(x) or not 0 <= x <= 1 for x in box)
or box[0] >= box[2] or box[1] >= box[3]):
raise ValueError("expected normalized xyxy boxes with positive area")
width = max(0, min(left[2], right[2]) - max(left[0], right[0]))
height = max(0, min(left[3], right[3]) - max(left[1], right[1]))
intersection = width * height
area = lambda b: (b[2] - b[0]) * (b[3] - b[1])
return intersection / (area(left) + area(right) - intersection)
def evaluate(rows: list[dict]) -> dict[str, float]:
if not rows or any(not row["relevant"] for row in rows):
raise ValueError("this fixture requires rows with relevant-image labels")
return {
"retrieval_recall_at_10": sum(
len(set(row["retrieved"][:10]) & set(row["relevant"])) / len(set(row["relevant"]))
for row in rows) / len(rows),
"ocr_exact_match": sum(row["ocr"] == row["tag"] for row in rows) / len(rows),
"mean_box_iou": sum(box_iou(row["box"], row["truth_box"]) for row in rows) / len(rows),
}
rows = [
{"relevant": ["rack-a"], "retrieved": ["rack-a"], "tag": "A-17", "ocr": "A-17",
"truth_box": (0, 0, 1, 1), "box": (0, 0, 1, 1)},
{"relevant": ["rack-b"], "retrieved": ["other", "rack-b"], "tag": "B-08", "ocr": "B-O8",
"truth_box": (0, 0, 1, 1), "box": (0, 0, 1, 1)},
{"relevant": ["rack-c"], "retrieved": ["rack-c"], "tag": "C-42", "ocr": "C-42",
"truth_box": (0, 0, 1, 1), "box": (0, 0, 0.5, 0.5)},
]
metrics = evaluate(rows)
thresholds = {"retrieval_recall_at_10": 0.90, "ocr_exact_match": 0.92, "mean_box_iou": 0.70}
failed = [name for name, value in metrics.items() if value < thresholds[name]]
print({name: round(value, 3) for name, value in metrics.items()})
print("failed_slices:", failed){'retrieval_recall_at_10': 1.0, 'ocr_exact_match': 0.667, 'mean_box_iou': 0.75}
failed_slices: ['ocr_exact_match']This three-row fixture checks metric plumbing, not release readiness. Include missing-detection and no-hazard cases, false positives and false negatives, multiple boxes with explicit matching, grouped train/test splits, and uncertainty at realistic sample sizes. Mean IoU alone can hide missed hazards; OCR exact match alone can hide abstention coverage. Report both quality and review rate rather than averaging unlike metrics into one score.
Production applications
Document understanding
Document AI is a token-budget problem. Dense documents contain tiny text, tables, figures, and layout cues that don't survive aggressive resizing.
Start with page tiling or crop-based preprocessing when small regions matter. Add OCR features for text that has to be copied exactly, then send ambiguous or layout-heavy cases to a VLM. The stages preserve different kinds of evidence.
The design target isn't maximum raw resolution. Preserve the evidence that matters while keeping the visual token budget survivable.
What is the core design goal for document understanding with VLMs?
Answer
Preserve the evidence that matters, such as small text, tables, layout, and figures, while keeping visual tokens and prefill cost survivable. Maximum raw resolution isn't the goal by itself.
Image search and retrieval
For image search, start with a dual encoder rather than a generative VLM. The search contract is ranking, not explaining.
A dual-encoder model like CLIP or SigLIP lets you precompute image embeddings offline, index them once, and embed only the query at request time.
This split keeps first-stage latency and cost measurable. A generative VLM earns a place after retrieval only when reranking or explanation needs its extra evidence contract and measured cost.
Why is CLIP or SigLIP usually better than a generative VLM for first-pass image search?
Answer
You can precompute image embeddings offline, index them once, and embed only the query at request time. A generative VLM is more expensive and better used for reranking or explanations after retrieval.
The running rack photo now forks by evidence type. Similarity search, OCR, generation, and grounding are different contracts, so don't send every photo through the generative VLM because one ticket asked for a caption.

The function turns that fork into a routing plan only. It doesn't execute a model or enforce authorization. Retrieval stays on the dual encoder; exact label copying can stop at OCR, while interpretation may use a VLM. Clicking a disable port requires an independently enforced action policy and human approval bound to the current target and screenshot, not a model-written approved field.
def select_path(task: str) -> list[str]:
if task == "retrieve_similar_assets":
return ["dual_encoder", "vector_index"]
if task == "answer_from_label_text":
return ["ocr", "generative_vlm", "citation_check"]
if task == "click_disable_port":
return ["grounding_model", "action_policy", "human_confirm"]
raise ValueError(task)
for task in ["retrieve_similar_assets", "answer_from_label_text", "click_disable_port"]:
print(task, "->", " + ".join(select_path(task)))retrieve_similar_assets -> dual_encoder + vector_index
answer_from_label_text -> ocr + generative_vlm + citation_check
click_disable_port -> grounding_model + action_policy + human_confirmVisual agents
Visual agents need more than a captioning model. They need grounding, a link between an answer and the region or coordinate an action will use.
Suppose the agent must disable a port from a screenshot. "The blue button is near the top-right" isn't an executable target. The system may have to emit:
- click
(x, y) - select the second menu item
- type into the input field beneath a label
That usually requires a grounding-aware model or a separate grounding component on top of the core VLM, plus safety checks around actions. The same boundary applies to a rack hazard: a caption can explain, but policy needs a region and a human-confirmed action.
Why do visual agents need grounding beyond captioning?
Answer
Actions need coordinates, UI targets, object regions, or bounding boxes. A caption can say "the button is near the top-right," but an agent often needs a grounded action like click (x, y) with safety checks.
Common pitfalls
A useful diagnosis starts with the output contract that failed. Ask what the service returned, what evidence the task required, and where those two stopped matching.
"One pooled CLIP embedding understands an image like a person does"
If a team asks one global embedding for counts, boxes, or grounded actions, the output contract is already too broad. CLIP learned global alignment, not dense spatial supervision. Use detectors, grounding-aware VLMs, or region features when localization or counting matters.
"CLIP or SigLIP can also serve my chat and captioning path"
Similarity scores are the wrong output when the product needs an open-ended explanation or grounded dialogue. Keep CLIP or SigLIP for retrieval and zero-shot scoring, then use a generative VLM for the answer path.
"Higher resolution always means better OCR"
If document quality rises a little while latency and memory spike, resolution was increased without a token, tiling, or compression plan. Measure visual token count, tile selectively, preserve the evidence that matters, and compress before the LLM when possible.
"Visual serving cost ends when the encoder finishes"
Offline profiling can look cheap while production decode concurrency collapses. The missing terms are prefill latency and KV-cache growth from large visual prefixes, so track both with encoder cost before choosing image resolution or crop count.
"Adaptive pruning makes the latency budget self-enforcing"
Typical images can stay fast while dense screenshots create token spikes or erase tiny defects. That happens when the reducer has no hard maximum, protected evidence floor, or failure route. Cap the admitted sequence, preserve task-critical regions, bucket requests by retained count, and route unsafe reductions to a slower path or review.
"Every in-batch non-match is a clean negative"
Contrastive training can underperform on visually similar assets or captions that describe the same scene in different words. Some in-batch negatives are false negatives, so curate batches carefully, use richer captions, and treat batch design as a modeling choice.
"Zero-shot prompts will hold in fine-grained domains"
Prompt wording can swing predictions or collapse specialist categories when generic templates lack domain signal. Ensemble prompts, calibrate on validation data, and fine-tune or probe when domain precision matters.
What the inspection system must prove
Leave a reviewer with evidence for each route, not one attractive aggregate score. Your implementation is ready for review when you can:
- Explain how CLIP's dual-encoder architecture aligns images and text in a shared embedding space.
- Walk through the contrastive loss logic with a concrete batch example.
- Implement zero-shot classification using prompt engineering and embedding similarity.
- Diagnose why CLIP fails on spatial reasoning, counting, or fine-grained OCR.
- Compare LLaVA, BLIP-2, and Flamingo-style designs for connecting vision encoders to language models.
- Size the visual token budget for a production VLM and choose fixed or adaptive compression with explicit latency and evidence protections.
Evaluation rubric
- Derive both directions of CLIP loss and distinguish SigLIP's sum-over-pairs divided by batch size from a pair mean.
- Reject zero/nonfinite vectors, malformed batches, and invalid token budgets; distinguish cosine scores from candidate-relative softmax weights.
- Bind the index to the paired encoders and preprocessing, and keep permissions, image content, and action approval separate.
- Compute retrieval, OCR, and grounding metrics from predictions, then explain why synthetic arithmetic isn't pretrained model evaluation.
Follow-up questions
Why does softmax return 1.0 for a single candidate with logit -10?
Answer
The denominator contains only that candidate, so its normalized weight is one. This proves neither a good match nor the presence of a hazard. Candidate-relative weights need separate open-set and calibration tests.
For a two-example SigLIP batch with four zero logits, how do the paper's reduction and a pair mean differ?
Answer
Every pair contributes log(2). The paper's sum divided by two is 2 log(2), about 1.386. A mean over four pairs is log(2), about 0.693. The extra factor affects gradients as well as the reported loss.