Steve's Blog

MiniMax H3 Explained

August 7, 2026

MiniMax H3 in ComfyUI — How It Actually Works

Written for a software engineer who knows tensors and basic linear algebra, but not model architecture. Every concept is built up before it is used.


Contents

Foundations — read these first if the terms are new


FOUNDATIONS

1. What a neural network actually is

A layer is a matrix multiply

Strip away the mystique and a neural network layer is this:

y = x @ W.T + b

x is your input vector, W is a matrix of learned numbers, b is a bias vector. That's it. That's a "linear layer" (also called "dense" or "fully connected", hence names like fc1fully connected 1).

Weights / parameters are the numbers in W and b. When people say "a 20 billion parameter model," they mean the total count of numbers across all its matrices. Training is the process of finding good values for them; inference (what you're doing) just uses them.

Reading matrix shapes

This is where your linear algebra pays off directly. In PyTorch a linear layer's weight has shape [out_features, in_features]. So:

fc1.weight = [28672, 5376]

means: takes a 5376-element vector, produces a 28672-element vector. Every shape in this document can be read that way. When you see a chain of layers, you're watching a vector get reshaped step by step:

5376 ──fc1──> 28672 ──(gating)──> 14336 ──fc2──> 5376

Why nonlinearity exists

If you stack two matrix multiplies you get... another matrix multiply. (xA)B = x(AB). Stacking would be pointless. So between layers you apply a nonlinear function elementwise — something bent, like max(0,x) or a smooth S-curve. That bend is what lets stacked layers express things a single matrix can't.

Names you'll see: ReLU, GELU, SiLU/swish. They're all "a bend."

Hidden size

Every neural net has an input, an output, and intermediate values in between. The intermediates are called hidden states — "hidden" only in the sense of not being the input or output. Nothing mysterious.

hidden_size is the width of that intermediate vector. In your model it's 5376, meaning: at every point between blocks, each token is represented by 5376 floating-point numbers.

If it helps, think of it as a record size. The model's working data is:

[sequence_length, 5376]      # one 5376-float row per token

and every layer is a transformation on that array. That single shape is the spine of the whole architecture.

Weights vs. activations

An important distinction for the memory discussion later:

What it is

Scales with

Weights

The learned matrices. Fixed after training.

Model size (constant at run time)

Activations

The intermediate tensors flowing through during a forward pass.

How much data you're processing

Your 20 GB of weights is constant no matter what you generate. Activations grow with the size of your video. That's why you OOM by asking for longer videos, not by loading a bigger model.

Forward pass

Running data through the network once, input → output. No learning, no gradients. All you ever do at inference time.


2. Transformers: tokens, and two alternating moves

Tokens

A token is one item in a sequence, represented as a vector.

  • In an LLM: roughly one word-piece.
  • In your video model: one small patch of compressed video (more on this later).

Your data is a sequence of tokens, i.e. that [sequence_length, 5376] array. sequence_length (often written S) is how many tokens — and it is the single number that determines your compute cost.

The two moves

A transformer repeats exactly two operations, over and over:

  1. Attention — tokens look at each other and exchange information. This is the only place tokens interact.
  2. MLP — each token is processed independently, in isolation. This is where most of the parameters live.

That's genuinely it. Attention mixes across the sequence; the MLP does per-token computation. A block (or layer) is one attention + one MLP. Your model stacks 50 of them.

tokens ─► [attn ─► MLP] ─► [attn ─► MLP] ─► ... ×50 ─► output
             block 1          block 2

The MLP

The per-token part. Two matrices: widen, bend, narrow.

5376 ──fc1──> 14336 ──nonlinearity──> ──fc2──> 5376

Why widen then narrow? The wide middle gives the nonlinearity more room to work — more dimensions in which to carve out useful distinctions before compressing back down. The widening factor is usually 3–4×.

SwiGLU — the variant your model uses — is a refinement. Instead of one wide output, fc1 produces two halves. One is passed through the bend and used as a gate, multiplied elementwise into the other:

gate, value = fc1(x).chunk(2)
hidden = swish(gate) * value      # elementwise multiply
out = fc2(hidden)

The gate lets the network dynamically suppress or amplify each dimension per token. This is why fc1 outputs 28672 = 2 × 14336 — double width, because it's producing two things. Costs 1.5× the parameters of a plain MLP and reliably works better.

Residual connections and normalization

Two supporting pieces you'll see everywhere:

  • Residual (skip) connection: x = x + block(x). Each block writes a correction onto a running total rather than replacing it. Without this, deep networks don't train. Think of it as the sequence accumulating edits.
  • Normalization: rescale a vector to a consistent magnitude before feeding it into a layer, so values don't explode or vanish across 50 blocks. RMSNorm (which your model uses) divides by the root-mean-square of the vector. eps (epsilon) is a tiny constant in the denominator preventing division by zero.

3. Attention, from scratch

This is the one genuinely non-obvious idea, so here it is slowly.

The problem

Each token needs information from other tokens. A token representing a patch of frame 40 needs to know what was in frame 39, and what's happening elsewhere in frame 40. But which other tokens matter depends entirely on content — you can't hard-code it.

The idea: a soft dictionary lookup

You know a hash lookup: exact key match, return one value.

value = table[key]        # exact match, one result

Attention is that, made soft and differentiable. Instead of matching one key exactly, you compare your query against every key, get a similarity score for each, and return a weighted blend of all the values:

scores  = query @ all_keys.T        # how well do I match each token?
weights = softmax(scores)           # normalize to sum to 1
result  = weights @ all_values      # weighted average of everything

Softmax turns arbitrary scores into positive numbers summing to 1 — a weighting. High-scoring tokens dominate; low-scoring ones contribute nearly nothing.

Q, K, V

Each token produces three vectors, each by its own learned matrix:

Name

Intuition

Q

Query

"What am I looking for?"

K

Key

"What do I have to offer?"

V

Value

"If you attend to me, here's what you get."

Q and K are compared to compute relevance; V is what actually gets passed along. Keeping them separate means "how do I decide relevance" and "what do I contribute" are learned independently.

In your model these are computed by one packed matrix — qkv_proj of shape [21504, 5376], where 21504 = 3 × 7168. Q, K and V are produced in a single matrix multiply and then split. One large GEMM is faster than three small ones.

Multi-head attention

Doing this once forces the model to blend all kinds of relationships into one score. Instead it does it many times in parallel, each with different learned projections. Each parallel copy is a head.

Your model: 56 heads, each 128 wide. 56 × 128 = 7168 — that's where the 7168 comes from. Each head learns its own notion of relevance (one might track motion continuity, another spatial neighbours). Their outputs are concatenated and passed through out_proj [5376, 7168] to get back to the model width.

  • heads = how many parallel attentions.
  • head_dim = how wide each one is.

Why attention is expensive

Every token compares against every other token. That's S × S comparisons — the cost grows with the square of sequence length. The MLP, by contrast, processes each token independently: cost grows linearly.

MLP:        cost ∝ S
Attention:  cost ∝ S²

This one fact drives nearly every performance decision in this document.

Causal vs. bidirectional

  • Causal: a token may only see earlier tokens. What LLMs use, because they generate left to right.
  • Bidirectional: every token sees every other token. What your video model uses, because the whole video is refined at once — there's no "later," everything already exists in draft form.

Position information

Attention as described has no notion of order — it's a set operation. Position must be injected explicitly.

RoPE (Rotary Position Embedding) does this by rotating the Q and K vectors by an angle determined by position. Two tokens close together get similar rotations, so their dot product stays high; distant ones get rotated apart. Position becomes a geometric property of the vectors.

Your model uses 3D RoPE: each token has a position (t, h, w) — which frame, and where in that frame. It rotates by all three axes.


4. How images and video get generated at all

Why not just output the video?

You could imagine a network that takes a prompt and emits all the pixels in one shot. In practice this produces mush. Generating a coherent video is too big a leap to make in one step.

Iterative refinement

Instead: start from pure random noise and refine it repeatedly, each pass making it slightly more like a real video. After 20–50 passes you have output. Each pass is a small, learnable step rather than one impossible jump.

That's diffusion. The network's job at each pass is: "given this partially-noisy thing, what should change?"

The denoiser is a transformer — hence "DiT"

Something has to be the network doing the refining. Historically this was a U-Net (a convolutional architecture). Modern models use a transformer instead.

DiT = Diffusion Transformer. A transformer used as the denoiser in a diffusion model. That's the entire meaning of the acronym. Your model is a DiT.

ODEs, in plain terms

Here's the framing that makes modern diffusion click.

Imagine every possible video is a point in a huge space. Pure noise is one region; real videos are another. Now imagine a field of arrows filling that space — at every point, an arrow saying "to become more like a real video, move this way." That is a vector field.

The model IS that field. You hand it a point (your current noisy latent) and a timestep, and it returns an arrow (a direction, called the velocity).

To generate, you follow the arrows in small steps:

x = random_noise
for t in schedule:          # your "steps" setting
    v = model(x, t, prompt) # which way should I move?
    x = x + v * dt          # take a small step

If that looks like a game physics loop — pos += velocity * dt — that's exactly right. It's the same operation.

ODE stands for Ordinary Differential Equation: a rule that gives you a rate of change at each point, which you integrate to get a path. That's precisely what we have. Numerical integration just means "take lots of small steps instead of solving it exactly," and the simplest method — x += v * dt — is Euler's method.

So:

A sampler step is one step of numerical integration. More steps = smaller dt = less error, with diminishing returns.

This formulation is called flow matching (your log prints model_type FLOW). Older models used a noisier, probabilistic formulation (DDPM); flow matching is the cleaner modern version and needs far fewer steps.

Timesteps and shift

t runs from 1 (pure noise) to 0 (finished). The schedule decides which values of t you actually visit.

Shift warps that schedule. Your model uses sigma_shift_video = 12.0, which spends disproportionately more steps near t = 1 (high noise). Why: early steps decide global structure — layout, motion, composition. Late steps polish texture. A structural mistake is unrecoverable; slightly soft texture isn't. So you buy resolution where it matters.

Latent space

Refining raw pixels 20–50 times would be brutally expensive. Instead everything happens in a compressed representation called a latent, produced by a separate model (a VAE — covered in §8). Roughly 128× fewer numbers than pixels.

The diffusion model never sees a pixel. It refines latents; the VAE decodes at the end.

Classifier-free guidance (CFG)

A trick to make output follow your prompt more closely. Run the model twice — once with your prompt, once with nothing — then extrapolate away from the promptless answer:

result = uncond + scale * (cond - uncond)

This amplifies whatever your prompt actually changed. It costs 2× per step, because you're doing two forward passes instead of one.


YOUR MODEL

5. What MiniMax H3 is

From comfy/ldm/minimax/model.py:413:

hidden_size=5376, num_layers=50, num_attention_heads=56,
attention_head_dim=128, ffn_hidden_size=14336, ...

You can now read every one of those. Working out one block's parameter count:

Matrix

Shape [out, in]

What it does

Params

qkv_proj

21504 × 5376

Makes Q, K, V (3 × 7168)

115.6 M

out_proj

5376 × 7168

Attention result → model width

38.5 M

fc1

28672 × 5376

MLP widen (2× for SwiGLU gating)

154.1 M

fc2

5376 × 14336

MLP narrow

77.1 M

per block

385.4 M

× 50 blocks ≈ 19.3 billion parameters, plus embeddings ≈ 20 B.

At int8 (one byte per parameter) that's ~19.3 GB — and your file on disk is 19.53 GB. The arithmetic closes, which is a good check that you've understood the structure.

For scale: this is comparable to a mid-large LLM. It barely fits on a 24 GB card, which is why all the memory machinery in §8 exists.

6. Inside one block

Three things happen, in order.

adaLN — how conditioning gets in

The model needs to know two things beyond the tokens themselves: what timestep we're at, and what you asked for.

The timestep enters through adaLN (adaptive Layer Normalization). Remember normalization rescales a vector. adaLN makes that rescaling conditional: a small network turns the timestep into scale, shift, and gate values applied around each norm.

So conditioning steers the model by modulating normalization — cheaper than adding extra attention layers, and empirically better for this job.

The mod_segments argument means this is applied per segment: video tokens and audio tokens get different modulation in the same pass.

Attention

56 heads × 128 dim, bidirectional, with 3D RoPE. As described in §3.

SwiGLU MLP

return comfy.ops.linear_input_act(self.fc2, self.fc1(x), "swiglu")

fc1 widens 5376 → 28672, splits into gate and value, multiplies, fc2 brings 14336 → 5376.

Why this differs from an LLM

LLM (e.g. LM Studio)

Video DiT

Attention

Causal — token n sees only < n

Bidirectional — all see all

KV cache

Yes; each new token is cheap

None

Per forward pass

1 new token

The entire sequence

Passes per output

1 per token

steps × (2 if CFG) over everything

KV cache is the LLM optimization where past K and V vectors are stored and reused, so generating token 500 doesn't redo tokens 1–499. It cannot apply here, because every token's value changes at every step. You recompute all 50 blocks over all tokens, 20–50 times over. This is why video generation is so much slower than chatting with a similarly sized LLM.

7. The pipeline end to end

prompt + first/last reference frames
   │
   ├─ tokenizer:  "<Picture 1>: " <vision> "<Picture 2>: " <vision> <prompt>
   │
   ▼
Qwen3-VL-32B  (a 32B LLM used as a feature extractor)  ──►  [L, 5120]
   │                                                       (then EVICTED)
   ▼
condition_proj 5120→5376  +  2 refiner blocks
   │
   ▼
┌──────────────────────────────────────────────────────────┐
│  DENOISING LOOP          × steps × (2 if CFG)            │
│                                                           │
│   ONE sequence, tagged by segment:                        │
│   [ text | cond | ref_img | video | ref_audio | audio ]   │
│                                                           │
│   50 × block:  adaLN → attention → SwiGLU MLP            │
│   → velocity → x += v·dt                                  │
└──────────────────────────────────────────────────────────┘
   │
   ├─►  Video VAE decode  → frames
   └─►  Audio VAE decode  → waveform

The structurally important part — model.py:588-599:

h = torch.empty(layout.seq_len, self.hidden_size, ...)
for a, b, kind in layout.segments:
    if kind == "text":                            h[a:b] = text_states
    elif kind in ("cond", "ref_img", "video"):    h[a:b] = video_embed[...]
    else:  # ref_audio / audio                    h[a:b] = audio_embed[...]

Text, reference images, video latents and audio latents all live in one tensor. Attention runs across the whole thing — which is how the model syncs a mouth movement to a phoneme: those tokens literally attend to each other.

It also means anything you add to the conditioning makes every step slower, because it permanently increases S.

8. The scaffolding

Model files

.safetensors is deliberately boring: an 8-byte header length, a JSON header, then raw tensor bytes. No pickle, so no code execution — which is why the ecosystem abandoned .ckpt. Metadata is free to read:

n   = struct.unpack("<Q", f.read(8))[0]
hdr = json.loads(f.read(n))          # shapes, dtypes, byte offsets

Loading is memory-mapped (mmap): the OS maps the file into the address space and pages in bytes on demand, zero-copy.

How ComfyUI identifies the model

There's no manifest. model_detection.py infers the architecture from tensor names and shapes, matching against candidates in supported_models.py. Yours matches {"image_model": "minimax_h3"}, which selects shift: 12.0 and memory_usage_factor: 0.114 (a heuristic for predicting activation memory — which is exactly why an OOM can surprise it).

The text encoder

Your prompt is turned into vectors by Qwen3-VL-32B — a 32-billion-parameter vision-language model. Three things about how it's used are unusual:

It's truncated and headless. The checkpoint stops at layer 50, and the conditioning is the raw hidden state there with no final normalization. The part of an LLM that predicts words is discarded entirely. You're using it purely as a semantic feature extractor — its internal representation of meaning, not its output.

No chat template. From the module docstring: The H3 presentation is NOT chat-templated. Raw token ids, no system prompt, none of the <|im_start|> scaffolding.

It handles your reference images. fl2va = first/last frame → video + audio. The tokenizer builds one interleaved sequence:

"<Picture 1>: " <vision block>   "<Picture 2>: " <vision block>   <your prompt>

Your frames go through Qwen's vision tower (the image half of the VLM) into the same token stream as your words, so images and text are cross-referenced before the diffusion model sees anything.

Why 14.6 GB is affordable: it runs once, then is evicted. Compare with the DiT running 50 blocks over ~37,000 tokens, 40+ times.

VAE

A Variational Autoencoder is two networks trained together:

  • Encoder: pixels → compact latent
  • Decoder: latent → pixels

The variational part matters. A plain autoencoder learns an arbitrary, pothole-ridden latent space — interpolate between two points and you get garbage. The VAE adds a KL penalty (KL divergence measures distance between probability distributions) pulling the latent distribution toward a standard Gaussian. The result is smooth and continuous: nearby points decode to similar images.

That smoothness is the precondition for diffusion. The sampler traces a continuous path through latent space, so the space has to be well-behaved everywhere along it.

The VAE is trained separately and frozen.

Your compression (latent_formats.py:570):

spacial_downscale_ratio  = 16     # a 16×16 pixel block → 1 latent position
temporal_downscale_ratio = 4      # nominal (real value is 3.4 — see §9)
latent_channels          = 24

For one second of 1280×704 @ 24 fps:

positions

values

Pixels

24 × 1280 × 704

64.9 M (×3 channels)

Latent

6 × 44 × 80

507 K (×24 channels)

~128× fewer numbers.

Two VAEs, because the modalities are unrelated: video uses 3D convolutions over space and time; audio uses a 1D waveform VAE. Your audio VAE loads in float32 while video is fp16 — phase errors in audio are far more audible than a slightly-off pixel.

Patchify

The latent is a 3D grid. To feed a transformer it must become a sequence. Patchify groups neighbouring latent cells into one token: patch_size = (1,2,2) means no temporal grouping, 2×2 spatial. So four latent positions × 24 channels = 96 numbers become one token, projected up to 5376.

latent_rgb_factors

A linear approximation of the VAE decoder — 24 latent channels → 3 RGB, one matrix multiply. Gives a rough live preview without running the real decoder. Enable with --preview-method latent2rgb; it's nearly free.

Memory choreography

1. Stage text encoder (15 GB)  → encode → EVICT
2. Stage DiT (20 GB)           → denoising loop
3. Stage VAEs (5.5 GB)         → decode

15 + 20 + 5.5 = 40 GB against a 24 GB card, so these must be sequential. Underneath, DynamicVRAM (comfy-aimdo) streams weights within each stage from pinned system RAM, prefetching block n+1 while block n computes. Your 128 GB of RAM is what makes this viable at all.


PRACTICAL

9. User-facing parameters → cost

The master formula

Spatially it's a clean divide-by-32 (16 for the VAE, 2 for the patch). Temporally it is not — the node snaps frames onto a 17-frames-to-5-latents grid (nodes_minimax_h3.py:33-40):

def align_frame_count(n):
    while n % 17 != 5:
        n += 1

def video_latent_t(frame_count):
    return 2 if frame_count <= 5 else ((frame_count - 5) // 17) * 5 + 2

Real temporal compression is 17:5 = 3.4×, not the nominal 4 in latent_formats.py.

╔═══════════════════════════════════════════════════════╗
║  latent_t = ((frames - 5) // 17) × 5 + 2              ║
║  tokens   = latent_t × (height / 32) × (width / 32)   ║
╚═══════════════════════════════════════════════════════╝

Resolution

Frames

latent_t

Tokens

832 × 480

90

27

~10,500

1280 × 704

124

37

~32,600

1344 × 768 (node default)

124

37

~37,300

1280 × 704

243

72

~63,400

1920 × 1088

124

37

~75,500

The two scaling laws

Component

Scales as

Why

MLP + projections

O(S)

Fixed work per token

Attention

O(S²)

Every token attends to every token

Activation memory

O(S)

Flash/Sage keep attention memory linear

Weight memory

O(1)

Always 20 GB

Measured on your 4090, after our optimizations (per forward pass, all 50 blocks):

Tokens

MLP + proj

Attention

Total

16,384

~1.9 s

~1.5 s

~3.4 s

32,768

~3.7 s

~5.8 s

~9.5 s

Doubling tokens doubled the linear cost and quadrupled attention. The crossover where attention overtakes everything else sits around 22–25k tokens.

Consequence: doubling both resolution dimensions = 4× tokens = 4× linear but 16× attention. Doubling length = 2× tokens = 2× and 4×. Length is much cheaper than resolution for the same added value.

Parameter by parameter

Resolution — your most expensive knob. Consider generating smaller and upscaling afterward (you have SeedVR2 installed for exactly this).

Length / frames — linear in tokens, so still quadratic in attention, but a gentler climb than resolution.

Steps — a pure linear multiplier. ODE integration steps; flow-matching models hold up well at lower counts. The cheapest dial to lower.

CFG — a flat on the entire loop. If your workflow tolerates CFG 1.0, that's an instant halving.

Shift — free. Reshapes where steps are spent, not how many. Tune this before spending more steps.

Reference images — cost you twice: as Qwen vision tokens (W/32) × (H/32), and as VAE-encoded condition rows. Both ride every step — the node docstring says condition latents are "re-injected every step (never denoised)".

You do not need a resize node upstream. MiniMaxH3ImageToVideo (nodes_minimax_h3.py:130-139) already resizes keyframes to the node's own width/height before Qwen or the VAE sees them. A 4K reference is downsampled on arrival.
>
But watch aspect ratio. first_frame uses crop="disabled" — a plain stretch that distorts if your reference's aspect doesn't match width/height. last_frame uses crop="center" and is safe. Put an aspect-preserving crop before first_frame if needed — for geometry, not speed.

At the default 1344 × 768, each keyframe adds ~1,008 Qwen tokens + ~1,008 latent rows; first + last ≈ 4,032 tokens on a ~37,300 base.

For ref2va, MiniMaxH3ReferenceToVideo exposes ref_image_size: "match" (default) or "max" (2048 px short edge). Its tooltip warns "max" "can be several times slower." That's your cost/fidelity dial there.

A worked estimate

Node defaults — 1344 × 768, 124 frames, 20 steps, CFG on:

tokens    ≈ 37,300 video + ~400 audio + ~4,000 keyframes  ≈ 42,000
attention ≈ 189 ms/block × 50  ≈  9.4 s     (S² scaling from the 32,768 measurement)
linears   ≈  4.8 s
per pass  ≈ 14 s
× 2 CFG   ≈ 28 s / step
× 20      ≈ 560 s (~9 min)   plus text encode + VAE decode

Drop to 832 × 480 and tokens fall to ~10,500 — attention drops roughly 16×.

10. What we changed on your machine

Change

Effect

Measured

torch cu126 → cu130

Re-enabled fused int8 GPU kernels

4.1–6.0× on MLP matrices

Same

Rescaling moved inside the matmul

fc1 peak VRAM 7.98 → 2.40 GB

SageAttention + Triton

int8-quantized attention

2.6× on attention

Why the fallback was so slow

comfy/quant_ops.py:40 disables the compiled GPU kernels when torch.version.cuda < 13. The Python fallback materialized a full int32 result, looped over it converting chunks to float, then concatenated a second full copy — roughly three extra full-tensor round-trips to VRAM per matrix, across ~200 matrices per step. The fused kernel does the rescaling inside the matmul; the intermediate never touches memory.

What convrot in the filename means

Quantization stores weights at lower precision (int8 = one byte instead of four) to save memory and time. It fails naively on transformers because activations have outlier channels — a few dimensions 100× larger than the rest. Since the scale must cover the maximum, outliers consume the whole numeric range and everything else collapses toward zero.

The fix: multiply activations by a Hadamard matrix (an orthogonal matrix of ±1) before quantizing. It mixes every channel into every other, smearing outlier energy across the group so the distribution becomes roughly Gaussian — which int8's uniform grid handles well. Being orthogonal it's exactly invertible, and the inverse folds into the weights for free. The linear algebra is unchanged; only the numerical conditioning improves.


11. Glossary

Core structure

Term

Meaning

Parameter / weight

A learned number. "20 B parameters" = 20 billion of them.

Linear / dense / fully-connected layer

A matrix multiply plus bias: y = xW.T + b.

fc1 / fc2

"Fully connected" — the MLP's two matrices.

Shape [out, in]

PyTorch weight convention. [28672, 5376] maps a 5376-vector to a 28672-vector.

Activation function

The elementwise "bend" between layers (ReLU, GELU, SiLU/swish). Without it, stacked layers collapse into one.

Hidden state

An intermediate vector — not input, not output.

hidden_size

Width of that vector. 5376 here.

Activation (tensor)

Intermediate data flowing through a forward pass. Scales with input size.

Forward pass

Running data through once, input → output.

Residual / skip connection

x = x + block(x). Blocks write corrections onto a running total.

Normalization / RMSNorm

Rescaling a vector to consistent magnitude. RMSNorm divides by root-mean-square.

eps

Tiny constant preventing division by zero inside a norm.

MLP / FFN

The per-token widen-bend-narrow stage.

GLU / SwiGLU

Gated variant: split the output, use one half to gate the other. Why fc1 is 2× wide.

Transformers & attention

Term

Meaning

Token

One item in the sequence, as a vector. Here: a patch of compressed video.

Sequence length (S)

How many tokens. Governs your cost.

Block / layer

One attention + one MLP. Your model has 50.

Attention

Tokens exchanging information — a soft, weighted dictionary lookup.

Q / K / V

Query ("what I want"), Key ("what I offer"), Value ("what you get").

qkv_proj

One matrix producing all three at once.

out_proj

Projects the attention result back to model width.

Softmax

Turns scores into positive weights summing to 1.

Head

One parallel attention computation. Yours: 56.

head_dim

Width of each head. 128. 56 × 128 = 7168.

Self-attention

Tokens attend within their own sequence.

Causal

A token sees only earlier ones (LLM generation).

Bidirectional

Every token sees every other. What your model uses.

KV cache

LLM speedup storing past K/V. Not applicable here.

RoPE

Rotary Position Embedding — encodes position by rotating Q and K. Yours is 3D over (t, h, w).

Patchify

Grouping neighbouring latent cells into one token.

Transformer

Architecture of repeated attention + MLP blocks.

DiT

Diffusion Transformer — a transformer used as a diffusion model's denoiser.

Diffusion & sampling

Term

Meaning

Diffusion

Generating by starting from noise and refining repeatedly.

Denoiser

The network doing the refining.

Latent / latent space

Compressed representation the model works in instead of pixels.

VAE

Variational Autoencoder — encoder/decoder between pixels and latents.

KL divergence

A distance between probability distributions. The VAE's KL penalty keeps latent space smooth.

Vector field

An arrow at every point in space. The model is one.

Velocity

The arrow the model returns — which way to move.

ODE

Ordinary Differential Equation — a rule giving rate of change; integrate it to get a path.

Numerical integration

Approximating that path with small steps.

Euler's method

The simplest one: x += v * dt.

Flow matching

Modern formulation: learn the velocity field, integrate the ODE. Your log: model_type FLOW.

DDPM

The older, probabilistic formulation. Needed more steps.

Timestep / sigma

Position on the noise→data path. 1 = noise, 0 = done.

Steps

Integration steps. More = finer, diminishing returns.

Scheduler

Chooses which timesteps to visit.

Sampler

The integration algorithm (Euler, DPM++, …).

Shift

Warps the schedule toward high noise. 12.0 for video here.

CFG

Classifier-Free Guidance — run with and without the prompt, extrapolate. Costs 2×.

Conditioning

Everything steering generation: prompt, reference frames, audio.

adaLN

Adaptive LayerNorm — inject conditioning by modulating normalization.

Text encoder

Turns your prompt into vectors. Here: Qwen3-VL-32B.

LLM / VLM

Large Language Model / Vision-Language Model.

Vision tower

The image-processing half of a VLM.

Tokenizer

Splits text into integer ids.

CLIP

An older text/image encoder. ComfyUI still labels the text-encoder slot "CLIP" for historical reasons.

Numerics & quantization

Term

Meaning

Quantization

Storing/computing at lower precision to save memory and time.

fp32 / fp16 / bf16

32- and 16-bit floats. bf16 trades precision for fp32's range.

fp8, int8, nvfp4

8-bit float, 8-bit integer, 4-bit float formats. Need scale factors.

AWQ

Activation-aware Weight Quantization — protects the weights that matter most.

Outlier channels

The few huge-valued dimensions that wreck naive quantization.

Hadamard matrix / convrot

Orthogonal ±1 matrix used to spread outliers so int8 works. Invertible, folds into weights.

Dequantize

Converting back to float using the scale.

Systems & performance

Term

Meaning

VRAM

GPU memory. Yours: 24 GB.

Kernel

A function that runs on the GPU.

GEMM

GEneral Matrix Multiply — the core operation.

Epilogue

Work fused onto the end of a GEMM (scaling, bias) so intermediates never hit memory. Where our 4–6× came from.

Fused kernel

One kernel doing several steps, avoiding memory round-trips.

Memory-bandwidth-bound

Limited by moving data, not by arithmetic. Most inference is.

FLOPs / TFLOPS

Floating-point operations; trillions per second.

CUDA

NVIDIA's GPU programming platform.

cuDNN / cuBLAS / CUTLASS

NVIDIA libraries for neural-net ops, linear algebra, templated GEMMs.

Triton

Language for writing GPU kernels in Python, compiled on first use.

JIT

Just-In-Time compilation. Cached in ~/.triton/cache.

SDPA

Scaled Dot-Product Attention — PyTorch's built-in attention.

Flash Attention

Attention that never materializes the S×S matrix, making memory O(S) not O(S²).

SageAttention

Quantized attention (int8 Q/K). 2.6× here, slightly lossy.

safetensors

Weight file format: JSON header + raw bytes, no code execution.

State dict

The {name: tensor} mapping that is the weights.

mmap

Memory-mapped file — OS pages it in on demand, zero-copy.

Pinned memory

Page-locked RAM that can't be swapped, enabling fast DMA to GPU. Yours: 52 GB.

PCIe

The bus between RAM and GPU. The bottleneck when streaming weights.

Offloading

Keeping weights in system RAM, moving to VRAM only when needed.

DynamicVRAM / comfy-aimdo

ComfyUI 0.30's automatic block-streaming system.

Prefetch

Fetching block n+1's weights while block n computes.

OOM

Out Of Memory.

Tiled decode

Decoding the VAE in overlapping chunks to cap peak VRAM.

Variable names in the code

Name

Meaning

x, h

The running hidden state — the [S, 5376] array.

t_emb

Timestep embedding — current noise level as a vector.

mod_segments

Which span is video / audio / text, so adaLN can treat them differently.

rope_freqs

Precomputed rotation table for positions.

layout.segments

(start, end, kind) triples describing sequence structure.

layout.seq_len

Total token count (S).

latents_dim

Latent channels — 24 video, 32 audio.

patch_size

(1,2,2) — temporal, height, width grouping.

sigma_shift_video/audio

Per-modality schedule warp (12.0 / 3.0).

spacial_downscale_ratio

VAE spatial compression (16). (sic — misspelled in ComfyUI.)

memory_usage_factor

Heuristic for predicting activation memory.

latent_rgb_factors

Linear latent→RGB approximation for previews.

img_update

Boolean mask: which rows are generated vs. held fixed as conditioning.

fl2va / ref2va / t2va

first-last / reference / text → video+audio. Yours is fl2va.

pruned

Layers removed from the original release to shrink it.

_C.abi3.pyd

Compiled C++/CUDA extension. abi3 = stable ABI across Python versions.


Written during a debugging session, 2026-08-06. Measurements from your RTX 4090; architecture read from your installed ComfyUI 0.30.0 source and model files.