Skip to content

ViT and DiT

Quick map

  • ViT (Vision Transformer): use the Transformer encoder (bidirectional self-attention; no causal mask) over a sequence of image patch tokens for classification and representation learning.
  • DiT (Diffusion Transformer): (standard DiT) also uses an encoder-style Transformer backbone (bidirectional self-attention; no causal mask) inside a diffusion model to predict noise / velocity / score-like targets over latent/image tokens.

If you’re coming from the LLM note, see the self-attention recap in transformer.md.

ViT (Vision Transformer)

Core idea

  • Tokenization: split an image into fixed-size patches (e.g., 16×16), flatten each patch, and project to an embedding dimension.
  • Add positional embeddings (learned or sinusoidal-like), then run a Transformer encoder.
  • Use a special classification token [CLS] (original ViT) or pooled token features for classification.

A common abstraction:

  • Patches: \(x \in \mathbb{R}^{H\times W\times C}\) → \(N\) patches → \(X \in \mathbb{R}^{N\times d}\)
  • Encoder: \(Z = \mathrm{Encoder}(X + \mathrm{PosEmbed})\)
  • Class head: \(\hat y = \mathrm{head}(Z_{\mathrm{cls}})\) (if using [CLS])

ViT Fig 1

What to pay attention to (implementation details)

  • Patch embedding layer: typically Conv2d(kernel=p, stride=p) is equivalent to patchify + linear.
  • Positional embeddings: learned absolute embeddings are common; resizing/interpolation is needed when changing resolution.
  • Regularization and data: ViT often needs strong augmentation and large-scale pretraining to match CNN baselines.

Minimal code sketch (patch embedding + tokens):

import torch
import torch.nn as nn


class PatchEmbed(nn.Module):
    """Image -> patch tokens using a strided conv."""

    def __init__(self, in_chans=3, d_model=768, patch_size=16):
        super().__init__()
        self.patch_size = patch_size
        self.proj = nn.Conv2d(in_chans, d_model, kernel_size=patch_size, stride=patch_size)

    def forward(self, x):
        # x: (B, C, H, W) with H,W divisible by patch_size
        x = self.proj(x)              # (B, d_model, H/p, W/p)
        x = x.flatten(2).transpose(1, 2)  # (B, N, d_model)
        return x


def make_vit_tokens(x, patch_embed, cls_token, pos_embed):
    """Returns token sequence ready for a Transformer encoder."""
    tok = patch_embed(x)  # (B, N, d)
    B = tok.shape[0]
    cls = cls_token.expand(B, -1, -1)  # (B, 1, d)
    tok = torch.cat([cls, tok], dim=1)  # (B, 1+N, d)
    tok = tok + pos_embed[:, : tok.shape[1]]
    return tok


# Example shapes (not a full model):
# x = torch.randn(2, 3, 224, 224)
# patch = PatchEmbed(in_chans=3, d_model=192, patch_size=16)
# cls_token = nn.Parameter(torch.zeros(1, 1, 192))
# pos_embed = nn.Parameter(torch.zeros(1, 1 + (224 // 16) ** 2, 192))
# tokens = make_vit_tokens(x, patch, cls_token, pos_embed)

Minimal attention block (ViT encoder block):

import torch
import torch.nn as nn


class ViTEncoderBlock(nn.Module):
    """Pre-LN encoder block used in most ViT-style implementations."""

    def __init__(self, d_model, n_heads, d_ff, dropout=0.0):
        super().__init__()
        self.ln1 = nn.LayerNorm(d_model)
        self.attn = nn.MultiheadAttention(
            embed_dim=d_model,
            num_heads=n_heads,
            dropout=dropout,
            batch_first=True,  # x: (B, N, C)
        )
        self.ln2 = nn.LayerNorm(d_model)
        self.mlp = nn.Sequential(
            nn.Linear(d_model, d_ff),
            nn.GELU(),
            nn.Dropout(dropout),
            nn.Linear(d_ff, d_model),
            nn.Dropout(dropout),
        )

    def forward(self, x):
        # x: (B, N, d_model)
        # Residual pathway (same ResNet-style "passway"): x <- x + F(x)
        attn_out, _ = self.attn(self.ln1(x), self.ln1(x), self.ln1(x), need_weights=False)
        x = x + attn_out
        x = x + self.mlp(self.ln2(x))
        return x

Minimal full ViT encoder skeleton (patch embed -> blocks -> head):

import torch
import torch.nn as nn


class ViT(nn.Module):
    def __init__(
        self,
        image_size=224,
        patch_size=16,
        in_chans=3,
        n_classes=1000,
        d_model=768,
        depth=12,
        n_heads=12,
        d_ff=3072,
        dropout=0.0,
    ):
        super().__init__()
        assert image_size % patch_size == 0
        n_patches = (image_size // patch_size) ** 2

        self.patch = PatchEmbed(in_chans=in_chans, d_model=d_model, patch_size=patch_size)
        self.cls_token = nn.Parameter(torch.zeros(1, 1, d_model))
        self.pos_embed = nn.Parameter(torch.zeros(1, 1 + n_patches, d_model))
        self.drop = nn.Dropout(dropout)

        self.blocks = nn.ModuleList(
            [ViTEncoderBlock(d_model, n_heads, d_ff, dropout=dropout) for _ in range(depth)]
        )
        self.norm = nn.LayerNorm(d_model)
        self.head = nn.Linear(d_model, n_classes)

    def forward(self, x):
        # x: (B, C, H, W)
        tok = make_vit_tokens(x, self.patch, self.cls_token, self.pos_embed)
        tok = self.drop(tok)
        for blk in self.blocks:
            tok = blk(tok)
        tok = self.norm(tok)
        cls = tok[:, 0]  # [CLS]
        return self.head(cls)


# Example:
# model = ViT(image_size=224, patch_size=16, d_model=192, depth=6, n_heads=3, d_ff=768)
# x = torch.randn(2, 3, 224, 224)
# logits = model(x)

References (paper + code)

  • Paper: https://arxiv.org/abs/2010.11929 (Dosovitskiy et al., 2020, An Image is Worth 16x16 Words)
  • Code (Google research): https://github.com/google-research/vision_transformer
  • Code (PyTorch models / baseline impls): https://github.com/rwightman/pytorch-image-models (timm)

DiT (Diffusion Transformer)

Core idea

Diffusion models learn to denoise a noisy sample \(x_t\) at time \(t\). A DiT replaces the usual U-Net backbone with a Transformer that operates on tokenized image/latent representations.

Typical training target choices include (depending on formulation):

  • Predict noise \(\epsilon\)
  • Predict velocity \(v\)
  • Predict the clean sample \(x_0\)

The Transformer is conditioned on the diffusion timestep \(t\) (and sometimes on class labels / text embeddings). Conditioning is often injected via adaptive normalization (FiLM/AdaLN-style) rather than plain concatenation.

DiT Fig 3

What to pay attention to (implementation details)

  • Tokenization: operate in pixel space or (more common) in VAE latent space (Stable Diffusion-style).
  • Timestep conditioning: how \(t\) embeds and modulates blocks (often via AdaLN).
  • Attention pattern: full attention vs windowed/local attention for high-resolution.
  • Objective choice: \(\epsilon\) vs \(v\) parameterization and how it affects stability/sampling.

Minimal code sketch (DDPM-specific parts: timestep + conditioning injection):

import math
import torch
import torch.nn as nn


def timestep_embedding(t, dim, max_period=10000):
    """Sinusoidal embedding for diffusion timesteps.

    t: (B,) int/float tensor
    Returns: (B, dim)
    """
    half = dim // 2
    freqs = torch.exp(
        -math.log(max_period) * torch.arange(0, half, device=t.device, dtype=torch.float32) / half
    )
    args = t.float()[:, None] * freqs[None]
    emb = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
    if dim % 2 == 1:
        emb = torch.cat([emb, torch.zeros_like(emb[:, :1])], dim=-1)
    return emb


class AdaLN(nn.Module):
    """Adaptive LayerNorm: produces per-channel scale/shift from a condition vector."""

    def __init__(self, d_model, cond_dim):
        super().__init__()
        self.ln = nn.LayerNorm(d_model, elementwise_affine=False)
        self.to_scale_shift = nn.Linear(cond_dim, 2 * d_model)

    def forward(self, x, cond):
        # x: (B, N, C), cond: (B, cond_dim)
        x = self.ln(x)
        s, b = self.to_scale_shift(cond).chunk(2, dim=-1)  # (B, C), (B, C)
        return x * (1 + s[:, None, :]) + b[:, None, :]


class DiTBlock(nn.Module):
    """Encoder-style Transformer block with timestep/class conditioning via AdaLN.

    This is the key structural difference vs a plain ViT block: the diffusion timestep (and optional semantic condition)
    modulates normalization inside each block.
    """

    def __init__(self, d_model, n_heads, d_ff, cond_dim, dropout=0.0):
        super().__init__()
        self.ada1 = AdaLN(d_model, cond_dim)
        self.attn = nn.MultiheadAttention(d_model, n_heads, dropout=dropout, batch_first=True)
        self.ada2 = AdaLN(d_model, cond_dim)
        self.mlp = nn.Sequential(
            nn.Linear(d_model, d_ff),
            nn.GELU(),
            nn.Dropout(dropout),
            nn.Linear(d_ff, d_model),
            nn.Dropout(dropout),
        )

    def forward(self, x, cond):
        # x: (B, N, C)
        a, _ = self.attn(self.ada1(x, cond), self.ada1(x, cond), self.ada1(x, cond), need_weights=False)
        x = x + a
        x = x + self.mlp(self.ada2(x, cond))
        return x


class DiT(nn.Module):
    """Minimal DiT-like backbone (latent tokens -> denoising prediction)."""

    def __init__(self, d_model=768, depth=12, n_heads=12, d_ff=3072, n_classes=None, time_dim=256, dropout=0.0):
        super().__init__()
        cond_dim = time_dim
        self.time_mlp = nn.Sequential(
            nn.Linear(time_dim, cond_dim),
            nn.SiLU(),
            nn.Linear(cond_dim, cond_dim),
        )
        self.class_emb = nn.Embedding(n_classes, cond_dim) if n_classes is not None else None
        self.blocks = nn.ModuleList(
            [DiTBlock(d_model, n_heads, d_ff, cond_dim=cond_dim, dropout=dropout) for _ in range(depth)]
        )
        self.norm = nn.LayerNorm(d_model)
        self.out = nn.Linear(d_model, d_model)  # placeholder: map tokens back to token-space prediction

    def forward(self, x_tokens, t, y=None):
        # x_tokens: (B, N, C) tokens of x_t (often VAE latents patchified)
        cond = self.time_mlp(timestep_embedding(t, self.time_mlp[0].in_features))
        if self.class_emb is not None and y is not None:
            cond = cond + self.class_emb(y)
        for blk in self.blocks:
            x_tokens = blk(x_tokens, cond)
        x_tokens = self.norm(x_tokens)
        return self.out(x_tokens)


# Notes:
# - In a real diffusion model, x_tokens represent a noised sample x_t, and the output is interpreted as epsilon/v/x0 in the same space.
# - The *DDPM-specific* part is the conditioning on t (and optional y/text) + the training/sampling process around this network.

Quick contrast vs VGG/CNNs (why this is a “structure” story):

  • VGG/CNNs bake in strong inductive biases (locality + translation equivariance via convolutions). ViT/DiT rely more on data/scale and attention’s global mixing.
  • ViT is usually a single-pass supervised encoder (classification/representation). DiT is used inside an iterative denoising procedure (DDPM/DDIM sampling), so the same network is called many times.
  • The key DiT-specific knobs are timestep embedding \(t\), how conditioning is injected (AdaLN/FiLM vs cross-attention vs extra tokens), and which target you predict (\(\epsilon\)/\(v\)/\(x_0\)).

References (paper + code)

  • Paper: https://arxiv.org/abs/2212.09748 (Peebles & Xie, 2022, Scalable Diffusion Models with Transformers)
  • Code (official-ish / common reference): https://github.com/facebookresearch/DiT
  • Video: https://www.youtube.com/watch?v=vXtapCFctTI

File references (inside this repo)

Code references (external)

  • ViT implementations: timm ViT variants and Google’s JAX reference (links above)
  • DiT implementation: Meta’s DiT repo (link above)

Next topics to expand (pick one per installment)

  • ViT patch embedding math + positional embedding resizing
  • ViT pretraining recipes (augmentation, regularization, distillation)
  • Diffusion basics: forward/noise schedule and what the model predicts (\(\epsilon\), \(v\), \(x_0\))
  • DiT conditioning (AdaLN/FiLM) and why U-Nets were dominant historically
  • Sampling: DDPM/DDIM and classifier-free guidance (CFG)