Architecture teardown · read from the released source

MiniMax H3 — one transformer that denoises video and audio together

H3 (Hailuo 3.0, open-weighted 31 July 2026) generates up to 15 s of 2K video with its own synchronised soundtrack from text, images, video and audio references. This page explains how, starting from zero — no prior knowledge of latents, patchify, or diffusion assumed. Every number here was read out of the released configs and the diffusers implementation, not from marketing copy.

33 Bdense transformer
50layers
5376hidden size
56 × 128attention heads
0cross-attention layers
2noise schedules

01The three ideas you need first

Latents, tokens, and denoising

Everything below rests on three moves. If these are familiar, skip to §02.

Idea 1 · A latent is a compressed video

Raw video is enormous and mostly redundant. So a separate small network — an autoencoder, here called a VAE — is trained first, and only to do one thing: squash video into a much smaller grid of numbers (encode) and expand it back to pixels (decode). That small grid is the latent. H3's visual VAE shrinks each frame 16× in width and height, shrinks the frame rate 4×, and describes every surviving cell with 24 numbers ("channels") instead of 3 RGB values.

The big 33 B model never sees a pixel. It works entirely in latent space. Pixels exist only at the very start and the very end.

Idea 2 · A token is one cell of that grid — after patchify

A transformer does not consume grids; it consumes a flat list of vectors, called tokens. So the latent grid has to be chopped into a list. Patchify is that chop: H3 takes every non-overlapping 1 × 2 × 2 block (1 time-step × 2 latent rows × 2 latent columns), and glues those 4 cells' 24 channels into a single vector of 24 × 1 × 2 × 2 = 96 numbers. That 96-number vector is one token.

Net effect: 16× from the VAE, times another 2× from patchify, means 32× spatial shrink from pixels to tokens. Worked example below.

Idea 3 · Generating = repeatedly cleaning up noise

To generate, H3 fills the target tokens with pure random noise and then runs the transformer N times. Each run predicts "which direction is less noisy?" (a velocity), and a small non-learned routine called the scheduler takes one step in that direction. After N steps the noise has become a video. This is flow matching, a modern form of diffusion.

The transformer is the only learned part of that loop, and it is run from scratch every step — there is no carry-over cache between steps, unlike a chatbot generating words.

02Pixels to tokens, with real numbers

How 5 seconds of 720p becomes 27,280 tokens

Following one concrete clip all the way down. This is the single most useful diagram on the page: almost every later cost and design decision follows from the token count it produces.

RAW PIXELS 121 frames 1280 × 704 3 channels (RGB) 5 s at 24 fps = 327 M numbers VAE LATENT GRID 31 × 44 × 80 24 channels time ÷4 · space ÷16 colour → 24 numbers = 2.6 M numbers (124× less) patchify TOKENS 31 × 22 × 40 96 numbers each 1 × 2 × 2 cells glued 24 × 4 = 96 SEQUENCE LENGTH 27,280 video tokens for five seconds WHAT PATCHIFY DOES, UP CLOSE 24 24 24 24 4 neighbouring latent cells concat 96 numbers = one token Linear 5376 numbers what the transformer sees Audio is chopped the same way, on its own clock: one token = one 32-number audio latent frame, 40 per second, per stereo channel → 400 tokens for 5 s. Audio is only ~1.4% of the sequence. Video is the cost.
Read left to right. The 124× compression is done by the VAE before the big model runs, and patchify removes another 4×. Together they are why a 33 B model can handle five seconds of video at all: it processes 27,280 items, not 327 million. Numbers are for 1280×704; H3's real 2K mode is larger, and the token count grows with area.
Careful: a "latent frame" is not a token

These two words get used loosely and they mean different things. A latent frame is one time-slice of the latent grid — a whole two-dimensional sheet of cells, 44 × 80 in the example above. Patchified 1 × 2 × 2, that single sheet becomes 22 × 40 = 880 tokens, every one of which carries the same time coordinate and differs only in height and width.

So the 27,280 figure is 31 latent frames × 880 tokens each. Audio works differently: one audio latent frame is a single 32-number vector, which is exactly one token — two, counting stereo.

One latent framebecomessharing
video, 720p880 tokensone time coordinate, 880 distinct (h, w)
audio2 tokensone time coordinate, left and right

Per second of output that is 6 × 880 = 5,280 video tokens against 40 × 2 = 80 audio tokens — about 66 : 1. Whenever a diagram on this page shows a block for a video latent frame, picture 880 tokens stacked behind it.

03Noise, σ, and what "shift" buys

The generation loop, in exact arithmetic

§01 said "repeatedly clean up noise". Here is what that actually means, with H3's own conventions — which differ from the textbook version in two places, so it is worth being precise.

σ is just "what fraction of the mix is noise"

Take the finished video's latents, call them x₀. Take pure random noise the same shape. Now blend them on a straight line. H3 writes that blend as x_t = t·x₀ + (1 − t)·noise.

So t = 1 is the clean video and t = 0 is pure noise. (Careful: most diffusion papers use the opposite convention, where t = 0 is clean. H3 flips it.) The noise fraction is σ = 1 − t. Generation starts at σ = 1 and walks down to σ = 0. That is the entire meaning of "timestep" here — a mixing ratio, nothing more.

A · THE MIXING LINE — EVERY INTERMEDIATE STATE LIVES ON IT σ = 1 pure noise t = 0 · where we start σ = 0 finished video t = 1 · where we end x_t = t · x₀ + (1 − t) · noise the loop walks left → right; each step is one small move along this line B · "SHIFT" DECIDES WHERE THE STEPS LAND — SAME 10 STEPS, THREE VALUES OF s s = 1 no shift evenly spaced — equal attention to every noise level s = 3 H3 audio pulled toward the noisy end s = 12 H3 video nine of ten steps spent above σ = 0.6 — almost all effort on global structure σ = 1 · noise σ = 0 · done Formula: σ′ = s·σ / (1 + (s−1)·σ) applied to an evenly spaced grid. 10 steps shown for legibility; real runs use more. C · WHY VIDEO GETS THE BIGGER SHIFT High σ is where the layout, motion and composition are decided; low σ only sharpens texture. Video has far more structure to settle than a soundtrack does — so it is given nine-tenths of the budget up there, and audio only about half.
Shift is a budget reallocation, not a change of destination. Both schedules start at σ = 1 and end at σ = 0 and take the same number of steps; they differ only in where along the line those steps land. This is the single most consequential tuning knob in modern diffusion samplers.

What one step actually computes

Three lines of arithmetic, run once per step. The transformer only supplies the first one.

StepComputedMeaning
1 · transformerv = model(x_t, t)a velocity: which direction points toward the clean data. (H3's velocity is data-ward — the opposite sign to the usual flow-matching convention.)
2 · implied guessx₀ = x_t + σ·v"if I went all the way in that direction, here is the finished video I'd land on" — a rough guess early, sharp later
3 · partial movex_next = r·x_t + (1−r)·x₀
r = σ_next / σ
don't jump all the way — blend part-way toward the guess. This is the Euler step.

Repeat N times with σ decreasing, and the guess in line 2 improves each round until it is the video. Nothing here is learned — the scheduler is fixed arithmetic. Only model() has weights.

04The whole system

Four encoders in, two decoders out, one loop in the middle

H3 has two separate visual paths, and this trips people up. A reference image goes through the VAE (to get its appearance as latents) and through a full vision-language model (to get its meaning as text-like tokens). They do different jobs and both feed the same sequence.

video / visual text & semantics audio runs once
INPUTS ENCODE — ONCE PACK — ONCE DENOISE — REPEATED N TIMES DECODE Text prompt Ref images ≤ 9 Ref videos ≤ 3 Ref audio ≤ 3 Qwen3-VL 64 layers · d 5120 · 262k ctx + ViT 27 layers · patch 16 meaning → text-like tokens H3-VisualVAE · encode f16 t4 d24 — space ÷16, time ÷4 appearance → 24-ch latents then patchify 1×2×2 → 96 H3-AudioVAE · encode 32 kHz → 40 Hz · 32 ch rates 2·4·4·5·5 = 800× ↑ images/video take BOTH paths Pack one flat sequence + token_tags + position_ids (t,h,w) + row timestep plan target rows filled with pure noise §05 has the layout H3-Omni-Transformer 33 B dense · 50 layers · d 5376 full self-attention, no mask 56 heads × 128 = 7168 predicts velocity for every row scheduler video shift 12.0 scheduler audio shift 3.0 write ONLY the generated rows × N steps · one forward each guidance-distilled — no CFG double pass VisualVAE decode 36-layer transformer AudioVAE decode BigVGAN-style 2K video 32 kHz stereo The asymmetry that governs cost: everything outside the dashed box runs exactly once. Everything inside runs N times. So the encoders and decoders — Qwen3-VL included, despite being large — are a rounding error next to N passes of the 33 B transformer. Two checkpoints ship: transformer/ drives text-to-video and first/last-frame modes; transformer_ref/ drives the multi-reference mode. Same class, same shape. Not shown: In-Context Regeneration, which reaches 2K by re-running this loop with the 768p result as conditioning, rather than by a separate upscaler network.
The dual visual path is the non-obvious part. Qwen3-VL answers "what is this?" and the VAE answers "what does this look like?". A reference photo of a person contributes both a semantic description and pixel-accurate appearance latents, at different places in the sequence.

05The packed sequence

Everything becomes one flat list — and coordinates do the rest

There is no separate branch per modality. Text, reference frames, audio and the video being generated are concatenated into a single list of vectors, all 5376-dimensional, and the transformer attends over the whole thing at once. Three small side-tensors tell it what each row is.

LAYOUT A — TEXT-TO-VIDEO / FIRST-&-LAST-FRAME text tag 1 keyframe latents tag 0 · never overwritten audio tag 2 target video — the part being generated tag 0 · starts as pure noise · 27,280 rows not to scale — target video is ~98% of real length LAYOUT B — MULTI-REFERENCE text ref image clock += 1 ref audio of ref video ref video shares its origin target audio target video reference blocks appear in request order; each pushes a shared rotary clock forward by the time it occupies THE THREE SIDE-TENSORS — ONE ENTRY PER ROW token_tags 0 = video · 1 = text · 2 = audio picks which modulation row applies (§07) position_ids (t, h, w) where the row sits in time and space a reference and the target differ here first — and in noise level, never by a type label timestep_indices how noisy THIS row is right now clean references and noisy targets coexist in one forward pass The stereo trick Audio rows carry no height coordinate. Channel 0 is pinned to the frame's leftmost width coordinate and channel 1 to the rightmost so left/right stereo literally sits at the left/right edges of the picture. Spatial audio becomes learnable by ordinary attention, with no extra machinery.
Layout A vs B are two different packing functions over the same transformer. Neither uses an attention mask: the model is told what everything is through coordinates and tags, never by being forbidden to look.

06The clock, and how position gets in

Before anything else: this model has TWO unrelated "times"

They are easy to confuse and they never touch. One says when in the video a token sits; the other says how noisy it currently is. They enter the transformer as different arguments and travel down completely different paths.

QuantityMeansPath through the modelShape
position_ids[:,0]
"rotary time"
when in the video — units of 1/40 s → rope() → rotates Q and K (seq_len,)
timestep_indices
the diffusion σ of §03
how noisy this row is right now → AdaLN → scales/shifts activations (seq_len,)

The noise level never enters RoPE; video time never enters AdaLN. Everything in this section is about the first row. Section 03's σ is the second, and section 08 shows them meeting in one forward pass.

First: there is no "clock" object in the model

The clock is one number attached to each token — a single float, stored as the first of that token's three coordinates. Nothing more. The rest of this section is about why that number has to exist and what the model does with it.

The problem it exists to solve

A transformer's attention is permutation-invariant: shuffle the tokens and it computes the identical answer, just shuffled back. Position in the list means nothing. So on its own the model cannot tell which video token is frame 1 versus frame 30, or which snippet of sound belongs with which picture. Every token is just a vector of 5376 numbers.

And the two streams tick at different rates — video produces 6 latent frames per second, audio produces 40. If each were numbered by its own index, "video 3" and "audio 3" would be entirely different moments: 0.5 s versus 0.075 s. There would be no way to express "these two happen at the same time", which is precisely the thing a video-with-sound model has to know.

The fix: measure everything on one ruler

Both streams are given a time coordinate on a single shared scale, whose unit is 1/40 of a second — one audio latent. Video is converted onto that same scale. Different streams, different indices, different rates, but the same number when they mean the same instant:

TokenIts own indext — the shared coordinateReal time
audio latent000.000 s
video latent frame000.000 s
audio latent30300.750 s
video latent frame6300.750 s

Audio index 30 and video index 6 are the same moment, and the shared coordinate says so directly. That equality is the entire purpose of the clock.

ONE RULER, TWO STREAMS — FIRST 1.0 SECOND video latents 6 per second uneven widths 0 1 2 3 4 5 6 7 0 5 10 15 20 25 30 35 40 t 1 unit = 1/40 s = 1.000 s audio latents 40 per second evenly spaced 0 30 t = 30 → 0.750 s video latent 6 and audio latent 30 — different streams, different index numbers, same coordinate. Video blocks are uneven because the VAE packs 17 raw frames into 5 latents (1 + 4 + 4 + 4 + 4). The ruler measures elapsed time, so widths must compensate.
This is the whole idea. Without a shared ruler the model could order video frames among themselves and audio frames among themselves, but could never relate the two. Lip-sync would be unlearnable — not hard, inexpressible.

Why the split is 1, 4, 4, 4, 4 — and could not be 3, 3, 3, 3, 5

The uneven block widths above are not a partition anyone chose; they are what the encoder computes. The 4× temporal reduction is two stacked causal convolutions of stride 2 (time_down: [1,2,2,1,1,1] sets the stride, while the kernel is fixed at 3 with a two-step causal pad on the past side). Stride and kernel then decide two different things, and it is worth keeping them apart.

17 RAW FRAMES → 5 LATENTS, VIA TWO STRIDE-2 STAGES raw frames 0 16 STRIDE decides the time allocation this is what the clock encodes 0 1 2 3 4 1 frame 4 frames 4 frames 4 frames 4 frames KERNEL 3 decides what each one sees wider, and overlapping latent 0 sees f0 only latent 1 sees f0 – f4 latent 2 sees f2 – f8 latent 3 sees f6 – f12 latent 4 sees f10 – f16 Read the allocation as 17 = 1 + 16. The 16 real frames are split perfectly evenly — 4, 4, 4, 4. The lone anomaly is an anchor latent at the front, which exists because causal padding leaves the first output no past to look at: it can only reach frame 0, so it becomes a latent of its own.
Two facts, often confused. The (1,4,4,4,4) in the rotary code is the stride allocation — how much clock time each latent advances — not the receptive field. Each latent actually sees beyond its nominal slot and shares frames with its neighbours, which is what gives the decoder enough context to reconstruct smooth motion across latent boundaries. Because latent 0 advances the clock by 1 frame where its neighbours advance 4, its rotary span is 5/3 units against 20/3 — so the time coordinate stays proportional to real elapsed time however lopsided the latents are.
Why 3, 3, 3, 3, 5 is not merely worse — it is not expressible

A convolution has one stride, not a different one per position. Uneven groupings like 3, 3, 3, 3, 5 cannot be produced by a strided conv stack at all; you would need a different architecture entirely, with learned or variable-width pooling.

And 4 = 2 × 2 is precisely what allows the reduction to be built from two stride-2 stages (assert time_stride in [1, 2] in the source). 3, 3, 3, 3, 5 factors into nothing usable.

Why the front anchor is wanted, not merely tolerated

A causal downsampler with ratio r maps T frames to (T−1)/r + 1 latents. That formula has a property plain T/r does not: T = 1 gives exactly 1 latent. T/r would give a quarter of a latent, which means nothing.

H3 cannot function without T = 1 working — reference images, first/last-frame conditioning and joint image+video training all feed single stills through this same encoder. The lopsided first latent is the price of that, and it is a price worth paying.

How the number actually enters the model

The coordinate is never fed in as a feature. It is not concatenated, not added to the token, not embedded. Its one and only use is to rotate the query and key vectors before attention multiplies them together — the technique called RoPE.

THE ENTIRE PATH — FOUR STEPS, THEN PLAIN ATTENTION position_ids[:, 0] one float per token rope() → 96 cos/sin angles rotate Q and K in all 50 blocks ordinary attention unchanged, unmasked Never concatenated · never added to the token · never an input feature. Rotation only. WHY ROTATING GIVES RELATIVE POSITION FOR FREE t = 30 t = 34 angle between = 4 units apart Attention scores are dot products. Rotating two vectors by angles θa and θb leaves their dot product depending only on θa − θb. Same t → zero angle between them → nothing damps their attention. That is the mechanism by which an audio token and a video token "discover" they belong to the same instant. No alignment loss, no sync module, no cross-attention — just a coordinate they agree on. 128 channels of one head 96 rotated 32 not 32 time · 32 height · 32 width the spare 32 carry content with no positional flavour
Three axes, one mechanism. Height and width work identically to time — they just rotate different channel groups. "Where in the frame" and "when in the clip" are the same kind of fact to this model.

How this differs from a language model's RoPE

In an LLM, RoPE position is a single integer — the token's index, 0, 1, 2, 3 … — and one axis rotates the whole head. H3 needs three numbers because its data is three-dimensional, so each head's channels are divided between the axes:

Language modelMiniMax H3
Position isone scalar — the token indexthree floats — (t, h, w)
Per tokenposition_ids: (seq_len,)position_ids: (seq_len, 3)
Channel useall of head_dim rotated by one axis32 by t · 32 by h · 32 by w · 32 untouched
Unitstoken counts (integers)1/40 s for time; a fixed [0, 32) canvas for space
Text is the degenerate case — and that is the point

Text tokens get t = 0, 1, 2, … (their index) and h = w = 0. So on the time axis they behave exactly like ordinary 1-D language-model RoPE, and the two spatial axes contribute nothing. No separate text-position machinery was needed: the 3-D scheme contains the 1-D one.

What every kind of token gets

Tokenth, w
text0 … N_text−1 (its index)0, 0
reference imageone slot — a still has no durationits own frame grid
reference videoits own temporal grid, from the block's originits own frame grid
reference audiolatent index from the block's originh = 0, w at the stereo extremes
keyframe anchorcoincident with the target frame it pinsframe grid
target audiolatent index from the target originh = 0, w at the stereo extremes
target videothe target's temporal gridframe grid

Every token gets all three coordinates. Some simply leave axes at zero.

Where the time unit lives — and it is not in theta

A natural guess is that "1 unit = 1/40 second" is encoded in RoPE's theta. It is not. The two are cleanly separated, and keeping them apart is what lets one frequency ladder serve time, height and width at once.

KnobWhat it setsValue in H3
rope_theta the frequency ladder — how a single number is fanned out across 16 channels, inv_freq[j] = θ^(−j/16) 10000 — generic, shared by all three axes
position_ids the unit — what number stands for this token's moment or place 5/3 per raw frame (time)
scale 32 (space)

The rotation is simply angle = position × inv_freq[j]. Theta answers "how do I spread one number over 16 channels"; position_ids answers "what is the number". Change the time unit and you edit the packing code, never the model.

Where 5/3 comes from — it is pure unit conversion

The clock unit is chosen to be one audio latent = 1/40 second. A video frame at 24 fps lasts 1/24 second, so in clock units it is (1/24) ÷ (1/40) = 40/24 = 5/3. That is the whole derivation, and that constant — _ROPE_FRAME_RESCALE — is the only place the model's sense of time is set.

Two things confirm the separation. rope_theta and rope_freq_dim live in transformer/config.json and would be byte-identical for 30 fps video or 48 kHz audio — only the packing constants would move. And rope.inv_freq is computed, not loaded: it is not a stored tensor, let alone a learned one. Nothing in the 33 B of weights encodes a time unit.

The chain end to end: _ROPE_FRAME_RESCALE → _temporal_position_grid() → position_ids[:,0] → rope() → rotate Q, K.

One ladder, three axes, very different ranges

Because the same 16 frequencies are reused for t, h and w, and those axes span wildly different ranges, each axis ends up effectively using a different part of the ladder:

AxisRange it spansFastest channelSlowest channel
space (h, w) [0, 32) — aspect-normalised 32 rad ≈ 5.1 turns across the frame 0.006 rad — effectively flat
time (t) 0 … ≈600 for a 15 s clip wraps ~95 times 0.107 rad — a near-linear absolute ramp

Space uses only the fast end; time uses the whole ladder, with the slow channels behaving as a coarse "how far into the clip am I" signal. Wavelengths run from 6.28 clock units (0.157 s) down to 35,333 units (883 s) — far longer than any clip, which is exactly what makes the slow end monotonic.

A consequence worth noticing: spatial coordinates are resolution-independent

The spatial grid is normalised by √(height × width) and centred on [0, 32), so a 768p frame and a 2K frame produce the same coordinate range — only the sampling density between the endpoints changes. Aspect ratio is preserved: a 16:9 frame spans wider in w than in h, both centred on the same midpoint.

That is how one set of weights generates at several resolutions. The model never learns "position 40 of 80"; it learns positions on a fixed unit canvas. It also explains why In-Context Regeneration can feed a 768p result back in as conditioning for a 2K pass — both live on the same coordinate system.

The third job: telling a reference from the thing being generated

This is the use that surprises people. A reference frame and a generated frame are identical kinds of object — same VAE, same patchify, same 96 numbers, same input projection, sitting in the same list. No label marks one as "given" and the other as "to be made". There is no learned "am I a reference" vector anywhere in the 33 B of weights.

It is a coordinate, not an ID — the distinction matters

A familiar way to separate two kinds of token is a segment embedding: a small learned table, x = x + segment_embed[0 or 1], added once at the input. H3 does nothing of the sort.

Segment-ID approachWhat H3 does
Mechanismlearned lookup tablea coordinate value
Applied byaddition, once at the inputrotation of Q and K, in every block
It says"you are type A, I am type B""you are at time 5, I am at time 200"

Three signals actually separate them, and none is a type label:

SignalCarried byHow
Time coordinateRoPEreferences occupy coordinates the target does not — or coincide deliberately with the frames they pin
Noise levelAdaLNreference rows sit at a pinned low σ while target rows ride the moving schedule, selecting a different modulation row
Content statisticsthe latents themselvesa reference is clean structure; a target at high σ is nearly pure noise

H3 does have one genuinely ID-like mechanism — token_tags (0 video, 1 text, 2 audio) indexing per-modality AdaLN rows. But it tags modality, never reference-versus-target.

The two conditioning modes then exploit the time coordinate differently:

First / last-frame mode anchors sit ON the target's own timeline — overlapping text the frames being generated first last given frames share a coordinate WITH generated ones → "this exact frame belongs exactly here" Multi-reference mode references queue BEFORE the target — disjoint text img ref audio ref video target audio + video start where the references ended no coordinate is ever shared with the target → "these happened earlier, they are exemplars, not placements" a still image advances the coordinate by exactly 1 unit — it has no duration; a reference video advances it by its real length Same weights, same attention, same everything. The difference between "start from this frame" and "make something that looks like this" is arithmetic on one number.
Text sits on this axis too, occupying coordinates 0 … N−1, with all media starting after it — so prompt length literally shifts where the video begins on the shared ruler.

Summary — three jobs, one number

JobWhat would break without it
Order the video framesno notion of motion or direction; a shuffled clip would look the same to the model
Align audio to videolip-sync would be inexpressible, not merely hard
Separate reference from targetthe model could not tell what it was given from what it must invent

07Inside one of the 50 blocks

What AdaLN is, from the beginning

This term has been used throughout without being defined. It stands for Adaptive Layer Normalization, the idea is small, and everything distinctive about how H3 handles noise rests on it.

Ordinary normalization → adaptive normalization

Every transformer normalizes activations before each sublayer, then applies a learned per-channel scale and shift: y = norm(x) · γ + β. Those two are learned constants — identical for every input the model will ever see.

AdaLN makes them computed instead of constant: y = norm(x) · (1 + scale) + shift, where scale and shift come out of a small network fed some conditioning signal. In diffusion that signal is the noise level.

So the same weights behave one way at σ = 0.9 and another at σ = 0.1 — one network acting like many, with the noise level turning the dial. Which is exactly what the task needs: at high noise a block should be settling coarse global structure, at low noise sharpening fine texture.

ORDINARY — CONSTANTS y = norm(x) · γ + β γ and β are learned once and frozen the layer does the same thing to every input, forever ADAPTIVE — COMPUTED PER CONDITION y = norm(x) · (1 + scale) + shift scale and shift come from MLP(noise level) the layer behaves differently at every noise level H3 EMITS SIX VALUES PER BLOCK — THREE FOR EACH SUBLAYER σ adaln_proj shift_msa scale_msa gate_msa shift_mlp scale_mlp gate_mlp two of each: one set for attention, one for the feed-forward x = residual + gate · sublayer( norm(x)·(1+scale) + shift ) shift and scale steer the input; gate controls how much comes back out The gate is the "AdaLN-Zero" trick. Initialised to zero, so every block starts as an exact identity function and then learns how much to contribute. That is what lets diffusion transformers train stably at 50 layers deep. Lineage: FiLM (2017) → AdaIN → AdaLN in DiT (2022) → AdaLN-Zero.
Why not simply add the noise level as an extra token? Because it would be one token among 27,000 — a whisper. The noise level must reach every channel of every token, strongly. Modulation does that directly, and costs nothing per token since the values depend only on σ.
H3's departure from a normal diffusion transformer

In a standard DiT there is one noise level for the whole image, so AdaLN is computed once per forward pass and applied uniformly. H3 instead builds a table indexed by (noise level, modality), and every row of the sequence looks up its own entry: adaln_indices = timestep_indices × 3 + token_tags.

That per-row lookup is precisely what lets a clean reference frame and a fully noised target frame sit in the same sequence and be processed correctly by the same weights — the mechanism §08 depends on.

Standard transformer, plus a per-row noise dial

The block itself is conventional: normalise, attend, normalise, feed-forward, with residual connections. What is unusual is AdaLN — the mechanism that tells each individual row how noisy it is. The timestep is turned into six vectors that stretch, shift and gate the activations, and each row looks up its own set.

THE RESIDUAL STREAM — 5376 NUMBERS PER ROW input from previous block RMSNorm × (1 + scale) + shift  — AdaLN Self-attention 56 heads × 128 = 7168 · QK-RMSNorm MM-RoPE applied to Q and K · no mask × gate, then add residual RMSNorm → AdaLN again SwiGLU feed-forward 5376 → 14336 → 5376 × gate, then add residual THE NOISE DIAL — 40% OF ALL PARAMETERS timestep → 256 sinusoids MLP 256 → 5376 → 2688 adaln_proj — per block Linear 2688 → 6 × 5376 × 3 produces six vectors: shift scale gate …once for attention, once for the feed-forward HOW A ROW FINDS ITS OWN SET adaln_indices = timestep_indices × 3 + token_tags the lookup table — one row per (noise level, modality): t₀ · video t₀ · text t₀ · audio t₁ · video t₁ · text t₁ · audio … etc This is the whole mechanism. A clean reference frame and a fully noised target frame sit side by side in one sequence, attend to each other freely, and are told apart only by which table row they select.
MM-RoPE, referenced in the attention box, is how position enters: the (t, h, w) coordinates are baked into the queries and keys by rotation, so attention scores depend on relative position. 96 of each head's 128 channels are rotated; the remaining 32 carry position-free content.

08One denoising step, in detail

Every row has its own noise level, and only some rows get written

This is where H3's design pays off. In a single forward pass, reference rows sit at a fixed low noise level while target rows step down their own schedules — and because references are simply never written back, they stay bit-exact for all N steps. No masking, no re-noising, no paste-back.

STEP i — THE SEQUENCE GOING IN text rows t = video's t (never reaches an output head) reference / keyframe video t = pinned, low reference audio t = pinned, low target audio t = audio schedule, shift 3.0 target video t = video schedule, shift 12.0 50 blocks full self-attention across ALL rows every row sees every row VELOCITY PREDICTED FOR… reference rows → computed, then discarded target audio → audio_scheduler.step() target video → scheduler.step() only these two slices are written back WHY TWO SCHEDULES — σ′ = s·σ / (1 + (s−1)·σ) progress through the loop → 1 0 noise σ′ video · s = 12 audio · s = 3 At the halfway point of the loop, video is still at σ′ = 0.92 while audio has already dropped to 0.75. The two modalities are deliberately not synchronised in noise — video is given far more of the budget at high noise, where global structure is decided. They are synchronised in attention instead: at every step, partially-formed audio attends to noisier video and vice versa. Lip-sync is not a loss term or a module — it emerges from co-denoising in a shared sequence on a shared clock.
The discarded predictions are real waste — velocity is computed for reference rows every step and thrown away. It buys an unmasked attention pattern, which is the better trade: every backend stays usable and there is no mask to build.
Why there is no KV cache here

In a chatbot, the prompt is encoded once and reused for every generated word. That does not work here. Text rows inherit the video timestep, which moves every step, so their AdaLN modulation — and therefore their contribution to attention — changes every step. And because attention is bidirectional, information flows from the changing target rows back into the reference rows after the very first block.

Net: the only reusable region is block 0's keys and values for the reference rows. Everything else is recomputed N times. That is the structural reason video diffusion is so much more expensive per output than text generation.

09Where the 33 billion parameters actually go

The noise dial is bigger than attention and feed-forward combined would suggest

PARAMETERS IN ONE BLOCK — 645.5 M TOTAL Attention 154.1 M · 24% SwiGLU feed-forward 231.2 M · 36% AdaLN projection 260.2 M · 40% × 50 blocks = 32.3 B, plus a 2-layer text refiner (0.77 B) and the embedders → ≈ 33 B. Of that, 13.0 B sits in the AdaLN projections — and depends only on the timestep, never on the sequence. Every step re-reads all 13 B from memory to produce a matmul over ~4 rows. The whole schedule is known before the loop starts, so it could be computed once up front.
Ordinal ramp, not categorical colour — the three segments are ordered by size, and each is directly labelled, so the encoding never rests on hue alone.

10What is deliberately missing

Sparse attention — trained with, not shipped

MiniMax states that native sparse attention was introduced during the final stage of training to cut the cost of long sequences, and that the open release runs full attention only; the sparse implementation is promised in a future update. Reading the released code confirms it: there is no mask builder, no block selection, no config flag, not even a disabled branch.

One fingerprint of the internal version survives in a source comment — the reference implementation pads the sequence to a multiple of 64 for FlashAttention and carries cu_seqlens. That is the variable-length / block-table interface, which means whatever the algorithm is, it is block-structured at 64-token granularity.

Consequence for anyone using the weights: you pay the full N² attention cost that the sparsity existed to avoid, and the configuration you are running is not the one the final training stage was tuned for. Dense attention is a superset of any sparsity mask, so quality holds — but efficiency benchmarks against the public release are measuring a deliberately handicapped path.

Also absent by design

Cross-attentionzero layers — stated verbatim in the source
Attention masknone; one packed document per request
Per-modality blocksnone in attention or feed-forward
CFG / guiderdistilled away — one forward per step, not two
Separate upscaler2K via In-Context Regeneration instead

Where modality actually lives

Input projectionsproj_in 96→5376 · audio_proj_in 32→5376
AdaLN tag3 modulation rows per noise level
Output headsproj_out 5376→96 · audio_proj_out 5376→32
Everything elseshared, modality-blind