Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
661d18ba55 | ||
|
|
4b4df1a94e | ||
|
|
7066408fb3 | ||
|
|
fcec554456 | ||
|
|
181725e3fd | ||
|
|
3fe9c0c70d | ||
|
|
de873eae2d | ||
|
|
4f80c5a3da | ||
|
|
b0047cfedc | ||
|
|
ac81198d33 | ||
|
|
cb02062930 | ||
|
|
d954fad9ec | ||
|
|
166b9b8a07 | ||
|
|
7e1ed5e28e | ||
|
|
cb6a9a13e1 | ||
|
|
0d1244a869 | ||
|
|
18b2a35106 | ||
|
|
4d0a10d989 | ||
|
|
d9e6c3eb82 | ||
|
|
7203b4976e | ||
|
|
6bb444eef1 | ||
|
|
6743f4b4b7 |
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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" />
|
||||
@@ -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" />
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) };
|
||||
}
|
||||
@@ -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];
|
||||
|
||||
@@ -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." };
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -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 });
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>=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<block>.<sub> 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. |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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."
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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).
|
||||
@@ -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`**
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 +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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 |
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|----------|-------------|
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
@@ -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
Reference in New Issue
Block a user