Compare commits

...
Author SHA1 Message Date
Aryan KumarandAryan Kumar 661d18ba55 [docs] Pika H3 Flex cookbook: --weights for the NVFP4 QAD releases and FP4 attention (#2054)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-11-03 01:07:54 -08:00
Aryan KumarandAryan Kumar 4b4df1a94e [perf] Fused Q/K preconditioning + NVFP4 quantize for FA4 NVFP4 attention (bit-identical) (#2052)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-11-03 01:07:11 -08:00
Aryan KumarandAryan Kumar 7066408fb3 [feat] Pika H3 Flex NVFP4 QAD training: calibration, QAD, FP4 attention and export (#2051)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-11-03 01:04:26 -08:00
Aryan KumarandAryan Kumar fcec554456 [feat]: shared FastH3 NVFP4 FP4-attention QAD recipe (OmniRef + V2 T2AV) (#1989)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-11-03 00:54:28 -08:00
Aryan KumarandAryan Kumar 181725e3fd [docs] Fix cookbook component selection and memory reporting (#2045)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-11-03 00:54:18 -08:00
Aryan KumarandAryan Kumar 3fe9c0c70d [bugfix]: get the full CI suite green on main (pytest 9 flag teardown, Qwen golden env) (#2059)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-11-02 23:32:40 -08:00
William Lin de873eae2d [bugfix] Pass disable_autocast through to FastVideoArgs (#2056) 2026-11-02 13:42:08 -08:00
4f80c5a3da [feat] Open-H3-Regenerate-2K: regenerate FastH3 QAD, Trim and V2 drafts at 2K (text to 2K 3.1-3.4x faster) (#2037)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
Co-authored-by: Aryan <aryan@aryan5v.dev>
2026-11-02 03:44:28 -08:00
Aryan KumarandAryan Kumar b0047cfedc [feat] Open-H3-Regenerate-2K: NVFP4 H3 video decoder (decode 23 s to 5 s at 2K) (#2036)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-11-01 22:51:41 -08:00
Aryan KumarandAryan Kumar ac81198d33 [new-model] Open-H3-Regenerate-2K: 768P H3 draft to 2560x1440, served on the OpenAI video routes (#2035)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-11-01 22:45:09 -08:00
Aryan KumarandAryan Kumar cb02062930 [new-model] Add Pika H3 Flex inference support (#2038)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-11-01 22:33:10 -08:00
Aryan KumarandAryan Kumar d954fad9ec [perf] MiniMax H3: faster FP4 attention padding, uint8 VAE pixel path and cuDNN FP4 GEMMs (#2046)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-11-01 22:23:10 -08:00
Aryan KumarandAryan Kumar 166b9b8a07 [perf] MiniMax H3: fuse VAE decoder QK RMSNorm and partial RoPE (#2041)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-11-01 22:15:35 -08:00
Aryan KumarandAryan Kumar 7e1ed5e28e [perf] MiniMax H3: decode eight VAE spatial tiles per call by default (#2040)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-11-01 22:06:23 -08:00
Aryan KumarandAryan Kumar cb6a9a13e1 [bugfix] FA4 JIT cache vs tuning env, MXFP8 import and scale dtype, H3 VSA gate double quantization (#2049)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-11-01 19:11:25 -08:00
Aryan KumarandAryan Kumar 0d1244a869 [perf] MiniMax H3: opt-in faster kernel tier for the DiT fused ops (#2042)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-11-01 15:22:09 -08:00
Aryan KumarandAryan Kumar 18b2a35106 [bugfix] FA4: stop forwarding max_seqlen to the varlen kernel (#2043)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-11-01 15:21:38 -08:00
Aryan KumarandAryan Kumar 4d0a10d989 [docs] Fix the docs build, cookbook memory status, and broken commands (#2023)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-10-30 17:08:41 -07:00
Lifa Ha d9e6c3eb82 [bugfix] Fix extra work in VSA64 layout benchmark (#2027) 2026-10-30 02:30:07 +00:00
Aryan Kumar 7203b4976e [bugfix]: compile only the LoRA delta, not the wrapped base layer
BaseLayerWithLoRA.forward was decorated with torch.compile, so wrapping a linear also compiled its base layer.
Compiling a packed NVFP4/FP8 linear on its own changes its numerics, so a wrapped layer at strength 0 could differ
from the unwrapped layer. forward now adds no compiled region: the base layer runs exactly as it would unwrapped
(eagerly, or traced with a caller's compiled block), and only the adapter delta is compiled.
2026-10-28 14:49:15 -07:00
Aryan Kumar 6bb444eef1 [bugfix]: run the base layer while a LoRA wrapper has no adapter 2026-10-28 14:49:15 -07:00
Aryan Kumar 6743f4b4b7 Revert "[new-model] Add PAN2 text-to-video and image-to-video" (#2025) 2026-10-27 18:03:59 -07:00
261 changed files with 20899 additions and 4715 deletions
@@ -0,0 +1,37 @@
---
date: 2026-11-02
experiment: FastVideo CI repair on main (Transformer and Unit lanes)
category: infrastructure
severity: important
---
# pytest 9 monkeypatch cannot restore torch flag proxies
## What Happened
After the CI image picked up pytest 9, the Transformer lane reported 22
teardown errors (`'cuBLASModule' object has no attribute 'allow_tf32'`) and
the Unit lane reported 28 setup errors in `test_lora_side_branch.py`
(`torch._dynamo.config ... has no attribute 'disable'`). The test bodies
still passed.
## Root Cause
pytest 9's `monkeypatch.setattr` reads the previous value from the target's
instance `__dict__`. `torch.backends.cuda.matmul` and `torch._dynamo.config`
serve their flags through `__getattr__`, so pytest records the value as
missing. Teardown then calls `delattr`: on cuBLAS that raises and leaves TF32
changed, and on the dynamo config it deletes the `disable` entry, so every
later `setattr` on it fails. pytest 8.4 restores both correctly.
## Fix / Workaround
Use the owner's own scoping instead of monkeypatch:
`torch._dynamo.config.patch(name=value)` for dynamo/inductor config, and the
`tf32_disabled` fixture in `fastvideo/tests/conftest.py` for the TF32 flags.
## Prevention
Do not `monkeypatch.setattr` attributes of `torch.backends.cuda.matmul`,
`torch._dynamo.config`, `torch._inductor.config` or other torch config
proxies. Module functions such as `torch.compiler.is_compiling` are safe.
+3 -2
View File
@@ -22,8 +22,9 @@ jobs:
trigger:
if: >-
inputs.pr_number > 0
|| (github.event.action == 'labeled' && github.event.label.name == 'ready')
|| github.event.action == 'synchronize'
|| (github.event.pull_request.base.ref == github.event.repository.default_branch
&& ((github.event.action == 'labeled' && github.event.label.name == 'ready')
|| github.event.action == 'synchronize'))
runs-on: ubuntu-latest
# Job-level concurrency: only this guarded job acquires the group, so an
# unrelated `labeled` event (which skips the job) cannot cancel an in-flight
+45 -3
View File
@@ -12,10 +12,52 @@ permissions:
issues: write
jobs:
publish-node:
name: Publish Custom Node to registry
# Any pyproject.toml edit triggers this workflow, but the registry rejects a
# version it already has ("The node version already exists"). Publish only
# when [project].version changed, as the PyPI workflow does.
check-version-change:
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'hao-ai-lab' }}
outputs:
changed: ${{ steps.check.outputs.changed }}
steps:
- name: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Compare [project].version with the version before this push
id: check
shell: python
env:
BEFORE_SHA: ${{ github.event.before }}
run: |
import os
import subprocess
import tomllib
def version(text: str) -> str | None:
return tomllib.loads(text).get("project", {}).get("version")
def git(*args: str) -> subprocess.CompletedProcess:
return subprocess.run(["git", *args], capture_output=True, text=True)
# A push can carry several commits; compare with the tip before the push, not HEAD~1.
base = os.environ.get("BEFORE_SHA", "").strip()
if not base or set(base) == {"0"} or git("fetch", "--no-tags", "--depth=1", "origin", base).returncode:
base = "HEAD~1"
new = version(open("pyproject.toml", encoding="utf-8").read())
previous = git("show", f"{base}:pyproject.toml")
old = version(previous.stdout) if previous.returncode == 0 else None
changed = new is not None and new != old
print(f"base: {base}, previous version: {old}, current version: {new}, changed: {changed}")
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
output.write(f"changed={str(changed).lower()}\n")
publish-node:
name: Publish Custom Node to registry
needs: check-version-change
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'hao-ai-lab' && (needs.check-version-change.outputs.changed == 'true' || github.event_name == 'workflow_dispatch') }}
steps:
- name: Check out code
uses: actions/checkout@v4
@@ -25,4 +67,4 @@ jobs:
uses: Comfy-Org/publish-node-action@v1
with:
## Add your own personal access token to your Github Repository secrets and reference it here.
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
@@ -49,7 +49,6 @@ export default function HardwarePicker({
value,
onChange,
gpuCount = 1,
purpose = "target",
reload,
}: {
backend: string;
@@ -59,7 +58,6 @@ export default function HardwarePicker({
value: HardwareSelection;
onChange: (selection: HardwareSelection) => void;
gpuCount?: number;
purpose?: "target" | "filter";
reload?: () => void;
}) {
const id = useId();
@@ -71,9 +69,9 @@ export default function HardwarePicker({
const [adding, setAdding] = useState(false);
const [draft, setDraft] = useState<Draft>(() => draftFrom(value.custom, backend));
const record = resolveHardware(value, catalog);
const unknown = !!value.id && value.id !== "custom" && !record;
const candidates =
purpose === "filter" ? catalog : catalog.filter((item) => item.backend === (backend === "mlx" ? "mlx" : "cuda"));
const unknown = status === "ready" && !!value.id && value.id !== "custom" && !record;
const loading = status === "loading" && !!value.id && value.id !== "custom";
const candidates = catalog.filter((item) => item.backend === (backend === "mlx" ? "mlx" : "cuda"));
const matches = searchHardware(candidates, query).slice(0, 20);
const draftMemory = amount(draft.memory);
const draftBudget = amount(draft.budget);
@@ -144,8 +142,7 @@ export default function HardwarePicker({
</span>
{record.usable_budget_gb ? (
<span className="hardware-selected-meta">
{value.id === "custom" ? "Your budget" : "Usable budget"}: {record.usable_budget_gb}{" "}
{record.memory_unit}
Usable memory: {record.usable_budget_gb} {record.memory_unit}
</span>
) : null}
{record.host_ram_gb && record.memory_type !== "unified" ? (
@@ -155,20 +152,24 @@ export default function HardwarePicker({
) : null}
</div>
<button type="button" className="hardware-clear" onClick={() => onChange({ id: "" })}>
Clear hardware {purpose}
Clear hardware
</button>
</div>
) : value.id === "custom" ? (
<p className="hardware-current" role="status">
Add this device's memory to finish selecting it.
</p>
) : loading ? (
<p className="hardware-current" role="status">
Loading hardware catalog…
</p>
) : unknown ? (
<p className="hardware-current" role="status">
Saved hardware target ({value.id}) is not in this catalog.
The saved device ({value.id}) is not in this catalog.
</p>
) : (
<p className="hardware-current" role="status">
No hardware {purpose} selected.
No hardware selected.
</p>
)}
{record ? (
@@ -184,7 +185,7 @@ export default function HardwarePicker({
<div className="hardware-picker-actions">
{!record && value.id ? (
<button type="button" className="hardware-clear" onClick={() => onChange({ id: "" })}>
Clear hardware {purpose}
Clear hardware
</button>
) : null}
<button
@@ -198,20 +199,12 @@ export default function HardwarePicker({
setAdding(false);
}}
>
{record
? "Change hardware"
: purpose === "filter"
? "Choose hardware"
: backend === "mlx"
? "+ Find or add a Mac"
: "+ Find or add a GPU"}
{record ? "Change hardware" : backend === "mlx" ? "+ Find or add a Mac" : "+ Find or add a GPU"}
</button>
</div>
{record || open ? (
<p className="hardware-picker-caption">
{purpose === "filter"
? "Filters by runtime. Memory fit is not estimated."
: "Single-GPU devices set GPU count to 1. Nothing else in the configuration changes."}
Sets the setup guide and memory budget. Choose checkpoint precision in the recipe settings.
</p>
) : null}
{open ? (
@@ -296,13 +289,13 @@ export default function HardwarePicker({
value={draft.memory_type}
onChange={(event) => setDraft({ ...draft, memory_type: event.target.value as MemoryType })}
>
<option value="dedicated">Dedicated GPU memory (VRAM)</option>
<option value="unified">Unified memory (CPU + GPU)</option>
<option value="system">System memory (host RAM)</option>
<option value="dedicated">GPU memory (VRAM)</option>
<option value="unified">Unified memory</option>
<option value="system">System memory (RAM)</option>
</select>
</label>
<label className="hardware-form-field">
<span>Nominal memory</span>
<span>Total memory</span>
<Input
type="number"
min="1"
@@ -323,7 +316,7 @@ export default function HardwarePicker({
</select>
</label>
<label className="hardware-form-field">
<span>Usable budget (optional)</span>
<span>Usable memory (optional)</span>
<Input
type="number"
min="1"
@@ -348,16 +341,14 @@ export default function HardwarePicker({
</label>
) : null}
</div>
{budgetTooLarge ? (
<p className="hardware-form-error">The usable budget cannot exceed the nominal memory.</p>
) : null}
{budgetTooLarge ? <p className="hardware-form-error">Usable memory cannot exceed total memory.</p> : null}
<p className="hardware-form-note">
Nominal memory is device capacity, not what FastVideo can use. A budget is your own limit and is not
verified. Unified memory is shared with the CPU; it is not dedicated VRAM.
Total memory is the device's capacity. Usable memory is the part FastVideo may use, such as a MIG slice
or a limit you set. Unified memory is shared with the CPU.
</p>
<div className="hardware-form-actions">
<button type="submit" disabled={!validDraft}>
Use this device
Save device
</button>
<button
type="button"
+65 -43
View File
@@ -1,59 +1,54 @@
import { useState } from "react";
import { useId, useState } from "react";
import type { MemoryAssessment } from "@/lib/capacity";
export type Validity = "valid" | "invalid" | "loading";
export type Fit = "fits" | "tight" | "exceeds" | "unsupported" | "unknown";
export type Evidence = "recorded" | "none" | "stale";
const validityLabel: Record<Validity, string> = {
valid: "Configuration valid",
valid: "Schema checks passed",
invalid: "Configuration invalid",
loading: "Checking configuration",
};
const fitLabel: Record<Fit, string> = {
fits: "Fits estimate",
tight: "Tight fit",
exceeds: "Likely exceeds memory",
unsupported: "Unsupported combination",
unknown: "Fit unknown",
exceeds: "Exceeds memory",
unsupported: "Unsupported",
unknown: "Memory not estimated",
};
const evidenceLabel: Record<Evidence, string> = {
recorded: "Run evidence available",
none: "No recorded run",
stale: "Run evidence does not match these settings",
};
/**
* Three separate facts: the recipe schema accepts the values, a memory estimate
* covers the configuration, and a recorded run matches it. No positive claim is
* made until the shared API supplies the corresponding fact.
* Two facts, each with its own evidence: the recipe schema accepts the values,
* and the analytical estimate covers the selected device. Neither is a promise
* that the recipe runs.
*/
export default function RecipeFacts({
validity,
fit = "unknown",
evidence = "none",
subject = "configuration",
detail,
memory,
validityText,
validityDetail,
detail,
retryMemory,
}: {
validity: Validity;
fit?: Fit;
evidence?: Evidence;
memory?: MemoryAssessment;
subject?: "configuration" | "example";
detail?: string;
validityText?: string;
validityDetail?: string;
detail?: string;
retryMemory?: () => void;
}) {
const [open, setOpen] = useState(false);
const id = `recipe-facts-${validity}-${fit}-${evidence}`;
const id = useId();
const fit: Fit = memory?.status ?? "unknown";
const estimated = fit === "fits" || fit === "tight" || fit === "exceeds";
const label = fitLabel[fit];
return (
<div className="recipe-facts">
<div className="recipe-fact-list" aria-label="Recipe facts">
<div className="recipe-fact-list" aria-label="Recipe checks">
<span data-state={validity}>
{validity === "valid" ? (validityText ?? `Valid ${subject}`) : validityLabel[validity]}
{validity === "valid" ? (validityText ?? validityLabel.valid) : validityLabel[validity]}
</span>
<span data-state={fit}>{fitLabel[fit]}</span>
<span data-state={evidence}>{evidenceLabel[evidence]}</span>
<span data-state={fit}>{label}</span>
</div>
<button
type="button"
@@ -64,31 +59,58 @@ export default function RecipeFacts({
>
Details
</button>
{memory?.reason && (
<p className="matrix-note" role="status">
{memory.reason}
</p>
)}
{memory?.basis === "checkpoint" && (
<div className="recipe-memory-breakdown">
<p>
Known checkpoint data: {memory.checkpointGiB?.toFixed(1)} GiB. Device budget:{" "}
{memory.capacityGiB?.toFixed(1)} GiB.
</p>
<dl>
{Object.entries(memory.parts ?? {}).map(([name, size]) => (
<div key={name}>
<dt>{name.replaceAll("_", " ")}</dt>
<dd>{size.toFixed(1)} GiB</dd>
</div>
))}
</dl>
</div>
)}
{retryMemory && (
<button type="button" onClick={retryMemory}>
Retry memory data
</button>
)}
{open && (
<div className="recipe-facts-detail" id={id}>
<p>
<strong>{validityLabel[validity]}.</strong>{" "}
{validityDetail ??
(validity === "valid"
? "Selected values pass the recipe schema and runtime topology checks."
? "These values pass the recipe schema and runtime topology checks. Model loading, selected components and GPU execution have not been verified."
: validity === "invalid"
? "Output is withheld until the highlighted fields are corrected."
? "Output is hidden until the highlighted fields are fixed."
: "Values are still being checked.")}
</p>
<p>
<strong>{fitLabel[fit]}.</strong>{" "}
{fit === "unknown"
? "No memory estimate covers this deployment, device, and request yet. This is not a fit claim."
: "The shared capacity API supplied this estimate; it is not a measured guarantee."}
</p>
<p>
<strong>{evidenceLabel[evidence]}.</strong>{" "}
{evidence === "none"
? "Successful runs on other checkpoints, settings, software revisions, or GPU layouts do not establish this configuration."
: evidence === "stale"
? "A recorded run exists, but edited settings fall outside what it covers."
: "A recorded run matches the deployment and settings shown."}
</p>
{!memory && (
<p>
<strong>{label}.</strong> FastVideo has no capacity model for this recipe, so it does not estimate memory
use here.
</p>
)}
{memory?.notes?.map((note) => (
<p key={note}>{note}</p>
))}
{estimated && (
<p>
<strong>{label}.</strong>{" "}
{`The ${memory?.basis === "measured" ? "recorded peak" : "analytical estimate"} needs ${memory?.requiredGiB?.toFixed(1)} GiB of ${memory?.capacityGiB?.toFixed(1)} GiB available. This applies only to the covered execution settings.`}
</p>
)}
{detail && <p>{detail}</p>}
</div>
)}
+6 -1
View File
@@ -4,12 +4,17 @@ description: Recipes for video, image, audio, and world generation.
mode: wide
---
Pick a model, choose your hardware, and copy the command. Every recipe starts from checked-in YAML, so the settings you see are the settings that run.
Before you pick a device, check [weights and quantization](weights/) for measured
checkpoint sizes and the FastH3 precision variants.
<a id="inference-cookbook"></a>
<a id="planned-families-heading"></a>
<a id="video-models-heading"></a>
<a id="image-models-heading"></a>
<a id="audio-models-heading"></a>
<a id="world-models-heading"></a>
<a id="planned-families-heading"></a>
<Cookbook />
@@ -1,7 +1,14 @@
---
title: FastH3 OmniRef
description: Configure FastH3 OmniRef reference-to-video serving on an RTX 5090, RTX PRO 6000, or DGX Spark.
title: FastH3 Omni v1
description: Configure FastH3 Omni v1 reference-to-video serving and copy inference or serving commands.
mode: frame
---
<Cookbook modelId="fasth3-omniref" />
## Apple silicon
The MLX path composes the BF16 Omni release with the NVFP4 text encoder, converts
the DiT to INT6, and runs the same image and audio references on Metal. Follow
[FastH3 Omni on MLX](/inference/fasth3_omniref_mlx).
For copyable local commands, open the [MLX recipe](/cookbook/minimax-h3/models/fasth3-omniref-mlx).
@@ -0,0 +1,7 @@
---
title: Open-H3-Regenerate-2K
description: Configure Open-H3-Regenerate-2K serving, which regenerates a 768P MiniMax-H3 draft at 2560x1440.
mode: frame
---
<Cookbook modelId="open-h3-regenerate-2k" />
-7
View File
@@ -1,7 +0,0 @@
---
title: PAN 2
description: Configure text-to-video and first-frame image-to-video generation with PAN 2.
mode: frame
---
<Cookbook modelId="pan2" />
+115
View File
@@ -0,0 +1,115 @@
---
title: Weights and quantization
description: Measured checkpoint sizes for every cookbook model, plus the FastH3 quantized variants and which one fits a given GPU.
mode: wide
---
Every number here is the size of the safetensors files in the model repository,
measured on 2026-10-27 with the Hugging Face tree API. Treat it as the weight
floor. A run also needs activations, VSA workspace, and decode buffers, so
weights that fit do not guarantee a run that fits.
The cookbook editor starts from each recipe's maintained deployment. Changing
a quantization control does not select one of the published exports below.
## Catalog weights
Sizes are GiB at the checkpoint's runtime precision. Repositories that store
F32 weights are converted to BF16 here, which is what the loader uses.
| Model | Weights | Transformer | Text encoder | VAE |
| ----------------------- | ------- | ----------- | ------------ | ---- |
| FastH3 8-Step V2 | 137.7 | 65.3 | 62.1 | 10.3 |
| FastH3 4-Step V1 | 137.7 | 65.3 | 62.1 | 10.3 |
| FastH3 Trim | 56.8 | 34.9 | 15.3 | 6.5 |
| FastH3 Omni v1 | 92.5 | 65.3 | 15.3 | 11.9 |
| Pika H3 Flex | 136.7 | 64.3 | 62.1 | 10.3 |
| FastWan2.1 1.3B | 13.6 | 2.8 | 10.6 | 0.3 |
| Wan2.1 T2V 14B | 37.4 | 26.6 | 10.6 | 0.3 |
| Wan2.1 I2V 14B | 41.9 | 30.5 | 10.6 | 0.3 |
| Wan2.2 I2V A14B | 58.8 | 53.2 | 5.3 | 0.3 |
| Hunyuan 1.5 I2V 480p | 32.2 | 15.5 | 13.6 | 2.3 |
| Hunyuan 1.5 T2V 480p | 24.9 | 15.5 | 7.0 | 2.4 |
| Kandinsky 5 Lite | 11.1 | 2.2 | 8.5 | 0.5 |
| Kandinsky 6 Pro | 41.9 | 32.5 | 8.0 | 1.4 |
| Kandinsky 6 Pro distill | 37.5 | 28.1 | 8.0 | 1.4 |
| LTX2 Distilled | 66.9 | 35.2 | 25.4 | 3.6 |
Pika H3 Flex's transformer is the 61.7 GiB H3 trunk plus its 2.6 GiB task adapter.
Ref2VA swaps in `transformer_ref` and the `ref2va` adapter, which are the same size.
The FastVideo organization publishes the same FastH3 model at several
precisions. The transformer shrinks with precision; the text encoder and VAE
only shrink when the repository ships them quantized too.
| Checkpoint | Variant | Weights | Transformer | Text encoder | VAE |
| ----------------- | -------------- | ------- | ----------- | ------------ | ---- |
| 8-Step V2 | BF16 | 137.7 | 65.3 | 62.1 | 10.3 |
| 8-Step V2 | FP8 | 69.2 | 47.4 | 15.3 | 6.5 |
| 8-Step V2 | NVFP4 | 114.5 | 49.8 | 62.1 | 2.5 |
| 8-Step V2 | NVFP4 Consumer | 58.7 | 36.9 | 15.3 | 6.5 |
| Trim | BF16 | 56.8 | 34.9 | 15.3 | 6.5 |
| Trim | FP8 | 41.7 | 19.9 | 15.3 | 6.5 |
| Trim | NVFP4 | 32.9 | 11.1 | 15.3 | 6.5 |
| Trim | MLX INT6 | 33.0 | 14.3 | 14.2 | 4.5 |
| 4-Step V1 | BF16 | 137.7 | 65.3 | 62.1 | 10.3 |
| 4-Step V1 | NVFP4 | 114.5 | 49.8 | 62.1 | 2.5 |
| Omni v1 | BF16 DiT | 65.3 | 65.3 | n/a | n/a |
| Omni v1 | FP8 | 67.2 | 47.4 | 15.3 | 4.5 |
| Omni v1 | NVFP4 QAD | 63.3 | 37.0 | 15.3 | 11.0 |
| NVFP4 VAE decoder | quality | 5.8 | n/a | n/a | 5.8 |
| NVFP4 VAE decoder | fast | 1.3 | n/a | n/a | 1.3 |
The NVFP4 checkpoint keeps the BF16 text encoder, which is why it is larger
than FP8. The Consumer variant pairs the smaller NVFP4 transformer with the
FP8 text encoder and VAE, which brings the listed weights to 58.7 GiB. These
export sizes do not establish device fit. Loading, offload, quantization
profiles, and temporary buffers affect runtime residency. The decoder-only
rows exclude the VAE encoder and can contain master weights that the loader
packs before inference.
## Pick by GPU
| GPU family | Examples | Use | Why |
| ---------------- | ----------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------- |
| Blackwell | RTX 5090, RTX PRO 6000, B200, GB200 | NVFP4 or FP8 with a matching maintained profile | Native FP4 tensor cores cut the transformer to about half of BF16 |
| Hopper | H100, H200 | FP8 | FP8 tensor cores; the NVFP4 path requires Blackwell |
| Ada | RTX 4090, L40S | FP8 | FP8 works, NVFP4 does not |
| Ampere and older | A100, RTX 3090 | BF16 | No FP8 or FP4 hardware |
| Apple silicon | M-series | MLX INT6 or INT8 | The MLX conversions are INT6 and INT8, not FP4 |
| DGX Spark | GB10 | NVFP4 with sequential load | Unified memory, so the host RAM counts toward the budget |
## What the cookbook does now
FastH3 pages show known checkpoint sizes beside the selected device budget.
These numbers are not a runtime fit check. Offload moves weights between phases;
sequence parallelism does not necessarily shard weights; unified memory is one
shared pool. The page does not turn those file sizes into a residency claim.
When an encoder, decoder or transformer precision changes, the page keeps sizes
for unchanged components and names the missing data. It never substitutes the
size of a different published export. A complete peak estimate still requires
loading, placement, activations and workspace models for the selected path.
The transformer quantization control writes
`generator.engine.quantization.transformer_quant`, so the copied YAML names the
precision. Encoder and decoder choices use component paths and stay visible in
the YAML too.
## Automatic selection
The editor does not pick a precision from the device yet. When it does, the
rule is:
1. Read the device architecture and memory from the hardware catalog.
2. Prefer the smallest variant the architecture accelerates: NVFP4 on
Blackwell, FP8 on Hopper and Ada, INT6 on Apple, BF16 everywhere else.
3. Keep the BF16 text encoder when the quantized checkpoint ships one, and
switch the decoder to NVFP4 only on Blackwell.
4. Expose the choice as a deployment setting, not a hidden default, so the
copied command still shows which checkpoint runs.
That needs one deployment per variant in
[`docs/cookbook/recipes`](https://github.com/hao-ai-lab/FastVideo/tree/main/docs/cookbook/recipes)
and a match rule in the editor. Until then, select the precision by hand and
read the weights fact.
+149 -75
View File
@@ -2,7 +2,7 @@ import { useEffect, useId, useRef, useState, type ReactNode } from "react";
import { MotionConfig } from "motion/react";
import { ArrowUpRight } from "lucide-react";
import { Tabs } from "radix-ui";
import { modelDirectory, recipePages, sitePath, type DirectoryModel } from "@/lib/cookbook-pages";
import { modelDirectory, recipePages, recentlyAdded, sitePath, type DirectoryModel } from "@/lib/cookbook-pages";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -19,6 +19,7 @@ import {
CardCurtainRevealTitle,
} from "@/components/ui/card-curtain-reveal";
import RecipeFacts from "@/components/cookbook/RecipeFacts";
import { assessRecipe, assessWeightsRecipe, useCapacityModel, weightsFor } from "@/lib/capacity";
import { configAndLaunch, inferenceOutput, sdkCommand } from "@/lib/cookbook-commands";
import {
gpuCountFrom,
@@ -130,7 +131,7 @@ function FieldControl({
</div>
{choices ? (
<div role="group" aria-label={label} className="option-chips">
{value === undefined && (
{value === undefined && !choices.some((choice) => choice === null) && (
<button type="button" aria-pressed={!changed} onClick={() => update(field.path, undefined, field)}>
Default
</button>
@@ -139,21 +140,12 @@ function FieldControl({
<button
key={JSON.stringify(choice)}
type="button"
aria-pressed={value === choice}
aria-pressed={value === choice || (value === undefined && choice === null)}
onClick={() => update(field.path, JSON.stringify(choice), field)}
>
{choiceLabel(field.path, choice)}
{choice === null ? (CHOICE_LABELS[field.path]?.null ?? "Default") : choiceLabel(field.path, choice)}
</button>
))}
{nullable && !choices.includes(null) && (
<button
type="button"
aria-pressed={value === null}
onClick={() => update(field.path, "null", { ...field, schema: { ...field.schema, enum: [null] } })}
>
null
</button>
)}
</div>
) : kind.includes("object") || kind.includes("array") ? (
<Textarea
@@ -209,7 +201,7 @@ function FieldControl({
Use null
</button>
)}
<p id={`${id}-hint`} className="sr-only">
<p id={`${id}-hint`} className="matrix-note">
{field.schema.description}
{value === undefined ? " Inherited from the runtime." : ""}
</p>
@@ -279,6 +271,18 @@ function Editor({
errors[path] = "Enter a valid number or JSON value for this field.";
}
}
const decoderPath = "generator.pipeline.experimental.h3_vae_nvfp4_decoder";
const backendPath = "generator.pipeline.experimental.video_decode_backend";
if (path === backendPath && next[path] === "taeh3" && fields.some((field) => field.path === decoderPath)) {
next[decoderPath] = null;
delete errors[decoderPath];
setNotice("TAEH3 selected. The NVFP4 decoder override was cleared.");
}
if (path === decoderPath && next[path] && fields.some((field) => field.path === backendPath)) {
next[backendPath] = "h3-vae";
delete errors[backendPath];
setNotice("H3 VAE selected for the NVFP4 decoder.");
}
if (path === GPU_COUNT_PATH && typeof next[path] === "number") {
const fitted = fitParallelism(fields, next, next[path] as number);
if (fitted !== next) {
@@ -313,6 +317,43 @@ function Editor({
const hardwareRecord = resolveHardware(hardware, hardwareCatalog.records);
const gpuCount = gpuCountFrom(result?.config);
const model = modelDirectory.find((model) => model.id === catalog.id.split("/")[0])!;
const capacityModel = useCapacityModel(model.id);
const weights = weightsFor(model.id);
const memory =
result && !failure
? hardwareRecord
? capacityModel.status === "error"
? { status: "unknown" as const, reason: `Memory data could not load. ${capacityModel.error}` }
: capacityModel.model
? assessRecipe({
model: capacityModel.model,
example: { workload: catalog.workload, env: catalog.env },
hardware: hardwareRecord,
config: result.config,
budgetGb: hardwareRecord.usable_budget_gb,
hostGb: hardwareRecord.host_ram_gb,
})
: catalog.runtime.backend === "mlx" && capacityModel.status !== "loading"
? {
status: "unknown" as const,
reason: "MLX checkpoints use a different precision and load pattern; memory is not estimated.",
}
: capacityModel.status !== "loading" && weights
? assessWeightsRecipe({
weights,
config: result.config,
hardware: hardwareRecord,
budgetGb: hardwareRecord.usable_budget_gb,
})
: {
status: "unknown" as const,
reason:
capacityModel.status === "loading"
? "Loading the memory estimate."
: "No capacity model ships for this checkpoint.",
}
: { status: "unknown" as const, reason: "Select a device to estimate memory for this recipe." }
: undefined;
const renderField = (field: Field) => (
<FieldControl
key={`${reset}-${field.path}`}
@@ -341,12 +382,16 @@ function Editor({
window.addEventListener("popstate", restore);
return () => window.removeEventListener("popstate", restore);
}, []);
const modes = mlx || catalog.runtime.interface === "websocket" ? ["serving"] : ["sdk", "inference", "serving"];
const videoInput = catalog.workload === "v2v";
const modes =
mlx || catalog.runtime.interface === "websocket" || videoInput ? ["serving"] : ["sdk", "inference", "serving"];
const modeNote = mlx
? "Apple Silicon runs through the native MLX server in this recipe. There is no CUDA CLI or Python SDK launch for this deployment."
: catalog.runtime.interface === "websocket"
? "This deployment uses the native WebSocket server. Only serving is recorded for it."
: "";
: videoInput
? "This deployment needs a draft video. Send it to the server as video_reference; generated CLI and Python SDK configs cannot carry it yet."
: "";
const advancedGroups = groups.filter(
(group) => group === "server" || group.includes("parallelism") || group.startsWith("streaming"),
);
@@ -560,7 +605,11 @@ function Editor({
</a>
</div>
</div>
<RecipeFacts validity={failure ? "invalid" : result ? "valid" : "loading"} fit="unknown" evidence="none" />
<RecipeFacts
validity={failure ? "invalid" : result ? "valid" : "loading"}
memory={memory}
retryMemory={capacityModel.status === "error" ? capacityModel.retry : undefined}
/>
</MatrixRow>
{catalog.workload === "i2v" && runMode !== "serving" && (
<MatrixRow title="Inputs">
@@ -581,7 +630,7 @@ function Editor({
}}
/>
<p id={`${sourceImageId}-hint`} className="matrix-note">
Path to an image on the machine running FastVideo. Included as request.inputs.image_path.
Local path to the first frame. FastVideo reads it on the machine that runs the command.
</p>
</div>
</MatrixRow>
@@ -671,6 +720,11 @@ function MatrixRow({ title, children }: { title: string; children: ReactNode })
);
}
function deploymentFromUrl() {
const requested = new URL(window.location.href).searchParams.get("deployment");
return requested === "fasth3-omniref/cuda-rest" ? "fasth3-omniref/rtx5090-rest" : requested;
}
export default function Cookbook({ modelId }: { modelId?: string }) {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
@@ -693,7 +747,7 @@ export default function Cookbook({ modelId }: { modelId?: string }) {
throw new Error("No cookbook deployments are available. Check the catalog and retry.");
}
setIndex(data);
const requested = new URL(window.location.href).searchParams.get("deployment");
const requested = deploymentFromUrl();
const requestedModel = data.models.find((model) =>
model.deployments.some((deployment) => deployment.id === requested),
);
@@ -739,7 +793,7 @@ export default function Cookbook({ modelId }: { modelId?: string }) {
}
useEffect(() => {
const restore = () => {
const requested = new URL(window.location.href).searchParams.get("deployment");
const requested = deploymentFromUrl();
const available = index?.models.find((item) => item.id === modelId)?.deployments ?? [];
const next = available.find((deployment) => deployment.id === requested)?.id ?? available[0]?.id;
if (!next) return;
@@ -806,11 +860,8 @@ export default function Cookbook({ modelId }: { modelId?: string }) {
}
const GPU_COUNT_PATH = "generator.engine.num_gpus";
const SINGLE_GPU_FORM_FACTOR = /desktop|laptop|workstation|system|soc/;
/** Desktop and workstation cards, DGX Spark, and Macs are one-GPU machines; datacenter parts keep the recipe default. */
function singleGpuDevice(record: { form_factor?: string }) {
return SINGLE_GPU_FORM_FACTOR.test(record.form_factor ?? "");
function singleGpuDevice(record: { form_factor?: string; kind?: string }) {
return record.kind === "system" || record.form_factor === "soc" || record.form_factor === "laptop";
}
/** Returns new edits where every fixed parallel size that no longer divides `gpus` becomes automatic. */
@@ -833,13 +884,19 @@ function deploymentLabel(deployment: DeploymentChoice, deployments: DeploymentCh
}
/** Mirrors the editor: MLX and WebSocket deployments offer serving only. */
function modelModes(index: Index | undefined, model: DirectoryModel): string[] | undefined {
const modes = model.configurable ? configurableModes(index, model.id) : exampleModes(model);
const hasMlx = model.recipes.some((recipe) => recipe.gpu_types.includes("Apple Silicon"));
if (hasMlx && modes && !modes.some((mode) => /mlx/i.test(mode))) return [...modes, "MLX"];
return modes;
}
function configurableModes(index: Index | undefined, modelId: string): string[] | undefined {
const deployments = index?.models.find((model) => model.id === modelId)?.deployments;
if (!deployments?.length) return undefined;
const servingOnly = (runtime: string) => runtime.includes("mlx") || runtime.includes("streaming");
return deployments.some((deployment) => !servingOnly(deployment.runtime))
? ["Python SDK", "CLI", "Serving"]
: ["Serving"];
const servingOnly = (deployment: { runtime: string; workload: string }) =>
deployment.runtime.includes("mlx") || deployment.runtime.includes("streaming") || deployment.workload === "v2v";
return deployments.some((deployment) => !servingOnly(deployment)) ? ["Python SDK", "CLI", "Serving"] : ["Serving"];
}
/** Mirrors the source recipe page: Python SDK and CLI always, serving when a recipe declares it. */
@@ -847,8 +904,20 @@ function exampleModes(model: DirectoryModel): string[] {
return ["Python SDK", "CLI", ...(model.recipes.some((recipe) => recipe.serving) ? ["Serving"] : [])];
}
const FASTH3_SECTION = new Set(["fasth3-omniref", "fasth3-8step", "fasth3-4step", "fasth3-trim"]);
/** Rest state shows identity only; hover or keyboard focus opens a curtain over the details. */
function RecipeCard({ model, href, modes }: { model: DirectoryModel; href: string; modes?: string[] }) {
function RecipeCard({
model,
href,
modes,
badge,
}: {
model: DirectoryModel;
href: string;
modes?: string[];
badge?: string;
}) {
const [focused, setFocused] = useState(false);
const details = useRef<HTMLDivElement>(null);
const [restOffset, setRestOffset] = useState(72);
@@ -869,6 +938,7 @@ function RecipeCard({ model, href, modes }: { model: DirectoryModel; href: strin
<img src={sitePath(`/assets/logos/${model.logo}.webp`)} alt="" width="40" height="40" />
<span className="recipe-card-kicker">{model.provider}</span>
{model.featured ? <span className="recipe-featured">Featured</span> : null}
{badge ? <span className="recipe-featured recipe-new">{badge}</span> : null}
</div>
<CardCurtainRevealTitle
as={model.configurable ? "h3" : "h4"}
@@ -932,6 +1002,7 @@ function RecipeDirectory({ index, error, retry }: { index?: Index; error: string
};
const visible = modelDirectory.filter(
(model) =>
!model.hidden &&
(category === "All models" || model.category === category) &&
(family === "All families" || model.family === family) &&
(task === "All tasks" || model.task === task) &&
@@ -947,9 +1018,10 @@ function RecipeDirectory({ index, error, retry }: { index?: Index; error: string
setRuntime("all");
setTask("All tasks");
}
const configurable = visible.filter((model) => model.configurable);
const examples = visible.filter((model) => !model.configurable);
const exampleFamilies = [...new Set(examples.map((model) => model.family))];
const fastH3 = visible.filter((model) => FASTH3_SECTION.has(model.id));
const recent = visible.filter((model) => !FASTH3_SECTION.has(model.id) && recentlyAdded(model.model));
const rest = visible.filter((model) => !FASTH3_SECTION.has(model.id) && !recentlyAdded(model.model));
const families = [...new Set(rest.map((model) => model.family))];
const filtered =
!!query.trim() ||
category !== "All models" ||
@@ -957,7 +1029,7 @@ function RecipeDirectory({ index, error, retry }: { index?: Index; error: string
runtime !== "all" ||
task !== "All tasks";
const categoryCount = (value: string) =>
modelDirectory.filter((model) => value === "All models" || model.category === value).length;
modelDirectory.filter((model) => !model.hidden && (value === "All models" || model.category === value)).length;
return (
<MotionConfig reducedMotion="user">
<div className="cookbook not-prose recipe-directory">
@@ -991,7 +1063,6 @@ function RecipeDirectory({ index, error, retry }: { index?: Index; error: string
<div className="recipe-directory-subbar">
<p role="status">
{visible.length} {visible.length === 1 ? "model" : "models"}
{configurable.length ? ` · ${configurable.length} configurable` : ""}
{runtime !== "all" ? ` · ${runtime === "mlx" ? "MLX" : "CUDA"} only` : ""}
{task !== "All tasks" ? ` · ${task}` : ""}
{filtered && visible.length ? (
@@ -1043,59 +1114,62 @@ function RecipeDirectory({ index, error, retry }: { index?: Index; error: string
</Button>
</div>
) : null}
{configurable.length ? (
<section className="recipe-section" aria-labelledby="recipe-configurable-heading">
{fastH3.length ? (
<section className="recipe-section" aria-labelledby="recipe-fasth3-heading">
<header className="recipe-section-head">
<h2 id="recipe-configurable-heading">Configurable recipes</h2>
<p>Edit settings, then copy one config for the Python SDK, the CLI, or an OpenAI-compatible server.</p>
<h2 id="recipe-fasth3-heading">FastVideo FastH3</h2>
</header>
<div className="recipe-card-grid recipe-card-grid-configurable">
{configurable.map((model) => (
{fastH3.map((model) => (
<RecipeCard key={model.id} model={model} href={sitePath(model.path)} modes={modelModes(index, model)} />
))}
</div>
</section>
) : null}
{recent.length ? (
<section className="recipe-section" aria-labelledby="recipe-new-heading">
<header className="recipe-section-head">
<h2 id="recipe-new-heading">New</h2>
</header>
<div className="recipe-card-grid recipe-card-grid-configurable">
{recent.map((model) => (
<RecipeCard
key={model.id}
model={model}
href={sitePath(model.path)}
modes={configurableModes(index, model.id)}
modes={modelModes(index, model)}
badge="New"
/>
))}
</div>
</section>
) : null}
{examples.length ? (
<section className="recipe-section" aria-labelledby="recipe-examples-heading">
<header className="recipe-section-head">
<h2 id="recipe-examples-heading">Example recipes</h2>
<p>
Maintained example scripts, grouped by model family. Adjust their documented settings and run them as
written.
</p>
</header>
{exampleFamilies.map((name) => {
const members = examples.filter((model) => model.family === name);
return (
<div className="recipe-family-group" key={name}>
<div className="recipe-family-head">
<h3>{name}</h3>
<span>
{members.length} {members.length === 1 ? "recipe" : "recipes"}
</span>
<a href={sitePath(members[0].familyPath)}>Family guide →</a>
</div>
<div className="recipe-card-grid">
{members.map((model) => (
<RecipeCard
key={model.id}
model={model}
href={sitePath(model.path)}
modes={exampleModes(model)}
/>
))}
</div>
{families.map((name) => {
const members = rest.filter((model) => model.family === name);
return (
<section className="recipe-section" key={name} aria-label={name}>
<div className="recipe-family-group">
<div className="recipe-family-head">
<h3>{name}</h3>
<span>
{members.length} {members.length === 1 ? "recipe" : "recipes"}
</span>
<a href={sitePath(members[0].familyPath)}>Family guide →</a>
</div>
);
})}
</section>
) : null}
<div className="recipe-card-grid">
{members.map((model) => (
<RecipeCard
key={model.id}
model={model}
href={sitePath(model.path)}
modes={modelModes(index, model)}
/>
))}
</div>
</div>
</section>
);
})}
{!visible.length ? (
<div className="recipe-empty">
<h2>No matching recipes</h2>
+3 -3
View File
@@ -14,7 +14,7 @@ export default function FastModels() {
models: group.models.filter(
(model) =>
(family === "All models" || family === group.name) &&
`${group.name} ${group.summary} ${model.id}`.toLowerCase().includes(query.trim().toLowerCase()),
`${group.name} ${model.id}`.toLowerCase().includes(query.trim().toLowerCase()),
),
}))
.filter((group) => group.models.length);
@@ -48,7 +48,7 @@ export default function FastModels() {
<header className="model-catalog-heading">
<div>
<h2 id="checkpoint-catalog">Browse checkpoints</h2>
<p>Find the export your runtime needs.</p>
<p>Pick the checkpoint that matches your runtime.</p>
</div>
<a href="https://huggingface.co/collections/FastVideo/fastvideo-fasth3">
Earlier FastH3 releases <span aria-hidden="true">↗</span>
@@ -67,7 +67,7 @@ export default function FastModels() {
<input
id={id}
type="search"
placeholder="Name, precision, or runtime…"
placeholder="Name, family, or precision…"
value={query}
onChange={(event) => setQuery(event.target.value)}
/>
+5 -4
View File
@@ -4,17 +4,18 @@ import { modelDirectory, sitePath } from "@/lib/cookbook-pages";
export default function RecipeFamily({ family }: { family: string }) {
const models = modelDirectory.filter(
(model) =>
model.recipes.some((recipe) => recipe.family === family) ||
(family === "minimax_h3" && model.family === "FastH3"),
!model.hidden &&
(model.recipes.some((recipe) => recipe.family === family) ||
(family === "minimax_h3" && (model.family === "FastH3" || model.familyPath === "/cookbook/minimax-h3"))),
);
return (
<div className="recipe-directory not-prose">
<a className="recipe-back" href={sitePath("/cookbook")}>
← All models
← All recipes
</a>
<div className="recipe-card-grid">
{models.map((model) => (
<a className="recipe-card" key={model.id} href={sitePath(model.path)}>
<a className="recipe-card recipe-card-static" key={model.id} href={sitePath(model.path)}>
<div className="recipe-card-top">
<img src={sitePath(`/assets/logos/${model.logo}.webp`)} alt="" width="40" height="40" />
<span className="recipe-card-kicker">{model.provider}</span>
+1 -3
View File
@@ -484,9 +484,7 @@ export default function SourceRecipe({ model }: { model: DirectoryModel }) {
<RecipeFacts
validity={failure ? "invalid" : example ? "valid" : "loading"}
validityText={yamlSource ? "YAML parses" : "Example settings applied"}
validityDetail="This page checks that the YAML parses and that edited numeric fields are finite and supported by the example. It does not run the full recipe schema or runtime topology validation."
fit="unknown"
evidence="none"
validityDetail="Checks that the YAML parses and that edited numbers are valid. It does not run the recipe."
subject={yamlSource ? "configuration" : "example"}
/>
</div>
+53
View File
@@ -0,0 +1,53 @@
import { useEffect, useState } from "react";
// @ts-expect-error The shared ES module has no TypeScript declarations.
import { loadCapacityIndex, loadCapacityModel } from "../../docs/js/cookbook-capacity.mjs";
export { assessRecipe, assessWeightsRecipe, weightsFor } from "./memory-assessment";
export type { MemoryAssessment } from "./memory-assessment";
interface CapacityModel {
id: string;
name?: string;
examples?: { deployment_id?: string }[];
reference_runs?: unknown[];
[key: string]: unknown;
}
interface CapacityState {
status: "loading" | "ready" | "unavailable" | "error";
model?: CapacityModel;
error?: string;
}
const CAPACITY_INDEX = `${import.meta.env.BASE_URL.replace(/\/$/, "")}/assets/cookbook-config/capacity/index.json`;
/**
* Load the capacity model for one cookbook entry. Most recipes do not have one;
* the hook stays `unavailable` and the UI says so instead of guessing.
*/
export function useCapacityModel(modelId: string | undefined): CapacityState & { retry: () => void } {
const [state, setState] = useState<CapacityState>({ status: "loading" });
const [attempt, setAttempt] = useState(0);
useEffect(() => {
if (!modelId) {
setState({ status: "unavailable" });
return;
}
const controller = new AbortController();
setState({ status: "loading" });
loadCapacityIndex(new URL(CAPACITY_INDEX, window.location.origin).href, { signal: controller.signal })
.then(async (index: { models: { id: string }[] }) => {
if (!index.models.some((model) => model.id === modelId)) {
if (!controller.signal.aborted) setState({ status: "unavailable" });
return;
}
const model = (await loadCapacityModel(index, modelId, { signal: controller.signal })) as CapacityModel;
if (!controller.signal.aborted) setState({ status: "ready", model });
})
.catch((error: unknown) => {
if (!controller.signal.aborted)
setState({ status: "error", error: error instanceof Error ? error.message : "Unable to load memory data." });
});
return () => controller.abort();
}, [modelId, attempt]);
return { ...state, retry: () => setAttempt((value) => value + 1) };
}
+98 -76
View File
@@ -2,6 +2,15 @@ import sourceCatalog from "../../docs/assets/cookbook-recipes.json" with { type:
/** Presentation metadata only. Deployments and editable fields come from the cookbook backend. */
export const recipePages = [
{
id: "fasth3-preview",
family: "MiniMax H3",
title: "FastH3 Preview V0.2",
mark: "H3",
path: "/cookbook/minimax-h3/fasth3-preview",
description: "The few-step FastH3 preview checkpoint on CUDA or native Apple Silicon MLX.",
tags: ["Text to video", "Audio", "4-step"],
},
{
id: "fasth3-8step",
family: "MiniMax H3",
@@ -20,15 +29,6 @@ export const recipePages = [
description: "The smaller FastH3 model for eight-step video generation with synchronized audio.",
tags: ["Text to video", "Audio", "NVFP4", "8-step"],
},
{
id: "fasth3-preview",
family: "MiniMax H3",
title: "FastH3 Preview V0.2",
mark: "H3",
path: "/cookbook/minimax-h3/fasth3-preview",
description: "The few-step FastH3 preview checkpoint on CUDA or native Apple Silicon MLX.",
tags: ["Text to video", "Audio", "4-step"],
},
{
id: "fasth3-4step",
family: "MiniMax H3",
@@ -41,13 +41,22 @@ export const recipePages = [
{
id: "fasth3-omniref",
family: "MiniMax H3",
title: "FastH3 OmniRef",
title: "FastH3 Omni v1",
mark: "H3",
path: "/cookbook/minimax-h3/fasth3-omniref",
description: "Video and audio from a prompt plus ordered image, video, and audio references.",
tags: ["Reference to video", "Audio", "NVFP4"],
featured: true,
},
{
id: "open-h3-regenerate-2k",
family: "MiniMax H3",
title: "Open-H3-Regenerate-2K",
mark: "H3",
path: "/cookbook/minimax-h3/open-h3-regenerate-2k",
description: "Regenerate a 768P MiniMax-H3 draft at 2560x1440 with a three-step refiner.",
tags: ["Video upscale", "2K", "3-step"],
},
{
id: "fastwan21",
family: "Wan",
@@ -66,15 +75,6 @@ export const recipePages = [
description: "Turn a reference image into video with Wan2.1.",
tags: ["Image to video", "14B", "480p"],
},
{
id: "pan2",
family: "PAN 2",
title: "PAN 2",
mark: "PAN",
path: "/cookbook/pan2/pan2",
description: "Text-to-video and first-frame image-to-video from the 29B PAN 2 model.",
tags: ["Text or image to video", "29B"],
},
{
id: "ltx2-distilled",
family: "LTX",
@@ -114,12 +114,12 @@ export interface SourceRecipe {
export const sourceRecipes = sourceCatalog.recipes as SourceRecipe[];
const familyInfo: Record<string, { title: string; path: string; logo: string; category: string }> = {
minimax_h3: { title: "MiniMax H3", path: "minimax-h3", logo: "minimax", category: "Video" },
pika_h3_flex: { title: "MiniMax H3", path: "minimax-h3", logo: "pika", category: "Video" },
wan: { title: "Wan", path: "wan", logo: "wan-ai", category: "Video" },
ltx2: { title: "LTX", path: "ltx", logo: "ltx", category: "Video" },
hunyuan: { title: "Hunyuan", path: "hunyuan", logo: "tencent-hunyuan", category: "Video" },
kandinsky5: { title: "Kandinsky 5", path: "kandinsky5", logo: "kandinsky", category: "Video" },
kandinsky6: { title: "Kandinsky 6", path: "kandinsky6", logo: "kandinsky", category: "Video" },
pan2: { title: "PAN 2", path: "pan2", logo: "ifm", category: "Video" },
flux: { title: "FLUX", path: "flux", logo: "black-forest-labs", category: "Image" },
qwen_image21: { title: "Qwen-Image-2.1", path: "qwen-image21", logo: "qwen", category: "Image" },
sd35: { title: "Stable Diffusion", path: "stable-diffusion", logo: "stabilityai", category: "Image" },
@@ -134,11 +134,21 @@ const backendModels: Record<string, string> = {
"FastVideo/FastVideo-Minimax-FastH3-Preview-v0.2": "fasth3-preview",
"FastVideo/FastVideo-FastH3-4-step-Preview-v1-VSA-DataFree": "fasth3-4step",
"FastVideo/FastH3-OmniRef-NVFP4-QAD": "fasth3-omniref",
"Efficient-Large-Model/Open-H3-Regenerate-2K": "open-h3-regenerate-2k",
"FastVideo/FastWan2.1-T2V-1.3B-Diffusers": "fastwan21",
"Wan-AI/Wan2.1-I2V-14B-480P-Diffusers": "wan21-i2v",
"FastVideo/LTX2-Distilled-Diffusers": "ltx2-distilled",
"IFM/PAN2": "pan2",
};
const hiddenBackends = new Set(["fasth3-preview"]);
const modelAddedOn: Record<string, string> = {
"Qwen/Qwen-Image-2.1": "2026-10-25",
};
export function recentlyAdded(model: string) {
const added = modelAddedOn[model];
return !!added && Date.now() - Date.parse(added) < 30 * 86_400_000;
}
const TASK_GROUPS: Array<[RegExp, string]> = [
[/world/, "Text to world"],
[/video\/text to audio|video.*to audio/, "Video to audio"],
@@ -149,7 +159,7 @@ const TASK_GROUPS: Array<[RegExp, string]> = [
[/first\/last frame to video/, "Image to video"],
[/text or image to video/, "Text/image to video"],
[/image to video/, "Image to video"],
[/super-resolution/, "Video upscale"],
[/super-resolution|upscale/, "Video upscale"],
[/video/, "Text to video"],
];
/** A small discovery vocabulary derived from the recipe task, not a capability claim. */
@@ -158,58 +168,70 @@ export function taskLabel(value: string): string {
return TASK_GROUPS.find(([pattern]) => pattern.test(normalized))?.[1] ?? "Text to video";
}
export const modelDirectory = [...new Set(sourceRecipes.map((recipe) => recipe.model))]
.map((model) => {
const recipes = sourceRecipes.filter((recipe) => recipe.model === model);
const first = recipes[0];
const family = familyInfo[first.family];
const backend = recipePages.find((page) => page.id === backendModels[model]);
const ownModel = model.startsWith("FastVideo/Fast");
const logo = ownModel ? "fastvideo" : family.logo;
const providers: Record<string, string> = {
fastvideo: "FastVideo",
minimax: "MiniMax",
"wan-ai": "Wan AI",
ltx: "Lightricks",
"tencent-hunyuan": "Tencent",
kandinsky: "Kandinsky Lab",
"black-forest-labs": "Black Forest Labs",
qwen: "Qwen",
ifm: "IFM",
stabilityai: "Stability AI",
nvidia: "NVIDIA",
};
return {
id: backend?.id ?? first.id,
model,
recipes,
title:
backend?.title ??
(model.includes("FastH3-4-step")
? "FastH3 4-Step V1"
: model.includes("FastH3-Preview-v0.2")
? "FastH3 Preview V0.2"
: model === "MiniMaxAI/MiniMax-H3"
? "MiniMax H3"
: first.label.split(" on ")[0]),
family: backend?.family ?? family.title,
familyPath: `/cookbook/${family.path}`,
path: backend?.path ?? `/cookbook/${family.path}/models/${first.id}`,
logo,
provider: providers[logo],
category: family.category,
description: backend?.description ?? first.summary,
tags: backend?.tags ?? [...new Set(recipes.map((recipe) => recipe.task))],
task: taskLabel(backend?.tags[0] ?? first.task),
platforms: [...new Set(recipes.flatMap((recipe) => recipe.gpu_types))],
configurable: !!backend,
featured: backend?.featured ?? false,
};
})
.sort(
(a, b) =>
Number(b.featured) - Number(a.featured) ||
Number(b.family === "MiniMax H3") - Number(a.family === "MiniMax H3") ||
Number(b.configurable) - Number(a.configurable),
);
const mappedModels = [...new Set(sourceRecipes.map((recipe) => recipe.model))].map((model) => {
const recipes = sourceRecipes.filter((recipe) => recipe.model === model);
const first = recipes[0];
const family = familyInfo[first.family];
const backend = recipePages.find((page) => page.id === backendModels[model]);
const ownModel = model.startsWith("FastVideo/Fast");
const logo = ownModel ? "fastvideo" : family.logo;
const providers: Record<string, string> = {
fastvideo: "FastVideo",
minimax: "MiniMax",
pika: "Pika",
"wan-ai": "Wan AI",
ltx: "Lightricks",
"tencent-hunyuan": "Tencent",
kandinsky: "Kandinsky Lab",
"black-forest-labs": "Black Forest Labs",
qwen: "Qwen",
stabilityai: "Stability AI",
nvidia: "NVIDIA",
};
return {
id: backend?.id ?? first.id,
model,
recipes,
title:
backend?.title ??
(model.includes("FastH3-4-step")
? "FastH3 4-Step V1"
: model.includes("FastH3-Preview-v0.2")
? "FastH3 Preview V0.2"
: model === "MiniMaxAI/MiniMax-H3"
? "MiniMax H3"
: first.label.split(" on ")[0]),
family: backend?.family ?? family.title,
familyPath: `/cookbook/${family.path}`,
path: backend?.path ?? `/cookbook/${family.path}/models/${first.id}`,
logo,
provider: providers[logo],
category: family.category,
description: backend?.description ?? first.summary,
tags: backend?.tags ?? [...new Set(recipes.map((recipe) => recipe.task))],
task: taskLabel(backend?.tags[0] ?? first.task),
platforms: [...new Set(recipes.flatMap((recipe) => recipe.gpu_types))],
configurable: !!backend,
hidden: hiddenBackends.has(backend?.id ?? ""),
featured: backend?.featured ?? false,
};
});
const byPage = new Map<string, (typeof mappedModels)[number]>();
for (const entry of mappedModels) {
const existing = byPage.get(entry.id);
if (!existing) {
byPage.set(entry.id, entry);
continue;
}
existing.recipes.push(...entry.recipes.filter((recipe) => !existing.recipes.some((seen) => seen.id === recipe.id)));
existing.platforms = [...new Set([...existing.platforms, ...entry.platforms])];
}
export const modelDirectory = [...byPage.values()].sort(
(a, b) =>
Number(b.featured) - Number(a.featured) ||
Number(b.family === "MiniMax H3") - Number(a.family === "MiniMax H3") ||
Number(b.configurable) - Number(a.configurable),
);
export type DirectoryModel = (typeof modelDirectory)[number];
+171
View File
@@ -0,0 +1,171 @@
// @ts-expect-error The shared ES module has no TypeScript declarations.
import { assessCapacity, normalizeHardware } from "../../docs/js/cookbook-memory.mjs";
import weightsData from "../../docs/assets/cookbook-weights.json" with { type: "json" };
export type MemoryStatus = "fits" | "tight" | "exceeds" | "unsupported" | "unknown";
/** The subset of an `assessCapacity` result the UI shows. */
export interface MemoryAssessment {
status: MemoryStatus;
reason?: string;
requiredGiB?: number;
capacityGiB?: number;
basis?: string;
parts?: Record<string, number>;
checkpointGiB?: number;
notes?: string[];
}
export interface WeightsModel {
parts: Record<string, number>;
alternates?: Record<string, number>;
sources?: string[];
}
const WEIGHTS = weightsData as { models: Record<string, WeightsModel> };
/** Measured checkpoint sizes for recipes that have no full capacity model yet. */
export function weightsFor(modelId: string | undefined): WeightsModel | undefined {
return modelId ? WEIGHTS.models[modelId] : undefined;
}
export interface AssessmentHardware {
id?: string;
backend?: string;
name?: string;
memory_gb?: number;
memory_unit?: string;
memory_type?: string;
usable_budget_gb?: number;
host_ram_gb?: number;
}
const GIB = 1024 ** 3;
/** Decimal GB for a host-memory amount entered in the device's memory unit. */
export function hostMemoryGb(hardware: AssessmentHardware, amount: number | undefined): number | undefined {
if (!amount) return undefined;
return hardware.memory_unit === "GiB" ? (amount * GIB) / 1e9 : amount;
}
export function capacityHardware(hardware: AssessmentHardware) {
return {
...hardware,
memory: {
type: hardware.memory_type,
nominal: { value: hardware.memory_gb, unit: hardware.memory_unit ?? "GB" },
...(hardware.usable_budget_gb
? { usable: { value: hardware.usable_budget_gb, unit: hardware.memory_unit ?? "GB" } }
: {}),
},
host_memory_gb: hostMemoryGb(hardware, hardware.host_ram_gb),
};
}
/** Checkpoint payload is not peak residency: offload, sharding and packing change placement. */
export function assessWeightsRecipe({
weights,
config,
hardware,
budgetGb,
}: {
weights: WeightsModel;
config: Record<string, unknown>;
hardware: AssessmentHardware;
budgetGb?: number;
}): MemoryAssessment {
const generator = (config.generator ?? {}) as {
engine?: { quantization?: { transformer_quant?: string | null }; offload?: Record<string, boolean> };
pipeline?: { components?: Record<string, string | null>; experimental?: Record<string, unknown> };
};
const device = normalizeHardware(capacityHardware(hardware), {
memoryBudgetGiB: budgetGb ? (hardware.memory_unit === "GiB" ? budgetGb : (budgetGb * 1e9) / GIB) : undefined,
});
if (!device)
return {
status: "unknown",
reason:
"Select dedicated GPU memory or unified memory with a valid budget. Host-only system RAM is not GPU capacity.",
};
const parts = { ...weights.parts };
const missing: string[] = [];
const experimental = generator.pipeline?.experimental ?? {};
if (generator.pipeline?.components?.text_encoder_weights) {
delete parts.text_encoder;
missing.push("replacement text encoder");
}
if (generator.engine?.quantization?.transformer_quant) {
delete parts.transformer;
missing.push("transformer at the selected quantization and layer profile");
}
if (experimental.video_decode_backend === "taeh3" || experimental.h3_vae_nvfp4_decoder) {
delete parts.vae;
missing.push(
experimental.video_decode_backend === "taeh3"
? "TAEH3 decoder"
: "VAE with the NVFP4 decoder and retained encoder",
);
}
const checkpointGiB = Object.values(parts).reduce((sum, value) => sum + value, 0);
return {
status: "unknown",
basis: "checkpoint",
checkpointGiB,
capacityGiB: device.capacityGiB,
parts,
reason: missing.length
? `Partial checkpoint data. Missing sizes for ${missing.join("; ")}. Runtime peak memory is not modeled.`
: "Checkpoint sizes are known, but runtime peak memory is not modeled. CPU offload and GPU count change residency, not checkpoint file sizes.",
notes: [
"Activations, temporary buffers, loading peaks and per-rank placement are not included. Frame count and resolution affect these unmodeled allocations.",
...(hardware.memory_type === "unified"
? ["Unified memory is shared by the CPU and GPU. Offloading to the CPU does not create a second memory pool."]
: []),
...(device.budgetAssumed
? ["The budget uses 75% of nominal unified memory because no usable memory budget was supplied."]
: []),
],
};
}
/**
* Run the analytical estimate for the edited configuration. Returns `undefined`
* when no capacity model covers the checkpoint, so the caller can say so.
*/
export function assessRecipe({
model,
example,
hardware,
config,
budgetGb,
hostGb,
}: {
model: Record<string, unknown>;
example: { workload?: string; env?: Record<string, string> };
hardware?: AssessmentHardware;
config?: Record<string, unknown>;
budgetGb?: number;
hostGb?: number;
}): MemoryAssessment | undefined {
if (!config || !hardware?.memory_gb) return undefined;
try {
const result = assessCapacity({
model,
example,
hardware: capacityHardware(hardware),
config,
memoryBudgetGiB: budgetGb ? (hardware.memory_unit === "GiB" ? budgetGb : (budgetGb * 1e9) / GIB) : undefined,
hostMemoryGb: hostMemoryGb(hardware, hostGb),
}) as MemoryAssessment;
return {
status: (result.status ?? "unknown") as MemoryStatus,
reason: result.reason,
requiredGiB: result.requiredGiB ?? (result as MemoryAssessment & { peakGiB?: number }).peakGiB,
capacityGiB: result.capacityGiB,
basis: result.basis,
notes: result.notes,
};
} catch (error) {
return { status: "unknown", reason: error instanceof Error ? error.message : "Memory assessment failed." };
}
}
+45
View File
@@ -0,0 +1,45 @@
const NEWS_HEADINGS = ["news", "recent updates", "what's new", "latest news", "announcements"];
const PROJECT_HEADINGS = [
"awesome work using fastvideo or our research projects",
"projects using fastvideo",
"built on fastvideo",
"awesome work",
];
function normalizeHeading(line) {
return line
.replace(/^#{2,3}\s+/, "")
.replace(/[`*_]/g, "")
.replace(/[^\p{L}\p{N}\s'’-]/gu, "")
.replace(/\s+/g, " ")
.trim()
.toLowerCase();
}
function sectionBody(readme, names) {
const lines = readme.split("\n");
const start = lines.findIndex((line) => /^##\s+/.test(line) && names.includes(normalizeHeading(line)));
if (start < 0) return null;
const rest = lines.slice(start + 1);
const end = rest.findIndex((line) => /^##\s+/.test(line));
return (end < 0 ? rest : rest.slice(0, end)).join("\n").trim();
}
const BULLET = /^-\s+(?:`(\d{4})[/-](\d{2})[/-](\d{2})`:|\*\*(\d{4})[/-](\d{2})[/-](\d{2}):?\*\*:?)\s*(.+)$/;
export function readReadmeSections(readme) {
const news = sectionBody(readme, NEWS_HEADINGS);
const projects = sectionBody(readme, PROJECT_HEADINGS);
if (news === null || projects === null) return { news: null, projects: null };
const items = [];
for (const line of news.split("\n")) {
const match = BULLET.exec(line.trim());
if (!match) continue;
const [, y1, m1, d1, y2, m2, d2, text] = match;
items.push({ date: `${y1 ?? y2}/${m1 ?? m2}/${d1 ?? d2}`, text: text.trim() });
}
return { news: items, projects };
}
export const README_HEADINGS = { news: NEWS_HEADINGS, projects: PROJECT_HEADINGS };
File diff suppressed because one or more lines are too long
+19 -34
View File
@@ -127,10 +127,6 @@
],
"display": "group"
},
{
"label": "PAN 2 text/image to video",
"href": "/inference/pan2"
},
{
"label": "Examples",
"href": "/inference/examples/examples_inference_index"
@@ -145,21 +141,17 @@
"label": "All recipes",
"href": "/cookbook"
},
{
"label": "H3 server and playground",
"href": "/cookbook/openai-api"
},
{
"label": "MiniMax H3",
"display": "group",
"collapsed": false,
"collapsed": true,
"items": [
{
"label": "MiniMax H3",
"href": "/cookbook/minimax-h3/models/minimax-h3-t2v"
},
{
"label": "FastH3 OmniRef",
"label": "FastH3 Omni v1",
"href": "/cookbook/minimax-h3/fasth3-omniref"
},
{
@@ -170,14 +162,18 @@
"label": "FastH3 Trim",
"href": "/cookbook/minimax-h3/fasth3-trim"
},
{
"label": "FastH3 Preview V0.2",
"href": "/cookbook/minimax-h3/fasth3-preview"
},
{
"label": "FastH3 4-Step V1",
"href": "/cookbook/minimax-h3/fasth3-4step"
},
{
"label": "Pika H3 Flex",
"href": "/cookbook/minimax-h3/models/pika-h3-flex-t2va"
},
{
"label": "Open-H3-Regenerate-2K",
"href": "/cookbook/minimax-h3/open-h3-regenerate-2k"
},
{
"label": "Recipes and setup",
"href": "/cookbook/minimax-h3"
@@ -191,7 +187,7 @@
{
"label": "Wan",
"display": "group",
"collapsed": false,
"collapsed": true,
"items": [
{
"label": "FastWan2.1 1.3B",
@@ -253,25 +249,6 @@
}
]
},
{
"label": "PAN 2",
"display": "group",
"collapsed": true,
"items": [
{
"label": "PAN 2",
"href": "/cookbook/pan2/pan2"
},
{
"label": "Family guide",
"href": "/cookbook/pan2"
},
{
"label": "Model reference",
"href": "/inference/pan2"
}
]
},
{
"label": "FLUX",
"href": "/cookbook/flux"
@@ -333,6 +310,14 @@
"label": "Attn-QAT Training",
"href": "/training/attn_qat"
},
{
"label": "FastH3 NVFP4 QAD",
"href": "/training/minimax_h3_fp4_qad"
},
{
"label": "Pika H3 Flex NVFP4 QAD",
"href": "/training/pika_h3_flex_qad"
},
{
"label": "HunyuanVideo 1.5 T2V",
"href": "/training/hunyuan15_overfit"
+1 -1
View File
@@ -15,7 +15,7 @@
"test": "playwright test",
"format": "prettier --write .",
"format:check": "prettier --check .",
"test:content": "node --test tests/content.test.mjs tests/commands.test.mjs"
"test:content": "node --test tests/content.test.mjs tests/commands.test.mjs tests/readme-sections.test.mjs tests/memory-assessment.test.mjs"
},
"dependencies": {
"blume": "2.2.3",
+11 -6
View File
@@ -6,6 +6,7 @@ import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promi
import { dirname, join, relative, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";
import { parse, stringify } from "yaml";
import { readReadmeSections, README_HEADINGS } from "../lib/readme.mjs";
const site = fileURLToPath(new URL("../", import.meta.url));
const repo = resolve(site, "..");
@@ -176,12 +177,11 @@ export async function transferDocs() {
return (await stat(path)).isDirectory() || /\.mdx?$/.test(path);
},
});
// Native callout directives and islands compile as MDX, while source links keep .md filenames.
for (const file of await files(content)) {
if (!file.endsWith(".md")) continue;
const text = await readFile(file, "utf8");
if (/^:::(?:note|tip|warning|danger|info|success)\b/m.test(text) || text.includes("<RecipeFamily ")) {
await writeFile(file.replace(/\.md$/, ".mdx"), text);
await writeFile(file.replace(/\.md$/, ".mdx"), text.replace(/\{#([^}]+)\}/g, "[#$1]"));
await rm(file);
}
}
@@ -251,10 +251,15 @@ export async function transferDocs() {
// The README owns announcements and the community list; the welcome page renders a short news digest.
const readme = await readFile(join(repo, "README.md"), "utf8");
const news = readme.match(/^## NEWS\n([\s\S]*?)(?=^##|^###)/m)?.[1];
const projects = readme.match(/^## Awesome work using FastVideo or our research projects\n([\s\S]*?)(?=^## )/m)?.[1];
if (!news || !projects) throw new Error("The README news or project section boundary needs updating.");
const recent = [...news.matchAll(/^- `([\d/]+)`: (.+)$/gm)].slice(0, 2).map(([, date, text]) => {
const { news, projects } = readReadmeSections(readme);
if (!news || !projects) {
const accepted = (names) => names.map((name) => `"## ${name.replace(/\b\w/g, (c) => c.toUpperCase())}"`).join(", ");
throw new Error(
`README section not found. Accepted news headings: ${accepted(README_HEADINGS.news)}. ` +
`Accepted project headings: ${accepted(README_HEADINGS.projects)}.`,
);
}
const recent = news.slice(0, 2).map(({ date, text }) => {
const links = [...text.matchAll(/\[([^\]]+)\]\(([^)]+)\)/g)];
const headline = text
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
+1 -1
View File
@@ -31,7 +31,7 @@ test("converted guides render admonitions, preserve content, and publish local i
const home = await readFile(new URL("dist/index.html", site), "utf8");
assert.ok(home.includes("Welcome to FastVideo"));
assert.ok(home.includes("/FastVideo/assets/logos/logo.svg"));
assert.ok(home.includes("FastVideo is an inference and post-training framework for diffusion models."));
assert.ok(home.includes("FastVideo is an inference and post-training framework for video diffusion models."));
assert.ok(!home.includes("About FastVideo and performance overview"));
await readFile(new URL("dist/assets/logos/logo.svg", site));
});
+267 -58
View File
@@ -224,17 +224,17 @@ test("catalog discovery opens dedicated model recipes and filters by family and
await page.goto("/FastVideo/cookbook");
const cards = page.locator(".recipe-card");
await expect(cards).toHaveCount(38);
await expect(cards.first()).toContainText("FastH3 OmniRef");
await expect(cards.first()).toContainText("FastH3 Omni v1");
await page.getByLabel("Model family", { exact: true }).selectOption("Wan");
await page.getByLabel("Search recipes").fill("Wan2.1 I2V");
await expect(cards).toHaveCount(1);
await cards.click();
await expect(page).toHaveURL(/cookbook\/wan\/wan21-i2v$/);
await expect(page).toHaveURL(/cookbook\/wan\/wan21-i2v\/?$/);
await expect(page.getByLabel("Base model")).toHaveCount(0);
await page.getByRole("link", { name: "← All recipes", exact: true }).click();
await page.getByLabel("Search recipes").fill("FastWan2.1");
await page.locator(".recipe-card").click();
await expect(page).toHaveURL(/cookbook\/wan\/fastwan21$/);
await expect(page).toHaveURL(/cookbook\/wan\/fastwan21\/?$/);
await expect(page.getByRole("heading", { level: 1 })).toHaveText("FastWan2.1 1.3B");
});
@@ -252,9 +252,8 @@ test("hardware preferences stay distinct from schema-backed GPU count and topolo
const yaml = page.getByLabel("Generated YAML", { exact: true });
await expect(yaml).toContainText('"num_gpus": 1');
await expect(yaml).not.toContainText('"hardware"');
await expect(page.getByText("Valid configuration", { exact: true })).toBeVisible();
await expect(page.getByText("Fit unknown", { exact: true })).toBeVisible();
await expect(page.getByText("No recorded run", { exact: true })).toBeVisible();
await expect(page.getByText("Schema checks passed", { exact: true })).toBeVisible();
await expect(page.getByText("Memory not estimated", { exact: true })).toBeVisible();
await page.getByLabel("GPU count", { exact: true }).fill("0");
await expect(yaml).toHaveCount(0);
await expect(page.getByLabel("GPU count", { exact: true })).toHaveAttribute("aria-invalid", "true");
@@ -395,6 +394,35 @@ test("source-backed controls update the copied command without changing models",
await expect(page.getByLabel("Inference command", { exact: true })).toContainText("--num-gpus '2'");
});
test("Pika H3 Flex knobs select the tier, bucket, duration, task, and NVFP4 decoder", async ({ page }) => {
await page.goto("/FastVideo/cookbook/minimax-h3");
const pika = page.locator(".recipe-card").filter({ hasText: "Pika H3 Flex" });
await expect(pika.locator("img")).toHaveAttribute("src", /\/pika\.webp$/);
await expect(pika).toContainText("Pika");
await pika.click();
await expect(page).toHaveURL(/cookbook\/minimax-h3\/models\/pika-h3-flex-t2va$/);
const command = page.getByLabel("Inference command", { exact: true });
await expect(command).toContainText("FASTVIDEO_ATTENTION_BACKEND=FLASH_ATTN FASTVIDEO_FA4=1");
await chip(page, "Resolution", "480p");
await chip(page, "Aspect ratio", "9:16");
await chip(page, "Duration", "10 s (243 frames)");
await chip(page, "VAE decoder", "NVFP4 36L quality");
await expect(command).toContainText("--resolution '480p'");
await expect(command).toContainText("--aspect '9:16'");
await expect(command).toContainText("--duration '10'");
await expect(command).toContainText("--nvfp4-decoder './fasth3-nvfp4-vae/quality-36L/decoder-nvfp4.safetensors'");
await page
.getByRole("group", { name: "Recipe variant", exact: true })
.getByRole("button", { name: "Pika H3 Flex keyframes", exact: true })
.click();
await expect(command).toContainText("--task fl2va --offload resident");
await chip(page, "Task", "First frame (I2VA)");
await chip(page, "Memory", "Lazy load + offload");
await expect(command).toContainText("--task 'i2va'");
await expect(command).toContainText("--offload 'lazy'");
await expect(page.getByRole("button", { name: "Copy example + command", exact: true })).toBeEnabled();
});
test("Wan I2V requires an image for CLI and SDK examples without changing server defaults", async ({ page }) => {
await page.goto("/FastVideo/cookbook/wan/wan21-i2v");
await chip(page, "Run mode", "CLI");
@@ -443,26 +471,6 @@ test("source-backed serving pages expose their actual server settings", async ({
await expect(page.getByRole("button", { name: "Copy config + command", exact: true })).toBeEnabled();
});
test("PAN 2 has a family card with the IFM logo and an editable serving deployment", async ({ page }) => {
await page.goto("/FastVideo/cookbook");
await page.getByLabel("Search recipes").fill("PAN 2");
const pan2 = page.locator(".recipe-card");
await expect(pan2).toHaveCount(1);
await expect(pan2.locator("img")).toHaveAttribute("src", /\/ifm\.webp$/);
await expect(pan2.locator(".recipe-card-kicker")).toHaveText("IFM");
await pan2.click();
await expect(page).toHaveURL(/cookbook\/pan2\/pan2$/);
await expect(page.locator(".recipe-matrix")).toBeVisible({ timeout: 30000 });
await expect(page.getByRole("heading", { level: 1 })).toHaveText("PAN 2");
await settings(page, "Serving");
await page.getByLabel("Port", { exact: true }).fill("9010");
await output(page, "config.yaml");
await expect(page.getByLabel("Generated YAML", { exact: true })).toContainText(/"port": 9010/);
await expect(page.getByLabel("Generated YAML", { exact: true })).toContainText(/"model_path": "IFM\/PAN2"/);
await expect(page.getByLabel("Generated YAML", { exact: true })).toContainText(/"dit_layerwise": false/);
await expect(page.getByRole("button", { name: "Download YAML", exact: false })).toBeEnabled();
});
test("source model hardware preferences survive hydration and reload", async ({ page }) => {
const errors: string[] = [];
page.on("pageerror", (error) => errors.push(error.message));
@@ -482,16 +490,16 @@ test("custom devices persist and no preview control can block generated YAML", a
await page.getByRole("button", { name: "Can't find it? Add a device", exact: true }).click();
await page.getByLabel("Device name", { exact: true }).fill("RTX 4090");
await page.getByRole("combobox", { name: "Memory type", exact: true }).selectOption("dedicated");
await page.getByRole("spinbutton", { name: "Nominal memory", exact: true }).fill("24");
await page.getByRole("spinbutton", { name: "Usable budget (optional)", exact: true }).fill("32");
await expect(page.getByText("The usable budget cannot exceed the nominal memory.", { exact: true })).toBeVisible();
await expect(page.getByRole("button", { name: "Use this device", exact: true })).toBeDisabled();
await page.getByRole("spinbutton", { name: "Usable budget (optional)", exact: true }).fill("22");
await page.getByRole("spinbutton", { name: "Total memory", exact: true }).fill("24");
await page.getByRole("spinbutton", { name: "Usable memory (optional)", exact: true }).fill("32");
await expect(page.getByText("Usable memory cannot exceed total memory.", { exact: true })).toBeVisible();
await expect(page.getByRole("button", { name: "Save device", exact: true })).toBeDisabled();
await page.getByRole("spinbutton", { name: "Usable memory (optional)", exact: true }).fill("22");
await page.getByRole("spinbutton", { name: "Host RAM (optional)", exact: true }).fill("64");
await page.getByRole("button", { name: "Use this device", exact: true }).click();
await page.getByRole("button", { name: "Save device", exact: true }).click();
await expect(page.getByText("RTX 4090", { exact: true })).toBeVisible();
await expect(page.locator(".hardware-selected")).toContainText("24 GB VRAM");
await expect(page.locator(".hardware-selected")).toContainText("Your budget: 22 GB");
await expect(page.locator(".hardware-selected")).toContainText("Usable memory: 22 GB");
await expect(page.locator(".hardware-selected")).toContainText("Host RAM: 64 GB");
await expect(page).toHaveURL(/hardware=custom/);
await expect(page).toHaveURL(/hwName=RTX\+4090/);
@@ -500,28 +508,26 @@ test("custom devices persist and no preview control can block generated YAML", a
await expect(page.locator(".hardware-selected")).toContainText("24 GB VRAM");
await expect(page.locator(".component-plan")).toHaveCount(0);
await expect(page.getByRole("button", { name: "Download YAML", exact: true })).toBeEnabled();
await page.getByRole("button", { name: "Clear hardware target", exact: true }).click();
await page.getByRole("button", { name: "Clear hardware", exact: true }).click();
await expect(page.getByText("RTX 4090", { exact: true })).toHaveCount(0);
await expect(page.getByText("No hardware target selected.", { exact: true })).toBeVisible();
await expect(page.getByText("No hardware selected.", { exact: true })).toBeVisible();
});
test("hardware is never preselected; a single-GPU device sets one GPU with valid parallelism", async ({ page }) => {
test("hardware is never preselected and a desktop card keeps the recipe GPU count", async ({ page }) => {
await page.goto(route);
await expect(page.locator(".hardware-picker")).toBeVisible({ timeout: 30000 });
await expect(page.getByText("No hardware target selected.", { exact: true })).toBeVisible();
await expect(page.getByText("No hardware selected.", { exact: true })).toBeVisible();
await output(page, "config.yaml");
const yaml = page.getByLabel("Generated YAML", { exact: true });
await expect(yaml).toContainText('"num_gpus": 4');
await chooseHardware(page, "RTX 5090");
await expect(page).toHaveURL(/hardware=geforce-rtx-5090-32gb/);
await expect(page.getByText(/GeForce RTX 5090 .*32 GB VRAM/)).toBeVisible();
await expect(page.getByText(/Set to 1 GPU for .*RTX 5090/)).toBeVisible();
await expect(yaml).toContainText('"num_gpus": 1');
await expect(yaml).toContainText('"sp_size": -1');
await expect(yaml).toContainText('"num_gpus": 4');
await expect(yaml).not.toContainText('"hardware"');
await expect(page.getByText("Valid configuration", { exact: true })).toBeVisible();
await page.getByRole("button", { name: "Clear hardware target", exact: true }).click();
await expect(page.getByText("No hardware target selected.", { exact: true })).toBeVisible();
await expect(page.getByText("Schema checks passed", { exact: true })).toBeVisible();
await page.getByRole("button", { name: "Clear hardware", exact: true }).click();
await expect(page.getByText("No hardware selected.", { exact: true })).toBeVisible();
});
test("lowering the GPU count resets parallelism that no longer fits", async ({ page }) => {
@@ -532,7 +538,7 @@ test("lowering the GPU count resets parallelism that no longer fits", async ({ p
await expect(page.getByText("Parallelism set to automatic for 2 GPUs.")).toBeVisible();
await expect(yaml).toContainText('"num_gpus": 2');
await expect(yaml).toContainText('"sp_size": -1');
await expect(page.getByText("Valid configuration", { exact: true })).toBeVisible();
await expect(page.getByText("Schema checks passed", { exact: true })).toBeVisible();
});
test("browser back after a hardware-only change keeps the editor loaded", async ({ page }) => {
@@ -542,7 +548,7 @@ test("browser back after a hardware-only change keeps the editor loaded", async
await expect(page).toHaveURL(/hardware=geforce-rtx-5090-32gb/);
await page.goBack();
await expect(page).not.toHaveURL(/hardware=/);
await expect(page.getByText("No hardware target selected.", { exact: true })).toBeVisible();
await expect(page.getByText("No hardware selected.", { exact: true })).toBeVisible();
await expect(page.getByRole("button", { name: "Download YAML", exact: true })).toBeEnabled();
await expect(page.getByText("Loading deployment settings…")).toHaveCount(0);
});
@@ -555,15 +561,173 @@ test("hardware links resolve catalog aliases and non-CUDA devices are not CUDA t
await expect(page.getByRole("radio", { name: /Arc B580/ })).toHaveCount(0);
});
test("recipe facts separate validity, fit, and run evidence with inspectable assumptions", async ({ page }) => {
test("recipe facts state validity and why memory has no estimate", async ({ page }) => {
await page.goto(route);
const facts = page.getByLabel("Recipe facts");
await expect(facts.getByText("Valid configuration", { exact: true })).toBeVisible();
await expect(facts.getByText("Fit unknown", { exact: true })).toBeVisible();
await expect(facts.getByText("No recorded run", { exact: true })).toBeVisible();
const facts = page.locator(".recipe-facts");
await expect(facts.getByText("Schema checks passed", { exact: true })).toBeVisible();
await expect(facts.getByText("Memory not estimated", { exact: true })).toBeVisible();
await page.getByRole("button", { name: "Details", exact: true }).click();
await expect(page.getByText(/This is not a fit claim/)).toBeVisible();
await expect(page.getByText(/do not establish this configuration/)).toBeVisible();
await expect(page.getByText(/Select a device to estimate memory/)).toBeVisible();
});
test("component defaults are selected and decoder choices stay consistent with copied YAML", async ({ page }) => {
await page.goto(route);
const decoder = page.getByRole("group", { name: "NVFP4 video decoder", exact: true });
await expect(decoder.getByRole("button", { name: "Original", exact: true })).toHaveAttribute("aria-pressed", "true");
await expect(
page
.getByRole("group", { name: "Text encoder", exact: true })
.getByRole("button", { name: "Bundled", exact: true }),
).toHaveAttribute("aria-pressed", "true");
await chip(page, "Video decoder", "taeh3");
await decoder.getByRole("button", { name: /8L/ }).click();
await expect(
page
.getByRole("group", { name: "Video decoder", exact: true })
.getByRole("button", { name: "h3-vae", exact: true }),
).toHaveAttribute("aria-pressed", "true");
await output(page, "config.yaml");
await expect(page.getByLabel("Generated YAML")).toContainText('"video_decode_backend": "h3-vae"');
await expect(page.getByLabel("Generated YAML")).toContainText("fast-8L/decoder-nvfp4.safetensors");
await chip(page, "Video decoder", "taeh3");
await expect(page.getByLabel("Generated YAML")).toContainText('"h3_vae_nvfp4_decoder": null');
await expect(decoder.getByRole("button", { name: "Original", exact: true })).toHaveAttribute("aria-pressed", "true");
await page.getByRole("button", { name: "Reset changes", exact: true }).click();
await expect(page.getByLabel("Generated YAML")).not.toContainText("h3_vae_nvfp4_decoder");
});
test("failed memory metadata is visible and retry restores checkpoint data", async ({ page }) => {
await page.route("**/capacity/index.json", (route) => route.fulfill({ status: 503, body: "unavailable" }));
await page.goto(`${route}?hardware=custom&hwName=GPU&hwType=dedicated&hwMemory=80&hwUnit=GiB&hwBackend=cuda`);
await expect(page.getByText(/Memory data could not load/)).toBeVisible();
await expect(page.getByText(/Known checkpoint data/)).toHaveCount(0);
await page.unroute("**/capacity/index.json");
await page.getByRole("button", { name: "Retry memory data", exact: true }).click();
await expect(page.getByText(/Known checkpoint data: 137\.7 GiB. Device budget: 80\.0 GiB/)).toBeVisible();
await expect(page.getByText(/Memory data could not load/)).toHaveCount(0);
await expect(page.getByText("Weights fit", { exact: true })).toHaveCount(0);
});
test("host-only system RAM cannot produce a GPU weights fit verdict", async ({ page }) => {
await page.goto(`${route}?hardware=custom&hwName=Host&hwType=system&hwMemory=128&hwUnit=GB&hwBackend=cuda`);
const facts = page.locator(".recipe-facts");
await expect(facts.getByText("Memory not estimated", { exact: true })).toBeVisible();
await page.getByRole("button", { name: "Details", exact: true }).click();
await expect(facts.getByText(/Host-only system RAM is not GPU capacity/)).toBeVisible();
await expect(facts.getByText(/Resident weights need/)).toHaveCount(0);
await expect(page.getByRole("button", { name: "Download YAML", exact: false })).toBeEnabled();
});
test("TAEH3 stops using the bundled VAE weights in memory facts", async ({ page }) => {
await page.goto(`${route}?hardware=custom&hwName=GPU&hwType=dedicated&hwMemory=80&hwUnit=GB&hwBackend=cuda`);
const facts = page.locator(".recipe-facts");
await expect(facts.getByText("Memory not estimated", { exact: true })).toBeVisible();
await chip(page, "Video decoder", "taeh3");
await expect(facts.getByText("Memory not estimated", { exact: true })).toBeVisible();
await page.getByRole("button", { name: "Details", exact: true }).click();
await expect(facts.getByText(/Missing sizes for TAEH3 decoder/)).toBeVisible();
await expect(facts.getByText(/Resident weights need/)).toHaveCount(0);
await output(page, "config.yaml");
await expect(page.getByLabel("Generated YAML")).toContainText('"video_decode_backend": "taeh3"');
await chip(page, "Video decoder", "h3-vae");
await expect(facts.getByText("Memory not estimated", { exact: true })).toBeVisible();
});
test("FastH3 keeps unmodeled quantization and decoder residency unknown", async ({ page }) => {
await page.goto(route);
await chooseHardware(page, "RTX 5090");
const facts = page.locator(".recipe-facts");
await expect(facts.getByText("Memory not estimated", { exact: true })).toBeVisible();
await page.getByRole("button", { name: "Details", exact: true }).click();
await expect(page.getByText(/Known checkpoint data: 137\.7 GiB. Device budget: 29\.8 GiB/)).toBeVisible();
await page
.getByRole("group", { name: "Transformer quantization", exact: true })
.getByRole("button", { name: "NVFP4", exact: true })
.click();
await expect(facts.getByText("Memory not estimated", { exact: true })).toBeVisible();
await expect(facts.getByText(/selected quantization and layer profile/)).toBeVisible();
await expect(facts.getByText(/Resident weights need/)).toHaveCount(0);
await page
.getByRole("group", { name: "NVFP4 video decoder", exact: true })
.getByRole("button", { name: /8L · fast/ })
.click();
await expect(facts.getByText("Memory not estimated", { exact: true })).toBeVisible();
await expect(facts.getByText(/VAE with the NVFP4 decoder and retained encoder/)).toBeVisible();
await expect(facts.getByText(/Resident weights need/)).toHaveCount(0);
await output(page, "config.yaml");
await expect(page.getByLabel("Generated YAML")).toContainText('"transformer_quant": "NVFP4"');
await expect(page.getByLabel("Generated YAML")).toContainText("fast-8L/decoder-nvfp4.safetensors");
});
test("4-Step quantization and full-depth decoder swaps also stay unestimated", async ({ page }) => {
await page.goto("/FastVideo/cookbook/minimax-h3/fasth3-4step");
await chooseHardware(page, "RTX 5090");
const facts = page.locator(".recipe-facts");
await page.getByRole("button", { name: "Details", exact: true }).click();
await chip(page, "Transformer quantization", "FP8");
await expect(facts.getByText(/selected quantization and layer profile/)).toBeVisible();
await chip(page, "Transformer quantization", "Default");
await page
.getByRole("group", { name: "NVFP4 video decoder", exact: true })
.getByRole("button", { name: /36L/ })
.click();
await expect(facts.getByText(/VAE with the NVFP4 decoder and retained encoder/)).toBeVisible();
await expect(facts.getByText(/Resident weights need/)).toHaveCount(0);
});
test("the retired Preview route and deployment links keep their checkpoint", async ({ page }) => {
await page.goto("/FastVideo/cookbook/minimax-h3/fasth3-preview");
await output(page, "config.yaml");
await expect(page.getByLabel("Generated YAML")).toContainText("FastVideo-Minimax-FastH3-Preview-v0.2");
await page.goto("/FastVideo/cookbook?deployment=fasth3-preview%2Fcuda-rest");
await expect(page).toHaveURL(/minimax-h3\/fasth3-preview/);
await output(page, "config.yaml");
await expect(page.getByLabel("Generated YAML")).toContainText("FastVideo-Minimax-FastH3-Preview-v0.2");
});
for (const [deployment, label, sequential, tileBatch] of [
["rtx5090-rest", "RTX 5090", true, "4"],
["rtx-pro6000-rest", "RTX PRO 6000", false, "8"],
["spark-rest", "DGX Spark", false, "12"],
["cuda-rest", "RTX 5090", true, "4"],
] as const) {
test(`Omni deployment link ${deployment} preserves its hardware baseline`, async ({ page }) => {
await page.goto(`/FastVideo/cookbook?deployment=fasth3-omniref%2F${deployment}`);
await expect(page).toHaveURL(/minimax-h3\/fasth3-omniref/);
await expect(page.getByRole("button", { name: label, exact: true })).toHaveAttribute("aria-pressed", "true");
await page.getByRole("button", { name: "Serving", exact: true }).click();
await output(page, "config.yaml");
const yaml = page.getByLabel("Generated YAML");
await expect(yaml).toContainText(`"h3_sequential_load": ${sequential}`);
await expect(yaml).toContainText('"layer_profile": "h3_dit_vsa"');
await output(page, "Commands");
await expect(page.getByLabel("Server command")).toContainText(`FASTVIDEO_H3_VAE_TILE_BATCH=${tileBatch}`);
if (deployment === "spark-rest")
await expect(page.getByLabel("Server command")).toContainText("FASTVIDEO_NVFP4_MM_BACKEND=cudnn");
});
}
test("packed FastH3 checkpoints keep their fixed precision", async ({ page }) => {
await page.goto("/FastVideo/cookbook/minimax-h3/fasth3-trim");
await page.getByRole("tab", { name: "config.yaml" }).click();
await expect(page.getByLabel("Generated YAML")).toContainText('"transformer_quant": "NVFP4"');
await expect(page.getByLabel("Generated YAML")).toContainText('"layer_profile": "h3_dit_vsa"');
await expect(page.getByText("Schema checks passed", { exact: true })).toBeVisible();
await expect(page.getByRole("group", { name: "Transformer quantization", exact: true })).toHaveCount(0);
});
test("MLX FastH3 stays unestimated and a GiB device keeps its unit", async ({ page }) => {
await page.goto("/FastVideo/cookbook/minimax-h3/fasth3-8step?deployment=fasth3-8step%2Fmlx-rest");
await page.getByRole("button", { name: "+ Find or add a Mac", exact: true }).click();
await page.getByLabel("Search hardware", { exact: true }).fill("M4 Max");
await page.getByRole("radio", { name: /Apple M4 Max 128 GB/ }).click();
await expect(page.getByText("Memory not estimated", { exact: true })).toBeVisible();
await page.getByRole("button", { name: "Details", exact: true }).click();
await expect(page.getByText(/MLX checkpoints use a different precision/)).toBeVisible();
await page.goto(
"/FastVideo/cookbook/minimax-h3/fasth3-8step?hardware=custom&hwName=Big&hwType=dedicated&hwMemory=80&hwUnit=GiB&hwBackend=cuda",
);
await expect(page.getByText(/Device budget: 80\.0 GiB/)).toBeVisible();
});
test("catalog lookup resolves aliases, variants, and unified memory without pooling", async ({ page }) => {
@@ -572,7 +736,7 @@ test("catalog lookup resolves aliases, variants, and unified memory without pool
await expect(page).toHaveURL(/hardware=rtx-pro-6000-blackwell-workstation-96gb/);
await expect(page.locator(".hardware-selected")).toContainText("96 GB VRAM");
await expect(page.locator(".hardware-address-note")).toContainText("Per device, not a combined pool");
await page.getByRole("button", { name: "Clear hardware target", exact: true }).click();
await page.getByRole("button", { name: "Clear hardware", exact: true }).click();
await page.getByRole("button", { name: "+ Find or add a GPU", exact: true }).click();
await page.getByLabel("Search hardware", { exact: true }).fill("GB10");
await page.getByRole("radio", { name: /DGX Spark/ }).click();
@@ -592,9 +756,9 @@ test("MLX lookup uses unified memory and a custom Mac stays reusable", async ({
await page.getByRole("radio", { name: /Apple M4 Max/ }).click();
await expect(page.locator(".hardware-selected")).toContainText("128 GB unified memory");
await expect(page.locator(".hardware-selected")).toContainText("Apple MLX");
await expect(page.locator(".hardware-selected")).toContainText("Usable budget: 96 GB");
await expect(page.locator(".hardware-selected")).toContainText("Usable memory: 96 GB");
await expect(page.locator(".hardware-address-note")).toContainText("not dedicated VRAM");
await expect(page.getByRole("button", { name: "Clear hardware target", exact: true })).toBeVisible();
await expect(page.getByRole("button", { name: "Clear hardware", exact: true })).toBeVisible();
});
test("the directory filters by runtime without a hardware picker", async ({ page }) => {
@@ -608,6 +772,33 @@ test("the directory filters by runtime without a hardware picker", async ({ page
await expect(page.locator(".recipe-card").filter({ hasText: "FastWan2.1 1.3B" })).toHaveCount(0);
});
test("MLX Omni discovery and its original route expose the MLX launch command", async ({ page }) => {
await page.goto("/FastVideo/cookbook");
await page.getByLabel("Runtime", { exact: true }).selectOption("mlx");
await page.getByLabel("Search recipes").fill("FastH3 Omni");
const cards = page.locator(".recipe-card");
await expect(cards).toHaveCount(1);
await expect(cards).toHaveAttribute("href", "/FastVideo/cookbook/minimax-h3/models/fasth3-omniref-mlx");
await cards.click();
await expect(page.getByLabel("Inference command")).toContainText("convert_minimax_h3_mlx.py");
await expect(page.getByLabel("Inference command")).toContainText("--mlx-checkpoint ./FastH3-OmniRef-MLX/int6");
await expect(page.getByLabel("Inference command")).not.toContainText("fastvideo serve");
await page.reload();
await expect(page.getByLabel("Inference command")).toContainText("--mlx-checkpoint ./FastH3-OmniRef-MLX/int6");
});
test("category counts match visible cards and exclude retired Preview", async ({ page }) => {
await page.goto("/FastVideo/cookbook");
await expect(page.locator(".recipe-card")).toHaveCount(38);
for (const category of ["All models", "Video", "Image", "Audio", "World"]) {
const button = page.getByRole("button", { name: category, exact: true });
const count = Number(await button.locator(".recipe-filter-count").innerText());
await button.click();
await expect(page.locator(".recipe-card")).toHaveCount(count);
await expect(page.locator(".recipe-card").filter({ hasText: "FastH3 Preview V0.2" })).toHaveCount(0);
}
});
test("task discovery filters the directory and round-trips through the URL", async ({ page }) => {
await page.goto("/FastVideo/cookbook");
await page.getByLabel("Task", { exact: true }).selectOption("Image to video");
@@ -633,10 +824,10 @@ test("directory filters round-trip through the URL", async ({ page }) => {
test("a saved target missing from the catalog stays visible and clearable", async ({ page }) => {
await page.goto(`${route}?hardware=retired-gpu`);
await expect(page.getByText(/Saved hardware target \(retired-gpu\) is not in this catalog/)).toBeVisible();
await page.getByRole("button", { name: "Clear hardware target", exact: true }).click();
await expect(page.getByText(/The saved device \(retired-gpu\) is not in this catalog/)).toBeVisible();
await page.getByRole("button", { name: "Clear hardware", exact: true }).click();
await expect(page).not.toHaveURL(/hardware=/);
await expect(page.getByText("No hardware target selected.", { exact: true })).toBeVisible();
await expect(page.getByText("No hardware selected.", { exact: true })).toBeVisible();
});
test("catalog search renders one plain no-results message", async ({ page }) => {
@@ -666,3 +857,21 @@ test("WebSocket deployments expose only the serving interface", async ({ page })
await expect(modes.getByRole("button", { name: "CLI", exact: true })).toHaveCount(0);
await expect(page.getByText(/Only serving is recorded for it/)).toBeVisible();
});
test("video-to-video deployments expose only the serving interface", async ({ page }) => {
await page.goto("/FastVideo/cookbook/minimax-h3/open-h3-regenerate-2k");
const modes = page.getByRole("group", { name: "Run mode" });
await expect(modes.getByRole("button")).toHaveCount(1);
await expect(modes.getByRole("button", { name: "Serving", exact: true })).toHaveAttribute("aria-pressed", "true");
await expect(page.getByText(/Send it to the server as video_reference/)).toBeVisible();
});
test("Open-H3-Regenerate-2K is discoverable as a video upscale with its serving page", async ({ page }) => {
await page.goto("/FastVideo/cookbook");
await page.getByLabel("Task", { exact: true }).selectOption("Video upscale");
const card = page.locator(".recipe-card").filter({ hasText: "Open-H3-Regenerate-2K" });
await expect(card).toHaveCount(1);
await card.click();
await expect(page).toHaveURL(/cookbook\/minimax-h3\/open-h3-regenerate-2k\/?$/);
await expect(page.getByRole("heading", { level: 1 })).toHaveText("Open-H3-Regenerate-2K");
});
+21 -1
View File
@@ -1,5 +1,19 @@
import { expect, test } from "@playwright/test";
test("Omni Apple silicon links open the published guide and MLX recipe", async ({ page }) => {
for (const slash of ["", "/"]) {
await page.goto(`/FastVideo/cookbook/minimax-h3/fasth3-omniref${slash}`);
const guide = page.getByRole("link", { name: "FastH3 Omni on MLX", exact: true });
await expect(guide).toHaveAttribute("href", "/FastVideo/inference/fasth3_omniref_mlx");
await guide.click();
await expect(page).toHaveURL(/\/FastVideo\/inference\/fasth3_omniref_mlx\/?$/);
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
}
await page.goto("/FastVideo/cookbook/minimax-h3/fasth3-omniref");
await page.getByRole("link", { name: "MLX recipe", exact: true }).click();
await expect(page.getByLabel("Inference command")).toContainText("--mlx-checkpoint");
});
test("GitHub stars refresh automatically and survive a failed refresh", async ({ page }) => {
await page.clock.install();
let requests = 0;
@@ -159,6 +173,12 @@ test("the model catalog filters by family and checkpoint name", async ({ page })
await expect(catalog.getByRole("status")).toHaveText("3 checkpoints");
await page.getByRole("searchbox", { name: "Search checkpoints", exact: true }).fill("14B");
await expect(catalog.getByRole("status")).toHaveText("1 checkpoint");
await page.getByRole("button", { name: "All models", exact: true }).click();
await page.getByRole("searchbox").fill("NVFP4");
const codes = await catalog.locator(".model-checkpoints code").allInnerTexts();
expect(codes.length).toBeGreaterThan(0);
expect(codes.every((text) => text.includes("NVFP4"))).toBe(true);
await expect(catalog.getByText("FastVideo/FastVideo-FastH3-8-Step-V2-FP8", { exact: true })).toHaveCount(0);
await page.getByRole("searchbox").fill("no-such-model");
await expect(catalog.getByRole("status")).toHaveText("0 checkpoints");
await page.getByRole("button", { name: "Clear filters", exact: true }).click();
@@ -178,7 +198,7 @@ test("family runbooks use React model cards without the old cookbook runtime", a
await page.goto("/FastVideo/cookbook/wan/");
await expect(page.locator(".recipe-card").first()).toBeVisible();
await expect(page.getByRole("button", { name: "Search", exact: true })).toBeVisible();
await expect(page.getByRole("link", { name: "← All models", exact: true })).toHaveAttribute(
await expect(page.getByRole("link", { name: "← All recipes", exact: true })).toHaveAttribute(
"href",
/\/FastVideo\/cookbook$/,
);
@@ -0,0 +1,95 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
assessRecipe,
assessWeightsRecipe,
capacityHardware,
hostMemoryGb,
weightsFor,
} from "../lib/memory-assessment.ts";
import { buildCapacityCatalog } from "../../docs/js/build-cookbook-capacity.mjs";
import { assessCapacity } from "../../docs/js/cookbook-memory.mjs";
const hardware = { backend: "cuda", memory_gb: 80, memory_unit: "GiB", memory_type: "dedicated" };
const weights = weightsFor("fasth3-8step");
const config = {
generator: {
engine: { num_gpus: 4, offload: { text_encoder: true, vae: true } },
pipeline: { experimental: {} },
},
};
test("checkpoint data never certifies runtime fit or subtracts CPU-offloaded weights", () => {
for (const unified of [false, true]) {
const target = { ...hardware, memory_type: unified ? "unified" : "dedicated", usable_budget_gb: 60 };
const baseline = assessWeightsRecipe({ weights, config, hardware: target });
const edited = structuredClone(config);
edited.generator.engine.offload.text_encoder = false;
edited.generator.engine.num_gpus = 1;
const result = assessWeightsRecipe({ weights, config: edited, hardware: target });
assert.equal(result.status, "unknown");
assert.ok(Math.abs(result.capacityGiB - 60) < 1e-9);
assert.deepEqual(result.parts, weights.parts);
assert.equal(result.checkpointGiB, baseline.checkpointGiB);
assert.equal(result.requiredGiB, undefined);
}
});
test("a component swap preserves other known sizes and identifies missing coverage", () => {
for (const swap of ["taeh3", "nvfp4-decoder", "nvfp4-transformer", "encoder"]) {
const edited = structuredClone(config);
if (swap === "taeh3") edited.generator.pipeline.experimental.video_decode_backend = "taeh3";
if (swap === "nvfp4-decoder") edited.generator.pipeline.experimental.h3_vae_nvfp4_decoder = "decoder.safetensors";
if (swap === "nvfp4-transformer") edited.generator.engine.quantization = { transformer_quant: "NVFP4" };
if (swap === "encoder") edited.generator.pipeline.components = { text_encoder_weights: "replacement" };
const result = assessWeightsRecipe({ weights, config: edited, hardware });
const missingRole =
swap.includes("decoder") || swap === "taeh3" ? "vae" : swap === "encoder" ? "text_encoder" : "transformer";
assert.equal(result.status, "unknown");
assert.match(result.reason, /Partial checkpoint data/);
assert.equal(result.parts[missingRole], undefined);
assert.equal(result.parts.audio_vae, weights.parts.audio_vae);
assert.equal(
result.checkpointGiB,
Object.values(result.parts).reduce((sum, size) => sum + size, 0),
);
}
});
test("host RAM is rejected and unified memory gets one budget", () => {
assert.equal(
assessWeightsRecipe({ weights, config, hardware: { ...hardware, memory_type: "system" } }).parts,
undefined,
);
const result = assessWeightsRecipe({ weights, config, hardware: { ...hardware, memory_type: "unified" } });
assert.equal(result.capacityGiB, 60);
assert.ok(result.notes.some((note) => note.includes("75%")));
});
test("the UI adapter preserves GB, GiB, usable budgets and host memory through the analytical engine", () => {
const model = buildCapacityCatalog().models.find((model) => model.id === "fastwan21");
const example = { workload: "t2v", env: { FASTVIDEO_ATTENTION_BACKEND: "TORCH_SDPA" } };
const dense = structuredClone(model.examples[0].base_config);
dense.generator.engine.compile.enabled = false;
dense.generator.pipeline.experimental = {};
for (const unit of ["GB", "GiB"]) {
const target = { ...hardware, memory_unit: unit, usable_budget_gb: 70, host_ram_gb: 128 };
const result = assessRecipe({ model, example, config: dense, hardware: target });
const raw = assessCapacity({ model, example, config: dense, hardware: capacityHardware(target) });
assert.equal(result.status, raw.status);
assert.notEqual(result.status, "unknown");
assert.equal(result.requiredGiB, raw.requiredGiB);
assert.equal(result.capacityGiB, unit === "GiB" ? 70 : 70e9 / 1024 ** 3);
const hostGiB = unit === "GiB" ? 128 : 128e9 / 1024 ** 3;
assert.ok(Math.abs(raw.host.capacityGiB - hostGiB) < 1e-9);
const { host_ram_gb: _ram, ...withoutHost } = target;
const edited = assessCapacity({
model,
example,
config: dense,
hardware: capacityHardware(withoutHost),
hostMemoryGb: hostMemoryGb(target, 128),
});
assert.ok(Math.abs(edited.host.capacityGiB - hostGiB) < 1e-9);
}
});
+64
View File
@@ -0,0 +1,64 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { readReadmeSections } from "../lib/readme.mjs";
const legacy = `# FastVideo
## NEWS
- \`2026/10/06\`: FastH3 V2 runs on a single consumer machine.
- \`2026/09/15\`: Release FastH3 8-Step V2.
## Awesome work using FastVideo or our research projects
- [Some project](https://example.com)
## Community
- Slack
`;
const rewritten = `# FastVideo
## Overview
FastVideo is a framework.
## Recent updates
- **2026-10-06:** FastH3 V2 runs on a single consumer machine.
- **2026-09-15:** Release FastH3 8-Step V2.
## Projects using FastVideo
- [Some project](https://example.com)
## Community and contributing
- Slack
`;
test("reads the legacy README headings and dates", () => {
const { news, projects } = readReadmeSections(legacy);
assert.equal(news.length, 2);
assert.deepEqual(
news.map((item) => item.date),
["2026/10/06", "2026/09/15"],
);
assert.match(news[0].text, /^FastH3 V2/);
assert.match(projects, /Some project/);
});
test("reads a rewritten README with sentence case headings and bold dates", () => {
const { news, projects } = readReadmeSections(rewritten);
assert.equal(news?.length, 2);
assert.deepEqual(
news.map((item) => item.date),
["2026/10/06", "2026/09/15"],
);
assert.doesNotMatch(news[0].text, /\*\*/);
assert.match(projects, /Some project/);
assert.doesNotMatch(projects, /Slack/);
});
test("stops at the next heading and ignores other sections", () => {
const { news } = readReadmeSections(legacy);
assert.doesNotMatch(news[1].text, /Awesome work/);
});
test("returns null when a section is missing", () => {
assert.deepEqual(readReadmeSections("# FastVideo\n\n## Overview\n"), { news: null, projects: null });
});
+77
View File
@@ -798,6 +798,39 @@
.recipe-card-grid-configurable .recipe-card {
border-block-start: 2px solid var(--blume-accent);
}
/* Family runbook cards are static: no curtain, so they need their own rhythm. */
.recipe-card-static {
display: flex;
flex-direction: column;
gap: 0.5rem;
min-block-size: 13.5rem;
padding: 1.25rem 1.25rem 1rem;
}
.recipe-card-static .recipe-card-top {
margin-block-end: 0;
padding-block-end: 0;
}
.recipe-card-static h2 {
margin: 0;
font-size: 1rem;
line-height: 1.35;
font-weight: 600;
letter-spacing: -0.01em;
}
.recipe-card-static footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
margin-block-start: auto;
margin-inline: 0;
padding-block: 0.75rem 0;
border-block-start: 1px solid var(--recipe-line);
font-size: 0.6875rem;
}
.recipe-card-static footer span:first-child {
color: var(--blume-muted-foreground);
}
.recipe-empty {
padding-block: 2rem;
text-align: center;
@@ -953,6 +986,28 @@
.recipe-facts-detail p + p {
margin-block-start: 0.375rem;
}
.recipe-facts > .matrix-note,
.recipe-memory-breakdown {
flex-basis: 100%;
}
.recipe-memory-breakdown {
color: var(--blume-muted-foreground);
font-size: 0.6875rem;
line-height: 1.65;
}
.recipe-memory-breakdown dl {
margin-block-start: 0.5rem;
max-inline-size: 26rem;
}
.recipe-memory-breakdown dl > div {
display: flex;
justify-content: space-between;
gap: 1rem;
}
.recipe-memory-breakdown dd {
margin: 0;
font-variant-numeric: tabular-nums;
}
.recipe-mode-note {
margin: -0.25rem 0 1rem;
color: var(--blume-muted-foreground);
@@ -1622,3 +1677,25 @@
.recipe-request-code summary {
padding-block: 0.25rem;
}
/* Article rhythm.
Blume's prose gives <details> no vertical margin, so consecutive cookbook
sections touch. Tables inherit the body line height, which makes dense cells
taller than the prose around them. Both are set here, once, instead of in
each page's markup. */
.prose details {
margin-block: 1rem;
}
.prose details + details {
margin-top: 1rem;
}
.prose summary {
cursor: pointer;
}
.prose table {
line-height: 1.5;
}
.prose td,
.prose th {
vertical-align: top;
}
+364 -27
View File
@@ -381,33 +381,6 @@
"expected_artifact": "Upscaled MP4 under outputs_video/kandinsky6_sr/",
"related": ["kandinsky6-vsr", "kandinsky6-ti2va-piflow"]
},
{
"id": "pan2-ti2v",
"family": "pan2",
"stage": "inference",
"task": "Text or image to video",
"label": "PAN 2",
"summary": "Generate a 13-second, 704 × 1248 video at 24 fps from text, or set IMAGE_PATH in the maintained example to condition on a first frame.",
"model": "IFM/PAN2",
"source": "examples/inference/basic/basic_pan2.py",
"serving": {
"source": "examples/serving/openai_pan2.yaml",
"install": "UV_TORCH_BACKEND=cu130 uv pip install -e .",
"task": "Text to video",
"limitations": [
"This server walkthrough sends a text prompt only. The server API also accepts an image reference (image_reference or input_reference) and then runs image-to-video from that first frame, but the checked-in clients do not upload one."
]
},
"command": "python examples/inference/basic/basic_pan2.py",
"gpu_types": ["NVIDIA"],
"hardware": {"platform": "cuda", "gpu_count": 1, "evidence": "source-configured"},
"evidence": "Source-backed",
"expected_artifact": "MP4 video under video_samples_pan2/",
"limitations": [
"The full IFM/PAN2 checkpoint is not public yet; numerical parity is checked with a public random-weight checkpoint.",
"The example keeps the 29B DiT resident in bf16 and offloads only the text encoder. GPU memory requirements have not been established for this recipe; set PAN2_NUM_GPUS to shard video tokens with sequence parallelism, which does not shard the DiT weights."
]
},
{
"id": "flux2-klein-t2i",
"family": "flux",
@@ -787,6 +760,370 @@
],
"limitations": ["Pass --reference-audio for an additional audio reference. The checked-in source defaults to four GPUs."]
},
{
"id": "pika-h3-flex-t2va",
"family": "pika_h3_flex",
"stage": "inference",
"task": "Text to video (with audio)",
"label": "Pika H3 Flex",
"summary": "Pika's post-trained MiniMax-H3: synchronized video and stereo audio from text in six forwards, at 480p or 768p, 5-15 s, in BF16 or as FastVideo's NVFP4 QAD release on Blackwell, with optional FP4 attention.",
"model": "Pika-Labs/Pika-H3-Flex",
"source": "examples/inference/basic/basic_pika_h3_flex.py",
"command": "FASTVIDEO_ATTENTION_BACKEND=FLASH_ATTN FASTVIDEO_FA4=1 python examples/inference/basic/basic_pika_h3_flex.py --task t2va --offload resident --prompt 'A paper boat drifts down a rainy city street at night; rain patters on the water.'",
"gpu_types": ["NVIDIA"],
"hardware": {
"platform": "cuda",
"gpu_count": 1,
"accelerator": "NVIDIA GB200",
"peak_memory": "About 129 GiB resident (BF16, 768p, 5 s)",
"evidence": "validated",
"evidence_url": "https://github.com/hao-ai-lab/FastVideo/pull/2038"
},
"evidence": "Verified",
"expected_artifact": "MP4 with synchronized stereo audio at outputs/pika_h3_flex/pika_h3_flex_t2va.mp4",
"modes": ["T2VA"],
"knobs": [
{
"key": "weights",
"label": "Weights",
"hint": "NVFP4 QAD needs Blackwell",
"flag": "--weights",
"options": [{"value": "bf16", "label": "BF16 (Pika release)"}, {"value": "nvfp4-qad", "label": "NVFP4 QAD"}, {"value": "nvfp4-qad-fp4-attention", "label": "NVFP4 QAD + FP4 attention"}],
"default": "bf16"
},
{
"key": "resolution",
"label": "Resolution",
"hint": "Pika resolution tier",
"flag": "--resolution",
"options": [{"value": "768p", "label": "768p"}, {"value": "480p", "label": "480p"}],
"default": "768p"
},
{
"key": "aspect",
"label": "Aspect ratio",
"hint": "Bucket within the tier",
"flag": "--aspect",
"options": ["21:9", "16:9", "3:2", "4:3", "5:4", "1:1", "4:5", "3:4", "2:3", "9:16", "9:21"],
"default": "16:9"
},
{
"key": "duration",
"label": "Duration",
"hint": "Rounded up to the 17n+5 frame grid",
"flag": "--duration",
"options": [
{"value": 5, "label": "5 s (124 frames)"},
{"value": 10, "label": "10 s (243 frames)"},
{"value": 15, "label": "15 s (362 frames)"}
],
"default": 5
},
{
"key": "nvfp4_decoder",
"label": "VAE decoder",
"hint": "NVFP4 needs Blackwell",
"flag": "--nvfp4-decoder",
"options": [
{"value": "", "label": "BF16 H3 VAE"},
{"value": "./fasth3-nvfp4-vae/quality-36L/decoder-nvfp4.safetensors", "label": "NVFP4 36L quality"},
{"value": "./fasth3-nvfp4-vae/fast-8L/decoder-nvfp4.safetensors", "label": "NVFP4 8L fast"}
],
"default": ""
},
{
"key": "nvfp4_text_encoder",
"label": "Text encoder",
"hint": "NVFP4 needs Blackwell",
"flag": "--nvfp4-text-encoder",
"options": [
{"value": "", "label": "BF16 Qwen3-VL"},
{"value": "./fasth3-nvfp4-encoder/text_encoder", "label": "NVFP4 Qwen3-VL"}
],
"default": ""
},
{
"key": "offload",
"label": "Memory",
"hint": "Resident is how the GB200 timings were measured",
"flag": "--offload",
"options": [
{"value": "resident", "label": "All resident (fastest)"},
{"value": "lazy", "label": "Lazy load + offload"}
],
"default": "resident"
},
{
"key": "num_gpus",
"label": "GPUs",
"hint": "Sequence-parallel degree",
"flag": "--num-gpus",
"options": [1, 2, 4, 8],
"default": 1
}
],
"limitations": [
"NVFP4 QAD release (--weights), one GB200, 768p 16:9, 5 s, warm: 16.8 s with BF16 attention and 15.2 s with FP4 attention; at 768p, 15 s, FP4 attention is 16% faster than BF16 attention (85.3 s vs 101.8 s).",
"Measured on one GB200 at 768p 16:9, 5 s, warm: official pika-h3-pro CLI 59.7 s; FastVideo BF16 26.2 s; with the NVFP4 quality-36L decoder 18.0 s; with the fast-8L decoder 17.4 s. The 36L decoder measured 40.5 dB PSNR against the BF16 decoder.",
"Peak resident memory was about 129 GiB for the all-BF16 run and 104.6 GiB with the NVFP4 text encoder (25.49 s) at 768p / 5 s on GB200; the official CLI's sampling peak was 71.7 GiB, a different measurement. The NVFP4 text encoder's first request in a fresh process pays a one-time cost of about 183 s.",
"--offload resident keeps every component on the GPU (104.6-108.5 GiB peak with the NVFP4 text encoder, about 129 GiB all-BF16, on GB200). --offload lazy loads components on demand for smaller devices; on GB200 it took 48.06 s for the same T2VA request.",
"FASTVIDEO_ATTENTION_BACKEND=FLASH_ATTN FASTVIDEO_FA4=1 selects FlashAttention-4 on Hopper or Blackwell; FA4 is never enabled just because it is installed. Without FA4, attention uses FA3/FA2 if installed and otherwise falls back to SDPA, which is slow and can run out of memory at 768p.",
"The NVFP4 decoder and text encoder are Blackwell-only. Download them before selecting them: hf download FastVideo/FastH3-NVFP4-VAE-Decoder --include 'quality-36L/decoder-nvfp4.safetensors' 'fast-8L/decoder-nvfp4.safetensors' --local-dir fasth3-nvfp4-vae; hf download FastVideo/FastVideo-FastH3-8-Step-V2-NVFP4-Consumer --include 'text_encoder/*' --local-dir fasth3-nvfp4-encoder. The 8L decoder is a distilled student; validate it on your prompts.",
"The six-forward re-noise ladder is part of the adapter contract; the example exposes no step-count flag."
],
"related": ["pika-h3-flex-keyframes", "pika-h3-flex-ref2va"]
},
{
"id": "pika-h3-flex-keyframes",
"family": "pika_h3_flex",
"stage": "inference",
"task": "First/last frame to video (with audio)",
"label": "Pika H3 Flex keyframes",
"summary": "Animate from a first frame (I2VA), toward a last frame (L2VA), or between both (FL2VA). Pika center-crops each keyframe to the selected bucket.",
"model": "Pika-Labs/Pika-H3-Flex",
"source": "examples/inference/basic/basic_pika_h3_flex.py",
"command": "FASTVIDEO_ATTENTION_BACKEND=FLASH_ATTN FASTVIDEO_FA4=1 python examples/inference/basic/basic_pika_h3_flex.py --task fl2va --offload resident --image assets/images/natural.jpg --last-image assets/images/peninsula.jpg --prompt 'The camera drifts from the misty mountain ridge down to the coastal cliffs above the sea.'",
"gpu_types": ["NVIDIA"],
"hardware": {
"platform": "cuda",
"gpu_count": 1,
"accelerator": "NVIDIA GB200",
"peak_memory": "105.2 GiB FL2VA, 104.9 GiB I2VA (NVFP4 text encoder, 768p, 5 s)",
"evidence": "validated",
"evidence_url": "https://github.com/hao-ai-lab/FastVideo/pull/2038"
},
"evidence": "Verified",
"expected_artifact": "MP4 with synchronized stereo audio at outputs/pika_h3_flex/pika_h3_flex_<task>.mp4",
"modes": ["I2VA", "L2VA", "FL2VA"],
"knobs": [
{
"key": "weights",
"label": "Weights",
"hint": "NVFP4 QAD needs Blackwell",
"flag": "--weights",
"options": [{"value": "bf16", "label": "BF16 (Pika release)"}, {"value": "nvfp4-qad", "label": "NVFP4 QAD"}, {"value": "nvfp4-qad-fp4-attention", "label": "NVFP4 QAD + FP4 attention"}],
"default": "bf16"
},
{
"key": "task",
"label": "Task",
"hint": "Which keyframes condition the clip",
"flag": "--task",
"options": [
{"value": "i2va", "label": "First frame (I2VA)"},
{"value": "l2va", "label": "Last frame (L2VA)"},
{"value": "fl2va", "label": "First and last (FL2VA)"}
],
"default": "fl2va"
},
{
"key": "resolution",
"label": "Resolution",
"hint": "Pika resolution tier",
"flag": "--resolution",
"options": [{"value": "768p", "label": "768p"}, {"value": "480p", "label": "480p"}],
"default": "768p"
},
{
"key": "aspect",
"label": "Aspect ratio",
"hint": "Bucket within the tier",
"flag": "--aspect",
"options": ["21:9", "16:9", "3:2", "4:3", "5:4", "1:1", "4:5", "3:4", "2:3", "9:16", "9:21"],
"default": "16:9"
},
{
"key": "duration",
"label": "Duration",
"hint": "Rounded up to the 17n+5 frame grid",
"flag": "--duration",
"options": [
{"value": 5, "label": "5 s (124 frames)"},
{"value": 10, "label": "10 s (243 frames)"},
{"value": 15, "label": "15 s (362 frames)"}
],
"default": 5
},
{
"key": "nvfp4_decoder",
"label": "VAE decoder",
"hint": "NVFP4 needs Blackwell",
"flag": "--nvfp4-decoder",
"options": [
{"value": "", "label": "BF16 H3 VAE"},
{"value": "./fasth3-nvfp4-vae/quality-36L/decoder-nvfp4.safetensors", "label": "NVFP4 36L quality"},
{"value": "./fasth3-nvfp4-vae/fast-8L/decoder-nvfp4.safetensors", "label": "NVFP4 8L fast"}
],
"default": ""
},
{
"key": "nvfp4_text_encoder",
"label": "Text encoder",
"hint": "NVFP4 needs Blackwell",
"flag": "--nvfp4-text-encoder",
"options": [
{"value": "", "label": "BF16 Qwen3-VL"},
{"value": "./fasth3-nvfp4-encoder/text_encoder", "label": "NVFP4 Qwen3-VL"}
],
"default": ""
},
{
"key": "offload",
"label": "Memory",
"hint": "Resident is how the GB200 timings were measured",
"flag": "--offload",
"options": [
{"value": "resident", "label": "All resident (fastest)"},
{"value": "lazy", "label": "Lazy load + offload"}
],
"default": "resident"
},
{
"key": "num_gpus",
"label": "GPUs",
"hint": "Sequence-parallel degree",
"flag": "--num-gpus",
"options": [1, 2, 4, 8],
"default": 1
}
],
"limitations": [
"Measured on one GB200 at 768p, 5 s, seed 42, NVFP4 text encoder, all resident: FL2VA 28.95 s (first frame 39.6 dB / SSIM 0.971 and last frame 29.1 dB / 0.819 against the inputs); I2VA 27.37 s (first frame 26.5 dB / 0.715). L2VA was not measured separately.",
"The command passes both keyframes; the example uses only the ones the selected task needs. Replace them with your own images.",
"--offload resident keeps every component on the GPU (104.6-108.5 GiB peak with the NVFP4 text encoder, about 129 GiB all-BF16, on GB200). --offload lazy loads components on demand for smaller devices; on GB200 it took 48.06 s for the same T2VA request.",
"FASTVIDEO_ATTENTION_BACKEND=FLASH_ATTN FASTVIDEO_FA4=1 selects FlashAttention-4 on Hopper or Blackwell; FA4 is never enabled just because it is installed. Without FA4, attention uses FA3/FA2 if installed and otherwise falls back to SDPA, which is slow and can run out of memory at 768p.",
"The NVFP4 decoder and text encoder are Blackwell-only. Download them before selecting them: hf download FastVideo/FastH3-NVFP4-VAE-Decoder --include 'quality-36L/decoder-nvfp4.safetensors' 'fast-8L/decoder-nvfp4.safetensors' --local-dir fasth3-nvfp4-vae; hf download FastVideo/FastVideo-FastH3-8-Step-V2-NVFP4-Consumer --include 'text_encoder/*' --local-dir fasth3-nvfp4-encoder. The 8L decoder is a distilled student; validate it on your prompts."
],
"related": ["pika-h3-flex-t2va", "pika-h3-flex-ref2va"]
},
{
"id": "pika-h3-flex-ref2va",
"family": "pika_h3_flex",
"stage": "inference",
"task": "Reference media to video (with audio)",
"label": "Pika H3 Flex references",
"summary": "Generate from ordered image, video, and audio references through Pika's ref2va adapter: up to 9 images, 3 videos, and 3 audio clips.",
"model": "Pika-Labs/Pika-H3-Flex",
"source": "examples/inference/basic/basic_pika_h3_flex.py",
"command": "FASTVIDEO_ATTENTION_BACKEND=FLASH_ATTN FASTVIDEO_FA4=1 python examples/inference/basic/basic_pika_h3_flex.py --task ref2va --offload resident --reference image:assets/images/natural.jpg --reference image:assets/images/bus_terminal.jpg --prompt 'Start on the misty mountain ridge from the first reference, then cut to the night bus terminal from the second.'",
"gpu_types": ["NVIDIA"],
"hardware": {
"platform": "cuda",
"gpu_count": 1,
"accelerator": "NVIDIA GB200",
"peak_memory": "108.5 GiB with two image references (NVFP4 text encoder, 768p, 5 s)",
"evidence": "validated",
"evidence_url": "https://github.com/hao-ai-lab/FastVideo/pull/2038"
},
"evidence": "Verified",
"expected_artifact": "MP4 with synchronized stereo audio at outputs/pika_h3_flex/pika_h3_flex_ref2va.mp4",
"modes": ["Ref2VA"],
"knobs": [
{
"key": "weights",
"label": "Weights",
"hint": "NVFP4 QAD needs Blackwell",
"flag": "--weights",
"options": [{"value": "bf16", "label": "BF16 (Pika release)"}, {"value": "nvfp4-qad", "label": "NVFP4 QAD"}, {"value": "nvfp4-qad-fp4-attention", "label": "NVFP4 QAD + FP4 attention"}],
"default": "bf16"
},
{
"key": "resolution",
"label": "Resolution",
"hint": "Pika resolution tier",
"flag": "--resolution",
"options": [{"value": "768p", "label": "768p"}, {"value": "480p", "label": "480p"}],
"default": "768p"
},
{
"key": "aspect",
"label": "Aspect ratio",
"hint": "Bucket within the tier",
"flag": "--aspect",
"options": ["21:9", "16:9", "3:2", "4:3", "5:4", "1:1", "4:5", "3:4", "2:3", "9:16", "9:21"],
"default": "16:9"
},
{
"key": "duration",
"label": "Duration",
"hint": "Rounded up to the 17n+5 frame grid",
"flag": "--duration",
"options": [
{"value": 5, "label": "5 s (124 frames)"},
{"value": 10, "label": "10 s (243 frames)"},
{"value": 15, "label": "15 s (362 frames)"}
],
"default": 5
},
{
"key": "nvfp4_decoder",
"label": "VAE decoder",
"hint": "NVFP4 needs Blackwell",
"flag": "--nvfp4-decoder",
"options": [
{"value": "", "label": "BF16 H3 VAE"},
{"value": "./fasth3-nvfp4-vae/quality-36L/decoder-nvfp4.safetensors", "label": "NVFP4 36L quality"},
{"value": "./fasth3-nvfp4-vae/fast-8L/decoder-nvfp4.safetensors", "label": "NVFP4 8L fast"}
],
"default": ""
},
{
"key": "nvfp4_text_encoder",
"label": "Text encoder",
"hint": "NVFP4 needs Blackwell",
"flag": "--nvfp4-text-encoder",
"options": [
{"value": "", "label": "BF16 Qwen3-VL"},
{"value": "./fasth3-nvfp4-encoder/text_encoder", "label": "NVFP4 Qwen3-VL"}
],
"default": ""
},
{
"key": "offload",
"label": "Memory",
"hint": "Resident is how the GB200 timings were measured",
"flag": "--offload",
"options": [
{"value": "resident", "label": "All resident (fastest)"},
{"value": "lazy", "label": "Lazy load + offload"}
],
"default": "resident"
},
{
"key": "num_gpus",
"label": "GPUs",
"hint": "Sequence-parallel degree",
"flag": "--num-gpus",
"options": [1, 2, 4, 8],
"default": 1
}
],
"limitations": [
"Measured on one GB200 at 768p, 5 s, seed 42, NVFP4 text encoder, all resident, two image references: 51.17 s (38.42 s denoise, 64,241 packed tokens); both references appear in the output.",
"Repeat --reference image:PATH, video:PATH, or audio:PATH in order. At least one image or video is required; a video reference needs at least 22 frames at 24 fps.",
"Ref2VA loads transformer_ref and the ref2va adapter, a separate 61.7 GiB trunk download. Reference images are encoded at up to a 1024 (480p) or 2048 (768p) short edge, so conditioning memory is higher than T2VA's.",
"--offload resident keeps every component on the GPU (104.6-108.5 GiB peak with the NVFP4 text encoder, about 129 GiB all-BF16, on GB200). --offload lazy loads components on demand for smaller devices; on GB200 it took 48.06 s for the same T2VA request.",
"FASTVIDEO_ATTENTION_BACKEND=FLASH_ATTN FASTVIDEO_FA4=1 selects FlashAttention-4 on Hopper or Blackwell; FA4 is never enabled just because it is installed. Without FA4, attention uses FA3/FA2 if installed and otherwise falls back to SDPA, which is slow and can run out of memory at 768p.",
"The NVFP4 decoder and text encoder are Blackwell-only. Download them before selecting them: hf download FastVideo/FastH3-NVFP4-VAE-Decoder --include 'quality-36L/decoder-nvfp4.safetensors' 'fast-8L/decoder-nvfp4.safetensors' --local-dir fasth3-nvfp4-vae; hf download FastVideo/FastVideo-FastH3-8-Step-V2-NVFP4-Consumer --include 'text_encoder/*' --local-dir fasth3-nvfp4-encoder. The 8L decoder is a distilled student; validate it on your prompts."
],
"related": ["pika-h3-flex-t2va", "pika-h3-flex-keyframes"]
},
{
"id": "open-h3-regenerate-2k",
"family": "minimax_h3",
"stage": "inference",
"task": "Video super-resolution (2K regeneration)",
"label": "Open-H3-Regenerate-2K",
"summary": "Regenerate a 768P MiniMax-H3 draft at 2560x1440 with NVlabs' three-step refiner. The output is silent.",
"model": "Efficient-Large-Model/Open-H3-Regenerate-2K",
"source": "examples/inference/basic/basic_open_h3_regenerate_2k.py",
"command": ": \"${INPUT_VIDEO:?Set INPUT_VIDEO to a 768P MiniMax-H3 draft}\"\npython examples/inference/basic/basic_open_h3_regenerate_2k.py --video \"$INPUT_VIDEO\" --prompt \"A red fox trots through fresh snow in a pine forest at golden hour.\"",
"gpu_types": ["NVIDIA"],
"hardware": {"platform": "cuda", "gpu_count": 1, "evidence": "source-configured"},
"evidence": "Source-backed",
"expected_artifact": "2560x1440 MP4 at outputs/open_h3_regenerate_2k/regen2k_0.mp4",
"related": ["minimax-h3-t2v", "fasth3-8step-v2-cuda"]
},
{
"id": "fasth3-lora-preview",
"family": "minimax_h3",
+92
View File
@@ -0,0 +1,92 @@
{
"generated": "2026-10-28",
"note": "Measured from Hugging Face safetensors files. Sizes are GiB at the checkpoint's own precision.",
"models": {
"fasth3-8step": {
"parts": {
"transformer": 65.29,
"text_encoder": 62.13,
"vae": 9.7,
"audio_vae": 0.56
},
"alternates": {
"transformer_nvfp4": 36.91,
"transformer_fp8": 47.35,
"vae_nvfp4_quality": 5.82,
"vae_nvfp4_fast": 1.33,
"text_encoder_quantized": 15.33
},
"sources": [
"https://huggingface.co/FastVideo/FastVideo-FastH3-8-Step-V2/tree/main"
]
},
"fasth3-4step": {
"parts": {
"transformer": 65.29,
"text_encoder": 62.13,
"vae": 9.7,
"audio_vae": 0.56
},
"alternates": {
"transformer_nvfp4": 36.91,
"transformer_fp8": 47.35,
"vae_nvfp4_quality": 5.82,
"vae_nvfp4_fast": 1.33,
"text_encoder_quantized": 15.33
},
"sources": [
"https://huggingface.co/FastVideo/FastVideo-FastH3-4-step-Preview-v1-VSA-DataFree/tree/main"
]
},
"fasth3-trim": {
"parts": {
"transformer": 34.94,
"text_encoder": 15.33,
"vae": 5.93,
"audio_vae": 0.56
},
"alternates": {
"transformer_nvfp4": 11.1,
"transformer_fp8": 19.88,
"vae_nvfp4_quality": 5.82,
"vae_nvfp4_fast": 1.33
},
"sources": [
"https://huggingface.co/FastVideo/FastVideo-FastH3-Trim-8-Step/tree/main"
]
},
"fasth3-omniref": {
"parts": {
"transformer": 65.33,
"text_encoder": 15.33,
"vae": 9.7,
"audio_vae": 0.56
},
"alternates": {
"transformer_nvfp4": 36.95,
"transformer_fp8": 47.39,
"vae_nvfp4_quality": 5.82,
"vae_nvfp4_fast": 1.33
},
"sources": [
"https://huggingface.co/FastVideo/FastVideo-FastH3-Omni-8-Step-V1/tree/main",
"https://huggingface.co/FastVideo/FastH3-Omni-NVFP4-QAD/tree/main"
]
},
"pika-h3-flex-t2va": {
"parts": {
"transformer": 61.73,
"adapter": 2.58,
"text_encoder": 62.13,
"vae": 9.7,
"audio_vae": 0.56
},
"alternates": {
"vae_nvfp4_quality": 5.82,
"vae_nvfp4_fast": 1.33,
"text_encoder_quantized": 15.33
},
"sources": ["https://huggingface.co/Pika-Labs/Pika-H3-Flex/tree/main"]
}
}
}
+1 -1
View File
@@ -13,9 +13,9 @@ plain typographic tile instead — that tile is a UI placeholder, not a logo.
| `nvidia.webp` | [Official NVIDIA Hugging Face organization avatar](https://huggingface.co/nvidia) | Cosmos and GEN3C cards, Cosmos page header |
| `kandinsky.webp` | [Official Kandinsky Lab Hugging Face organization avatar](https://huggingface.co/kandinskylab) | Kandinsky 5 and Kandinsky 6 cards and page headers |
| `black-forest-labs.webp` | [Official Black Forest Labs Hugging Face organization avatar](https://huggingface.co/black-forest-labs) | FLUX family card and page header |
| `ifm.webp` | [Official Institute of Foundation Models (IFM) Hugging Face organization avatar](https://huggingface.co/IFM) | PAN 2 family card and page header |
| `qwen.webp` | [Official Qwen Hugging Face organization avatar](https://huggingface.co/Qwen) | Qwen-Image-2.1 family card and page header |
| `minimax.webp` | [Official MiniMax Hugging Face organization avatar](https://huggingface.co/MiniMaxAI) | MiniMax H3 family card and page header |
| `pika.webp` | [Official Pika-Labs Hugging Face organization avatar](https://huggingface.co/Pika-Labs) | Pika H3 Flex card and page header |
| `stabilityai.webp` | [Official Stability AI Hugging Face organization avatar](https://huggingface.co/stabilityai) | Stable Diffusion and Stable Audio cards and page headers |
| `fastvideo.webp` | [Official FastVideo Hugging Face organization avatar](https://huggingface.co/FastVideo) | Matrix Game and MMAudio cards (converted weights published by this org) |
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

+13 -6
View File
@@ -157,15 +157,22 @@ __all__ = [
## 4. Register the Backend
Update `fastvideo/attention/backends/__init__.py` to export your new class.
Registration is two edits:
```python
from .mynew_attn import MyNewAttnBackend
```
1. Add a member to `AttentionBackendEnum` in `fastvideo/platforms/interface.py`.
2. Add the selection branch in `fastvideo/platforms/cuda.py`, next to the
existing backends, so `--attention-backend` maps the enum to your class.
## 5. Platform Integration
`fastvideo/attention/backends/__init__.py` is empty and does not register
anything.
If your backend requires specific platform checks (e.g., checking for H100 support), handle that in `fastvideo/platforms/cuda.py` or within your backend's `__init__`.
## 5. Platform integration
If your backend needs platform checks, such as an H100 guard, put them in
`fastvideo/platforms/cuda.py` or in the backend constructor.
The maintained version of this guide is
[Add an attention backend](../../contributing/attention_backend.md).
## 6. Add Documentation
+4 -4
View File
@@ -85,10 +85,10 @@ from fastvideo_kernel import video_sparse_attn
# topk: Number of blocks to attend
output = video_sparse_attn(
q, k, v,
block_sizes,
block_sizes,
topk=32
q, k, v,
variable_block_sizes,
q_variable_block_sizes,
topk=32,
)
```
+37 -1
View File
@@ -49,7 +49,7 @@ node docs/js/build-cookbook-capacity.mjs --check
node docs/js/build-cookbook-capacity.mjs --preview fastwan21
```
The check currently prints `Validated 1 capacity model(s) and 50 hardware variant(s).`
The check currently prints `Validated 1 capacity model(s) and 52 hardware variant(s).`
The preview prints the model with component references expanded and each
example's baseline under `base_config`. Inspect its identity, counts, geometry,
and evidence. Both commands validate deployments and capacity metadata without
@@ -404,6 +404,42 @@ matching metric, scope, phases, and execution context. One point can calibrate a
existing scaling assumption, not establish accuracy across modes. Missing startup
evidence leaves startup coverage unknown. Keep observations intact when deriving coefficients.
## FastH3 coverage
No FastH3 checkpoint has a full capacity model yet. The editor shows a
checkpoint-size breakdown and device budget, with `status: unknown` for runtime
fit. It does not subtract CPU-offloaded weights or divide file sizes by GPU
count. Those operations need phase residency and per-rank placement models.
Replacement components without matching data are omitted from the known-size
subtotal and named as missing coverage. Host-only system RAM cannot establish
GPU fit. A custom GiB budget keeps its unit; unified memory uses the shared
normalizer's usable-memory policy instead of treating nominal RAM as VRAM.
| Profile | First stop | Why |
| --- | --- | --- |
| OmniRef | Workload | Runs image-to-video; the estimator models text-to-video only |
| 8-Step CUDA | Topology | Defaults to four GPUs and Ulysses sequence parallelism |
| Trim Spark | Attention | `VIDEO_SPARSE_ATTN_H3` is not a dense backend, so VSA workspace is unmodeled |
| 4-Step Spark | Execution | `h3_sequential_load` loads components on demand |
| Omni NVFP4 decoder | Precision | The decoder swap changes decoder precision |
To make a FastH3 estimate possible, in order:
1. Add capacity entries for each checkpoint: transformer parameter counts and
geometry, the text encoder, the VAE, and the NVFP4 decoder variants.
2. Model an image-to-video request: reference latents and audio tokens change
the working set, and OmniRef passes ordered image, video, and audio inputs.
3. Model per-rank sharding for four-GPU Ulysses, including replicated weights
and per-rank sequence slices, so the estimate compares against one device.
4. Add a VSA-H3 workspace term, or a dense upper bound that is labeled as one.
5. Support quantized decoder precision instead of rejecting the configuration.
6. Record successful runs on the RTX 5090, RTX PRO 6000, and DGX Spark profiles
so the analytical estimate can be calibrated rather than trusted blindly.
Until those land, the cookbook can report checkpoint data but cannot answer
whether an edited FastH3 request fits a GPU.
## Static consumers
[`cookbook-capacity.mjs`](../js/cookbook-capacity.mjs) exposes these async loaders,
+16 -5
View File
@@ -13,7 +13,9 @@ test in the same pull request.
them directly with `os.environ.get("NAME")`, and the name must be in the external-variable allowlist
(`EXTERNAL_ALLOWLIST` in the contract test). When FastVideo sets such a variable for the other tool, it calls
`envs.set_external`, `envs.setdefault_external`, or `envs.unset_external`, and the name must be in
`EXTERNAL_WRITE_ALLOWLIST`. Variables that FastVideo's CI and CI tooling define (for example `TEST_SCOPE` and
`EXTERNAL_WRITE_ALLOWLIST`. Code that needs every variable of a family that another tool owns, such as FA4's
`FA4_*` tuning knobs, calls `envs.external_with_prefixes("FA4_")` with literal prefixes that the allowlist
covers. Variables that FastVideo's CI and CI tooling define (for example `TEST_SCOPE` and
`PERF_RUN_SOURCE`) keep their names, and test code under `fastvideo/tests/` reads them directly; they are listed
in `CI_ONLY_VARIABLES` in the contract test, together with the file that sets each one.
2. **Read with `envs.NAME.get()`, write with `envs.NAME.set()`, and change a value in tests with
@@ -177,6 +179,7 @@ longer exists also fails the test, so the fixing pull request deletes its entry.
| `FASTVIDEO_CACHE_ROOT` | path | computed | path | Root directory for FastVideo cache files. Defaults to ~/.cache/fastvideo, or $XDG_CACHE_HOME/fastvideo when XDG_CACHE_HOME is set. |
| `FASTVIDEO_REASON1_WEIGHTS_PATH` | str | unset | path | Local path or Hugging Face id of Reason1 weights to load instead of the checkpoint's own. |
| `FASTVIDEO_HOST_IP` | str | `""` | distributed | IP address of this node when the node has several network interfaces. Set it on each node for multi-node inference. |
| `FASTVIDEO_PG_TIMEOUT_MIN` | int | `0` | distributed | Timeout in minutes for the training process groups' collectives (0 keeps PyTorch's 10-minute default). Raise it for large models whose rank-0 load and scatter, checkpoint saves or long evaluations outlast the default. |
| `FASTVIDEO_LOOPBACK_IP` | str | `""` | distributed | Loopback IP address to use instead of the detected one. |
| `FASTVIDEO_RAY_PER_WORKER_GPUS` | float | `1.0` | distributed | GPUs per Ray worker. A fraction lets Ray schedule several actors on one GPU, so other actors can share the GPUs with FastVideo. |
| `FASTVIDEO_NCCL_SO_PATH` | str | unset | distributed | Path to the NCCL library file. Needed because the nccl&gt;=2.19 that PyTorch ships has a bug (https://github.com/NVIDIA/nccl/issues/1234). |
@@ -194,9 +197,13 @@ longer exists also fails the test, so the fixing pull request deletes its entry.
| `FASTVIDEO_ATTENTION_BACKEND` | str | unset | attention | Attention backend, as an AttentionBackendEnum name such as TORCH_SDPA, FLASH_ATTN, VIDEO_SPARSE_ATTN, SAGE_ATTN, or SAGE_ATTN_THREE. FastVideoArgs uses it when FastVideoArgs.attention_backend is unset. |
| `FASTVIDEO_FA4` | bool | `0` | attention | The FLASH_ATTN backend uses FlashAttention-4 (flash_attn.cute) instead of FA3 or FA2. |
| `FASTVIDEO_MINIMAX_H3_FA4_PACKED_VARLEN` | bool | `0` | attention | MiniMax-H3 dense DiT self-attention uses the FlashAttention-4 packed-varlen entry point. This changes the floating-point reduction order, so it is an inference-only opt-in. |
| `FASTVIDEO_MINIMAX_H3_DENSE_ATTN` | one of flash, cudnn | `flash` | attention | Kernel for MiniMax-H3 dense DiT self-attention: flash keeps the selected FlashAttention backend; with FLASH_ATTN, cudnn sends the no-mask, no-grad dense call through cuDNN's fused SDPA. cuDNN has a different reduction order, so this is an inference-only opt-in. |
| `FASTVIDEO_VSA_PTX_BLACKWELL` | bool | `0` | attention | VIDEO_SPARSE_ATTN_H3 sends tile-64 forwards to the data-center Blackwell (sm_100a / sm_103a) kernel; grad-tracking forwards need its autograd operator, else Triton. fastvideo-kernel reads the same variable with the same rule. Deprecated names: `FASTVIDEO_VSA_SM100A`. |
| `FASTVIDEO_VSA_TRITON` | bool | `0` | attention | Force the Triton MiniMax-H3 sparse attention kernel. fastvideo-kernel reads the same variable. |
| `FASTVIDEO_NVFP4_FA4` | bool | `0` | attention | FlashAttention-4 quantizes Q and K to NVFP4. An explicit nvfp4_fa4 attention implementation argument takes precedence. |
| `FASTVIDEO_NVFP4_FA4_QK_TRANSFORM` | one of none, smooth_k, hadamard, smooth_k+hadamard | `none` | attention | Exact Q/K preconditioning applied right before NVFP4 quantization on the FlashAttention-4 NVFP4 path: smooth_k subtracts each head's token-mean of K, hadamard rotates Q and K by the head-dim Hadamard matrix. Must match the qk_transform the model was QAD-trained with. An explicit nvfp4_qk_transform attention implementation argument takes precedence. |
| `FASTVIDEO_NVFP4_FA4_FUSED_QK_QUANT` | bool | `1` | attention | With FASTVIDEO_NVFP4_FA4_QK_TRANSFORM other than none, apply the transform and the NVFP4 quantization in one fused Triton kernel per tensor (same FP4 codes and scale factors as the unfused path). Set it to 0 to use the unfused PyTorch transform and flashinfer quantization. |
| `FASTVIDEO_NVFP4_FA4_BF16_BLOCKS` | str | `""` | attention | Comma-separated MiniMax-H3 transformer_blocks indices whose attention stays in BF16 while FASTVIDEO_NVFP4_FA4 quantizes the rest, for example 45,49. |
| `FASTVIDEO_FA4_PV_MODE` | one of bf16, fp8 | `bf16` | attention | V dtype on the NVFP4 FlashAttention-4 path (FLASH_ATTN with nvfp4_fa4, ATTN_QAT_INFER on sm_100a/sm_103a): bf16 keeps V in BF16; fp8 casts V to float8 e4m3 without scaling. An explicit fa4_pv_mode attention implementation argument takes precedence. |
| `FASTVIDEO_DISABLE_ATTENTION_COMPILE` | bool | `1` | attention | Keep attention forward out of torch.compile graphs (torch.compiler.disable). Set it to 0 to let attention constructed under that setting be traced. Setting it explicitly to true also blocks regional compile. |
| `FASTVIDEO_MLX_WINDOW` | int | `0` | attention | MLX FastWan windowed attention size in tokens. 0 uses full attention. |
@@ -208,7 +215,7 @@ longer exists also fails the test, so the fixing pull request deletes its entry.
| `FASTVIDEO_VAE_PARALLEL_DECODE_STRATEGY` | str | unset | performance | Collective that moves chunks in parallel VAE decode: gather (used when unset) or all_gather. |
| `FASTVIDEO_H3_ENCODER_SPLIT` | bool | `0` | distributed | MiniMax-H3 runs the Qwen3-VL text encoder on a dedicated group of nodes, so the denoising ranks never load it. Same as FastVideoArgs.h3_encoder_split=True. |
| `FASTVIDEO_H3_ENCODER_NODES` | int | `1` | distributed | Number of leading nodes that FASTVIDEO_H3_ENCODER_SPLIT dedicates to the MiniMax-H3 text encoder. Values below 1 count as 1. |
| `FASTVIDEO_MINIMAX_H3_FUSIONS` | str | `""` | performance | MiniMax-H3 inference-only Triton fusions: all, 1, or a comma-separated subset of modulate,qknorm_rope,swiglu. Empty, 0, or none keeps the eager implementation. |
| `FASTVIDEO_MINIMAX_H3_FUSIONS` | str | `""` | performance | MiniMax-H3 inference-only Triton fusions: all, 1, or a comma-separated subset of modulate,qknorm_rope,swiglu. The whole-tier value mk selects the MotionKernel kernels instead of the Sol-Engine ones. Empty, 0, or none keeps the eager implementation. |
| `FASTVIDEO_MINIMAX_H3_EXACT_KERNELS` | str | `""` | performance | MiniMax-H3 inference-only Triton kernels that reproduce the eager output bit for bit: all, 1, or a comma-separated subset of rope,modulate,swiglu. An op whose FASTVIDEO_MINIMAX_H3_FUSIONS fusion is on keeps the fusion. Empty, 0, or none keeps the eager implementation. |
| `FASTVIDEO_FSDP2_AUTOWRAP` | bool | `0` | performance | FSDP2 shards modules by parameter count instead of the model's shard conditions. Not supported by self-forcing distillation. |
| `FASTVIDEO_FSDP2_MIN_PARAMS` | int | `10000000` | performance | Minimum parameter count of a module that FASTVIDEO_FSDP2_AUTOWRAP shards. |
@@ -267,7 +274,7 @@ longer exists also fails the test, so the fixing pull request deletes its entry.
| `FASTVIDEO_H3_FP8_ATTENTION` | bool | `0` | performance | With NVFP4 layer_profile h3_dit_ffn, run MiniMax-H3 attention projections in FP8. |
| `FASTVIDEO_H3_FP8_GRANULARITY` | one of tensor, channel | `tensor` | performance | FP8 scaling granularity for FASTVIDEO_H3_FP8_ATTENTION. |
| `FASTVIDEO_LORA_SIDE_BRANCH` | bool | `0` | performance | Apply inference LoRA adapters as a bf16 side branch next to every target linear instead of merging them, so the adapter strength can change at runtime. Packed NVFP4 or FP8 linears without their original dense weight always use the side branch. |
| `FASTVIDEO_NVFP4_MM_BACKEND` | str | `auto` | performance | FlashInfer mm_fp4 backend for NVFP4 linears, e.g. auto or cutlass. |
| `FASTVIDEO_NVFP4_MM_BACKEND` | str | `auto` | performance | FlashInfer mm_fp4 backend for NVFP4 linears: auto (cudnn for MiniMax-H3 DiT profiles on sm_100), cutlass or cudnn. |
| `FASTVIDEO_NVFP4_MM_ROW_SPLIT` | one of off, table, auto | `off` | performance | Run large cuDNN NVFP4 GEMMs over row chunks where its plan heuristic is slow: off, table (fixed per-shape chunk counts) or auto (time 1/2/4 chunks once per shape; not deterministic across runs). |
| `FASTVIDEO_NVFP4_ACT_AMAX` | path | unset | performance | JSON of calibrated NVFP4 input amax per linear, keyed b&lt;block&gt;.&lt;sub&gt; or full prefix; sets a static activation scale. |
| `FASTVIDEO_NVFP4_DYNAMIC_ACT` | str | `""` | performance | NVFP4 linears that derive the activation scale per call: all, or comma-separated layer-name suffixes such as ff.fc_out. |
@@ -278,15 +285,19 @@ longer exists also fails the test, so the fixing pull request deletes its entry.
| `FASTVIDEO_H3_SPLICE_FROM_STEP` | int | `4` | eval | First denoising step run by FASTVIDEO_H3_SPLICE_TRANSFORMER. |
| `FASTVIDEO_H3_ENCODER_LAYERWISE` | bool | `0` | performance | Stream MiniMax-H3 text-encoder language layers through exact-size pinned host memory (text-only prompts). |
| `FASTVIDEO_H3_ENCODER_FUSED_DEQUANT` | bool | `0` | performance | Expand the serialized NVFP4 MiniMax-H3 text encoder with one fused Triton pass on GPUs without FP4 GEMM. |
| `FASTVIDEO_H3_VAE_TILE_BATCH` | int | `1` | performance | Spatial tiles per MiniMax-H3 video VAE decoder call; 1 decodes per tile. |
| `FASTVIDEO_H3_VAE_TILE_BATCH` | int | `8` | performance | Spatial tiles per MiniMax-H3 video VAE decoder call. The ViT decoder treats batch entries independently, so batching tiles matches per-tile decoding, except for NVFP4 decoders with dynamic activation scales, which always decode per tile. On GB200 at 768x1344, 8 tiles per call decodes 107 frames in 3.7 s instead of 5.4 s and 362 frames in 12.8 s instead of 18.3 s (+0.7 GiB peak). 1 decodes per tile. |
| `FASTVIDEO_H3_VAE_FUSED_QK_NORM_ROPE` | bool | `0` | performance | Fuse the MiniMax-H3 video VAE decoder per-head QK RMSNorm and partial RoPE into one Triton pass per attention layer (same fp32/inference-dtype arithmetic; the RMSNorm reduction order differs, like the DiT FASTVIDEO_MINIMAX_H3_FUSIONS qknorm_rope). |
| `FASTVIDEO_H3_REF2VA_MEMO_ENTRIES` | int | `0` | performance | Entries in each content-keyed MiniMax-H3 memo of reference encodes (Qwen3-VL Ref2VA presentation, VAE keyframe latents); repeat references are reused exactly. 0 disables. |
| `FASTVIDEO_H3_VAE_TILE_PARALLEL` | bool | `0` | performance | Split MiniMax-H3 VAE spatial tiles (not only temporal chunks) across the sequence-parallel ranks in the parallel decode and keyframe encode; bitwise equal to the serial path. |
| `FASTVIDEO_H3_VAE_TILE_PARALLEL` | bool | `0` | performance | Split MiniMax-H3 VAE spatial tiles (not only temporal chunks) across the sequence-parallel ranks in the parallel decode and keyframe encode; bitwise equal to the serial path. Requires FASTVIDEO_H3_VAE_TILE_BATCH=1 (the split replays the per-tile path). |
| `FASTVIDEO_H3_VAE_NVFP4_FUSED` | bool | `1` | performance | Run NVFP4 MiniMax-H3 VAE decoder blocks through the bit-exact fused Triton kernels at inference. |
| `FASTVIDEO_H3_VAE_NVFP4_CUDA_GRAPH` | bool | `1` | performance | Replay small fused NVFP4 MiniMax-H3 VAE decoder calls (at most four 1797-token tiles) as per-shape CUDA graphs. |
| `FASTVIDEO_H3_VAE_NVFP4_DECODER` | path | unset | performance | Exported NVFP4 MiniMax-H3 video decoder (.pt, or .safetensors with bf16 masters or pre-packed NVFP4 weights) that replaces the VAE decoder at load; skips the INT8 ConvRot overlay. |
| `FASTVIDEO_H3_VAE_INT8_OVERLAY` | bool | `1` | performance | Apply the INT8 ConvRot decoder overlay when the MiniMax-H3 VAE folder ships one; 0 decodes with the dense weights. |
| `FASTVIDEO_H3_VAE_INT8_SHARED_QKV` | bool | `0` | performance | Share the INT8 activation rotation and quantization across the MiniMax-H3 VAE Q/K/V projections. |
| `FASTVIDEO_H3_VAE_INT8_TRANSPOSE_VIEW` | bool | `0` | performance | Use transposed weight views in the MiniMax-H3 VAE INT8 projections. |
| `FASTVIDEO_H3_VAE_COMPILE_BLOCKS` | bool | `0` | performance | Run the MiniMax-H3 video VAE decoder transformer blocks through one regional torch.compile graph: Inductor fuses the fp32 residual scale/add pairs, the RMSNorm+cast and the SwiGLU silu\*mul. |
| `FASTVIDEO_H3_VAE_UINT8_PIXELS` | bool | `1` | performance | Quantize single-GPU MiniMax-H3 video VAE output to uint8 on the GPU per chunk and stream uint8 into the host buffer; frames are bitwise identical to the fp32 path. 0 streams fp32. |
| `FASTVIDEO_H3_VAE_ENCODE_TILE_BATCH` | int | `1` | performance | Spatial tiles per MiniMax-H3 video VAE encoder call (reference and keyframe encodes); 1 encodes per tile. Batched tiles differ from per-tile encoding only in reduction order. |
| `FASTVIDEO_H3_VAE_INT8_FUSED_DEQUANT` | bool | `0` | performance | Fused dequantization epilogue for the MiniMax-H3 VAE INT8 projections. |
| `FASTVIDEO_H3_PINNED_SWAP` | bool | `1` | performance | Swap offloaded MiniMax-H3 modules through exact-size pinned host arenas. |
| `FASTVIDEO_H3_PARK_MODULES` | str | unset | performance | Comma-separated MiniMax-H3 denoise modules parked on the host while the text encoder runs, e.g. vae,audio_vae. |
+6
View File
@@ -162,6 +162,12 @@ Reset = resolveConfig(catalog, {})
counts and parallel degrees that divide the count, with supported `-1`
automatic values. Invalid combinations are rejected without changing edits.
Component checks reject a TAEH3 backend with an NVFP4 H3 decoder override and
reject an NVFP4 layer profile paired with another transformer precision. In the
editor, selecting TAEH3 clears the override; selecting an NVFP4 decoder restores
`h3-vae`. The copied YAML and visible chips follow the same choices. Schema
checks do not certify model loading or GPU execution.
The native runtime still resolves model-, hardware- and request-dependent
defaults. Cookbook validation does not establish complete native validity,
server readiness or GPU memory fit. Client instructions use same-machine
-1
View File
@@ -6,7 +6,6 @@ The interactive catalog groups models by family and generates YAML for supported
- [FastH3](minimax-h3.md)
- [Wan and FastMetal](wan.md)
- [LTX](ltx.md)
- [PAN 2](pan2.md)
- [FLUX](flux.md)
- [Qwen-Image-2.1](qwen-image21.md)
- [Stable Diffusion](stable-diffusion.md)
+29 -3
View File
@@ -59,7 +59,7 @@
</tr>
<tr>
<td>DGX Spark</td>
<td>FastH3 V1 on one GB10, or two Sparks with Ray sequence parallel (<code>sp_size=2</code>) over QSFP RoCE. Select NVIDIA DGX Spark, then 1 Spark or 2 Sparks.</td>
<td>FastH3 V1 on one GB10. Two Sparks run one clip faster over QSFP RoCE (<code>sp_size=2</code>), but this page configures one Spark.</td>
<td>Not wired</td>
</tr>
<tr>
@@ -72,6 +72,32 @@
</div>
</details>
<details id="pika-h3-flex">
<summary>Pika H3 Flex</summary>
<div >
<p>Pika H3 Flex is Pika's post-trained MiniMax-H3: the H3 components plus a rank-128 adapter that FastVideo applies automatically, sampled with a fixed six-forward re-noise ladder. One recipe page covers T2VA, the keyframe tasks (I2VA, L2VA, FL2VA), and Ref2VA. Its builder selects the resolution tier (480p or 768p), the aspect bucket, the duration (5, 10, or 15 s), and the optional Blackwell NVFP4 decoder and text encoder. The <a href="../../inference/pika_h3_flex/">Pika H3 Flex runbook</a> has the full sampling and geometry contract.</p>
<pre><code>FASTVIDEO_ATTENTION_BACKEND=FLASH_ATTN FASTVIDEO_FA4=1 \
python examples/inference/basic/basic_pika_h3_flex.py --task t2va --offload resident \
--resolution 768p --aspect 16:9 --duration 5 \
--prompt "A paper boat drifts down a rainy city street at night."</code></pre>
<table>
<thead>
<tr>
<th>T2VA on one GB200, 768p 16:9, 5 s, warm, all resident</th>
<th>Time</th>
</tr>
</thead>
<tbody>
<tr><td>Official <code>pika-h3-pro</code> CLI</td><td>59.7 s</td></tr>
<tr><td>FastVideo BF16</td><td>26.2 s</td></tr>
<tr><td>+ NVFP4 <code>quality-36L</code> decoder (40.5 dB PSNR vs BF16 decoder)</td><td>18.0 s</td></tr>
<tr><td>+ NVFP4 <code>fast-8L</code> decoder</td><td>17.4 s</td></tr>
</tbody>
</table>
<p>The BF16 run peaked at about 129 GiB resident on GB200; the official CLI's sampling peak was 71.7 GiB. With the NVFP4 text encoder, I2VA took 27.37 s, FL2VA 28.95 s, and Ref2VA with two image references 51.17 s, at 104.9 to 108.5 GiB peak. The example's default <code>--offload lazy</code> fits smaller devices but reloads components per request (48.06 s for the same T2VA request on GB200). FlashAttention-4 needs <code>FASTVIDEO_FA4=1</code> even when it is installed. Without it, attention uses FA3 or FA2 if one is installed and otherwise falls back to SDPA, which is slow and can run out of memory at 768p. GB10 has no FA4, so drop the prefix on DGX Spark.</p>
</div>
</details>
<details id="cookbook-setup">
<summary>Setup</summary>
<div >
@@ -100,7 +126,7 @@ cd FastVideo</code></pre>
<li>The FastH3 CUDA performance profile was measured on four GB200 GPUs. Use its strict profile when exact operation order matters more than the measured performance configuration.</li>
<li>The MLX source runtime supports T2VA, optional temporal <code>--fast</code>, optional spatial <code>--fast-spatial</code>, and opt-in VSA on <code>--include-vsa</code> checkpoints. FastH3 V2 MLX converts with <code>--include-vsa</code> and runs eight forwards. FL2VA, Ref2VA, and two-pass refinement are not wired.</li>
<li>GPU count and VAE decode backend are configurable in the builder above for FastH3 CUDA recipes. Only the value shown by default has a recorded run; other supported values are unmeasured here.</li>
<li>DGX Spark is a runtime on FastH3 V1, not a separate family card. Select NVIDIA DGX Spark, then 1 Spark or 2 Sparks. The CUDA GPU-count knob does not apply to Spark.</li>
<li>DGX Spark is a runtime on FastH3 V1, not a separate family card. This page configures one Spark. For two Sparks, run <code>examples/inference/basic/basic_fasth3_spark_pair.yaml</code>; there is no serving baseline. The CUDA GPU-count knob does not apply to Spark.</li>
<li>CompactH3 NVFP4 is one Blackwell GPU. RTX 5090 (32 GB) parks the encoder in pinned host RAM. RTX PRO 6000 Blackwell (96 GB) keeps encoder, DiT, and VAE resident. Keep <code>FASTVIDEO_ATTENTION_BACKEND=ATTN_QAT_INFER</code>, <code>FASTVIDEO_FA4=0</code>, <code>FASTVIDEO_VSA_PTX_BLACKWELL=0</code>, and <code>FLASHINFER_CUDA_ARCH_LIST=12.0a</code>. On PRO 6000 enable VAE compile and leave DiT <code>inference_torch_compile</code> off. CompactH3 is a dense prune; do not enable <code>VIDEO_SPARSE_ATTN_H3</code> until a VSA-trained student exists.</li>
<li>GB10 has no FA4 / ptx_blackwell VSA kernel. Keep <code>FASTVIDEO_FA4=0</code> and <code>FASTVIDEO_VSA_PTX_BLACKWELL=0</code>. Legal <code>num_frames</code> values are <code>17n+5</code>, capped at 362 (15.08 s). A 345-frame request on one Spark can OOM. Native 16:9 sizes include 832×480 and 1344×768.</li>
<li>Gated or missing checkpoints: run <code>huggingface-cli login</code> and confirm you accepted the model's license on Hugging Face.</li>
@@ -111,6 +137,6 @@ cd FastVideo</code></pre>
<details id="cookbook-evidence">
<summary>Evidence status</summary>
<div >
<p>Every command, model ID, and flag on this page maps to a checked-in FastVideo source. Recipes marked <strong>Verified</strong> also have a recorded hardware path in linked FastVideo evidence. The full H3 CUDA examples remain <strong>Source-backed</strong> where the source records a GPU count but no GPU model or memory requirement. Unlisted hardware is unknown, not unsupported.</p>
<p>Every command, model ID, and flag on this page maps to a checked-in FastVideo source. No recipe here carries a recorded run for a specific device, so the editor does not claim one. The full H3 CUDA examples remain <strong>Source-backed</strong> where the source records a GPU count but no GPU model or memory requirement. Unlisted hardware is unknown, not unsupported.</p>
</div>
</details>
+17 -20
View File
@@ -1,30 +1,27 @@
# Run H3 with a server and playground
Start FastH3 on CUDA, one DGX Spark, or Apple Silicon MLX, then iterate on prompts in a browser,
with cURL, or from your app. The server and clients can run on the same machine.
Start FastH3 on CUDA, one DGX Spark, or Apple Silicon MLX, then send requests from a
browser, cURL, or your app. Several clients can share one server process.
The server supports the OpenAI-compatible video-job API. The Python and
JavaScript client examples use that interface, but requests go to FastVideo.
You do not need an OpenAI account or cloud key.
CUDA requests reuse a loaded `VideoGenerator`. MLX keeps one pipeline object and
a prompt-embedding cache, but loads and releases heavyweight components between
phases to fit unified memory. MLX serving still pays that component-loading cost
on subsequent requests.
The [H3 recipe selector](minimax-h3.md) provides the same workflow with runtime
selection. This guide covers FastH3 V1 (four forwards) and FastH3 V2
(eight forwards). Start a server, then iterate in the playground or with the
OpenAI Python client. Other H3 recipes keep their direct Python commands.
The server implements the OpenAI-compatible video-job API. The Python and
JavaScript clients use that API, but requests go to FastVideo. You do not need an
OpenAI account or cloud key.
CUDA requests reuse one loaded `VideoGenerator`. The Python SDK can do the same
when you reuse the generator across `generate()` calls. MLX keeps one
`MiniMaxH3MLXPipeline` and a prompt-embedding cache, but still loads and releases
components between phases to fit unified memory. MLX serving does not keep all
weights resident or remove phase-loading time. Use a server when you want
separate clients to share the process without restarting scripts.
This guide covers FastH3 V1 (four forwards) and FastH3 V2 (eight forwards). For
direct Python generation, see the [H3 cookbook](minimax-h3.md).
## Install and start the server
All runtimes need a FastVideo clone and an activated Python environment.
### CUDA
Use a FastVideo clone and an activated Python environment. Complete the
[CUDA installation requirements](../getting_started/installation/gpu.md)
Complete the [CUDA installation requirements](../getting_started/installation/gpu.md)
before running these commands:
```bash
@@ -33,9 +30,9 @@ fastvideo serve --config examples/serving/openai_fasth3.yaml --server.host 127.0
```
The configuration loads `FastVideo/FastVideo-Minimax-FastH3-Preview-v0.2` and
advertises it as `fasth3`. It configures four CUDA GPUs but does not record
a GPU model or VRAM requirement. This is a source-backed server profile, not
the measured GB200 Python performance profile. Compilation is disabled.
advertises it as `fasth3`. It configures four CUDA GPUs and does not record a GPU
model or VRAM requirement, so treat it as a starting point, not a measured
profile. Compilation is disabled.
For FastH3 V2, use the same install and the 8-step config (nine sigma
points, eight DiT forwards, VSA sparsity 0.8):
+9 -1
View File
@@ -44,7 +44,7 @@ runtimes:
host: 0.0.0.0
port: 8000
served_model_name: null
workloads: [t2v, i2v]
workloads: [t2v, i2v, v2v]
options: &cuda_options
server.served_model_name:
anyOf:
@@ -97,6 +97,14 @@ runtimes:
title: Compile transformer
type: boolean
default: false
generator.engine.quantization.transformer_quant:
title: Transformer quantization
description: >-
Quantizes the DiT at load time. NVFP4 needs a Blackwell GPU. FP8 runs on Hopper and Ada.
Leave empty to use the checkpoint's own precision.
type: [string, "null"]
enum: [null, NVFP4, FP8]
default: null
# MiniMax-H3 only (fastvideo_args.video_decode_backend). TAEH3 fetches its own SHA-pinned weights.
generator.pipeline.experimental.video_decode_backend:
title: Video decoder
-76
View File
@@ -1,76 +0,0 @@
# PAN 2 recipes
<RecipeFamily family="pan2" />
Generate a video from text, or from a first frame, with `IFM/PAN2`. One pipeline
serves both modes: leave out the first frame for text-to-video and supply one
for image-to-video. The defaults are 704×1248, 313 frames (13 s at 24 fps),
50 steps and guidance 3.0. See [PAN 2 Text/Image to Video](../inference/pan2.md)
for the model, sampler and parallelism details.
## Run on a GPU
Follow the [CUDA installation guide](../getting_started/installation/gpu.md), then run
from the repository root. The full `IFM/PAN2` checkpoint is not public yet; pass a
local directory or private repo you can access with `PAN2_MODEL_PATH`.
```bash
python examples/inference/basic/basic_pan2.py
```
Set `IMAGE_PATH` in the script to a first frame for image-to-video. With more
GPUs, `PAN2_NUM_GPUS=4` shards the video tokens with sequence parallelism; the
count must divide PAN 2's 32 attention heads. Each rank still holds the full
29B DiT, so extra GPUs speed up generation but do not reduce per-device weight
memory. See [CPU offloading](../inference/offloading.md) for devices that cannot
hold the DiT.
## Serve with the OpenAI-compatible API
`examples/serving/openai_pan2.yaml` serves PAN 2 through the shared
`/v1/videos` routes. It mirrors the Python example: one GPU, the text encoder
offloaded between requests, and the pipeline's own sampling defaults.
```bash
fastvideo serve --config examples/serving/openai_pan2.yaml --server.host 127.0.0.1
```
Text-to-video uses the same request as any other served video model, with the
alias `pan2`:
```bash
curl --fail-with-body http://127.0.0.1:8000/v1/videos \
-H 'Content-Type: application/json' \
-d '{"model": "pan2", "prompt": "A red fox trots through fresh snow in a pine forest at dawn."}'
```
For image-to-video, add a first frame in `image_reference` (or the legacy
`input_reference`). The server then runs image-to-video; no second deployment or
workload is needed.
```bash
curl --fail-with-body http://127.0.0.1:8000/v1/videos \
-H 'Content-Type: application/json' \
-d '{"model": "pan2", "prompt": "The fox turns toward the camera.",
"image_reference": {"image_url": "https://example.com/first-frame.png"}}'
```
Local image paths must sit under `server.media_roots`, and private or loopback
URLs need `server.allow_private_media_urls`. Poll `/v1/videos/{id}` and download
`/v1/videos/{id}/content` as in the [serving guide](openai-api.md). Generation
time for the default 13-second clip is not measured, so raise the client's
polling deadline for long jobs instead of treating a slow job as failed.
The cookbook's generated serving clients send a text prompt only. The image
request above is the supported way to supply a first frame over HTTP, and no
recorded generation backs it yet.
## Evidence
Both recipes are **Source-backed**: the model ID, example and serving YAML are
checked against each other, and the serving defaults are tested against the
registered `pan2_t2v` preset. No GPU generation, memory measurement or HTTP
client run is recorded, so hardware fit and throughput are unknown. Numerical
parity with the reference implementation is checked on CPU with a public
random-weight checkpoint, which verifies the port and produces noise rather than
useful video.
+1
View File
@@ -24,6 +24,7 @@ deployments:
- generator.engine.parallelism.tp_size
- generator.engine.parallelism.sp_size
- generator.engine.offload.text_encoder
- generator.engine.quantization.transformer_quant
- generator.engine.offload.vae
- generator.pipeline.experimental.video_decode_backend
- generator.pipeline.experimental.h3_vae_nvfp4_decoder
+1
View File
@@ -21,6 +21,7 @@ deployments:
- generator.engine.parallelism.*
- "!generator.engine.parallelism.hsdp_*"
- generator.engine.offload.text_encoder
- generator.engine.quantization.transformer_quant
- generator.engine.offload.vae
- generator.pipeline.experimental.video_decode_backend
- generator.pipeline.experimental.h3_vae_nvfp4_decoder
+5 -1
View File
@@ -1,4 +1,4 @@
title: FastH3 OmniRef
title: FastH3 Omni v1
summary: Video and synchronized audio from a prompt plus ordered image, video, and audio references, in eight forwards.
guide: docs/inference/fasth3_omniref.md
deployments:
@@ -39,10 +39,12 @@ deployments:
directories in server.media_roots.
- Based on the maintained inference recipe; this serving adaptation has not been GPU-validated.
- "Optional NVFP4 decoder (Blackwell only): hf download FastVideo/FastH3-NVFP4-VAE-Decoder --include 'quality-36L/decoder-nvfp4.safetensors' 'fast-8L/decoder-nvfp4.safetensors' --local-dir fasth3-nvfp4-vae"
- "Optional NVFP4 text encoder (Blackwell only): hf download FastVideo/FastVideo-FastH3-8-Step-V2-NVFP4-Consumer --include 'text_encoder/*' --local-dir fasth3-nvfp4-encoder"
options: &omniref_options
- server.*
- generator.engine.offload.text_encoder
- generator.pipeline.experimental.h3_vae_nvfp4_decoder
- generator.pipeline.components.text_encoder_weights
- default_request.sampling.num_frames
- default_request.sampling.height
- default_request.sampling.width
@@ -92,6 +94,7 @@ deployments:
directories in server.media_roots.
- Based on the maintained inference recipe; this serving adaptation has not been GPU-validated.
- "Optional NVFP4 decoder (Blackwell only): hf download FastVideo/FastH3-NVFP4-VAE-Decoder --include 'quality-36L/decoder-nvfp4.safetensors' 'fast-8L/decoder-nvfp4.safetensors' --local-dir fasth3-nvfp4-vae"
- "Optional NVFP4 text encoder (Blackwell only): hf download FastVideo/FastVideo-FastH3-8-Step-V2-NVFP4-Consumer --include 'text_encoder/*' --local-dir fasth3-nvfp4-encoder"
options: *omniref_options
overrides:
default_request.sampling.num_frames:
@@ -142,5 +145,6 @@ deployments:
directories in server.media_roots.
- Based on the maintained inference recipe; this serving adaptation has not been GPU-validated.
- "Optional NVFP4 decoder (Blackwell only): hf download FastVideo/FastH3-NVFP4-VAE-Decoder --include 'quality-36L/decoder-nvfp4.safetensors' 'fast-8L/decoder-nvfp4.safetensors' --local-dir fasth3-nvfp4-vae"
- "Optional NVFP4 text encoder (Blackwell only): hf download FastVideo/FastVideo-FastH3-8-Step-V2-NVFP4-Consumer --include 'text_encoder/*' --local-dir fasth3-nvfp4-encoder"
options: *omniref_options
overrides: *omniref_overrides
+2
View File
@@ -21,6 +21,7 @@ deployments:
- Based on the released one-Spark inference recipe. This serving adaptation and edited hardware combinations have not been GPU-validated.
- The checkpoint includes NVFP4 weights and encoder, a lightweight video VAE, and its trained eight-forward schedule. No local CompactH3 assembly is needed.
- "Optional NVFP4 decoder (Blackwell only): hf download FastVideo/FastH3-NVFP4-VAE-Decoder --include 'quality-36L/decoder-nvfp4.safetensors' 'fast-8L/decoder-nvfp4.safetensors' --local-dir fasth3-nvfp4-vae"
- "Optional NVFP4 text encoder (Blackwell only): hf download FastVideo/FastVideo-FastH3-8-Step-V2-NVFP4-Consumer --include 'text_encoder/*' --local-dir fasth3-nvfp4-encoder"
options:
- server.*
- generator.engine.num_gpus
@@ -30,6 +31,7 @@ deployments:
- generator.engine.offload.vae
- generator.pipeline.experimental.video_decode_backend
- generator.pipeline.experimental.h3_vae_nvfp4_decoder
- generator.pipeline.components.text_encoder_weights
- default_request.prompt
- default_request.sampling.num_frames
- default_request.sampling.height
@@ -0,0 +1,18 @@
title: Open-H3-Regenerate-2K
summary: Regenerate a 768P MiniMax-H3 draft at 2560x1440 with three refine steps.
deployments:
cuda-rest:
runtime: fastvideo-cuda-rest
guide: docs/inference/open_h3_regenerate_2k.md
workload: v2v
defaults: examples/serving/openai_open_h3_regenerate_2k.yaml
requirements:
- 'UV_TORCH_BACKEND=cu130 uv pip install -e ".[fasth3]"'
- "One GPU with about 150 GB free (B200 or GB200). Send the draft as video_reference; local paths must sit under server.media_roots."
- "Optional NVFP4 decoder (Blackwell only): hf download FastVideo/FastH3-NVFP4-VAE-Decoder --include 'quality-36L/decoder-nvfp4.safetensors' --local-dir fasth3-nvfp4-vae"
options:
- server.*
- generator.engine.offload.text_encoder
- generator.pipeline.experimental.h3_vae_nvfp4_decoder
- default_request.sampling.seed
-38
View File
@@ -1,38 +0,0 @@
title: PAN 2
summary: Text-to-video and first-frame image-to-video from the 29B PAN 2 mixture-of-transformers model.
deployments:
cuda-rest:
runtime: fastvideo-cuda-rest
workload: t2v
defaults: examples/serving/openai_pan2.yaml
requirements:
- >-
Requests without a reference run text-to-video. Add image_reference or input_reference
to condition on a first frame; the same server and pipeline handle both.
- >-
To use more than one GPU, set sp_size equal to num_gpus (Ulysses sequence parallelism).
It must divide PAN 2's 32 attention heads, and each rank still holds the full DiT weights.
options:
- server.*
- generator.engine.num_gpus
- generator.engine.parallelism.*
- "!generator.engine.parallelism.hsdp_*"
- generator.engine.offload.text_encoder
- generator.engine.offload.vae
- default_request.sampling.num_frames
- default_request.sampling.height
- default_request.sampling.width
- default_request.sampling.fps
overrides:
default_request.sampling.num_frames:
default: 313
description: num_frames - 1 must be a multiple of 4.
default_request.sampling.height:
default: 704
minimum: 32
multipleOf: 32
default_request.sampling.width:
default: 1248
minimum: 32
multipleOf: 32
-1
View File
@@ -49,7 +49,6 @@ never deleted. See `examples/serving/README.md`.
| `openai_hunyuan15_i2v_480p.yaml` | HunyuanVideo 1.5 I2V 480p | `i2v` | CUDA REST | `hunyuan15-i2v-480p` | Source-backed; preset renders 720x1280 |
| `openai_kandinsky6_ti2va.yaml` | Kandinsky 6 Pro TI2VA | `t2v` | CUDA REST | `kandinsky6-ti2va` | Source-backed |
| `openai_kandinsky6_ti2va_distilled.yaml` | Kandinsky 6 Pro-distill TI2VA | `t2v` | CUDA REST | `kandinsky6-ti2va-distilled` | Source-backed |
| `openai_pan2.yaml` | PAN 2 | `t2v` (a reference switches to image-to-video) | CUDA REST | `pan2/cuda-rest` | Source-backed; checkpoint not public, no recorded run |
| `openai_ltx2_distilled.yaml` | LTX2 Distilled | `t2v` | CUDA REST | `ltx2-distilled/cuda-rest` | Source-backed |
| `streaming_demo.yaml` | LTX2 Distilled | `t2v` | CUDA streaming | `ltx2-distilled/cuda-streaming` | Source-backed |
| `mlx_fasth3.yaml` | FastH3 V1 converted | `t2v` | MLX REST | none (legacy catalog only) | Source runtime; T2VA only |
@@ -88,7 +88,7 @@ surfaces:
video_decode_backend: "MiniMax-H3 video decoder selection (full VAE vs TAEH3 preview); model-specific optimization not yet represented in the typed public schema."
taeh3_checkpoint: "Optional local TAEH3 safetensors path; model-specific optimization not yet represented in the typed public schema."
taeh3_chunk_size: "TAEH3 temporal chunk length; model-specific optimization not yet represented in the typed public schema."
h3_vae_nvfp4_decoder: "Exported NVFP4 MiniMax-H3 video decoder file that replaces the VAE decoder at load; model-specific optimization not yet represented in the typed public schema."
h3_vae_nvfp4_decoder: "Exported NVFP4 MiniMax-H3 video decoder file that replaces the VAE decoder at load; represented as ComponentConfig.h3_vae_nvfp4_decoder."
vae_parallel_encode: "MiniMax-H3 sequence-parallel reference VAE encode opt-in; model-specific optimization not yet represented in the typed public schema."
vae_parallel_decode_strategy: "Chunk-transport collective for vae_parallel_decode; model-specific optimization not yet represented in the typed public schema."
attention_backend: "Process-wide default attention-backend request applied per component at load time; kernel-selection knob not yet represented in the typed public schema."
@@ -153,6 +153,18 @@ surfaces:
pipeline_config_extensions:
preset_owned:
draft_size:
sources:
- fastvideo.configs.pipelines.open_h3_regenerate_2k.OpenH3Regenerate2KPipelineConfig
refine_start_sigma:
sources:
- fastvideo.configs.pipelines.open_h3_regenerate_2k.OpenH3Regenerate2KPipelineConfig
refine_transformer_dir:
sources:
- fastvideo.configs.pipelines.open_h3_regenerate_2k.OpenH3Regenerate2KPipelineConfig
draft_latent_path:
sources:
- fastvideo.configs.pipelines.open_h3_regenerate_2k.OpenH3Regenerate2KPipelineConfig
audio_encoder_precision:
sources:
- fastvideo.configs.pipelines.wan.WanS2V14BConfig
@@ -283,18 +295,6 @@ surfaces:
- fastvideo.configs.pipelines.hunyuan15.Hunyuan15T2V720PConfig
- fastvideo.configs.pipelines.hyworld.HYWorldConfig
- fastvideo.configs.pipelines.hyworld.Hunyuan15T2V480PConfig
text_system_prompt:
sources:
- fastvideo.configs.pipelines.pan2.PAN2PipelineConfig
text_encoder_skip_layers:
sources:
- fastvideo.configs.pipelines.pan2.PAN2PipelineConfig
text_max_sequence_length:
sources:
- fastvideo.configs.pipelines.pan2.PAN2PipelineConfig
i2v_skip_first_latent_cfg:
sources:
- fastvideo.configs.pipelines.pan2.PAN2PipelineConfig
text_encoder_max_lengths:
sources:
- fastvideo.configs.pipelines.hunyuan15.Hunyuan15I2V480PStepDistilledConfig
@@ -464,6 +464,9 @@ surfaces:
kv_cache_device:
target: generator.pipeline.experimental.kv_cache_device
sources: [fastvideo.configs.pipelines.qwen_image21.QwenImage21PipelineConfig]
pika_resolution_tier:
target: generator.pipeline.experimental.pika_resolution_tier
sources: [fastvideo.configs.pipelines.pika_h3_flex.PikaH3FlexPipelineConfig]
compatibility_only:
batch_size: "Gen3C inference-only tuning field pending typed batching design."
gradient_checkpointing: "Gen3C inference-only compatibility field pending typed batching design."
@@ -477,6 +480,7 @@ surfaces:
frame_receptive_field: "MagiHuman internal data-proxy receptive-field setting."
image_conditioning: "MagiHuman preset variant marker for reference-image conditioning."
pdd_step_indices: "MiniMax-H3 PDD fine-grid partition; resolve_checkpoint_settings sets it from fastvideo_inference.json."
pika_denoising_steps: "Pika H3 Flex six-forward re-noise ladder fixed by the adapter contract; check_pipeline_config rejects any other value."
ref_audio_offset: "MagiHuman internal data-proxy audio alignment offset."
sr_local_attn_layers: "MagiHuman SR internal sparse-attention layer selection."
text_offset: "MagiHuman internal data-proxy text alignment offset."
+1 -1
View File
@@ -35,7 +35,7 @@ The preprocessing steps are identical to training. Run the appropriate preproces
```bash
# For Wan2.1 T2V
bash scripts/preprocess/v1_preprocess_wan_data_t2v
bash scripts/preprocess/v1_preprocess_wan_data_t2v.sh
# For Wan2.2 TI2V
bash examples/distill/Wan2.2-TI2V-5B-Diffusers/crush_smol/preprocess_wan_data_ti2v_5b.sh
+4 -4
View File
@@ -11,16 +11,16 @@ Attention. See [Attn-QAT Training](../training/attn_qat.md).
## 📊 Model Overview
We provide two distilled models:
We provide three distilled models:
- **[FastWan2.1-T2V-1.3B-Diffusers](https://huggingface.co/FastVideo/FastWan2.1-T2V-1.3B-Diffusers)**: 3-step inference, up to **16 FPS** on H100 GPU
- **[FastWan2.1-T2V-14B-480P-Diffusers](https://huggingface.co/FastVideo/FastWan2.1-T2V-14B-480P-Diffusers)**: 3-step inference, up to **60x speed up** at 480P, **90x speed up** at 720P for denoising loop
- **[FastWan2.2-TI2V-5B-FullAttn-Diffusers](https://huggingface.co/FastVideo/FastWan2.2-TI2V-5B-FullAttn-Diffusers)**: 3-step inference, up to **50x speed up** at 720P for denoising loop
Both models are trained on **61×448×832** resolution but support generating videos with **any resolution** (1.3B model mainly support 480P, 14B model support 480P and 720P, quality may degrade for different resolutions).
All three models are trained on 61 frames at 448×832 but support generating videos with **any resolution** (1.3B model mainly support 480P, 14B model support 480P and 720P, quality may degrade for different resolutions).
## ⚙️ Inference
First install [VSA](../attention/vsa/index.md). Set `MODEL_BASE` to your own model path and run:
First install [VSA](../attention/vsa/index.md), then run the 1.3B config. The config pins the checkpoint; edit `model_path` there to use a local copy.
```bash
FASTVIDEO_ATTENTION_BACKEND=VIDEO_SPARSE_ATTN \
@@ -45,7 +45,7 @@ python scripts/huggingface/download_hf.py \
### Wan2.1 1.3B Model Sparse-Distill
For the 1.3B model, we use **4 nodes with 32 H200 GPUs** (8 GPUs per node):
For the 1.3B model, we use **8 nodes with 64 H200 GPUs** (8 GPUs per node):
```bash
# Multi-node training (8 nodes, 64 GPUs total)
+1 -1
View File
@@ -35,11 +35,11 @@ COOKBOOK_FAMILIES = {
"cosmos",
"kandinsky5",
"kandinsky6",
"pan2",
"flux",
"qwen_image21",
"sd35",
"minimax_h3",
"pika_h3_flex",
"stable_audio",
"mmaudio",
"matrixgame",
@@ -6,9 +6,9 @@ load stands down when lazy owns deferral). Two boxes connected
by the QSFP ConnectX-7 cables can run **one clip faster** and can hold a
**longer clip** (up to the FastH3 15 s cap).
Copy-paste commands for both counts are on the
[MiniMax H3 cookbook](../../cookbook/minimax-h3.md): FastH3 V1 → NVIDIA DGX
Spark → 1 Spark or 2 Sparks.
The [MiniMax H3 cookbook](../../cookbook/minimax-h3.md) covers one Spark. For
two, run `examples/inference/basic/basic_fasth3_spark_pair.yaml`; it has no
serving baseline.
This is FastVideo sequence parallel (`sp_size=2`) over Ray, not a third-party
xDiT vendor. Do not install xDiT for this path.
@@ -156,7 +156,7 @@ lightweight video VAE. Use the environment above plus the release kernel
settings, then run one of these configs from the head:
```bash
export FASTVIDEO_MINIMAX_H3_FUSIONS=all FASTVIDEO_NVFP4_MM_BACKEND=cutlass
export FASTVIDEO_MINIMAX_H3_FUSIONS=all FASTVIDEO_NVFP4_MM_BACKEND=cudnn
export FASTVIDEO_H3_VAE_TILE_BATCH=1 FASTVIDEO_VSA_TRITON=1
export FASTVIDEO_VSA_PTX_BLACKWELL=0 FASTVIDEO_FA4=0
export FASTVIDEO_ATTENTION_BACKEND=VIDEO_SPARSE_ATTN_H3 FASTVIDEO_STAGE_LOGGING=1
+25 -9
View File
@@ -1,28 +1,44 @@
# 🚀 Quick Start
# Quick start
Get up and running with FastVideo in minutes!
Install FastVideo, generate one video, then start from a maintained recipe.
## Installation
## Install
First, install FastVideo:
Requires Python 3.10 to 3.12 and CUDA 12.6 or newer.
```bash
# If you previously used Conda, use uv instead for a faster, more stable setup
uv venv --python 3.12 --seed
source .venv/bin/activate
# Install FastVideo on NVIDIA CUDA 12
# CUDA 12. Use cu130 on CUDA 13.
UV_TORCH_BACKEND=cu126 uv pip install fastvideo
```
Use `UV_TORCH_BACKEND=cu130` instead on CUDA 13.
Also optionally install flash-attn:
For the fastest attention kernels, install flash-attn:
```bash
uv pip install flash-attn --no-build-isolation -v
```
## Generate a video
```python
from fastvideo import VideoGenerator
generator = VideoGenerator.from_pretrained(
"Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
num_gpus=1,
text_encoder_cpu_offload=True,
)
generator.generate_video(
"A raccoon peers through a field of sunflowers.",
output_path="videos/",
save_video=True,
)
```
The first run downloads the Wan2.1 1.3B checkpoint. The call writes an MP4 under `videos/`. The full example is `examples/inference/basic/basic.py`.
## Choose a maintained recipe
The cookbook selects complete, checked-in recipes instead of mixing model,
+1 -1
View File
@@ -1,6 +1,6 @@
# Welcome to FastVideo
FastVideo is an inference and post-training framework for diffusion models. It features an end-to-end unified pipeline for accelerating diffusion models, starting from data preprocessing to model training, finetuning, distillation, and inference. FastVideo is designed to be modular and extensible, allowing users to easily add new optimizations and techniques. Whether it is training-free optimizations or post-training optimizations, FastVideo has you covered.
FastVideo is an inference and post-training framework for video diffusion models. One pipeline covers data preprocessing, fine-tuning, distillation, and inference. Attention backends, parallelism, and quantization are separate modules, so you can add one without changing model code.
## Key Features
+3 -3
View File
@@ -90,7 +90,7 @@ PipelineConfig (fastvideo/configs/pipelines/base.py)
│ ├── WanT2V720PConfig
│ └── WanI2V480PConfig
├── HunyuanConfig (fastvideo/configs/pipelines/hunyuan.py)
├── LTX2T2VConfig (fastvideo/configs/pipelines/ltx2.py)
├── LTX2T2VConfig (fastvideo/pipelines/basic/ltx2/pipeline_configs.py)
├── CosmosConfig (fastvideo/configs/pipelines/cosmos.py)
└── ... (15+ model families)
```
@@ -136,7 +136,7 @@ Concrete hierarchy: `DiTConfig` → `DiTArchConfig`, `VAEConfig` →
### SamplingParam (`fastvideo/api/sampling_param.py`)
Generation parameters separate from pipeline config. Each model family
provides defaults via a profile (see `fastvideo/pipelines/basic/<family>/profiles.py`):
provides defaults via a profile (see `fastvideo/pipelines/basic/<family>/presets.py`):
```python
sp = SamplingParam.from_pretrained("Wan-AI/Wan2.1-T2V-1.3B-Diffusers")
@@ -505,7 +505,7 @@ User: generator.generate_video(prompt, ...)
flow_shift, precision defaults.
2. **Sampling param profile** — Create a profile in
`fastvideo/pipelines/basic/<model>/profiles.py` with default height,
`fastvideo/pipelines/basic/<model>/presets.py` with default height,
width, num_frames, guidance_scale, num_inference_steps.
3. **Register configs** — In `fastvideo/registry.py`, add a
+1 -1
View File
@@ -72,7 +72,7 @@ def main():
sampling_param.guidance_scale = 7.5
# Random seed for reproducibility
sampling_param.seed = 42 # Optional, leave unset for random results
sampling_param.seed = 42 # Optional; the default is 1024
# Generate video with custom parameters
prompt = "A beautiful sunset over a calm ocean, with gentle waves."
+278
View File
@@ -0,0 +1,278 @@
# Open-H3-Regenerate-2K
`OpenH3Regenerate2KPipeline` regenerates a 768P [MiniMax-H3](https://huggingface.co/MiniMaxAI/MiniMax-H3) draft video
(1344x768, 121 frames, 24 fps) at **2560x1440** with three refine steps. It is video to video: the draft is the input,
and the output is a silent 2K MP4. FastVideo ports NVlabs'
[Open-H3-Regenerate-2K](https://huggingface.co/Efficient-Large-Model/Open-H3-Regenerate-2K)
([project page](https://nvlabs.github.io/Sana/Open-H3-Regenerate-2K/),
[reference code](https://github.com/NVlabs/Sana/tree/sol-engine/models/Open-H3-Regenerate-2K)); the dense BF16
`standard_fwd` path of that repo is the parity reference. The accelerated `sol_engine_fwd` path (Sol-Attn, MXFP8, CUDA
graphs) and the fast 24-layer VAE decoder are not ported.
```text
draft video -> H3 VAE encode -> latent upscaler (2x, 3D conv) -> 3 Euler refine steps -> H3 VAE decode -> 2560x1440
H3 DiT + merged LoRA + Sharp curve AdaLN branches
```
## What runs
| Step | Details |
|---|---|
| Draft encode | `ffmpeg` resamples to 24 fps, cover-scales (bilinear) and centre-crops to 1344x768 (`draft_size: 768p`, or 1024x576 for `576p`), holds the last frame to 121 frames plus 3 copies. The FP32 H3 VAE posterior is sampled under seed 42. |
| Latent upscale | The author's 3D-conv upscaler (`fastvideo/models/upsamplers/minimax_h3_latent_upscaler.py`), BF16, to the 160x90 latent grid. |
| Refine | Euler on sigmas `[0.909375, 0.857143, 0.666667, 0]`, CFG 1, model time `1 - sigma`, initial state `(1 - 0.909375) * upscaled + 0.909375 * noise`. `num_inference_steps: 2` uses `[0.7, 0.494118, 0]` (softer, about 30% faster). |
| Transformer | The base MiniMax-H3 DiT (the same module as the other H3 pipelines) with the released merged LoRA (FP32, rank 576 on 312 linears, scale 1) and 50 LMS Sharp curve AdaLN branches. |
| Decode | The released H3 VAE, or an NVFP4 decoder on Blackwell (see below); the output is trimmed to 121 frames. |
The merged LoRA loads through the standard H3 community-LoRA path as a constructor-time adapter: the pipeline sets
`lora_path` to `<model>/lora/open_h3_regenerate_2k_lora.safetensors` when you do not pass one. The Sharp branches take an
8-D timestep curve (`lora/adaln_t_table.bin`, 1025x8) instead of the layer input, so they cannot be folded into weights;
`sharp_curve.py` adds them to the AdaLN projection outputs with forward hooks.
**Audio.** The reference feeds an encoded-silence audio stream to the DiT as conditioning and writes the video with no
audio track (`-an`); it neither regenerates nor passes through the draft's audio. FastVideo does the same: the draft's
soundtrack is ignored and the output MP4 has no audio.
## Weights
The release repo keeps the unmodified MiniMax-H3 components (a copy of `MiniMaxAI/MiniMax-H3` at `6818f6c`) under
`base_model/` next to `lora/` and `latent_upsampler/`. Point `model_path` at the Hub id or at a local copy of that layout.
`adaln_t_table.bin` is base-derived and is not redistributed in the release repo: put the 32,800-byte file (SHA-256
`c02a6c11888297688c1e6278185ea1f947023acfc69f9003bbcdcec9a229a8e7`) at `lora/adaln_t_table.bin`. The reference
`download.py` fetches it by byte range from `Comfy-Org/MiniMax-H3` at revision
`e5eb578a89295337b8ff433a035929ce0279e0b6` (tensor `adaln_t_table` of
`diffusion_models/minimax_h3_ref2va_pruned_int8_convrot.safetensors`) and verifies the hash.
A GB200/B200-class GPU is needed (about 150 GB with the DiT, the 2K activations and the VAE).
## Usage
```bash
python examples/inference/basic/basic_open_h3_regenerate_2k.py \
--model-path Efficient-Large-Model/Open-H3-Regenerate-2K \
--video draft_768p.mp4 --prompt "A red fox trots through fresh snow" --output outputs/regen2k
```
The prompt conditions the refine steps (use the draft's own prompt). Drafts from other video models are better encoded at
576P (`--draft-size 576p`). `--steps 2` runs the two-step variant. The output is always 2560x1440; the first 121 frames of
the draft are used.
## Serving
```bash
fastvideo serve --config examples/serving/openai_open_h3_regenerate_2k.yaml
```
`examples/serving/openai_open_h3_regenerate_2k.yaml` loads the release on one GPU, sets `media_roots`, and pins the 2K
defaults. The draft arrives as the typed `video_reference` of `POST /v1/videos/sync` (the response body is the MP4) or of
`POST /v1/videos` (an async job; poll `GET /v1/videos/{id}` and download `GET /v1/videos/{id}/content`):
```bash
curl -sS -X POST http://localhost:8000/v1/videos/sync \
-H 'Content-Type: application/json' \
-d '{
"prompt": "A red fox trots through fresh snow",
"video_reference": {"video_url": "https://example.com/draft_768p.mp4"},
"seed": 1766580932
}' \
-o regen_2k.mp4 -D -
```
The response headers carry `X-Request-Id`, `X-Model`, `X-Inference-Time-S` and `X-Stage-Durations`. Rules:
- Exactly one `video_reference` (or the legacy `video_path` / `video_url`) and no image or audio references.
- `http(s)` URLs must resolve to public addresses unless `server.allow_private_media_urls` is set. A local path is accepted
only inside a directory listed in `server.media_roots`. `data:` URLs are refused for video references; this policy is
unchanged.
- `size`, `width` and `height` may be omitted or `2560x1440`; `num_frames` may be omitted or 124 (the frame count of the
VAE grid; the video has 121 frames); `fps` must be 24. `seconds` is not accepted.
- `num_inference_steps` is 3 (default) or 2; `seed` sets the refine noise.
- Serve with `attention_backend: TORCH_SDPA`: the refine stage selects cuDNN attention, which at about 133k tokens is
about 3x faster than PyTorch's default flash dispatch. The `FLASH_ATTN` backend bypasses that selection.
## NVFP4 video decoder
On Blackwell GPUs the released 36-block video decoder can be replaced by a pre-packed NVFP4 one; the latent space is
unchanged, so the draft encode, the upscale and the refine steps are the same and only the decode changes. Both tiers of
[FastVideo/FastH3-NVFP4-VAE-Decoder](https://huggingface.co/FastVideo/FastH3-NVFP4-VAE-Decoder) apply here, because the
release carries MiniMax-H3's full 36-block VAE (`base_model/vae`):
| Tier | Decoder blocks | File | Pick it when |
|---|---|---|---|
| `quality-36L` | 36 | `quality-36L/decoder-nvfp4.safetensors` | you want the released detail back; the default choice |
| `fast-8L` | 8 (evenly spaced; the load drops 28 blocks) | `fast-8L/decoder-nvfp4.safetensors` | decode time or memory dominate and some fine detail can go |
```bash
hf download FastVideo/FastH3-NVFP4-VAE-Decoder --include 'quality-36L/decoder-nvfp4.safetensors' \
'fast-8L/decoder-nvfp4.safetensors' --local-dir fasth3-nvfp4-vae
```
Select it through the surface you drive the pipeline with. The config field (also a dotted override of
`fastvideo generate`), the example's `--vae-decoder` and the environment variable all set
`fastvideo_args.h3_vae_nvfp4_decoder`, and the config field wins over the environment.
```bash
# example script
python examples/inference/basic/basic_open_h3_regenerate_2k.py --video draft.mp4 --prompt "..." \
--vae-decoder fasth3-nvfp4-vae/quality-36L/decoder-nvfp4.safetensors
# `fastvideo generate`: a run config, with the decoder as a dotted override
fastvideo generate --config regen2k_run.yaml \
--generator.pipeline.experimental.h3_vae_nvfp4_decoder fasth3-nvfp4-vae/quality-36L/decoder-nvfp4.safetensors
```
```yaml
generator:
pipeline:
experimental:
h3_vae_nvfp4_decoder: ./fasth3-nvfp4-vae/quality-36L/decoder-nvfp4.safetensors
```
`FASTVIDEO_H3_VAE_NVFP4_DECODER` stays as the fallback when the config does not set the field. The
swap happens in the VAE loader, so the dense decoder never reaches the GPU: it is replaced on the CPU by the exported
checkpoint (2.0 GiB of packed weights instead of 9.7 GiB of FP32), moved to the device, and decoded under bf16 autocast.
The decoder keeps the pipeline's tiling (at 2560x1440: 8x13 tiles of 256 px with 64 px overlap) and the fused Triton
block kernels, which are on by default (`FASTVIDEO_H3_VAE_NVFP4_FUSED`); `FASTVIDEO_H3_VAE_TILE_BATCH` decodes several
tiles per decoder call (4 is bit-identical and faster here), and `FASTVIDEO_H3_VAE_NVFP4_CUDA_GRAPH` replays the small
fused calls. The MP4 is unaffected: the same 121 frames are trimmed and written.
Warm decode of one refined latent at 2560x1440 (124 frames) on one GB200, median of 3, with the released decode as the
reference:
| Decoder | Decode (s) | Peak GPU memory (GiB) | PSNR (dB) | SSIM |
|---|---:|---:|---:|---:|
| released H3 VAE (dense, fp16 autocast) | 23.3 | 18.4 | - | - |
| `quality-36L` (fused) | 6.6 | 6.2 | 41.3 | 0.979 |
| `quality-36L` (fused, tile batch 4) | 5.0 | 6.3 | 41.3 | 0.979 |
| `quality-36L` (eager NVFP4 GEMM) | 67.0 | 6.2 | 41.3 | 0.979 |
| `fast-8L` (fused) | 1.7 | 5.3 | 32.8 | 0.892 |
| `fast-8L` (eager NVFP4 GEMM) | 16.1 | 5.2 | 32.8 | 0.892 |
The fused kernels are an exact speedup: the eager and fused rows decode bit-identical frames, and so does batching
tiles, so the PSNR/SSIM columns measure the tier and nothing else. The eager NVFP4 GEMM path loses to the dense fp16
decoder at these tile sizes (67 s against 23 s), so keep `FASTVIDEO_H3_VAE_NVFP4_FUSED` on. PSNR against the released
decode is bounded by the released decode itself: the refined latent already sits 45.2 dB from the NVlabs reference (see
below), so 41.3 dB is a decoder-only difference of the same order. `fast-8L` softens fine texture (fur, foliage) while
keeping structure; use it only when the decode budget is tight.
## Refining on a FastH3 DiT
`refine_transformer_dir` swaps the refine DiT for another MiniMax-H3 transformer while the rest of the release (VAEs,
encoders, upscaler, merged LoRA, Sharp branches) stays put. Point it at a transformer component directory, or at a
checkpoint whose `transformer/` subdirectory is one, such as a FastH3 export:
```python
PipelineSelection(components=ComponentConfig(),
experimental={"refine_transformer_dir": "/models/FastH3-QAD-v2"})
```
The released LoRA and Sharp branches are base-H3-derived adapters, so they keep loading over the chosen DiT. A packed
NVFP4 FastH3 export needs its quantization profile
(`EngineConfig(quantization=QuantizationConfig(transformer_quant="NVFP4", layer_profile="h3_dit_vsa"))`) and refuses a
dense attention backend, because its `to_gate_compress` linears are built only for VSA. Do not set
`FASTVIDEO_H3_ADALN_TABLE` for a different sigma grid than the one a checkpoint's `adaln_tables.pt` covers; the refine
grid is not FastH3's 8-step schedule, so load the real AdaLN projections.
This swap does not reproduce the release's refine. On the fox draft, FastH3 V2 BF16 walking its own DMD rungs from 500, the
best adapter subset reached 26 dB PSNR and 0.84 SSIM against the base-H3 refine (the base reproduces itself at 45 dB), with no
faster forwards at this sequence length. Draft with FastH3 and refine with the base DiT instead.
`draft_latent_path` skips the draft video: a `.pt` holding the normalized native H3 draft latent `[C, T, h, w]`
(float32) goes straight to the upscaler, so a draft generated by a FastH3 t2v run never needs decoding to pixels and
re-encoding. Starting from `vae.encode_pixels(...).latent_dist`, sample it and apply `vae.normalize_latents` before
saving, as the draft encoder does. A batched latent is accepted only with a leading dimension of 1.
```bash
python examples/inference/basic/basic_open_h3_regenerate_2k.py --video draft.mp4 --prompt "..." \
--refine-transformer /models/FastH3-8-Step-V2 --attention-backend VIDEO_SPARSE_ATTN_H3
```
## Regenerating a FastH3 draft
Only the draft model changes: a FastH3 checkpoint drafts at 768P (1344x768, 124 frames) and the regen stack stays as
released (base MiniMax-H3, merged LoRA, Sharp branches, three Euler steps). All H3-family drafts share the H3 VAE's latent
space (`[24, 37, 48, 84]` for a 124-frame 768P clip), so a FastH3 draft latent goes straight to the upscaler.
```bash
# FastH3 QAD (V2.1) NVFP4 draft, NVFP4 decode at 2K
python examples/inference/basic/basic_fasth3_to_regen2k.py \
--draft-model FastVideo/FastH3-QAD-v2 --draft-quant h3_dit_vsa \
--vae-decoder fasth3-nvfp4-vae/quality-36L/decoder-nvfp4.safetensors \
--prompt "A red fox trots through fresh snow" --output outputs/fasth3_regen2k
# FastH3 Trim NVFP4 draft: --draft-model FastVideo/FastVideo-FastH3-Trim-8-Step-NVFP4 --draft-quant h3_dit_vsa
# FastH3 V2 BF16 draft: --draft-model FastVideo/FastVideo-FastH3-8-Step-V2 (no --draft-quant)
```
Two handoffs are available:
- **Video:** decode the draft to an MP4 and pass it as `video_path`. The regen decodes it, resamples and VAE-encodes it (about
11 s).
- **Latent:** pass the draft's normalized native latent `[C, T, h, w]` as `draft_latent_path`, or per request as a `.pt`
`video_path` (through the OpenAI server it must be a local path under `server.media_roots`; not exercised in this run). The encode drops to 0.6 s and the
draft is never decoded to pixels.
The two give different 2K videos (24-27 dB PSNR against each other). On smooth content the MP4 route is much sharper (a
portrait: Laplacian variance 180-244 against 84-106 for the latent route). Writing the same draft as a lossless MP4 gives 85,
close to the latent route, so the extra energy comes from the H.264 artifacts of the draft MP4, which the refine turns into
texture. The latent route also leaves PSNR against the draft within 0.8 dB of the MP4 route. Prefer it: it is faster and does
not amplify codec noise.
Measured on one GB200 with nothing else on the node (regen at `TORCH_SDPA`, seed 1766580932, warm requests; fox, portrait
and harbor prompts, 5.2 s clips):
| Draft model | Draft | Regen: MP4 / latent / latent + NVFP4 decoder | Text to 2K (latent handoff) |
|---|---:|---:|---:|
| FastH3 QAD V2.1 NVFP4 | 36 s | 106 s / 96 s / 79 s | 132 s (115 s) |
| FastH3 Trim NVFP4 | 34 s | 106 s / 96 s / 79 s | 130 s (113 s) |
| FastH3 V2 BF16 | 35 s | 106 s / 96 s / 79 s | 131 s (114 s) |
| base H3 50-step | 316 s | 106 s / 96 s / 79 s | 412 s (395 s) |
The regen costs the same for every draft: about 70 s of refine (3 forwards at 133k tokens) and 24 s of decode. Setting
`h3_vae_nvfp4_decoder` to `quality-36L/decoder-nvfp4.safetensors` from `FastVideo/FastH3-NVFP4-VAE-Decoder` cuts the decode
to 7 s (82 s on the first call of a process) with no visible metric change. Against the draft, the 2K output keeps the scene
(PSNR 19-22 dB at 768P: the refine regenerates texture, so pixel agreement stays low for base and FastH3 drafts alike) and
raises Laplacian variance 1.7-3.3x with a 1.3-1.5x flicker ratio for every draft model. FastH3 drafts are sharper than base
drafts before the regen, so their absolute PSNR against the draft is about 2 dB lower.
## Parity with the reference
Measured on one GB200 (BF16, `TORCH_SDPA` with cuDNN attention) against the reference `standard_fwd` with the released
VAE, same draft, seed (1766580932), prompt and VAE posterior seed (42). The reference's noise is a CUDA generator draw
that FastVideo reproduces (the same `torch.Generator(device="cuda")` seed), so no injected noise was needed.
| Stage / quantity | Fox draft (FastVideo H3 t2v, 768P) | Dance draft (576P, project page) |
|---|---|---|
| Draft latent, upscaled latent, DiT input | bit-identical | bit-identical |
| One DiT forward (LoRA + Sharp), relative L2 | 1.1e-2 | 2.6e-2 |
| Refined latent, relative L2 | 1.3e-2 | 3.7e-2 |
| Decoded video PSNR / SSIM (21 frames) | 45.2 dB / 0.993 | 42.1 dB / 0.992 |
The differences come from text-encoder rounding (relative L2 1e-3 on the prompt embeddings) and BF16 kernel choices;
the reference reports the same order (1.4% relative L2 on the refined latent between its own runs).
Warm timings, one GB200 (seconds):
| | Upscale | Refine (3 steps) | VAE decode | Total |
|---|---:|---:|---:|---:|
| Reference `standard_fwd` | 0.87 | 71.6 | 15.0 | 87.5 |
| FastVideo (end to end, incl. draft encode 5-10 s and decode to host) | 5.9-10.7 (encode + upscale + silence) | 69.5 | 25.0 | 107.5 |
The first request builds nothing extra but loads weights (about 70 s once). The decode stage is slower than the
reference because it also copies 121 frames to host memory and the MP4 encode is outside both columns.
## Licences
The release is not covered by one licence; its `README.md` says so, and the runtime code's licence does not replace the
model licences. FastVideo only loads these weights and does not redistribute them.
- The refiner LoRA, the fast H3 VAE decoder and `base_model/` (a copy of `MiniMaxAI/MiniMax-H3` at `6818f6c`: DiT, VAEs,
text encoder) follow the
[MiniMax H3 Community License](https://huggingface.co/MiniMaxAI/MiniMax-H3/blob/6818f6c32d12b210915e44ad56a4228c2608f160/LICENSE),
including its territory, use and redistribution terms (`base_model/LICENSE`, `NOTICE`). The Qwen3-VL text encoder's
Apache-2.0 text ships as `LICENSE-Qwen.txt`.
- The latent upscaler follows its own licence:
[LBH-123-AI/Comfyui_Minimax_h3_latent_Upscaler](https://github.com/LBH-123-AI/Comfyui_Minimax_h3_latent_Upscaler)
(MIT, per the reference code) and its [weights](https://huggingface.co/LBH-123-AI/Minimax_h3_latent_Upscaler)
(`latent_upsampler/LICENSE`, Apache-2.0). `minimax_h3_latent_upscaler.py` is a port of that node's network.
- The merged LoRA includes [LightX2V FL2VA Turbo](https://huggingface.co/lightx2v/Minimax-h3-Turbo) and
[LMS Sharp](https://huggingface.co/Alissonerdx/Minimax-H3-ComfyUI), which keep their own licences; the Sharp
`adaln_t_table` is read from [Comfy-Org/MiniMax-H3](https://huggingface.co/Comfy-Org/MiniMax-H3).
+18
View File
@@ -114,6 +114,24 @@ the Preview checkpoint's sparse VSA blocks. Packed-varlen changes floating-point
reduction order relative to fixed-length FA4, so treat it as a speed/quality
evaluation option rather than an exact-parity mode.
### cuDNN SDPA for H3 dense attention (opt-in)
With the `FLASH_ATTN` backend, MiniMax-H3's dense, unmasked, inference-only DiT
self-attention can use cuDNN's fused SDPA engine instead of FlashAttention:
```bash
export FASTVIDEO_MINIMAX_H3_DENSE_ATTN=cudnn
```
It keeps the same precision contract (bf16 operands, fp32 softmax/accumulation,
bf16 output) but uses a different reduction order, so treat it as a
speed/quality option rather than an exact-parity mode. Training calls, compiled
regions, head sizes above 128, non-contiguous inputs, and the sparse VSA blocks
stay on their existing paths. Standalone kernel timings at 37.7k and 109k tokens
are within measurement noise of FA4; an in-model A/B on one GB200 measured a
2.2% faster DiT forward. Requires CUDA and a torch build with
`aten::_scaled_dot_product_cudnn_attention` (cuDNN 9.x).
### FP4 Flash Attention 4 (Blackwell only)
**`FLASH_ATTN`** with **`--nvfp4_fa4`**
-86
View File
@@ -1,86 +0,0 @@
# PAN 2 Text/Image to Video
PAN 2 is a 29B mixture-of-transformers video DiT with separate video and text streams, a Qwen3.5 text encoder and the
Wan2.2 VAE. One pipeline serves text-to-video and image-to-video: pass `image_path` to condition on a first frame and
leave it out for text only.
## Model
| Hub repo | Pipeline | Workloads | Example |
|---|---|---|---|
| `IFM/PAN2` | `PAN2ModularPipeline` | T2V, I2V | [`basic_pan2.py`](https://github.com/hao-ai-lab/FastVideo/blob/main/examples/inference/basic/basic_pan2.py) |
PAN2 checkpoints use the Modular Diffusers layout (`modular_model_index.json`). Any repo id or local directory whose
name contains `pan2` (or whose manifest declares `PAN2ModularPipeline`) resolves to this pipeline. The defaults come
from the `pan2_t2v` and `pan2_i2v` presets:
| Setting | Default |
|---|---|
| Resolution | 704x1248 (height and width must be multiples of 32) |
| Frames | 313, 13 s at 24 fps (`num_frames - 1` must be a multiple of 4) |
| Steps | 50, uniform sigmas from 1 to 0 with the checkpoint's flow shift (9 for `IFM/PAN2-TEST`, the reference sampler's value) |
| Guidance | 3.0, classifier-free guidance. Image-to-video keeps the first latent frame out of guidance (`i2v_skip_first_latent_cfg`, on by default, as in the reference sampler) |
| Negative prompt | None unless the request carries one. The reference sampler always sends the release's structured negative caption (`negative_prompt.json` of the PAN2 repository) with its three `time_range_sec` values set to the clip length, `0 to 20` for 481 frames at 24 fps |
PAN 2 is trained on the structured captions its prompt rewriter produces, serialized as one-line JSON. Short raw
prompts work, with weaker results.
## Usage
```bash
python examples/inference/basic/basic_pan2.py
```
```python
from fastvideo import VideoGenerator
generator = VideoGenerator.from_pretrained(
"IFM/PAN2",
num_gpus=1,
dit_cpu_offload=False,
dit_layerwise_offload=False,
text_encoder_cpu_offload=True,
)
generator.generate_video(
"A red fox trots through fresh snow in a pine forest at dawn.",
image_path=None, # or the path of a first frame for image-to-video
output_path="video_samples_pan2",
)
```
For image-to-video, the first frame is scaled to cover the requested resolution and center cropped, then encoded by the
VAE into the first latent frame of the conditioning input.
## Parallelism
With `num_gpus > 1`, FastVideo runs Ulysses sequence parallelism: the video tokens are sharded across ranks and the
text tokens are replicated. The number of GPUs must divide the 32 attention heads. Sequence-parallel padding of the
video tokens is trimmed before attention, so no attention mask is needed.
Padding is always excluded, without an opt-in setting. At 704x1248 with 313 frames, the 67,782 video tokens need
two padding tokens at SP=4. They are removed before joining the text tokens in every attention block. This provides
the same exclusion as vLLM-Omni's unconditional padding mask in
[revision `e0c4c731`](https://github.com/vllm-project/vllm-omni/commit/e0c4c73158a5e4f44eda8eaa8879f7fd9cedbd74).
The trimmed sequence can use either FlashAttention or Torch SDPA.
```bash
PAN2_NUM_GPUS=4 python examples/inference/basic/basic_pan2.py
```
## Memory and offloading
The example keeps the DiT on the GPUs and offloads the text encoder between requests. Sequence parallelism shards
video tokens; each rank still holds the DiT weights. Increasing `num_gpus` alone does not shard those weights.
For devices that cannot hold the DiT, see [CPU offloading](offloading.md) for `dit_cpu_offload` and
`dit_layerwise_offload`. Hardware recipes and timings reported by vLLM-Omni use its own offload implementation;
they have not been validated for FastVideo.
The full `IFM/PAN2` checkpoint is not public yet. Local parity tests use the public random-weight
`wuqing157/tiny-pan2-modular-pipe` checkpoint; it verifies the integration and produces noise rather than useful video.
## Differences from the vLLM-Omni integration
- The optional prompt and video guardrails (`pan2-guardrail`) are not included.
- The Cache-DiT `quality` policy and CFG-parallel execution are vLLM-Omni serving features and are not ported.
- The text encoder is a FastVideo-native Qwen3.5 port. Its Gated DeltaNet layers use a PyTorch chunked delta rule
instead of the `fla` kernels.
+237
View File
@@ -0,0 +1,237 @@
# Pika H3 Flex
Pika H3 Flex is a post-trained MiniMax-H3 model that keeps H3's packed
video/audio transformer, Qwen3-VL conditioner, and video/audio VAEs. Pika ships
two rank-128 PEFT adapters instead of a second base checkpoint:
| Adapter | Trunk | Tasks |
|---|---|---|
| `fl2va/adapter.safetensors` | `transformer` | T2VA, I2VA, L2VA, FL2VA |
| `ref2va/adapter.safetensors` | `transformer_ref` | Ref2VA |
FastVideo detects `Pika-Labs/Pika-H3-Flex`, downloads only the selected task's
adapter, and applies it as an ordinary LoRA. The released adapter is selected
automatically; pass `lora_path` only when overriding it with a compatible
Pika adapter.
## Sampling contract
Pika's sampler is fixed, not a tunable step count:
* 6 transformer forwards over clean-time rungs
`1000, 750, 500, 250, 100, 40, 0`;
* video shift `12.0`, audio shift `3.0`;
* no CFG, `guidance_scale=1.0`, empty negative prompt;
* each step re-noises the predicted clean sample with fresh Gaussian noise from
a generator seeded `seed + 1`.
`num_inference_steps` must therefore be `6`; other values are rejected. This is
not a PDD checkpoint and it has no trained VSA gates, so PDD fused blocks and
VSA sparsity are not used.
## Tasks
The commands below assume a FlashAttention-4 host; see
[Attention backend](#attention-backend) before running them elsewhere.
```bash
export FASTVIDEO_ATTENTION_BACKEND=FLASH_ATTN FASTVIDEO_FA4=1
# text to video + audio
python examples/inference/basic/basic_pika_h3_flex.py --task t2va \
--prompt "a paper boat drifting down a rainy street"
# 480p portrait, ten seconds
python examples/inference/basic/basic_pika_h3_flex.py --task t2va \
--resolution 480p --aspect 9:16 --duration 10 \
--prompt "a lighthouse at dusk, waves breaking below"
# first frame, last frame, or both
python examples/inference/basic/basic_pika_h3_flex.py --task i2va \
--image first.png --prompt "the camera slowly pushes in"
python examples/inference/basic/basic_pika_h3_flex.py --task fl2va \
--image first.png --last-image last.png --prompt "the flower opens"
# ordered references, including reference video and audio
python examples/inference/basic/basic_pika_h3_flex.py --task ref2va \
--reference image:person.png --reference video:dance.mp4 \
--reference audio:voice.wav --prompt "the person dances to the music"
```
Ref2VA accepts up to 12 references: 9 images, 3 videos, and 3 audio. These
counts are enforced locally, as is the rule that audio-only requests are
invalid. A video reference needs at least 22 decoded frames at 24 fps. At least
one image or video is required, and the Ref2VA pipeline must be selected with
`PikaH3FlexRef2VAModularPipeline` (the example does this).
Pika's hosted API also documents a 2–15 s limit per video/audio reference and a
15 s total per modality. **The local path does not currently enforce those
duration caps**; it trims each reference to the requested output duration. Treat
the per-reference and per-modality limits as recommendations until the checks
are added.
## Canvas and frames
Pika uses an exact 32-pixel bucket lattice. A request that matches a bucket is
kept exactly; otherwise FastVideo picks the tier from the requested short edge
(`<= 480` → p480, else p768) and snaps to the nearest aspect bucket in that tier.
| Tier | Short edge | Max pixels | Reference-image short edge |
|---|---|---|---|
| p480 | 480 | 832×480 | 1024 |
| p768 | 768 | 1344×768 | 2048 |
Frame counts follow Pika's `17n + 5` grid at 24 fps (5–15 s, 120–362 aligned
frames). The default 16:9 p768 canvas is 1344×768 and 124 frames.
| Duration | 5 s | 10 s | 15 s |
|---|---|---|---|
| `num_frames` | 124 | 243 | 362 |
The example script picks the bucket with `--resolution {480p,768p}` and
`--aspect` (`21:9`, `16:9`, `3:2`, `4:3`, `5:4`, `1:1`, `4:5`, `3:4`, `2:3`,
`9:16`, `9:21`), and the frame count with `--duration` seconds. `--height`,
`--width`, and `--num-frames` override them.
Through the API, a request that omits `height` and `width` uses the 16:9
bucket of the configured tier:
```python
PipelineSelection(experimental={"pika_resolution_tier": "p480"}) # 832x480 when the size is omitted
```
An explicit `height`/`width` always wins and selects its own tier. "Omitted"
means absent from a request parsed from a mapping or config file, or from a
`generate_video(...)` call without `height`/`width`; a `GenerationRequest`
constructed in Python counts `SamplingConfig`'s 1280×720 default as set, which
snaps to the 768p 16:9 bucket. Pass the size explicitly in that case.
Pika center-crops both keyframes to the bucket. MiniMax-H3 stretches its first
frame instead; do not reuse that fit for Pika.
## Attention backend
Run Pika with FlashAttention-4 on Hopper or Blackwell:
```bash
FASTVIDEO_ATTENTION_BACKEND=FLASH_ATTN FASTVIDEO_FA4=1 \
python examples/inference/basic/basic_pika_h3_flex.py --task t2va --prompt "..."
```
FA4 is opt-in. Installing `flash_attn.cute` is not enough:
`fastvideo/attention/utils/flash_attn_default.py` only selects it when
`FASTVIDEO_FA4=1`, and neither the example's defaults nor `--lossless-accel`
set that variable. With `FASTVIDEO_FA4=1` and an unusable FA4 install the run
fails at import instead of silently falling back.
Without FA4, the `FLASH_ATTN` backend uses FA3 or FA2 if one is installed. On a
host with neither (common on Blackwell), backend selection falls back to
`TORCH_SDPA`. SDPA at 768p is much slower and can run out of memory, so do not
use it for 768p benchmarks or production runs.
## Performance and memory
All measurements: one GB200, 768p 16:9 (1344×768), 5 s (124 frames), six
forwards, warm requests, FA4, every component resident (`--offload resident`).
| Configuration (T2VA) | Generation time |
|---|---|
| Official `pika-h3-pro` CLI | 59.7 s |
| FastVideo, BF16 | 26.2 s |
| FastVideo, NVFP4 `quality-36L` decoder | 18.0 s |
| FastVideo, NVFP4 `fast-8L` decoder | 17.4 s |
The 36L decoder measured 40.5 dB PSNR against the BF16 decoder on the same
latents. The 8L decoder was timed but not scored here.
Task coverage, seed 42, NVFP4 text encoder, BF16 decoder, also with
`FASTVIDEO_MINIMAX_H3_FUSIONS=all`:
| Task | Wall | Denoise | Decode | Peak memory | Check |
|---|---|---|---|---|---|
| T2VA | 25.49 s | 16.70 s | 8.04 s | 104.6 GiB | on prompt; 21.7 dB PSNR vs the BF16-encoder output |
| I2VA | 27.37 s | 18.02 s | 8.23 s | 104.9 GiB | first frame vs input 26.5 dB, SSIM 0.715 |
| FL2VA | 28.95 s | 19.49 s | 8.12 s | 105.2 GiB | first frame 39.6 dB / 0.971, last frame 29.1 dB / 0.819 |
| Ref2VA, two image references | 51.17 s | 38.42 s | 7.74 s | 108.5 GiB | 64,241 packed tokens; both references used |
The NVFP4 text encoder's first request in a fresh process paid a one-time
first-use cost of about 183 s; later requests did not.
Peak resident memory for the BF16 FastVideo T2VA run was about 129 GiB. The
official CLI's sampling peak was 71.7 GiB; the two figures measure different
things (process-resident memory versus the sampling phase).
The example's default `--offload lazy` loads components on demand and offloads
the encoders and VAE. It fits smaller devices but reloads the DiT on every
request: the same T2VA request took 48.06 s (28.31 s denoise) on GB200 in that
mode. Use `--offload resident` when the device holds every component.
## Training-free acceleration
The 6 forwards already use FastVideo's normal inference path. Available
lossless switches:
* `FASTVIDEO_MINIMAX_H3_EXACT_KERNELS=all` — exact fused H3 kernels;
* `FASTVIDEO_H3_REF2VA_MEMO_ENTRIES=16` — cache reference conditioning
(Ref2VA);
* `FASTVIDEO_H3_VAE_TILE_PARALLEL=1` — parallel VAE tiles;
* `FASTVIDEO_ULYSSES_A2A=auto` — Ulysses all-to-all for multi-GPU sequence
parallelism;
* `OffloadConfig(lazy_module_load=True)` and `dit_layerwise=True` for smaller
GPUs;
* `CompileConfig(enabled=True)` for `torch.compile`;
* FlashAttention-4 through `FASTVIDEO_ATTENTION_BACKEND=FLASH_ATTN
FASTVIDEO_FA4=1` (see [Attention backend](#attention-backend)).
`--lossless-accel` in the example enables the first four before the generator
starts. It does not enable FA4; set the two attention variables at launch. Kernel work beyond the existing exact kernels is intentionally not part
of this integration.
## NVFP4 on Blackwell
Pika can reuse FastVideo's existing H3 NVFP4 VAE decoder and NVFP4 Qwen3-VL
text encoder. Both are weight formats, not new sampler paths; the six-forward
Pika schedule is unchanged.
```bash
# 36-block quality decoder: needs the full 36-block H3 VAE
python examples/inference/basic/basic_pika_h3_flex.py --task t2va \
--prompt "a paper boat drifting down a rainy street" \
--nvfp4-decoder ./FastH3-NVFP4-VAE-Decoder/quality-36L/decoder-nvfp4.safetensors
# 8-block fast decoder: depth-cuts any H3 VAE with at least 8 blocks
python examples/inference/basic/basic_pika_h3_flex.py --task ref2va \
--reference image:person.png --reference video:dance.mp4 \
--prompt "the person dances to the music" \
--nvfp4-decoder ./FastH3-NVFP4-VAE-Decoder/fast-8L/decoder-nvfp4.safetensors \
--nvfp4-text-encoder ./fasth3-nvfp4-encoder/text_encoder
```
* `quality-36L` keeps the full decoder depth and is the closest match to Pika's
dense decoder. `fast-8L` is much smaller and faster but is a distilled
student; expect a visible quality drop. Pika's six-step re-noise latents are
not the student's original training distribution, so validate 8L on your
prompts before using it for delivery.
* The NVFP4 decoder replaces only `vae.decoder`; keyframe and reference
encoding still use the FP32 VAE encoder.
* The NVFP4 text encoder quantizes the Qwen3-VL language stack while keeping
the vision tower in bf16, so Ref2VA reference images/videos still work.
* Do not select `taeh3` with an NVFP4 decoder; `h3-vae` is required.
* Both options require Blackwell FP4 hardware. The fused decoder kernels and
CUDA graphs are already enabled by default; see
`FASTVIDEO_H3_VAE_NVFP4_FUSED` and `FASTVIDEO_H3_VAE_NVFP4_CUDA_GRAPH`.
* There is no NVFP4 **VAE encoder** path. The H3 video VAE encoder is a causal
3D CNN and stays FP32; quantizing it would need a separate convolution
kernel/quantization effort.
If a process first loads the 8L decoder it depth-cuts the VAE to eight blocks;
loading 36L afterwards in the same process is not supported. Choose one decoder
tier per process.
## Hardware
The selected H3 trunk plus Qwen3-VL does not fit comfortably on a 24 GB card
without offload. A single 80 GB GPU can run with text-encoder offload; use
layerwise DiT offload or multiple GPUs for smaller devices. Ref2VA encodes
reference images at up to a 2048 short edge, so its conditioning memory is
higher than T2VA's.
+5 -1
View File
@@ -68,7 +68,8 @@ column links a runnable script in `examples/inference/basic/` where one exists.
| matrixgame | `FastVideo/Matrix-Game-2.0-Base-Distilled-Diffusers`<br />`FastVideo/Matrix-Game-2.0-GTA-Distilled-Diffusers`<br />`FastVideo/Matrix-Game-2.0-TempleRun-Distilled-Diffusers`<br />`FastVideo/Matrix-Game-2.0-Base-Diffusers`<br />`FastVideo/Matrix-Game-2.0-GTA-Diffusers`<br />`FastVideo/Matrix-Game-2.0-TempleRun-Diffusers`<br />`mignonjia/mg_longtuning_distilled_zelda`<br />`mignonjia/mg_sf_distilled_zelda_1k_steps`<br />`mignonjia/mg_sf_distilled_zelda`<br />`mignonjia/mg_causal_zelda`<br />`mignonjia/mg_bidirectional_zelda` | I2V | [basic_matrixgame2.py](https://github.com/hao-ai-lab/FastVideo/blob/main/examples/inference/basic/basic_matrixgame2.py) |
| matrixgame | `FastVideo/Matrix-Game-3.0-Base-Distilled-Diffusers` | I2V | [basic_matrixgame3.py](https://github.com/hao-ai-lab/FastVideo/blob/main/examples/inference/basic/basic_matrixgame3.py) |
| minimax_h3 | `MiniMaxAI/MiniMax-H3` | T2V, I2V | [T2VA](https://github.com/hao-ai-lab/FastVideo/blob/main/examples/inference/basic/basic_minimax_h3_t2v.py)<br />[FL2VA](https://github.com/hao-ai-lab/FastVideo/blob/main/examples/inference/basic/basic_minimax_h3_fl2va.py)<br />[Ref2VA](https://github.com/hao-ai-lab/FastVideo/blob/main/examples/inference/basic/basic_minimax_h3_ref2va.py) |
| pan2 | `IFM/PAN2` | T2V, I2V | [basic_pan2.py](https://github.com/hao-ai-lab/FastVideo/blob/main/examples/inference/basic/basic_pan2.py) |
| pika_h3_flex | `Pika-Labs/Pika-H3-Flex` | T2V, I2V | [T2VA/I2VA/L2VA/FL2VA/Ref2VA](https://github.com/hao-ai-lab/FastVideo/blob/main/examples/inference/basic/basic_pika_h3_flex.py) |
| open_h3_regenerate_2k | `Efficient-Large-Model/Open-H3-Regenerate-2K` | — | [basic_open_h3_regenerate_2k.py](https://github.com/hao-ai-lab/FastVideo/blob/main/examples/inference/basic/basic_open_h3_regenerate_2k.py) |
| sd35 | `stabilityai/stable-diffusion-3.5-medium` | T2I | [basic_sd35_t2i.py](https://github.com/hao-ai-lab/FastVideo/blob/main/examples/inference/basic/basic_sd35_t2i.py) |
| qwen_image21 | `Qwen/Qwen-Image-2.1` | T2I, I2I | [basic_qwen_image21.py](https://github.com/hao-ai-lab/FastVideo/blob/main/examples/inference/basic/basic_qwen_image21.py) |
| stable_audio | `FastVideo/stable-audio-open-1.0-Diffusers` | T2V | [basic_stable_audio.py](https://github.com/hao-ai-lab/FastVideo/blob/main/examples/inference/basic/basic_stable_audio.py) |
@@ -122,6 +123,9 @@ and distilled) generate video with audio from text, optionally plus an image (se
`kandinsky6_sr` IDs upscale an existing video (see the
[Video SR guide](kandinsky6_sr.md)).
**Note (Open-H3-Regenerate-2K)**: `open_h3_regenerate_2k` regenerates an existing 768P MiniMax-H3 draft video at
2560x1440 (video to video, silent output); see the [Open-H3-Regenerate-2K guide](open_h3_regenerate_2k.md).
**Note (LTX-2.5)**: T2V and I2V produce synchronized video and audio. The
official `Lightricks/LTX-2.5` repository uses gated, split component files, so
it must first be converted to FastVideo's component layout. See the
+14
View File
@@ -395,6 +395,20 @@ function resolveConfig(catalog, selections = {}) {
}
}
validateTopology(catalog, config);
const engine = config.generator?.engine ?? {};
const experimental = config.generator?.pipeline?.experimental ?? {};
if (engine.quantization?.layer_profile && engine.quantization.transformer_quant?.toLowerCase() !== "nvfp4") {
throw invalidValue(
"A quantization layer profile requires NVFP4 transformer quantization.",
"generator.engine.quantization.transformer_quant",
);
}
if (experimental.video_decode_backend === "taeh3" && experimental.h3_vae_nvfp4_decoder) {
throw invalidValue(
"TAEH3 uses its own decoder. Clear the NVFP4 decoder override or select h3-vae.",
"generator.pipeline.experimental.h3_vae_nvfp4_decoder",
);
}
const argv = [...catalog.runtime.launch_argv];
const environment = Object.entries(catalog.env).map(([key, value]) => `${key}=${shellQuote(value)}`);
const sample = sampleRequest(catalog, config);
+8
View File
@@ -32,6 +32,14 @@ export const WORKLOADS = Object.freeze({
extension: "wav",
media_type: "audio/wav",
}),
v2v: Object.freeze({
input: "video",
output: "video",
endpoint: "/v1/videos/sync",
extension: "mp4",
media_type: "video/mp4",
reference: "video_reference",
}),
v2a: Object.freeze({
input: "video",
output: "audio",
-1
View File
@@ -1 +0,0 @@
/Users/aryank/os/FastVideo-review-int/docs/node_modules
@@ -12,6 +12,25 @@ function at(value, path) {
return path.split(".").reduce((current, key) => current?.[key], value);
}
test("every local NVFP4 encoder choice includes its download requirement", () => {
const path = "generator.pipeline.components.text_encoder_weights";
const catalogs = buildCatalogs().filter((catalog) => catalog.controls.some((control) => control.path === path));
assert.ok(catalogs.length > 0);
for (const catalog of catalogs) {
const option = catalog.controls.find((control) => control.path === path);
if (!option.schema.enum?.includes("./fasth3-nvfp4-encoder/text_encoder")) continue;
assert.ok(
catalog.requirements.some(
(requirement) =>
requirement.includes("hf download ") &&
requirement.includes("--include 'text_encoder/*'") &&
requirement.includes("--local-dir fasth3-nvfp4-encoder"),
),
`${catalog.id} must explain how to create the selected encoder directory`,
);
}
});
function fixture({ interfaceName = "rest", backend = "cuda", workload = "t2v" } = {}) {
return {
id: "example/deployment",
+22
View File
@@ -16,6 +16,27 @@ import {
const require = createRequire(import.meta.url);
test("configuration output rejects ignored decoder overrides and incompatible quantization profiles", () => {
const data = catalog();
data.base_config.generator.pipeline.experimental = {
video_decode_backend: "taeh3",
h3_vae_nvfp4_decoder: "decoder.safetensors",
};
assert.throws(
() => resolveConfig(data),
(error) => {
assert.equal(error.path, "generator.pipeline.experimental.h3_vae_nvfp4_decoder");
return /TAEH3 uses its own decoder/.test(error.message);
},
);
data.base_config.generator.pipeline.experimental.h3_vae_nvfp4_decoder = null;
assert.doesNotThrow(() => resolveConfig(data));
data.base_config.generator.engine.quantization = { transformer_quant: "FP8", layer_profile: "h3_dit_vsa" };
assert.throws(() => resolveConfig(data), /requires NVFP4/);
data.base_config.generator.engine.quantization.transformer_quant = "NVFP4";
assert.doesNotThrow(() => resolveConfig(data));
});
function catalog() {
return {
id: "example/rest",
@@ -488,6 +509,7 @@ test("sample requests follow shared workload media", () => {
t2i: { endpoint: /\/v1\/images\/generations/, output: /--output output\.json/, reference: null },
t2a: { endpoint: /\/v1\/videos\/sync/, output: /--output output\.wav/, reference: null },
v2a: { endpoint: /\/v1\/videos\/sync/, output: /--output output\.wav/, reference: "video_reference" },
v2v: { endpoint: /\/v1\/videos\/sync/, output: /--output output\.mp4/, reference: "video_reference" },
};
for (const [workload, expected] of Object.entries(expectations)) {
const data = catalog();
+1 -1
View File
@@ -56,7 +56,7 @@ torchrun --nproc_per_node=$GPU_NUM \
| `--preprocess.dataset_path` | Path to dataset (HF repo ID or local folder) |
| `--preprocess.dataset_output_dir` | Output directory for Parquet files |
| `--preprocess.video_loader_type` | Video decoder: `torchcodec` or `torchvision` |
| `--preprocess.max_height` / `max_width` | Target resolution for videos |
| `--preprocess.max_height` / `--preprocess.max_width` | Target resolution for videos |
| `--preprocess.num_frames` | Number of frames to extract per video |
| `--preprocess.train_fps` | Target FPS for frame extraction |
+3 -3
View File
@@ -1,12 +1,12 @@
# 🧠 Finetuning
# Fine-tuning
This guide covers finetuning video diffusion models with FastVideo, including full finetuning and LoRA.
## Training Arguments
## Training arguments
FastVideo training scripts use several argument groups:
### Training Arguments
### Loop and model arguments
| Argument | Description |
|----------|-------------|
+23 -23
View File
@@ -12,7 +12,7 @@ parquet that step produces.
## What you need
| | |
| Requirement | Value |
|---|---|
| GPU | 48GB or more for the LoRA overfit; multi-GPU for full-parameter runs |
| Disk | ~40GB for the checkpoint (DiT 17GB + Qwen2.5-VL 16GB + ByT5/VAE) |
@@ -145,16 +145,10 @@ a real problem — stop and investigate rather than burning GPU hours.
## 4. Scale out
`examples/train/configs/fine_tuning/hunyuan15/t2v.yaml` runs
full-parameter training across 8 GPUs with FSDP. Full-parameter training
does not fit one 80GB card:
full-parameter training across 8 GPUs with FSDP. One 80 GB card cannot hold it:
It reads `data/hunyuan15_preprocessed` (and its `validation_prompts.json`),
not the overfit directory above — either preprocess into that path (set
`FASTVIDEO_TEST_HUNYUAN15_OVERFIT_OUTPUT_DIR=data/hunyuan15_preprocessed`) or
pass `--training.data.data_path` and `--callbacks.validation.dataset_file`.
| | bf16 |
|---|---|
| Item | Per rank (bf16) |
|---|---:|
| Weights | 16.7 GB |
| Gradients | 16.7 GB |
| AdamW `exp_avg` + `exp_avg_sq` | 33.4 GB |
@@ -163,21 +157,27 @@ pass `--training.data.data_path` and `--callbacks.validation.dataset_file`.
| **Total** | **~80 GB** |
That subtotal is independent of sequence length, so shortening the clip
(`--training.data.num_latent_t`) barely helps — measured peak memory moved
by 0.07GB going from 21 latent frames down to 9. Options that do move the
needle:
(`--training.data.num_latent_t`) barely helps: measured peak memory moved by
0.07 GB going from 21 latent frames to 9. Options that do move the needle:
- **LoRA** (what the overfit config uses) — gradients and optimizer state
shrink to the adapter parameters, ~30GB total.
- **FSDP sharding across GPUs** — the 66.8GB subtotal divides by the number
of shards.
- **A memory-light optimizer** — SGD with momentum keeps a single state
tensor instead of two. `torch.optim.Adafactor` is smaller still, but is
not DTensor-safe: its in-place `.square_()` fails under FSDP2 with
`aten.pow_.Scalar: in-place operations that require placement changes
are not supported`.
- **LoRA** (what the overfit config uses): gradients and optimizer state
shrink to the adapter parameters, about 30 GB total.
- **FSDP sharding across GPUs**: the 66.8 GB subtotal divides by the number of
shards.
- **A memory-light optimizer**: SGD with momentum keeps a single state tensor
instead of two. `torch.optim.Adafactor` is smaller still, but is not
DTensor-safe. Its in-place `.square_()` fails under FSDP2 with
`aten.pow_.Scalar: in-place operations that require placement changes are
not supported`.
Set `training.dit_precision: bf16` for any single-device run. The default
The config reads `data/hunyuan15_preprocessed` and its
`validation_prompts.json`, not the overfit directory above. Either preprocess
into that path (set
`FASTVIDEO_TEST_HUNYUAN15_OVERFIT_OUTPUT_DIR=data/hunyuan15_preprocessed`) or
pass `--training.data.data_path` and `--callbacks.validation.dataset_file`.
The shipped overfit config already sets `training.dit_precision: bf16`. If you
write your own config, set it for any single-device run. The default
is fp32, which doubles weights, gradients, and optimizer state.
## Troubleshooting
+253
View File
@@ -0,0 +1,253 @@
# FastH3 NVFP4 QAD
This page describes the shared quantization-aware distillation (QAD) recipe for
the FastH3 (MiniMax-H3) family. One recipe covers two model profiles:
| Profile | Model plugin | Task | Sampling | VSA tile |
|---|---|---|---|---|
| OmniRef | `omniref_pdd.MiniMaxH3OmniRefPDDModel` | Ref2VA | 8-forward PDD ladder | 128 |
| V2 T2AV QAD | `v2_t2v.MiniMaxH3V2T2VModel` | T2VA | 8-step DMD | 64 |
Both profiles use the same shared recipe: NVFP4 W4A4 linears in all 50 blocks,
the FP4 sparse-attention emulator, and the same objective and guards. QAD
trains the student against a frozen bf16 teacher so that quantization error is
absorbed by the weights instead of appearing as drift, ghosting or audio loss
at inference. The published OmniRef QAD checkpoint includes FP4 attention; the
published FastH3-QAD-v2 checkpoint predates it and keeps BF16 VSA attention (see
[Released checkpoints](#released-checkpoints)).
The runnable configs are:
- `examples/train/configs/knowledge_distillation/minimax_h3/omniref_nvfp4_qad.yaml`
- `examples/train/configs/knowledge_distillation/minimax_h3/v2_nvfp4_qad.yaml`
## Released checkpoints
| Role | Checkpoint |
|---|---|
| OmniRef QAD result | `FastVideo/FastH3-OmniRef-NVFP4-QAD` |
| FastH3-QAD-v2 result | `FastVideo/FastH3-QAD-v2` |
| V2 bf16 teacher / starting weights | `FastVideo/FastVideo-FastH3-8-Step-V2` |
| NVFP4 consumer baseline | `FastVideo/FastVideo-FastH3-8-Step-V2-NVFP4-Consumer` |
The OmniRef config starts from the **calibrated OmniRef checkpoint used as QAD
step 0**, not from the released OmniRef QAD result; point `init_from` at that
checkpoint. The V2 config starts from the bf16 V2 snapshot.
The published FastH3-QAD-v2 checkpoint is the **linears-only variant** of this
schedule: NVFP4 W4A4 linears with BF16 VSA attention, 300 steps. The shared
FP4 recipe on this page additionally runs the FP4 attention emulator from step
0; it is the same training code and objective, but it does not reproduce the
published FastH3-QAD-v2 checkpoint bit for bit. Set
`models.student.qad.fp4_attention.enabled: false` to get the published
linears-only variant.
## Target numerics
The student starts from the bf16 masters, and the deployed numerics are active
from the first forward, so every update sees the quantization error it is
trained to absorb.
- **Linears:** NVFP4 W4A4 for `attn.to_q`, `attn.to_k`, `attn.to_v`,
`attn.to_out`, `ff.fc_in` and `ff.fc_out` in every block.
- FFN inputs use static activation scales from the calibrated `amax` table.
- Attention and gate linears use the unit activation scale; their `amax`
entries are optional.
- `skip_blocks` is empty: no block falls back to bf16 linears.
- **Attention:** the FP4 sparse-attention emulator for VSA tile 128 (OmniRef)
or tile 64 (V2 T2AV). Two-level P quantization and K smoothing are on. The
emulator reproduces the deployed kernel's block order and rounding on any
CUDA GPU and provides straight-through gradients.
- **Kept frozen:** the VSA compression gates and every norm stay at their
released values. Training the gate weights drifts the sparse-attention tile
selection and produces ghosting; the gate linears still execute the deployed
NVFP4 format, they are just not updated. `proj_in`, the time embedder,
`proj_out` and the audio projections stay fp32 and frozen, matching
inference.
The calibrated `amax` tables for both profiles are vendored under
`examples/train/calibration/fasth3/`:
| Table | sha256 |
|---|---|
| `amax-omniref-mse-bf16-top8.json` | `14b93ec3…df083d` |
| `amax-consumer.json` | `0800516c…d39e52` |
`qad.linears.amax_sha256` pins the table; the run copies it into the output
directory and refuses to start if the bytes differ.
## Training objective
For each row the frozen bf16 teacher walks the profile's sampling ladder. At
the same inputs and timesteps, the student minimizes:
- **Output regression:** per-forward x0 error, normalized per forward and
modality by its step-0 value.
- **Layer-wise reconstruction:** each attention and FFN sub-layer receives the
teacher's input (stop-gradient) and matches the teacher sub-layer's output.
The loss is weighted per row group (target video and audio 1.0,
conditioning 0.25) because audio is a small fraction of the packed tokens.
`recon_normalize: step0` divides it by its step-0 value so `recon_lambda`
balances the two terms.
From `rollout_start_step`, `rollout_fraction` of the rows continue from the
student's own trajectory instead of the teacher's, which reduces exposure bias
in the sampled output.
## Evaluation and guards
- **Teacher-forced eval** every `eval_every` steps on held-out rows.
- **Full eval** every `full_eval_every` steps: the student's own sampled output
against the teacher at the same seed, keyframe alignment, and saved audio and
video latents.
- **Noise floor:** `noise_floor_eval` re-runs the step-0 eval at seed + 1 so a
metric move can be compared with teacher seed noise.
- **Best checkpoint:** selected by the teacher-forced `score`. A checkpoint is
eligible only if the keys in `eligibility_keys` (audio error and endpoint)
stay within their step-0 guardrails.
- **Auto-stop:** after `patience` consecutive bad evals.
- **Step-0 cache:** `step0_cache` fingerprints the teacher, student,
calibration, quantization plan, eval set and loss weights, so reruns skip the
baseline evaluation.
- **Resume:** checkpoints carry the step-0 baseline and guard state; do not
delete `method.qad_baseline` if you edit a checkpoint by hand.
## Data preparation
V2 T2AV is data-free: it needs prompts and their Qwen3-VL text embeddings, not
VAE latents.
1. Build the split manifests from prompt sources (parquet shards with a
`prompt` column, JSONL with a `prompt` field):
```bash
python scripts/distill/minimax_h3_v2_qad/build_v2_prompts.py \
--parquet 'data/prompts/*.parquet' --out-dir data/fasth3-v2 --eval-count 30
```
This writes `train.jsonl` and `eval.jsonl` plus an `embeddings/` directory.
2. Fill the embeddings (safe to rerun and shard across GPUs):
```bash
python scripts/distill/minimax_h3_v2_qad/precompute_v2_text.py \
--config examples/inference/basic/basic_fasth3_spark_v2_nvfp4.yaml \
--model-path data/models/FastH3-V2 \
--manifests data/fasth3-v2/train.jsonl data/fasth3-v2/eval.jsonl \
--shard 0 --num-shards 8
```
The V2 config's `train_manifest` and `eval_manifest` point at these files.
OmniRef training rows are preprocessed Ref2VA parquet rows that carry the
reference latents, text embeddings and geometry; the encoded dataset itself is
an input to this recipe and is not produced by the training code. Provide a
`MANIFEST.jsonl` with `case`, `id`, `parquet` and `resolution` fields and point
`models.student.data.manifests` at it.
`scripts/distill/minimax_h3_omniref_qad/generate_omniref_latents.py` builds the
held-out eval plan and runs the PDD ladder once per row: use `--eval-dir` to
write the held-out `manifest.json` consumed by `models.student.data.eval_manifest`
(and the raw latents for decoding).
## Running
Fill in the placeholder paths in the config (starting checkpoint, data
manifests, output directory), then launch with `torchrun`.
OmniRef, 4 nodes x 4 GPUs (sequence parallel 4):
```bash
torchrun --nnodes=4 --nproc_per_node=4 --node_rank=$NODE_RANK \
--master_addr=$MASTER_ADDR --master_port=$MASTER_PORT \
-m fastvideo.train.entrypoint.train \
--config examples/train/configs/knowledge_distillation/minimax_h3/omniref_nvfp4_qad.yaml
```
V2 T2AV, 4 nodes x 4 GPUs (16-way HSDP, lr 5e-6, 300 steps):
```bash
torchrun --nnodes=4 --nproc_per_node=4 --node_rank=$NODE_RANK \
--master_addr=$MASTER_ADDR --master_port=$MASTER_PORT \
-m fastvideo.train.entrypoint.train \
--config examples/train/configs/knowledge_distillation/minimax_h3/v2_nvfp4_qad.yaml
```
To reproduce the published FastH3-QAD-v2 linears-only checkpoint, disable FP4 attention
for the same schedule:
```bash
--models.student.qad.fp4_attention.enabled false
```
## FP4-attention adaptation
The internal follow-up that this recipe tracks started from a linears-only
300-step DCP, enabled FP4 attention, and trained 300 more steps at lr 1.5e-6
with no warmup and a constant LR on 16 GPUs. It is an **adaptation of the
linears-only checkpoint to FP4 attention**, not a second epoch of the FP4
stage 1 above: the source checkpoint's attention numerics were BF16, and the
continuation needs a DCP of that linears-only model (the packed HF release is
not one). It is therefore not part of the public reproduction path, and the
published FastH3-QAD-v2 checkpoint remains the linears-only result. The exact overrides
are listed at the bottom of the V2 config.
## Checkpoints
Runs write regular `checkpoint-<step>/` DCP checkpoints plus a `best/`
directory:
- `best/`: the best eligible state (weights only), refreshed at every eval and
never pruned.
- `checkpoint-<step>/`: weights, optimizer, RNG state and the method baseline;
the last step is always saved.
The saved state is the bf16 master plus the quantization plan, not packed NVFP4
weights; inference applies the same converter used for the released model. Use
`fastvideo/train/entrypoint/dcp_to_diffusers.py` to export a checkpoint if your
deployment path expects Diffusers weights. It detects the weights-only marker
on a tagged checkpoint, so `best/` can be exported directly:
```bash
python -m fastvideo.train.entrypoint.dcp_to_diffusers \
--checkpoint outputs/v2_1_nvfp4_qad/best \
--output-dir outputs/v2_1_nvfp4_qad/export
```
## Code map
| Path | Purpose |
|---|---|
| `fastvideo/train/methods/knowledge_distillation/pdd_qad.py` | Method: ladder, losses, evaluation, guards |
| `fastvideo/train/methods/knowledge_distillation/pdd_qad_metrics.py` | Rung aggregation and score |
| `fastvideo/train/methods/knowledge_distillation/pdd_qad_recon.py` | Layer-wise reconstruction hooks |
| `fastvideo/train/methods/knowledge_distillation/pdd_qad_step0.py` | Step-0 baseline cache |
| `fastvideo/train/models/minimax_h3/omniref_pdd.py` | OmniRef Ref2VA profile |
| `fastvideo/train/models/minimax_h3/v2_t2v.py` | V2 T2AV profile |
| `fastvideo/layers/quantization/nvfp4_qad.py` | NVFP4 linear emulation and STE |
| `fastvideo/attention/backends/fp4_vsa_qat.py` | VSA fine-attention override |
| `fastvideo-kernel/python/fastvideo_kernel/triton_kernels/attn_qat_vsa_train.py` | Tile 64/128 FP4 block-sparse Triton kernel |
| `fastvideo-kernel/python/fastvideo_kernel/triton_kernels/attn_qat_vsa_reference.py` | PyTorch numeric reference |
| `scripts/distill/minimax_h3_v2_qad/build_v2_prompts.py` | V2 prompt split manifests |
| `scripts/distill/minimax_h3_v2_qad/precompute_v2_text.py` | V2 Qwen3-VL text embeddings |
| `scripts/distill/minimax_h3_omniref_qad/generate_omniref_latents.py` | OmniRef held-out eval plan and latents |
| `examples/train/calibration/fasth3/` | Pinned FFN activation `amax` tables |
## Expected behavior
Teacher-forced video and audio error both fall over the first tens of steps,
and the student's own video samples improve beyond the teacher's seed noise.
Audio samples improve more slowly and can remain within noise even when
teacher-forced audio error clearly drops. Gains slow after roughly 60-100
steps; the remaining error is dominated by FP4 attention. Evaluate a checkpoint
with the full-eval samples, not the teacher-forced score alone, before
promoting it.
Released reference points (lower is better):
| Run | Metric | Baseline | Selected |
|---|---|---|---|
| FastH3-QAD-v2 (linears-only) | mean per-forward x0 rel-L2 | 0.431 | 0.351 (video 0.382, audio 0.319) |
| OmniRef QAD | teacher-forced score | 0.362 | 0.303 at step 120 (video 0.198, audio 0.409) |
The FastH3-QAD-v2 baseline is the NVFP4 Consumer model before QAD; the OmniRef baseline
is the calibrated checkpoint at QAD step 0.
+384
View File
@@ -0,0 +1,384 @@
# Pika H3 Flex NVFP4 QAD
This page describes how the NVFP4 version of
[Pika H3 Flex](https://huggingface.co/Pika-Labs/Pika-H3-Flex) is trained: a
calibration pass followed by quantization-aware distillation (QAD) against
the bf16 Flex model, for both Flex adapters. It applies the shared FastH3 QAD
recipe ([FastH3 NVFP4 QAD](minimax_h3_fp4_qad.md)) to Flex's six-forward
re-noise sampler.
| Adapter | Trunk | Tasks | Model plugin |
|---|---|---|---|
| Ref2VA | `transformer_ref` | image / video / audio references | `flex.MiniMaxH3FlexRef2VAModel` |
| FL2VA | `transformer` | T2VA, I2VA (first frame), L2VA (last frame), FL2VA (both) | `flex.MiniMaxH3FlexFL2VAModel` |
Training runs in two stages per adapter:
1. **NVFP4 linears** (`*_nvfp4_qad.yaml`): W4A4 linears with BF16 dense
attention.
2. **Dense FP4 attention** (`*_fp4attn_qad.yaml`): continues from stage 1's
best checkpoint with FlashAttention-4 NVFP4 attention numerics.
The stage-1 result is the released checkpoint below; stage 2 is optional and
trades a small quality cost for faster long clips (see
[Dense FP4 attention](#stage-2-dense-fp4-attention)).
## Released checkpoint
| Role | Checkpoint |
|---|---|
| Teacher (bf16 adapters) | `Pika-Labs/Pika-H3-Flex` on `MiniMaxAI/MiniMax-H3` @ `42ed227` |
| QAD result (packed NVFP4, both trunks) | `FastVideo/Pika-H3-Flex-NVFP4-QAD` |
The release contains packed NVFP4 `transformer_ref` (Ref2VA) and `transformer`
(FL2VA) trunks with the adapters already merged, an NVFP4 Qwen3-VL text
encoder, an NVFP4 VAE decoder and the Pika schedulers. It targets Blackwell
GPUs and loads without a conversion step (validated on GB200):
```bash
FASTVIDEO_ATTENTION_BACKEND=FLASH_ATTN FASTVIDEO_FA4=1 \
python examples/inference/basic/basic_pika_h3_flex.py \
--model-path FastVideo/Pika-H3-Flex-NVFP4-QAD --nvfp4-dit h3_dit \
--nvfp4-decoder <snapshot>/vae_decoder_nvfp4/decoder-nvfp4.safetensors \
--task ref2va --reference image:character.png --reference video:motion.mp4 \
--prompt "$(cat prompt.txt)" --resolution 480p --duration 5
```
## Target numerics
- **Linears:** NVFP4 W4A4 for `attn.to_q`, `attn.to_k`, `attn.to_v`,
`attn.to_out`, `ff.fc_in` and `ff.fc_out` in all 50 blocks, active from
step 0. FFN inputs use static activation scales (`448 * 6 / amax`) from the
calibrated table; attention linears use the unit activation scale.
- **Attention:** BF16 dense attention in stage 1. Flex has no VSA gate, so
there is no sparse-attention stage and no gate to freeze. Stage 2 switches
to the dense FP4 emulator (below).
- **Trained:** only the six quantized linear families. Norms, AdaLN, the token
refiner, embeddings and the fp32 boundary modules stay frozen.
- **Teacher:** the merged bf16 Flex trunk, frozen. The student starts from the
same weights with fp32 masters.
### Merged adapters
Flex ships each adapter as an fp32 PEFT LoRA (rank and alpha 128) over the
MiniMax-H3 trunk. QAD needs the teacher as plain weights, so
`merge_pika_adapter.py` merges `W + (alpha / rank) * B @ A` for every adapted
module in fp32 and rounds once to bf16. The merged directory also carries
`pika_flex_qad.json`, the sampling contract the model plugin reads (six
forwards at knots 1000/750/500/250/100/40, video shift 12, audio shift 3,
stochastic re-noise between forwards).
Exported trunks that already contain the adapter are marked by
`pika_h3_flex_merged.json` at the model root; the Pika pipeline then skips
loading the adapter again.
## Training objective
Each optimizer step takes one row. The frozen teacher walks the six-forward
ladder; three of the six forwards are trained per row. At each trained forward
the student predicts from the teacher's state:
- **Output:** relative L2 of the student's x0 prediction against the
teacher's, for video and audio.
- **Reconstruction:** relative L2 of every block's output against the
teacher's block output (`objective: output+recon`, `recon_lambda: 1.0`).
Both terms are normalized by their step-0 values. From step 20, 25% of rows
roll out from the student's own states instead of the teacher's. The learning
rate is 5e-6 (cosine to 0.3x after a 10-step warmup); gradients are clipped at
1.0. The re-noise draws are seeded per row and per forward, so teacher and
student see identical noise.
## Evaluation and guards
Every `eval_every` steps the run scores held-out rows (teacher-forced x0 error
per forward, averaged into `eval/score`); every `full_eval_every` steps it also
samples each row with the student's own six forwards and compares the result
with the teacher's (`endpoint/video`, `endpoint/audio`) and, for keyframe
tasks, the first and last frames.
- **Eligibility:** a checkpoint can become `best/` only if its full-eval
audio, endpoint and keyframe metrics stay within `step0 * (1 + tolerance)`.
With `noise_floor_eval: true` the tolerance per metric is twice its seed
noise at step 0 (re-evaluated at seed + 1), at least 2%.
- **Patience:** the run stops after `patience` consecutive evaluations that are
worse than step 0 on the score, or (full evaluations only) misaligned on the
endpoint and keyframe metrics.
Keyframe metrics matter most for FL2VA: the last-frame match is the metric most
sensitive to the learning rate (see [Expected behavior](#expected-behavior)).
## Data preparation
QAD regresses on the teacher, so rows need conditioning (text embeddings and
reference latents) and target geometry, not curated targets. The starting
point is a set of encoded Ref2VA rows in the packed schema
(`fastvideo/dataset/dataloader/schema.py`, `pyarrow_schema_ref2va`) with a
`MANIFEST.jsonl` listing `case`, `id`, `parquet` and `resolution`, plus a
held-out manifest, as in the FastH3 OmniRef recipe (see
[Data preparation](minimax_h3_fp4_qad.md#data-preparation) there). Raw clips can
be encoded into this format with
`fastvideo/pipelines/preprocess/preprocess_minimax_h3_packed_ref2va.py`. Row
ids must contain the source video's id as `asset-<32 hex digits>`; clips of the
same video share it. Below, the source rows are `data/omniref/MANIFEST.jsonl`
and their held-out manifest is `data/omniref/eval-manifest.json`.
1. **Merge each adapter** into its trunk:
```bash
python scripts/distill/pika_h3_flex_qad/merge_pika_adapter.py \
--base <MiniMax-H3 snapshot @42ed227> --adapter Pika-Labs/Pika-H3-Flex \
--task ref2va --output data/models/pika-flex-ref2va-merged
python scripts/distill/pika_h3_flex_qad/merge_pika_adapter.py \
--base <MiniMax-H3 snapshot @42ed227> --adapter Pika-Labs/Pika-H3-Flex \
--task fl2va --output data/models/pika-flex-fl2va-merged
```
2. **Build Ref2VA rows** with `build_flex_ref2va_rows.py` (`plan` on CPU,
`encode` sharded over GPUs, then `finalize`). Each reference is decoded
from its stored latents and re-encoded at Flex's reference sizing, so the
source media is not needed. The plan re-tiers the rows the QAD config will
train on and adds input mixes the source set does not cover: a standalone
audio reference, mixed image and video references, 6-9 image references,
and two video references.
`plan` reads the QAD plan from `--plan-config`. The Flex rows do not exist
yet at this point, so plan with a copy of `ref2va_nvfp4_qad.yaml`
(`plan-ref2va.yaml` below) whose `manifests` lists only
`data/omniref/MANIFEST.jsonl` and whose `eval_manifest` is
`data/omniref/eval-manifest.json`. `--eval-manifest` is the source held-out
manifest; its clips are kept out of the new rows.
```bash
python scripts/distill/pika_h3_flex_qad/build_flex_ref2va_rows.py plan --mode ref2va \
--source-manifest data/omniref/MANIFEST.jsonl --plan-config plan-ref2va.yaml \
--eval-manifest data/omniref/eval-manifest.json --output-dir data/flex-ref2va
CUDA_VISIBLE_DEVICES=$i python scripts/distill/pika_h3_flex_qad/build_flex_ref2va_rows.py encode \
--model-path data/models/pika-flex-ref2va-merged --output-dir data/flex-ref2va \
--shard $i --num-shards 8
python scripts/distill/pika_h3_flex_qad/build_flex_ref2va_rows.py finalize \
--output-dir data/flex-ref2va --eval-manifest data/omniref/eval-manifest.json
```
`finalize` merges the shard manifests into `data/flex-ref2va/MANIFEST.jsonl`
and writes `data/flex-ref2va/eval-manifest.json`: the source held-out rows,
with each row that was re-tiered pointing at its Flex-sized parquet. The
Ref2VA configs list the Flex manifest ahead of the source manifest, so
Flex-sized rows replace source rows with the same id and resolution.
3. **Build FL2VA rows** from the same source rows. `--mode fl2va` builds T2VA,
I2VA, L2VA and FL2VA rows whose keyframes are the clip's exact first/last
frames, with the caption's reference paragraphs rewritten as keyframe
definitions. `--fl2va-counts` and `--resolution-split` size the set
(defaults: 1,100 rows, 80% 480p). FL2VA holds out its eval rows from its own
groups, so `finalize` takes no `--eval-manifest` and writes an empty one.
```bash
python scripts/distill/pika_h3_flex_qad/build_flex_ref2va_rows.py plan --mode fl2va \
--source-manifest data/omniref/MANIFEST.jsonl \
--eval-manifest data/omniref/eval-manifest.json --output-dir data/flex-fl2va
CUDA_VISIBLE_DEVICES=$i python scripts/distill/pika_h3_flex_qad/build_flex_ref2va_rows.py encode \
--model-path data/models/pika-flex-fl2va-merged --output-dir data/flex-fl2va \
--shard $i --num-shards 8
python scripts/distill/pika_h3_flex_qad/build_flex_ref2va_rows.py finalize --output-dir data/flex-fl2va
```
Rows that fail to encode (for example a reference video longer than Flex's
15-second limit) are recorded per shard; `encode --retry-skipped` retries only
those. The configs train on rows up to 345 frames at 480p and 768p
(`per_group: {480p: 160, 768p: 40}` per case) and hold out 3 rows per case.
## Calibration
`calibrate_flex_nvfp4.py` picks the static FFN activation scales on rows walked
by the bf16 teacher over its six forwards:
1. `collect` (torchrun): forward pre-hooks record each row's per-layer input
amax, split into target video, target audio and conditioning tokens.
`--max-rows` takes the first rows of the plan; leave rows uncollected for
`mse`, which stops if every planned row was collected.
2. `candidates` (CPU): per layer, the max over rows plus clipped alternatives
(per-row p99/p90, and 0.7/0.5/0.35/0.25 of the max).
3. `mse` (torchrun): on disjoint rows plus the rows that set the maxima, each
candidate's NVFP4 GEMM output is compared with the bf16 output.
4. `choose` (CPU): per layer, the candidate with the lowest mean of video and
audio relative error (`amax-mse-balanced.json`), next to the all-token
criterion (`amax-mse.json`) and the plain max.
```bash
torchrun --nproc_per_node 4 scripts/distill/pika_h3_flex_qad/calibrate_flex_nvfp4.py collect \
--config examples/train/configs/knowledge_distillation/pika_h3_flex/ref2va_nvfp4_qad.yaml \
--output-dir calib/ref2va --shard $NODE --num-shards $NODES --max-rows $COLLECT_ROWS \
--training.distributed.num_gpus 4 --training.distributed.hsdp_shard_dim 4
python scripts/distill/pika_h3_flex_qad/calibrate_flex_nvfp4.py candidates --output-dir calib/ref2va
torchrun --nproc_per_node 4 scripts/distill/pika_h3_flex_qad/calibrate_flex_nvfp4.py mse \
--config <same config> --output-dir calib/ref2va --shard $NODE --num-shards $NODES \
--training.distributed.num_gpus 4 --training.distributed.hsdp_shard_dim 4
python scripts/distill/pika_h3_flex_qad/calibrate_flex_nvfp4.py choose --output-dir calib/ref2va
```
Compare the candidate tables by the QAD harness's step-0 score (run the QAD
config with each table and read `eval/score` at step 0) and pin the best one.
For both adapters the video/audio-balanced table scored best:
| Adapter | Calibration rows | Step-0 score: max / all-token MSE / balanced |
|---|---|---|
| Ref2VA | 1,133 | 0.3311 / 0.3299 / **0.3260** |
| FL2VA | 546 | 0.2730 / 0.2741 / **0.2724** |
The selected tables are vendored and pinned by sha256:
| Table | sha256 |
|---|---|
| `examples/train/calibration/pika_h3_flex/amax-ref2va-balanced.json` | `ad60b45f…da90b373` |
| `examples/train/calibration/pika_h3_flex/amax-fl2va-balanced.json` | `b690d117…553e0daa` |
## Running
Point the placeholder paths in the config at the merged model, the row
manifests and an output directory, then launch with `torchrun`. The stage-1
configs assume 16 GPUs and the stage-2 configs 12 (sequence parallel 4,
HSDP over all GPUs); the released FL2VA stage-1 run used 24. Rows reach about
58k tokens; on GB200 the runs use about 90-100 GB per GPU.
```bash
torchrun --nnodes=4 --nproc_per_node=4 --node_rank=$NODE_RANK \
--master_addr=$MASTER_ADDR --master_port=$MASTER_PORT \
-m fastvideo.train.entrypoint.train \
--config examples/train/configs/knowledge_distillation/pika_h3_flex/ref2va_nvfp4_qad.yaml
```
Loading and the step-0 evaluation of a multi-node run can exceed the default
10-minute process-group timeout; set `FASTVIDEO_PG_TIMEOUT_MIN=120`. Any config
key can be overridden on the command line (`--training.distributed.num_gpus 24
--training.distributed.hsdp_shard_dim 24` for 6 nodes, for example). Stage 2
launches the same way with `ref2va_fp4attn_qad.yaml` or `fl2va_fp4attn_qad.yaml`
once stage 1 has written `best/`.
## Stage 2: dense FP4 attention
Deployment can additionally run attention through FlashAttention-4's NVFP4
path: Q and K are quantized to NVFP4 per 16 channels with a unit global scale,
P and V stay BF16. Stage 2 adapts the stage-1 student to these numerics with
`fp4_dense_qat.py`, which routes every dense attention layer through the FP4
attention training kernel with full block lists and straight-through
gradients. `scripts/distill/pika_h3_flex_qad/check_fp4_attention_parity.py`
checks that the emulator matches the FA4 kernel (on a Blackwell GPU): the
emulator-to-kernel distance is under 1% of the FP4 quantization error.
Plain FP4 attention costs 13-17% on the step-0 score. Two exact Q/K transforms
and two BF16 blocks recover most of it before any training:
- **smooth-K:** subtract each head's token-mean of K before quantizing
(softmax is invariant to a per-query shift).
- **Hadamard:** rotate Q and K by the same orthonormal 128-point Hadamard
matrix (`Q H (K H)^T = Q K^T`), spreading outlier channels across the
16-channel scale blocks.
- **BF16 blocks 45 and 49:** the two attention layers with the largest FP4
error keep BF16 attention.
| Per-layer FP4 attention error vs BF16 (real activations) | FL2VA | Ref2VA |
|---|---|---|
| Plain FP4 | 0.115 | 0.113 |
| smooth-K + Hadamard | 0.095 | 0.095 |
| + blocks 45 and 49 in BF16 | 0.083 | 0.080 |
The stage-2 configs set `qk_transform: smooth_k+hadamard` and
`bf16_blocks: [45, 49]`, start from stage 1's `best/`
(`method.init_student_dcp`) and train at 5e-6 (Ref2VA) or 3e-6 (FL2VA). Deploy
a stage-2 checkpoint with the same settings:
```bash
python examples/inference/basic/basic_pika_h3_flex.py ... \
--nvfp4-attention smooth_k+hadamard --nvfp4-attention-bf16-blocks 45,49
```
(equivalently `FASTVIDEO_NVFP4_FA4=1`,
`FASTVIDEO_NVFP4_FA4_QK_TRANSFORM=smooth_k+hadamard`,
`FASTVIDEO_NVFP4_FA4_BF16_BLOCKS=45,49`).
Measured on one GB200 (FL2VA, NVFP4 DiT and decoder, warm), FP4 attention
helps long clips and is neutral on short ones:
| Clip | BF16 attention | FP4 attention | FP4 + transforms |
|---|---|---|---|
| 480p, 5 s | 5.87 s | 5.66 s | 6.08 s |
| 768p, 15 s | 99.7 s | 84.7 s | 87.4 s |
The transforms currently run as separate PyTorch ops before the FP4 quantizer;
fusing them into the quantizer removes most of their cost.
## Export
Checkpoints hold fp32 masters in DCP format. Export a checkpoint to the packed
NVFP4 format the inference loader reads, then verify it:
```bash
# 1. bf16 trunk from the student's masters (CPU)
python scripts/distill/pika_h3_flex_qad/export_student_bf16.py \
--dcp outputs/pika_flex_ref2va_nvfp4_qad/best/dcp \
--base data/models/pika-flex-ref2va-merged --trunk transformer_ref \
--out exports/ref2va/transformer_bf16
# 2. pack NVFP4 linears with the pinned activation scales
python scripts/checkpoint_conversion/convert_minimax_h3_modelopt_nvfp4_dit.py \
--src exports/ref2va/transformer_bf16 --dst exports/ref2va/transformer_ref \
--quantize-ffn --quantize-attention \
--act-amax examples/train/calibration/pika_h3_flex/amax-ref2va-balanced.json
# 3. byte-level parity with the QAD numerics (1 GPU)
python scripts/distill/pika_h3_flex_qad/check_nvfp4_export_parity.py \
--export exports/ref2va/transformer_ref --bf16 exports/ref2va/transformer_bf16 \
--amax examples/train/calibration/pika_h3_flex/amax-ref2va-balanced.json
```
The parity check confirms that packed weights and block scales are
byte-identical to what training quantized and that the deployed NVFP4 GEMM
matches the training GEMM bit for bit. Repeat for FL2VA with
`--trunk transformer` and its table, then assemble a model directory:
```bash
python scripts/distill/pika_h3_flex_qad/assemble_flex_release.py --output release/Pika-H3-Flex-NVFP4 \
--base <MiniMax-H3 snapshot> --pika <Pika-H3-Flex snapshot> \
--transformer-ref exports/ref2va/transformer_ref --ref2va-merged data/models/pika-flex-ref2va-merged \
--transformer exports/fl2va/transformer --fl2va-merged data/models/pika-flex-fl2va-merged
```
`--text-encoder` and `--vae-decoder-nvfp4` add NVFP4 text-encoder and decoder
exports.
## Code map
| Path | Purpose |
|---|---|
| `fastvideo/train/models/minimax_h3/flex.py` | Flex model plugins: sampling contract, re-noise ladder, FL2VA keyframe rows, QAD install |
| `fastvideo/train/methods/knowledge_distillation/pdd_qad.py` | Shared QAD method: losses, evaluation, guards |
| `fastvideo/attention/backends/fp4_dense_qat.py` | Dense FP4 attention emulator and install hook |
| `fastvideo/attention/utils/nvfp4_qk_precondition.py` | Exact Q/K transforms shared by training and inference |
| `fastvideo/attention/backends/flash_attn.py` | Deployed FA4 NVFP4 attention (`nvfp4_fa4`, `nvfp4_qk_transform`) |
| `fastvideo/pipelines/basic/pika_h3_flex/adapter.py` | Adapter loading and the merged-adapter marker |
| `scripts/distill/pika_h3_flex_qad/merge_pika_adapter.py` | Merge an adapter into its trunk |
| `scripts/distill/pika_h3_flex_qad/build_flex_ref2va_rows.py` | Ref2VA and FL2VA training rows |
| `scripts/distill/pika_h3_flex_qad/calibrate_flex_nvfp4.py` | FFN activation-scale calibration |
| `scripts/distill/pika_h3_flex_qad/check_fp4_attention_parity.py` | Emulator vs FA4 NVFP4 kernel parity |
| `scripts/distill/pika_h3_flex_qad/export_student_bf16.py` | DCP masters to a bf16 trunk |
| `scripts/distill/pika_h3_flex_qad/check_nvfp4_export_parity.py` | Packed export vs QAD numerics parity |
| `scripts/distill/pika_h3_flex_qad/assemble_flex_release.py` | Self-contained NVFP4 model directory |
| `examples/train/configs/knowledge_distillation/pika_h3_flex/` | Stage-1 and stage-2 configs for both adapters |
| `examples/train/calibration/pika_h3_flex/` | Pinned FFN activation `amax` tables |
## Expected behavior
Teacher-forced error falls steadily for the first 30-60 steps and flattens by
step 90-120. Video improves faster than audio. Results of the released runs
(held-out rows, lower is better, distances to the bf16 Flex teacher):
| Run | Step 0 (PTQ) | Selected | Notes |
|---|---|---|---|
| Ref2VA stage 1, 16 GPUs | 0.326 | **0.270** at step 120 (-17.1%) | video x0 0.185, audio x0 0.356 |
| FL2VA stage 1, 24 GPUs | 0.272 | **0.234** at step 60 (-14.1%) | first-frame 0.072, last-frame 0.247 |
- FL2VA's last-frame match can drift above its tolerance while the score keeps
improving; at 5e-6 this stopped stage 2 at step 60 with no eligible
checkpoint, so its stage-2 config uses 3e-6.
- A lower teacher-forced score with a failed guard is not a better model:
promote only eligible checkpoints, and review the full-eval samples
(`eval_save_dir`) before release.
+2 -2
View File
@@ -18,7 +18,7 @@ should use the config-driven system documented here.
### Launch with the helper script
```bash
bash examples/train/run.sh examples/train/distill_wan2.1_t2v_1.3B_dmd2.yaml
bash examples/train/run.sh examples/train/configs/distribution_matching/wan/dmd2_t2v.yaml
```
The script auto-detects available GPUs and sets up `torchrun`. Override with
@@ -35,7 +35,7 @@ NUM_GPUS=4 NNODES=2 NODE_RANK=0 \
```bash
torchrun --nproc_per_node=8 \
fastvideo/train/entrypoint/train.py \
--config examples/train/distill_wan2.1_t2v_1.3B_dmd2.yaml
--config examples/train/configs/distribution_matching/wan/dmd2_t2v.yaml
```
### CLI flags
@@ -0,0 +1,140 @@
# SPDX-License-Identifier: Apache-2.0
"""Draft with a FastH3 checkpoint at 768P, then regenerate the draft at 2560x1440 with Open-H3-Regenerate-2K.
The draft's native latent goes straight to the regen's upscaler: the draft is never decoded to pixels and re-encoded.
The regen stack is unchanged (base MiniMax-H3, the released merged LoRA and Sharp branches, three Euler steps).
python examples/inference/basic/basic_fasth3_to_regen2k.py \
--draft-model FastVideo/FastVideo-FastH3-8-Step-V2 --regen-model Efficient-Large-Model/Open-H3-Regenerate-2K \
--prompt "A red fox trots through fresh snow" --output outputs/fasth3_regen2k
A packed NVFP4 draft checkpoint (FastH3 QAD, FastH3 Trim NVFP4) needs ``--draft-quant h3_dit_vsa``; ``--vae-decoder``
selects an NVFP4 decoder for the 2K decode.
"""
from __future__ import annotations
import argparse
import json
import os
from pathlib import Path
import torch
from fastvideo import VideoGenerator
from fastvideo.api import (
ComponentConfig,
EngineConfig,
GenerationRequest,
GeneratorConfig,
InputConfig,
OffloadConfig,
OutputConfig,
PipelineSelection,
QuantizationConfig,
SamplingConfig,
)
from fastvideo.utils import maybe_download_model
DRAFT_SIZE = (768, 1344)
NUM_FRAMES = 124
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--draft-model", required=True, help="FastH3 checkpoint id or local path")
parser.add_argument("--draft-quant", default=None, help="NVFP4 layer profile of a packed draft, e.g. h3_dit_vsa")
parser.add_argument("--regen-model", default="Efficient-Large-Model/Open-H3-Regenerate-2K")
parser.add_argument("--prompt", required=True)
parser.add_argument("--draft-steps", type=int, default=9, help="sigma-grid points of the draft (9 = 8 forwards)")
parser.add_argument("--draft-seed", type=int, default=1000)
parser.add_argument("--seed", type=int, default=1766580932, help="regen refine noise seed")
parser.add_argument("--output", type=Path, default=Path("outputs/fasth3_regen2k"))
parser.add_argument("--vae-decoder",
default=None,
help="NVFP4 H3 video decoder for the regen decode (Blackwell), e.g. "
"quality-36L/decoder-nvfp4.safetensors from FastVideo/FastH3-NVFP4-VAE-Decoder")
return parser.parse_args()
def normalize_h3_latents(latents: torch.Tensor, regen_model: str) -> torch.Tensor:
"""Re-apply the H3 VAE's latent statistics: a draft run returns denormalized latents, the regen takes normalized."""
vae_config = os.path.join(maybe_download_model(regen_model, allow_patterns=["base_model/vae/config.json"]),
"base_model", "vae", "config.json")
with open(vae_config, encoding="utf-8") as handle:
config = json.load(handle)
mean = torch.tensor(config["latents_mean"], dtype=torch.float32).view(-1, 1, 1, 1)
std = torch.tensor(config["latents_std"], dtype=torch.float32).view(-1, 1, 1, 1)
return (latents.float().cpu().reshape(-1, *latents.shape[-3:]) - mean) / std
def draft_latent(args: argparse.Namespace) -> torch.Tensor:
engine = EngineConfig(num_gpus=1, offload=OffloadConfig(dit=False, dit_layerwise=False, vae=False))
if args.draft_quant:
engine.quantization = QuantizationConfig(transformer_quant="NVFP4", layer_profile=args.draft_quant)
generator = VideoGenerator.from_config(
GeneratorConfig(model_path=args.draft_model,
engine=engine,
pipeline=PipelineSelection(components=ComponentConfig(),
experimental={
"attention_backend": "VIDEO_SPARSE_ATTN_H3",
"VSA_sparsity": 0.8,
"VSA_tile_size": 64,
"output_type": "latent",
})))
try:
result = generator.generate(
GenerationRequest(prompt=args.prompt,
sampling=SamplingConfig(height=DRAFT_SIZE[0],
width=DRAFT_SIZE[1],
num_frames=NUM_FRAMES,
fps=24,
num_inference_steps=args.draft_steps,
guidance_scale=1.0,
seed=args.draft_seed),
output=OutputConfig(save_video=False, return_frames=True)))
finally:
generator.shutdown()
return normalize_h3_latents(torch.as_tensor(result.samples), args.regen_model)
def main() -> None:
args = parse_args()
args.output.mkdir(parents=True, exist_ok=True)
latent_path = args.output / "draft_latent.pt"
torch.save(draft_latent(args), latent_path)
generator = VideoGenerator.from_config(
GeneratorConfig(model_path=args.regen_model,
engine=EngineConfig(num_gpus=1,
offload=OffloadConfig(dit=False, dit_layerwise=False, text_encoder=True,
vae=False)),
pipeline=PipelineSelection(components=ComponentConfig(),
experimental={
"attention_backend": "TORCH_SDPA",
"draft_size": "768p",
**({
"h3_vae_nvfp4_decoder": args.vae_decoder
} if args.vae_decoder else {}),
})))
try:
result = generator.generate(
GenerationRequest(prompt=args.prompt,
inputs=InputConfig(video_path=str(latent_path)),
sampling=SamplingConfig(height=1440,
width=2560,
num_frames=NUM_FRAMES,
fps=24,
num_inference_steps=3,
guidance_scale=1.0,
seed=args.seed),
output=OutputConfig(output_path=str(args.output / "regen2k.mp4"),
save_video=True,
return_frames=False)))
print(f"wrote {result.video_path}")
finally:
generator.shutdown()
if __name__ == "__main__":
main()
@@ -0,0 +1,108 @@
# SPDX-License-Identifier: Apache-2.0
"""Regenerate a 768P MiniMax-H3 draft video at 2560x1440 with Open-H3-Regenerate-2K.
The draft is VAE-encoded (the first 121 frames at 24 fps, 1344x768), its latent is upscaled, and three refine steps
of the H3 transformer with the released LoRA run at 2560x1440. The output video is silent. See
docs/inference/open_h3_regenerate_2k.md.
python examples/inference/basic/basic_open_h3_regenerate_2k.py --video draft.mp4 --prompt "..." --output out/
"""
from __future__ import annotations
import argparse
import time
from pathlib import Path
from fastvideo import VideoGenerator
from fastvideo.api import (
ComponentConfig,
EngineConfig,
GenerationRequest,
GeneratorConfig,
InputConfig,
OffloadConfig,
OutputConfig,
PipelineSelection,
QuantizationConfig,
SamplingConfig,
)
DEFAULT_MODEL = "Efficient-Large-Model/Open-H3-Regenerate-2K"
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--model-path", default=DEFAULT_MODEL, help="release repo id or a local copy of it")
parser.add_argument("--video", default=None, help="768P draft video (URL-free local path)")
parser.add_argument("--prompt", required=True, help="text the refine steps are conditioned on")
parser.add_argument("--output", type=Path, default=Path("outputs/open_h3_regenerate_2k"))
parser.add_argument("--steps", type=int, choices=(2, 3), default=3, help="refine steps (3 is the released setting)")
parser.add_argument("--seed", type=int, default=1766580932)
parser.add_argument("--attention-backend", default="TORCH_SDPA")
parser.add_argument("--draft-size", choices=("768p", "576p"), default="768p",
help="size the draft is encoded at; 576p for drafts from other video models")
parser.add_argument("--vae-decoder", default=None,
help="NVFP4 decoder-nvfp4.safetensors that replaces the VAE decoder at load "
"(Blackwell only, see docs/inference/open_h3_regenerate_2k.md)")
parser.add_argument("--refine-transformer", default=None,
help="refine on another MiniMax-H3 DiT (e.g. a FastH3 export) instead of the release's own")
parser.add_argument("--refine-transformer-quant", default=None,
help="NVFP4 layer profile for a packed refine DiT, e.g. h3_dit_vsa")
parser.add_argument("--draft-latent", default=None,
help="native draft latent .pt to refine instead of encoding --video")
parser.add_argument("--runs", type=int, default=1, help="repeat the request to measure warm timings")
return parser.parse_args()
def main() -> None:
args = parse_args()
if (args.video is None) == (args.draft_latent is None):
raise SystemExit("pass exactly one of --video or --draft-latent")
args.output.mkdir(parents=True, exist_ok=True)
experimental = {
"attention_backend": args.attention_backend,
"draft_size": args.draft_size,
}
if args.vae_decoder:
experimental["h3_vae_nvfp4_decoder"] = args.vae_decoder
experimental["refine_transformer_dir"] = args.refine_transformer
experimental["draft_latent_path"] = args.draft_latent
engine = EngineConfig(num_gpus=1,
offload=OffloadConfig(dit=False, dit_layerwise=False, text_encoder=True, vae=False))
if args.refine_transformer_quant:
engine.quantization = QuantizationConfig(transformer_quant="NVFP4", layer_profile=args.refine_transformer_quant)
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path=args.model_path,
engine=engine,
pipeline=PipelineSelection(
components=ComponentConfig(),
experimental=experimental,
),
))
try:
for run in range(args.runs):
started = time.perf_counter()
result = generator.generate(
GenerationRequest(
prompt=args.prompt,
inputs=InputConfig(video_path=args.video),
sampling=SamplingConfig(height=1440,
width=2560,
num_frames=124,
fps=24,
num_inference_steps=args.steps,
guidance_scale=1.0,
seed=args.seed),
output=OutputConfig(output_path=str(args.output / f"regen2k_{run}.mp4"),
save_video=True,
return_frames=False),
))
print(f"run {run}: {time.perf_counter() - started:.1f}s -> {result.video_path}")
finally:
generator.shutdown()
if __name__ == "__main__":
main()
-40
View File
@@ -1,40 +0,0 @@
import os
from fastvideo import VideoGenerator
# PAN2 is trained on the structured captions its prompt rewriter produces (serialized as one-line JSON). Plain
# prompts like this one work, with weaker results.
PROMPT = ("A red fox trots through fresh snow in a pine forest at dawn, its breath visible in the cold air. "
"The camera tracks it from the side at ground level as low golden light filters through the trees.")
OUTPUT_PATH = "video_samples_pan2"
MODEL_PATH = os.environ.get("PAN2_MODEL_PATH", "IFM/PAN2")
# Set to a first frame (e.g. "assets/images/girl.png") for image-to-video; the same pipeline runs both modes.
IMAGE_PATH = None
def main():
# The 29B DiT runs in bf16; with more GPUs, num_gpus enables Ulysses sequence parallelism (the 32 attention
# heads must be divisible by it).
generator = VideoGenerator.from_pretrained(
MODEL_PATH,
num_gpus=int(os.environ.get("PAN2_NUM_GPUS", "1")),
use_fsdp_inference=False,
dit_cpu_offload=False,
dit_layerwise_offload=False,
vae_cpu_offload=False,
text_encoder_cpu_offload=True,
pin_cpu_memory=True,
)
generator.generate_video(
PROMPT,
image_path=IMAGE_PATH,
output_path=OUTPUT_PATH,
save_video=True,
)
if __name__ == "__main__":
main()
@@ -0,0 +1,319 @@
# SPDX-License-Identifier: Apache-2.0
"""Generate synchronized video/audio with Pika H3 Flex.
Five task shapes share one model release:
``t2va`` text only
``i2va`` first image
``l2va`` last image
``fl2va`` first and last images
``ref2va`` ordered image/video/audio references
The six-forward ladder is part of the adapter contract; this script deliberately
does not expose a step-count flag. ``--lossless-accel`` turns on the existing
training-free FastVideo switches (bit-identical kernels, reference memo,
parallel VAE tiles) before the generator starts. ``--nvfp4-decoder`` and
``--nvfp4-text-encoder`` opt into the existing Blackwell FP4 weight paths;
``--weights nvfp4-qad`` (or ``nvfp4-qad-fp4-attention``) selects FastVideo's NVFP4 QAD release;
``--nvfp4-dit h3_dit`` loads any packed NVFP4 DiT export (e.g.
FastVideo/Pika-H3-Flex-NVFP4-QAD). They change weights, not the six-forward sampler. Kernel/VSA work is separate.
``--nvfp4-attention`` turns on FlashAttention-4 NVFP4 attention (Q/K in NVFP4, P/V in BF16; Blackwell and FA4 only)
with the named exact Q/K preconditioning, which must match the ``qk_transform`` the checkpoint was QAD-trained with
(``smooth_k+hadamard`` for Pika-H3-Flex-NVFP4-QAD). ``--nvfp4-attention-bf16-blocks 45,49`` keeps those
``transformer_blocks`` on BF16 attention, as in training. ``none`` quantizes Q/K without preconditioning.
Without ``--nvfp4-attention`` attention stays BF16.
``--resolution`` (480p/768p), ``--aspect`` (one of Pika's eleven buckets), and
``--duration`` (5-15 s) pick the bucket and the ``17n + 5`` frame count;
``--height``/``--width``/``--num-frames`` override them with explicit values.
Launch with ``FASTVIDEO_ATTENTION_BACKEND=FLASH_ATTN FASTVIDEO_FA4=1`` on a
FlashAttention-4 host (Hopper/Blackwell); without FA4 attention falls back to
FA3/FA2 or, if neither is installed, to SDPA.
``--offload resident`` keeps every component on the GPU, which is how the
GB200 timings in docs/inference/pika_h3_flex.md were measured; the default
``lazy`` mode loads components on demand for smaller devices and is slower.
Examples:
FASTVIDEO_ATTENTION_BACKEND=FLASH_ATTN FASTVIDEO_FA4=1 \
python examples/inference/basic/basic_pika_h3_flex.py --task t2va --offload resident \
--prompt "a paper boat drifting down a rainy street"
python examples/inference/basic/basic_pika_h3_flex.py --task t2va \
--resolution 480p --aspect 9:16 --duration 10 --prompt "a lighthouse at dusk"
python examples/inference/basic/basic_pika_h3_flex.py --task fl2va \
--image first.png --last-image last.png --prompt "the flower opens"
python examples/inference/basic/basic_pika_h3_flex.py --task ref2va \
--reference image:person.png --reference video:dance.mp4 \
--reference audio:voice.wav --prompt "the person dances to the music"
"""
from __future__ import annotations
import argparse
import os
from pathlib import Path
from typing import Any
from fastvideo import VideoGenerator, envs
from fastvideo.api import (
CompileConfig,
ComponentConfig,
EngineConfig,
GenerationRequest,
GeneratorConfig,
InputConfig,
OffloadConfig,
OutputConfig,
ParallelismConfig,
PipelineSelection,
QuantizationConfig,
SamplingConfig,
)
from fastvideo.attention.utils.nvfp4_qk_precondition import QK_TRANSFORMS, parse_block_indices
from fastvideo.models.vision_utils import load_image
from fastvideo.pipelines.basic.minimax_h3 import MiniMaxH3Reference
from fastvideo.pipelines.basic.pika_h3_flex.buckets import (
PIKA_ASPECTS,
PIKA_FPS,
PIKA_MAX_DURATION,
PIKA_MIN_DURATION,
canvas_for_aspect,
resolve_frames,
)
# Training-free switches FastVideo already exposes. Workers inherit the
# environment, so they must be set before VideoGenerator starts.
LOSSLESS_ACCEL_ENV = {
"FASTVIDEO_MINIMAX_H3_EXACT_KERNELS": "all",
"FASTVIDEO_H3_REF2VA_MEMO_ENTRIES": "16",
"FASTVIDEO_H3_VAE_TILE_PARALLEL": "1",
"FASTVIDEO_ULYSSES_A2A": "auto",
}
TASKS = ("t2va", "i2va", "l2va", "fl2va", "ref2va")
RESOLUTIONS = {"480p": "p480", "768p": "p768"}
def _reference(value: str) -> MiniMaxH3Reference:
try:
media_type, source = value.split(":", 1)
except ValueError as error:
raise argparse.ArgumentTypeError("A reference must be image:PATH, video:PATH, or audio:PATH.") from error
if media_type not in ("image", "video", "audio"):
raise argparse.ArgumentTypeError(f"Unknown reference type {media_type!r}.")
return MiniMaxH3Reference(source=source, media_type=media_type)
WEIGHT_PRESETS = {
"bf16": {
"model_path": "Pika-Labs/Pika-H3-Flex"
},
"nvfp4-qad": {
"model_path": "FastVideo/Pika-H3-Flex-NVFP4-QAD",
"nvfp4_dit": "h3_dit"
},
"nvfp4-qad-fp4-attention": {
"model_path": "FastVideo/Pika-H3-Flex-NVFP4-FP4Attn-QAD",
"nvfp4_dit": "h3_dit",
"nvfp4_attention": "smooth_k+hadamard",
"nvfp4_attention_bf16_blocks": "45,49"
},
}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--weights",
choices=tuple(WEIGHT_PRESETS),
default="bf16",
help="bf16: Pika's release; nvfp4-qad: FastVideo's NVFP4 QAD release; "
"nvfp4-qad-fp4-attention: NVFP4 QAD with FP4 attention (both need Blackwell)")
parser.add_argument("--model-path", help="local model directory or Hub repo (overrides --weights)")
parser.add_argument("--task", choices=TASKS, default="t2va")
parser.add_argument("--prompt", required=True)
parser.add_argument("--image", help="first frame (i2va/fl2va)")
parser.add_argument("--last-image", help="last frame (l2va/fl2va)")
parser.add_argument("--reference",
action="append",
type=_reference,
default=[],
help="ordered reference (repeatable; ref2va)")
parser.add_argument("--resolution", choices=tuple(RESOLUTIONS), default="768p", help="Pika resolution tier")
parser.add_argument("--aspect", choices=PIKA_ASPECTS, default="16:9", help="bucket aspect ratio (W:H)")
parser.add_argument("--duration",
type=float,
default=5.0,
help=f"seconds ({PIKA_MIN_DURATION:g}-{PIKA_MAX_DURATION:g}); rounded up to the 17n+5 grid")
parser.add_argument("--height", type=int, help="explicit canvas height (with --width; overrides the bucket)")
parser.add_argument("--width", type=int, help="explicit canvas width (with --height; overrides the bucket)")
parser.add_argument("--num-frames", type=int, help="explicit frame count (overrides --duration)")
parser.add_argument("--seed", type=int, default=0)
parser.add_argument("--num-gpus", type=int, default=1)
parser.add_argument("--compile", action="store_true", help="torch.compile the DiT")
parser.add_argument("--lossless-accel", action="store_true", help="enable bit-identical speed-ups")
parser.add_argument("--offload",
choices=("lazy", "resident"),
default="lazy",
help="lazy: load components on demand and offload the encoders/VAE (fits smaller devices); "
"resident: keep everything on the GPU (fastest, about 105-130 GiB on GB200)")
parser.add_argument("--low-vram", action="store_true", help="stream the DiT layerwise (lazy offload only)")
parser.add_argument("--nvfp4-decoder",
help="Blackwell-only NVFP4 H3 decoder (quality-36L or fast-8L decoder-nvfp4.safetensors)")
parser.add_argument("--nvfp4-text-encoder",
help="Blackwell-only serialized NVFP4 Qwen3-VL text_encoder directory")
parser.add_argument("--nvfp4-dit",
choices=("h3_dit", "h3_dit_ffn"),
help="load a packed NVFP4 DiT export with this layer profile (h3_dit: attention + FFN linears)")
parser.add_argument("--nvfp4-attention",
choices=tuple(QK_TRANSFORMS),
help="FA4 NVFP4 Q/K attention with this exact Q/K preconditioning (default: BF16 attention)")
parser.add_argument("--nvfp4-attention-bf16-blocks",
help="comma-separated transformer_blocks indices that keep BF16 attention, e.g. 45,49")
parser.add_argument("--output", default="outputs/pika_h3_flex")
args = parser.parse_args()
for name, value in WEIGHT_PRESETS[args.weights].items():
if getattr(args, name) is None:
setattr(args, name, value)
if args.nvfp4_attention_bf16_blocks and not args.nvfp4_attention:
parser.error("--nvfp4-attention-bf16-blocks needs --nvfp4-attention")
try:
parse_block_indices(args.nvfp4_attention_bf16_blocks)
except ValueError as error:
parser.error(f"--nvfp4-attention-bf16-blocks: {error}")
if args.task in ("i2va", "fl2va") and not args.image:
parser.error(f"--task {args.task} needs --image")
if args.task in ("l2va", "fl2va") and not args.last_image:
parser.error(f"--task {args.task} needs --last-image")
if args.task == "ref2va" and not args.reference:
parser.error("--task ref2va needs at least one --reference")
if args.task != "ref2va" and args.reference:
parser.error("--reference is only valid with --task ref2va")
if (args.height is None) != (args.width is None):
parser.error("--height and --width must be passed together")
if args.low_vram and args.offload == "resident":
parser.error("--low-vram streams the DiT and cannot be combined with --offload resident")
return args
def _offload(args: argparse.Namespace) -> OffloadConfig:
if args.offload == "resident":
return OffloadConfig(dit=False,
dit_layerwise=False,
text_encoder=False,
vae=False,
pin_cpu_memory=False,
lazy_module_load=False)
return OffloadConfig(
dit=False,
dit_layerwise=args.low_vram,
text_encoder=True,
vae=True,
pin_cpu_memory=not args.low_vram,
lazy_module_load=True,
)
def _canvas(args: argparse.Namespace) -> tuple[int, int]:
if args.height is not None:
return args.height, args.width
return canvas_for_aspect(args.aspect, RESOLUTIONS[args.resolution])
def _num_frames(args: argparse.Namespace) -> int:
if args.num_frames is not None:
return args.num_frames
return resolve_frames(round(args.duration * PIKA_FPS))
def _inputs(args: argparse.Namespace) -> InputConfig:
if args.task == "ref2va":
return InputConfig(references=args.reference)
first = args.image if args.task in ("i2va", "fl2va") else None
last = args.last_image if args.task in ("l2va", "fl2va") else None
# The stage loads ``image_path`` itself but requires ``last_image`` as a PIL image.
return InputConfig(image_path=first, last_image=load_image(last) if last else None)
def _pipeline(args: argparse.Namespace) -> PipelineSelection:
components = ComponentConfig()
if args.task == "ref2va":
components.override_pipeline_cls_name = "PikaH3FlexRef2VAModularPipeline"
if args.nvfp4_decoder:
components.h3_vae_nvfp4_decoder = args.nvfp4_decoder
if args.nvfp4_text_encoder:
components.text_encoder_weights = args.nvfp4_text_encoder
return PipelineSelection(
workload_type="t2v" if args.task == "t2va" else "i2v",
components=components,
vae_tiling=True,
)
def _apply_nvfp4_attention(args: argparse.Namespace) -> None:
"""Workers inherit the environment, so set it before VideoGenerator starts."""
if not args.nvfp4_attention:
return
envs.FASTVIDEO_NVFP4_FA4.set(True)
envs.FASTVIDEO_NVFP4_FA4_QK_TRANSFORM.set(args.nvfp4_attention)
envs.FASTVIDEO_NVFP4_FA4_BF16_BLOCKS.set(args.nvfp4_attention_bf16_blocks or "")
envs.setdefault_external("CUTE_DSL_ENABLE_TVM_FFI", "1")
def main() -> None:
args = parse_args()
_apply_nvfp4_attention(args)
if args.lossless_accel:
for key, value in LOSSLESS_ACCEL_ENV.items():
os.environ.setdefault(key, value)
height, width = _canvas(args)
num_frames = _num_frames(args)
print(f"Pika H3 Flex {args.task}: {width}x{height}, {num_frames} frames ({num_frames / PIKA_FPS:.2f} s)")
output_path = Path(args.output) / f"pika_h3_flex_{args.task}.mp4"
output_path.parent.mkdir(parents=True, exist_ok=True)
offload = _offload(args)
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path=args.model_path,
engine=EngineConfig(
num_gpus=args.num_gpus,
use_fsdp_inference=args.num_gpus > 1,
parallelism=ParallelismConfig(tp_size=1, sp_size=args.num_gpus),
offload=offload,
compile=CompileConfig(enabled=args.compile),
quantization=(QuantizationConfig(transformer_quant="NVFP4", layer_profile=args.nvfp4_dit)
if args.nvfp4_dit else None),
),
pipeline=_pipeline(args),
))
try:
result = generator.generate(
GenerationRequest(
prompt=args.prompt,
negative_prompt="",
inputs=_inputs(args),
sampling=SamplingConfig(
height=height,
width=width,
num_frames=num_frames,
fps=24,
num_inference_steps=6,
guidance_scale=1.0,
batch_cfg=False,
seed=args.seed,
),
output=OutputConfig(
output_path=str(output_path),
save_video=True,
return_frames=False,
),
))
print(f"Output written to: {result.video_path}")
finally:
generator.shutdown()
if __name__ == "__main__":
main()
-1
View File
@@ -90,7 +90,6 @@ uncovered registry families.
| `openai_hunyuan15_i2v_480p.yaml` | HunyuanVideo 1.5 I2V 480p | image-to-video | 1 |
| `openai_kandinsky6_ti2va.yaml` | Kandinsky 6 Pro TI2VA | text/image-to-video with audio | 1 |
| `openai_kandinsky6_ti2va_distilled.yaml` | Kandinsky 6 Pro-distill TI2VA | text/image-to-video with audio | 1 |
| `openai_pan2.yaml` | PAN 2 | text/image-to-video | 1 |
| `openai_ltx2_distilled.yaml` | LTX2 Distilled | text-to-video with audio | 4 |
| `openai_flux1_dev.yaml` | FLUX.1-dev | text-to-image | 1 |
| `openai_flux2_dev.yaml` | FLUX.2-dev | text-to-image | 1 |
@@ -0,0 +1,46 @@
# OpenAI-compatible Open-H3-Regenerate-2K server (one GPU with about 150 GB free, e.g. B200 / GB200).
# POST /v1/videos/sync (or /v1/videos) with a 768P H3 draft as video_reference; the response is the 2560x1440 MP4.
# Local draft paths are accepted only inside server.media_roots; http(s) URLs must resolve to public addresses unless
# server.allow_private_media_urls is set. data: URLs are always refused. See docs/inference/open_h3_regenerate_2k.md.
generator:
model_path: Efficient-Large-Model/Open-H3-Regenerate-2K
engine:
num_gpus: 1
offload:
dit: false
dit_layerwise: false
text_encoder: true
vae: false
pin_cpu_memory: true
compile:
enabled: false
vae_enabled: false
pipeline:
experimental:
attention_backend: TORCH_SDPA
# 768p for MiniMax-H3 drafts; 576p encodes drafts from other video models at 1024x576.
draft_size: 768p
# NVFP4 video decoder (Blackwell only). Download one tier from FastVideo/FastH3-NVFP4-VAE-Decoder and point
# this at it; the release VAE has the full 36 blocks, so both tiers apply. `fast-8L` is the cheap option.
# h3_vae_nvfp4_decoder: ./fasth3-nvfp4-vae/quality-36L/decoder-nvfp4.safetensors
server:
host: 0.0.0.0
port: 8000
output_dir: outputs/openai_open_h3_regenerate_2k
served_model_name: open-h3-regenerate-2k
media_roots:
- /data/drafts
media_reference_max_bytes: 536870912
default_request:
sampling:
height: 1440
width: 2560
num_frames: 124
fps: 24
num_inference_steps: 3
guidance_scale: 1.0
seed: 1766580932
output:
return_frames: false
-36
View File
@@ -1,36 +0,0 @@
# OpenAI-compatible server for PAN 2 text-to-video and first-frame image-to-video.
# fastvideo serve --config examples/serving/openai_pan2.yaml
# Omit image_reference for text-to-video; send it (or input_reference) for image-to-video.
generator:
model_path: IFM/PAN2
engine:
num_gpus: 1
use_fsdp_inference: false
parallelism:
tp_size: 1
sp_size: 1
offload:
dit: false
dit_layerwise: false
vae: false
text_encoder: true
pin_cpu_memory: true
pipeline:
workload_type: t2v
server:
host: 0.0.0.0
port: 8000
output_dir: outputs/openai_pan2
served_model_name: pan2
default_request:
sampling:
height: 704
width: 1248
num_frames: 313
fps: 24
num_inference_steps: 50
guidance_scale: 3.0
output:
return_frames: false
@@ -0,0 +1,102 @@
{
"b0.ff.fc_in": 747.9899299279987,
"b0.ff.fc_out": 2462.017121214983,
"b1.ff.fc_in": 671.2121750026851,
"b1.ff.fc_out": 1285.509862734237,
"b2.ff.fc_in": 1153.55816110724,
"b2.ff.fc_out": 3796.948980859055,
"b3.ff.fc_in": 1153.55816110724,
"b3.ff.fc_out": 4715.270819259384,
"b4.ff.fc_in": 1285.509862734237,
"b4.ff.fc_out": 4715.270819259384,
"b5.ff.fc_in": 2209.302022692089,
"b5.ff.fc_out": 5254.635977347013,
"b6.ff.fc_in": 2209.302022692089,
"b6.ff.fc_out": 5254.635977347013,
"b7.ff.fc_in": 2462.017121214983,
"b7.ff.fc_out": 4715.270819259384,
"b8.ff.fc_in": 2462.017121214983,
"b8.ff.fc_out": 5855.696365467314,
"b9.ff.fc_in": 2743.6392141259485,
"b9.ff.fc_out": 5855.696365467314,
"b10.ff.fc_in": 2743.6392141259485,
"b10.ff.fc_out": 5855.696365467314,
"b11.ff.fc_in": 2743.6392141259485,
"b11.ff.fc_out": 6525.510437495776,
"b12.ff.fc_in": 2743.6392141259485,
"b12.ff.fc_out": 6525.510437495776,
"b13.ff.fc_in": 3057.4751817363253,
"b13.ff.fc_out": 6525.510437495776,
"b14.ff.fc_in": 2743.6392141259485,
"b14.ff.fc_out": 6525.510437495776,
"b15.ff.fc_in": 2743.6392141259485,
"b15.ff.fc_out": 6525.510437495776,
"b16.ff.fc_in": 3407.2097180654623,
"b16.ff.fc_out": 6525.510437495776,
"b17.ff.fc_in": 3057.4751817363253,
"b17.ff.fc_out": 9030.717912847476,
"b18.ff.fc_in": 2743.6392141259485,
"b18.ff.fc_out": 9030.717912847476,
"b19.ff.fc_in": 2743.6392141259485,
"b19.ff.fc_out": 9030.717912847476,
"b20.ff.fc_in": 3057.4751817363253,
"b20.ff.fc_out": 10063.712585454898,
"b21.ff.fc_in": 3057.4751817363253,
"b21.ff.fc_out": 10063.712585454898,
"b22.ff.fc_in": 3407.2097180654623,
"b22.ff.fc_out": 10063.712585454898,
"b23.ff.fc_in": 3407.2097180654623,
"b23.ff.fc_out": 12497.700799896355,
"b24.ff.fc_in": 3796.948980859055,
"b24.ff.fc_out": 13927.271620769527,
"b25.ff.fc_in": 3796.948980859055,
"b25.ff.fc_out": 19274.08888735978,
"b26.ff.fc_in": 4231.26976101764,
"b26.ff.fc_out": 21478.789600085922,
"b27.ff.fc_in": 4715.270819259384,
"b27.ff.fc_out": 29724.70802687751,
"b28.ff.fc_in": 4715.270819259384,
"b28.ff.fc_out": 45841.764386937015,
"b29.ff.fc_in": 4715.270819259384,
"b29.ff.fc_out": 56928.958163414085,
"b30.ff.fc_in": 5254.635977347013,
"b30.ff.fc_out": 78784.54787887946,
"b31.ff.fc_in": 5254.635977347013,
"b31.ff.fc_out": 109030.7109780948,
"b32.ff.fc_in": 5855.696365467314,
"b32.ff.fc_out": 135400.63235256448,
"b33.ff.fc_in": 5855.696365467314,
"b33.ff.fc_out": 168148.35127510442,
"b34.ff.fc_in": 6525.510437495776,
"b34.ff.fc_out": 168148.35127510442,
"b35.ff.fc_in": 6525.510437495776,
"b35.ff.fc_out": 232702.1493904285,
"b36.ff.fc_in": 6525.510437495776,
"b36.ff.fc_out": 232702.1493904285,
"b37.ff.fc_in": 6525.510437495776,
"b37.ff.fc_out": 208816.33399986484,
"b38.ff.fc_in": 6525.510437495776,
"b38.ff.fc_out": 288982.97321111924,
"b39.ff.fc_in": 7271.942168450891,
"b39.ff.fc_out": 358875.77201571054,
"b40.ff.fc_in": 7271.942168450891,
"b40.ff.fc_out": 445672.69186462485,
"b41.ff.fc_in": 8103.755565259112,
"b41.ff.fc_out": 496651.7297372453,
"b42.ff.fc_in": 9030.717912847476,
"b42.ff.fc_out": 616770.844395513,
"b43.ff.fc_in": 9030.717912847476,
"b43.ff.fc_out": 616770.844395513,
"b44.ff.fc_in": 10063.712585454898,
"b44.ff.fc_out": 687321.2672027495,
"b45.ff.fc_in": 11214.867875490954,
"b45.ff.fc_out": 853555.2832070139,
"b46.ff.fc_in": 12497.700799896355,
"b46.ff.fc_out": 1059994.2350776498,
"b47.ff.fc_in": 13927.271620769527,
"b47.ff.fc_out": 1316362.0994398587,
"b48.ff.fc_in": 17295.691014283653,
"b48.ff.fc_out": 2030107.8425012752,
"b49.ff.fc_in": 19274.08888735978,
"b49.ff.fc_out": 2809486.555264971
}
@@ -0,0 +1,102 @@
{
"transformer_blocks.0.ff.fc_in": 7.34375,
"transformer_blocks.0.ff.fc_out": 6432.0,
"transformer_blocks.1.ff.fc_in": 6.3125,
"transformer_blocks.1.ff.fc_out": 8960.0,
"transformer_blocks.10.ff.fc_in": 15.25,
"transformer_blocks.10.ff.fc_out": 6368.0,
"transformer_blocks.11.ff.fc_in": 17.75,
"transformer_blocks.11.ff.fc_out": 16128.0,
"transformer_blocks.12.ff.fc_in": 18.549999999999997,
"transformer_blocks.12.ff.fc_out": 4064.0,
"transformer_blocks.13.ff.fc_in": 20.25,
"transformer_blocks.13.ff.fc_out": 78848.0,
"transformer_blocks.14.ff.fc_in": 12.0,
"transformer_blocks.14.ff.fc_out": 1784.0,
"transformer_blocks.15.ff.fc_in": 13.1875,
"transformer_blocks.15.ff.fc_out": 1400.0,
"transformer_blocks.16.ff.fc_in": 12.000625,
"transformer_blocks.16.ff.fc_out": 2768.16,
"transformer_blocks.17.ff.fc_in": 22.049999999999997,
"transformer_blocks.17.ff.fc_out": 4960.0,
"transformer_blocks.18.ff.fc_in": 8.875,
"transformer_blocks.18.ff.fc_out": 15616.0,
"transformer_blocks.19.ff.fc_in": 12.4375,
"transformer_blocks.19.ff.fc_out": 3203.2,
"transformer_blocks.2.ff.fc_in": 4.2875,
"transformer_blocks.2.ff.fc_out": 7648.0,
"transformer_blocks.20.ff.fc_in": 8.96875,
"transformer_blocks.20.ff.fc_out": 5696.0,
"transformer_blocks.21.ff.fc_in": 10.0625,
"transformer_blocks.21.ff.fc_out": 6464.0,
"transformer_blocks.22.ff.fc_in": 14.000625,
"transformer_blocks.22.ff.fc_out": 11712.0,
"transformer_blocks.23.ff.fc_in": 13.125,
"transformer_blocks.23.ff.fc_out": 13312.0,
"transformer_blocks.24.ff.fc_in": 18.125,
"transformer_blocks.24.ff.fc_out": 9497.599999999999,
"transformer_blocks.25.ff.fc_in": 10.7625,
"transformer_blocks.25.ff.fc_out": 12864.0,
"transformer_blocks.26.ff.fc_in": 10.443750000000001,
"transformer_blocks.26.ff.fc_out": 11776.0,
"transformer_blocks.27.ff.fc_in": 13.9375,
"transformer_blocks.27.ff.fc_out": 13504.0,
"transformer_blocks.28.ff.fc_in": 13.9375,
"transformer_blocks.28.ff.fc_out": 8448.0,
"transformer_blocks.29.ff.fc_in": 11.4375,
"transformer_blocks.29.ff.fc_out": 13058.559999999998,
"transformer_blocks.3.ff.fc_in": 6.28125,
"transformer_blocks.3.ff.fc_out": 10880.0,
"transformer_blocks.30.ff.fc_in": 12.000625,
"transformer_blocks.30.ff.fc_out": 18816.0,
"transformer_blocks.31.ff.fc_in": 7.75,
"transformer_blocks.31.ff.fc_out": 8705.279999999999,
"transformer_blocks.32.ff.fc_in": 7.9375,
"transformer_blocks.32.ff.fc_out": 13312.0,
"transformer_blocks.33.ff.fc_in": 10.0,
"transformer_blocks.33.ff.fc_out": 8035.200000000001,
"transformer_blocks.34.ff.fc_in": 6.95625,
"transformer_blocks.34.ff.fc_out": 7424.0,
"transformer_blocks.35.ff.fc_in": 9.25,
"transformer_blocks.35.ff.fc_out": 20480.0,
"transformer_blocks.36.ff.fc_in": 9.40625,
"transformer_blocks.36.ff.fc_out": 29440.0,
"transformer_blocks.37.ff.fc_in": 16.0,
"transformer_blocks.37.ff.fc_out": 14272.0,
"transformer_blocks.38.ff.fc_in": 15.751249999999999,
"transformer_blocks.38.ff.fc_out": 28160.0,
"transformer_blocks.39.ff.fc_in": 15.064374999999998,
"transformer_blocks.39.ff.fc_out": 149504.0,
"transformer_blocks.4.ff.fc_in": 6.75,
"transformer_blocks.4.ff.fc_out": 14400.0,
"transformer_blocks.40.ff.fc_in": 11.438125,
"transformer_blocks.40.ff.fc_out": 31232.0,
"transformer_blocks.41.ff.fc_in": 17.25,
"transformer_blocks.41.ff.fc_out": 37383.67999999999,
"transformer_blocks.42.ff.fc_in": 15.0625,
"transformer_blocks.42.ff.fc_out": 15296.0,
"transformer_blocks.43.ff.fc_in": 17.625,
"transformer_blocks.43.ff.fc_out": 24064.0,
"transformer_blocks.44.ff.fc_in": 24.375,
"transformer_blocks.44.ff.fc_out": 99840.0,
"transformer_blocks.45.ff.fc_in": 40.0025,
"transformer_blocks.45.ff.fc_out": 350208.0,
"transformer_blocks.46.ff.fc_in": 42.75749999999999,
"transformer_blocks.46.ff.fc_out": 20070.399999999998,
"transformer_blocks.47.ff.fc_in": 57.2525,
"transformer_blocks.47.ff.fc_out": 49920.0,
"transformer_blocks.48.ff.fc_in": 145.0,
"transformer_blocks.48.ff.fc_out": 96256.0,
"transformer_blocks.49.ff.fc_in": 146.0,
"transformer_blocks.49.ff.fc_out": 200704.0,
"transformer_blocks.5.ff.fc_in": 13.1875,
"transformer_blocks.5.ff.fc_out": 15168.0,
"transformer_blocks.6.ff.fc_in": 19.375,
"transformer_blocks.6.ff.fc_out": 11904.0,
"transformer_blocks.7.ff.fc_in": 33.5,
"transformer_blocks.7.ff.fc_out": 15872.0,
"transformer_blocks.8.ff.fc_in": 16.5,
"transformer_blocks.8.ff.fc_out": 9216.0,
"transformer_blocks.9.ff.fc_in": 19.375,
"transformer_blocks.9.ff.fc_out": 4096.0
}
@@ -0,0 +1,102 @@
{
"b0.ff.fc_in": 5.625,
"b0.ff.fc_out": 2008.0,
"b1.ff.fc_in": 4.5,
"b1.ff.fc_out": 2432.0,
"b2.ff.fc_in": 4.25,
"b2.ff.fc_out": 1936.0,
"b3.ff.fc_in": 4.84375,
"b3.ff.fc_out": 1432.0,
"b4.ff.fc_in": 6.5625,
"b4.ff.fc_out": 2912.0,
"b5.ff.fc_in": 17.549999999999955,
"b5.ff.fc_out": 4592.0,
"b6.ff.fc_in": 26.0,
"b6.ff.fc_out": 5008.0,
"b7.ff.fc_in": 19.862499999999997,
"b7.ff.fc_out": 4224.0,
"b8.ff.fc_in": 12.424999999999999,
"b8.ff.fc_out": 2208.0,
"b9.ff.fc_in": 12.53125,
"b9.ff.fc_out": 2576.0,
"b10.ff.fc_in": 10.1875,
"b10.ff.fc_out": 4832.0,
"b11.ff.fc_in": 17.625,
"b11.ff.fc_out": 6406.4,
"b12.ff.fc_in": 18.125,
"b12.ff.fc_out": 1464.0,
"b13.ff.fc_in": 6.9125,
"b13.ff.fc_out": 20736.0,
"b14.ff.fc_in": 7.46875,
"b14.ff.fc_out": 1444.8,
"b15.ff.fc_in": 9.1875,
"b15.ff.fc_out": 1688.7999999999993,
"b16.ff.fc_in": 10.381249999999994,
"b16.ff.fc_out": 3216.0,
"b17.ff.fc_in": 25.125,
"b17.ff.fc_out": 2480.0,
"b18.ff.fc_in": 10.971874999999997,
"b18.ff.fc_out": 12736.0,
"b19.ff.fc_in": 9.6875,
"b19.ff.fc_out": 3852.7999999999997,
"b20.ff.fc_in": 8.35625,
"b20.ff.fc_out": 2755.2,
"b21.ff.fc_in": 11.0,
"b21.ff.fc_out": 8630.39999999998,
"b22.ff.fc_in": 13.9375,
"b22.ff.fc_out": 7188.799999999996,
"b23.ff.fc_in": 12.95,
"b23.ff.fc_out": 6208.0,
"b24.ff.fc_in": 12.6875,
"b24.ff.fc_out": 7572.799999999996,
"b25.ff.fc_in": 9.31875,
"b25.ff.fc_out": 10304.0,
"b26.ff.fc_in": 14.25,
"b26.ff.fc_out": 5632.0,
"b27.ff.fc_in": 11.125,
"b27.ff.fc_out": 9705.599999999991,
"b28.ff.fc_in": 10.671874999999986,
"b28.ff.fc_out": 9542.4,
"b29.ff.fc_in": 9.21875,
"b29.ff.fc_out": 12550.399999999994,
"b30.ff.fc_in": 10.0,
"b30.ff.fc_out": 18176.0,
"b31.ff.fc_in": 9.846874999999997,
"b31.ff.fc_out": 13440.0,
"b32.ff.fc_in": 11.5,
"b32.ff.fc_out": 8704.0,
"b33.ff.fc_in": 10.125,
"b33.ff.fc_out": 12185.599999999999,
"b34.ff.fc_in": 9.42812499999998,
"b34.ff.fc_out": 8512.0,
"b35.ff.fc_in": 7.375,
"b35.ff.fc_out": 6720.0,
"b36.ff.fc_in": 17.875,
"b36.ff.fc_out": 30208.0,
"b37.ff.fc_in": 10.53125,
"b37.ff.fc_out": 13056.0,
"b38.ff.fc_in": 13.9125,
"b38.ff.fc_out": 29535.99999999997,
"b39.ff.fc_in": 7.7437499999999995,
"b39.ff.fc_out": 302182.3999999999,
"b40.ff.fc_in": 10.3125,
"b40.ff.fc_out": 11712.0,
"b41.ff.fc_in": 13.1875,
"b41.ff.fc_out": 10368.0,
"b42.ff.fc_in": 16.25,
"b42.ff.fc_out": 15641.599999999977,
"b43.ff.fc_in": 17.625,
"b43.ff.fc_out": 28544.0,
"b44.ff.fc_in": 36.25,
"b44.ff.fc_out": 113254.4,
"b45.ff.fc_in": 44.52499999999998,
"b45.ff.fc_out": 156262.4,
"b46.ff.fc_in": 57.25,
"b46.ff.fc_out": 14272.0,
"b47.ff.fc_in": 56.5,
"b47.ff.fc_out": 30400.0,
"b48.ff.fc_in": 219.0999999999999,
"b48.ff.fc_out": 75264.0,
"b49.ff.fc_in": 111.0,
"b49.ff.fc_out": 166912.0
}
@@ -0,0 +1,102 @@
{
"b0.ff.fc_in": 5.75,
"b0.ff.fc_out": 1792.0,
"b1.ff.fc_in": 8.9375,
"b1.ff.fc_out": 2704.0,
"b2.ff.fc_in": 4.5625,
"b2.ff.fc_out": 2096.0,
"b3.ff.fc_in": 6.875,
"b3.ff.fc_out": 2384.0,
"b4.ff.fc_in": 11.125,
"b4.ff.fc_out": 2720.0,
"b5.ff.fc_in": 18.549999999999997,
"b5.ff.fc_out": 8225.280000000006,
"b6.ff.fc_in": 33.0,
"b6.ff.fc_out": 4352.0,
"b7.ff.fc_in": 18.75,
"b7.ff.fc_out": 4608.0,
"b8.ff.fc_in": 13.375,
"b8.ff.fc_out": 2320.0,
"b9.ff.fc_in": 13.212499999999999,
"b9.ff.fc_out": 2890.880000000001,
"b10.ff.fc_in": 23.875,
"b10.ff.fc_out": 4320.0,
"b11.ff.fc_in": 13.737499999999999,
"b11.ff.fc_out": 6227.2,
"b12.ff.fc_in": 14.874999999999998,
"b12.ff.fc_out": 1632.0,
"b13.ff.fc_in": 15.049999999999999,
"b13.ff.fc_out": 22144.0,
"b14.ff.fc_in": 7.46875,
"b14.ff.fc_out": 1552.0,
"b15.ff.fc_in": 9.4375,
"b15.ff.fc_out": 1444.8,
"b16.ff.fc_in": 11.272500000000008,
"b16.ff.fc_out": 3216.0,
"b17.ff.fc_in": 30.5,
"b17.ff.fc_out": 3760.0,
"b18.ff.fc_in": 11.4375,
"b18.ff.fc_out": 14656.0,
"b19.ff.fc_in": 12.5,
"b19.ff.fc_out": 4144.0,
"b20.ff.fc_in": 15.5,
"b20.ff.fc_out": 4544.0,
"b21.ff.fc_in": 10.149999999999999,
"b21.ff.fc_out": 9920.0,
"b22.ff.fc_in": 14.524999999999999,
"b22.ff.fc_out": 6656.0,
"b23.ff.fc_in": 18.75,
"b23.ff.fc_out": 8288.0,
"b24.ff.fc_in": 12.987500000000004,
"b24.ff.fc_out": 11520.0,
"b25.ff.fc_in": 14.625,
"b25.ff.fc_out": 9963.520000000004,
"b26.ff.fc_in": 10.0625,
"b26.ff.fc_out": 9390.080000000016,
"b27.ff.fc_in": 11.5625,
"b27.ff.fc_out": 6784.0,
"b28.ff.fc_in": 10.587499999999999,
"b28.ff.fc_out": 9900.8,
"b29.ff.fc_in": 8.44375,
"b29.ff.fc_out": 11991.040000000008,
"b30.ff.fc_in": 10.25,
"b30.ff.fc_out": 15411.199999999999,
"b31.ff.fc_in": 11.355000000000004,
"b31.ff.fc_out": 8576.0,
"b32.ff.fc_in": 12.9375,
"b32.ff.fc_out": 12928.0,
"b33.ff.fc_in": 11.125,
"b33.ff.fc_out": 9907.200000000004,
"b34.ff.fc_in": 10.230000000000004,
"b34.ff.fc_out": 14656.0,
"b35.ff.fc_in": 13.75,
"b35.ff.fc_out": 15360.0,
"b36.ff.fc_in": 14.417500000000004,
"b36.ff.fc_out": 29568.0,
"b37.ff.fc_in": 11.4375,
"b37.ff.fc_out": 12759.040000000008,
"b38.ff.fc_in": 19.085000000000008,
"b38.ff.fc_out": 30848.0,
"b39.ff.fc_in": 8.44375,
"b39.ff.fc_out": 290816.0,
"b40.ff.fc_in": 12.272500000000008,
"b40.ff.fc_out": 20787.199999999997,
"b41.ff.fc_in": 10.8125,
"b41.ff.fc_out": 18887.68000000014,
"b42.ff.fc_in": 11.06875,
"b42.ff.fc_out": 11264.0,
"b43.ff.fc_in": 16.275,
"b43.ff.fc_out": 25728.0,
"b44.ff.fc_in": 36.75,
"b44.ff.fc_out": 54476.799999999996,
"b45.ff.fc_in": 45.51000000000005,
"b45.ff.fc_out": 118272.0,
"b46.ff.fc_in": 40.20000000000002,
"b46.ff.fc_out": 20070.399999999998,
"b47.ff.fc_in": 54.0,
"b47.ff.fc_out": 31232.0,
"b48.ff.fc_in": 113.5,
"b48.ff.fc_out": 40960.0,
"b49.ff.fc_in": 128.0,
"b49.ff.fc_out": 167731.19999999998
}
@@ -0,0 +1,135 @@
# FastH3 OmniRef NVFP4 QAD (the shared FP4 QAD recipe, Ref2VA profile).
#
# Student: OmniRef Ref2VA (8-forward PDD ladder) with the deployed NVFP4
# numerics active from step 0: NVFP4 W4A4 q/k/v/out plus FFN fc_in/fc_out in
# all 50 blocks, and the FP4 sparse-attention emulator at VSA tile 128. The
# VSA compression gates and every norm stay frozen at the calibrated bf16
# values; training them drifts the tile selection and causes ghosting. The
# teacher is the same checkpoint in bf16.
#
# See docs/training/minimax_h3_fp4_qad.md for the recipe shared with the V2
# (T2AV QAD) profile, calibration, and the checkpoint contract.
#
# Released result: FastVideo/FastH3-OmniRef-NVFP4-QAD (step 120 of 200).
# `init_from` is the calibrated OmniRef checkpoint used as QAD step 0,
# not that released result. The calibrated amax table is vendored under
# examples/train/calibration/fasth3/ and pinned by sha256 below.
models:
student:
_target_: fastvideo.train.models.minimax_h3.omniref_pdd.MiniMaxH3OmniRefPDDModel
init_from: data/models/omniref-nvfp4-ptq
trainable: true
enable_gradient_checkpointing_type: full
qad:
enabled: true
linears:
enabled: true
amax_json: examples/train/calibration/fasth3/amax-omniref-mse-bf16-top8.json
# Attention and gate linears use the unit activation scale; only the
# FFN linears read their calibrated amax from this table.
attention: true
gate: true
ffn: true
skip_blocks: []
amax_sha256: 14b93ec308faf0d89bd13ded507d287c030bb3005c96b8063213eb8a09df083d
fp4_attention:
enabled: true
two_level_p: true
smooth_k: true
first_block_max_floor: null
data:
manifests:
- data/omniref/train-manifest.jsonl
cases:
- first_frame
- first_last_frame
- storyboard
- continue_scene
- continue_shot
- full_scene_audio_image_reference
per_group:
480p: 400
768p: 50
seed: 20261007
max_frames: 243
eval_manifest: data/omniref/eval-manifest.json
eval_resolutions:
- 480p
- 768p
eval_per_group:
480p: 2
768p: 1
eval_max_frames: 0
# Only the projections and FFN linears train: the same set the deployed
# NVFP4 export quantizes. Gates, norms and modulation stay frozen.
trainable_patterns: transformer_blocks\.\d+\.(attn\.(to_q|to_k|to_v|to_out)|ff\.(fc_in|fc_out))\.
teacher:
_target_: fastvideo.train.models.minimax_h3.omniref_pdd.MiniMaxH3OmniRefPDDModel
init_from: data/models/omniref-nvfp4-ptq
trainable: false
frozen_bf16: true
method:
_target_: fastvideo.train.methods.knowledge_distillation.pdd_qad.OmniRefPDDQADMethod
video_weight: 1.0
audio_weight: 1.0
rollout_fraction: 0.25
rollout_start_step: 20
rungs_per_row: 4
eval_every: 15
full_eval_every: 30
patience: 2
fidelity_tolerance: 0.05
require_bitwise_init: true
save_best: true
select_metric: score
eval_save_dir: outputs/omniref_nvfp4_qad/samples
objective: output+recon
recon_lambda: 1.0
recon_normalize: step0
max_rollout_rows: 10
noise_floor_eval: true
step0_cache: outputs/omniref_nvfp4_qad/step0_cache.json
training:
distributed:
num_gpus: 16
sp_size: 4
tp_size: 1
hsdp_replicate_dim: 1
hsdp_shard_dim: 16
pin_cpu_memory: false
data:
train_batch_size: 1
seed: 1234
optimizer:
learning_rate: 5.0e-06
betas: [0.9, 0.999]
eps: 1.0e-08
weight_decay: 0.0
lr_scheduler: cosine_with_min_lr
lr_warmup_steps: 10
min_lr_ratio: 0.3
loop:
max_train_steps: 200
gradient_accumulation_steps: 1
checkpoint:
output_dir: outputs/omniref_nvfp4_qad
training_state_checkpointing_steps: 25
checkpoints_total_limit: 2
resume_from_checkpoint: ''
tracker:
trackers: [wandb]
project_name: fasth3-omniref-nvfp4
run_name: omniref-nvfp4-qad
model:
enable_gradient_checkpointing_type: full
dit_precision: fp32
vsa_sparsity: 0.0
callbacks:
grad_clip:
_target_: fastvideo.train.callbacks.grad_clip.GradNormClipCallback
max_grad_norm: 1.0
pipeline: {}
@@ -0,0 +1,149 @@
# FastH3 V2 NVFP4 QAD (the shared FP4 QAD recipe, T2VA profile).
#
# Student: V2 bf16 weights with the deployed NVFP4 numerics active from step 0:
# NVFP4 W4A4 q/k/v/out plus FFN fc_in/fc_out in all 50 blocks, and the FP4
# sparse-attention emulator at VSA tile 64. The VSA compression gates and every
# norm stay frozen at the V2 bf16 values. The teacher is the same weights in
# bf16. Middle-block token compression is disabled: pure QAD.
#
# This is one recipe with two profiles; see docs/training/minimax_h3_fp4_qad.md
# for the shared objective, evaluation and checkpoint contract, and for the
# OmniRef (Ref2VA, tile 128) config.
#
# Paths are placeholders. Fill in the bf16 V2 model, the calibration table
# shipped with it, the latent manifests and the output dir.
# Starting weights: FastVideo/FastVideo-FastH3-8-Step-V2 (bf16). The released
# QAD result is FastVideo/FastH3-QAD-v2; the
# NVFP4 baseline it replaces is
# FastVideo/FastVideo-FastH3-8-Step-V2-NVFP4-Consumer.
models:
student:
_target_: fastvideo.train.models.minimax_h3.v2_t2v.MiniMaxH3V2T2VModel
init_from: data/models/FastH3-V2
trainable: true
enable_gradient_checkpointing_type: full
qad:
enabled: true
linears:
enabled: true
amax_json: examples/train/calibration/fasth3/amax-consumer.json
# Attention and gate linears use the unit activation scale; only the
# FFN linears read their calibrated amax from this table.
attention: true
gate: true
ffn: true
skip_blocks: []
amax_sha256: 0800516caec04ec829d21c45ca8b18eede6b81008c040997c0b84d48e0d39e52
fp4_attention:
enabled: true
two_level_p: true
smooth_k: true
first_block_max_floor: null
compression:
enabled: false
start: 3
end: 41
mode: subsample
data:
train_manifest: data/fasth3-v2/train.jsonl
eval_manifest: data/fasth3-v2/eval.jsonl
width: 832
height: 480
num_frames: 124
# Only the projections and FFN linears train: the same set the deployed
# NVFP4 export quantizes. Gates, norms and modulation stay frozen.
trainable_patterns: transformer_blocks\.\d+\.(attn\.(to_q|to_k|to_v|to_out)|ff\.(fc_in|fc_out))\.
teacher:
_target_: fastvideo.train.models.minimax_h3.v2_t2v.MiniMaxH3V2T2VModel
init_from: data/models/FastH3-V2
trainable: false
method:
_target_: fastvideo.train.methods.knowledge_distillation.pdd_qad.OmniRefPDDQADMethod
video_weight: 1.0
audio_weight: 1.0
rollout_fraction: 0.25
rollout_start_step: 20
rungs_per_row: 4
eval_every: 25
full_eval_every: 50
patience: 3
fidelity_tolerance: 0.05
require_bitwise_init: true
save_best: true
select_metric: score
eval_save_dir: outputs/v2_1_nvfp4_qad/samples
objective: output+recon
recon_lambda: 1.0
recon_normalize: step0
max_rollout_rows: 8
noise_floor_eval: true
# A checkpoint is eligible for "best" only if audio stays inside its
# step-0 guardrail; the ELBO-style score alone can trade audio for video.
eligibility_keys:
- endpoint/audio
- x0_rel_l2/audio/mean
step0_cache: outputs/v2_1_nvfp4_qad/step0_cache.json
training:
distributed:
num_gpus: 16
sp_size: 1
tp_size: 1
hsdp_replicate_dim: 1
hsdp_shard_dim: 16
pin_cpu_memory: false
data:
train_batch_size: 1
seed: 1234
optimizer:
learning_rate: 5.0e-06
betas: [0.9, 0.999]
eps: 1.0e-08
weight_decay: 0.0
lr_scheduler: cosine_with_min_lr
lr_warmup_steps: 10
min_lr_ratio: 0.3
loop:
max_train_steps: 300
gradient_accumulation_steps: 1
checkpoint:
output_dir: outputs/v2_1_nvfp4_qad
training_state_checkpointing_steps: 50
checkpoints_total_limit: 1
resume_from_checkpoint: ''
tracker:
trackers: [wandb]
project_name: fasth3-v2-nvfp4-qad
run_name: v2-nvfp4-qad
model:
enable_gradient_checkpointing_type: full
dit_precision: fp32
vsa_sparsity: 0.0
callbacks:
grad_clip:
_target_: fastvideo.train.callbacks.grad_clip.GradNormClipCallback
max_grad_norm: 1.0
pipeline: {}
# The published FastVideo/FastH3-QAD-v2
# checkpoint is the linears-only variant of this schedule: NVFP4 linears with
# BF16 VSA attention, i.e. the same config with fp4_attention.enabled=false.
# The shared FP4 recipe above adds FP4 attention from step 0 and does not
# reproduce that checkpoint bit for bit.
#
# FP4-attention adaptation (provenance note, not part of the public
# reproduction). The internal follow-up started from a linears-only 300-step
# DCP, enabled FP4 attention, and trained 300 more steps at lr 1.5e-6 with no
# warmup and a constant LR. It is an adaptation of the linears-only checkpoint
# to FP4 attention, not a second epoch of the FP4 stage 1 above, and it needs
# a DCP of the linears-only model (the packed HF release is not one):
#
# method.init_student_dcp=<linears-only 300-step DCP>
# training.optimizer.learning_rate=1.5e-06
# training.optimizer.lr_warmup_steps=0
# training.optimizer.min_lr_ratio=1.0
# training.loop.max_train_steps=300
# training.checkpoint.output_dir=outputs/v2_1_nvfp4_qad_fp4_adapt
@@ -0,0 +1,132 @@
# Pika H3 Flex FL2VA stage 2: adapt the stage-1 NVFP4 QAD student to dense FP4 attention.
#
# Same recipe, data and calibration as fl2va_nvfp4_qad.yaml, starting from that run's best
# checkpoint (method.init_student_dcp). Attention runs the FlashAttention-4
# NVFP4 numerics (Q/K in NVFP4 per 16 channels, P and V in BF16) through the
# dense FP4 emulator with straight-through gradients. Q/K are preconditioned
# exactly before quantization (token-mean subtraction on K, then a shared
# 128-point Hadamard rotation of Q and K), and blocks 45 and 49, the two most
# FP4-sensitive attention layers, keep BF16 attention. Deploy with the same
# settings: FASTVIDEO_NVFP4_FA4_QK_TRANSFORM=smooth_k+hadamard and
# FASTVIDEO_NVFP4_FA4_BF16_BLOCKS=45,49 (or the example's --nvfp4-attention
# flags).
#
# Data and model paths are placeholders.
models:
student:
_target_: fastvideo.train.models.minimax_h3.flex.MiniMaxH3FlexFL2VAModel
init_from: data/models/pika-flex-fl2va-merged
trainable: true
enable_gradient_checkpointing_type: full
qad:
enabled: true
linears:
enabled: true
amax_json: examples/train/calibration/pika_h3_flex/amax-fl2va-balanced.json
amax_sha256: b690d1176702b399975d4e0e838784edc89584fda520abb94911e53f553e0daa
# Attention linears use the unit activation scale; only the FFN
# linears read their calibrated amax from this table.
attention: true
ffn: true
skip_blocks: []
fp4_attention:
enabled: true
qk_transform: smooth_k+hadamard
bf16_blocks: [45, 49]
data:
manifests:
- data/flex-fl2va/MANIFEST.jsonl
cases:
- t2va
- i2va
- l2va
- fl2va
per_group:
480p: 160
768p: 40
seed: 20261007
max_frames: 345
# FL2VA holds out rows from its own groups (eval_per_group top-up); finalize writes an empty
# manifest here, which the trainer still expects.
eval_manifest: data/flex-fl2va/eval-manifest.json
eval_resolutions:
- 480p
- 768p
eval_per_group:
480p: 2
768p: 1
eval_max_frames: 0
teacher:
_target_: fastvideo.train.models.minimax_h3.flex.MiniMaxH3FlexFL2VAModel
init_from: data/models/pika-flex-fl2va-merged
trainable: false
frozen_bf16: true
method:
_target_: fastvideo.train.methods.knowledge_distillation.pdd_qad.OmniRefPDDQADMethod
# Stage-1 best checkpoint (its tagged student weights).
init_student_dcp: outputs/pika_flex_fl2va_nvfp4_qad/best
video_weight: 1.0
audio_weight: 1.0
rollout_fraction: 0.25
rollout_start_step: 20
# Half of the six forwards train per row, as OmniRef trains 4 of 8.
rungs_per_row: 3
eval_every: 15
full_eval_every: 30
patience: 2
fidelity_tolerance: 0.05
# Ignored when init_student_dcp is set (the student no longer starts from the teacher's weights).
require_bitwise_init: true
save_best: true
select_metric: score
eval_save_dir: outputs/pika_flex_fl2va_fp4attn_qad/samples
objective: output+recon
recon_lambda: 1.0
recon_normalize: step0
max_rollout_rows: 10
noise_floor_eval: true
step0_cache: outputs/pika_flex_fl2va_fp4attn_qad/step0_cache.json
training:
distributed:
num_gpus: 12
sp_size: 4
tp_size: 1
hsdp_replicate_dim: 1
hsdp_shard_dim: 12
pin_cpu_memory: false
data:
train_batch_size: 1
seed: 1234
optimizer:
learning_rate: 3.0e-06
betas: [0.9, 0.999]
eps: 1.0e-08
weight_decay: 0.0
lr_scheduler: cosine_with_min_lr
lr_warmup_steps: 10
min_lr_ratio: 0.3
loop:
max_train_steps: 200
gradient_accumulation_steps: 1
checkpoint:
output_dir: outputs/pika_flex_fl2va_fp4attn_qad
training_state_checkpointing_steps: 25
checkpoints_total_limit: 2
resume_from_checkpoint: ''
tracker:
trackers: [wandb]
project_name: pika-h3-flex-nvfp4
run_name: pika-flex-fl2va-fp4attn-qad
model:
enable_gradient_checkpointing_type: full
dit_precision: fp32
vsa_sparsity: 0.0
callbacks:
grad_clip:
_target_: fastvideo.train.callbacks.grad_clip.GradNormClipCallback
max_grad_norm: 1.0
pipeline: {}
@@ -0,0 +1,128 @@
# Pika H3 Flex FL2VA NVFP4 QAD (the FastH3 QAD recipe on Flex's six-forward ladder).
#
# Teacher: Pika H3 Flex FL2VA in bf16, i.e. MiniMax-H3 `transformer` with the
# released `fl2va` adapter merged by
# scripts/distill/pika_h3_flex_qad/merge_pika_adapter.py. Student: the same
# weights with NVFP4 W4A4 q/k/v/out plus FFN fc_in/fc_out in all 50 blocks,
# static FFN scales from the Flex FL2VA calibration table, BF16 dense attention
# (fl2va_fp4attn_qad.yaml adapts the result to dense FP4 attention).
#
# Rows are T2VA / I2VA / L2VA / FL2VA keyframe-anchor rows built from encoded
# Ref2VA rows by scripts/distill/pika_h3_flex_qad/build_flex_ref2va_rows.py
# --mode fl2va (exact first/last frames, re-encoded at the canvas size).
#
# Data and model paths are placeholders: point them at the merged model, the
# row manifests and an output dir. The calibration table is vendored and pinned.
models:
student:
_target_: fastvideo.train.models.minimax_h3.flex.MiniMaxH3FlexFL2VAModel
init_from: data/models/pika-flex-fl2va-merged
trainable: true
enable_gradient_checkpointing_type: full
qad:
enabled: true
linears:
enabled: true
amax_json: examples/train/calibration/pika_h3_flex/amax-fl2va-balanced.json
amax_sha256: b690d1176702b399975d4e0e838784edc89584fda520abb94911e53f553e0daa
# Attention linears use the unit activation scale; only the FFN
# linears read their calibrated amax from this table.
attention: true
ffn: true
skip_blocks: []
fp4_attention:
enabled: false
data:
manifests:
- data/flex-fl2va/MANIFEST.jsonl
cases:
- t2va
- i2va
- l2va
- fl2va
per_group:
480p: 160
768p: 40
seed: 20261007
max_frames: 345
# FL2VA holds out rows from its own groups (eval_per_group top-up); finalize writes an empty
# manifest here, which the trainer still expects.
eval_manifest: data/flex-fl2va/eval-manifest.json
eval_resolutions:
- 480p
- 768p
eval_per_group:
480p: 2
768p: 1
eval_max_frames: 0
teacher:
_target_: fastvideo.train.models.minimax_h3.flex.MiniMaxH3FlexFL2VAModel
init_from: data/models/pika-flex-fl2va-merged
trainable: false
frozen_bf16: true
method:
_target_: fastvideo.train.methods.knowledge_distillation.pdd_qad.OmniRefPDDQADMethod
video_weight: 1.0
audio_weight: 1.0
rollout_fraction: 0.25
rollout_start_step: 20
# Half of the six forwards train per row, as OmniRef trains 4 of 8.
rungs_per_row: 3
eval_every: 15
full_eval_every: 30
patience: 2
fidelity_tolerance: 0.05
require_bitwise_init: true
save_best: true
select_metric: score
eval_save_dir: outputs/pika_flex_fl2va_nvfp4_qad/samples
objective: output+recon
recon_lambda: 1.0
recon_normalize: step0
max_rollout_rows: 10
noise_floor_eval: true
step0_cache: outputs/pika_flex_fl2va_nvfp4_qad/step0_cache.json
training:
distributed:
num_gpus: 16
sp_size: 4
tp_size: 1
hsdp_replicate_dim: 1
hsdp_shard_dim: 16
pin_cpu_memory: false
data:
train_batch_size: 1
seed: 1234
optimizer:
learning_rate: 5.0e-06
betas: [0.9, 0.999]
eps: 1.0e-08
weight_decay: 0.0
lr_scheduler: cosine_with_min_lr
lr_warmup_steps: 10
min_lr_ratio: 0.3
loop:
max_train_steps: 200
gradient_accumulation_steps: 1
checkpoint:
output_dir: outputs/pika_flex_fl2va_nvfp4_qad
training_state_checkpointing_steps: 25
checkpoints_total_limit: 2
resume_from_checkpoint: ''
tracker:
trackers: [wandb]
project_name: pika-h3-flex-nvfp4
run_name: pika-flex-fl2va-nvfp4-qad
model:
enable_gradient_checkpointing_type: full
dit_precision: fp32
vsa_sparsity: 0.0
callbacks:
grad_clip:
_target_: fastvideo.train.callbacks.grad_clip.GradNormClipCallback
max_grad_norm: 1.0
pipeline: {}
@@ -0,0 +1,139 @@
# Pika H3 Flex Ref2VA stage 2: adapt the stage-1 NVFP4 QAD student to dense FP4 attention.
#
# Same recipe, data and calibration as ref2va_nvfp4_qad.yaml, starting from that run's best
# checkpoint (method.init_student_dcp). Attention runs the FlashAttention-4
# NVFP4 numerics (Q/K in NVFP4 per 16 channels, P and V in BF16) through the
# dense FP4 emulator with straight-through gradients. Q/K are preconditioned
# exactly before quantization (token-mean subtraction on K, then a shared
# 128-point Hadamard rotation of Q and K), and blocks 45 and 49, the two most
# FP4-sensitive attention layers, keep BF16 attention. Deploy with the same
# settings: FASTVIDEO_NVFP4_FA4_QK_TRANSFORM=smooth_k+hadamard and
# FASTVIDEO_NVFP4_FA4_BF16_BLOCKS=45,49 (or the example's --nvfp4-attention
# flags).
#
# Data and model paths are placeholders.
models:
student:
_target_: fastvideo.train.models.minimax_h3.flex.MiniMaxH3FlexRef2VAModel
init_from: data/models/pika-flex-ref2va-merged
trainable: true
enable_gradient_checkpointing_type: full
qad:
enabled: true
linears:
enabled: true
amax_json: examples/train/calibration/pika_h3_flex/amax-ref2va-balanced.json
amax_sha256: ad60b45fea00906c55e370a0dec82638eb413d6b68537d1ceb38fc52da90b373
# Attention linears use the unit activation scale; only the FFN
# linears read their calibrated amax from this table.
attention: true
ffn: true
skip_blocks: []
fp4_attention:
enabled: true
qk_transform: smooth_k+hadamard
bf16_blocks: [45, 49]
data:
manifests:
# Flex-sized rows (build_flex_ref2va_rows.py) first: they replace source rows with the same id
# and resolution.
- data/flex-ref2va/MANIFEST.jsonl
- data/omniref/MANIFEST.jsonl
cases:
- first_frame
- first_last_frame
- storyboard
- continue_scene
- continue_shot
- audio_reference
- mixed_image_video
- many_images
- multi_video
per_group:
480p: 160
768p: 40
seed: 20261007
max_frames: 345
# Held-out rows: the source held-out manifest with Flex-sized rows (written by finalize).
eval_manifest: data/flex-ref2va/eval-manifest.json
eval_resolutions:
- 480p
- 768p
eval_per_group:
480p: 2
768p: 1
eval_max_frames: 0
teacher:
_target_: fastvideo.train.models.minimax_h3.flex.MiniMaxH3FlexRef2VAModel
init_from: data/models/pika-flex-ref2va-merged
trainable: false
frozen_bf16: true
method:
_target_: fastvideo.train.methods.knowledge_distillation.pdd_qad.OmniRefPDDQADMethod
# Stage-1 best checkpoint (its tagged student weights).
init_student_dcp: outputs/pika_flex_ref2va_nvfp4_qad/best
video_weight: 1.0
audio_weight: 1.0
rollout_fraction: 0.25
rollout_start_step: 20
# Half of the six forwards train per row, as OmniRef trains 4 of 8.
rungs_per_row: 3
eval_every: 15
full_eval_every: 30
patience: 2
fidelity_tolerance: 0.05
# Ignored when init_student_dcp is set (the student no longer starts from the teacher's weights).
require_bitwise_init: true
save_best: true
select_metric: score
eval_save_dir: outputs/pika_flex_ref2va_fp4attn_qad/samples
objective: output+recon
recon_lambda: 1.0
recon_normalize: step0
max_rollout_rows: 10
noise_floor_eval: true
step0_cache: outputs/pika_flex_ref2va_fp4attn_qad/step0_cache.json
training:
distributed:
num_gpus: 12
sp_size: 4
tp_size: 1
hsdp_replicate_dim: 1
hsdp_shard_dim: 12
pin_cpu_memory: false
data:
train_batch_size: 1
seed: 1234
optimizer:
learning_rate: 5.0e-06
betas: [0.9, 0.999]
eps: 1.0e-08
weight_decay: 0.0
lr_scheduler: cosine_with_min_lr
lr_warmup_steps: 10
min_lr_ratio: 0.3
loop:
max_train_steps: 200
gradient_accumulation_steps: 1
checkpoint:
output_dir: outputs/pika_flex_ref2va_fp4attn_qad
training_state_checkpointing_steps: 25
checkpoints_total_limit: 2
resume_from_checkpoint: ''
tracker:
trackers: [wandb]
project_name: pika-h3-flex-nvfp4
run_name: pika-flex-ref2va-fp4attn-qad
model:
enable_gradient_checkpointing_type: full
dit_precision: fp32
vsa_sparsity: 0.0
callbacks:
grad_clip:
_target_: fastvideo.train.callbacks.grad_clip.GradNormClipCallback
max_grad_norm: 1.0
pipeline: {}
@@ -0,0 +1,138 @@
# Pika H3 Flex Ref2VA NVFP4 QAD (the FastH3 QAD recipe on Flex's six-forward ladder).
#
# Teacher: Pika H3 Flex Ref2VA in bf16, i.e. MiniMax-H3 `transformer_ref` with the
# released `ref2va` adapter merged by
# scripts/distill/pika_h3_flex_qad/merge_pika_adapter.py. Student: the same
# weights with NVFP4 W4A4 q/k/v/out plus FFN fc_in/fc_out in all 50 blocks
# active from step 0, static FFN activation scales from the Flex calibration
# table. Attention stays BF16 dense in this stage (Flex has no VSA gate to
# freeze); ref2va_fp4attn_qad.yaml adapts the result to dense FP4 attention.
# Norms, AdaLN, the token refiner and the FP32 boundary modules stay frozen.
#
# The objective, evaluation, guards and checkpoint contract are the shared
# recipe's (docs/training/minimax_h3_fp4_qad.md). Rows are OmniRef Ref2VA
# parquet rows; the teacher computes every target on the fly, so the OmniRef
# held-out eval manifest is reused as is.
#
# Data and model paths are placeholders: point them at the merged model, the
# row manifests and an output dir. The calibration table is vendored and pinned.
models:
student:
_target_: fastvideo.train.models.minimax_h3.flex.MiniMaxH3FlexRef2VAModel
init_from: data/models/pika-flex-ref2va-merged
trainable: true
enable_gradient_checkpointing_type: full
qad:
enabled: true
linears:
enabled: true
amax_json: examples/train/calibration/pika_h3_flex/amax-ref2va-balanced.json
amax_sha256: ad60b45fea00906c55e370a0dec82638eb413d6b68537d1ceb38fc52da90b373
# Attention linears use the unit activation scale; only the FFN
# linears read their calibrated amax from this table.
attention: true
ffn: true
skip_blocks: []
fp4_attention:
enabled: false
data:
manifests:
# Flex-sized rows (build_flex_ref2va_rows.py) first: they replace source rows with the same id
# and resolution.
- data/flex-ref2va/MANIFEST.jsonl
- data/omniref/MANIFEST.jsonl
cases:
- first_frame
- first_last_frame
- storyboard
- continue_scene
- continue_shot
- audio_reference
- mixed_image_video
- many_images
- multi_video
per_group:
480p: 160
768p: 40
seed: 20261007
max_frames: 345
# Held-out rows: the source held-out manifest with Flex-sized rows (written by finalize).
eval_manifest: data/flex-ref2va/eval-manifest.json
eval_resolutions:
- 480p
- 768p
eval_per_group:
480p: 2
768p: 1
eval_max_frames: 0
teacher:
_target_: fastvideo.train.models.minimax_h3.flex.MiniMaxH3FlexRef2VAModel
init_from: data/models/pika-flex-ref2va-merged
trainable: false
frozen_bf16: true
method:
_target_: fastvideo.train.methods.knowledge_distillation.pdd_qad.OmniRefPDDQADMethod
video_weight: 1.0
audio_weight: 1.0
rollout_fraction: 0.25
rollout_start_step: 20
# Half of the six forwards train per row, as OmniRef trains 4 of 8.
rungs_per_row: 3
eval_every: 15
full_eval_every: 30
patience: 2
fidelity_tolerance: 0.05
require_bitwise_init: true
save_best: true
select_metric: score
eval_save_dir: outputs/pika_flex_ref2va_nvfp4_qad/samples
objective: output+recon
recon_lambda: 1.0
recon_normalize: step0
max_rollout_rows: 10
noise_floor_eval: true
step0_cache: outputs/pika_flex_ref2va_nvfp4_qad/step0_cache.json
training:
distributed:
num_gpus: 16
sp_size: 4
tp_size: 1
hsdp_replicate_dim: 1
hsdp_shard_dim: 16
pin_cpu_memory: false
data:
train_batch_size: 1
seed: 1234
optimizer:
learning_rate: 5.0e-06
betas: [0.9, 0.999]
eps: 1.0e-08
weight_decay: 0.0
lr_scheduler: cosine_with_min_lr
lr_warmup_steps: 10
min_lr_ratio: 0.3
loop:
max_train_steps: 200
gradient_accumulation_steps: 1
checkpoint:
output_dir: outputs/pika_flex_ref2va_nvfp4_qad
training_state_checkpointing_steps: 25
checkpoints_total_limit: 2
resume_from_checkpoint: ''
tracker:
trackers: [wandb]
project_name: pika-h3-flex-nvfp4
run_name: pika-flex-ref2va-nvfp4-qad
model:
enable_gradient_checkpointing_type: full
dit_precision: fp32
vsa_sparsity: 0.0
callbacks:
grad_clip:
_target_: fastvideo.train.callbacks.grad_clip.GradNormClipCallback
max_grad_norm: 1.0
pipeline: {}
@@ -5,7 +5,7 @@ Benchmark the SM100 fused VSA64 tile layout against the legacy scatter/transpose
Times Wan VSA inference preprocessing through the backend
(``VideoSparseAttentionImpl.preprocess_qkv``), toggling
``FASTVIDEO_DISABLE_VSA64_FUSED_LAYOUT``:
- legacy: tile scatter into BSHD + four BSHD->BHSD transposes
- legacy: tile scatter into BSHD + three contiguous Q/K/V transposes + a gate view
- fused: one Triton kernel writing tiled BHSD directly
With ``--include_attn`` the timed region also covers ``forward`` (the
@@ -59,20 +59,20 @@ def parse_arguments() -> argparse.Namespace:
def make_step(impl: VideoSparseAttentionImpl, qkvg: torch.Tensor, builder: VideoSparseAttentionMetadataBuilder,
build_kwargs: dict, fused: bool, include_attn: bool) -> Callable[[], torch.Tensor]:
build_kwargs: dict, fused: bool, include_attn: bool) -> Callable[[], tuple[torch.Tensor, ...]]:
metadata = builder.build(**build_kwargs)
def step() -> torch.Tensor:
def step() -> tuple[torch.Tensor, ...]:
# The routing gate reads the switch on every call.
with envs.FASTVIDEO_DISABLE_VSA64_FUSED_LAYOUT.override(not fused):
tiled = impl.preprocess_qkv(qkvg, metadata)
q, k, v, gate = tiled.chunk(4)
if include_attn:
return impl.forward(q, k, v, gate, metadata)
return (impl.forward(q, k, v, gate, metadata), )
if metadata.fused_layout_active:
return tiled
# Legacy route: the transposes forward() applies before video_sparse_attn.
return torch.cat([t.transpose(1, 2).contiguous() for t in (q, k, v, gate)])
return q, k, v, gate
return (q.transpose(1, 2).contiguous(), k.transpose(1, 2).contiguous(), v.transpose(1, 2).contiguous(),
gate.transpose(1, 2))
with torch.no_grad():
step()
@@ -100,7 +100,7 @@ def main() -> None:
legacy = make_step(impl, qkvg, builder, build_kwargs, fused=False, include_attn=args.include_attn)
fused = make_step(impl, qkvg, builder, build_kwargs, fused=True, include_attn=args.include_attn)
with torch.no_grad():
if not torch.equal(legacy(), fused()):
if not all(torch.equal(old, new) for old, new in zip(legacy(), fused(), strict=True)):
raise RuntimeError("Fused and legacy outputs differ.")
legacy_ms = do_bench(legacy, warmup=args.warmup, rep=args.rep, return_mode="median")
fused_ms = do_bench(fused, warmup=args.warmup, rep=args.rep, return_mode="median")
@@ -32,6 +32,7 @@ from __future__ import annotations
import math
import torch
import torch.nn.functional as F
QUANT_GROUP = 16
BLOCK = 128
@@ -99,30 +100,73 @@ def key_valid_mask(kv_valid: torch.Tensor | None, n_blocks: int, device: torch.d
return (col % HALF)[None, :] < limit
def tile128_mask_to_fp4_blocks(tile_mask: torch.Tensor,
tile_valid: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""``attn_qat_infer.api.vsa_tile_mask_to_fp4_blocks(mask, 128, tile_valid)`` without the CUDA extensions.
def tile_mask_to_fp4_blocks(tile_mask: torch.Tensor,
tile_tokens: int,
tile_valid: torch.Tensor | None = None) -> tuple[
torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor | None]:
"""Pure-PyTorch ``attn_qat_infer.api.vsa_tile_mask_to_fp4_blocks`` for tile 64 or 128.
``attn_qat_infer`` imports its sm_12x extensions at module load, so data
center GPUs cannot call it; this is the same computation for 128-token
tiles (one tile per kernel block, so no quadrant masks and no padding
block): descending lists and per-64-key-half valid counts. The fidelity
check script asserts equality with the real function on sm_12x.
The inference API imports sm_12x extensions at module load, so data center
GPUs cannot call it. The kernel works on 128x128 blocks. With 64-token VSA
tiles two query tiles and two key tiles are paired into one block; a block
is listed when any of its four 64x64 quadrants is selected and ``q2k_quad``
carries the selected quadrants (bit ``2 * query_half + key_half``). With
128-token tiles the mapping is 1:1 and no quadrant mask is needed.
Returns ``(q2k_idx [B, H, M, N], q2k_num [B, H, M], kv_valid [2N],
q2k_quad [B, H, M, N] | None)``; queries cover ``M * 128`` padded rows.
"""
n_tiles = tile_mask.shape[-1]
half_valid = (tile_valid.to(torch.int32)[:, None] -
torch.tensor([0, HALF], device=tile_mask.device, dtype=torch.int32)[None, :]).clamp(0, HALF)
block_mask = tile_mask & (tile_valid > 0)[None, None, None, :]
valid = half_valid.reshape(-1)
n_blocks = n_tiles
if tile_tokens % HALF or (tile_tokens > HALF and tile_tokens % BLOCK):
raise ValueError(f"tile_tokens={tile_tokens} must be {HALF} or a multiple of {BLOCK}")
batch, heads, n_tiles, _ = tile_mask.shape
device = tile_mask.device
halves_per_tile = tile_tokens // HALF
half_mask = tile_mask
if halves_per_tile > 1:
half_mask = half_mask.repeat_interleave(halves_per_tile, dim=2).repeat_interleave(halves_per_tile, dim=3)
n_halves = n_tiles * halves_per_tile
if tile_valid is None:
half_valid = torch.full((n_halves, ), HALF, device=device, dtype=torch.int32)
else:
offsets = torch.arange(halves_per_tile, device=device, dtype=torch.int32) * HALF
half_valid = (tile_valid.to(torch.int32)[:, None] - offsets[None, :]).clamp(0, HALF).reshape(-1)
if n_halves % 2:
# Pad to whole 128-blocks: the extra key half is empty, the extra query
# half (discarded output) attends block 0 only.
half_mask = F.pad(half_mask, (0, 1, 0, 1), value=False)
half_mask[:, :, -1, 0] = True
half_valid = F.pad(half_valid, (0, 1), value=0)
n_halves += 1
half_mask = half_mask & (half_valid > 0)[None, None, None, :]
n_blocks = n_halves // 2
quads = half_mask.view(batch, heads, n_blocks, 2, n_blocks, 2)
weights = torch.tensor([[1, 2], [4, 8]], device=device, dtype=torch.uint8) # [row_half, col_half]
quad = (quads.to(torch.uint8) * weights[None, None, None, :, None, :]).sum(dim=(3, 5), dtype=torch.uint8)
block_mask = quad != 0
# Descending compaction without a sort: position = running count from the right.
rev = block_mask.flip(-1)
pos = rev.cumsum(-1, dtype=torch.int32) - 1
q2k_num = (pos[..., -1] + 1).contiguous()
cols = torch.arange(n_blocks - 1, -1, -1, device=tile_mask.device, dtype=torch.int32).expand_as(pos)
cols = torch.arange(n_blocks - 1, -1, -1, device=device, dtype=torch.int32).expand_as(pos)
slot = torch.where(rev, pos, torch.full_like(pos, n_blocks)).long()
q2k_idx = torch.zeros((*tile_mask.shape[:2], n_blocks, n_blocks + 1), device=tile_mask.device, dtype=torch.int32)
q2k_idx = torch.zeros((batch, heads, n_blocks, n_blocks + 1), device=device, dtype=torch.int32)
q2k_idx.scatter_(-1, slot, cols)
return q2k_idx[..., :n_blocks].contiguous(), q2k_num, valid.contiguous()
q2k_idx = q2k_idx[..., :n_blocks].contiguous()
q2k_quad = None
if halves_per_tile == 1:
q2k_quad = torch.zeros((batch, heads, n_blocks, n_blocks + 1), device=device, dtype=torch.uint8)
q2k_quad.scatter_(-1, slot, quad.flip(-1))
q2k_quad = q2k_quad[..., :n_blocks].contiguous()
return q2k_idx, q2k_num, half_valid.contiguous(), q2k_quad
def tile128_mask_to_fp4_blocks(tile_mask: torch.Tensor,
tile_valid: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""Backward-compatible 128-token-tile wrapper."""
idx, num, valid, quad = tile_mask_to_fp4_blocks(tile_mask, BLOCK, tile_valid)
if quad is not None:
raise AssertionError("128-token tiles must not produce a quadrant mask")
return idx, num, valid
def invert_block_lists(q2k_idx: torch.Tensor, q2k_num: torch.Tensor,
@@ -162,6 +206,7 @@ def fp4_vsa_attention_reference(q: torch.Tensor,
q2k_idx: torch.Tensor,
q2k_num: torch.Tensor,
kv_valid: torch.Tensor | None = None,
q2k_quad: torch.Tensor | None = None,
*,
two_level_p: bool = False,
smooth_k: bool = False,
@@ -199,17 +244,31 @@ def fp4_vsa_attention_reference(q: torch.Tensor,
row_sum = torch.zeros(BLOCK, device=q.device)
acc = torch.zeros(BLOCK, dim, device=q.device)
visited: list[tuple[int, torch.Tensor, torch.Tensor]] = []
for position, n in enumerate(visit_order(q2k_idx, q2k_num, b, h, m)):
count = int(q2k_num[b, h, m])
entries = list(range(count))[::-1]
for position, entry in enumerate(entries):
n = int(q2k_idx[b, h, m, entry])
cols = slice(n * BLOCK, (n + 1) * BLOCK)
scores = q_hat[b, h, rows] @ k_hat[b, h, cols].T
scores = scores.masked_fill(~valid[n][None, :], -math.inf)
if q2k_quad is not None:
quad = int(q2k_quad[b, h, m, entry])
half = torch.arange(BLOCK, device=q.device) // HALF
qvalid = ((quad >> (half[:, None] * 2 + half[None, :])) & 1).bool()
scores = scores.masked_fill(~qvalid, -math.inf)
group_max = scores.unflatten(-1, (-1, QUANT_GROUP)).amax(dim=-1)
if position == 0 and first_block_max_floor is not None:
group_max = group_max.clamp(min=first_block_max_floor)
new_max = torch.maximum(row_max, group_max.amax(dim=-1))
m_scaled = new_max * scale_log2 + offset
p_tilde = torch.exp2(scores * scale_log2 - m_scaled[:, None])
alpha = torch.exp2((row_max - new_max) * scale_log2)
# A listed block can have no selected keys for this query
# half; both maxima are -inf and the usual exp2 terms are
# NaN, so emit zeros and keep the running state unchanged.
empty = torch.isinf(new_max) & (new_max < 0)
m_scaled = torch.where(empty, torch.zeros_like(new_max), new_max * scale_log2 + offset)
p_tilde = torch.where(empty[:, None], torch.zeros_like(scores),
torch.exp2(scores * scale_log2 - m_scaled[:, None]))
alpha = torch.where(empty, torch.zeros_like(new_max),
torch.exp2((row_max - new_max) * scale_log2))
alpha = torch.where(torch.isinf(row_max), torch.zeros_like(alpha), alpha)
row_sum = row_sum * alpha + p_tilde.sum(dim=-1)
if quantize:
@@ -226,7 +285,9 @@ def fp4_vsa_attention_reference(q: torch.Tensor,
out[b, h, rows] = acc * inv[:, None]
if weights is not None:
for n, p_hat, block_max in visited:
decay = torch.exp2((block_max - row_max) * scale_log2) * inv
finite = torch.isfinite(block_max) & torch.isfinite(row_max)
decay = torch.where(finite, torch.exp2((block_max - row_max) * scale_log2) * inv,
torch.zeros_like(inv))
weights[b, h, rows, n * BLOCK:(n + 1) * BLOCK] = p_hat * decay[:, None]
if weights is not None:
return out, weights
@@ -141,9 +141,9 @@ def _key_ok(KV_VALID, n_block, cols, HAS_VALID: tl.constexpr, HALF: tl.constexpr
@triton.jit
def _fwd_kernel(Q, K, V, O, OHP, LSE, MRUN, Q2K_IDX, Q2K_NUM, KV_VALID, BIAS, QGROUP, n_groups, seq_len, n_qblocks,
list_width, scale_log2, OFFSET: tl.constexpr, QUANT: tl.constexpr, HAS_VALID: tl.constexpr,
HAS_BIAS: tl.constexpr, FLOOR: tl.constexpr, FLOOR_VALUE: tl.constexpr, HIGH_PREC_O: tl.constexpr,
def _fwd_kernel(Q, K, V, O, OHP, LSE, MRUN, Q2K_IDX, Q2K_NUM, KV_VALID, Q2K_QUAD, BIAS, QGROUP, n_groups, seq_len,
n_qblocks, list_width, scale_log2, OFFSET: tl.constexpr, QUANT: tl.constexpr, HAS_VALID: tl.constexpr,
HAS_QUAD: tl.constexpr, HAS_BIAS: tl.constexpr, FLOOR: tl.constexpr, FLOOR_VALUE: tl.constexpr, HIGH_PREC_O: tl.constexpr,
SAVE_MRUN: tl.constexpr,
D: tl.constexpr, ROWS: tl.constexpr, BLOCK_N: tl.constexpr):
pid = tl.program_id(0)
@@ -174,18 +174,28 @@ def _fwd_kernel(Q, K, V, O, OHP, LSE, MRUN, Q2K_IDX, Q2K_NUM, KV_VALID, BIAS, QG
if HAS_BIAS: # per-key score correction (Q smoothing: mu_q . k_j, high precision)
s = s + tl.load(bias_row + keys)[None, :]
ok = _key_ok(KV_VALID, n_block, offs_n, HAS_VALID, BLOCK_N // 2)
if HAS_QUAD:
quad = tl.load(Q2K_QUAD + meta * list_width + slot).to(tl.int32)
qh = pid % halves
bit = tl.where(offs_n < ROWS, (quad >> (qh * 2)) & 1, (quad >> (qh * 2 + 1)) & 1)
ok = ok & (bit != 0)
s = tl.where(ok[None, :], s, float("-inf"))
group_max = tl.max(tl.reshape(s, (ROWS, BLOCK_N // 16, 16)), axis=2)
if FLOOR:
group_max = tl.where(it == 0, tl.maximum(group_max, FLOOR_VALUE), group_max)
m_new = tl.maximum(m_i, tl.max(group_max, axis=1))
# Tile 64 pairs two query halves per block, so a listed block can have no selected keys for this half:
# both maxima are -inf and exp2(-inf - -inf) is NaN. Keep the running state and emit zero probabilities.
empty = m_new == float("-inf")
m_scaled = m_new * scale_log2 + OFFSET
alpha = tl.math.exp2((m_i - m_new) * scale_log2)
alpha = tl.where(empty, 0.0, tl.math.exp2((m_i - m_new) * scale_log2))
if QUANT:
p_tilde, p_hat = _quantized_p(s, m_scaled, it == 0, scale_log2, FLOOR, FLOOR_VALUE, ROWS, BLOCK_N)
else:
p_tilde = tl.math.exp2(s * scale_log2 - m_scaled[:, None])
p_hat = p_tilde
p_tilde = tl.where(empty[:, None], 0.0, p_tilde)
p_hat = tl.where(empty[:, None], 0.0, p_hat)
l_i = l_i * alpha + tl.sum(p_tilde, axis=1)
v = tl.load(V + base + keys[:, None] * D + offs_d[None, :])
acc = acc * alpha[:, None] + tl.dot(p_hat.to(tl.bfloat16), v)
@@ -202,9 +212,9 @@ def _fwd_kernel(Q, K, V, O, OHP, LSE, MRUN, Q2K_IDX, Q2K_NUM, KV_VALID, BIAS, QG
@triton.jit
def _bwd_dq_kernel(Q, K, V, DO, LSE, DELTA, DQ, Q2K_IDX, Q2K_NUM, KV_VALID, seq_len, n_qblocks, list_width,
scale_log2, sm_scale, HAS_VALID: tl.constexpr, D: tl.constexpr, ROWS: tl.constexpr,
BLOCK_N: tl.constexpr):
def _bwd_dq_kernel(Q, K, V, DO, LSE, DELTA, DQ, Q2K_IDX, Q2K_NUM, KV_VALID, Q2K_QUAD, seq_len, n_qblocks,
list_width, scale_log2, sm_scale, HAS_VALID: tl.constexpr, HAS_QUAD: tl.constexpr,
D: tl.constexpr, ROWS: tl.constexpr, BLOCK_N: tl.constexpr):
pid = tl.program_id(0)
bh = tl.program_id(1).to(tl.int64)
halves: tl.constexpr = BLOCK_N // ROWS
@@ -229,7 +239,13 @@ def _bwd_dq_kernel(Q, K, V, DO, LSE, DELTA, DQ, Q2K_IDX, Q2K_NUM, KV_VALID, seq_
v = tl.load(V + base + keys[:, None] * D + offs_d[None, :])
s = tl.dot(q, tl.trans(k))
ok = _key_ok(KV_VALID, n_block, cols, HAS_VALID, BLOCK_N // 2)
p = tl.where(ok[None, :], tl.math.exp2(s * scale_log2 - lse[:, None]), 0.0)
if HAS_QUAD:
quad = tl.load(Q2K_QUAD + meta * list_width + (it // halves)).to(tl.int32)
bit = (quad >> ((pid % halves) * 2 + (it % halves))) & 1
ok = ok & (bit != 0)
# A row with no valid keys has lse = -inf; clamp it so it contributes zero instead of NaN.
lse_safe = tl.where(lse == float("-inf"), 0.0, lse)
p = tl.where(ok[None, :], tl.math.exp2(s * scale_log2 - lse_safe[:, None]), 0.0)
dp = tl.dot(do, tl.trans(v))
ds = p * (dp - delta[:, None])
dq += tl.dot(ds.to(tl.bfloat16), k)
@@ -237,10 +253,11 @@ def _bwd_dq_kernel(Q, K, V, DO, LSE, DELTA, DQ, Q2K_IDX, Q2K_NUM, KV_VALID, seq_
@triton.jit
def _bwd_dkdv_kernel(Q, K, V, DO, LSE, DELTA, MRUN, DK, DV, K2Q_IDX, K2Q_SLOT, K2Q_NUM, Q2K_NUM, KV_VALID, seq_len,
n_qblocks, n_kblocks, list_width, scale_log2, sm_scale, OFFSET: tl.constexpr,
QUANT: tl.constexpr, HAS_VALID: tl.constexpr, FLOOR: tl.constexpr,
FLOOR_VALUE: tl.constexpr, D: tl.constexpr, ROWS: tl.constexpr, BLOCK_N: tl.constexpr):
def _bwd_dkdv_kernel(Q, K, V, DO, LSE, DELTA, MRUN, DK, DV, K2Q_IDX, K2Q_SLOT, K2Q_NUM, Q2K_NUM, KV_VALID,
Q2K_QUAD, seq_len, n_qblocks, n_kblocks, list_width, scale_log2, sm_scale,
OFFSET: tl.constexpr, QUANT: tl.constexpr, HAS_VALID: tl.constexpr, HAS_QUAD: tl.constexpr,
FLOOR: tl.constexpr, FLOOR_VALUE: tl.constexpr, D: tl.constexpr, ROWS: tl.constexpr,
BLOCK_N: tl.constexpr):
pid = tl.program_id(0)
bh = tl.program_id(1).to(tl.int64)
halves: tl.constexpr = BLOCK_N // ROWS
@@ -268,15 +285,22 @@ def _bwd_dkdv_kernel(Q, K, V, DO, LSE, DELTA, MRUN, DK, DV, K2Q_IDX, K2Q_SLOT, K
lse = tl.load(LSE + bh * seq_len + rows)
delta = tl.load(DELTA + bh * seq_len + rows)
s = tl.dot(q, tl.trans(k))
s = tl.where(ok[None, :], s, float("-inf"))
p = tl.math.exp2(s * scale_log2 - lse[:, None])
if HAS_QUAD:
quad = tl.load(Q2K_QUAD + (bh * n_qblocks + m_block) * list_width + slot).to(tl.int32)
bit = (quad >> ((it % halves) * 2 + (pid % halves))) & 1
s = tl.where(ok[None, :] & (bit != 0), s, float("-inf"))
else:
s = tl.where(ok[None, :], s, float("-inf"))
lse_safe = tl.where(lse == float("-inf"), 0.0, lse)
p = tl.math.exp2(s * scale_log2 - lse_safe[:, None])
if QUANT:
qmeta = bh * n_qblocks + m_block
first_visit = slot == tl.load(Q2K_NUM + qmeta) - 1
m_run = tl.load(MRUN + (qmeta * list_width + slot) * BLOCK_N + row0 + offs_r)
m_scaled = m_run * scale_log2 + OFFSET
m_ok = m_run != float("-inf")
m_scaled = tl.where(m_ok, m_run * scale_log2 + OFFSET, 0.0)
_, p_hat = _quantized_p(s, m_scaled, first_visit, scale_log2, FLOOR, FLOOR_VALUE, ROWS, ROWS)
weight = p_hat * tl.math.exp2(m_scaled - lse)[:, None]
weight = tl.where(m_ok[:, None], p_hat * tl.math.exp2(m_scaled - lse_safe)[:, None], 0.0)
else:
weight = p
dv += tl.dot(tl.trans(weight.to(tl.bfloat16)), do)
@@ -290,8 +314,8 @@ def _bwd_dkdv_kernel(Q, K, V, DO, LSE, DELTA, MRUN, DK, DV, K2Q_IDX, K2Q_SLOT, K
class _FP4VSAAttnQAT(torch.autograd.Function):
@staticmethod
def forward(ctx, q, k, v, q2k_idx, q2k_num, kv_valid, quantize, quantize_pv, two_level_p, first_block_max_floor,
sm_scale, high_prec_o, key_bias=None, q_group=None, need_backward=False): # type: ignore[override]
def forward(ctx, q, k, v, q2k_idx, q2k_num, kv_valid, q2k_quad, quantize, quantize_pv, two_level_p,
first_block_max_floor, sm_scale, high_prec_o, key_bias=None, q_group=None, need_backward=False): # type: ignore[override]
batch, heads, seq_len, dim = q.shape
if seq_len % BLOCK or dim not in (64, 128):
raise ValueError(f"need L % {BLOCK} == 0 and D in (64, 128), got L={seq_len}, D={dim}")
@@ -306,6 +330,8 @@ class _FP4VSAAttnQAT(torch.autograd.Function):
q2k_num = q2k_num.to(torch.int32).contiguous()
has_valid = kv_valid is not None
kv_valid_arg = kv_valid.to(torch.int32).contiguous() if has_valid else q2k_num
has_quad = q2k_quad is not None
q2k_quad_arg = q2k_quad.to(torch.uint8).contiguous() if has_quad else q2k_idx
width = q2k_idx.shape[-1]
out = torch.empty_like(q_hat)
out_hp = torch.empty_like(q_hat) if high_prec_o else out
@@ -326,15 +352,17 @@ class _FP4VSAAttnQAT(torch.autograd.Function):
q_group = (q_group if q_group is not None else torch.zeros(n_blocks, dtype=torch.int32,
device=q.device)).to(torch.int32).contiguous()
grid = (n_blocks * (BLOCK // _ROWS), batch * heads)
_fwd_kernel[grid](q_hat, k_hat, v_hat, out, out_hp, lse, m_run, q2k_idx, q2k_num, kv_valid_arg,
_fwd_kernel[grid](q_hat, k_hat, v_hat, out, out_hp, lse, m_run, q2k_idx, q2k_num, kv_valid_arg, q2k_quad_arg,
key_bias if has_bias else lse, q_group if has_bias else q2k_num,
key_bias.shape[2] if has_bias else 1, seq_len, n_blocks, width, scale_log2, OFFSET=offset,
QUANT=bool(quantize), HAS_VALID=has_valid, HAS_BIAS=has_bias, FLOOR=floor, FLOOR_VALUE=float(first_block_max_floor or 0.0),
QUANT=bool(quantize), HAS_VALID=has_valid, HAS_QUAD=has_quad, HAS_BIAS=has_bias, FLOOR=floor, FLOOR_VALUE=float(first_block_max_floor or 0.0),
HIGH_PREC_O=bool(high_prec_o), SAVE_MRUN=save_mrun, D=dim, ROWS=_ROWS, BLOCK_N=BLOCK, num_warps=_WARPS,
num_stages=2)
if need_backward:
ctx.save_for_backward(q_hat, k_hat, v_hat, out_hp, lse, m_run, q2k_idx, q2k_num, kv_valid_arg)
ctx.options = (bool(quantize), has_valid, floor, float(first_block_max_floor or 0.0), offset, sm_scale)
ctx.save_for_backward(q_hat, k_hat, v_hat, out_hp, lse, m_run, q2k_idx, q2k_num, kv_valid_arg,
q2k_quad_arg)
ctx.options = (bool(quantize), has_valid, has_quad, floor, float(first_block_max_floor or 0.0), offset,
sm_scale)
ctx.has_bias = has_bias
return out
@@ -342,8 +370,8 @@ class _FP4VSAAttnQAT(torch.autograd.Function):
def backward(ctx, grad_out): # type: ignore[override]
if ctx.has_bias:
raise NotImplementedError("the Q-smoothing score bias is an eval-only emulator option (no backward)")
q_hat, k_hat, v_hat, out_hp, lse, m_run, q2k_idx, q2k_num, kv_valid = ctx.saved_tensors
quantize, has_valid, floor, floor_value, offset, sm_scale = ctx.options
q_hat, k_hat, v_hat, out_hp, lse, m_run, q2k_idx, q2k_num, kv_valid, q2k_quad = ctx.saved_tensors
quantize, has_valid, has_quad, floor, floor_value, offset, sm_scale = ctx.options
batch, heads, seq_len, dim = q_hat.shape
n_blocks = seq_len // BLOCK
width = q2k_idx.shape[-1]
@@ -355,15 +383,15 @@ class _FP4VSAAttnQAT(torch.autograd.Function):
dv = torch.empty_like(v_hat)
scale_log2 = sm_scale * LOG2E
grid = (n_blocks * (BLOCK // _ROWS), batch * heads)
_bwd_dq_kernel[grid](q_hat, k_hat, v_hat, grad_out, lse, delta, dq, q2k_idx, q2k_num, kv_valid, seq_len,
n_blocks, width, scale_log2, sm_scale, HAS_VALID=has_valid, D=dim, ROWS=_ROWS,
BLOCK_N=BLOCK, num_warps=_WARPS, num_stages=2)
_bwd_dq_kernel[grid](q_hat, k_hat, v_hat, grad_out, lse, delta, dq, q2k_idx, q2k_num, kv_valid, q2k_quad,
seq_len, n_blocks, width, scale_log2, sm_scale, HAS_VALID=has_valid,
HAS_QUAD=has_quad, D=dim, ROWS=_ROWS, BLOCK_N=BLOCK, num_warps=_WARPS, num_stages=2)
_bwd_dkdv_kernel[grid](q_hat, k_hat, v_hat, grad_out, lse, delta, m_run, dk, dv, k2q_idx, k2q_slot, k2q_num,
q2k_num, kv_valid, seq_len, n_blocks, n_blocks, width, scale_log2, sm_scale,
OFFSET=offset, QUANT=quantize, HAS_VALID=has_valid, FLOOR=floor,
q2k_num, kv_valid, q2k_quad, seq_len, n_blocks, n_blocks, width, scale_log2, sm_scale,
OFFSET=offset, QUANT=quantize, HAS_VALID=has_valid, HAS_QUAD=has_quad, FLOOR=floor,
FLOOR_VALUE=floor_value, D=dim, ROWS=_ROWS, BLOCK_N=BLOCK, num_warps=_WARPS,
num_stages=2)
return dq, dk, dv, None, None, None, None, None, None, None, None, None, None, None, None
return dq, dk, dv, None, None, None, None, None, None, None, None, None, None, None, None, None
def _hadamard(dim: int, device: torch.device) -> torch.Tensor:
@@ -426,6 +454,7 @@ def fp4_vsa_attn_qat(q: torch.Tensor,
q2k_idx: torch.Tensor,
q2k_num: torch.Tensor,
kv_valid: torch.Tensor | None = None,
q2k_quad: torch.Tensor | None = None,
*,
quantize: bool = True,
quantize_pv: bool = True,
@@ -442,7 +471,8 @@ def fp4_vsa_attn_qat(q: torch.Tensor,
``q``/``k``/``v`` are ``[B, H, L, D]`` (L a multiple of 128, tile-ordered,
pad rows zero); the lists are ``vsa_tile_mask_to_fp4_blocks(mask, 128, vbs)``
outputs (``q2k_quad`` must be None: 128-token tiles map 1:1 onto blocks).
outputs (``q2k_quad`` is None for 128-token tiles, which map 1:1 onto
blocks, and carries the four 64x64 quadrant bits for 64-token tiles).
``smooth_k`` subtracts each head's token mean from K first (exact for
softmax, differentiable through autograd). ``quantize_pv=False`` quantizes
Q/K only and keeps P and V in BF16 (the FA4 NVFP4-QK + BF16-PV kernel).
@@ -483,7 +513,7 @@ def fp4_vsa_attn_qat(q: torch.Tensor,
# Capture the caller's grad mode here, including no_grad on grad-enabled inputs.
need_backward = torch.is_grad_enabled() and any(t.requires_grad for t in (q, k, v))
if not (hadamard or smooth_q or prefix_precision):
return _FP4VSAAttnQAT.apply(q, k, v, q2k_idx, q2k_num, kv_valid, quantize, quantize_pv, two_level_p,
return _FP4VSAAttnQAT.apply(q, k, v, q2k_idx, q2k_num, kv_valid, q2k_quad, quantize, quantize_pv, two_level_p,
first_block_max_floor, scale, high_prec_o, None, None, need_backward)
n_blocks = q.shape[2] // BLOCK
hp_tiles = min(int(num_prefix_tiles or 0), n_blocks) if prefix_precision else 0
@@ -500,14 +530,14 @@ def fp4_vsa_attn_qat(q: torch.Tensor,
n_groups = 2 if smooth_q == "segment" else 1
groups = _query_groups(n_blocks, num_prefix_tiles, smooth_q == "segment", q.device)
q_lo, bias = _smooth_queries(q_lo, k_lo, groups, n_groups)
out = _FP4VSAAttnQAT.apply(q_lo, k_lo, v, q2k_idx, num_fp4, kv_valid, quantize, quantize_pv, two_level_p,
out = _FP4VSAAttnQAT.apply(q_lo, k_lo, v, q2k_idx, num_fp4, kv_valid, q2k_quad, quantize, quantize_pv, two_level_p,
first_block_max_floor, scale, high_prec_o, bias, groups, need_backward)
if not hp_tiles:
return out
num_hp = q2k_num - num_fp4
hp_quant = {"bf16": lambda t: t, "fp8": fp8_per_token_fake_quant, "mxfp8": mxfp8_fake_quant}[prefix_precision]
q_hp, k_hp = hp_quant(q), hp_quant(k)
out_hp = _FP4VSAAttnQAT.apply(q_hp, k_hp, v, q2k_idx, num_hp, kv_valid, False, True, False, None, scale,
out_hp = _FP4VSAAttnQAT.apply(q_hp, k_hp, v, q2k_idx, num_hp, kv_valid, q2k_quad, False, True, False, None, scale,
high_prec_o, None, None, need_backward)
return out + out_hp
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
"""Block-sparse FP4 Attn-QAT (VSA tile 128): the PyTorch specification and the Triton kernel.
"""Block-sparse FP4 Attn-QAT (VSA tiles 64 and 128): the PyTorch specification and the Triton kernel.
CPU tests pin the reference emulator (E2M1/E4M3 rounding, list builder, online
softmax, dV weights). GPU tests (any CUDA GPU with Triton; run on GB200)
@@ -255,7 +255,7 @@ def test_triton_key_bias_matches_reference():
"""The eval-only per-key score bias (Q-smoothing correction) enters the scores before the softmax."""
q, k, v, q2k_idx, q2k_num, kv_valid, _, _ = _problem(5, heads=2, seed=11, device="cuda")
bias = 3.0 * torch.randn(1, 2, 1, q.shape[2], device="cuda")
out = _triton()._FP4VSAAttnQAT.apply(q, k, v, q2k_idx, q2k_num, kv_valid, False, True, False, None,
out = _triton()._FP4VSAAttnQAT.apply(q, k, v, q2k_idx, q2k_num, kv_valid, None, False, True, False, None,
1 / math.sqrt(128), True, bias, None)
token_mask = ref.dense_block_mask(q2k_idx, q2k_num, q.shape[2], kv_valid)
assert _rel(out, _bias_reference(q, k, v, token_mask, bias[:, :, 0])) < 5e-3
@@ -301,3 +301,154 @@ def test_triton_qk_variants_reduce_to_known_modes():
assert _rel(smoothed, exact) < 0.5
with pytest.raises(ValueError):
fn(q, k, v, q2k_idx, q2k_num, kv_valid, hadamard=True) # P.V quantized: not a Q/K-only mode
def _problem64(n_tiles: int, heads: int = 2, keep: float = 0.4, seed: int = 0, device: str = "cpu"):
"""A 64-token VSA problem: two tiles share one 128-token kernel block."""
gen = torch.Generator().manual_seed(seed)
tile_valid = torch.full((n_tiles, ), 64, dtype=torch.int32)
if n_tiles > 2:
tile_valid[-1] = 21
tile_valid[n_tiles // 2] = 40
tile_mask = torch.rand(1, heads, n_tiles, n_tiles, generator=gen) < keep
tile_mask[..., 0] = True
q2k_idx, q2k_num, kv_valid, q2k_quad = ref.tile_mask_to_fp4_blocks(tile_mask, 64, tile_valid)
n_blocks = q2k_idx.shape[-1]
seq_len = n_blocks * ref.BLOCK
token_valid = torch.zeros(seq_len, dtype=torch.bool)
for tile in range(n_tiles):
token_valid[tile * 64:(tile + 1) * 64] = torch.arange(64) < tile_valid[tile]
q, k, v = (torch.randn(1, heads, seq_len, 128, generator=gen) * token_valid[None, None, :, None]
for _ in range(3))
k = k + 0.5 * torch.randn(1, heads, 1, 128, generator=gen) * token_valid[None, None, :, None]
move = lambda t: t.to(device) # noqa: E731
return (move(q.bfloat16()), move(k.bfloat16()), move(v.bfloat16()), move(q2k_idx), move(q2k_num),
move(kv_valid), move(q2k_quad), move(tile_mask), move(tile_valid))
def _dense_quad_mask(q2k_idx: torch.Tensor, q2k_num: torch.Tensor, q2k_quad: torch.Tensor, seq_len: int) -> torch.Tensor:
"""``[B, H, S, S]`` token mask: listed blocks intersected with their selected 64x64 quadrants."""
batch, heads, n_blocks, _ = q2k_idx.shape
mask = torch.zeros(batch, heads, seq_len, seq_len, dtype=torch.bool, device=q2k_idx.device)
half = torch.arange(ref.BLOCK, device=q2k_idx.device) // ref.HALF
for b in range(batch):
for h in range(heads):
for m in range(n_blocks):
for slot in range(int(q2k_num[b, h, m])):
n = int(q2k_idx[b, h, m, slot])
quad = int(q2k_quad[b, h, m, slot])
qvalid = ((quad >> (half[:, None] * 2 + half[None, :])) & 1).bool()
mask[b, h, m * ref.BLOCK:(m + 1) * ref.BLOCK, n * ref.BLOCK:(n + 1) * ref.BLOCK] = qvalid
return mask
def test_tile64_lists_a_block_for_any_selected_quadrant():
_, _, _, q2k_idx, q2k_num, kv_valid, q2k_quad, tile_mask, tile_valid = _problem64(6, heads=2, seed=2)
n_blocks = q2k_idx.shape[-1]
half_mask = tile_mask & (tile_valid > 0)[None, None, None, :]
for h in range(2):
for m in range(n_blocks):
listed = q2k_idx[0, h, m, :int(q2k_num[0, h, m])].tolist()
assert listed == sorted(listed, reverse=True)
expected = torch.nonzero(half_mask[0, h, 2 * m:2 * m + 2, :].any(dim=0).view(-1, 2).any(dim=1)).flatten()
assert listed == sorted(expected.tolist(), reverse=True)
assert kv_valid.numel() == 2 * n_blocks
def test_tile64_quadrant_bits_match_the_half_tile_mask():
_, _, _, q2k_idx, q2k_num, _, q2k_quad, tile_mask, tile_valid = _problem64(6, heads=2, seed=3)
half_mask = tile_mask & (tile_valid > 0)[None, None, None, :]
weights = torch.tensor([[1, 2], [4, 8]])
for h in range(2):
for m in range(q2k_idx.shape[2]):
for slot in range(int(q2k_num[0, h, m])):
n = int(q2k_idx[0, h, m, slot])
quads = half_mask[0, h, 2 * m:2 * m + 2, 2 * n:2 * n + 2]
expected = int((quads.to(torch.int32) * weights).sum())
assert int(q2k_quad[0, h, m, slot]) == expected
def test_tile128_route_is_the_general_route_without_quadrants():
gen = torch.Generator().manual_seed(4)
tile_mask = torch.rand(1, 2, 5, 5, generator=gen) < 0.5
tile_valid = torch.tensor([128, 128, 77, 0, 128], dtype=torch.int32)
q2k_idx, q2k_num, kv_valid, q2k_quad = ref.tile_mask_to_fp4_blocks(tile_mask, 128, tile_valid)
old_idx, old_num, old_valid = ref.tile128_mask_to_fp4_blocks(tile_mask, tile_valid)
assert q2k_quad is None
assert torch.equal(q2k_idx, old_idx) and torch.equal(q2k_num, old_num) and torch.equal(kv_valid, old_valid)
def test_tile64_first_listed_block_can_be_empty_for_one_half():
"""Regression: an empty half in the first visited block must not poison later blocks."""
n_tiles = 4
gen = torch.Generator().manual_seed(7)
seq_len = (n_tiles // 2) * ref.BLOCK
q, k, v = (torch.randn(1, 1, seq_len, 128, generator=gen).bfloat16() for _ in range(3))
tile_mask = torch.zeros(1, 1, n_tiles, n_tiles, dtype=torch.bool)
tile_mask[0, 0, 1, 2] = True # query tile 1 lists block 1 (empty for query tile 0)
tile_mask[0, 0, 0, 0] = True # query tile 0 lists block 0 (valid)
tile_mask[0, 0, 2, 2] = True
tile_mask[0, 0, 3, 3] = True
tile_valid = torch.full((n_tiles, ), 64, dtype=torch.int32)
q2k_idx, q2k_num, kv_valid, q2k_quad = ref.tile_mask_to_fp4_blocks(tile_mask, 64, tile_valid)
assert int(q2k_num[0, 0, 0]) == 2 and int(q2k_idx[0, 0, 0, 0]) == 1
assert int(q2k_quad[0, 0, 0, 0]) & 3 == 0 # first slot is block 1, empty for query half 0
out = ref.fp4_vsa_attention_reference(q, k, v, q2k_idx, q2k_num, kv_valid, q2k_quad, quantize=False)
assert torch.isfinite(out).all()
mask = _dense_quad_mask(q2k_idx, q2k_num, q2k_quad, seq_len)
scores = (q.float() @ k.float().transpose(-1, -2)) / math.sqrt(128)
expected = torch.softmax(scores.masked_fill(~mask, -math.inf), dim=-1) @ v.float()
torch.testing.assert_close(out, expected, rtol=1e-4, atol=1e-5)
def test_tile64_quadrant_mask_is_exact_masked_softmax():
q, k, v, q2k_idx, q2k_num, kv_valid, q2k_quad, _, _ = _problem64(5, seed=5)
out = ref.fp4_vsa_attention_reference(q, k, v, q2k_idx, q2k_num, kv_valid, q2k_quad, quantize=False)
mask = _dense_quad_mask(q2k_idx, q2k_num, q2k_quad, q.shape[2])
valid = ref.key_valid_mask(kv_valid, q2k_idx.shape[-1], q.device).reshape(-1)
mask = mask & valid[None, None, None, :]
scores = (q.float() @ k.float().transpose(-1, -2)) / math.sqrt(128)
expected = torch.softmax(scores.masked_fill(~mask, -math.inf), dim=-1) @ v.float()
torch.testing.assert_close(out, expected, rtol=1e-4, atol=1e-5)
# ----------------------------------------------------------------------------- Triton (GPU)
def _triton():
return _module("attn_qat_vsa_train")
def _rel(a: torch.Tensor, b: torch.Tensor) -> float:
return float((a.float() - b.float()).norm() / b.float().norm().clamp_min(1e-12))
@needs_gpu
@pytest.mark.parametrize("two_level_p,smooth_k", [(False, False), (True, True)])
def test_triton_tile64_forward_matches_reference(two_level_p, smooth_k):
q, k, v, q2k_idx, q2k_num, kv_valid, q2k_quad, _, _ = _problem64(6, heads=2, seed=10, device="cuda")
expected = ref.fp4_vsa_attention_reference(q, k, v, q2k_idx, q2k_num, kv_valid, q2k_quad,
two_level_p=two_level_p, smooth_k=smooth_k)
out = _triton().fp4_vsa_attn_qat(q, k, v, q2k_idx, q2k_num, kv_valid, q2k_quad=q2k_quad,
two_level_p=two_level_p, smooth_k=smooth_k)
exact = ref.fp4_vsa_attention_reference(q, k, v, q2k_idx, q2k_num, kv_valid, q2k_quad, quantize=False)
assert _rel(out, expected) < 2e-3 < 0.1 * _rel(expected, exact)
@needs_gpu
@pytest.mark.parametrize("two_level_p", [False, True])
def test_triton_tile64_backward_matches_ste_reference(two_level_p):
q, k, v, q2k_idx, q2k_num, kv_valid, q2k_quad, _, _ = _problem64(6, heads=2, seed=11, device="cuda")
q, k, v = (t.clone().requires_grad_(True) for t in (q, k, v))
out = _triton().fp4_vsa_attn_qat(q, k, v, q2k_idx, q2k_num, kv_valid, q2k_quad=q2k_quad,
two_level_p=two_level_p)
grad_out = torch.randn_like(out)
dq, dk, dv = torch.autograd.grad(out, (q, k, v), grad_out)
_, weights = ref.fp4_vsa_attention_reference(q, k, v, q2k_idx, q2k_num, kv_valid, q2k_quad,
two_level_p=two_level_p, return_dv_weights=True)
q_hat, k_hat, v_hat = ref.fake_quant_qkv(q.detach(), k.detach(), v.detach())
token_mask = _dense_quad_mask(q2k_idx, q2k_num, q2k_quad, q.shape[2])
valid = ref.key_valid_mask(kv_valid, q2k_idx.shape[-1], q.device).reshape(-1)
token_mask = token_mask & valid[None, None, None, :]
expected = ref.ste_gradients_reference(q_hat, k_hat, v_hat, token_mask, weights, grad_out, 1 / math.sqrt(128))
for name, got, want in zip("qkv", (dq, dk, dv), expected, strict=True):
assert _rel(got, want) < 2e-2, name
+61 -1
View File
@@ -5,6 +5,9 @@ Production only routes SM100 here, but the kernel is plain Triton, so parity is
checked on any CUDA device.
"""
import importlib.util
from pathlib import Path
import pytest
import torch
@@ -40,6 +43,63 @@ def test_tile_to_bhsd_matches_scatter_and_four_transposes(dit_shape):
source_index[metadata.non_pad_index] = metadata.tile_partition_indices.to(torch.int32)
out = torch.empty((4, 2, padded_sequence, 128), device="cuda", dtype=torch.bfloat16)
actual = tile_to_bhsd(qkvg, source_index, out, sequence, padded_sequence, 2, 128).chunk(4)
for name, old, new in zip(("q", "k", "v", "gate"), expected, actual):
for name, old, new in zip(("q", "k", "v", "gate"), expected, actual, strict=True):
assert old.shape == new.shape, name
assert torch.equal(old, new), name
@pytest.mark.parametrize("fused", [False, True])
def test_layout_benchmark_matches_forward_preprocessing(monkeypatch, fused):
if not torch.cuda.is_available():
pytest.skip("CUDA device required")
from fastvideo.attention.backends import video_sparse_attn as vsa
path = Path(__file__).resolve().parents[1] / "benchmarks/bench_vsa_tile_layout.py"
spec = importlib.util.spec_from_file_location("bench_vsa_tile_layout", path)
benchmark = importlib.util.module_from_spec(spec)
spec.loader.exec_module(benchmark)
original_gate = vsa._can_fuse_vsa64_layout
def allow_test_device(x, metadata):
with monkeypatch.context() as gate_patch:
gate_patch.setattr(torch.cuda, "get_device_capability", lambda *args: (10, 0))
return original_gate(x, metadata)
monkeypatch.setattr(vsa, "_can_fuse_vsa64_layout", allow_test_device)
impl = object.__new__(vsa.VideoSparseAttentionImpl)
builder = vsa.VideoSparseAttentionMetadataBuilder()
kwargs = dict(current_timestep=0,
raw_latent_shape=(5, 5, 6),
patch_size=(1, 1, 1),
VSA_sparsity=0.8,
device=torch.device("cuda"),
cache_tile_buf=True)
qkvg = torch.randn((4, 157, 2, 128), device="cuda", dtype=torch.bfloat16)[:, :150]
captured = []
def capture_attention_inputs(q, k, v, *args, **kwargs):
captured[:] = [q, k, v, kwargs["compress_attn_weight"]]
return q
monkeypatch.setattr(vsa, "video_sparse_attn", capture_attention_inputs)
with torch.no_grad():
layout_step = benchmark.make_step(impl, qkvg, builder, kwargs, fused=fused, include_attn=False)
attention_step = benchmark.make_step(impl, qkvg, builder, kwargs, fused=fused, include_attn=True)
def reject_cat(*args, **kwargs):
pytest.fail("The timed layout step must not concatenate its outputs")
with monkeypatch.context() as timed_patch:
timed_patch.setattr(torch, "cat", reject_cat)
layout = layout_step()
attention = attention_step()
assert isinstance(layout, tuple) and len(layout) == 4
for actual, expected in zip(layout, captured, strict=True):
assert torch.equal(actual, expected)
assert actual.stride() == expected.stride()
assert all(t.is_contiguous() for t in layout[:3])
assert layout[3].is_contiguous() is fused
assert isinstance(attention, tuple) and len(attention) == 1
assert torch.equal(attention[0], captured[0].transpose(1, 2))
+4
View File
@@ -199,6 +199,8 @@ def legacy_from_pretrained_to_config(
components["transformer_weights"] = value
elif key == "init_weights_from_safetensors_2":
components["transformer_2_weights"] = value
elif key == "h3_vae_nvfp4_decoder":
components["h3_vae_nvfp4_decoder"] = value
else:
experimental[key] = deepcopy(value)
@@ -331,6 +333,8 @@ def generator_config_to_fastvideo_args(config: GeneratorConfig | Mapping[str, An
kwargs["init_weights_from_safetensors"] = components.transformer_weights
if components.transformer_2_weights is not None:
kwargs["init_weights_from_safetensors_2"] = components.transformer_2_weights
if components.h3_vae_nvfp4_decoder is not None:
kwargs["h3_vae_nvfp4_decoder"] = components.h3_vae_nvfp4_decoder
if components.config_root is not None:
kwargs["config_model_path"] = components.config_root
if components.upsampler_weights is not None:
+24
View File
@@ -0,0 +1,24 @@
# SPDX-License-Identifier: Apache-2.0
"""Sampling defaults for Pika H3 Flex."""
from dataclasses import dataclass
from fastvideo.api.sampling_param import SamplingParam
@dataclass
class PikaH3FlexSamplingParam(SamplingParam):
"""Six re-noise forwards, no guidance, and the 768p bucket by default."""
num_inference_steps: int = 6
guidance_scale: float = 1.0
batch_cfg: bool = False
negative_prompt: str = ""
height: int = 768
width: int = 1344
num_frames: int = 124
fps: int = 24
seed: int = 0
__all__ = ["PikaH3FlexSamplingParam"]
+1
View File
@@ -120,6 +120,7 @@ class ComponentConfig:
transformer_weights: str | None = None
transformer_2_weights: str | None = None
vae_weights: str | None = None
h3_vae_nvfp4_decoder: str | None = None
upsampler_weights: str | None = None
lora_path: str | None = None
lora_nickname: str = "default"

Some files were not shown because too many files have changed in this diff Show More