Skip to content
AILinkDeepTech
Go back
Reinforcement Learning Medium

LLaVA: Visual Instruction Tuning, Cross-Modal Projection, and PyTorch Architecture

Abstract

Master LLaVA: visual instruction tuning, 2-stage cross-modal projection, CLIP/SigLIP feature alignment, AnyRes scaling, and PyTorch VLM pipelines.

Cross-Modal Alignment and the Limits of Bottleneck Adapters

Vision-Language Models (VLMs) bridge continuous visual representations with discrete autoregressive language models. Prior multimodal architectures introduced complex intermediate cross-attention modules or bottleneck queries:

  1. Flamingo (Alayrac et al., NeurIPS 2022): Interleaves gated cross-attention layers inside every frozen transformer block. While expressive, the architecture incurs significant compute overhead and requires training from scratch across multi-billion-token multimodal corpora.
  2. BLIP-2 / Q-Former (Li et al., ICML 2023): Employs a Querying Transformer with learnable bottleneck queries to compress visual features via cross-attention. While computationally compact, compressing spatial patches into discrete tokens creates an information bottleneck that degrades long-form conversational grounding and spatial reasoning.

LLaVA (Large Language and Vision Assistant; Liu et al., NeurIPS 2023) resolves this bottleneck by treating visual patches as direct pseudo-token embeddings. By mapping uncompressed visual patch features into the LLM token space via a 2-layer MLP projector and training on GPT-4-synthesized visual instruction-following dialogues, LLaVA establishes a scalable, compute-efficient paradigm for open-source multimodal generation.


Architectural Comparison

Architectural DimensionFlamingo (DeepMind)BLIP-2 (Salesforce)MiniGPT-4 (KAUST)LLaVA-1.5 (Liu et al.)LLaVA-NeXT / OneVision (2024)
Vision BackboneFrozen NFNet / ViT-LFrozen EVA-CLIP ()Frozen EVA-CLIPFrozen CLIP-ViT-L-336pFrozen SigLIP-SO400M
Cross-Modal AdapterInterleaved Cross-AttentionQ-Former ( Queries)Single Linear Layer2-Layer MLP ()2-Layer MLP ()
Visual Token Count
Visual CompressionLossy ResamplerHeavy BottleneckHeavy BottleneckUncompressed Patch MappingUncompressed Multi-Grid Mapping
LLM BackboneChinchilla ()OPT / Flan-T5Vicuna-13BLLaMA-2 / Vicuna ()Qwen2 / Llama-3 ()
Instruction TuningWeb Scraping / Few-ShotPretraining / VQA Pairs GPT-4 Dialogues Multi-Turn Dialogues Consolidated Multimodal
Training Budget

Mathematical Foundations

flowchart TD IMAGE["Input Image I in R^(3 x H x W)\n(e.g., 336 x 336 x 3)"] --> VIT["Frozen CLIP ViT-L/14-336p\nPatch Extraction (p=14)"] VIT --> PATCHES["Spatial Patch Grid Z_v in R^(576 x 1024)\n(CLS Token Discarded)"] PATCHES --> MLP["Trainable 2-Layer MLP Projector phi\nW_2 * GELU(W_1 * Z_v + b_1) + b_2"] MLP --> V_TOKENS["Visual Pseudo-Tokens H_v in R^(576 x D_l)\n(D_l = 4096 for 7B, 5120 for 13B)"] TEXT["Input Prompt x_prompt\ne.g., 'USER: Describe this scene.'"] --> TOKENIZER["LLM Tokenizer & Embedding Table E"] TOKENIZER --> T_TOKENS["Text Token Embeddings H_t in R^(L x D_l)"] V_TOKENS --> CONCAT["Sequence Concatenation:\nH_input = [H_v ; H_t] in R^((576+L) x D_l)"] T_TOKENS --> CONCAT CONCAT --> LLM["Autoregressive LLM Decoder\n(LLaMA-2 / Vicuna / Mistral)\nCausal Self-Attention across All Tokens"] LLM --> LOSS["Next-Token Prediction on Assistant Responses\n(Visual Token Positions Masked with labels=-100)"]

Figure 1: LLaVA cross-modal architecture topology: uncompressed spatial patch features projected directly into LLM token embeddings.

1. Spatial Patch Extraction and Feature Formulation

Given an input image , a frozen Vision Transformer (e.g., CLIP ViT-L/14-336) extracts spatial grid representations using a non-overlapping convolutional patch projection with patch size ():

Passing through transformer layers yields hidden activations where . Discarding the class token [CLS] isolates uncompressed spatial patch features:


2. Cross-Modal MLP Projection Mapping

The projection module aligns vision representations with the language model embedding dimension ( for LLaMA-2-7B, for 13B). In LLaVA-1.5, is parameterized as a two-layer Multi-Layer Perceptron with activation:

where , , .

Information Capacity Comparison:

Let represent the visual token capacity passed to the LLM:

LLaVA provides an higher information throughput, preserving localized spatial boundaries essential for dense scene interpretation, chart reading, and visual grounding.


3. Unified Sequence Construction & Autoregressive Objective

Given a multi-turn conversation comprising language tokens , the token embedding lookup yields text embeddings .

The visual pseudo-tokens and text embeddings are concatenated into a single input matrix:

The joint probability of generating sequence is computed autoregressively:


4. Two-Stage Optimization Protocol

Training decouples visual-semantic alignment from conversational instruction tuning across two distinct optimization phases:

flowchart LR subgraph STAGE1["Stage 1: Pre-training (Feature Alignment)"] direction TB S1_DATA["CC3M-595K / LCS-558K Image Captions\n'Describe the image briefly.'"] S1_VIT["Vision Encoder: FROZEN"] S1_PROJ["MLP Projector: TRAINABLE (lr = 1e-3)"] S1_LLM["LLM Backbone: FROZEN"] end subgraph STAGE2["Stage 2: Visual Instruction Tuning"] direction TB S2_DATA["LLaVA-Instruct-150K + ShareGPT4V-158K\n(Multi-Turn Complex Reasoning)"] S2_VIT["Vision Encoder: FROZEN"] S2_PROJ["MLP Projector: TRAINABLE (lr = 2e-5)"] S2_LLM["LLM Backbone: TRAINABLE / LoRA (lr = 2e-5)"] end STAGE1 --> STAGE2

Figure 2: Two-stage optimization protocol ensuring modular cross-modal alignment without linguistic catastrophic forgetting.

Stage 1: Feature Alignment Pretraining

Only the projector parameters are updated using image-caption pairs :

Stage 2: Visual Instruction Tuning

The vision encoder remains frozen, while the projector and language model (or LoRA parameters ) are jointly optimized on instruction datasets:

Visual token positions and user prompt tokens are masked from gradient computation by assigning loss target indices to .


5. AnyRes High-Resolution Grid Partitioning (LLaVA-NeXT)

To preserve small-scale features (text, symbols, fine textures) beyond fixed resolutions, LLaVA-NeXT introduces AnyRes dynamic grid partitioning.

An arbitrary high-resolution image is resized and split into a grid of sub-images, accompanied by a global downsampled thumbnail :

For a input ( grid + thumbnail):

Visual tokens from each grid cell are delimited using specialized <image_break> tokens, maintaining 2D spatial coordinate topology inside the 1D causal attention context.


Production PyTorch Implementation

Below is a complete, modular PyTorch implementation of the MultimodalProjector, Vision-Language Sequence Formatter, and End-to-End LLaVA Model Architecture.

Step 1: Modular MLP Projector & Vision Tower Wrapper

from __future__ import annotations

import torch
import torch.nn as nn
from transformers import CLIPVisionModel


class MultimodalProjector(nn.Module):
    """2-Layer MLP Projector mapping Vision Features to LLM Token Dimension."""

    def __init__(self, vision_dim: int = 1024, llm_dim: int = 4096) -> None:
        super().__init__()
        self.linear_1 = nn.Linear(vision_dim, llm_dim, bias=True)
        self.act = nn.GELU()
        self.linear_2 = nn.Linear(llm_dim, llm_dim, bias=True)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return self.linear_2(self.act(self.linear_1(x)))


class CLIPVisionTower(nn.Module):
    """Frozen CLIP Vision Tower extracting uncompressed spatial patch features."""

    def __init__(self, vision_tower_id: str = "openai/clip-vit-large-patch14-336") -> None:
        super().__init__()
        self.vision_tower = CLIPVisionModel.from_pretrained(vision_tower_id)
        self.vision_tower.requires_grad_(False)
        self.vision_tower.eval()

    @torch.no_grad()
    def forward(self, pixel_values: torch.Tensor) -> torch.Tensor:
        # [B, 3, 336, 336] -> Hidden states: [B, 577, 1024]
        outputs = self.vision_tower(pixel_values, output_hidden_states=False)
        # Discard [CLS] token at position 0, keep 576 patch features
        return outputs.last_hidden_state[:, 1:, :]

Step 2: Full LLaVA Multimodal Causal Architecture

from __future__ import annotations

import torch
import torch.nn as nn
from llava_modules import CLIPVisionTower, MultimodalProjector
from transformers import AutoModelForCausalLM, AutoTokenizer


class LlavaForConditionalGeneration(nn.Module):
    """Complete LLaVA Vision-Language Architecture."""

    def __init__(
        self,
        llm_model_id: str = "meta-llama/Llama-2-7b-chat-hf",
        vision_tower_id: str = "openai/clip-vit-large-patch14-336",
        image_token_id: int = 32000,
    ) -> None:
        super().__init__()
        self.vision_tower = CLIPVisionTower(vision_tower_id)
        self.projector = MultimodalProjector(vision_dim=1024, llm_dim=4096)
        self.language_model = AutoModelForCausalLM.from_pretrained(
            llm_model_id,
            torch_dtype=torch.bfloat16,
        )
        self.image_token_id = image_token_id

    def forward(
        self,
        input_ids: torch.Tensor,
        pixel_values: torch.Tensor | None = None,
        labels: torch.Tensor | None = None,
        attention_mask: torch.Tensor | None = None,
    ) -> dict[str, torch.Tensor]:
        # 1. Embed text tokens
        text_embeddings = self.language_model.get_input_embeddings()(input_ids)

        if pixel_values is not None:
            # 2. Extract and project visual patch features
            with torch.no_grad():
                patch_features = self.vision_tower(pixel_values)
            visual_embeddings = self.projector(patch_features) # [B, 576, 4096]

            # 3. Fuse visual tokens into <image> placeholder slots
            inputs_embeds = []
            fused_labels = [] if labels is not None else None

            for i in range(input_ids.shape[0]):
                cur_input_ids = input_ids[i]
                cur_text_embeds = text_embeddings[i]
                cur_vis_embeds = visual_embeddings[i]

                # Identify image token positions
                img_positions = (cur_input_ids == self.image_token_id).nonzero(as_tuple=True)[0]
                if len(img_positions) == 0:
                    inputs_embeds.append(cur_text_embeds)
                    if labels is not None:
                        fused_labels.append(labels[i])
                    continue

                # Splice visual tokens
                pos = img_positions[0].item()
                fused_embed = torch.cat(
                    [cur_text_embeds[:pos], cur_vis_embeds, cur_text_embeds[pos + 1 :]], dim=0
                )
                inputs_embeds.append(fused_embed)

                if labels is not None:
                    cur_labels = labels[i]
                    # Mask visual token positions with -100 (ignored in loss computation)
                    vis_labels = torch.full((cur_vis_embeds.shape[0],), -100, dtype=cur_labels.dtype, device=cur_labels.device)
                    fused_lbl = torch.cat(
                        [cur_labels[:pos], vis_labels, cur_labels[pos + 1 :]], dim=0
                    )
                    fused_labels.append(fused_lbl)

            inputs_embeds = torch.stack(inputs_embeds, dim=0)
            if labels is not None:
                labels = torch.stack(fused_labels, dim=0)
        else:
            inputs_embeds = text_embeddings

        # 4. Pass through LLM
        return self.language_model(
            inputs_embeds=inputs_embeds,
            labels=labels,
            attention_mask=attention_mask,
            return_dict=True,
        )

Step 3: Production Visual Instruction Training Step

from __future__ import annotations

import torch
from llava_model import LlavaForConditionalGeneration


def train_step_stage2(
    model: LlavaForConditionalGeneration,
    optimizer: torch.optim.Optimizer,
    batch: dict[str, torch.Tensor],
) -> float:
    """Executes Stage 2 Visual Instruction Tuning forward/backward pass."""
    model.train()
    model.vision_tower.eval() # Vision tower strictly frozen
    optimizer.zero_grad(set_to_none=True)

    input_ids = batch["input_ids"].cuda()
    pixel_values = batch["pixel_values"].cuda()
    labels = batch["labels"].cuda()
    attention_mask = batch["attention_mask"].cuda()

    with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
        outputs = model(
            input_ids=input_ids,
            pixel_values=pixel_values,
            labels=labels,
            attention_mask=attention_mask,
        )
        loss = outputs.loss

    loss.backward()
    torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
    optimizer.step()

    return loss.item()

Empirical Benchmark Evaluation

Quantitative evaluation comparing open-source and proprietary vision-language architectures across standardized visual reasoning benchmarks:

Model ArchitectureVision BackboneLLM ParametersScienceQA (Img )MME Total ()MMBench ()MMMU ()POPE F1 ()
BLIP-2EVA-CLIP () (Flan-T5)
InstructBLIPEVA-CLIP () (Vicuna)
MiniGPT-4EVA-CLIP () (Vicuna)
LLaVA-1.5 (7B)CLIP-ViT-L () (Vicuna)
LLaVA-1.5 (13B)CLIP-ViT-L () (Vicuna)
LLaVA-NeXT (7B)CLIP-ViT-L (AnyRes) (Mistral)
LLaVA-NeXT (34B)CLIP-ViT-L (AnyRes) (Yi)
LLaVA-OneVision (7B)SigLIP-SO400M (AnyRes) (Qwen2)
GPT-4V (Proprietary)UnknownClosed Frontier

Troubleshooting Common Multimodal Deployment Faults

1. Object Hallucination on Non-Existent Visual Entities

  • Symptom: Model asserts the presence of common contextual objects (e.g., cups, chairs, animals) not visible in the input image.
  • Root Cause: Language model priors dominating visual patch activations due to over-represented semantic co-occurrences in text pretraining.
  • Remedy: Evaluate hallucination rates via the POPE (Polling-based Object Probing Evaluation) benchmark and prepend explicit grounding constraints: "Answer strictly based on visually verifiable objects. If an object is not clearly visible, explicitly state its absence."

2. Loss of Fine-Grained Text and Dense OCR Degradation

  • Symptom: Text strings inside receipts, license plates, and diagrams are transcribed with high character error rates.
  • Root Cause: Single-tile resolution downsamples text regions below the patch receptive field Nyquist limit.
  • Remedy: Upgrade from standard LLaVA-1.5 to LLaVA-NeXT / OneVision AnyRes dynamic grid partitioning ( sub-grids + global context thumbnail).

3. Gradient Explosion During Stage 2 Joint Tuning

  • Symptom: Training loss evaluates to NaN within the first 50 iterations when unfreezing the language model.
  • Root Cause: Learning rate disparity between the randomly initialized/adapted MLP projector and the pretrained LLM weights.
  • Remedy: Enforce distinct learning rate schedules: apply lr = 2e-5 with cosine decay and warmup ratio for the LLM, while keeping the vision encoder strictly frozen in eval() mode.

References

  1. Liu, H., Li, C., Wu, Q., & Lee, Y. J. (2023). Visual Instruction Tuning. Advances in Neural Information Processing Systems (NeurIPS 2023).
  2. Liu, H., Li, C., Li, Y., & Lee, Y. J. (2024). Improved Baselines with Visual Instruction Tuning (LLaVA-1.5). CVPR 2024.
  3. Li, C., et al. (2024). LLaVA-NeXT: Improved reasoning, OCR, and world knowledge.
  4. Li, B., et al. (2024). LLaVA-OneVision: Easy Visual Task Transfer. arXiv:2408.03326.
  5. Radford, A., et al. (2021). Learning Transferable Visual Models From Natural Language Supervision (CLIP). ICML 2021.
  6. Alayrac, J. B., et al. (2022). Flamingo: a Visual Language Model for Few-Shot Learning. NeurIPS 2022.


Cite this Explanation

@article{ailinkdeeptech2025llava,
  title={LLaVA: Visual Instruction Tuning, Cross-Modal Projection, and PyTorch Architecture},
  author={AILinkDeepTech},
  journal={AILinkDeepTech Algorithm Explanations},
  year={2025},
  url={https://ailinkdeeptech.com/research/llava}
}

Related Explanations