Skip to content

[perf] MiniMax H3: fuse VAE decoder QK RMSNorm and partial RoPE - #2041

Open
aryan5v wants to merge 3 commits into
mainfrom
aryan/h3-vae-fused-qk-norm-rope
Open

aryan5v wants to merge 3 commits into
mainfrom
aryan/h3-vae-fused-qk-norm-rope

Conversation

@aryan5v

@aryan5v aryan5v commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • End to end (Pika H3 Flex, 768x1344, 1 GB200), on top of tile batching: VAE decode -35% (3.65 s -> 2.37 s at 5 s clips; 12.7 s -> 8.1 s at 15 s), about 1.3-1.4 s (~7%) off a 5 s generation.
  • Exact precision: same arithmetic as eager; only the fp32 RMSNorm reduction order differs (every differing pixel is off by one 8-bit code, 73.6 dB PSNR vs eager).
  • Opt-in: FASTVIDEO_H3_VAE_FUSED_QK_NORM_ROPE=1.

How this was found. These changes came out of an agent-driven kernel optimization run with MotionKernel, a tool we're developing that profiles a real generation, finds where the time goes, and only keeps changes that hold up end to end with the quality checks below.

Problem

Every attention layer of the H3 video VAE decoder ran its Q/K prologue as ten kernels:

query = self.norm_q(query.float()).to(query.dtype)   # cast + fp32 RMSNorm + cast
key   = self.norm_k(key.float()).to(key.dtype)
... 4x torch.cat, 2x neg, 4x mul/add ...             # rotate-half partial RoPE

At 768x1344 that is ~1.9 ms per layer (measured; 36 layers x 21 batched tile decodes at tile
batch 8 = ~1.4 s of a 3.65 s decode), and most of it is kernel-launch and elementwise traffic on
[8, 1797, 32, 64] tensors.

Fix

One Triton kernel (fastvideo/models/vaes/minimax_h3_qk_norm_rope.py) reads Q and K once,
normalizes in fp32, rounds to the activation dtype exactly as eager does, applies the rotate-half
RoPE on the rope prefix with the same per-op rounding, and stores back in place: one launch per
attention layer per chunk instead of ten. Opt-in via
FASTVIDEO_H3_VAE_FUSED_QK_NORM_ROPE=1; the eager prologue is factored into _apply_rotary and
kept as the CPU / unsupported-shape fallback (_can_fuse_qk_norm_rope checks CUDA, contiguity,
even rope width and a power-of-two head dim).

Evidence

Decode-only, tile batch 8, one loaded VAE, warm, interleaved, GB200 (768x1344, fp16 autocast):

frames arm decode s (runs) median peak alloc
107 eager 3.643 / 3.647 3.647 16.13 GiB
107 fused 2.374 / 2.310 2.374 16.13 GiB
362 eager 12.728 / 12.741 / 12.741 12.741 16.16 GiB
362 fused 8.083 / 8.074 / 8.081 8.081 16.16 GiB
  • -35% / -36.6% decode time, no extra peak memory.
  • Kernel microbench at the production layer shape ([8, 1797, 32, 64] fp16): eager prologue
    1.875 ms -> 0.090 ms per layer (20.8x); the existing DiT fused_qknorm_rope kernel takes
    0.619 ms at the same shape.

Parity (this is an exact-precision fusion, not a bitwise one)

The arithmetic is the eager arithmetic; only the fp32 RMSNorm reduction order differs (Triton's
tree vs at::native layer norm). Measured on the real VAE decode:

shape max abs diff mean abs PSNR differing uint8 values
107 frames 1/255 0.0029/255 73.57 dB 0.29%
362 frames 1/255 0.0028/255 73.60 dB 0.28%

Every differing pixel is off by exactly one 8-bit code value. Q/K parity on random
[8, 337, 32, 64] fp16 inputs including register/cls suffix tokens: bit-equal 99.998%, rel-L2
3.7e-6, max |d| 1 ULP (bf16: 99.9996%, 9.9e-6). This matches how upstream already treats the DiT
fused_qknorm_rope ("Triton's reduction order ... can produce small, expected rounding drift").

Tests

python -m pytest fastvideo/tests/vaes/test_minimax_h3_qk_norm_rope.py \
    fastvideo/tests/stages/test_minimax_h3_vae_streaming.py \
    fastvideo/tests/vaes/test_minimax_h3_video_vae_streaming.py \
    fastvideo/tests/vaes/test_minimax_h3_parallel_vae.py -q
# 102 passed on GB200 after the review fixes, which add the fusion-gate tests (originally 4 new: CPU no-op fallback, Q/K parity fp16/bf16, in-model attention parity)

Verification on current main (d9e6c3e): the patch applies cleanly; pre-commit (yapf, ruff, codespell, PyMarkdown, mypy) passes on all changed files; the tests listed above were run on GB200.

Independent GB200 verification (after the review fixes)

An agent rechecked this PR on a separate GB200:

  • Decode on top of tile batch 8: 3.72 → 2.37 s at 107 frames (−36%) and 13.00 → 8.40 s at 362 frames (−35%). Peak memory unchanged.
  • Kernel: 1.59 → 0.061 ms at [8,1797,32,64] fp16.
  • Parity on a real 107-frame latent: 0.28% of uint8 values differ, each by one code.
  • End to end: base MiniMax-H3 124 frames: wall −1.70 s.
  • Review fixes: the fused path now requires packed rotary tables on the query's device, no gradient tracking, and matching Q/K dtypes; anything else runs eager.

@mergify

mergify Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Merge Protections

🔴 1 of 1 protections blocking · waiting on 👀 reviews and 🤖 CI

Protection Waiting on
🔴 PR merge requirements 👀 reviews and 🤖 CI

🔴 PR merge requirements

Waiting for

  • #approved-reviews-by>=1
  • check-success=fastcheck-passed
  • check-success=full-suite-passed
This rule is failing.
  • #approved-reviews-by>=1
  • check-success=fastcheck-passed
  • check-success=full-suite-passed
  • check-success~=pre-commit
  • title~=(?i)^\[(feat|feature|bugfix|fix|refactor|perf|ci|doc|docs|misc|chore|kernel|new.?model|skill|skills|infra)\]

@mergify mergify Bot added type: perf Performance improvement scope: infra CI, tests, Docker, build scope: model Model architecture (DiTs, encoders, VAEs) labels Oct 30, 2026
@greptile-apps

greptile-apps Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 3/5

[Low impact] The change is limited to an opt-in path in MiniMax-H3 video VAE decoding.

Fix the rotary layout and gradient guards, and satisfy the comment requirement before merging.

Findings

  1. P1 Shared rotary tables break attention ▶
  2. P1 Decoder gradients become wrong ▶
  3. P2 Added comment violates repository rules ▶
Fix with agent prompt
### Issue 1
fastvideo/models/vaes/minimax_h3_video.py:324-327
`_can_fuse_qk_norm_rope` accepts broadcast or strided rotary tables, but the kernel reads them as packed `[B, S, 1, RD]` buffers. Passing `[1, S, 1, RD]` tables with batch size 2 works in the eager path, but the fused path reads past those tables for the second batch. This can produce wrong results or an illegal GPU memory access.

Check both tables' shapes, devices, and contiguity before selecting the kernel; otherwise use the eager path.

### Issue 2
fastvideo/models/vaes/minimax_h3_video.py:386-391
With the flag enabled, the fused branch also runs on CUDA calls that need gradients. Its in-place Triton writes record no backward operation for RMSNorm or RoPE. A decoder forward followed by backward therefore silently computes the wrong gradients for the query/key projections and earlier layers.

The decoder explicitly supports gradient checkpointing. Keep gradient-tracked calls on the eager path, as the existing DiT fusion does.

### Issue 3
fastvideo/models/vaes/minimax_h3_video.py:387-388
This comment narrates which operations the fused call replaces and repeats the kernel documentation. The repository requires removing comments outside its small keep-list, which includes required non-obvious behavior and public API docs-not performance narration. Remove these two lines before merging to satisfy that requirement.

```suggestion

```

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Adds an opt-in Triton kernel that combines the H3 VAE decoder's query/key RMSNorm and partial RoPE.

  • MiniMax-H3 VAE attention can fuse Q/K normalization and rotary embedding.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Query and key projections] --> B{Flag enabled and checks pass?}
  B -->|Yes| C[In-place fused RMSNorm and RoPE]
  B -->|No| D[Eager RMSNorm and RoPE]
  C --> E[Attention]
  D --> E
Loading

Reviews (1) · Last reviewed commit: "[perf] MiniMax H3: fuse VAE decoder QK R..." · Reviewed by Greptile

Comment on lines +324 to +327
if not key.is_contiguous() or key.shape != query.shape or not query.is_floating_point():
return False
head_dim, rotary_dim = query.shape[-1], rotary_emb[0].shape[-1]
return 0 < rotary_dim <= head_dim and rotary_dim % 2 == 0 and head_dim & (head_dim - 1) == 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Shared rotary tables break attention

_can_fuse_qk_norm_rope accepts broadcast or strided rotary tables, but the kernel reads them as packed [B, S, 1, RD] buffers. Passing [1, S, 1, RD] tables with batch size 2 works in the eager path, but the fused path reads past those tables for the second batch. This can produce wrong results or an illegal GPU memory access.

Check both tables' shapes, devices, and contiguity before selecting the kernel; otherwise use the eager path.

Prompt To Fix With AI
This is a comment left during a code review.
Path: fastvideo/models/vaes/minimax_h3_video.py
Line: 324-327

Comment:
**Shared rotary tables break attention**

`_can_fuse_qk_norm_rope` accepts broadcast or strided rotary tables, but the kernel reads them as packed `[B, S, 1, RD]` buffers. Passing `[1, S, 1, RD]` tables with batch size 2 works in the eager path, but the fused path reads past those tables for the second batch. This can produce wrong results or an illegal GPU memory access.

Check both tables' shapes, devices, and contiguity before selecting the kernel; otherwise use the eager path.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in aa2fa42: the gate now requires contiguous [B, S, 1, RD] cos/sin tables on the query's device, with B and S matching the query. Anything else, including broadcast [1, S, 1, RD] tables, takes the eager path. The production rotary embedding still qualifies because its position ids are materialized per batch.

Comment on lines +386 to +391
if envs.FASTVIDEO_H3_VAE_FUSED_QK_NORM_ROPE.get() and _can_fuse_qk_norm_rope(query, key, rotary_emb):
# One Triton pass replaces the two fp32 RMSNorm kernels, the two activation-dtype
# casts and the four cat/neg/mul/add kernels of the eager prologue.
from fastvideo.models.vaes.minimax_h3_qk_norm_rope import qk_norm_rope_
cos, sin = rotary_emb
cos = cos.to(query.dtype)
sin = sin.to(query.dtype)
rotary_dim = cos.shape[-1]
query_rotary, query_pass = query[..., :rotary_dim], query[..., rotary_dim:]
key_rotary, key_pass = key[..., :rotary_dim], key[..., rotary_dim:]
query_first, query_second = query_rotary.chunk(2, dim=-1)
key_first, key_second = key_rotary.chunk(2, dim=-1)
query_rotated = torch.cat([-query_second, query_first], dim=-1)
key_rotated = torch.cat([-key_second, key_first], dim=-1)
query = torch.cat([query_rotary * cos + query_rotated * sin, query_pass], dim=-1)
key = torch.cat([key_rotary * cos + key_rotated * sin, key_pass], dim=-1)
qk_norm_rope_(query, key, cos, sin, self.heads, query.shape[-1], cos.shape[-1], float(self.norm_q.eps))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Decoder gradients become wrong

With the flag enabled, the fused branch also runs on CUDA calls that need gradients. Its in-place Triton writes record no backward operation for RMSNorm or RoPE. A decoder forward followed by backward therefore silently computes the wrong gradients for the query/key projections and earlier layers.

The decoder explicitly supports gradient checkpointing. Keep gradient-tracked calls on the eager path, as the existing DiT fusion does.

Prompt To Fix With AI
This is a comment left during a code review.
Path: fastvideo/models/vaes/minimax_h3_video.py
Line: 386-391

Comment:
**Decoder gradients become wrong**

With the flag enabled, the fused branch also runs on CUDA calls that need gradients. Its in-place Triton writes record no backward operation for RMSNorm or RoPE. A decoder forward followed by backward therefore silently computes the wrong gradients for the query/key projections and earlier layers.

The decoder explicitly supports gradient checkpointing. Keep gradient-tracked calls on the eager path, as the existing DiT fusion does.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in aa2fa42: calls that track gradients (torch.is_grad_enabled() with Q or K requiring grad) now use the eager path, matching the DiT fusion gate.

Comment on lines +387 to +388
# One Triton pass replaces the two fp32 RMSNorm kernels, the two activation-dtype
# casts and the four cat/neg/mul/add kernels of the eager prologue.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Added comment violates repository rules

This comment narrates which operations the fused call replaces and repeats the kernel documentation. The repository requires removing comments outside its small keep-list, which includes required non-obvious behavior and public API docs—not performance narration. Remove these two lines before merging to satisfy that requirement.

Suggested change
# One Triton pass replaces the two fp32 RMSNorm kernels, the two activation-dtype
# casts and the four cat/neg/mul/add kernels of the eager prologue.

Rule Used: What: Remove all unwanted code comments in the scoped diff except for a small keep-list (license headers, dependency/protocol-required non-obvious behavior, // prettier-ignore, public API doc comments, and issue/RFC constraint links). Why: Comment... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: fastvideo/models/vaes/minimax_h3_video.py
Line: 387-388

Comment:
**Added comment violates repository rules**

This comment narrates which operations the fused call replaces and repeats the kernel documentation. The repository requires removing comments outside its small keep-list, which includes required non-obvious behavior and public API docs—not performance narration. Remove these two lines before merging to satisfy that requirement.

```suggestion

```

**Rule Used:** What: Remove all unwanted code comments in the scoped diff except for a small keep-list (license headers, dependency/protocol-required non-obvious behavior, `// prettier-ignore`, public API doc comments, and issue/RFC constraint links).  Why: Comment... ([source](https://app.greptile.com/hao-ai-lab/-/custom-context?memory=856788fb-b8e1-47f8-b01b-d83ea205fc6b))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in aa2fa42.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: infra CI, tests, Docker, build scope: model Model architecture (DiTs, encoders, VAEs) type: perf Performance improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant