n4nAI

What is a vision-language model and how it works

A precise technical definition of vision-language models, how they fuse vision and text, architecture patterns, and what engineers get wrong.

n4n Team6 min read1,361 words

Audio narration

Coming soon — every post will get a voice note here.

A vision-language model (VLM) is a neural network that jointly processes images and text within a single architecture, enabling it to reason across both modalities without hand-crafted pipelines. Unlike traditional approaches that chain separate vision and language models, VLMs learn shared representations where visual concepts and linguistic tokens inhabit the same embedding space. This unified approach is what powers systems like GPT-4o, Gemini 1.5 Pro, and Claude 3.5 Sonnet to describe images, answer visual questions, and generate code from screenshots.

How vision-language models work

At a high level, a VLM consists of three components: a vision encoder, a projection layer (or connector), and a language model backbone. The vision encoder — typically a ViT (Vision Transformer) like CLIP’s image tower or SigLIP — converts raw pixels into a sequence of patch embeddings. The projection layer maps these visual embeddings into the language model’s token embedding dimension. The language model then attends over the combined sequence of visual tokens and text tokens as if they were a single modality.

# Conceptual forward pass of a typical VLM
class VisionLanguageModel(nn.Module):
    def __init__(self, vision_encoder, projector, llm):
        super().__init__()
        self.vision_encoder = vision_encoder      # ViT, outputs [B, N_patches, D_vis]
        self.projector = projector                # MLP or attention pool, maps D_vis -> D_llm
        self.llm = llm                            # Decoder-only transformer

    def forward(self, images, input_ids, attention_mask):
        # images: [B, C, H, W] -> vision_encoder -> [B, N_patches, D_vis]
        visual_features = self.vision_encoder(images)
        
        # Project to LLM dimension: [B, N_patches, D_llm]
        visual_tokens = self.projector(visual_features)
        
        # Embed text tokens: [B, L_text, D_llm]
        text_embeddings = self.llm.embed_tokens(input_ids)
        
        # Concatenate: visual tokens first, then text (common convention)
        combined_embeddings = torch.cat([visual_tokens, text_embeddings], dim=1)
        
        # Build combined attention mask
        visual_mask = torch.ones(visual_tokens.shape[:2], device=visual_tokens.device)
        combined_mask = torch.cat([visual_mask, attention_mask], dim=1)
        
        # Single forward through LLM
        return self.llm(inputs_embeds=combined_embeddings, attention_mask=combined_mask)

Training stages

Most production VLMs follow a multi-stage training recipe:

Stage 1: Contrastive alignment — Train the vision encoder and a text encoder (or the LLM’s embedding layer) with a contrastive loss like CLIP. This aligns image and text representations before the LLM sees visual tokens. The model learns that “a photo of a cat” and the image of a cat should have high cosine similarity.

Stage 2: Vision-language pretraining — Freeze the vision encoder and LLM, train only the projector on massive image-text pair datasets (LAION, COYO, DataComp). The projector learns to translate ViT patch embeddings into “soft tokens” the LLM can understand.

Stage 3: Supervised fine-tuning (SFT) — Unfreeze the LLM (and sometimes the projector) and train on instruction-following data: VQA, captioning, OCR, visual reasoning. This is where the model learns to use visual information to answer questions rather than just align representations.

Stage 4: Preference optimization — DPO or RLHF on visual tasks to reduce hallucination and improve instruction adherence.

Architecture variants

Not all VLMs use the same connector strategy. Three patterns dominate:

Approach Description Examples
MLP projector 2-layer MLP (often with GELU) mapping each patch independently LLaVA-1.5, LLaVA-NeXT
Attention pooling Cross-attention queries compress variable patch counts to fixed token budget Qwen-VL, InternVL
Resampler / Perceiver Learned latent queries attend to patch embeddings, outputting fixed-length sequence Flamingo, IDEFICS, Gemini

The attention pooling and resampler approaches matter when you need a fixed visual token budget regardless of input resolution — critical for long-context VLMs processing high-resolution images or video frames.

Why vision-language models matter for engineers

If you’re building LLM applications, VLMs change the system architecture in three concrete ways:

1. Elimination of the OCR + LLM pipeline

Before VLMs, extracting structured data from documents meant: PDF → rasterize → OCR (Tesseract, PaddleOCR, Azure Form Recognizer) → post-process → LLM. Each stage introduced latency, error propagation, and maintenance burden. A VLM replaces the entire pipeline with a single forward pass.

# Before: Multi-stage pipeline
def extract_invoice_fields_old(pdf_path):
    images = pdf_to_images(pdf_path)                    # 200-500ms/page
    ocr_results = [paddle_ocr(img) for img in images]   # 300-800ms/page
    cleaned_text = postprocess_ocr(ocr_results)         # heuristic rules
    return llm_extract(cleaned_text, schema)            # separate API call

# After: Single VLM call
def extract_invoice_fields_vlm(pdf_path):
    images = pdf_to_images(pdf_path)
    prompt = "Extract invoice fields as JSON: vendor, total, date, line_items"
    return vlm.generate(images, prompt, response_format=InvoiceSchema)

The VLM approach handles layout, tables, handwriting, and visual context (logos, checkboxes) that pure OCR misses. Latency drops from seconds to hundreds of milliseconds per page.

2. Visual reasoning as a primitive

VLMs enable capabilities that were previously separate services: chart interpretation, diagram-to-code, UI understanding, defect detection. You can now write evals for “can the model convert this Figma screenshot to React components?” and treat it as a unit test.

# Visual regression testing with a VLM
def test_ui_matches_design(screenshot_path, design_spec):
    prompt = f"""
    Compare this screenshot to the design spec. Return JSON:
    {{
      "matches": bool,
      "discrepancies": [
        {{"element": str, "expected": str, "actual": str, "severity": "high|medium|low"}}
      ]
    }}
    Design spec: {design_spec}
    """
    result = vlm.generate(screenshot_path, prompt, response_format=VisualDiff)
    assert result.matches, f"Visual regressions: {result.discrepancies}"

3. Multimodal RAG without separate embedding models

Traditional multimodal RAG requires separate image embeddings (CLIP) and text embeddings, then a fusion strategy at query time. With a VLM, you can embed entire documents (text + images + tables) as interleaved sequences and retrieve with the same model that generates answers. This reduces index complexity and improves retrieval relevance because the query and document share the exact same representation space.

Concrete example: Building a screenshot-to-code tool

Let’s walk through a realistic engineering task: convert a dashboard screenshot to a working React component with Tailwind.

from pydantic import BaseModel
from typing import Literal

class ComponentSpec(BaseModel):
    component_name: str
    props: list[dict]
    jsx: str
    tailwind_config: dict | None = None

SYSTEM_PROMPT = """You are a senior frontend engineer. Convert the UI screenshot to a production-ready React component.
- Use TypeScript, React 18, Tailwind CSS
- Extract reusable sub-components
- Handle responsive breakpoints (sm, md, lg, xl)
- Use semantic HTML and ARIA attributes
- Output ONLY valid JSON matching the schema"""

def screenshot_to_component(image_path: str, vlm_client) -> ComponentSpec:
    result = vlm_client.generate(
        images=[image_path],
        prompt=SYSTEM_PROMPT,
        response_format=ComponentSpec,
        temperature=0.1,
        max_tokens=8192
    )
    return result.parsed

What the VLM actually does here:

  1. Layout decomposition — Identifies grid/flex structure, spacing tokens, alignment
  2. Component recognition — Maps visual patterns to known UI primitives (cards, tables, charts, navbars)
  3. Style extraction — Infers color palette, border radius, shadows, typography scale from pixels
  4. State inference — Detects interactive states (hover, focus, loading, empty) from visual cues
  5. Code synthesis — Generates structured JSX with proper component composition

The same model handles a marketing landing page, a data-dense admin panel, or a mobile app screen because it learned visual-language correspondences at scale — not from hand-written heuristics.

Common misconceptions

“VLMs are just LLMs with CLIP glued on”

This was true for early models (Flamingo, BLIP-2), but modern VLMs like LLaVA-NeXT, Qwen2-VL, and Molmo train the vision encoder and LLM together in later stages. The vision encoder adapts to the LLM’s representation space, and the LLM adapts to visual tokens. Treating them as frozen modules leaves performance on the table.

“Higher resolution always helps”

Naively increasing input resolution (e.g., 336px → 1024px) quadratically increases visual tokens. A 1024×1024 image at patch size 14 yields 5,376 tokens — exceeding the context window of many LLMs. Production systems use dynamic resolution: the VLM processes multiple crops at native resolution (e.g., 448×448 tiles) and the projector compresses each tile to a fixed token budget. Qwen2-VL and InternVL2 use this approach; LLaVA-NeXT uses anyres with a fixed grid.

# Dynamic tiling strategy (simplified)
def dynamic_preprocess(image, min_tiles=1, max_tiles=6, tile_size=448):
    """Split image into variable tiles based on aspect ratio."""
    w, h = image.size
    aspect = w / h
    
    # Find grid that minimizes padding while staying in tile budget
    best_grid = (1, 1)
    best_score = float('inf')
    for rows in range(1, max_tiles + 1):
        for cols in range(1, max_tiles + 1):
            if rows * cols > max_tiles or rows * cols < min_tiles:
                continue
            # Score by how close tile aspect matches image aspect
            tile_aspect = (cols * tile_size) / (rows * tile_size)
            score = abs(tile_aspect - aspect)
            if score < best_score:
                best_score = score
                best_grid = (rows, cols)
    
    rows, cols = best_grid
    tiles = []
    for r in range(rows):
        for c in range(cols):
            box = (c * tile_size, r * tile_size, (c+1) * tile_size, (r+1) * tile_size)
            tiles.append(image.crop(box))
    return tiles

“VLMs solve OCR perfectly”

They don’t. VLMs struggle with: dense tables spanning pages, rotated text, low-contrast scans, and character-level accuracy on long strings (serial numbers, codes). For high-stakes extraction, pair a VLM with a specialized OCR engine — use the VLM for layout detection and reading order, OCR for character fidelity.

“One VLM fits all visual tasks”

A model trained on natural images (COCO, web data) performs poorly on: technical diagrams, medical imaging, satellite imagery, UI screenshots, handwritten math. Domain-specific VLMs (e.g., DocVLM for documents, ChartVLM for plots, Med-Flamingo for radiology) exist because the visual vocabulary differs. If your use case is narrow, fine-tune or use a specialist.

“Visual tokens are free context”

Each visual token consumes context window exactly like a text token. A 1024×1024 image at 14px patches = 5,376 tokens. At 128K context, that’s ~4% of your budget per image. Video at 1fps for 60 seconds = 300K+ tokens — you must compress. This is why resamplers (Flamingo, Gemini) and token merging (ToMe, FastV) matter in production.

Evaluation: What to measure

Don’t rely on academic benchmarks (MMMU, MMBench, TextVQA) alone. They correlate poorly with your specific failure modes. Build evals for your task:

# Example: Custom eval for your screenshot-to-code pipeline
EVAL_CASES = [
    {
        "name": "dashboard_with_charts",
        "image": "fixtures/dashboard.png",
        "must_have": ["<LineChart", "<BarChart", "recharts", "ResponsiveContainer"],
        "must_not_have": ["<img", "background-image", "hardcoded_data"],
        "accessibility": ["aria-label", "role="],
    },
    {
        "name": "mobile_nav_bar",
        "image": "fixtures/mobile_nav.png",
        "must_have": ["useState", "useEffect", "md:flex", "hidden"],
        "responsive_breakpoints": ["sm:", "md:", "lg:"],
    },
]

def evaluate_vlm(vlm_client, cases=EVAL_CASES):
    results = []
    for case in cases:
        output = screenshot_to_component(case["image"], vlm_client)
        jsx = output.jsx
        
        checks = {
            "required_imports": all(m in jsx for m in case.get("must_have", [])),
            "no_anti_patterns": all(m not in jsx for m in case.get("must_not_have", [])),
            "accessibility": all(m in jsx for m in case.get("accessibility", [])),
            "responsive": all(bp in jsx for bp in case.get("responsive_breakpoints", [])),
        }
        results.append({"case": case["name"], **checks, "pass": all(checks.values())})
    return results

Measure: task success rate, token efficiency (output quality per input token), latency p50/p99, and hallucination rate (invented props, non-existent components).

Deployment considerations

Quantization

VLMs are larger than pure LLMs (vision encoder + projector + LLM). A 7B VLM is ~14GB FP16. Quantize to 4-bit (AWQ, GPTQ, GGUF) for GPU inference; the vision encoder is often more sensitive than the LLM — keep it at 8-bit or FP16 if accuracy drops.

# Example: Quantize LLaVA-NeXT with AWQ, keep vision encoder higher precision
python quantize.py \
  --model llava-hf/llava-v1.6-vicuna-7b-hf \
  --quant-method awq \
  --bits 4 \
  --group-size 128 \
  --modules-to-not-convert "vision_tower,multi_modal_projector" \
  --output-dir llava-7b-awq-4bit

Batching and padding

Visual token counts vary by image resolution and tiling strategy. Pad to strategy. Your inference server must support variable-length sequences with attention masking — not just left-padding. vLLM, TGI, and TensorRT-LLM handle this, but custom engines often don’t.

Caching

The vision encoder output for a given image is deterministic. Cache it. If users upload the same screenshot twice (common in dashboard tools), skip the ViT forward pass entirely.

# Vision encoder cache (Redis, disk, in-memory)
class CachedVisionEncoder:
    def __init__(self, vision_encoder, cache):
        self.encoder = vision_encoder
        self.cache = cache  # key: image_hash -> value: visual_features
    
    def forward(self, images):
        hashes = [hash_image(img) for img in images]
        cached = {}
        to_encode = []
        to_encode_idx = []
        
        for i, h in enumerate(hashes):
            if h in self.cache:
                cached[i] = self.cache[h]
            else:
                to_encode.append(images[i])
                to_encode_idx.append(i)
        
        if to_encode:
            new_features = self.encoder(torch.stack(to_encode))
            for idx, feat in zip(to_encode_idx, new_features):
                self.cache[hashes[idx]] = feat
                cached[idx] = feat
        
        # Reorder to original batch order
        return torch.stack([cached[i] for i in range(len(images))])

Where this fits in your stack

If you’re routing requests across multiple providers — some with VLMs, some without — you need a gateway that understands model capabilities and forwards the right request shape. n4n.ai’s routing directives let you specify modalities: ["image", "text"] and the gateway selects a compatible endpoint, handles fallback when a vision model is rate-limited, and normalizes usage metering across providers. The provider’s cache-control hints also pass through, so repeated image analysis benefits from prefix caching where supported.

Closing thought

Vision-language models are not “LLMs with eyes.” They are a distinct architecture class with different scaling laws, failure modes, and deployment constraints. Treat them as such: evaluate on your tasks, quantize carefully, cache the vision encoder, and build evals that catch the visual reasoning errors your users will actually hit.

Tagsvision-language-modelvlmmultimodal-ai

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All vision-language models: gpt-5, gemini 3 & claude opus 4.8 posts →