Compare commits

..
Author SHA1 Message Date
Mingyang ZhongandClaude Opus 5.5 a01c50a806 [bugfix] End partial causal Wan mask blocks at the real token boundary (#1976)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 02:38:27 +00:00
0b3d2db9f3 [bugfix] Stop the preprocessing dataset from keeping every decoded video in memory (#2069)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Aryan Kumar <156166681+aryan5v@users.noreply.github.com>
2026-11-06 02:31:54 +00:00
Keith 58940992ae [bugfix] Streaming: answer malformed client frames with invalid_message instead of dropping the session (#2073) 2026-11-05 18:14:53 +00:00
Raghav KandClaude Opus 5.5 f8864246db [bugfix] Map merged-LoRA export keys back to HF names when LoRA was enabled after loading (#2070)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 15:40:06 +00:00
Raghav KandClaude Opus 5.5 47010cf91c [bugfix] Fall back to PyAV when torchvision.io.read_video is unavailable in preprocessing (#2068)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 13:26:45 +00:00
Minshen ZhangandClaude Opus 5.5 764f50c711 [feat] OpenAI video API: accept data: URLs for video and audio references (#2030)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 04:55:07 +00:00
FV SuperAgentandAryan Kumar d866c2d507 [bugfix] Gate the FA4 NVFP4 path on the installed kernel, not the import (#2067)
Co-authored-by: Aryan Kumar <156166681+aryan5v@users.noreply.github.com>
2026-11-04 19:50:30 -08:00
Aryan KumarandAryan Kumar c845a8dee2 [feat] Per-request community LoRA in the OpenAI video API (#2026)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-11-04 19:44:47 -08:00
tianwei-moandClaude Opus 5.5 36dc971dc6 [feat] MiniMax-H3 Ref2VA: continue an earlier video from its last frames (#2001)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 01:24:11 +00:00
tianwei-moandClaude Opus 5.5 229c43c8d3 [feat] MiniMax-H3 Ref2VA SFT: train each clip at its own canvas and frame count (#2003)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 01:12:34 +00:00
Haoyu Cheng 9e1915ceb4 [bugfix] Honor output_video_name when selecting output filenames (#2044) 2026-11-04 23:25:14 +00:00
Kostiantyn Dementiev 40651713d5 [bugfix] Count the wait for a concurrency slot in bench latency (#1960) 2026-11-04 23:14:33 +00:00
0z5a ebe8fb489c [bugfix] Fix Python-only kernel build default (#2064) 2026-11-04 20:40:47 +00:00
702e62f2b2 [bugfix] SP warmup: use the rank's own device instead of hardcoded CUDA (#1988)
Signed-off-by: li-lizhe <147392333@qq.com>
Co-authored-by: aryan5v <aryan5v@users.noreply.github.com>
Co-authored-by: Aryan Kumar <156166681+aryan5v@users.noreply.github.com>
2026-11-04 20:28:49 +00:00
0z5a 709457c71d [bugfix] Honor explicit ThunderKittens build requests on aarch64 (#2062) 2026-11-04 19:22:57 +00:00
Aryan KumarandAryan Kumar 661d18ba55 [docs] Pika H3 Flex cookbook: --weights for the NVFP4 QAD releases and FP4 attention (#2054)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-11-03 01:07:54 -08:00
Aryan KumarandAryan Kumar 4b4df1a94e [perf] Fused Q/K preconditioning + NVFP4 quantize for FA4 NVFP4 attention (bit-identical) (#2052)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-11-03 01:07:11 -08:00
Aryan KumarandAryan Kumar 7066408fb3 [feat] Pika H3 Flex NVFP4 QAD training: calibration, QAD, FP4 attention and export (#2051)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-11-03 01:04:26 -08:00
Aryan KumarandAryan Kumar fcec554456 [feat]: shared FastH3 NVFP4 FP4-attention QAD recipe (OmniRef + V2 T2AV) (#1989)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-11-03 00:54:28 -08:00
Aryan KumarandAryan Kumar 181725e3fd [docs] Fix cookbook component selection and memory reporting (#2045)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-11-03 00:54:18 -08:00
Aryan KumarandAryan Kumar 3fe9c0c70d [bugfix]: get the full CI suite green on main (pytest 9 flag teardown, Qwen golden env) (#2059)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-11-02 23:32:40 -08:00
William Lin de873eae2d [bugfix] Pass disable_autocast through to FastVideoArgs (#2056) 2026-11-02 13:42:08 -08:00
4f80c5a3da [feat] Open-H3-Regenerate-2K: regenerate FastH3 QAD, Trim and V2 drafts at 2K (text to 2K 3.1-3.4x faster) (#2037)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
Co-authored-by: Aryan <aryan@aryan5v.dev>
2026-11-02 03:44:28 -08:00
Aryan KumarandAryan Kumar b0047cfedc [feat] Open-H3-Regenerate-2K: NVFP4 H3 video decoder (decode 23 s to 5 s at 2K) (#2036)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-11-01 22:51:41 -08:00
Aryan KumarandAryan Kumar ac81198d33 [new-model] Open-H3-Regenerate-2K: 768P H3 draft to 2560x1440, served on the OpenAI video routes (#2035)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-11-01 22:45:09 -08:00
Aryan KumarandAryan Kumar cb02062930 [new-model] Add Pika H3 Flex inference support (#2038)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-11-01 22:33:10 -08:00
Aryan KumarandAryan Kumar d954fad9ec [perf] MiniMax H3: faster FP4 attention padding, uint8 VAE pixel path and cuDNN FP4 GEMMs (#2046)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-11-01 22:23:10 -08:00
Aryan KumarandAryan Kumar 166b9b8a07 [perf] MiniMax H3: fuse VAE decoder QK RMSNorm and partial RoPE (#2041)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-11-01 22:15:35 -08:00
Aryan KumarandAryan Kumar 7e1ed5e28e [perf] MiniMax H3: decode eight VAE spatial tiles per call by default (#2040)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-11-01 22:06:23 -08:00
Aryan KumarandAryan Kumar cb6a9a13e1 [bugfix] FA4 JIT cache vs tuning env, MXFP8 import and scale dtype, H3 VSA gate double quantization (#2049)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-11-01 19:11:25 -08:00
Aryan KumarandAryan Kumar 0d1244a869 [perf] MiniMax H3: opt-in faster kernel tier for the DiT fused ops (#2042)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-11-01 15:22:09 -08:00
Aryan KumarandAryan Kumar 18b2a35106 [bugfix] FA4: stop forwarding max_seqlen to the varlen kernel (#2043)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-11-01 15:21:38 -08:00
Aryan KumarandAryan Kumar 4d0a10d989 [docs] Fix the docs build, cookbook memory status, and broken commands (#2023)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-10-30 17:08:41 -07:00
Lifa Ha d9e6c3eb82 [bugfix] Fix extra work in VSA64 layout benchmark (#2027) 2026-10-30 02:30:07 +00:00
Aryan Kumar 7203b4976e [bugfix]: compile only the LoRA delta, not the wrapped base layer
BaseLayerWithLoRA.forward was decorated with torch.compile, so wrapping a linear also compiled its base layer.
Compiling a packed NVFP4/FP8 linear on its own changes its numerics, so a wrapped layer at strength 0 could differ
from the unwrapped layer. forward now adds no compiled region: the base layer runs exactly as it would unwrapped
(eagerly, or traced with a caller's compiled block), and only the adapter delta is compiled.
2026-10-28 14:49:15 -07:00
Aryan Kumar 6bb444eef1 [bugfix]: run the base layer while a LoRA wrapper has no adapter 2026-10-28 14:49:15 -07:00
Aryan Kumar 6743f4b4b7 Revert "[new-model] Add PAN2 text-to-video and image-to-video" (#2025) 2026-10-27 18:03:59 -07:00
465654d900 [bugfix] Reject missing CFG negative conditioning during input validation (#2011)
Co-authored-by: Weiers <16632241+zoechen1212@user.noreply.gitee.com>
Co-authored-by: Aryan Kumar <156166681+aryan5v@users.noreply.github.com>
Co-authored-by: aryan5v <aryan5v@users.noreply.github.com>
2026-10-27 08:45:21 +00:00
69b3829293 [bugfix] Normalize local and HF-cache model paths before registry lookup (#1997)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Aryan Kumar <156166681+aryan5v@users.noreply.github.com>
2026-10-27 07:47:20 +00:00
William Linandaryan5v a0e6cf1f50 [new-model] Add PAN2 text-to-video and image-to-video (#1961)
Co-authored-by: aryan5v <email.aryan1005@gmail.com>
2026-10-25 20:20:00 -07:00
Loay RashidandAryan Kumar 0d05b09c57 [new-model] Add initial Qwen-Image-2.1 inference (#1918)
Co-authored-by: Aryan Kumar <156166681+aryan5v@users.noreply.github.com>
2026-10-25 17:28:49 +00:00
Kyle HuandAryan Kumar a92c5c1578 [docs]: add spark4 CI backend routing (#2014)
Co-authored-by: Aryan Kumar <aryank@Aryans-Mac-Studio.local>
2026-10-24 16:29:44 -07:00
Aryan KumarandAryan Kumar dfc5550c35 [ci] Add FA2-enabled Spark CI image and GB10 reference setup (#2017)
Co-authored-by: Aryan Kumar <aryank@Aryans-Mac-Studio.local>
2026-10-24 16:16:25 -07:00
posiposi443-opsandAryan Kumar 8db2d71ef2 [perf] Cache MLX H3 text refinement and speed up attention (#2002)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-10-23 12:40:47 -07:00
Aryan Kumar 1a1e7216d2 [ci]: Run macOS MLX smoke when a draft PR is marked ready 2026-10-23 12:01:17 -07:00
Aryan Kumar 33bdd2528b [ci]: Require macOS MLX smoke for PRs that touch MLX code 2026-10-23 12:01:17 -07:00
Aryan Kumar 9bbdd18548 [ci]: Fix env-policy and schema-parity contract tests
The unit lane fails on main in two contract tests. All three failures are
bookkeeping that drifted from the code, not runtime bugs:

- `test_env_policy.py::test_env_doc_table_matches_registry`: the generated
  table in docs/contributing/env_vars.md is stale for
  FASTVIDEO_MLX_QUANT_GEMM. Regenerated with the documented command
  (`python fastvideo/tests/contract/test_env_policy.py`).
- `test_env_policy.py::test_env_access_follows_policy`: two new violations.
  - fastvideo/tests/mlx/test_mlx_affine_dq_gemm.py wrote
    FASTVIDEO_MLX_QUANT_GEMM through monkeypatch.setenv; now uses the
    registered field's override() through the env_overrides fixture.
  - fastvideo/tests/train/models/test_load_kandinsky5.py read
    FASTVIDEO_ATTENTION_BACKEND with `not in os.environ`; now uses
    envs.FASTVIDEO_ATTENTION_BACKEND.is_set() and override(). Its entry in
    KNOWN_VIOLATIONS is deleted, as the shrinking-list rule requires.
- `test_schema_parity_inventory.py::..._extension_fields_are_classified`:
  MiniMaxH3PipelineConfig declares video_scheduler_shift and
  audio_scheduler_shift (training-time noise clocks rejected by
  check_pipeline_config at inference); both are added to
  pipeline_config_extensions.internal_only.

Test plan (CPU, no GPU required):

  python -m pytest fastvideo/tests/contract/test_env_policy.py \
      fastvideo/tests/api/test_schema_parity_inventory.py \
      fastvideo/tests/mlx/test_mlx_affine_dq_gemm.py -q
  # 45 passed

  python -m pytest fastvideo/tests/contract/ fastvideo/tests/api/ -q -m "not gpu"
  # 501 passed
2026-10-23 11:08:37 -07:00
389 changed files with 31616 additions and 7071 deletions
@@ -1,27 +0,0 @@
---
date: 2026-10-23
category: evaluation
severity: important
---
# Synchronized GEMM parity does not establish request determinism
A fixed cuDNN FP4 GEMM plan matched isolated captured outputs, a complete step,
and three scored requests on one GB10. Repeated requests on another GB10 later
produced different latents. Restoring the prior row path reproduced the known
latents on three fresh requests. The replacement was withdrawn.
Comparing every GEMM output with `torch.equal` made all 400 calls and the complete
request match. That comparison synchronizes execution; it can conceal an
ordering or lifetime problem. Repeated component tests also passed. The exact
cause remains unresolved, so these observations do not establish a vendor bug.
A separate larger workspace happened to make two requests match, but cuDNN
reported zero required workspace for the selected plan. Treat that as an
allocation/timing perturbation, not evidence of a workspace fix.
After component parity, repeat complete requests without additional per-kernel
synchronization, including another device when the change is intended to be
portable. Compare latent and decoded-output hashes. Preserve failing evidence
and restore the stable path when the proposed gain is small and the cause is
unresolved; passing a finite scored seed set does not establish determinism.
@@ -1,38 +0,0 @@
---
date: 2026-10-23
category: evaluation
severity: important
---
# Copying an instance-compiled module can benchmark the original module
## What happened
A decoder quantization experiment replaced every intended linear in a copied
module, yet measured exactly the original output and almost the original time.
The replacement layers were present in `named_modules()`, which was insufficient
evidence that the new computation actually ran.
## Root cause
`ComposedPipelineBase._compile_with_conditions` assigns
`submodule.forward = torch.compile(submodule.forward)`. The returned function
closes over a bound method of the original module. Python's `copy.deepcopy`
copies module state but retains function objects and their closures. The copied
instance therefore still has a compiled `forward` that calls the original
instance. Unwrapping `_orig_mod` alone does not handle this form of compilation.
## Fix
Prefer constructing a fresh module and loading the intended state. If copying
this known instance-compiled form for an experiment, remove the copied instance's
`forward` override before modifying children, then compile its newly bound class
method. Do not remove arbitrary user-defined forward overrides blindly.
## Prevention
Inspect the callable as well as the module tree. Verify a replacement executes
with a temporary hook or profiler trace, compare its output against the intended
numerical oracle, and run final timing in a fresh process. An unchanged output
from a supposedly lossy quantization is a reason to investigate dispatch before
accepting the measurement.
@@ -1,26 +0,0 @@
---
date: 2026-10-23
category: performance
severity: important
---
# Wide Triton gathers can turn an affine slice into expensive selection code
A decoder RMSNorm prototype kept a 2,048-element row in registers, then used
`tl.gather` with indices `512 * chunk + 4 * lane + component` to extract the
128 lanes needed by its original reduction order. Although every index was
an affine expression, this lowered poorly on the Spark's Triton/SM121 toolchain.
The eight-row variant emitted PTX with 168,062 virtual 32-bit registers and
spent minutes in ptxas. Earlier variants were numerically exact but took
392–1,475 ms for work the existing kernel completed in about 2.4 ms.
For this layout, reshaping the row into four 512-element chunks and extracting
them with `tl.trans` and `tl.split` avoided the selection expansion. The same
ordered FMAs then compiled promptly and matched the original outputs. Keeping
the row in registers still gave no meaningful speedup, so the rewrite was dropped.
Inspect generated IR and benchmark one small launch before compiling a whole
parameter sweep of wide gathers. Prefer static reshape/split operations for
known affine partitions; reserve gathers for indexing that actually needs them.
After changing an in-place residual path, also zero invalid padded rows before
quantization: the old two-pass kernel obtained those zeros from its masked reload.
@@ -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.
+15 -7
View File
@@ -1,15 +1,15 @@
---
name: ci-runner
description: Work on FastVideo's Slurm-only, change-aware GPU CI lanes, static Buildkite graph, trusted ci-runner policy, lane scripts, and GB200 validation.
description: Work on FastVideo's Spark 4 and Slinky Slurm GPU CI lanes, static Buildkite graph, trusted ci-runner policy, lane scripts, and hardware validation.
---
# Slinky Slurm CI lanes
# GPU CI lanes
FastVideo's `ci-runner` Buildkite queue is the control plane for all active
GPU CI. A private host-owned dispatcher leases GPUs from the Slinky Slurm tray
and runs the immutable PR SHA inside an isolated Enroot container. Buildkite
pipeline upload and Slurm submission occur on the login plane; every test
payload executes on Slurm compute.
GPU CI. The trusted uploader runs on the Slinky login plane. Slinky agents
dispatch cluster lanes through Slurm into isolated Enroot containers. The
Spark 4 agent dispatches eligible single-GPU lanes through local Slurm into
isolated Docker containers. Both execute the immutable PR SHA.
The files under `fastvideo/tests/modal/` and `.buildkite/scripts/pr_test.sh`
are dormant rollback code. Never add an active Buildkite or slash-command
@@ -33,6 +33,14 @@ and the coordination contract with that bundle.
A Spark runner sets `FASTVIDEO_CI_BACKEND=spark4`; gate cluster-only tests on
that flag, never on memory heuristics. Keep
`SPARK_CAPABLE_LANES` in `test_ci_test_collection.py` in sync.
- The trusted uploader fills `FV_SPARK_BACKEND`: `spark4` for `fastcheck`,
`merge` and `direct` builds, including unset scope treated as Fastcheck,
while an agent tagged `queue=ci-runner,backend=spark4` is connected and
not paused, `slurm`
otherwise. Selection happens at upload time. Legacy graphs without backend
tags need explicit normalization; setting the variable alone does not pin
those jobs to Slurm. It rejects any other `backend` value. The upload step
in each entry pipeline requires `backend=slurm` so it never lands on the Spark runner.
- Active CI contains no `pr_test.sh` command, Modal invocation, default queue,
Buildkite plugin, `soft_fail`, or job-controlled artifact glob.
- The six Fastcheck lanes use `:microscope:` labels. Full-Suite-only lanes use
@@ -64,7 +72,7 @@ and the coordination contract with that bundle.
- Tests must preserve an inherited `MASTER_PORT`. Packed containers share the
tray network namespace, so the private runner assigns a distinct port range
per GPU lease and the SSIM scheduler assigns task offsets within its range.
- The ARM64 runner image includes the pinned FA4 CuTe overlay validated on
- The Slinky ARM64 runner image includes the pinned FA4 CuTe overlay validated on
GB200. Keep SSIM at `FASTVIDEO_FA4=1` because its references were seeded with
FA4; keep lanes with FA2 baselines at `FASTVIDEO_FA4=0`. A runner image change
must revalidate both the FA4 import and an actual GB200 forward kernel.
+1
View File
@@ -24,6 +24,7 @@ exec pytest \
./fastvideo/tests/stages/ \
./fastvideo/tests/ops/ \
./fastvideo/tests/worker/ \
./fastvideo/tests/distributed/test_sp_warmup_device.py \
./fastvideo/tests/training/test_runner.py \
./fastvideo/tests/training/test_trackers.py \
./fastvideo/tests/training/test_ltx2_rope_fps.py \
+22
View File
@@ -9,6 +9,28 @@ merge_protections:
- check-success=fastcheck-passed
- check-success=full-suite-passed
# Mirrors the `paths` filter of .github/workflows/ci-macos-mlx.yml so the
# required checks always exist whenever this protection applies.
- name: MLX smoke requirements
if:
- base = main
- or:
- files~=^\.github/workflows/ci-macos-mlx\.yml$
- files~=^fastvideo/mlx_runtime/
- files~=^fastvideo/tests/mlx/
- files~=^fastvideo/tests/platforms/test_mps_vsa_error\.py$
- files~=^fastvideo/tests/platforms/test_cpu_sdpa\.py$
- files~=^fastvideo/platforms/cpu\.py$
- files~=^fastvideo/platforms/mps\.py$
- files~=^fastvideo/platforms/__init__\.py$
- files~=^fastvideo/__init__\.py$
- files~=^examples/inference/basic/mlx_[^/]*\.py$
- files~=^fastvideo/benchmarks/mlx_[^/]*\.py$
- files~=^pyproject\.toml$
success_conditions:
- check-success=mlx-smoke
- check-success=mlx-smoke-linux-cpu
pull_request_rules:
# ============================================================
+5
View File
@@ -131,6 +131,11 @@ class FamilyCoverage:
FAMILY_COVERAGE = (
FamilyCoverage(
re.compile(r"(^|[/_.-])qwen[_-]?image[_-]?2[._-]?1([/_.-]|$)"),
("test_qwen_image21.py", ),
("test_qwen_image21_similarity.py", ),
),
FamilyCoverage(
re.compile(r"(^|[/_.-])flux[_-]?2([/_.-]|$)"),
("test_flux2_klein.py", ),
+9
View File
@@ -2,6 +2,9 @@ name: macOS MLX Smoke
on:
pull_request:
# The defaults plus ready_for_review: both jobs skip drafts, and Mergify requires them
# for MLX changes, so marking a draft ready must run them.
types: [opened, synchronize, reopened, ready_for_review]
branches: [main]
paths:
- ".github/workflows/ci-macos-mlx.yml"
@@ -85,7 +88,10 @@ jobs:
fastvideo/tests/mlx/test_mlx_affine_dq_gemm.py \
fastvideo/tests/mlx/test_mlx_minimax_h3_parity.py \
fastvideo/tests/mlx/test_mlx_minimax_h3_vsa.py \
fastvideo/tests/mlx/test_mlx_minimax_h3_vsa_sdpa.py \
fastvideo/tests/mlx/test_mlx_minimax_h3_vsa_nax.py \
fastvideo/tests/mlx/test_mlx_minimax_h3_vsa_regressions.py \
fastvideo/tests/mlx/test_mlx_minimax_h3_prefix_kv.py \
fastvideo/tests/mlx/test_mlx_minimax_h3_fast_mode.py \
fastvideo/tests/mlx/test_mlx_minimax_h3_fast_spatial.py \
fastvideo/tests/mlx/test_mlx_minimax_h3_ref_layout.py \
@@ -157,7 +163,10 @@ jobs:
fastvideo/tests/mlx/test_mlx_affine_dq_gemm.py \
fastvideo/tests/mlx/test_mlx_minimax_h3_parity.py \
fastvideo/tests/mlx/test_mlx_minimax_h3_vsa.py \
fastvideo/tests/mlx/test_mlx_minimax_h3_vsa_sdpa.py \
fastvideo/tests/mlx/test_mlx_minimax_h3_vsa_nax.py \
fastvideo/tests/mlx/test_mlx_minimax_h3_vsa_regressions.py \
fastvideo/tests/mlx/test_mlx_minimax_h3_prefix_kv.py \
fastvideo/tests/mlx/test_mlx_minimax_h3_fast_mode.py \
fastvideo/tests/mlx/test_mlx_minimax_h3_fast_spatial.py \
fastvideo/tests/mlx/test_mlx_minimax_h3_ref_layout.py \
+3 -2
View File
@@ -22,8 +22,9 @@ jobs:
trigger:
if: >-
inputs.pr_number > 0
|| (github.event.action == 'labeled' && github.event.label.name == 'ready')
|| github.event.action == 'synchronize'
|| (github.event.pull_request.base.ref == github.event.repository.default_branch
&& ((github.event.action == 'labeled' && github.event.label.name == 'ready')
|| github.event.action == 'synchronize'))
runs-on: ubuntu-latest
# Job-level concurrency: only this guarded job acquires the group, so an
# unrelated `labeled` event (which skips the job) cannot cancel an in-flight
+45 -3
View File
@@ -12,10 +12,52 @@ permissions:
issues: write
jobs:
publish-node:
name: Publish Custom Node to registry
# Any pyproject.toml edit triggers this workflow, but the registry rejects a
# version it already has ("The node version already exists"). Publish only
# when [project].version changed, as the PyPI workflow does.
check-version-change:
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'hao-ai-lab' }}
outputs:
changed: ${{ steps.check.outputs.changed }}
steps:
- name: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Compare [project].version with the version before this push
id: check
shell: python
env:
BEFORE_SHA: ${{ github.event.before }}
run: |
import os
import subprocess
import tomllib
def version(text: str) -> str | None:
return tomllib.loads(text).get("project", {}).get("version")
def git(*args: str) -> subprocess.CompletedProcess:
return subprocess.run(["git", *args], capture_output=True, text=True)
# A push can carry several commits; compare with the tip before the push, not HEAD~1.
base = os.environ.get("BEFORE_SHA", "").strip()
if not base or set(base) == {"0"} or git("fetch", "--no-tags", "--depth=1", "origin", base).returncode:
base = "HEAD~1"
new = version(open("pyproject.toml", encoding="utf-8").read())
previous = git("show", f"{base}:pyproject.toml")
old = version(previous.stdout) if previous.returncode == 0 else None
changed = new is not None and new != old
print(f"base: {base}, previous version: {old}, current version: {new}, changed: {changed}")
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
output.write(f"changed={str(changed).lower()}\n")
publish-node:
name: Publish Custom Node to registry
needs: check-version-change
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'hao-ai-lab' && (needs.check-version-change.outputs.changed == 'true' || github.event_name == 'workflow_dispatch') }}
steps:
- name: Check out code
uses: actions/checkout@v4
@@ -25,4 +67,4 @@ jobs:
uses: Comfy-Org/publish-node-action@v1
with:
## Add your own personal access token to your Github Repository secrets and reference it here.
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
+11
View File
@@ -0,0 +1,11 @@
# syntax=docker/dockerfile:1.7
# CI-only GB10 image. The regular Spark image keeps its native SDPA defaults.
ARG SPARK_BASE_IMAGE=ghcr.io/hao-ai-lab/fastvideo/fastvideo-dev:py3.12-cuda13.0.0-latest
FROM ${SPARK_BASE_IMAGE}
# VMoBA and the Wan causal gate require FA2. Install only this wheel so the
# validated torch/flashinfer/CUTLASS stack from the base image stays intact.
ARG FLASH_ATTN_WHEEL=https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.9.22/flash_attn-2.8.3+cu130torch2.12-cp312-cp312-linux_aarch64.whl
RUN /root/.local/bin/uv pip install --python /opt/venv/bin/python --no-deps "${FLASH_ATTN_WHEEL}" && \
/opt/venv/bin/python -c "import glob, shutil; [shutil.rmtree(p) for p in glob.glob('/opt/venv/lib/python*/site-packages/flash_attn/cute')]" && \
/opt/venv/bin/python -c "import flash_attn, flashinfer; assert hasattr(flash_attn, 'flash_attn_varlen_func'); print('FA2', flash_attn.__version__, 'flashinfer', flashinfer.__version__)"
@@ -49,7 +49,6 @@ export default function HardwarePicker({
value,
onChange,
gpuCount = 1,
purpose = "target",
reload,
}: {
backend: string;
@@ -59,7 +58,6 @@ export default function HardwarePicker({
value: HardwareSelection;
onChange: (selection: HardwareSelection) => void;
gpuCount?: number;
purpose?: "target" | "filter";
reload?: () => void;
}) {
const id = useId();
@@ -71,9 +69,9 @@ export default function HardwarePicker({
const [adding, setAdding] = useState(false);
const [draft, setDraft] = useState<Draft>(() => draftFrom(value.custom, backend));
const record = resolveHardware(value, catalog);
const unknown = !!value.id && value.id !== "custom" && !record;
const candidates =
purpose === "filter" ? catalog : catalog.filter((item) => item.backend === (backend === "mlx" ? "mlx" : "cuda"));
const unknown = status === "ready" && !!value.id && value.id !== "custom" && !record;
const loading = status === "loading" && !!value.id && value.id !== "custom";
const candidates = catalog.filter((item) => item.backend === (backend === "mlx" ? "mlx" : "cuda"));
const matches = searchHardware(candidates, query).slice(0, 20);
const draftMemory = amount(draft.memory);
const draftBudget = amount(draft.budget);
@@ -144,8 +142,7 @@ export default function HardwarePicker({
</span>
{record.usable_budget_gb ? (
<span className="hardware-selected-meta">
{value.id === "custom" ? "Your budget" : "Usable budget"}: {record.usable_budget_gb}{" "}
{record.memory_unit}
Usable memory: {record.usable_budget_gb} {record.memory_unit}
</span>
) : null}
{record.host_ram_gb && record.memory_type !== "unified" ? (
@@ -155,20 +152,24 @@ export default function HardwarePicker({
) : null}
</div>
<button type="button" className="hardware-clear" onClick={() => onChange({ id: "" })}>
Clear hardware {purpose}
Clear hardware
</button>
</div>
) : value.id === "custom" ? (
<p className="hardware-current" role="status">
Add this device's memory to finish selecting it.
</p>
) : loading ? (
<p className="hardware-current" role="status">
Loading hardware catalog…
</p>
) : unknown ? (
<p className="hardware-current" role="status">
Saved hardware target ({value.id}) is not in this catalog.
The saved device ({value.id}) is not in this catalog.
</p>
) : (
<p className="hardware-current" role="status">
No hardware {purpose} selected.
No hardware selected.
</p>
)}
{record ? (
@@ -184,7 +185,7 @@ export default function HardwarePicker({
<div className="hardware-picker-actions">
{!record && value.id ? (
<button type="button" className="hardware-clear" onClick={() => onChange({ id: "" })}>
Clear hardware {purpose}
Clear hardware
</button>
) : null}
<button
@@ -198,20 +199,12 @@ export default function HardwarePicker({
setAdding(false);
}}
>
{record
? "Change hardware"
: purpose === "filter"
? "Choose hardware"
: backend === "mlx"
? "+ Find or add a Mac"
: "+ Find or add a GPU"}
{record ? "Change hardware" : backend === "mlx" ? "+ Find or add a Mac" : "+ Find or add a GPU"}
</button>
</div>
{record || open ? (
<p className="hardware-picker-caption">
{purpose === "filter"
? "Filters by runtime. Memory fit is not estimated."
: "Single-GPU devices set GPU count to 1. Nothing else in the configuration changes."}
Sets the setup guide and memory budget. Choose checkpoint precision in the recipe settings.
</p>
) : null}
{open ? (
@@ -296,13 +289,13 @@ export default function HardwarePicker({
value={draft.memory_type}
onChange={(event) => setDraft({ ...draft, memory_type: event.target.value as MemoryType })}
>
<option value="dedicated">Dedicated GPU memory (VRAM)</option>
<option value="unified">Unified memory (CPU + GPU)</option>
<option value="system">System memory (host RAM)</option>
<option value="dedicated">GPU memory (VRAM)</option>
<option value="unified">Unified memory</option>
<option value="system">System memory (RAM)</option>
</select>
</label>
<label className="hardware-form-field">
<span>Nominal memory</span>
<span>Total memory</span>
<Input
type="number"
min="1"
@@ -323,7 +316,7 @@ export default function HardwarePicker({
</select>
</label>
<label className="hardware-form-field">
<span>Usable budget (optional)</span>
<span>Usable memory (optional)</span>
<Input
type="number"
min="1"
@@ -348,16 +341,14 @@ export default function HardwarePicker({
</label>
) : null}
</div>
{budgetTooLarge ? (
<p className="hardware-form-error">The usable budget cannot exceed the nominal memory.</p>
) : null}
{budgetTooLarge ? <p className="hardware-form-error">Usable memory cannot exceed total memory.</p> : null}
<p className="hardware-form-note">
Nominal memory is device capacity, not what FastVideo can use. A budget is your own limit and is not
verified. Unified memory is shared with the CPU; it is not dedicated VRAM.
Total memory is the device's capacity. Usable memory is the part FastVideo may use, such as a MIG slice
or a limit you set. Unified memory is shared with the CPU.
</p>
<div className="hardware-form-actions">
<button type="submit" disabled={!validDraft}>
Use this device
Save device
</button>
<button
type="button"
+65 -43
View File
@@ -1,59 +1,54 @@
import { useState } from "react";
import { useId, useState } from "react";
import type { MemoryAssessment } from "@/lib/capacity";
export type Validity = "valid" | "invalid" | "loading";
export type Fit = "fits" | "tight" | "exceeds" | "unsupported" | "unknown";
export type Evidence = "recorded" | "none" | "stale";
const validityLabel: Record<Validity, string> = {
valid: "Configuration valid",
valid: "Schema checks passed",
invalid: "Configuration invalid",
loading: "Checking configuration",
};
const fitLabel: Record<Fit, string> = {
fits: "Fits estimate",
tight: "Tight fit",
exceeds: "Likely exceeds memory",
unsupported: "Unsupported combination",
unknown: "Fit unknown",
exceeds: "Exceeds memory",
unsupported: "Unsupported",
unknown: "Memory not estimated",
};
const evidenceLabel: Record<Evidence, string> = {
recorded: "Run evidence available",
none: "No recorded run",
stale: "Run evidence does not match these settings",
};
/**
* Three separate facts: the recipe schema accepts the values, a memory estimate
* covers the configuration, and a recorded run matches it. No positive claim is
* made until the shared API supplies the corresponding fact.
* Two facts, each with its own evidence: the recipe schema accepts the values,
* and the analytical estimate covers the selected device. Neither is a promise
* that the recipe runs.
*/
export default function RecipeFacts({
validity,
fit = "unknown",
evidence = "none",
subject = "configuration",
detail,
memory,
validityText,
validityDetail,
detail,
retryMemory,
}: {
validity: Validity;
fit?: Fit;
evidence?: Evidence;
memory?: MemoryAssessment;
subject?: "configuration" | "example";
detail?: string;
validityText?: string;
validityDetail?: string;
detail?: string;
retryMemory?: () => void;
}) {
const [open, setOpen] = useState(false);
const id = `recipe-facts-${validity}-${fit}-${evidence}`;
const id = useId();
const fit: Fit = memory?.status ?? "unknown";
const estimated = fit === "fits" || fit === "tight" || fit === "exceeds";
const label = fitLabel[fit];
return (
<div className="recipe-facts">
<div className="recipe-fact-list" aria-label="Recipe facts">
<div className="recipe-fact-list" aria-label="Recipe checks">
<span data-state={validity}>
{validity === "valid" ? (validityText ?? `Valid ${subject}`) : validityLabel[validity]}
{validity === "valid" ? (validityText ?? validityLabel.valid) : validityLabel[validity]}
</span>
<span data-state={fit}>{fitLabel[fit]}</span>
<span data-state={evidence}>{evidenceLabel[evidence]}</span>
<span data-state={fit}>{label}</span>
</div>
<button
type="button"
@@ -64,31 +59,58 @@ export default function RecipeFacts({
>
Details
</button>
{memory?.reason && (
<p className="matrix-note" role="status">
{memory.reason}
</p>
)}
{memory?.basis === "checkpoint" && (
<div className="recipe-memory-breakdown">
<p>
Known checkpoint data: {memory.checkpointGiB?.toFixed(1)} GiB. Device budget:{" "}
{memory.capacityGiB?.toFixed(1)} GiB.
</p>
<dl>
{Object.entries(memory.parts ?? {}).map(([name, size]) => (
<div key={name}>
<dt>{name.replaceAll("_", " ")}</dt>
<dd>{size.toFixed(1)} GiB</dd>
</div>
))}
</dl>
</div>
)}
{retryMemory && (
<button type="button" onClick={retryMemory}>
Retry memory data
</button>
)}
{open && (
<div className="recipe-facts-detail" id={id}>
<p>
<strong>{validityLabel[validity]}.</strong>{" "}
{validityDetail ??
(validity === "valid"
? "Selected values pass the recipe schema and runtime topology checks."
? "These values pass the recipe schema and runtime topology checks. Model loading, selected components and GPU execution have not been verified."
: validity === "invalid"
? "Output is withheld until the highlighted fields are corrected."
? "Output is hidden until the highlighted fields are fixed."
: "Values are still being checked.")}
</p>
<p>
<strong>{fitLabel[fit]}.</strong>{" "}
{fit === "unknown"
? "No memory estimate covers this deployment, device, and request yet. This is not a fit claim."
: "The shared capacity API supplied this estimate; it is not a measured guarantee."}
</p>
<p>
<strong>{evidenceLabel[evidence]}.</strong>{" "}
{evidence === "none"
? "Successful runs on other checkpoints, settings, software revisions, or GPU layouts do not establish this configuration."
: evidence === "stale"
? "A recorded run exists, but edited settings fall outside what it covers."
: "A recorded run matches the deployment and settings shown."}
</p>
{!memory && (
<p>
<strong>{label}.</strong> FastVideo has no capacity model for this recipe, so it does not estimate memory
use here.
</p>
)}
{memory?.notes?.map((note) => (
<p key={note}>{note}</p>
))}
{estimated && (
<p>
<strong>{label}.</strong>{" "}
{`The ${memory?.basis === "measured" ? "recorded peak" : "analytical estimate"} needs ${memory?.requiredGiB?.toFixed(1)} GiB of ${memory?.capacityGiB?.toFixed(1)} GiB available. This applies only to the covered execution settings.`}
</p>
)}
{detail && <p>{detail}</p>}
</div>
)}
+6 -1
View File
@@ -4,12 +4,17 @@ description: Recipes for video, image, audio, and world generation.
mode: wide
---
Pick a model, choose your hardware, and copy the command. Every recipe starts from checked-in YAML, so the settings you see are the settings that run.
Before you pick a device, check [weights and quantization](weights/) for measured
checkpoint sizes and the FastH3 precision variants.
<a id="inference-cookbook"></a>
<a id="planned-families-heading"></a>
<a id="video-models-heading"></a>
<a id="image-models-heading"></a>
<a id="audio-models-heading"></a>
<a id="world-models-heading"></a>
<a id="planned-families-heading"></a>
<Cookbook />
@@ -1,7 +1,14 @@
---
title: FastH3 OmniRef
description: Configure FastH3 OmniRef reference-to-video serving on an RTX 5090, RTX PRO 6000, or DGX Spark.
title: FastH3 Omni v1
description: Configure FastH3 Omni v1 reference-to-video serving and copy inference or serving commands.
mode: frame
---
<Cookbook modelId="fasth3-omniref" />
## Apple silicon
The MLX path composes the BF16 Omni release with the NVFP4 text encoder, converts
the DiT to INT6, and runs the same image and audio references on Metal. Follow
[FastH3 Omni on MLX](/inference/fasth3_omniref_mlx).
For copyable local commands, open the [MLX recipe](/cookbook/minimax-h3/models/fasth3-omniref-mlx).
@@ -0,0 +1,7 @@
---
title: Open-H3-Regenerate-2K
description: Configure Open-H3-Regenerate-2K serving, which regenerates a 768P MiniMax-H3 draft at 2560x1440.
mode: frame
---
<Cookbook modelId="open-h3-regenerate-2k" />
+115
View File
@@ -0,0 +1,115 @@
---
title: Weights and quantization
description: Measured checkpoint sizes for every cookbook model, plus the FastH3 quantized variants and which one fits a given GPU.
mode: wide
---
Every number here is the size of the safetensors files in the model repository,
measured on 2026-10-27 with the Hugging Face tree API. Treat it as the weight
floor. A run also needs activations, VSA workspace, and decode buffers, so
weights that fit do not guarantee a run that fits.
The cookbook editor starts from each recipe's maintained deployment. Changing
a quantization control does not select one of the published exports below.
## Catalog weights
Sizes are GiB at the checkpoint's runtime precision. Repositories that store
F32 weights are converted to BF16 here, which is what the loader uses.
| Model | Weights | Transformer | Text encoder | VAE |
| ----------------------- | ------- | ----------- | ------------ | ---- |
| FastH3 8-Step V2 | 137.7 | 65.3 | 62.1 | 10.3 |
| FastH3 4-Step V1 | 137.7 | 65.3 | 62.1 | 10.3 |
| FastH3 Trim | 56.8 | 34.9 | 15.3 | 6.5 |
| FastH3 Omni v1 | 92.5 | 65.3 | 15.3 | 11.9 |
| Pika H3 Flex | 136.7 | 64.3 | 62.1 | 10.3 |
| FastWan2.1 1.3B | 13.6 | 2.8 | 10.6 | 0.3 |
| Wan2.1 T2V 14B | 37.4 | 26.6 | 10.6 | 0.3 |
| Wan2.1 I2V 14B | 41.9 | 30.5 | 10.6 | 0.3 |
| Wan2.2 I2V A14B | 58.8 | 53.2 | 5.3 | 0.3 |
| Hunyuan 1.5 I2V 480p | 32.2 | 15.5 | 13.6 | 2.3 |
| Hunyuan 1.5 T2V 480p | 24.9 | 15.5 | 7.0 | 2.4 |
| Kandinsky 5 Lite | 11.1 | 2.2 | 8.5 | 0.5 |
| Kandinsky 6 Pro | 41.9 | 32.5 | 8.0 | 1.4 |
| Kandinsky 6 Pro distill | 37.5 | 28.1 | 8.0 | 1.4 |
| LTX2 Distilled | 66.9 | 35.2 | 25.4 | 3.6 |
Pika H3 Flex's transformer is the 61.7 GiB H3 trunk plus its 2.6 GiB task adapter.
Ref2VA swaps in `transformer_ref` and the `ref2va` adapter, which are the same size.
The FastVideo organization publishes the same FastH3 model at several
precisions. The transformer shrinks with precision; the text encoder and VAE
only shrink when the repository ships them quantized too.
| Checkpoint | Variant | Weights | Transformer | Text encoder | VAE |
| ----------------- | -------------- | ------- | ----------- | ------------ | ---- |
| 8-Step V2 | BF16 | 137.7 | 65.3 | 62.1 | 10.3 |
| 8-Step V2 | FP8 | 69.2 | 47.4 | 15.3 | 6.5 |
| 8-Step V2 | NVFP4 | 114.5 | 49.8 | 62.1 | 2.5 |
| 8-Step V2 | NVFP4 Consumer | 58.7 | 36.9 | 15.3 | 6.5 |
| Trim | BF16 | 56.8 | 34.9 | 15.3 | 6.5 |
| Trim | FP8 | 41.7 | 19.9 | 15.3 | 6.5 |
| Trim | NVFP4 | 32.9 | 11.1 | 15.3 | 6.5 |
| Trim | MLX INT6 | 33.0 | 14.3 | 14.2 | 4.5 |
| 4-Step V1 | BF16 | 137.7 | 65.3 | 62.1 | 10.3 |
| 4-Step V1 | NVFP4 | 114.5 | 49.8 | 62.1 | 2.5 |
| Omni v1 | BF16 DiT | 65.3 | 65.3 | n/a | n/a |
| Omni v1 | FP8 | 67.2 | 47.4 | 15.3 | 4.5 |
| Omni v1 | NVFP4 QAD | 63.3 | 37.0 | 15.3 | 11.0 |
| NVFP4 VAE decoder | quality | 5.8 | n/a | n/a | 5.8 |
| NVFP4 VAE decoder | fast | 1.3 | n/a | n/a | 1.3 |
The NVFP4 checkpoint keeps the BF16 text encoder, which is why it is larger
than FP8. The Consumer variant pairs the smaller NVFP4 transformer with the
FP8 text encoder and VAE, which brings the listed weights to 58.7 GiB. These
export sizes do not establish device fit. Loading, offload, quantization
profiles, and temporary buffers affect runtime residency. The decoder-only
rows exclude the VAE encoder and can contain master weights that the loader
packs before inference.
## Pick by GPU
| GPU family | Examples | Use | Why |
| ---------------- | ----------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------- |
| Blackwell | RTX 5090, RTX PRO 6000, B200, GB200 | NVFP4 or FP8 with a matching maintained profile | Native FP4 tensor cores cut the transformer to about half of BF16 |
| Hopper | H100, H200 | FP8 | FP8 tensor cores; the NVFP4 path requires Blackwell |
| Ada | RTX 4090, L40S | FP8 | FP8 works, NVFP4 does not |
| Ampere and older | A100, RTX 3090 | BF16 | No FP8 or FP4 hardware |
| Apple silicon | M-series | MLX INT6 or INT8 | The MLX conversions are INT6 and INT8, not FP4 |
| DGX Spark | GB10 | NVFP4 with sequential load | Unified memory, so the host RAM counts toward the budget |
## What the cookbook does now
FastH3 pages show known checkpoint sizes beside the selected device budget.
These numbers are not a runtime fit check. Offload moves weights between phases;
sequence parallelism does not necessarily shard weights; unified memory is one
shared pool. The page does not turn those file sizes into a residency claim.
When an encoder, decoder or transformer precision changes, the page keeps sizes
for unchanged components and names the missing data. It never substitutes the
size of a different published export. A complete peak estimate still requires
loading, placement, activations and workspace models for the selected path.
The transformer quantization control writes
`generator.engine.quantization.transformer_quant`, so the copied YAML names the
precision. Encoder and decoder choices use component paths and stay visible in
the YAML too.
## Automatic selection
The editor does not pick a precision from the device yet. When it does, the
rule is:
1. Read the device architecture and memory from the hardware catalog.
2. Prefer the smallest variant the architecture accelerates: NVFP4 on
Blackwell, FP8 on Hopper and Ada, INT6 on Apple, BF16 everywhere else.
3. Keep the BF16 text encoder when the quantized checkpoint ships one, and
switch the decoder to NVFP4 only on Blackwell.
4. Expose the choice as a deployment setting, not a hidden default, so the
copied command still shows which checkpoint runs.
That needs one deployment per variant in
[`docs/cookbook/recipes`](https://github.com/hao-ai-lab/FastVideo/tree/main/docs/cookbook/recipes)
and a match rule in the editor. Until then, select the precision by hand and
read the weights fact.
+149 -75
View File
@@ -2,7 +2,7 @@ import { useEffect, useId, useRef, useState, type ReactNode } from "react";
import { MotionConfig } from "motion/react";
import { ArrowUpRight } from "lucide-react";
import { Tabs } from "radix-ui";
import { modelDirectory, recipePages, sitePath, type DirectoryModel } from "@/lib/cookbook-pages";
import { modelDirectory, recipePages, recentlyAdded, sitePath, type DirectoryModel } from "@/lib/cookbook-pages";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -19,6 +19,7 @@ import {
CardCurtainRevealTitle,
} from "@/components/ui/card-curtain-reveal";
import RecipeFacts from "@/components/cookbook/RecipeFacts";
import { assessRecipe, assessWeightsRecipe, useCapacityModel, weightsFor } from "@/lib/capacity";
import { configAndLaunch, inferenceOutput, sdkCommand } from "@/lib/cookbook-commands";
import {
gpuCountFrom,
@@ -130,7 +131,7 @@ function FieldControl({
</div>
{choices ? (
<div role="group" aria-label={label} className="option-chips">
{value === undefined && (
{value === undefined && !choices.some((choice) => choice === null) && (
<button type="button" aria-pressed={!changed} onClick={() => update(field.path, undefined, field)}>
Default
</button>
@@ -139,21 +140,12 @@ function FieldControl({
<button
key={JSON.stringify(choice)}
type="button"
aria-pressed={value === choice}
aria-pressed={value === choice || (value === undefined && choice === null)}
onClick={() => update(field.path, JSON.stringify(choice), field)}
>
{choiceLabel(field.path, choice)}
{choice === null ? (CHOICE_LABELS[field.path]?.null ?? "Default") : choiceLabel(field.path, choice)}
</button>
))}
{nullable && !choices.includes(null) && (
<button
type="button"
aria-pressed={value === null}
onClick={() => update(field.path, "null", { ...field, schema: { ...field.schema, enum: [null] } })}
>
null
</button>
)}
</div>
) : kind.includes("object") || kind.includes("array") ? (
<Textarea
@@ -209,7 +201,7 @@ function FieldControl({
Use null
</button>
)}
<p id={`${id}-hint`} className="sr-only">
<p id={`${id}-hint`} className="matrix-note">
{field.schema.description}
{value === undefined ? " Inherited from the runtime." : ""}
</p>
@@ -279,6 +271,18 @@ function Editor({
errors[path] = "Enter a valid number or JSON value for this field.";
}
}
const decoderPath = "generator.pipeline.experimental.h3_vae_nvfp4_decoder";
const backendPath = "generator.pipeline.experimental.video_decode_backend";
if (path === backendPath && next[path] === "taeh3" && fields.some((field) => field.path === decoderPath)) {
next[decoderPath] = null;
delete errors[decoderPath];
setNotice("TAEH3 selected. The NVFP4 decoder override was cleared.");
}
if (path === decoderPath && next[path] && fields.some((field) => field.path === backendPath)) {
next[backendPath] = "h3-vae";
delete errors[backendPath];
setNotice("H3 VAE selected for the NVFP4 decoder.");
}
if (path === GPU_COUNT_PATH && typeof next[path] === "number") {
const fitted = fitParallelism(fields, next, next[path] as number);
if (fitted !== next) {
@@ -313,6 +317,43 @@ function Editor({
const hardwareRecord = resolveHardware(hardware, hardwareCatalog.records);
const gpuCount = gpuCountFrom(result?.config);
const model = modelDirectory.find((model) => model.id === catalog.id.split("/")[0])!;
const capacityModel = useCapacityModel(model.id);
const weights = weightsFor(model.id);
const memory =
result && !failure
? hardwareRecord
? capacityModel.status === "error"
? { status: "unknown" as const, reason: `Memory data could not load. ${capacityModel.error}` }
: capacityModel.model
? assessRecipe({
model: capacityModel.model,
example: { workload: catalog.workload, env: catalog.env },
hardware: hardwareRecord,
config: result.config,
budgetGb: hardwareRecord.usable_budget_gb,
hostGb: hardwareRecord.host_ram_gb,
})
: catalog.runtime.backend === "mlx" && capacityModel.status !== "loading"
? {
status: "unknown" as const,
reason: "MLX checkpoints use a different precision and load pattern; memory is not estimated.",
}
: capacityModel.status !== "loading" && weights
? assessWeightsRecipe({
weights,
config: result.config,
hardware: hardwareRecord,
budgetGb: hardwareRecord.usable_budget_gb,
})
: {
status: "unknown" as const,
reason:
capacityModel.status === "loading"
? "Loading the memory estimate."
: "No capacity model ships for this checkpoint.",
}
: { status: "unknown" as const, reason: "Select a device to estimate memory for this recipe." }
: undefined;
const renderField = (field: Field) => (
<FieldControl
key={`${reset}-${field.path}`}
@@ -341,12 +382,16 @@ function Editor({
window.addEventListener("popstate", restore);
return () => window.removeEventListener("popstate", restore);
}, []);
const modes = mlx || catalog.runtime.interface === "websocket" ? ["serving"] : ["sdk", "inference", "serving"];
const videoInput = catalog.workload === "v2v";
const modes =
mlx || catalog.runtime.interface === "websocket" || videoInput ? ["serving"] : ["sdk", "inference", "serving"];
const modeNote = mlx
? "Apple Silicon runs through the native MLX server in this recipe. There is no CUDA CLI or Python SDK launch for this deployment."
: catalog.runtime.interface === "websocket"
? "This deployment uses the native WebSocket server. Only serving is recorded for it."
: "";
: videoInput
? "This deployment needs a draft video. Send it to the server as video_reference; generated CLI and Python SDK configs cannot carry it yet."
: "";
const advancedGroups = groups.filter(
(group) => group === "server" || group.includes("parallelism") || group.startsWith("streaming"),
);
@@ -560,7 +605,11 @@ function Editor({
</a>
</div>
</div>
<RecipeFacts validity={failure ? "invalid" : result ? "valid" : "loading"} fit="unknown" evidence="none" />
<RecipeFacts
validity={failure ? "invalid" : result ? "valid" : "loading"}
memory={memory}
retryMemory={capacityModel.status === "error" ? capacityModel.retry : undefined}
/>
</MatrixRow>
{catalog.workload === "i2v" && runMode !== "serving" && (
<MatrixRow title="Inputs">
@@ -581,7 +630,7 @@ function Editor({
}}
/>
<p id={`${sourceImageId}-hint`} className="matrix-note">
Path to an image on the machine running FastVideo. Included as request.inputs.image_path.
Local path to the first frame. FastVideo reads it on the machine that runs the command.
</p>
</div>
</MatrixRow>
@@ -671,6 +720,11 @@ function MatrixRow({ title, children }: { title: string; children: ReactNode })
);
}
function deploymentFromUrl() {
const requested = new URL(window.location.href).searchParams.get("deployment");
return requested === "fasth3-omniref/cuda-rest" ? "fasth3-omniref/rtx5090-rest" : requested;
}
export default function Cookbook({ modelId }: { modelId?: string }) {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
@@ -693,7 +747,7 @@ export default function Cookbook({ modelId }: { modelId?: string }) {
throw new Error("No cookbook deployments are available. Check the catalog and retry.");
}
setIndex(data);
const requested = new URL(window.location.href).searchParams.get("deployment");
const requested = deploymentFromUrl();
const requestedModel = data.models.find((model) =>
model.deployments.some((deployment) => deployment.id === requested),
);
@@ -739,7 +793,7 @@ export default function Cookbook({ modelId }: { modelId?: string }) {
}
useEffect(() => {
const restore = () => {
const requested = new URL(window.location.href).searchParams.get("deployment");
const requested = deploymentFromUrl();
const available = index?.models.find((item) => item.id === modelId)?.deployments ?? [];
const next = available.find((deployment) => deployment.id === requested)?.id ?? available[0]?.id;
if (!next) return;
@@ -806,11 +860,8 @@ export default function Cookbook({ modelId }: { modelId?: string }) {
}
const GPU_COUNT_PATH = "generator.engine.num_gpus";
const SINGLE_GPU_FORM_FACTOR = /desktop|laptop|workstation|system|soc/;
/** Desktop and workstation cards, DGX Spark, and Macs are one-GPU machines; datacenter parts keep the recipe default. */
function singleGpuDevice(record: { form_factor?: string }) {
return SINGLE_GPU_FORM_FACTOR.test(record.form_factor ?? "");
function singleGpuDevice(record: { form_factor?: string; kind?: string }) {
return record.kind === "system" || record.form_factor === "soc" || record.form_factor === "laptop";
}
/** Returns new edits where every fixed parallel size that no longer divides `gpus` becomes automatic. */
@@ -833,13 +884,19 @@ function deploymentLabel(deployment: DeploymentChoice, deployments: DeploymentCh
}
/** Mirrors the editor: MLX and WebSocket deployments offer serving only. */
function modelModes(index: Index | undefined, model: DirectoryModel): string[] | undefined {
const modes = model.configurable ? configurableModes(index, model.id) : exampleModes(model);
const hasMlx = model.recipes.some((recipe) => recipe.gpu_types.includes("Apple Silicon"));
if (hasMlx && modes && !modes.some((mode) => /mlx/i.test(mode))) return [...modes, "MLX"];
return modes;
}
function configurableModes(index: Index | undefined, modelId: string): string[] | undefined {
const deployments = index?.models.find((model) => model.id === modelId)?.deployments;
if (!deployments?.length) return undefined;
const servingOnly = (runtime: string) => runtime.includes("mlx") || runtime.includes("streaming");
return deployments.some((deployment) => !servingOnly(deployment.runtime))
? ["Python SDK", "CLI", "Serving"]
: ["Serving"];
const servingOnly = (deployment: { runtime: string; workload: string }) =>
deployment.runtime.includes("mlx") || deployment.runtime.includes("streaming") || deployment.workload === "v2v";
return deployments.some((deployment) => !servingOnly(deployment)) ? ["Python SDK", "CLI", "Serving"] : ["Serving"];
}
/** Mirrors the source recipe page: Python SDK and CLI always, serving when a recipe declares it. */
@@ -847,8 +904,20 @@ function exampleModes(model: DirectoryModel): string[] {
return ["Python SDK", "CLI", ...(model.recipes.some((recipe) => recipe.serving) ? ["Serving"] : [])];
}
const FASTH3_SECTION = new Set(["fasth3-omniref", "fasth3-8step", "fasth3-4step", "fasth3-trim"]);
/** Rest state shows identity only; hover or keyboard focus opens a curtain over the details. */
function RecipeCard({ model, href, modes }: { model: DirectoryModel; href: string; modes?: string[] }) {
function RecipeCard({
model,
href,
modes,
badge,
}: {
model: DirectoryModel;
href: string;
modes?: string[];
badge?: string;
}) {
const [focused, setFocused] = useState(false);
const details = useRef<HTMLDivElement>(null);
const [restOffset, setRestOffset] = useState(72);
@@ -869,6 +938,7 @@ function RecipeCard({ model, href, modes }: { model: DirectoryModel; href: strin
<img src={sitePath(`/assets/logos/${model.logo}.webp`)} alt="" width="40" height="40" />
<span className="recipe-card-kicker">{model.provider}</span>
{model.featured ? <span className="recipe-featured">Featured</span> : null}
{badge ? <span className="recipe-featured recipe-new">{badge}</span> : null}
</div>
<CardCurtainRevealTitle
as={model.configurable ? "h3" : "h4"}
@@ -932,6 +1002,7 @@ function RecipeDirectory({ index, error, retry }: { index?: Index; error: string
};
const visible = modelDirectory.filter(
(model) =>
!model.hidden &&
(category === "All models" || model.category === category) &&
(family === "All families" || model.family === family) &&
(task === "All tasks" || model.task === task) &&
@@ -947,9 +1018,10 @@ function RecipeDirectory({ index, error, retry }: { index?: Index; error: string
setRuntime("all");
setTask("All tasks");
}
const configurable = visible.filter((model) => model.configurable);
const examples = visible.filter((model) => !model.configurable);
const exampleFamilies = [...new Set(examples.map((model) => model.family))];
const fastH3 = visible.filter((model) => FASTH3_SECTION.has(model.id));
const recent = visible.filter((model) => !FASTH3_SECTION.has(model.id) && recentlyAdded(model.model));
const rest = visible.filter((model) => !FASTH3_SECTION.has(model.id) && !recentlyAdded(model.model));
const families = [...new Set(rest.map((model) => model.family))];
const filtered =
!!query.trim() ||
category !== "All models" ||
@@ -957,7 +1029,7 @@ function RecipeDirectory({ index, error, retry }: { index?: Index; error: string
runtime !== "all" ||
task !== "All tasks";
const categoryCount = (value: string) =>
modelDirectory.filter((model) => value === "All models" || model.category === value).length;
modelDirectory.filter((model) => !model.hidden && (value === "All models" || model.category === value)).length;
return (
<MotionConfig reducedMotion="user">
<div className="cookbook not-prose recipe-directory">
@@ -991,7 +1063,6 @@ function RecipeDirectory({ index, error, retry }: { index?: Index; error: string
<div className="recipe-directory-subbar">
<p role="status">
{visible.length} {visible.length === 1 ? "model" : "models"}
{configurable.length ? ` · ${configurable.length} configurable` : ""}
{runtime !== "all" ? ` · ${runtime === "mlx" ? "MLX" : "CUDA"} only` : ""}
{task !== "All tasks" ? ` · ${task}` : ""}
{filtered && visible.length ? (
@@ -1043,59 +1114,62 @@ function RecipeDirectory({ index, error, retry }: { index?: Index; error: string
</Button>
</div>
) : null}
{configurable.length ? (
<section className="recipe-section" aria-labelledby="recipe-configurable-heading">
{fastH3.length ? (
<section className="recipe-section" aria-labelledby="recipe-fasth3-heading">
<header className="recipe-section-head">
<h2 id="recipe-configurable-heading">Configurable recipes</h2>
<p>Edit settings, then copy one config for the Python SDK, the CLI, or an OpenAI-compatible server.</p>
<h2 id="recipe-fasth3-heading">FastVideo FastH3</h2>
</header>
<div className="recipe-card-grid recipe-card-grid-configurable">
{configurable.map((model) => (
{fastH3.map((model) => (
<RecipeCard key={model.id} model={model} href={sitePath(model.path)} modes={modelModes(index, model)} />
))}
</div>
</section>
) : null}
{recent.length ? (
<section className="recipe-section" aria-labelledby="recipe-new-heading">
<header className="recipe-section-head">
<h2 id="recipe-new-heading">New</h2>
</header>
<div className="recipe-card-grid recipe-card-grid-configurable">
{recent.map((model) => (
<RecipeCard
key={model.id}
model={model}
href={sitePath(model.path)}
modes={configurableModes(index, model.id)}
modes={modelModes(index, model)}
badge="New"
/>
))}
</div>
</section>
) : null}
{examples.length ? (
<section className="recipe-section" aria-labelledby="recipe-examples-heading">
<header className="recipe-section-head">
<h2 id="recipe-examples-heading">Example recipes</h2>
<p>
Maintained example scripts, grouped by model family. Adjust their documented settings and run them as
written.
</p>
</header>
{exampleFamilies.map((name) => {
const members = examples.filter((model) => model.family === name);
return (
<div className="recipe-family-group" key={name}>
<div className="recipe-family-head">
<h3>{name}</h3>
<span>
{members.length} {members.length === 1 ? "recipe" : "recipes"}
</span>
<a href={sitePath(members[0].familyPath)}>Family guide →</a>
</div>
<div className="recipe-card-grid">
{members.map((model) => (
<RecipeCard
key={model.id}
model={model}
href={sitePath(model.path)}
modes={exampleModes(model)}
/>
))}
</div>
{families.map((name) => {
const members = rest.filter((model) => model.family === name);
return (
<section className="recipe-section" key={name} aria-label={name}>
<div className="recipe-family-group">
<div className="recipe-family-head">
<h3>{name}</h3>
<span>
{members.length} {members.length === 1 ? "recipe" : "recipes"}
</span>
<a href={sitePath(members[0].familyPath)}>Family guide →</a>
</div>
);
})}
</section>
) : null}
<div className="recipe-card-grid">
{members.map((model) => (
<RecipeCard
key={model.id}
model={model}
href={sitePath(model.path)}
modes={modelModes(index, model)}
/>
))}
</div>
</div>
</section>
);
})}
{!visible.length ? (
<div className="recipe-empty">
<h2>No matching recipes</h2>
+3 -3
View File
@@ -14,7 +14,7 @@ export default function FastModels() {
models: group.models.filter(
(model) =>
(family === "All models" || family === group.name) &&
`${group.name} ${group.summary} ${model.id}`.toLowerCase().includes(query.trim().toLowerCase()),
`${group.name} ${model.id}`.toLowerCase().includes(query.trim().toLowerCase()),
),
}))
.filter((group) => group.models.length);
@@ -48,7 +48,7 @@ export default function FastModels() {
<header className="model-catalog-heading">
<div>
<h2 id="checkpoint-catalog">Browse checkpoints</h2>
<p>Find the export your runtime needs.</p>
<p>Pick the checkpoint that matches your runtime.</p>
</div>
<a href="https://huggingface.co/collections/FastVideo/fastvideo-fasth3">
Earlier FastH3 releases <span aria-hidden="true">↗</span>
@@ -67,7 +67,7 @@ export default function FastModels() {
<input
id={id}
type="search"
placeholder="Name, precision, or runtime…"
placeholder="Name, family, or precision…"
value={query}
onChange={(event) => setQuery(event.target.value)}
/>
+5 -4
View File
@@ -4,17 +4,18 @@ import { modelDirectory, sitePath } from "@/lib/cookbook-pages";
export default function RecipeFamily({ family }: { family: string }) {
const models = modelDirectory.filter(
(model) =>
model.recipes.some((recipe) => recipe.family === family) ||
(family === "minimax_h3" && model.family === "FastH3"),
!model.hidden &&
(model.recipes.some((recipe) => recipe.family === family) ||
(family === "minimax_h3" && (model.family === "FastH3" || model.familyPath === "/cookbook/minimax-h3"))),
);
return (
<div className="recipe-directory not-prose">
<a className="recipe-back" href={sitePath("/cookbook")}>
← All models
← All recipes
</a>
<div className="recipe-card-grid">
{models.map((model) => (
<a className="recipe-card" key={model.id} href={sitePath(model.path)}>
<a className="recipe-card recipe-card-static" key={model.id} href={sitePath(model.path)}>
<div className="recipe-card-top">
<img src={sitePath(`/assets/logos/${model.logo}.webp`)} alt="" width="40" height="40" />
<span className="recipe-card-kicker">{model.provider}</span>
+1 -3
View File
@@ -484,9 +484,7 @@ export default function SourceRecipe({ model }: { model: DirectoryModel }) {
<RecipeFacts
validity={failure ? "invalid" : example ? "valid" : "loading"}
validityText={yamlSource ? "YAML parses" : "Example settings applied"}
validityDetail="This page checks that the YAML parses and that edited numeric fields are finite and supported by the example. It does not run the full recipe schema or runtime topology validation."
fit="unknown"
evidence="none"
validityDetail="Checks that the YAML parses and that edited numbers are valid. It does not run the recipe."
subject={yamlSource ? "configuration" : "example"}
/>
</div>
+53
View File
@@ -0,0 +1,53 @@
import { useEffect, useState } from "react";
// @ts-expect-error The shared ES module has no TypeScript declarations.
import { loadCapacityIndex, loadCapacityModel } from "../../docs/js/cookbook-capacity.mjs";
export { assessRecipe, assessWeightsRecipe, weightsFor } from "./memory-assessment";
export type { MemoryAssessment } from "./memory-assessment";
interface CapacityModel {
id: string;
name?: string;
examples?: { deployment_id?: string }[];
reference_runs?: unknown[];
[key: string]: unknown;
}
interface CapacityState {
status: "loading" | "ready" | "unavailable" | "error";
model?: CapacityModel;
error?: string;
}
const CAPACITY_INDEX = `${import.meta.env.BASE_URL.replace(/\/$/, "")}/assets/cookbook-config/capacity/index.json`;
/**
* Load the capacity model for one cookbook entry. Most recipes do not have one;
* the hook stays `unavailable` and the UI says so instead of guessing.
*/
export function useCapacityModel(modelId: string | undefined): CapacityState & { retry: () => void } {
const [state, setState] = useState<CapacityState>({ status: "loading" });
const [attempt, setAttempt] = useState(0);
useEffect(() => {
if (!modelId) {
setState({ status: "unavailable" });
return;
}
const controller = new AbortController();
setState({ status: "loading" });
loadCapacityIndex(new URL(CAPACITY_INDEX, window.location.origin).href, { signal: controller.signal })
.then(async (index: { models: { id: string }[] }) => {
if (!index.models.some((model) => model.id === modelId)) {
if (!controller.signal.aborted) setState({ status: "unavailable" });
return;
}
const model = (await loadCapacityModel(index, modelId, { signal: controller.signal })) as CapacityModel;
if (!controller.signal.aborted) setState({ status: "ready", model });
})
.catch((error: unknown) => {
if (!controller.signal.aborted)
setState({ status: "error", error: error instanceof Error ? error.message : "Unable to load memory data." });
});
return () => controller.abort();
}, [modelId, attempt]);
return { ...state, retry: () => setAttempt((value) => value + 1) };
}
+100 -63
View File
@@ -2,6 +2,15 @@ import sourceCatalog from "../../docs/assets/cookbook-recipes.json" with { type:
/** Presentation metadata only. Deployments and editable fields come from the cookbook backend. */
export const recipePages = [
{
id: "fasth3-preview",
family: "MiniMax H3",
title: "FastH3 Preview V0.2",
mark: "H3",
path: "/cookbook/minimax-h3/fasth3-preview",
description: "The few-step FastH3 preview checkpoint on CUDA or native Apple Silicon MLX.",
tags: ["Text to video", "Audio", "4-step"],
},
{
id: "fasth3-8step",
family: "MiniMax H3",
@@ -20,15 +29,6 @@ export const recipePages = [
description: "The smaller FastH3 model for eight-step video generation with synchronized audio.",
tags: ["Text to video", "Audio", "NVFP4", "8-step"],
},
{
id: "fasth3-preview",
family: "MiniMax H3",
title: "FastH3 Preview V0.2",
mark: "H3",
path: "/cookbook/minimax-h3/fasth3-preview",
description: "The few-step FastH3 preview checkpoint on CUDA or native Apple Silicon MLX.",
tags: ["Text to video", "Audio", "4-step"],
},
{
id: "fasth3-4step",
family: "MiniMax H3",
@@ -41,13 +41,22 @@ export const recipePages = [
{
id: "fasth3-omniref",
family: "MiniMax H3",
title: "FastH3 OmniRef",
title: "FastH3 Omni v1",
mark: "H3",
path: "/cookbook/minimax-h3/fasth3-omniref",
description: "Video and audio from a prompt plus ordered image, video, and audio references.",
tags: ["Reference to video", "Audio", "NVFP4"],
featured: true,
},
{
id: "open-h3-regenerate-2k",
family: "MiniMax H3",
title: "Open-H3-Regenerate-2K",
mark: "H3",
path: "/cookbook/minimax-h3/open-h3-regenerate-2k",
description: "Regenerate a 768P MiniMax-H3 draft at 2560x1440 with a three-step refiner.",
tags: ["Video upscale", "2K", "3-step"],
},
{
id: "fastwan21",
family: "Wan",
@@ -105,12 +114,14 @@ 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" },
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" },
stable_audio: { title: "Stable Audio", path: "stable-audio", logo: "stabilityai", category: "Audio" },
mmaudio: { title: "MMAudio", path: "mmaudio", logo: "fastvideo", category: "Audio" },
@@ -123,20 +134,32 @@ 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",
};
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"],
[/text to audio/, "Text to audio"],
[/text to image/, "Text to image"],
[/image to image/, "Image to image"],
[/reference media to video/, "Reference to video"],
[/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. */
@@ -145,56 +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",
stabilityai: "Stability AI",
nvidia: "NVIDIA",
};
return {
id: backend?.id ?? first.id,
model,
recipes,
title:
backend?.title ??
(model.includes("FastH3-4-step")
? "FastH3 4-Step V1"
: model.includes("FastH3-Preview-v0.2")
? "FastH3 Preview V0.2"
: model === "MiniMaxAI/MiniMax-H3"
? "MiniMax H3"
: first.label.split(" on ")[0]),
family: backend?.family ?? family.title,
familyPath: `/cookbook/${family.path}`,
path: backend?.path ?? `/cookbook/${family.path}/models/${first.id}`,
logo,
provider: providers[logo],
category: family.category,
description: backend?.description ?? first.summary,
tags: backend?.tags ?? [...new Set(recipes.map((recipe) => recipe.task))],
task: taskLabel(backend?.tags[0] ?? first.task),
platforms: [...new Set(recipes.flatMap((recipe) => recipe.gpu_types))],
configurable: !!backend,
featured: backend?.featured ?? false,
};
})
.sort(
(a, b) =>
Number(b.featured) - Number(a.featured) ||
Number(b.family === "MiniMax H3") - Number(a.family === "MiniMax H3") ||
Number(b.configurable) - Number(a.configurable),
);
const mappedModels = [...new Set(sourceRecipes.map((recipe) => recipe.model))].map((model) => {
const recipes = sourceRecipes.filter((recipe) => recipe.model === model);
const first = recipes[0];
const family = familyInfo[first.family];
const backend = recipePages.find((page) => page.id === backendModels[model]);
const ownModel = model.startsWith("FastVideo/Fast");
const logo = ownModel ? "fastvideo" : family.logo;
const providers: Record<string, string> = {
fastvideo: "FastVideo",
minimax: "MiniMax",
pika: "Pika",
"wan-ai": "Wan AI",
ltx: "Lightricks",
"tencent-hunyuan": "Tencent",
kandinsky: "Kandinsky Lab",
"black-forest-labs": "Black Forest Labs",
qwen: "Qwen",
stabilityai: "Stability AI",
nvidia: "NVIDIA",
};
return {
id: backend?.id ?? first.id,
model,
recipes,
title:
backend?.title ??
(model.includes("FastH3-4-step")
? "FastH3 4-Step V1"
: model.includes("FastH3-Preview-v0.2")
? "FastH3 Preview V0.2"
: model === "MiniMaxAI/MiniMax-H3"
? "MiniMax H3"
: first.label.split(" on ")[0]),
family: backend?.family ?? family.title,
familyPath: `/cookbook/${family.path}`,
path: backend?.path ?? `/cookbook/${family.path}/models/${first.id}`,
logo,
provider: providers[logo],
category: family.category,
description: backend?.description ?? first.summary,
tags: backend?.tags ?? [...new Set(recipes.map((recipe) => recipe.task))],
task: taskLabel(backend?.tags[0] ?? first.task),
platforms: [...new Set(recipes.flatMap((recipe) => recipe.gpu_types))],
configurable: !!backend,
hidden: hiddenBackends.has(backend?.id ?? ""),
featured: backend?.featured ?? false,
};
});
const byPage = new Map<string, (typeof mappedModels)[number]>();
for (const entry of mappedModels) {
const existing = byPage.get(entry.id);
if (!existing) {
byPage.set(entry.id, entry);
continue;
}
existing.recipes.push(...entry.recipes.filter((recipe) => !existing.recipes.some((seen) => seen.id === recipe.id)));
existing.platforms = [...new Set([...existing.platforms, ...entry.platforms])];
}
export const modelDirectory = [...byPage.values()].sort(
(a, b) =>
Number(b.featured) - Number(a.featured) ||
Number(b.family === "MiniMax H3") - Number(a.family === "MiniMax H3") ||
Number(b.configurable) - Number(a.configurable),
);
export type DirectoryModel = (typeof modelDirectory)[number];
+171
View File
@@ -0,0 +1,171 @@
// @ts-expect-error The shared ES module has no TypeScript declarations.
import { assessCapacity, normalizeHardware } from "../../docs/js/cookbook-memory.mjs";
import weightsData from "../../docs/assets/cookbook-weights.json" with { type: "json" };
export type MemoryStatus = "fits" | "tight" | "exceeds" | "unsupported" | "unknown";
/** The subset of an `assessCapacity` result the UI shows. */
export interface MemoryAssessment {
status: MemoryStatus;
reason?: string;
requiredGiB?: number;
capacityGiB?: number;
basis?: string;
parts?: Record<string, number>;
checkpointGiB?: number;
notes?: string[];
}
export interface WeightsModel {
parts: Record<string, number>;
alternates?: Record<string, number>;
sources?: string[];
}
const WEIGHTS = weightsData as { models: Record<string, WeightsModel> };
/** Measured checkpoint sizes for recipes that have no full capacity model yet. */
export function weightsFor(modelId: string | undefined): WeightsModel | undefined {
return modelId ? WEIGHTS.models[modelId] : undefined;
}
export interface AssessmentHardware {
id?: string;
backend?: string;
name?: string;
memory_gb?: number;
memory_unit?: string;
memory_type?: string;
usable_budget_gb?: number;
host_ram_gb?: number;
}
const GIB = 1024 ** 3;
/** Decimal GB for a host-memory amount entered in the device's memory unit. */
export function hostMemoryGb(hardware: AssessmentHardware, amount: number | undefined): number | undefined {
if (!amount) return undefined;
return hardware.memory_unit === "GiB" ? (amount * GIB) / 1e9 : amount;
}
export function capacityHardware(hardware: AssessmentHardware) {
return {
...hardware,
memory: {
type: hardware.memory_type,
nominal: { value: hardware.memory_gb, unit: hardware.memory_unit ?? "GB" },
...(hardware.usable_budget_gb
? { usable: { value: hardware.usable_budget_gb, unit: hardware.memory_unit ?? "GB" } }
: {}),
},
host_memory_gb: hostMemoryGb(hardware, hardware.host_ram_gb),
};
}
/** Checkpoint payload is not peak residency: offload, sharding and packing change placement. */
export function assessWeightsRecipe({
weights,
config,
hardware,
budgetGb,
}: {
weights: WeightsModel;
config: Record<string, unknown>;
hardware: AssessmentHardware;
budgetGb?: number;
}): MemoryAssessment {
const generator = (config.generator ?? {}) as {
engine?: { quantization?: { transformer_quant?: string | null }; offload?: Record<string, boolean> };
pipeline?: { components?: Record<string, string | null>; experimental?: Record<string, unknown> };
};
const device = normalizeHardware(capacityHardware(hardware), {
memoryBudgetGiB: budgetGb ? (hardware.memory_unit === "GiB" ? budgetGb : (budgetGb * 1e9) / GIB) : undefined,
});
if (!device)
return {
status: "unknown",
reason:
"Select dedicated GPU memory or unified memory with a valid budget. Host-only system RAM is not GPU capacity.",
};
const parts = { ...weights.parts };
const missing: string[] = [];
const experimental = generator.pipeline?.experimental ?? {};
if (generator.pipeline?.components?.text_encoder_weights) {
delete parts.text_encoder;
missing.push("replacement text encoder");
}
if (generator.engine?.quantization?.transformer_quant) {
delete parts.transformer;
missing.push("transformer at the selected quantization and layer profile");
}
if (experimental.video_decode_backend === "taeh3" || experimental.h3_vae_nvfp4_decoder) {
delete parts.vae;
missing.push(
experimental.video_decode_backend === "taeh3"
? "TAEH3 decoder"
: "VAE with the NVFP4 decoder and retained encoder",
);
}
const checkpointGiB = Object.values(parts).reduce((sum, value) => sum + value, 0);
return {
status: "unknown",
basis: "checkpoint",
checkpointGiB,
capacityGiB: device.capacityGiB,
parts,
reason: missing.length
? `Partial checkpoint data. Missing sizes for ${missing.join("; ")}. Runtime peak memory is not modeled.`
: "Checkpoint sizes are known, but runtime peak memory is not modeled. CPU offload and GPU count change residency, not checkpoint file sizes.",
notes: [
"Activations, temporary buffers, loading peaks and per-rank placement are not included. Frame count and resolution affect these unmodeled allocations.",
...(hardware.memory_type === "unified"
? ["Unified memory is shared by the CPU and GPU. Offloading to the CPU does not create a second memory pool."]
: []),
...(device.budgetAssumed
? ["The budget uses 75% of nominal unified memory because no usable memory budget was supplied."]
: []),
],
};
}
/**
* Run the analytical estimate for the edited configuration. Returns `undefined`
* when no capacity model covers the checkpoint, so the caller can say so.
*/
export function assessRecipe({
model,
example,
hardware,
config,
budgetGb,
hostGb,
}: {
model: Record<string, unknown>;
example: { workload?: string; env?: Record<string, string> };
hardware?: AssessmentHardware;
config?: Record<string, unknown>;
budgetGb?: number;
hostGb?: number;
}): MemoryAssessment | undefined {
if (!config || !hardware?.memory_gb) return undefined;
try {
const result = assessCapacity({
model,
example,
hardware: capacityHardware(hardware),
config,
memoryBudgetGiB: budgetGb ? (hardware.memory_unit === "GiB" ? budgetGb : (budgetGb * 1e9) / GIB) : undefined,
hostMemoryGb: hostMemoryGb(hardware, hostGb),
}) as MemoryAssessment;
return {
status: (result.status ?? "unknown") as MemoryStatus,
reason: result.reason,
requiredGiB: result.requiredGiB ?? (result as MemoryAssessment & { peakGiB?: number }).peakGiB,
capacityGiB: result.capacityGiB,
basis: result.basis,
notes: result.notes,
};
} catch (error) {
return { status: "unknown", reason: error instanceof Error ? error.message : "Memory assessment failed." };
}
}
+45
View File
@@ -0,0 +1,45 @@
const NEWS_HEADINGS = ["news", "recent updates", "what's new", "latest news", "announcements"];
const PROJECT_HEADINGS = [
"awesome work using fastvideo or our research projects",
"projects using fastvideo",
"built on fastvideo",
"awesome work",
];
function normalizeHeading(line) {
return line
.replace(/^#{2,3}\s+/, "")
.replace(/[`*_]/g, "")
.replace(/[^\p{L}\p{N}\s'’-]/gu, "")
.replace(/\s+/g, " ")
.trim()
.toLowerCase();
}
function sectionBody(readme, names) {
const lines = readme.split("\n");
const start = lines.findIndex((line) => /^##\s+/.test(line) && names.includes(normalizeHeading(line)));
if (start < 0) return null;
const rest = lines.slice(start + 1);
const end = rest.findIndex((line) => /^##\s+/.test(line));
return (end < 0 ? rest : rest.slice(0, end)).join("\n").trim();
}
const BULLET = /^-\s+(?:`(\d{4})[/-](\d{2})[/-](\d{2})`:|\*\*(\d{4})[/-](\d{2})[/-](\d{2}):?\*\*:?)\s*(.+)$/;
export function readReadmeSections(readme) {
const news = sectionBody(readme, NEWS_HEADINGS);
const projects = sectionBody(readme, PROJECT_HEADINGS);
if (news === null || projects === null) return { news: null, projects: null };
const items = [];
for (const line of news.split("\n")) {
const match = BULLET.exec(line.trim());
if (!match) continue;
const [, y1, m1, d1, y2, m2, d2, text] = match;
items.push({ date: `${y1 ?? y2}/${m1 ?? m2}/${d1 ?? d2}`, text: text.trim() });
}
return { news: items, projects };
}
export const README_HEADINGS = { news: NEWS_HEADINGS, projects: PROJECT_HEADINGS };
File diff suppressed because one or more lines are too long
+23 -11
View File
@@ -141,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"
},
{
@@ -166,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"
@@ -187,7 +187,7 @@
{
"label": "Wan",
"display": "group",
"collapsed": false,
"collapsed": true,
"items": [
{
"label": "FastWan2.1 1.3B",
@@ -253,6 +253,10 @@
"label": "FLUX",
"href": "/cookbook/flux"
},
{
"label": "Qwen-Image-2.1",
"href": "/cookbook/qwen-image21"
},
{
"label": "Stable Diffusion",
"href": "/cookbook/stable-diffusion"
@@ -306,6 +310,14 @@
"label": "Attn-QAT Training",
"href": "/training/attn_qat"
},
{
"label": "FastH3 NVFP4 QAD",
"href": "/training/minimax_h3_fp4_qad"
},
{
"label": "Pika H3 Flex NVFP4 QAD",
"href": "/training/pika_h3_flex_qad"
},
{
"label": "HunyuanVideo 1.5 T2V",
"href": "/training/hunyuan15_overfit"
+1 -1
View File
@@ -15,7 +15,7 @@
"test": "playwright test",
"format": "prettier --write .",
"format:check": "prettier --check .",
"test:content": "node --test tests/content.test.mjs tests/commands.test.mjs"
"test:content": "node --test tests/content.test.mjs tests/commands.test.mjs tests/readme-sections.test.mjs tests/memory-assessment.test.mjs"
},
"dependencies": {
"blume": "2.2.3",
+11 -6
View File
@@ -6,6 +6,7 @@ import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promi
import { dirname, join, relative, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";
import { parse, stringify } from "yaml";
import { readReadmeSections, README_HEADINGS } from "../lib/readme.mjs";
const site = fileURLToPath(new URL("../", import.meta.url));
const repo = resolve(site, "..");
@@ -176,12 +177,11 @@ export async function transferDocs() {
return (await stat(path)).isDirectory() || /\.mdx?$/.test(path);
},
});
// Native callout directives and islands compile as MDX, while source links keep .md filenames.
for (const file of await files(content)) {
if (!file.endsWith(".md")) continue;
const text = await readFile(file, "utf8");
if (/^:::(?:note|tip|warning|danger|info|success)\b/m.test(text) || text.includes("<RecipeFamily ")) {
await writeFile(file.replace(/\.md$/, ".mdx"), text);
await writeFile(file.replace(/\.md$/, ".mdx"), text.replace(/\{#([^}]+)\}/g, "[#$1]"));
await rm(file);
}
}
@@ -251,10 +251,15 @@ export async function transferDocs() {
// The README owns announcements and the community list; the welcome page renders a short news digest.
const readme = await readFile(join(repo, "README.md"), "utf8");
const news = readme.match(/^## NEWS\n([\s\S]*?)(?=^##|^###)/m)?.[1];
const projects = readme.match(/^## Awesome work using FastVideo or our research projects\n([\s\S]*?)(?=^## )/m)?.[1];
if (!news || !projects) throw new Error("The README news or project section boundary needs updating.");
const recent = [...news.matchAll(/^- `([\d/]+)`: (.+)$/gm)].slice(0, 2).map(([, date, text]) => {
const { news, projects } = readReadmeSections(readme);
if (!news || !projects) {
const accepted = (names) => names.map((name) => `"## ${name.replace(/\b\w/g, (c) => c.toUpperCase())}"`).join(", ");
throw new Error(
`README section not found. Accepted news headings: ${accepted(README_HEADINGS.news)}. ` +
`Accepted project headings: ${accepted(README_HEADINGS.projects)}.`,
);
}
const recent = news.slice(0, 2).map(({ date, text }) => {
const links = [...text.matchAll(/\[([^\]]+)\]\(([^)]+)\)/g)];
const headline = text
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
+1 -1
View File
@@ -31,7 +31,7 @@ test("converted guides render admonitions, preserve content, and publish local i
const home = await readFile(new URL("dist/index.html", site), "utf8");
assert.ok(home.includes("Welcome to FastVideo"));
assert.ok(home.includes("/FastVideo/assets/logos/logo.svg"));
assert.ok(home.includes("FastVideo is an inference and post-training framework for diffusion models."));
assert.ok(home.includes("FastVideo is an inference and post-training framework for video diffusion models."));
assert.ok(!home.includes("About FastVideo and performance overview"));
await readFile(new URL("dist/assets/logos/logo.svg", site));
});
+292 -43
View File
@@ -223,18 +223,18 @@ test("inherited values, explicit false, zero and null retain their meanings", as
test("catalog discovery opens dedicated model recipes and filters by family and task", async ({ page }) => {
await page.goto("/FastVideo/cookbook");
const cards = page.locator(".recipe-card");
await expect(cards).toHaveCount(36);
await expect(cards.first()).toContainText("FastH3 OmniRef");
await expect(cards).toHaveCount(38);
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");
@@ -277,7 +276,7 @@ test("output tabs support keyboard navigation and precision chips use the backen
await page.getByLabel("Search recipes").fill("does not exist");
await expect(page.getByRole("heading", { name: "No matching recipes" })).toBeVisible();
await page.getByRole("button", { name: "Clear filters" }).click();
await expect(page.locator(".recipe-card")).toHaveCount(36);
await expect(page.locator(".recipe-card")).toHaveCount(38);
});
test("cookbook cards and controls remain readable in dark mode and reflow at 320px", async ({ page }) => {
@@ -297,7 +296,7 @@ test("cookbook cards and controls remain readable in dark mode and reflow at 320
test("image and audio models have logos and dedicated source recipe pages", async ({ page }) => {
await page.goto("/FastVideo/cookbook");
await page.getByRole("button", { name: "Image", exact: true }).click();
await expect(page.locator(".recipe-card")).toHaveCount(4);
await expect(page.locator(".recipe-card")).toHaveCount(5);
const card = page.locator(".recipe-card").filter({ hasText: "FLUX.2 Klein" });
await expect(card.locator("img")).toBeVisible();
await card.click();
@@ -309,6 +308,26 @@ test("image and audio models have logos and dedicated source recipe pages", asyn
await expect(page.getByRole("button", { name: "Serving", exact: true })).toHaveCount(0);
await page.getByLabel("Seed", { exact: true }).fill("0");
await expect(page.getByRole("button", { name: "Copy example + command", exact: true })).toBeEnabled();
await page.goto("/FastVideo/cookbook/qwen-image21");
const qwen = page.locator(".recipe-card").filter({ hasText: "Qwen-Image-2.1" });
await expect(qwen.locator("img")).toHaveAttribute("src", /\/qwen\.webp$/);
await qwen.click();
await expect(page.getByRole("heading", { level: 1 })).toHaveText("Qwen-Image-2.1");
await expect(page.getByRole("button", { name: "Serving", exact: true })).toHaveCount(0);
await page.getByLabel("Height", { exact: true }).fill("512");
await page.getByRole("tab", { name: "config.yaml", exact: true }).click();
const qwenConfig = page.getByLabel("Source example", { exact: true });
await expect(qwenConfig).toContainText("model_path: Qwen/Qwen-Image-2.1");
await expect(qwenConfig).toContainText("height: 512");
await expect(qwenConfig).toContainText("dit_layerwise: true");
await expect(page.getByRole("button", { name: "Download YAML", exact: true })).toBeEnabled();
await page
.getByRole("group", { name: "Recipe variant", exact: true })
.getByRole("button", { name: "Qwen-Image-2.1 references and editing", exact: true })
.click();
await page.getByRole("tab", { name: "Commands", exact: true }).click();
await expect(page.getByLabel("Inference command", { exact: true })).toContainText("--reference source.png");
await expect(page.getByRole("button", { name: "Copy example + command", exact: true })).toBeEnabled();
await page.goto("/FastVideo/cookbook");
await page.getByRole("button", { name: "Audio", exact: true }).click();
await expect(page.locator(".recipe-card")).toHaveCount(3);
@@ -375,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");
@@ -442,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/);
@@ -460,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 }) => {
@@ -492,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 }) => {
@@ -502,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);
});
@@ -515,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 }) => {
@@ -532,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();
@@ -552,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 }) => {
@@ -568,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");
@@ -584,19 +815,19 @@ test("directory filters round-trip through the URL", async ({ page }) => {
await page.goto("/FastVideo/cookbook?category=Image&runtime=cuda");
await expect(page.getByRole("button", { name: "Image", exact: true })).toHaveAttribute("aria-pressed", "true");
await expect(page.getByLabel("Runtime", { exact: true })).toHaveValue("cuda");
await expect(page.locator(".recipe-card")).toHaveCount(4);
await expect(page.locator(".recipe-card")).toHaveCount(5);
await page.reload();
await expect(page.getByRole("button", { name: "Image", exact: true })).toHaveAttribute("aria-pressed", "true");
await expect(page.getByLabel("Runtime", { exact: true })).toHaveValue("cuda");
await expect(page.locator(".recipe-card")).toHaveCount(4);
await expect(page.locator(".recipe-card")).toHaveCount(5);
});
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 }) => {
@@ -626,3 +857,21 @@ test("WebSocket deployments expose only the serving interface", async ({ page })
await expect(modes.getByRole("button", { name: "CLI", exact: true })).toHaveCount(0);
await expect(page.getByText(/Only serving is recorded for it/)).toBeVisible();
});
test("video-to-video deployments expose only the serving interface", async ({ page }) => {
await page.goto("/FastVideo/cookbook/minimax-h3/open-h3-regenerate-2k");
const modes = page.getByRole("group", { name: "Run mode" });
await expect(modes.getByRole("button")).toHaveCount(1);
await expect(modes.getByRole("button", { name: "Serving", exact: true })).toHaveAttribute("aria-pressed", "true");
await expect(page.getByText(/Send it to the server as video_reference/)).toBeVisible();
});
test("Open-H3-Regenerate-2K is discoverable as a video upscale with its serving page", async ({ page }) => {
await page.goto("/FastVideo/cookbook");
await page.getByLabel("Task", { exact: true }).selectOption("Video upscale");
const card = page.locator(".recipe-card").filter({ hasText: "Open-H3-Regenerate-2K" });
await expect(card).toHaveCount(1);
await card.click();
await expect(page).toHaveURL(/cookbook\/minimax-h3\/open-h3-regenerate-2k\/?$/);
await expect(page.getByRole("heading", { level: 1 })).toHaveText("Open-H3-Regenerate-2K");
});
+21 -1
View File
@@ -1,5 +1,19 @@
import { expect, test } from "@playwright/test";
test("Omni Apple silicon links open the published guide and MLX recipe", async ({ page }) => {
for (const slash of ["", "/"]) {
await page.goto(`/FastVideo/cookbook/minimax-h3/fasth3-omniref${slash}`);
const guide = page.getByRole("link", { name: "FastH3 Omni on MLX", exact: true });
await expect(guide).toHaveAttribute("href", "/FastVideo/inference/fasth3_omniref_mlx");
await guide.click();
await expect(page).toHaveURL(/\/FastVideo\/inference\/fasth3_omniref_mlx\/?$/);
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
}
await page.goto("/FastVideo/cookbook/minimax-h3/fasth3-omniref");
await page.getByRole("link", { name: "MLX recipe", exact: true }).click();
await expect(page.getByLabel("Inference command")).toContainText("--mlx-checkpoint");
});
test("GitHub stars refresh automatically and survive a failed refresh", async ({ page }) => {
await page.clock.install();
let requests = 0;
@@ -159,6 +173,12 @@ test("the model catalog filters by family and checkpoint name", async ({ page })
await expect(catalog.getByRole("status")).toHaveText("3 checkpoints");
await page.getByRole("searchbox", { name: "Search checkpoints", exact: true }).fill("14B");
await expect(catalog.getByRole("status")).toHaveText("1 checkpoint");
await page.getByRole("button", { name: "All models", exact: true }).click();
await page.getByRole("searchbox").fill("NVFP4");
const codes = await catalog.locator(".model-checkpoints code").allInnerTexts();
expect(codes.length).toBeGreaterThan(0);
expect(codes.every((text) => text.includes("NVFP4"))).toBe(true);
await expect(catalog.getByText("FastVideo/FastVideo-FastH3-8-Step-V2-FP8", { exact: true })).toHaveCount(0);
await page.getByRole("searchbox").fill("no-such-model");
await expect(catalog.getByRole("status")).toHaveText("0 checkpoints");
await page.getByRole("button", { name: "Clear filters", exact: true }).click();
@@ -178,7 +198,7 @@ test("family runbooks use React model cards without the old cookbook runtime", a
await page.goto("/FastVideo/cookbook/wan/");
await expect(page.locator(".recipe-card").first()).toBeVisible();
await expect(page.getByRole("button", { name: "Search", exact: true })).toBeVisible();
await expect(page.getByRole("link", { name: "← All models", exact: true })).toHaveAttribute(
await expect(page.getByRole("link", { name: "← All recipes", exact: true })).toHaveAttribute(
"href",
/\/FastVideo\/cookbook$/,
);
@@ -0,0 +1,95 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
assessRecipe,
assessWeightsRecipe,
capacityHardware,
hostMemoryGb,
weightsFor,
} from "../lib/memory-assessment.ts";
import { buildCapacityCatalog } from "../../docs/js/build-cookbook-capacity.mjs";
import { assessCapacity } from "../../docs/js/cookbook-memory.mjs";
const hardware = { backend: "cuda", memory_gb: 80, memory_unit: "GiB", memory_type: "dedicated" };
const weights = weightsFor("fasth3-8step");
const config = {
generator: {
engine: { num_gpus: 4, offload: { text_encoder: true, vae: true } },
pipeline: { experimental: {} },
},
};
test("checkpoint data never certifies runtime fit or subtracts CPU-offloaded weights", () => {
for (const unified of [false, true]) {
const target = { ...hardware, memory_type: unified ? "unified" : "dedicated", usable_budget_gb: 60 };
const baseline = assessWeightsRecipe({ weights, config, hardware: target });
const edited = structuredClone(config);
edited.generator.engine.offload.text_encoder = false;
edited.generator.engine.num_gpus = 1;
const result = assessWeightsRecipe({ weights, config: edited, hardware: target });
assert.equal(result.status, "unknown");
assert.ok(Math.abs(result.capacityGiB - 60) < 1e-9);
assert.deepEqual(result.parts, weights.parts);
assert.equal(result.checkpointGiB, baseline.checkpointGiB);
assert.equal(result.requiredGiB, undefined);
}
});
test("a component swap preserves other known sizes and identifies missing coverage", () => {
for (const swap of ["taeh3", "nvfp4-decoder", "nvfp4-transformer", "encoder"]) {
const edited = structuredClone(config);
if (swap === "taeh3") edited.generator.pipeline.experimental.video_decode_backend = "taeh3";
if (swap === "nvfp4-decoder") edited.generator.pipeline.experimental.h3_vae_nvfp4_decoder = "decoder.safetensors";
if (swap === "nvfp4-transformer") edited.generator.engine.quantization = { transformer_quant: "NVFP4" };
if (swap === "encoder") edited.generator.pipeline.components = { text_encoder_weights: "replacement" };
const result = assessWeightsRecipe({ weights, config: edited, hardware });
const missingRole =
swap.includes("decoder") || swap === "taeh3" ? "vae" : swap === "encoder" ? "text_encoder" : "transformer";
assert.equal(result.status, "unknown");
assert.match(result.reason, /Partial checkpoint data/);
assert.equal(result.parts[missingRole], undefined);
assert.equal(result.parts.audio_vae, weights.parts.audio_vae);
assert.equal(
result.checkpointGiB,
Object.values(result.parts).reduce((sum, size) => sum + size, 0),
);
}
});
test("host RAM is rejected and unified memory gets one budget", () => {
assert.equal(
assessWeightsRecipe({ weights, config, hardware: { ...hardware, memory_type: "system" } }).parts,
undefined,
);
const result = assessWeightsRecipe({ weights, config, hardware: { ...hardware, memory_type: "unified" } });
assert.equal(result.capacityGiB, 60);
assert.ok(result.notes.some((note) => note.includes("75%")));
});
test("the UI adapter preserves GB, GiB, usable budgets and host memory through the analytical engine", () => {
const model = buildCapacityCatalog().models.find((model) => model.id === "fastwan21");
const example = { workload: "t2v", env: { FASTVIDEO_ATTENTION_BACKEND: "TORCH_SDPA" } };
const dense = structuredClone(model.examples[0].base_config);
dense.generator.engine.compile.enabled = false;
dense.generator.pipeline.experimental = {};
for (const unit of ["GB", "GiB"]) {
const target = { ...hardware, memory_unit: unit, usable_budget_gb: 70, host_ram_gb: 128 };
const result = assessRecipe({ model, example, config: dense, hardware: target });
const raw = assessCapacity({ model, example, config: dense, hardware: capacityHardware(target) });
assert.equal(result.status, raw.status);
assert.notEqual(result.status, "unknown");
assert.equal(result.requiredGiB, raw.requiredGiB);
assert.equal(result.capacityGiB, unit === "GiB" ? 70 : 70e9 / 1024 ** 3);
const hostGiB = unit === "GiB" ? 128 : 128e9 / 1024 ** 3;
assert.ok(Math.abs(raw.host.capacityGiB - hostGiB) < 1e-9);
const { host_ram_gb: _ram, ...withoutHost } = target;
const edited = assessCapacity({
model,
example,
config: dense,
hardware: capacityHardware(withoutHost),
hostMemoryGb: hostMemoryGb(target, 128),
});
assert.ok(Math.abs(edited.host.capacityGiB - hostGiB) < 1e-9);
}
});
+64
View File
@@ -0,0 +1,64 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { readReadmeSections } from "../lib/readme.mjs";
const legacy = `# FastVideo
## NEWS
- \`2026/10/06\`: FastH3 V2 runs on a single consumer machine.
- \`2026/09/15\`: Release FastH3 8-Step V2.
## Awesome work using FastVideo or our research projects
- [Some project](https://example.com)
## Community
- Slack
`;
const rewritten = `# FastVideo
## Overview
FastVideo is a framework.
## Recent updates
- **2026-10-06:** FastH3 V2 runs on a single consumer machine.
- **2026-09-15:** Release FastH3 8-Step V2.
## Projects using FastVideo
- [Some project](https://example.com)
## Community and contributing
- Slack
`;
test("reads the legacy README headings and dates", () => {
const { news, projects } = readReadmeSections(legacy);
assert.equal(news.length, 2);
assert.deepEqual(
news.map((item) => item.date),
["2026/10/06", "2026/09/15"],
);
assert.match(news[0].text, /^FastH3 V2/);
assert.match(projects, /Some project/);
});
test("reads a rewritten README with sentence case headings and bold dates", () => {
const { news, projects } = readReadmeSections(rewritten);
assert.equal(news?.length, 2);
assert.deepEqual(
news.map((item) => item.date),
["2026/10/06", "2026/09/15"],
);
assert.doesNotMatch(news[0].text, /\*\*/);
assert.match(projects, /Some project/);
assert.doesNotMatch(projects, /Slack/);
});
test("stops at the next heading and ignores other sections", () => {
const { news } = readReadmeSections(legacy);
assert.doesNotMatch(news[1].text, /Awesome work/);
});
test("returns null when a section is missing", () => {
assert.deepEqual(readReadmeSections("# FastVideo\n\n## Overview\n"), { news: null, projects: null });
});
+77
View File
@@ -798,6 +798,39 @@
.recipe-card-grid-configurable .recipe-card {
border-block-start: 2px solid var(--blume-accent);
}
/* Family runbook cards are static: no curtain, so they need their own rhythm. */
.recipe-card-static {
display: flex;
flex-direction: column;
gap: 0.5rem;
min-block-size: 13.5rem;
padding: 1.25rem 1.25rem 1rem;
}
.recipe-card-static .recipe-card-top {
margin-block-end: 0;
padding-block-end: 0;
}
.recipe-card-static h2 {
margin: 0;
font-size: 1rem;
line-height: 1.35;
font-weight: 600;
letter-spacing: -0.01em;
}
.recipe-card-static footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
margin-block-start: auto;
margin-inline: 0;
padding-block: 0.75rem 0;
border-block-start: 1px solid var(--recipe-line);
font-size: 0.6875rem;
}
.recipe-card-static footer span:first-child {
color: var(--blume-muted-foreground);
}
.recipe-empty {
padding-block: 2rem;
text-align: center;
@@ -953,6 +986,28 @@
.recipe-facts-detail p + p {
margin-block-start: 0.375rem;
}
.recipe-facts > .matrix-note,
.recipe-memory-breakdown {
flex-basis: 100%;
}
.recipe-memory-breakdown {
color: var(--blume-muted-foreground);
font-size: 0.6875rem;
line-height: 1.65;
}
.recipe-memory-breakdown dl {
margin-block-start: 0.5rem;
max-inline-size: 26rem;
}
.recipe-memory-breakdown dl > div {
display: flex;
justify-content: space-between;
gap: 1rem;
}
.recipe-memory-breakdown dd {
margin: 0;
font-variant-numeric: tabular-nums;
}
.recipe-mode-note {
margin: -0.25rem 0 1rem;
color: var(--blume-muted-foreground);
@@ -1622,3 +1677,25 @@
.recipe-request-code summary {
padding-block: 0.25rem;
}
/* Article rhythm.
Blume's prose gives <details> no vertical margin, so consecutive cookbook
sections touch. Tables inherit the body line height, which makes dense cells
taller than the prose around them. Both are set here, once, instead of in
each page's markup. */
.prose details {
margin-block: 1rem;
}
.prose details + details {
margin-top: 1rem;
}
.prose summary {
cursor: pointer;
}
.prose table {
line-height: 1.5;
}
.prose td,
.prose th {
vertical-align: top;
}
+403 -1
View File
@@ -1,5 +1,5 @@
{
"version": 18,
"version": 20,
"recipes": [
{
"id": "fastwan21-t2v",
@@ -429,6 +429,44 @@
"expected_artifact": "PNG images under outputs/flux_dev/samples/",
"limitations": ["FLUX.1 is loadable by ID but registers no model_family in fastvideo/registry.py; it is grouped under FLUX for documentation only."]
},
{
"id": "qwen-image21-t2i",
"family": "qwen_image21",
"stage": "inference",
"task": "Text to image",
"label": "Qwen-Image-2.1",
"summary": "Generate a 1024 × 1024 image in 40 steps on one CUDA GPU, with the CPU offload settings recorded in the maintained YAML.",
"model": "Qwen/Qwen-Image-2.1",
"source": "examples/inference/basic/qwen_image21_t2i.yaml",
"command": "fastvideo generate --config examples/inference/basic/qwen_image21_t2i.yaml",
"gpu_types": ["NVIDIA"],
"hardware": {"platform": "cuda", "gpu_count": 1, "evidence": "source-configured"},
"evidence": "Source-backed",
"expected_artifact": "RGBA PNG image at outputs/qwen_image21/teapot.png",
"limitations": ["The source uses DiT layerwise offload, text-encoder and VAE offload, and lazy loading. GPU memory requirements have not been established for this recipe."],
"related": ["qwen-image21-i2i"]
},
{
"id": "qwen-image21-i2i",
"family": "qwen_image21",
"stage": "inference",
"task": "Image to image",
"label": "Qwen-Image-2.1 references and editing",
"summary": "Generate or edit an image from a prompt and up to ten ordered reference images, using the maintained single-GPU Python example.",
"model": "Qwen/Qwen-Image-2.1",
"source": "examples/inference/basic/basic_qwen_image21.py",
"command": "python examples/inference/basic/basic_qwen_image21.py --reference source.png --prompt 'Change the shirt to blue; retain the subject and composition.' --output outputs/qwen_image21/edit.png",
"gpu_types": ["NVIDIA"],
"hardware": {"platform": "cuda", "gpu_count": 1, "evidence": "source-configured"},
"evidence": "Source-backed",
"expected_artifact": "RGBA PNG image at outputs/qwen_image21/edit.png",
"limitations": [
"Replace source.png with an existing image. Repeat --reference for additional images and describe their order in the prompt.",
"Mask images use the same ordered-reference path; they do not impose hard pixel-preservation constraints.",
"The source enables CPU offload and lazy loading. This entry does not establish a minimum GPU memory requirement."
],
"related": ["qwen-image21-t2i"]
},
{
"id": "sd35-medium-t2i",
"family": "sd35",
@@ -722,6 +760,370 @@
],
"limitations": ["Pass --reference-audio for an additional audio reference. The checked-in source defaults to four GPUs."]
},
{
"id": "pika-h3-flex-t2va",
"family": "pika_h3_flex",
"stage": "inference",
"task": "Text to video (with audio)",
"label": "Pika H3 Flex",
"summary": "Pika's post-trained MiniMax-H3: synchronized video and stereo audio from text in six forwards, at 480p or 768p, 5-15 s, in BF16 or as FastVideo's NVFP4 QAD release on Blackwell, with optional FP4 attention.",
"model": "Pika-Labs/Pika-H3-Flex",
"source": "examples/inference/basic/basic_pika_h3_flex.py",
"command": "FASTVIDEO_ATTENTION_BACKEND=FLASH_ATTN FASTVIDEO_FA4=1 python examples/inference/basic/basic_pika_h3_flex.py --task t2va --offload resident --prompt 'A paper boat drifts down a rainy city street at night; rain patters on the water.'",
"gpu_types": ["NVIDIA"],
"hardware": {
"platform": "cuda",
"gpu_count": 1,
"accelerator": "NVIDIA GB200",
"peak_memory": "About 129 GiB resident (BF16, 768p, 5 s)",
"evidence": "validated",
"evidence_url": "https://github.com/hao-ai-lab/FastVideo/pull/2038"
},
"evidence": "Verified",
"expected_artifact": "MP4 with synchronized stereo audio at outputs/pika_h3_flex/pika_h3_flex_t2va.mp4",
"modes": ["T2VA"],
"knobs": [
{
"key": "weights",
"label": "Weights",
"hint": "NVFP4 QAD needs Blackwell",
"flag": "--weights",
"options": [{"value": "bf16", "label": "BF16 (Pika release)"}, {"value": "nvfp4-qad", "label": "NVFP4 QAD"}, {"value": "nvfp4-qad-fp4-attention", "label": "NVFP4 QAD + FP4 attention"}],
"default": "bf16"
},
{
"key": "resolution",
"label": "Resolution",
"hint": "Pika resolution tier",
"flag": "--resolution",
"options": [{"value": "768p", "label": "768p"}, {"value": "480p", "label": "480p"}],
"default": "768p"
},
{
"key": "aspect",
"label": "Aspect ratio",
"hint": "Bucket within the tier",
"flag": "--aspect",
"options": ["21:9", "16:9", "3:2", "4:3", "5:4", "1:1", "4:5", "3:4", "2:3", "9:16", "9:21"],
"default": "16:9"
},
{
"key": "duration",
"label": "Duration",
"hint": "Rounded up to the 17n+5 frame grid",
"flag": "--duration",
"options": [
{"value": 5, "label": "5 s (124 frames)"},
{"value": 10, "label": "10 s (243 frames)"},
{"value": 15, "label": "15 s (362 frames)"}
],
"default": 5
},
{
"key": "nvfp4_decoder",
"label": "VAE decoder",
"hint": "NVFP4 needs Blackwell",
"flag": "--nvfp4-decoder",
"options": [
{"value": "", "label": "BF16 H3 VAE"},
{"value": "./fasth3-nvfp4-vae/quality-36L/decoder-nvfp4.safetensors", "label": "NVFP4 36L quality"},
{"value": "./fasth3-nvfp4-vae/fast-8L/decoder-nvfp4.safetensors", "label": "NVFP4 8L fast"}
],
"default": ""
},
{
"key": "nvfp4_text_encoder",
"label": "Text encoder",
"hint": "NVFP4 needs Blackwell",
"flag": "--nvfp4-text-encoder",
"options": [
{"value": "", "label": "BF16 Qwen3-VL"},
{"value": "./fasth3-nvfp4-encoder/text_encoder", "label": "NVFP4 Qwen3-VL"}
],
"default": ""
},
{
"key": "offload",
"label": "Memory",
"hint": "Resident is how the GB200 timings were measured",
"flag": "--offload",
"options": [
{"value": "resident", "label": "All resident (fastest)"},
{"value": "lazy", "label": "Lazy load + offload"}
],
"default": "resident"
},
{
"key": "num_gpus",
"label": "GPUs",
"hint": "Sequence-parallel degree",
"flag": "--num-gpus",
"options": [1, 2, 4, 8],
"default": 1
}
],
"limitations": [
"NVFP4 QAD release (--weights), one GB200, 768p 16:9, 5 s, warm: 16.8 s with BF16 attention and 15.2 s with FP4 attention; at 768p, 15 s, FP4 attention is 16% faster than BF16 attention (85.3 s vs 101.8 s).",
"Measured on one GB200 at 768p 16:9, 5 s, warm: official pika-h3-pro CLI 59.7 s; FastVideo BF16 26.2 s; with the NVFP4 quality-36L decoder 18.0 s; with the fast-8L decoder 17.4 s. The 36L decoder measured 40.5 dB PSNR against the BF16 decoder.",
"Peak resident memory was about 129 GiB for the all-BF16 run and 104.6 GiB with the NVFP4 text encoder (25.49 s) at 768p / 5 s on GB200; the official CLI's sampling peak was 71.7 GiB, a different measurement. The NVFP4 text encoder's first request in a fresh process pays a one-time cost of about 183 s.",
"--offload resident keeps every component on the GPU (104.6-108.5 GiB peak with the NVFP4 text encoder, about 129 GiB all-BF16, on GB200). --offload lazy loads components on demand for smaller devices; on GB200 it took 48.06 s for the same T2VA request.",
"FASTVIDEO_ATTENTION_BACKEND=FLASH_ATTN FASTVIDEO_FA4=1 selects FlashAttention-4 on Hopper or Blackwell; FA4 is never enabled just because it is installed. Without FA4, attention uses FA3/FA2 if installed and otherwise falls back to SDPA, which is slow and can run out of memory at 768p.",
"The NVFP4 decoder and text encoder are Blackwell-only. Download them before selecting them: hf download FastVideo/FastH3-NVFP4-VAE-Decoder --include 'quality-36L/decoder-nvfp4.safetensors' 'fast-8L/decoder-nvfp4.safetensors' --local-dir fasth3-nvfp4-vae; hf download FastVideo/FastVideo-FastH3-8-Step-V2-NVFP4-Consumer --include 'text_encoder/*' --local-dir fasth3-nvfp4-encoder. The 8L decoder is a distilled student; validate it on your prompts.",
"The six-forward re-noise ladder is part of the adapter contract; the example exposes no step-count flag."
],
"related": ["pika-h3-flex-keyframes", "pika-h3-flex-ref2va"]
},
{
"id": "pika-h3-flex-keyframes",
"family": "pika_h3_flex",
"stage": "inference",
"task": "First/last frame to video (with audio)",
"label": "Pika H3 Flex keyframes",
"summary": "Animate from a first frame (I2VA), toward a last frame (L2VA), or between both (FL2VA). Pika center-crops each keyframe to the selected bucket.",
"model": "Pika-Labs/Pika-H3-Flex",
"source": "examples/inference/basic/basic_pika_h3_flex.py",
"command": "FASTVIDEO_ATTENTION_BACKEND=FLASH_ATTN FASTVIDEO_FA4=1 python examples/inference/basic/basic_pika_h3_flex.py --task fl2va --offload resident --image assets/images/natural.jpg --last-image assets/images/peninsula.jpg --prompt 'The camera drifts from the misty mountain ridge down to the coastal cliffs above the sea.'",
"gpu_types": ["NVIDIA"],
"hardware": {
"platform": "cuda",
"gpu_count": 1,
"accelerator": "NVIDIA GB200",
"peak_memory": "105.2 GiB FL2VA, 104.9 GiB I2VA (NVFP4 text encoder, 768p, 5 s)",
"evidence": "validated",
"evidence_url": "https://github.com/hao-ai-lab/FastVideo/pull/2038"
},
"evidence": "Verified",
"expected_artifact": "MP4 with synchronized stereo audio at outputs/pika_h3_flex/pika_h3_flex_<task>.mp4",
"modes": ["I2VA", "L2VA", "FL2VA"],
"knobs": [
{
"key": "weights",
"label": "Weights",
"hint": "NVFP4 QAD needs Blackwell",
"flag": "--weights",
"options": [{"value": "bf16", "label": "BF16 (Pika release)"}, {"value": "nvfp4-qad", "label": "NVFP4 QAD"}, {"value": "nvfp4-qad-fp4-attention", "label": "NVFP4 QAD + FP4 attention"}],
"default": "bf16"
},
{
"key": "task",
"label": "Task",
"hint": "Which keyframes condition the clip",
"flag": "--task",
"options": [
{"value": "i2va", "label": "First frame (I2VA)"},
{"value": "l2va", "label": "Last frame (L2VA)"},
{"value": "fl2va", "label": "First and last (FL2VA)"}
],
"default": "fl2va"
},
{
"key": "resolution",
"label": "Resolution",
"hint": "Pika resolution tier",
"flag": "--resolution",
"options": [{"value": "768p", "label": "768p"}, {"value": "480p", "label": "480p"}],
"default": "768p"
},
{
"key": "aspect",
"label": "Aspect ratio",
"hint": "Bucket within the tier",
"flag": "--aspect",
"options": ["21:9", "16:9", "3:2", "4:3", "5:4", "1:1", "4:5", "3:4", "2:3", "9:16", "9:21"],
"default": "16:9"
},
{
"key": "duration",
"label": "Duration",
"hint": "Rounded up to the 17n+5 frame grid",
"flag": "--duration",
"options": [
{"value": 5, "label": "5 s (124 frames)"},
{"value": 10, "label": "10 s (243 frames)"},
{"value": 15, "label": "15 s (362 frames)"}
],
"default": 5
},
{
"key": "nvfp4_decoder",
"label": "VAE decoder",
"hint": "NVFP4 needs Blackwell",
"flag": "--nvfp4-decoder",
"options": [
{"value": "", "label": "BF16 H3 VAE"},
{"value": "./fasth3-nvfp4-vae/quality-36L/decoder-nvfp4.safetensors", "label": "NVFP4 36L quality"},
{"value": "./fasth3-nvfp4-vae/fast-8L/decoder-nvfp4.safetensors", "label": "NVFP4 8L fast"}
],
"default": ""
},
{
"key": "nvfp4_text_encoder",
"label": "Text encoder",
"hint": "NVFP4 needs Blackwell",
"flag": "--nvfp4-text-encoder",
"options": [
{"value": "", "label": "BF16 Qwen3-VL"},
{"value": "./fasth3-nvfp4-encoder/text_encoder", "label": "NVFP4 Qwen3-VL"}
],
"default": ""
},
{
"key": "offload",
"label": "Memory",
"hint": "Resident is how the GB200 timings were measured",
"flag": "--offload",
"options": [
{"value": "resident", "label": "All resident (fastest)"},
{"value": "lazy", "label": "Lazy load + offload"}
],
"default": "resident"
},
{
"key": "num_gpus",
"label": "GPUs",
"hint": "Sequence-parallel degree",
"flag": "--num-gpus",
"options": [1, 2, 4, 8],
"default": 1
}
],
"limitations": [
"Measured on one GB200 at 768p, 5 s, seed 42, NVFP4 text encoder, all resident: FL2VA 28.95 s (first frame 39.6 dB / SSIM 0.971 and last frame 29.1 dB / 0.819 against the inputs); I2VA 27.37 s (first frame 26.5 dB / 0.715). L2VA was not measured separately.",
"The command passes both keyframes; the example uses only the ones the selected task needs. Replace them with your own images.",
"--offload resident keeps every component on the GPU (104.6-108.5 GiB peak with the NVFP4 text encoder, about 129 GiB all-BF16, on GB200). --offload lazy loads components on demand for smaller devices; on GB200 it took 48.06 s for the same T2VA request.",
"FASTVIDEO_ATTENTION_BACKEND=FLASH_ATTN FASTVIDEO_FA4=1 selects FlashAttention-4 on Hopper or Blackwell; FA4 is never enabled just because it is installed. Without FA4, attention uses FA3/FA2 if installed and otherwise falls back to SDPA, which is slow and can run out of memory at 768p.",
"The NVFP4 decoder and text encoder are Blackwell-only. Download them before selecting them: hf download FastVideo/FastH3-NVFP4-VAE-Decoder --include 'quality-36L/decoder-nvfp4.safetensors' 'fast-8L/decoder-nvfp4.safetensors' --local-dir fasth3-nvfp4-vae; hf download FastVideo/FastVideo-FastH3-8-Step-V2-NVFP4-Consumer --include 'text_encoder/*' --local-dir fasth3-nvfp4-encoder. The 8L decoder is a distilled student; validate it on your prompts."
],
"related": ["pika-h3-flex-t2va", "pika-h3-flex-ref2va"]
},
{
"id": "pika-h3-flex-ref2va",
"family": "pika_h3_flex",
"stage": "inference",
"task": "Reference media to video (with audio)",
"label": "Pika H3 Flex references",
"summary": "Generate from ordered image, video, and audio references through Pika's ref2va adapter: up to 9 images, 3 videos, and 3 audio clips.",
"model": "Pika-Labs/Pika-H3-Flex",
"source": "examples/inference/basic/basic_pika_h3_flex.py",
"command": "FASTVIDEO_ATTENTION_BACKEND=FLASH_ATTN FASTVIDEO_FA4=1 python examples/inference/basic/basic_pika_h3_flex.py --task ref2va --offload resident --reference image:assets/images/natural.jpg --reference image:assets/images/bus_terminal.jpg --prompt 'Start on the misty mountain ridge from the first reference, then cut to the night bus terminal from the second.'",
"gpu_types": ["NVIDIA"],
"hardware": {
"platform": "cuda",
"gpu_count": 1,
"accelerator": "NVIDIA GB200",
"peak_memory": "108.5 GiB with two image references (NVFP4 text encoder, 768p, 5 s)",
"evidence": "validated",
"evidence_url": "https://github.com/hao-ai-lab/FastVideo/pull/2038"
},
"evidence": "Verified",
"expected_artifact": "MP4 with synchronized stereo audio at outputs/pika_h3_flex/pika_h3_flex_ref2va.mp4",
"modes": ["Ref2VA"],
"knobs": [
{
"key": "weights",
"label": "Weights",
"hint": "NVFP4 QAD needs Blackwell",
"flag": "--weights",
"options": [{"value": "bf16", "label": "BF16 (Pika release)"}, {"value": "nvfp4-qad", "label": "NVFP4 QAD"}, {"value": "nvfp4-qad-fp4-attention", "label": "NVFP4 QAD + FP4 attention"}],
"default": "bf16"
},
{
"key": "resolution",
"label": "Resolution",
"hint": "Pika resolution tier",
"flag": "--resolution",
"options": [{"value": "768p", "label": "768p"}, {"value": "480p", "label": "480p"}],
"default": "768p"
},
{
"key": "aspect",
"label": "Aspect ratio",
"hint": "Bucket within the tier",
"flag": "--aspect",
"options": ["21:9", "16:9", "3:2", "4:3", "5:4", "1:1", "4:5", "3:4", "2:3", "9:16", "9:21"],
"default": "16:9"
},
{
"key": "duration",
"label": "Duration",
"hint": "Rounded up to the 17n+5 frame grid",
"flag": "--duration",
"options": [
{"value": 5, "label": "5 s (124 frames)"},
{"value": 10, "label": "10 s (243 frames)"},
{"value": 15, "label": "15 s (362 frames)"}
],
"default": 5
},
{
"key": "nvfp4_decoder",
"label": "VAE decoder",
"hint": "NVFP4 needs Blackwell",
"flag": "--nvfp4-decoder",
"options": [
{"value": "", "label": "BF16 H3 VAE"},
{"value": "./fasth3-nvfp4-vae/quality-36L/decoder-nvfp4.safetensors", "label": "NVFP4 36L quality"},
{"value": "./fasth3-nvfp4-vae/fast-8L/decoder-nvfp4.safetensors", "label": "NVFP4 8L fast"}
],
"default": ""
},
{
"key": "nvfp4_text_encoder",
"label": "Text encoder",
"hint": "NVFP4 needs Blackwell",
"flag": "--nvfp4-text-encoder",
"options": [
{"value": "", "label": "BF16 Qwen3-VL"},
{"value": "./fasth3-nvfp4-encoder/text_encoder", "label": "NVFP4 Qwen3-VL"}
],
"default": ""
},
{
"key": "offload",
"label": "Memory",
"hint": "Resident is how the GB200 timings were measured",
"flag": "--offload",
"options": [
{"value": "resident", "label": "All resident (fastest)"},
{"value": "lazy", "label": "Lazy load + offload"}
],
"default": "resident"
},
{
"key": "num_gpus",
"label": "GPUs",
"hint": "Sequence-parallel degree",
"flag": "--num-gpus",
"options": [1, 2, 4, 8],
"default": 1
}
],
"limitations": [
"Measured on one GB200 at 768p, 5 s, seed 42, NVFP4 text encoder, all resident, two image references: 51.17 s (38.42 s denoise, 64,241 packed tokens); both references appear in the output.",
"Repeat --reference image:PATH, video:PATH, or audio:PATH in order. At least one image or video is required; a video reference needs at least 22 frames at 24 fps.",
"Ref2VA loads transformer_ref and the ref2va adapter, a separate 61.7 GiB trunk download. Reference images are encoded at up to a 1024 (480p) or 2048 (768p) short edge, so conditioning memory is higher than T2VA's.",
"--offload resident keeps every component on the GPU (104.6-108.5 GiB peak with the NVFP4 text encoder, about 129 GiB all-BF16, on GB200). --offload lazy loads components on demand for smaller devices; on GB200 it took 48.06 s for the same T2VA request.",
"FASTVIDEO_ATTENTION_BACKEND=FLASH_ATTN FASTVIDEO_FA4=1 selects FlashAttention-4 on Hopper or Blackwell; FA4 is never enabled just because it is installed. Without FA4, attention uses FA3/FA2 if installed and otherwise falls back to SDPA, which is slow and can run out of memory at 768p.",
"The NVFP4 decoder and text encoder are Blackwell-only. Download them before selecting them: hf download FastVideo/FastH3-NVFP4-VAE-Decoder --include 'quality-36L/decoder-nvfp4.safetensors' 'fast-8L/decoder-nvfp4.safetensors' --local-dir fasth3-nvfp4-vae; hf download FastVideo/FastVideo-FastH3-8-Step-V2-NVFP4-Consumer --include 'text_encoder/*' --local-dir fasth3-nvfp4-encoder. The 8L decoder is a distilled student; validate it on your prompts."
],
"related": ["pika-h3-flex-t2va", "pika-h3-flex-keyframes"]
},
{
"id": "open-h3-regenerate-2k",
"family": "minimax_h3",
"stage": "inference",
"task": "Video super-resolution (2K regeneration)",
"label": "Open-H3-Regenerate-2K",
"summary": "Regenerate a 768P MiniMax-H3 draft at 2560x1440 with NVlabs' three-step refiner. The output is silent.",
"model": "Efficient-Large-Model/Open-H3-Regenerate-2K",
"source": "examples/inference/basic/basic_open_h3_regenerate_2k.py",
"command": ": \"${INPUT_VIDEO:?Set INPUT_VIDEO to a 768P MiniMax-H3 draft}\"\npython examples/inference/basic/basic_open_h3_regenerate_2k.py --video \"$INPUT_VIDEO\" --prompt \"A red fox trots through fresh snow in a pine forest at golden hour.\"",
"gpu_types": ["NVIDIA"],
"hardware": {"platform": "cuda", "gpu_count": 1, "evidence": "source-configured"},
"evidence": "Source-backed",
"expected_artifact": "2560x1440 MP4 at outputs/open_h3_regenerate_2k/regen2k_0.mp4",
"related": ["minimax-h3-t2v", "fasth3-8step-v2-cuda"]
},
{
"id": "fasth3-lora-preview",
"family": "minimax_h3",
+92
View File
@@ -0,0 +1,92 @@
{
"generated": "2026-10-28",
"note": "Measured from Hugging Face safetensors files. Sizes are GiB at the checkpoint's own precision.",
"models": {
"fasth3-8step": {
"parts": {
"transformer": 65.29,
"text_encoder": 62.13,
"vae": 9.7,
"audio_vae": 0.56
},
"alternates": {
"transformer_nvfp4": 36.91,
"transformer_fp8": 47.35,
"vae_nvfp4_quality": 5.82,
"vae_nvfp4_fast": 1.33,
"text_encoder_quantized": 15.33
},
"sources": [
"https://huggingface.co/FastVideo/FastVideo-FastH3-8-Step-V2/tree/main"
]
},
"fasth3-4step": {
"parts": {
"transformer": 65.29,
"text_encoder": 62.13,
"vae": 9.7,
"audio_vae": 0.56
},
"alternates": {
"transformer_nvfp4": 36.91,
"transformer_fp8": 47.35,
"vae_nvfp4_quality": 5.82,
"vae_nvfp4_fast": 1.33,
"text_encoder_quantized": 15.33
},
"sources": [
"https://huggingface.co/FastVideo/FastVideo-FastH3-4-step-Preview-v1-VSA-DataFree/tree/main"
]
},
"fasth3-trim": {
"parts": {
"transformer": 34.94,
"text_encoder": 15.33,
"vae": 5.93,
"audio_vae": 0.56
},
"alternates": {
"transformer_nvfp4": 11.1,
"transformer_fp8": 19.88,
"vae_nvfp4_quality": 5.82,
"vae_nvfp4_fast": 1.33
},
"sources": [
"https://huggingface.co/FastVideo/FastVideo-FastH3-Trim-8-Step/tree/main"
]
},
"fasth3-omniref": {
"parts": {
"transformer": 65.33,
"text_encoder": 15.33,
"vae": 9.7,
"audio_vae": 0.56
},
"alternates": {
"transformer_nvfp4": 36.95,
"transformer_fp8": 47.39,
"vae_nvfp4_quality": 5.82,
"vae_nvfp4_fast": 1.33
},
"sources": [
"https://huggingface.co/FastVideo/FastVideo-FastH3-Omni-8-Step-V1/tree/main",
"https://huggingface.co/FastVideo/FastH3-Omni-NVFP4-QAD/tree/main"
]
},
"pika-h3-flex-t2va": {
"parts": {
"transformer": 61.73,
"adapter": 2.58,
"text_encoder": 62.13,
"vae": 9.7,
"audio_vae": 0.56
},
"alternates": {
"vae_nvfp4_quality": 5.82,
"vae_nvfp4_fast": 1.33,
"text_encoder_quantized": 15.33
},
"sources": ["https://huggingface.co/Pika-Labs/Pika-H3-Flex/tree/main"]
}
}
}
+2
View File
@@ -13,7 +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 |
| `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.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

+13 -6
View File
@@ -157,15 +157,22 @@ __all__ = [
## 4. Register the Backend
Update `fastvideo/attention/backends/__init__.py` to export your new class.
Registration is two edits:
```python
from .mynew_attn import MyNewAttnBackend
```
1. Add a member to `AttentionBackendEnum` in `fastvideo/platforms/interface.py`.
2. Add the selection branch in `fastvideo/platforms/cuda.py`, next to the
existing backends, so `--attention-backend` maps the enum to your class.
## 5. Platform Integration
`fastvideo/attention/backends/__init__.py` is empty and does not register
anything.
If your backend requires specific platform checks (e.g., checking for H100 support), handle that in `fastvideo/platforms/cuda.py` or within your backend's `__init__`.
## 5. Platform integration
If your backend needs platform checks, such as an H100 guard, put them in
`fastvideo/platforms/cuda.py` or in the backend constructor.
The maintained version of this guide is
[Add an attention backend](../../contributing/attention_backend.md).
## 6. Add Documentation
+4 -4
View File
@@ -85,10 +85,10 @@ from fastvideo_kernel import video_sparse_attn
# topk: Number of blocks to attend
output = video_sparse_attn(
q, k, v,
block_sizes,
block_sizes,
topk=32
q, k, v,
variable_block_sizes,
q_variable_block_sizes,
topk=32,
)
```
+37 -1
View File
@@ -49,7 +49,7 @@ node docs/js/build-cookbook-capacity.mjs --check
node docs/js/build-cookbook-capacity.mjs --preview fastwan21
```
The check currently prints `Validated 1 capacity model(s) and 50 hardware variant(s).`
The check currently prints `Validated 1 capacity model(s) and 52 hardware variant(s).`
The preview prints the model with component references expanded and each
example's baseline under `base_config`. Inspect its identity, counts, geometry,
and evidence. Both commands validate deployments and capacity metadata without
@@ -404,6 +404,42 @@ matching metric, scope, phases, and execution context. One point can calibrate a
existing scaling assumption, not establish accuracy across modes. Missing startup
evidence leaves startup coverage unknown. Keep observations intact when deriving coefficients.
## FastH3 coverage
No FastH3 checkpoint has a full capacity model yet. The editor shows a
checkpoint-size breakdown and device budget, with `status: unknown` for runtime
fit. It does not subtract CPU-offloaded weights or divide file sizes by GPU
count. Those operations need phase residency and per-rank placement models.
Replacement components without matching data are omitted from the known-size
subtotal and named as missing coverage. Host-only system RAM cannot establish
GPU fit. A custom GiB budget keeps its unit; unified memory uses the shared
normalizer's usable-memory policy instead of treating nominal RAM as VRAM.
| Profile | First stop | Why |
| --- | --- | --- |
| OmniRef | Workload | Runs image-to-video; the estimator models text-to-video only |
| 8-Step CUDA | Topology | Defaults to four GPUs and Ulysses sequence parallelism |
| Trim Spark | Attention | `VIDEO_SPARSE_ATTN_H3` is not a dense backend, so VSA workspace is unmodeled |
| 4-Step Spark | Execution | `h3_sequential_load` loads components on demand |
| Omni NVFP4 decoder | Precision | The decoder swap changes decoder precision |
To make a FastH3 estimate possible, in order:
1. Add capacity entries for each checkpoint: transformer parameter counts and
geometry, the text encoder, the VAE, and the NVFP4 decoder variants.
2. Model an image-to-video request: reference latents and audio tokens change
the working set, and OmniRef passes ordered image, video, and audio inputs.
3. Model per-rank sharding for four-GPU Ulysses, including replicated weights
and per-rank sequence slices, so the estimate compares against one device.
4. Add a VSA-H3 workspace term, or a dense upper bound that is labeled as one.
5. Support quantized decoder precision instead of rejecting the configuration.
6. Record successful runs on the RTX 5090, RTX PRO 6000, and DGX Spark profiles
so the analytical estimate can be calibrated rather than trusted blindly.
Until those land, the cookbook can report checkpoint data but cannot answer
whether an edited FastH3 request fits a GPU.
## Static consumers
[`cookbook-capacity.mjs`](../js/cookbook-capacity.mjs) exposes these async loaders,
+60 -13
View File
@@ -17,7 +17,7 @@ PR opened or updated
| style, lint, type, spelling, Markdown, workflow syntax, filenames
|
|-- Tier 2: Fastcheck
| Buildkite schedules six lanes on the Slinky Slurm cluster
| Buildkite schedules six lanes across Spark 4 and Slinky Slurm
| encoder, VAE, transformer, kernel, unit, DreamVerse
|
|-- /merge or ready label
@@ -25,7 +25,7 @@ PR opened or updated
`-- Tier 3: Change-aware merge gate
trusted base-branch planner classifies the complete PR diff
Buildkite adds only relevant integration, quality, training,
API, or performance lanes on Slinky Slurm
API, or performance lanes on Spark 4 or Slinky Slurm
|
pass -> Mergify squash-merges when all merge conditions pass
fail -> fix, push, and re-run
@@ -43,11 +43,12 @@ CI is not one monolithic job:
updates, docs deployment, image builds, package publishing, and community
automations.
- Buildkite owns the GPU test graph, statuses, and trusted dispatch control
plane. Its agent runs on the Slurm login plane; it does not execute test
payloads.
- Slinky Slurm is the only active CI compute backend. A host-owned dispatcher
leases GPUs from a persistent four-GPU allocation and runs each lane in an
isolated Enroot container at the immutable PR SHA.
plane. Slinky agents dispatch tests from the login plane; the Spark 4 agent
dispatches local Slurm jobs.
- Slinky Slurm runs cluster lanes in isolated Enroot containers using a
persistent four-GPU allocation. Spark 4 runs eligible single-GPU lanes in
isolated Docker containers through its own Slurm queue. Both test the
immutable PR SHA.
- Mergify owns merge protection, labeling, and the final squash merge.
The old files under `fastvideo/tests/modal/` are retained as dormant manual
@@ -99,7 +100,7 @@ debugging a hook implementation.
| Attribute | Value |
|---|---|
| Triggered by | Buildkite PR builds with `TEST_SCOPE=fastcheck` or unset |
| Compute | Slinky Slurm (`ci-runner` queue) |
| Compute | Spark 4 when connected and unpaused at upload time; otherwise Slinky Slurm. Both use `ci-runner`. |
| Definition | `.buildkite/pipeline.yml` |
| Entrypoint | Trusted host driver -> `.buildkite/scripts/unit_test.sh` or `.buildkite/scripts/lanes/*.sh` |
@@ -122,15 +123,61 @@ Setting it to `spark4` moves only the Spark-capable lanes to a GB10 runner.
That runner exports `FASTVIDEO_CI_BACKEND=spark4`, which the lane scripts and
a few tests honor to skip GB200-only coverage (for example the LTX-2 finetune
test, which needs more than 128 GB). GB10 golden references are keyed by
`NVIDIA_GB10`; see `fastvideo/tests/golden_gate`. The private uploader policy
must allow the `backend` agent key before this routing is active.
`NVIDIA_GB10`; see `fastvideo/tests/golden_gate`.
The trusted uploader on the Slurm login plane picks the value at upload time.
It sets `FV_SPARK_BACKEND=spark4` only when both hold:
- the build's `TEST_SCOPE` is `fastcheck`, `merge` or `direct`, or is unset;
- the Buildkite agents API reports an agent tagged
`queue=ci-runner,backend=spark4` that is connected and not paused.
Otherwise it sets `slurm`. For pipelines with the backend tags above,
`/test full`, the weekly Full Suite, a paused or disconnected Spark agent, and
any failure to read the agents API select Slurm for every lane. This choice
happens at upload time; it does not move jobs already queued for Spark.
Older graphs without backend tags need the normalization described below.
The uploader rejects any `backend` value other than the two forms in the
table, so a pull request cannot pin a lane to the Spark runner directly. The upload step in each entry pipeline requires `backend=slurm`, so
the Spark runner never takes it and this policy always runs on the login plane.
The regular GB10 inference image intentionally omits FlashAttention to match
native Spark output. CI uses `docker/Dockerfile.spark-ci` to add the pinned
FA2 wheel required by VMoBA and Wan causal tests. This image keeps the base
Torch, FlashInfer, and CUTLASS packages and removes the wheel's unsupported
FA4 CuTe implementation. Build it separately from the inference image:
```bash
docker build -f docker/Dockerfile.spark-ci \
--build-arg SPARK_BASE_IMAGE=<validated-GB10-image> \
-t fastvideo-ci-gb10:<version> .
```
Golden references must match this CI image, including its FA2 version. Seed
only from unchanged upstream main, verify in separate processes, and review
before publishing new `golden_gates/NVIDIA_GB10/` files. Preserve existing
references for other GPUs. A successful import alone is insufficient; validate
an actual FA2 forward, the VMoBA tests, and each lane's complete payload before
selecting the new image in the host-owned runner configuration.
The host runner must allowlist Spark-capable lane types before allocating the
GPU. Label each Docker container with its job token and remove it from the
outer runner on completion or cancellation. Slurm killing the Docker client
does not stop a container owned by the Docker daemon.
The trusted uploader must preserve each literal `backend: "slurm"` even when
it selects `FV_SPARK_BACKEND=spark4`. Older PR revisions without backend tags
must be normalized to `backend: "slurm"` before upload; `queue: "ci-runner"`
alone also matches the Spark agent. Update the private uploader's lane policy
with routing changes and verify the resolved agent queries on a canary build.
### Tier 3: Change-Aware Merge Gate
| Attribute | Value |
|---|---|
| Triggered by | `/merge`, adding `ready`, or a new push to a PR that already has `ready` |
| Compute | Slinky Slurm only (`ci-runner` queue) |
| Compute | Spark 4 or Slinky Slurm, selected per lane (`ci-runner` queue) |
| Definition | `.buildkite/pipeline.yml` |
| Entrypoint | `/opt/fastvideo-ci-runner/run-ci` (`run-unit` is a compatibility wrapper) |
@@ -396,7 +443,7 @@ Valid direct test names:
| `/test eval` | `eval` |
The temporary `<name>-ci` spellings remain accepted as compatibility aliases;
they select the same Slurm lane and do not identify a second backend.
they select the same test lane and do not identify a second backend.
When a direct test completes successfully, Buildkite posts
`direct-test-completed`. `.github/workflows/ci-aggregate-status.yml` then reads
@@ -476,7 +523,7 @@ Every active test selection lives in `.buildkite/scripts/unit_test.sh` or a
focused `.buildkite/scripts/lanes/<lane>.sh`. The private, agent-owned lane
table binds each internal `*_ci` type to that script, its GPU count, wall-clock
limit, dependency extras, kernel-build policy, secrets, and artifacts. The
internal suffix is an implementation detail; there is only one active backend.
internal suffix is an implementation detail; backend tags select the compute host.
SSIM uses `fastvideo/tests/ssim/ci_runner.py` inside a single four-GPU lease.
It discovers `REQUIRED_GPUS` and `*_MODEL_TO_PARAMS` with AST parsing, then
+17 -6
View File
@@ -13,7 +13,9 @@ test in the same pull request.
them directly with `os.environ.get("NAME")`, and the name must be in the external-variable allowlist
(`EXTERNAL_ALLOWLIST` in the contract test). When FastVideo sets such a variable for the other tool, it calls
`envs.set_external`, `envs.setdefault_external`, or `envs.unset_external`, and the name must be in
`EXTERNAL_WRITE_ALLOWLIST`. Variables that FastVideo's CI and CI tooling define (for example `TEST_SCOPE` and
`EXTERNAL_WRITE_ALLOWLIST`. Code that needs every variable of a family that another tool owns, such as FA4's
`FA4_*` tuning knobs, calls `envs.external_with_prefixes("FA4_")` with literal prefixes that the allowlist
covers. Variables that FastVideo's CI and CI tooling define (for example `TEST_SCOPE` and
`PERF_RUN_SOURCE`) keep their names, and test code under `fastvideo/tests/` reads them directly; they are listed
in `CI_ONLY_VARIABLES` in the contract test, together with the file that sets each one.
2. **Read with `envs.NAME.get()`, write with `envs.NAME.set()`, and change a value in tests with
@@ -177,6 +179,7 @@ longer exists also fails the test, so the fixing pull request deletes its entry.
| `FASTVIDEO_CACHE_ROOT` | path | computed | path | Root directory for FastVideo cache files. Defaults to ~/.cache/fastvideo, or $XDG_CACHE_HOME/fastvideo when XDG_CACHE_HOME is set. |
| `FASTVIDEO_REASON1_WEIGHTS_PATH` | str | unset | path | Local path or Hugging Face id of Reason1 weights to load instead of the checkpoint's own. |
| `FASTVIDEO_HOST_IP` | str | `""` | distributed | IP address of this node when the node has several network interfaces. Set it on each node for multi-node inference. |
| `FASTVIDEO_PG_TIMEOUT_MIN` | int | `0` | distributed | Timeout in minutes for the training process groups' collectives (0 keeps PyTorch's 10-minute default). Raise it for large models whose rank-0 load and scatter, checkpoint saves or long evaluations outlast the default. |
| `FASTVIDEO_LOOPBACK_IP` | str | `""` | distributed | Loopback IP address to use instead of the detected one. |
| `FASTVIDEO_RAY_PER_WORKER_GPUS` | float | `1.0` | distributed | GPUs per Ray worker. A fraction lets Ray schedule several actors on one GPU, so other actors can share the GPUs with FastVideo. |
| `FASTVIDEO_NCCL_SO_PATH` | str | unset | distributed | Path to the NCCL library file. Needed because the nccl&gt;=2.19 that PyTorch ships has a bug (https://github.com/NVIDIA/nccl/issues/1234). |
@@ -194,9 +197,13 @@ longer exists also fails the test, so the fixing pull request deletes its entry.
| `FASTVIDEO_ATTENTION_BACKEND` | str | unset | attention | Attention backend, as an AttentionBackendEnum name such as TORCH_SDPA, FLASH_ATTN, VIDEO_SPARSE_ATTN, SAGE_ATTN, or SAGE_ATTN_THREE. FastVideoArgs uses it when FastVideoArgs.attention_backend is unset. |
| `FASTVIDEO_FA4` | bool | `0` | attention | The FLASH_ATTN backend uses FlashAttention-4 (flash_attn.cute) instead of FA3 or FA2. |
| `FASTVIDEO_MINIMAX_H3_FA4_PACKED_VARLEN` | bool | `0` | attention | MiniMax-H3 dense DiT self-attention uses the FlashAttention-4 packed-varlen entry point. This changes the floating-point reduction order, so it is an inference-only opt-in. |
| `FASTVIDEO_MINIMAX_H3_DENSE_ATTN` | one of flash, cudnn | `flash` | attention | Kernel for MiniMax-H3 dense DiT self-attention: flash keeps the selected FlashAttention backend; with FLASH_ATTN, cudnn sends the no-mask, no-grad dense call through cuDNN's fused SDPA. cuDNN has a different reduction order, so this is an inference-only opt-in. |
| `FASTVIDEO_VSA_PTX_BLACKWELL` | bool | `0` | attention | VIDEO_SPARSE_ATTN_H3 sends tile-64 forwards to the data-center Blackwell (sm_100a / sm_103a) kernel; grad-tracking forwards need its autograd operator, else Triton. fastvideo-kernel reads the same variable with the same rule. Deprecated names: `FASTVIDEO_VSA_SM100A`. |
| `FASTVIDEO_VSA_TRITON` | bool | `0` | attention | Force the Triton MiniMax-H3 sparse attention kernel. fastvideo-kernel reads the same variable. |
| `FASTVIDEO_NVFP4_FA4` | bool | `0` | attention | FlashAttention-4 quantizes Q and K to NVFP4. An explicit nvfp4_fa4 attention implementation argument takes precedence. |
| `FASTVIDEO_NVFP4_FA4_QK_TRANSFORM` | one of none, smooth_k, hadamard, smooth_k+hadamard | `none` | attention | Exact Q/K preconditioning applied right before NVFP4 quantization on the FlashAttention-4 NVFP4 path: smooth_k subtracts each head's token-mean of K, hadamard rotates Q and K by the head-dim Hadamard matrix. Must match the qk_transform the model was QAD-trained with. An explicit nvfp4_qk_transform attention implementation argument takes precedence. |
| `FASTVIDEO_NVFP4_FA4_FUSED_QK_QUANT` | bool | `1` | attention | With FASTVIDEO_NVFP4_FA4_QK_TRANSFORM other than none, apply the transform and the NVFP4 quantization in one fused Triton kernel per tensor (same FP4 codes and scale factors as the unfused path). Set it to 0 to use the unfused PyTorch transform and flashinfer quantization. |
| `FASTVIDEO_NVFP4_FA4_BF16_BLOCKS` | str | `""` | attention | Comma-separated MiniMax-H3 transformer_blocks indices whose attention stays in BF16 while FASTVIDEO_NVFP4_FA4 quantizes the rest, for example 45,49. |
| `FASTVIDEO_FA4_PV_MODE` | one of bf16, fp8 | `bf16` | attention | V dtype on the NVFP4 FlashAttention-4 path (FLASH_ATTN with nvfp4_fa4, ATTN_QAT_INFER on sm_100a/sm_103a): bf16 keeps V in BF16; fp8 casts V to float8 e4m3 without scaling. An explicit fa4_pv_mode attention implementation argument takes precedence. |
| `FASTVIDEO_DISABLE_ATTENTION_COMPILE` | bool | `1` | attention | Keep attention forward out of torch.compile graphs (torch.compiler.disable). Set it to 0 to let attention constructed under that setting be traced. Setting it explicitly to true also blocks regional compile. |
| `FASTVIDEO_MLX_WINDOW` | int | `0` | attention | MLX FastWan windowed attention size in tokens. 0 uses full attention. |
@@ -208,14 +215,14 @@ 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. |
| `FASTVIDEO_MLX_COMPILE` | bool | `0` | performance | Compile the MLX DiT forward with mx.compile. |
| `FASTVIDEO_MLX_FAST_NORM` | bool | `0` | performance | Use MLX fast normalization kernels. |
| `FASTVIDEO_MLX_DQ_GEMM` | str | `1` | performance | MLX dequantized GEMM for affine-quantized weights: 0 turns it off, 1 uses the measured minimum row count, and an integer sets the minimum row count. |
| `FASTVIDEO_MLX_QUANT_GEMM` | str | `auto` | performance | MLX H3 affine-quantized GEMM path: `auto` keeps `FASTVIDEO_MLX_DQ_GEMM` behaviour, `dequant` always uses dequantize + dense GEMM for wide inputs, `qmm` always uses `mx.quantized_matmul`. |
| `FASTVIDEO_MLX_QUANT_GEMM` | str | `auto` | performance | MLX H3 affine-quantized GEMM path: auto keeps FASTVIDEO_MLX_DQ_GEMM behaviour, dequant always uses dequantize + dense GEMM for wide inputs, qmm always uses mx.quantized_matmul. |
| `FASTVIDEO_LTX2_VAE_CHANNELS_LAST_3D` | bool | `1` | performance | LTX-2 VAE uses the channels_last_3d memory format. |
| `FASTVIDEO_LTX2_DISABLE_AUDIO_AUTOCAST` | bool | `1` | performance | LTX-2 audio decoding runs without CUDA autocast. Deprecated names: `LTX2_DISABLE_AUDIO_AUTOCAST`. |
| `FASTVIDEO_FLUX2_DISABLE_BF16_REDUCED_PRECISION_REDUCTION` | bool | `0` | performance | Flux denoising disables reduced-precision reductions in bf16 matmuls, which tightens accumulation for the 4-step Klein model. |
@@ -267,7 +274,7 @@ longer exists also fails the test, so the fixing pull request deletes its entry.
| `FASTVIDEO_H3_FP8_ATTENTION` | bool | `0` | performance | With NVFP4 layer_profile h3_dit_ffn, run MiniMax-H3 attention projections in FP8. |
| `FASTVIDEO_H3_FP8_GRANULARITY` | one of tensor, channel | `tensor` | performance | FP8 scaling granularity for FASTVIDEO_H3_FP8_ATTENTION. |
| `FASTVIDEO_LORA_SIDE_BRANCH` | bool | `0` | performance | Apply inference LoRA adapters as a bf16 side branch next to every target linear instead of merging them, so the adapter strength can change at runtime. Packed NVFP4 or FP8 linears without their original dense weight always use the side branch. |
| `FASTVIDEO_NVFP4_MM_BACKEND` | str | `auto` | performance | FlashInfer mm_fp4 backend for NVFP4 linears, e.g. auto or cutlass. |
| `FASTVIDEO_NVFP4_MM_BACKEND` | str | `auto` | performance | FlashInfer mm_fp4 backend for NVFP4 linears: auto (cudnn for MiniMax-H3 DiT profiles on sm_100), cutlass or cudnn. |
| `FASTVIDEO_NVFP4_MM_ROW_SPLIT` | one of off, table, auto | `off` | performance | Run large cuDNN NVFP4 GEMMs over row chunks where its plan heuristic is slow: off, table (fixed per-shape chunk counts) or auto (time 1/2/4 chunks once per shape; not deterministic across runs). |
| `FASTVIDEO_NVFP4_ACT_AMAX` | path | unset | performance | JSON of calibrated NVFP4 input amax per linear, keyed b&lt;block&gt;.&lt;sub&gt; or full prefix; sets a static activation scale. |
| `FASTVIDEO_NVFP4_DYNAMIC_ACT` | str | `""` | performance | NVFP4 linears that derive the activation scale per call: all, or comma-separated layer-name suffixes such as ff.fc_out. |
@@ -278,15 +285,19 @@ longer exists also fails the test, so the fixing pull request deletes its entry.
| `FASTVIDEO_H3_SPLICE_FROM_STEP` | int | `4` | eval | First denoising step run by FASTVIDEO_H3_SPLICE_TRANSFORMER. |
| `FASTVIDEO_H3_ENCODER_LAYERWISE` | bool | `0` | performance | Stream MiniMax-H3 text-encoder language layers through exact-size pinned host memory (text-only prompts). |
| `FASTVIDEO_H3_ENCODER_FUSED_DEQUANT` | bool | `0` | performance | Expand the serialized NVFP4 MiniMax-H3 text encoder with one fused Triton pass on GPUs without FP4 GEMM. |
| `FASTVIDEO_H3_VAE_TILE_BATCH` | int | `1` | performance | Spatial tiles per MiniMax-H3 video VAE decoder call; 1 decodes per tile. |
| `FASTVIDEO_H3_VAE_TILE_BATCH` | int | `8` | performance | Spatial tiles per MiniMax-H3 video VAE decoder call. The ViT decoder treats batch entries independently, so batching tiles matches per-tile decoding, except for NVFP4 decoders with dynamic activation scales, which always decode per tile. On GB200 at 768x1344, 8 tiles per call decodes 107 frames in 3.7 s instead of 5.4 s and 362 frames in 12.8 s instead of 18.3 s (+0.7 GiB peak). 1 decodes per tile. |
| `FASTVIDEO_H3_VAE_FUSED_QK_NORM_ROPE` | bool | `0` | performance | Fuse the MiniMax-H3 video VAE decoder per-head QK RMSNorm and partial RoPE into one Triton pass per attention layer (same fp32/inference-dtype arithmetic; the RMSNorm reduction order differs, like the DiT FASTVIDEO_MINIMAX_H3_FUSIONS qknorm_rope). |
| `FASTVIDEO_H3_REF2VA_MEMO_ENTRIES` | int | `0` | performance | Entries in each content-keyed MiniMax-H3 memo of reference encodes (Qwen3-VL Ref2VA presentation, VAE keyframe latents); repeat references are reused exactly. 0 disables. |
| `FASTVIDEO_H3_VAE_TILE_PARALLEL` | bool | `0` | performance | Split MiniMax-H3 VAE spatial tiles (not only temporal chunks) across the sequence-parallel ranks in the parallel decode and keyframe encode; bitwise equal to the serial path. |
| `FASTVIDEO_H3_VAE_TILE_PARALLEL` | bool | `0` | performance | Split MiniMax-H3 VAE spatial tiles (not only temporal chunks) across the sequence-parallel ranks in the parallel decode and keyframe encode; bitwise equal to the serial path. Requires FASTVIDEO_H3_VAE_TILE_BATCH=1 (the split replays the per-tile path). |
| `FASTVIDEO_H3_VAE_NVFP4_FUSED` | bool | `1` | performance | Run NVFP4 MiniMax-H3 VAE decoder blocks through the bit-exact fused Triton kernels at inference. |
| `FASTVIDEO_H3_VAE_NVFP4_CUDA_GRAPH` | bool | `1` | performance | Replay small fused NVFP4 MiniMax-H3 VAE decoder calls (at most four 1797-token tiles) as per-shape CUDA graphs. |
| `FASTVIDEO_H3_VAE_NVFP4_DECODER` | path | unset | performance | Exported NVFP4 MiniMax-H3 video decoder (.pt, or .safetensors with bf16 masters or pre-packed NVFP4 weights) that replaces the VAE decoder at load; skips the INT8 ConvRot overlay. |
| `FASTVIDEO_H3_VAE_INT8_OVERLAY` | bool | `1` | performance | Apply the INT8 ConvRot decoder overlay when the MiniMax-H3 VAE folder ships one; 0 decodes with the dense weights. |
| `FASTVIDEO_H3_VAE_INT8_SHARED_QKV` | bool | `0` | performance | Share the INT8 activation rotation and quantization across the MiniMax-H3 VAE Q/K/V projections. |
| `FASTVIDEO_H3_VAE_INT8_TRANSPOSE_VIEW` | bool | `0` | performance | Use transposed weight views in the MiniMax-H3 VAE INT8 projections. |
| `FASTVIDEO_H3_VAE_COMPILE_BLOCKS` | bool | `0` | performance | Run the MiniMax-H3 video VAE decoder transformer blocks through one regional torch.compile graph: Inductor fuses the fp32 residual scale/add pairs, the RMSNorm+cast and the SwiGLU silu\*mul. |
| `FASTVIDEO_H3_VAE_UINT8_PIXELS` | bool | `1` | performance | Quantize single-GPU MiniMax-H3 video VAE output to uint8 on the GPU per chunk and stream uint8 into the host buffer; frames are bitwise identical to the fp32 path. 0 streams fp32. |
| `FASTVIDEO_H3_VAE_ENCODE_TILE_BATCH` | int | `1` | performance | Spatial tiles per MiniMax-H3 video VAE encoder call (reference and keyframe encodes); 1 encodes per tile. Batched tiles differ from per-tile encoding only in reduction order. |
| `FASTVIDEO_H3_VAE_INT8_FUSED_DEQUANT` | bool | `0` | performance | Fused dequantization epilogue for the MiniMax-H3 VAE INT8 projections. |
| `FASTVIDEO_H3_PINNED_SWAP` | bool | `1` | performance | Swap offloaded MiniMax-H3 modules through exact-size pinned host arenas. |
| `FASTVIDEO_H3_PARK_MODULES` | str | unset | performance | Comma-separated MiniMax-H3 denoise modules parked on the host while the text encoder runs, e.g. vae,audio_vae. |
+6
View File
@@ -162,6 +162,12 @@ Reset = resolveConfig(catalog, {})
counts and parallel degrees that divide the count, with supported `-1`
automatic values. Invalid combinations are rejected without changing edits.
Component checks reject a TAEH3 backend with an NVFP4 H3 decoder override and
reject an NVFP4 layer profile paired with another transformer precision. In the
editor, selecting TAEH3 clears the override; selecting an NVFP4 decoder restores
`h3-vae`. The copied YAML and visible chips follow the same choices. Schema
checks do not certify model loading or GPU execution.
The native runtime still resolves model-, hardware- and request-dependent
defaults. Cookbook validation does not establish complete native validity,
server readiness or GPU memory fit. Client instructions use same-machine
+1
View File
@@ -7,6 +7,7 @@ The interactive catalog groups models by family and generates YAML for supported
- [Wan and FastMetal](wan.md)
- [LTX](ltx.md)
- [FLUX](flux.md)
- [Qwen-Image-2.1](qwen-image21.md)
- [Stable Diffusion](stable-diffusion.md)
- [Stable Audio](stable-audio.md)
- [MMAudio](mmaudio.md)
+29 -3
View File
@@ -59,7 +59,7 @@
</tr>
<tr>
<td>DGX Spark</td>
<td>FastH3 V1 on one GB10, or two Sparks with Ray sequence parallel (<code>sp_size=2</code>) over QSFP RoCE. Select NVIDIA DGX Spark, then 1 Spark or 2 Sparks.</td>
<td>FastH3 V1 on one GB10. Two Sparks run one clip faster over QSFP RoCE (<code>sp_size=2</code>), but this page configures one Spark.</td>
<td>Not wired</td>
</tr>
<tr>
@@ -72,6 +72,32 @@
</div>
</details>
<details id="pika-h3-flex">
<summary>Pika H3 Flex</summary>
<div >
<p>Pika H3 Flex is Pika's post-trained MiniMax-H3: the H3 components plus a rank-128 adapter that FastVideo applies automatically, sampled with a fixed six-forward re-noise ladder. One recipe page covers T2VA, the keyframe tasks (I2VA, L2VA, FL2VA), and Ref2VA. Its builder selects the resolution tier (480p or 768p), the aspect bucket, the duration (5, 10, or 15 s), and the optional Blackwell NVFP4 decoder and text encoder. The <a href="../../inference/pika_h3_flex/">Pika H3 Flex runbook</a> has the full sampling and geometry contract.</p>
<pre><code>FASTVIDEO_ATTENTION_BACKEND=FLASH_ATTN FASTVIDEO_FA4=1 \
python examples/inference/basic/basic_pika_h3_flex.py --task t2va --offload resident \
--resolution 768p --aspect 16:9 --duration 5 \
--prompt "A paper boat drifts down a rainy city street at night."</code></pre>
<table>
<thead>
<tr>
<th>T2VA on one GB200, 768p 16:9, 5 s, warm, all resident</th>
<th>Time</th>
</tr>
</thead>
<tbody>
<tr><td>Official <code>pika-h3-pro</code> CLI</td><td>59.7 s</td></tr>
<tr><td>FastVideo BF16</td><td>26.2 s</td></tr>
<tr><td>+ NVFP4 <code>quality-36L</code> decoder (40.5 dB PSNR vs BF16 decoder)</td><td>18.0 s</td></tr>
<tr><td>+ NVFP4 <code>fast-8L</code> decoder</td><td>17.4 s</td></tr>
</tbody>
</table>
<p>The BF16 run peaked at about 129 GiB resident on GB200; the official CLI's sampling peak was 71.7 GiB. With the NVFP4 text encoder, I2VA took 27.37 s, FL2VA 28.95 s, and Ref2VA with two image references 51.17 s, at 104.9 to 108.5 GiB peak. The example's default <code>--offload lazy</code> fits smaller devices but reloads components per request (48.06 s for the same T2VA request on GB200). FlashAttention-4 needs <code>FASTVIDEO_FA4=1</code> even when it is installed. Without it, attention uses FA3 or FA2 if one is installed and otherwise falls back to SDPA, which is slow and can run out of memory at 768p. GB10 has no FA4, so drop the prefix on DGX Spark.</p>
</div>
</details>
<details id="cookbook-setup">
<summary>Setup</summary>
<div >
@@ -100,7 +126,7 @@ cd FastVideo</code></pre>
<li>The FastH3 CUDA performance profile was measured on four GB200 GPUs. Use its strict profile when exact operation order matters more than the measured performance configuration.</li>
<li>The MLX source runtime supports T2VA, optional temporal <code>--fast</code>, optional spatial <code>--fast-spatial</code>, and opt-in VSA on <code>--include-vsa</code> checkpoints. FastH3 V2 MLX converts with <code>--include-vsa</code> and runs eight forwards. FL2VA, Ref2VA, and two-pass refinement are not wired.</li>
<li>GPU count and VAE decode backend are configurable in the builder above for FastH3 CUDA recipes. Only the value shown by default has a recorded run; other supported values are unmeasured here.</li>
<li>DGX Spark is a runtime on FastH3 V1, not a separate family card. Select NVIDIA DGX Spark, then 1 Spark or 2 Sparks. The CUDA GPU-count knob does not apply to Spark.</li>
<li>DGX Spark is a runtime on FastH3 V1, not a separate family card. This page configures one Spark. For two Sparks, run <code>examples/inference/basic/basic_fasth3_spark_pair.yaml</code>; there is no serving baseline. The CUDA GPU-count knob does not apply to Spark.</li>
<li>CompactH3 NVFP4 is one Blackwell GPU. RTX 5090 (32 GB) parks the encoder in pinned host RAM. RTX PRO 6000 Blackwell (96 GB) keeps encoder, DiT, and VAE resident. Keep <code>FASTVIDEO_ATTENTION_BACKEND=ATTN_QAT_INFER</code>, <code>FASTVIDEO_FA4=0</code>, <code>FASTVIDEO_VSA_PTX_BLACKWELL=0</code>, and <code>FLASHINFER_CUDA_ARCH_LIST=12.0a</code>. On PRO 6000 enable VAE compile and leave DiT <code>inference_torch_compile</code> off. CompactH3 is a dense prune; do not enable <code>VIDEO_SPARSE_ATTN_H3</code> until a VSA-trained student exists.</li>
<li>GB10 has no FA4 / ptx_blackwell VSA kernel. Keep <code>FASTVIDEO_FA4=0</code> and <code>FASTVIDEO_VSA_PTX_BLACKWELL=0</code>. Legal <code>num_frames</code> values are <code>17n+5</code>, capped at 362 (15.08 s). A 345-frame request on one Spark can OOM. Native 16:9 sizes include 832×480 and 1344×768.</li>
<li>Gated or missing checkpoints: run <code>huggingface-cli login</code> and confirm you accepted the model's license on Hugging Face.</li>
@@ -111,6 +137,6 @@ cd FastVideo</code></pre>
<details id="cookbook-evidence">
<summary>Evidence status</summary>
<div >
<p>Every command, model ID, and flag on this page maps to a checked-in FastVideo source. Recipes marked <strong>Verified</strong> also have a recorded hardware path in linked FastVideo evidence. The full H3 CUDA examples remain <strong>Source-backed</strong> where the source records a GPU count but no GPU model or memory requirement. Unlisted hardware is unknown, not unsupported.</p>
<p>Every command, model ID, and flag on this page maps to a checked-in FastVideo source. No recipe here carries a recorded run for a specific device, so the editor does not claim one. The full H3 CUDA examples remain <strong>Source-backed</strong> where the source records a GPU count but no GPU model or memory requirement. Unlisted hardware is unknown, not unsupported.</p>
</div>
</details>
+17 -20
View File
@@ -1,30 +1,27 @@
# Run H3 with a server and playground
Start FastH3 on CUDA, one DGX Spark, or Apple Silicon MLX, then iterate on prompts in a browser,
with cURL, or from your app. The server and clients can run on the same machine.
Start FastH3 on CUDA, one DGX Spark, or Apple Silicon MLX, then send requests from a
browser, cURL, or your app. Several clients can share one server process.
The server supports the OpenAI-compatible video-job API. The Python and
JavaScript client examples use that interface, but requests go to FastVideo.
You do not need an OpenAI account or cloud key.
CUDA requests reuse a loaded `VideoGenerator`. MLX keeps one pipeline object and
a prompt-embedding cache, but loads and releases heavyweight components between
phases to fit unified memory. MLX serving still pays that component-loading cost
on subsequent requests.
The [H3 recipe selector](minimax-h3.md) provides the same workflow with runtime
selection. This guide covers FastH3 V1 (four forwards) and FastH3 V2
(eight forwards). Start a server, then iterate in the playground or with the
OpenAI Python client. Other H3 recipes keep their direct Python commands.
The server implements the OpenAI-compatible video-job API. The Python and
JavaScript clients use that API, but requests go to FastVideo. You do not need an
OpenAI account or cloud key.
CUDA requests reuse one loaded `VideoGenerator`. The Python SDK can do the same
when you reuse the generator across `generate()` calls. MLX keeps one
`MiniMaxH3MLXPipeline` and a prompt-embedding cache, but still loads and releases
components between phases to fit unified memory. MLX serving does not keep all
weights resident or remove phase-loading time. Use a server when you want
separate clients to share the process without restarting scripts.
This guide covers FastH3 V1 (four forwards) and FastH3 V2 (eight forwards). For
direct Python generation, see the [H3 cookbook](minimax-h3.md).
## Install and start the server
All runtimes need a FastVideo clone and an activated Python environment.
### CUDA
Use a FastVideo clone and an activated Python environment. Complete the
[CUDA installation requirements](../getting_started/installation/gpu.md)
Complete the [CUDA installation requirements](../getting_started/installation/gpu.md)
before running these commands:
```bash
@@ -33,9 +30,9 @@ fastvideo serve --config examples/serving/openai_fasth3.yaml --server.host 127.0
```
The configuration loads `FastVideo/FastVideo-Minimax-FastH3-Preview-v0.2` and
advertises it as `fasth3`. It configures four CUDA GPUs but does not record
a GPU model or VRAM requirement. This is a source-backed server profile, not
the measured GB200 Python performance profile. Compilation is disabled.
advertises it as `fasth3`. It configures four CUDA GPUs and does not record a GPU
model or VRAM requirement, so treat it as a starting point, not a measured
profile. Compilation is disabled.
For FastH3 V2, use the same install and the 8-step config (nine sigma
points, eight DiT forwards, VSA sparsity 0.8):
+9 -1
View File
@@ -44,7 +44,7 @@ runtimes:
host: 0.0.0.0
port: 8000
served_model_name: null
workloads: [t2v, i2v]
workloads: [t2v, i2v, v2v]
options: &cuda_options
server.served_model_name:
anyOf:
@@ -97,6 +97,14 @@ runtimes:
title: Compile transformer
type: boolean
default: false
generator.engine.quantization.transformer_quant:
title: Transformer quantization
description: >-
Quantizes the DiT at load time. NVFP4 needs a Blackwell GPU. FP8 runs on Hopper and Ada.
Leave empty to use the checkpoint's own precision.
type: [string, "null"]
enum: [null, NVFP4, FP8]
default: null
# MiniMax-H3 only (fastvideo_args.video_decode_backend). TAEH3 fetches its own SHA-pinned weights.
generator.pipeline.experimental.video_decode_backend:
title: Video decoder
+108
View File
@@ -0,0 +1,108 @@
# Qwen-Image-2.1 recipes
<RecipeFamily family="qwen_image21" />
Generate images, edit an image, or combine up to ten ordered references with
`Qwen/Qwen-Image-2.1`. Outputs are RGBA PNGs. Request a transparent background
in the prompt when needed.
## Run on a GPU
Follow the [CUDA installation guide](../getting_started/installation/gpu.md), then
run from the repository root on one CUDA GPU (`tp_size=sp_size=1`). The examples
use CPU offload and VAE tiling. The loader downloads the complete checkpoint;
use `--model-path /path/to/Qwen-Image-2.1` for local weights. No conversion is needed.
```bash
python examples/inference/basic/basic_qwen_image21.py \
--prompt 'A red ceramic teapot, isolated on a transparent background.' \
--output outputs/teapot.png
```
This saves a 1024×1024 PNG after 40 denoising steps. For instruction editing,
replace `source.png` with your image:
```bash
python examples/inference/basic/basic_qwen_image21.py \
--reference source.png \
--prompt 'Change the shirt to blue; retain the subject and composition.' \
--output outputs/edit.png
```
Repeat `--reference` for reference-to-image generation, annotations or masks:
```bash
python examples/inference/basic/basic_qwen_image21.py \
--reference subject.png --reference style.png \
--prompt 'Paint the subject from image 1 in the style of image 2.'
```
A mask is another reference interpreted through the prompt; pixels outside it
are not guaranteed to remain identical.
## Configuration
- Set `--height`, `--width`, `--steps` and `--seed` as needed. Dimensions must be
positive multiples of 32; use `--height 2048 --width 2048` for 2K output.
- `--reference-resolution` defaults to 1024 and must be a positive multiple of
32. References retain their aspect ratio and are resized to approximately
that resolution squared in pixels, independently of output dimensions.
- Prefix KV caching avoids recomputing text/reference tokens each step. The
Python example caches on CPU; `--no-use-kv-cache` disables reuse.
- For two-branch CFG, set `--true-cfg-scale` above 1 and supply
`--negative-prompt` (an empty string is valid). Embedded guidance is not used.
For the CLI, use the checked-in [T2I YAML](../../examples/inference/basic/qwen_image21_t2i.yaml)
or [editing YAML](../../examples/inference/basic/qwen_image21_edit.yaml) with
`fastvideo generate --config <file>`. The T2I YAML keeps prefix KV caches on CUDA;
the Python example and editing YAML default to CPU caches. See
[Configuration](../inference/configuration.md) for the Python API and YAML options.
## Attention
Dense image attention uses FastVideo's shared automatic backend selector, which
prefers FlashAttention on supported CUDA hardware. Masked text attention uses
SDPA. To request FA4 with its dependencies installed on supported hardware:
```bash
export FASTVIDEO_ATTENTION_BACKEND=FLASH_ATTN
export FASTVIDEO_FA4=1
```
Use `FASTVIDEO_ATTENTION_BACKEND=TORCH_SDPA` and `FASTVIDEO_FA4=0` for upstream
parity comparisons. Different attention kernels can produce different pixels.
See [Optimizations](../inference/optimizations.md) for backend and offload settings.
## Measured memory
Peaks below are in **GiB**, measured on one **NVIDIA GB200** with BF16,
1024×1024 output, seed 42, compilation disabled and two 40-step requests per
process. Image cases use one reference. These measurements do not establish
minimum hardware capacity; an actual 24 GB GPU has not been verified.
| Configuration | KV cache | GPU allocated | GPU reserved | Device VRAM including driver | Host process-tree RSS |
| --- | --- | ---: | ---: | ---: | ---: |
| T2I YAML, SDPA, offloaded, tiled | CUDA | 13.28 | 22.54 | 24.19 | 43.87 |
| Plain I2I Python, SDPA, offloaded, tiled | CPU | 13.29 | 16.11 | 17.76 | 48.29 |
| Editing Python, SDPA, offloaded, tiled | CPU | 13.29 | 16.11 | 17.76 | 49.25 |
| T2I, FA4, resident models, untiled VAE | CUDA | 32.77 | 35.50 | 37.09 | 13.12 |
Measurements cover loading, cold/warm requests, PNG export and shutdown.
Allocated/reserved values use PyTorch counters; device VRAM and simultaneous
process-tree RSS were sampled every 20 ms. Device VRAM includes the GB200's
0.72 GiB driver reservation. Allow host RAM headroom beyond process RSS.
Offloaded rows explicitly use SDPA; the resident FA4 row disables offload and
tiling and is a separate configuration, not a controlled latency comparison.
## Validation and scope
On GB200, 40-step upstream SDPA parity passed for eleven 1024×1024 cases:
T2I, plain I2I, editing, reference-to-image, RGBA generation/editing, annotations,
mask images, true CFG, uncached execution and ten references. Separate 2048×2048
T2I also passed. Denoising trajectories and output pixels matched exactly.
FA4 block and T2I/edit image regressions passed against their matching references.
See the [validation runbook](../../tests/local_tests/qwen_image21/README.md)
for reproducible commands, pinned revisions and numerical diagnostics.
The supported scope is single GPU, batch one and unquantized inference.
SP/TP, LoRA, prompt rewriting and HTTP serving are not included.
+1
View File
@@ -24,6 +24,7 @@ deployments:
- generator.engine.parallelism.tp_size
- generator.engine.parallelism.sp_size
- generator.engine.offload.text_encoder
- generator.engine.quantization.transformer_quant
- generator.engine.offload.vae
- generator.pipeline.experimental.video_decode_backend
- generator.pipeline.experimental.h3_vae_nvfp4_decoder
+1
View File
@@ -21,6 +21,7 @@ deployments:
- generator.engine.parallelism.*
- "!generator.engine.parallelism.hsdp_*"
- generator.engine.offload.text_encoder
- generator.engine.quantization.transformer_quant
- generator.engine.offload.vae
- generator.pipeline.experimental.video_decode_backend
- generator.pipeline.experimental.h3_vae_nvfp4_decoder
+5 -1
View File
@@ -1,4 +1,4 @@
title: FastH3 OmniRef
title: FastH3 Omni v1
summary: Video and synchronized audio from a prompt plus ordered image, video, and audio references, in eight forwards.
guide: docs/inference/fasth3_omniref.md
deployments:
@@ -39,10 +39,12 @@ deployments:
directories in server.media_roots.
- Based on the maintained inference recipe; this serving adaptation has not been GPU-validated.
- "Optional NVFP4 decoder (Blackwell only): hf download FastVideo/FastH3-NVFP4-VAE-Decoder --include 'quality-36L/decoder-nvfp4.safetensors' 'fast-8L/decoder-nvfp4.safetensors' --local-dir fasth3-nvfp4-vae"
- "Optional NVFP4 text encoder (Blackwell only): hf download FastVideo/FastVideo-FastH3-8-Step-V2-NVFP4-Consumer --include 'text_encoder/*' --local-dir fasth3-nvfp4-encoder"
options: &omniref_options
- server.*
- generator.engine.offload.text_encoder
- generator.pipeline.experimental.h3_vae_nvfp4_decoder
- generator.pipeline.components.text_encoder_weights
- default_request.sampling.num_frames
- default_request.sampling.height
- default_request.sampling.width
@@ -92,6 +94,7 @@ deployments:
directories in server.media_roots.
- Based on the maintained inference recipe; this serving adaptation has not been GPU-validated.
- "Optional NVFP4 decoder (Blackwell only): hf download FastVideo/FastH3-NVFP4-VAE-Decoder --include 'quality-36L/decoder-nvfp4.safetensors' 'fast-8L/decoder-nvfp4.safetensors' --local-dir fasth3-nvfp4-vae"
- "Optional NVFP4 text encoder (Blackwell only): hf download FastVideo/FastVideo-FastH3-8-Step-V2-NVFP4-Consumer --include 'text_encoder/*' --local-dir fasth3-nvfp4-encoder"
options: *omniref_options
overrides:
default_request.sampling.num_frames:
@@ -142,5 +145,6 @@ deployments:
directories in server.media_roots.
- Based on the maintained inference recipe; this serving adaptation has not been GPU-validated.
- "Optional NVFP4 decoder (Blackwell only): hf download FastVideo/FastH3-NVFP4-VAE-Decoder --include 'quality-36L/decoder-nvfp4.safetensors' 'fast-8L/decoder-nvfp4.safetensors' --local-dir fasth3-nvfp4-vae"
- "Optional NVFP4 text encoder (Blackwell only): hf download FastVideo/FastVideo-FastH3-8-Step-V2-NVFP4-Consumer --include 'text_encoder/*' --local-dir fasth3-nvfp4-encoder"
options: *omniref_options
overrides: *omniref_overrides
+2
View File
@@ -21,6 +21,7 @@ deployments:
- Based on the released one-Spark inference recipe. This serving adaptation and edited hardware combinations have not been GPU-validated.
- The checkpoint includes NVFP4 weights and encoder, a lightweight video VAE, and its trained eight-forward schedule. No local CompactH3 assembly is needed.
- "Optional NVFP4 decoder (Blackwell only): hf download FastVideo/FastH3-NVFP4-VAE-Decoder --include 'quality-36L/decoder-nvfp4.safetensors' 'fast-8L/decoder-nvfp4.safetensors' --local-dir fasth3-nvfp4-vae"
- "Optional NVFP4 text encoder (Blackwell only): hf download FastVideo/FastVideo-FastH3-8-Step-V2-NVFP4-Consumer --include 'text_encoder/*' --local-dir fasth3-nvfp4-encoder"
options:
- server.*
- generator.engine.num_gpus
@@ -30,6 +31,7 @@ deployments:
- generator.engine.offload.vae
- generator.pipeline.experimental.video_decode_backend
- generator.pipeline.experimental.h3_vae_nvfp4_decoder
- generator.pipeline.components.text_encoder_weights
- default_request.prompt
- default_request.sampling.num_frames
- default_request.sampling.height
@@ -0,0 +1,18 @@
title: Open-H3-Regenerate-2K
summary: Regenerate a 768P MiniMax-H3 draft at 2560x1440 with three refine steps.
deployments:
cuda-rest:
runtime: fastvideo-cuda-rest
guide: docs/inference/open_h3_regenerate_2k.md
workload: v2v
defaults: examples/serving/openai_open_h3_regenerate_2k.yaml
requirements:
- 'UV_TORCH_BACKEND=cu130 uv pip install -e ".[fasth3]"'
- "One GPU with about 150 GB free (B200 or GB200). Send the draft as video_reference; local paths must sit under server.media_roots."
- "Optional NVFP4 decoder (Blackwell only): hf download FastVideo/FastH3-NVFP4-VAE-Decoder --include 'quality-36L/decoder-nvfp4.safetensors' --local-dir fasth3-nvfp4-vae"
options:
- server.*
- generator.engine.offload.text_encoder
- generator.pipeline.experimental.h3_vae_nvfp4_decoder
- default_request.sampling.seed
@@ -88,8 +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_quantize_dense: "Quantize the loaded MiniMax-H3 decoder to NVFP4 in memory without changing its depth; carried through generator.pipeline.experimental."
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."
@@ -150,9 +149,22 @@ surfaces:
preprocess_text_funcs: "Internal text preprocessing hooks."
postprocess_text_funcs: "Internal text postprocessing hooks."
scheduler_step_in_fp32: "Runtime scheduler precision toggle; not part of the public typed inference API."
output_channels: "Decoded pixel channel count fixed by the model; used to allocate the output buffer."
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
@@ -443,6 +455,18 @@ surfaces:
sources: [fastvideo.configs.pipelines.kandinsky6.Kandinsky6TI2VAConfig]
sample_fps:
sources: [fastvideo.configs.pipelines.kandinsky6.Kandinsky6TI2VAConfig]
reference_resolution:
target: generator.pipeline.experimental.reference_resolution
sources: [fastvideo.configs.pipelines.qwen_image21.QwenImage21PipelineConfig]
use_kv_cache:
target: generator.pipeline.experimental.use_kv_cache
sources: [fastvideo.configs.pipelines.qwen_image21.QwenImage21PipelineConfig]
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."
@@ -456,11 +480,14 @@ 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."
vae_stride: "MagiHuman internal VAE/data-proxy stride setting."
vsa_ref_keep_rate: "MiniMax-H3 PDD reference-video VSA keep rate; defaults to the checkpoint's trained value."
video_scheduler_shift: "MiniMax-H3 training-time video noise clock; inference samples with the checkpoint's scheduler config."
audio_scheduler_shift: "MiniMax-H3 training-time audio noise clock; inference samples with the checkpoint's scheduler config."
z_dim: "MagiHuman internal VAE latent channel setting."
vocoder_config: "Legacy internal component config object."
vocoder_precision: "Precision override pending dedicated component precision design."
@@ -601,6 +628,7 @@ surfaces:
audio_reference: request.inputs.references
video_path: request.inputs.video_path
video_url: request.inputs.video_path
continuation_video: request.inputs.references
video_params: request.sampling.width,height,num_frames,fps
size:
target: request.sampling.width,height
+21 -6
View File
@@ -88,12 +88,27 @@ block. If `num_frames` is absent, `seconds * fps` is used. FastVideo also keeps
the legacy `input_reference`, `reference_url`, `video_path`, and `video_url`
spellings.
Reference objects support URL or local-path strings through `image_url`,
`video_url`, and `audio_url`. `file_id` references are schema-compatible but
return HTTP 400 because FastVideo does not provide an OpenAI Files store.
Image URLs, data URLs, local paths, and multipart `input_reference` uploads are
materialized and decoded under the configured output directory during
admission. Invalid media returns HTTP 400 before a job is created.
Reference objects carry one string each through `image_url`, `video_url`, and
`audio_url`. Each field accepts these forms:
| Field | `http(s)` URL | `data:` URL | Local path |
| --- | --- | --- | --- |
| `image_reference[].image_url`, `input_reference`, `reference_url` | yes | `data:image/...;base64,` | under `server.media_roots` |
| `video_reference[].video_url`, `video_path`, `video_url`, `continuation_video` | yes | `data:video/...;base64,` | under `server.media_roots` |
| `audio_reference[].audio_url` | yes | `data:audio/...;base64,` | under `server.media_roots` |
`input_reference` also accepts a multipart upload. `file_id` references are
schema-compatible but return HTTP 400 because FastVideo does not provide an
OpenAI Files store.
URLs must resolve to public addresses unless
`server.allow_private_media_urls` is set. A `data:` URL must be base64-encoded,
and its media type must match its field: a `data:audio/...` URL in
`video_reference` is refused. Each downloaded file, and each decoded video or
audio file, is limited to `server.media_reference_max_bytes`. URLs and `data:` URLs are
written under `server.output_dir/uploads` during admission, keep their order
within each field, and are deleted when the job ends. Invalid, oversized, or
mismatched media returns HTTP 400 before a job is created.
## Jobs and synchronous responses
@@ -79,6 +79,11 @@ The opening frame must be `session_init_v2`. Any other first frame is
rejected with an `error` (code `invalid_message`) and the WebSocket is
closed.
Client frames are JSON text. A binary frame, or text that is not JSON, is
answered with an `error` (code `invalid_message`) as well: as the opening
frame it closes the WebSocket with 1003 `invalid_init`; later in the session
the error is `retryable` and the session continues.
### Server → client
| `type` | Carries | When emitted |
+1 -1
View File
@@ -35,7 +35,7 @@ The preprocessing steps are identical to training. Run the appropriate preproces
```bash
# For Wan2.1 T2V
bash scripts/preprocess/v1_preprocess_wan_data_t2v
bash scripts/preprocess/v1_preprocess_wan_data_t2v.sh
# For Wan2.2 TI2V
bash examples/distill/Wan2.2-TI2V-5B-Diffusers/crush_smol/preprocess_wan_data_ti2v_5b.sh
+4 -4
View File
@@ -11,16 +11,16 @@ Attention. See [Attn-QAT Training](../training/attn_qat.md).
## 📊 Model Overview
We provide two distilled models:
We provide three distilled models:
- **[FastWan2.1-T2V-1.3B-Diffusers](https://huggingface.co/FastVideo/FastWan2.1-T2V-1.3B-Diffusers)**: 3-step inference, up to **16 FPS** on H100 GPU
- **[FastWan2.1-T2V-14B-480P-Diffusers](https://huggingface.co/FastVideo/FastWan2.1-T2V-14B-480P-Diffusers)**: 3-step inference, up to **60x speed up** at 480P, **90x speed up** at 720P for denoising loop
- **[FastWan2.2-TI2V-5B-FullAttn-Diffusers](https://huggingface.co/FastVideo/FastWan2.2-TI2V-5B-FullAttn-Diffusers)**: 3-step inference, up to **50x speed up** at 720P for denoising loop
Both models are trained on **61×448×832** resolution but support generating videos with **any resolution** (1.3B model mainly support 480P, 14B model support 480P and 720P, quality may degrade for different resolutions).
All three models are trained on 61 frames at 448×832 but support generating videos with **any resolution** (1.3B model mainly support 480P, 14B model support 480P and 720P, quality may degrade for different resolutions).
## ⚙️ Inference
First install [VSA](../attention/vsa/index.md). Set `MODEL_BASE` to your own model path and run:
First install [VSA](../attention/vsa/index.md), then run the 1.3B config. The config pins the checkpoint; edit `model_path` there to use a local copy.
```bash
FASTVIDEO_ATTENTION_BACKEND=VIDEO_SPARSE_ATTN \
@@ -45,7 +45,7 @@ python scripts/huggingface/download_hf.py \
### Wan2.1 1.3B Model Sparse-Distill
For the 1.3B model, we use **4 nodes with 32 H200 GPUs** (8 GPUs per node):
For the 1.3B model, we use **8 nodes with 64 H200 GPUs** (8 GPUs per node):
```bash
# Multi-node training (8 nodes, 64 GPUs total)
+2
View File
@@ -36,8 +36,10 @@ COOKBOOK_FAMILIES = {
"kandinsky5",
"kandinsky6",
"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.
@@ -149,39 +149,6 @@ fastvideo generate --config examples/inference/basic/basic_fasth3_spark_pair.yam
Stop the cluster when you are done: `ray stop` on both nodes.
For eager FastH3 FP4 attention, `generator.pipeline.experimental.h3_vsa_fp4.sp_head_chunks`
overlaps head-group communication with independent attention work. It defaults to `1`.
The head count must divide evenly by `sp_size * sp_head_chunks`; on a two-Spark pair FastH3's
56 heads allow 2, 4, 7, 14 or 28 chunks. The pair example config uses 7 (four heads per group). The grouped path packs directly into each exchange buffer and writes
received heads into their original output positions. Changing the attention batch shape can change
floating-point reductions, so compare generated quality as well as wall time for your checkpoint.
The optional `h3_vsa_fp4.sp_projection_overlap: true` starts each Q/K/V exchange as soon as its
projection is packed, overlapping traffic with the remaining projection GEMMs. It defaults to
`false` and requires two ranks, head dimension 128, eager BF16 inference and compatible NVFP4
QKV/gate projections with shared input quantization. Other configurations use the ordinary
head-group exchange. Packing arithmetic and FP8 transport precision are unchanged.
The optional `h3_vsa_fp4.sp_input_gather: true` gathers the existing packed FP4 inputs and
rotary tables before computing each rank's assigned Q/K/V heads. The gate projection overlaps
that smaller input transfer. It defaults to `false`, requires `sp_projection_overlap: true`,
and currently supports the measured GB10/cuDNN shape: 7430 local rows, 5376 input channels,
56 heads and bias-free NVFP4 QKV projections. Other shapes use the existing exchange path.
All projections, normalization, rotary operations and FP8 packet rounding remain present;
no steps or tokens are omitted. Validate generated output when changing this GEMM shape.
The optional `h3_vsa_fp4.sp_pooled_key_mean: true` reuses tile statistics for key smoothing
on the grouped tile-64 path with FP8 prefix attention and `smooth_k: true`. It defaults to
`false`. All key rows still contribute, including zero padding in the normalization length,
but the changed reduction order can alter generated samples. Validate quality for the checkpoint
and workload before enabling it.
The optional `h3_vsa_fp4.sp_direct_keys: true` avoids the intermediate BF16 key tensor on
that pooled-mean path. It defaults to `false`. Key preparation reads the current exchange's
FP8 packets and applies the same FP32 multiply, BF16 rounding and smoothing as the ordinary
receive path. All keys and their tile statistics still contribute; other attention configurations
retain ordinary key stores.
## Released V2, FastH3-QAD-v2 and Trim NVFP4 stacks
The public eight-forward V2 and Trim stacks include the NVFP4 encoder and
@@ -189,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
@@ -390,9 +390,10 @@ off on other GPUs unless a matched benchmark validates the split there.
`FASTVIDEO_H3_VAE_NVFP4_DECODER=/path/to/decoder.pt` swaps in an NVFP4
decoder distilled with quantization-aware training and exported by
`scripts/distill/minimax_h3_nvfp4_decoder/export_deploy.py`. At load the
INT8 overlay is skipped, the VAE uses the exported decoder's block count
(a complete export can also replace a shallower light decoder when its
head/width geometry matches), and only the packed FP4 weights stay resident (0.31 GiB for 8 blocks instead of
INT8 overlay is skipped, the VAE is depth-cut to the decoder's block count
(the 26-layer light VAE hosts an 8-block decoder; a 36-block decoder needs
the full `MiniMaxAI/MiniMax-H3` `vae/` in the stack's `vae/` folder), and
only the packed FP4 weights stay resident (0.31 GiB for 8 blocks instead of
2.3 GiB). The decode stage runs bf16 autocast for these decoders, the
precision they were trained in. The decoder GEMMs use the cuDNN backend on
GB10; outputs match CUTLASS bit for bit.
@@ -405,16 +406,6 @@ read straight onto the GPU and never materializes the full-precision weights
of the quantized linears; it decodes bit-identically to packing the bf16 file
at load.
To quantize the original decoder without replacing it with a distilled export,
set `generator.pipeline.experimental.h3_vae_nvfp4_quantize_dense: true` and
`generator.engine.compile.vae_enabled: false`. This experimental option keeps
every decoder block, quantizes its block linears to NVFP4 with unit activation
scale during loading, and uses the existing fused BF16 decoder path. It leaves
the checkpoint files unchanged and skips the INT8 overlay. Clear
`FASTVIDEO_H3_VAE_NVFP4_DECODER` and `h3_vae_nvfp4_decoder`: exported and runtime
quantization selections are mutually exclusive. The output is lossy and needs
quality validation for the selected checkpoint. Blackwell FP4 hardware is required.
V2 at 832x480, 124 frames, seed 1234, one Spark, warm calls:
| Video decoder | End to end | Denoise | Video decode | Peak allocated |
+25 -9
View File
@@ -1,28 +1,44 @@
# 🚀 Quick Start
# Quick start
Get up and running with FastVideo in minutes!
Install FastVideo, generate one video, then start from a maintained recipe.
## Installation
## Install
First, install FastVideo:
Requires Python 3.10 to 3.12 and CUDA 12.6 or newer.
```bash
# If you previously used Conda, use uv instead for a faster, more stable setup
uv venv --python 3.12 --seed
source .venv/bin/activate
# Install FastVideo on NVIDIA CUDA 12
# CUDA 12. Use cu130 on CUDA 13.
UV_TORCH_BACKEND=cu126 uv pip install fastvideo
```
Use `UV_TORCH_BACKEND=cu130` instead on CUDA 13.
Also optionally install flash-attn:
For the fastest attention kernels, install flash-attn:
```bash
uv pip install flash-attn --no-build-isolation -v
```
## Generate a video
```python
from fastvideo import VideoGenerator
generator = VideoGenerator.from_pretrained(
"Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
num_gpus=1,
text_encoder_cpu_offload=True,
)
generator.generate_video(
"A raccoon peers through a field of sunflowers.",
output_path="videos/",
save_video=True,
)
```
The first run downloads the Wan2.1 1.3B checkpoint. The call writes an MP4 under `videos/`. The full example is `examples/inference/basic/basic.py`.
## Choose a maintained recipe
The cookbook selects complete, checked-in recipes instead of mixing model,
+36
View File
@@ -26,3 +26,39 @@ to receive the decoded CPU tensor in `GenerationResult.samples` without building
preview grids or writing media. Pixel samples retain the existing float `[0, 1]`
contract. `return_frames=True` continues to return both samples and preview frames.
The legacy sampling/keyword interface accepts the same `return_samples` option.
### Choosing an output filename
Set `request.output.output_video_name` to choose a filename when
`request.output.output_path` is a directory:
```python
result = generator.generate({
"prompt": "A waterfall in the mountains",
"output": {"output_path": "outputs/", "output_video_name": "my_clip"},
})
# Saves outputs/my_clip.mp4 when that file does not already exist.
```
An explicit file path such as `outputs/custom.mp4` takes precedence over
`output_video_name`. Without a name, the filename is derived from the prompt
(or the input video for prompt-less super-resolution pipelines). Names are
sanitized, and existing files are preserved by adding numeric suffixes such as
`my_clip_1.mp4`, including when processing multiple prompts. A matching extension
in the name is not duplicated. Image workloads accept PNG, JPEG, or WebP names
and default to PNG; audio workloads use WAV.
For requests that save media, the sanitized filename is checked against the
destination filesystem's filename byte limit, including its extension and any
collision suffix. If the limit cannot be determined, 255 bytes is used.
Overlong names are rejected before model execution; use a shorter name rather
than relying on automatic truncation. Multibyte characters may use more than
one byte each. Prompt-file batches retain their existing per-prompt error
handling.
Requests with `save_video=False` or `output_type="latent"` do not prepare an
output file: filename validation, directory creation, and collision checks are
skipped. This also applies to latent output when `save_video=True`.
The legacy `SamplingParam` and `generate_video` keyword interfaces accept the
same `output_video_name` option.
+1 -1
View File
@@ -1,6 +1,6 @@
# Welcome to FastVideo
FastVideo is an inference and post-training framework for diffusion models. It features an end-to-end unified pipeline for accelerating diffusion models, starting from data preprocessing to model training, finetuning, distillation, and inference. FastVideo is designed to be modular and extensible, allowing users to easily add new optimizations and techniques. Whether it is training-free optimizations or post-training optimizations, FastVideo has you covered.
FastVideo is an inference and post-training framework for video diffusion models. One pipeline covers data preprocessing, fine-tuning, distillation, and inference. Attention backends, parallelism, and quantization are separate modules, so you can add one without changing model code.
## Key Features
+3 -3
View File
@@ -90,7 +90,7 @@ PipelineConfig (fastvideo/configs/pipelines/base.py)
│ ├── WanT2V720PConfig
│ └── WanI2V480PConfig
├── HunyuanConfig (fastvideo/configs/pipelines/hunyuan.py)
├── LTX2T2VConfig (fastvideo/configs/pipelines/ltx2.py)
├── LTX2T2VConfig (fastvideo/pipelines/basic/ltx2/pipeline_configs.py)
├── CosmosConfig (fastvideo/configs/pipelines/cosmos.py)
└── ... (15+ model families)
```
@@ -136,7 +136,7 @@ Concrete hierarchy: `DiTConfig` → `DiTArchConfig`, `VAEConfig` →
### SamplingParam (`fastvideo/api/sampling_param.py`)
Generation parameters separate from pipeline config. Each model family
provides defaults via a profile (see `fastvideo/pipelines/basic/<family>/profiles.py`):
provides defaults via a profile (see `fastvideo/pipelines/basic/<family>/presets.py`):
```python
sp = SamplingParam.from_pretrained("Wan-AI/Wan2.1-T2V-1.3B-Diffusers")
@@ -505,7 +505,7 @@ User: generator.generate_video(prompt, ...)
flow_shift, precision defaults.
2. **Sampling param profile** — Create a profile in
`fastvideo/pipelines/basic/<model>/profiles.py` with default height,
`fastvideo/pipelines/basic/<model>/presets.py` with default height,
width, num_frames, guidance_scale, num_inference_steps.
3. **Register configs** — In `fastvideo/registry.py`, add a
+1 -1
View File
@@ -72,7 +72,7 @@ def main():
sampling_param.guidance_scale = 7.5
# Random seed for reproducibility
sampling_param.seed = 42 # Optional, leave unset for random results
sampling_param.seed = 42 # Optional; the default is 1024
# Generate video with custom parameters
prompt = "A beautiful sunset over a calm ocean, with gentle waves."
+32
View File
@@ -148,6 +148,38 @@ Select the NVFP4 decoder per configuration with
holds base MiniMax-H3's full 36-block VAE, so both the 36-block quality tier and
the 8-block fast tier apply.
## Continuing a video
A Ref2VA request can continue an earlier video. It takes the earlier video's last
5, 22 or 39 frames (and their sound, if any) as a continuation clip, and the
generated video starts by regenerating those frames at the same positions: the
clip's latent rows take the coordinates of the target's first latent frames and
audio latents, the reference context of
[ComfyUI-H3-Continuum](https://github.com/ukr8b3g-cmyk/ComfyUI-H3-Continuum).
Drop the clip's frames, and the matching audio, from the new video, then append
the rest to the earlier one.
- Over HTTP, set `continuation_video` to the clip's URL, a base64
`data:video/...` URL or a local path (under `server.media_roots`) beside the
usual references. In Python, append
`MiniMaxH3Reference(source=clip, media_type="continuation")` to `references`.
- The clip is resampled to 24 fps and must then have 5, 22 or 39 frames on the
target canvas. A request takes at most one clip and still needs at least one
reference; the clip does not count toward the reference limits and is not
shown to the text encoder.
- `num_frames` counts the clip: 22 frames plus five new seconds is 142,
aligned up to 158.
- Write the prompt from the clip's actual end state. When the prompt's opening
contradicts the clip, the model tends to cut to a new shot right after it.
- Each continuation drifts a little toward higher saturation and sharpness, so
fewer, longer segments (two 15-second ones rather than six 5-second ones) hold
up better.
Measured on a FastH3 OmniRef PDD student at 1344×768, 22-frame clips re-encoded
from the earlier output: the jump at a join was about half that of conditioning
the next segment on the earlier last frame as an image reference, and the sound
continued instead of restarting from near silence.
## Environment
The profiles set these; set them yourself when driving the YAML recipes.
+28
View File
@@ -73,6 +73,34 @@ Quantized checkpoints pick a linear path per layer. `--quant-gemm` (or
The same flag is on the example: `--quant-gemm {auto,dequant,qmm}`.
### Opt-in M5 attention backend
`--vsa-impl sdpa` evaluates the trained VSA tile mask through fused MLX SDPA,
sharing each attention head's K/V across its query tiles. On M5, the built-in
matrix operations can be faster than the custom SIMD kernel even though they
visit masked keys. The default `auto` selection is unchanged.
This backend keeps the reference resolution, selected tiles, ragged-row masks,
and compression gate. Accumulation differs from the SIMD backend, so outputs
are not bit-identical. It supports tiles 64/128 with head dimension 128 and
raises if fused SDPA is unavailable. Prefix K/V reuse is not supported; do not
combine it with `--reuse-prefix-kv`. Query tiles are processed in chunks of at
most 128, capped at 256 MiB per boolean-mask chunk with two chunks in flight.
`--vsa-impl nax` is a separate opt-in backend for Apple M5 GPUs. It uses Metal
TensorOps to visit only the selected key blocks and their valid rows, avoiding
the dense mask and masked-key traversal of the SDPA backend. Dense prefix
queries and the trained compression branch keep their existing implementations.
The selected reference tokens, output resolution and denoising steps are unchanged.
The NAX backend uses private helper headers from the installed MLX package;
it accepts only the header hashes validated with MLX 0.32.4. Changed headers need
numerical and generation validation before their hashes are accepted. Missing headers, unsupported GPUs
and compilation failures raise errors rather than silently selecting another
backend. Tiles 64/128 and head dimension 128 are supported. Prefix K/V reuse is
not supported. Floating-point accumulation differs from both SIMD and SDPA,
so the generated clip can differ even with the same seed. `auto` remains unchanged.
## Generate
```bash
+58
View File
@@ -109,6 +109,64 @@ the consumer profiles already use, for packed NVFP4 and for bf16. Pass `lora_pat
adapter is attached when the transformer arrives: as a bf16 side branch beside packed weights, or merged into a
dense bf16 transformer. You do not load the LoRA as a separate resident copy of the model.
## Per-request adapters on the OpenAI API
`fastvideo serve` can hold an allow-list of adapters and apply one per request on `POST /v1/videos/sync` (the whole
MP4 comes back in the response) and on the async `POST /v1/videos` job route. Name each adapter and its strength
range in the serve config; see `examples/serving/openai_fasth3_nvfp4_lora_allowlist.yaml`:
```yaml
generator:
model_path: /models/FastVideo-FastH3-8-Step-V2.1-NVFP4-Consumer
engine:
quantization:
transformer_quant: NVFP4 # required: per-request adapters need the packed NVFP4 side-branch layers
layer_profile: h3_dit_vsa
lora:
adapters:
- id: fal/MiniMax-H3-Realism-People-LoRA
path: fal/MiniMax-H3-Realism-People-LoRA
default_strength: 0.8
min_strength: 0.0
max_strength: 1.5
license: Apache-2.0
source_url: https://huggingface.co/fal/MiniMax-H3-Realism-People-LoRA
```
An allow-list cannot be combined with a startup adapter (`generator.pipeline.components.lora_path`); the server
refuses to start with both. Without an allow-list, the startup-adapter behavior is unchanged.
`GET /v1/model_info` reports the allow-list in its `lora` block, never the load paths. A request selects one
adapter with the vLLM-style `lora` object:
```bash
curl -s http://127.0.0.1:8000/v1/videos/sync \
-H 'content-type: application/json' \
-d '{"prompt": "...", "size": "1344x768", "num_frames": 124, "seed": 7,
"lora": {"name": "fal/MiniMax-H3-Realism-People-LoRA", "scale": 0.8}}' \
-o out.mp4
```
`name` (or `lora_name`, `adapter`) must be an allow-listed `id`. `scale` (or `lora_scale`) is the strength; it
defaults to the adapter's `default_strength` and must lie in `[min_strength, max_strength]`. A client-supplied `path`
is rejected. A bad selection gets HTTP 400 before any job is stored.
Strength `0` is exactly the base model, with output identical to a request without `lora`. A request without `lora`
sets the last-used adapter to strength 0 instead of unloading it, so one request never inherits another's
adapter. The server applies the adapter under the same lock as the generation, so requests never interleave. If
a change fails, that request fails with the loader's message, and the server clears every LoRA layer back to the
base weights without reading an adapter file, so it never renders with an adapter the request did not select and
requests without `lora` keep working.
Per-request adapters need side-branch LoRA layers, so the server refuses an allow-list unless the model is a packed
NVFP4 export (a `transformer/nvfp4_weights.safetensors` file with `transformer_quant: NVFP4`) or
`FASTVIDEO_LORA_SIDE_BRANCH=1` is set. At startup it also checks that every adapter resolves to a safetensors file
and is purely low-rank: adapters with dense replacement payloads (`.diff` and `.set_weight` tensors, as in the
[FastH3 adapter](fasth3-distilled.md)) can only be applied when the model loads, and the server refuses to start with
one. A side-branch layer holds one adapter at a time. Linears that a packed export leaves unquantized still merge the
adapter into their weights; switching to another adapter first restores each of them, including those the new
adapter does not cover.
## Limitations
- Block-pruned models (FastH3 Trim, 42 of 50 blocks) are not supported yet. Community LoRAs index the full
+278
View File
@@ -0,0 +1,278 @@
# Open-H3-Regenerate-2K
`OpenH3Regenerate2KPipeline` regenerates a 768P [MiniMax-H3](https://huggingface.co/MiniMaxAI/MiniMax-H3) draft video
(1344x768, 121 frames, 24 fps) at **2560x1440** with three refine steps. It is video to video: the draft is the input,
and the output is a silent 2K MP4. FastVideo ports NVlabs'
[Open-H3-Regenerate-2K](https://huggingface.co/Efficient-Large-Model/Open-H3-Regenerate-2K)
([project page](https://nvlabs.github.io/Sana/Open-H3-Regenerate-2K/),
[reference code](https://github.com/NVlabs/Sana/tree/sol-engine/models/Open-H3-Regenerate-2K)); the dense BF16
`standard_fwd` path of that repo is the parity reference. The accelerated `sol_engine_fwd` path (Sol-Attn, MXFP8, CUDA
graphs) and the fast 24-layer VAE decoder are not ported.
```text
draft video -> H3 VAE encode -> latent upscaler (2x, 3D conv) -> 3 Euler refine steps -> H3 VAE decode -> 2560x1440
H3 DiT + merged LoRA + Sharp curve AdaLN branches
```
## What runs
| Step | Details |
|---|---|
| Draft encode | `ffmpeg` resamples to 24 fps, cover-scales (bilinear) and centre-crops to 1344x768 (`draft_size: 768p`, or 1024x576 for `576p`), holds the last frame to 121 frames plus 3 copies. The FP32 H3 VAE posterior is sampled under seed 42. |
| Latent upscale | The author's 3D-conv upscaler (`fastvideo/models/upsamplers/minimax_h3_latent_upscaler.py`), BF16, to the 160x90 latent grid. |
| Refine | Euler on sigmas `[0.909375, 0.857143, 0.666667, 0]`, CFG 1, model time `1 - sigma`, initial state `(1 - 0.909375) * upscaled + 0.909375 * noise`. `num_inference_steps: 2` uses `[0.7, 0.494118, 0]` (softer, about 30% faster). |
| Transformer | The base MiniMax-H3 DiT (the same module as the other H3 pipelines) with the released merged LoRA (FP32, rank 576 on 312 linears, scale 1) and 50 LMS Sharp curve AdaLN branches. |
| Decode | The released H3 VAE, or an NVFP4 decoder on Blackwell (see below); the output is trimmed to 121 frames. |
The merged LoRA loads through the standard H3 community-LoRA path as a constructor-time adapter: the pipeline sets
`lora_path` to `<model>/lora/open_h3_regenerate_2k_lora.safetensors` when you do not pass one. The Sharp branches take an
8-D timestep curve (`lora/adaln_t_table.bin`, 1025x8) instead of the layer input, so they cannot be folded into weights;
`sharp_curve.py` adds them to the AdaLN projection outputs with forward hooks.
**Audio.** The reference feeds an encoded-silence audio stream to the DiT as conditioning and writes the video with no
audio track (`-an`); it neither regenerates nor passes through the draft's audio. FastVideo does the same: the draft's
soundtrack is ignored and the output MP4 has no audio.
## Weights
The release repo keeps the unmodified MiniMax-H3 components (a copy of `MiniMaxAI/MiniMax-H3` at `6818f6c`) under
`base_model/` next to `lora/` and `latent_upsampler/`. Point `model_path` at the Hub id or at a local copy of that layout.
`adaln_t_table.bin` is base-derived and is not redistributed in the release repo: put the 32,800-byte file (SHA-256
`c02a6c11888297688c1e6278185ea1f947023acfc69f9003bbcdcec9a229a8e7`) at `lora/adaln_t_table.bin`. The reference
`download.py` fetches it by byte range from `Comfy-Org/MiniMax-H3` at revision
`e5eb578a89295337b8ff433a035929ce0279e0b6` (tensor `adaln_t_table` of
`diffusion_models/minimax_h3_ref2va_pruned_int8_convrot.safetensors`) and verifies the hash.
A GB200/B200-class GPU is needed (about 150 GB with the DiT, the 2K activations and the VAE).
## Usage
```bash
python examples/inference/basic/basic_open_h3_regenerate_2k.py \
--model-path Efficient-Large-Model/Open-H3-Regenerate-2K \
--video draft_768p.mp4 --prompt "A red fox trots through fresh snow" --output outputs/regen2k
```
The prompt conditions the refine steps (use the draft's own prompt). Drafts from other video models are better encoded at
576P (`--draft-size 576p`). `--steps 2` runs the two-step variant. The output is always 2560x1440; the first 121 frames of
the draft are used.
## Serving
```bash
fastvideo serve --config examples/serving/openai_open_h3_regenerate_2k.yaml
```
`examples/serving/openai_open_h3_regenerate_2k.yaml` loads the release on one GPU, sets `media_roots`, and pins the 2K
defaults. The draft arrives as the typed `video_reference` of `POST /v1/videos/sync` (the response body is the MP4) or of
`POST /v1/videos` (an async job; poll `GET /v1/videos/{id}` and download `GET /v1/videos/{id}/content`):
```bash
curl -sS -X POST http://localhost:8000/v1/videos/sync \
-H 'Content-Type: application/json' \
-d '{
"prompt": "A red fox trots through fresh snow",
"video_reference": {"video_url": "https://example.com/draft_768p.mp4"},
"seed": 1766580932
}' \
-o regen_2k.mp4 -D -
```
The response headers carry `X-Request-Id`, `X-Model`, `X-Inference-Time-S` and `X-Stage-Durations`. Rules:
- Exactly one `video_reference` (or the legacy `video_path` / `video_url`) and no image or audio references.
- `http(s)` URLs must resolve to public addresses unless `server.allow_private_media_urls` is set. A local path is accepted
only inside a directory listed in `server.media_roots`. A base64 `data:video/...` URL is decoded, up to
`server.media_reference_max_bytes`.
- `size`, `width` and `height` may be omitted or `2560x1440`; `num_frames` may be omitted or 124 (the frame count of the
VAE grid; the video has 121 frames); `fps` must be 24. `seconds` is not accepted.
- `num_inference_steps` is 3 (default) or 2; `seed` sets the refine noise.
- Serve with `attention_backend: TORCH_SDPA`: the refine stage selects cuDNN attention, which at about 133k tokens is
about 3x faster than PyTorch's default flash dispatch. The `FLASH_ATTN` backend bypasses that selection.
## NVFP4 video decoder
On Blackwell GPUs the released 36-block video decoder can be replaced by a pre-packed NVFP4 one; the latent space is
unchanged, so the draft encode, the upscale and the refine steps are the same and only the decode changes. Both tiers of
[FastVideo/FastH3-NVFP4-VAE-Decoder](https://huggingface.co/FastVideo/FastH3-NVFP4-VAE-Decoder) apply here, because the
release carries MiniMax-H3's full 36-block VAE (`base_model/vae`):
| Tier | Decoder blocks | File | Pick it when |
|---|---|---|---|
| `quality-36L` | 36 | `quality-36L/decoder-nvfp4.safetensors` | you want the released detail back; the default choice |
| `fast-8L` | 8 (evenly spaced; the load drops 28 blocks) | `fast-8L/decoder-nvfp4.safetensors` | decode time or memory dominate and some fine detail can go |
```bash
hf download FastVideo/FastH3-NVFP4-VAE-Decoder --include 'quality-36L/decoder-nvfp4.safetensors' \
'fast-8L/decoder-nvfp4.safetensors' --local-dir fasth3-nvfp4-vae
```
Select it through the surface you drive the pipeline with. The config field (also a dotted override of
`fastvideo generate`), the example's `--vae-decoder` and the environment variable all set
`fastvideo_args.h3_vae_nvfp4_decoder`, and the config field wins over the environment.
```bash
# example script
python examples/inference/basic/basic_open_h3_regenerate_2k.py --video draft.mp4 --prompt "..." \
--vae-decoder fasth3-nvfp4-vae/quality-36L/decoder-nvfp4.safetensors
# `fastvideo generate`: a run config, with the decoder as a dotted override
fastvideo generate --config regen2k_run.yaml \
--generator.pipeline.experimental.h3_vae_nvfp4_decoder fasth3-nvfp4-vae/quality-36L/decoder-nvfp4.safetensors
```
```yaml
generator:
pipeline:
experimental:
h3_vae_nvfp4_decoder: ./fasth3-nvfp4-vae/quality-36L/decoder-nvfp4.safetensors
```
`FASTVIDEO_H3_VAE_NVFP4_DECODER` stays as the fallback when the config does not set the field. The
swap happens in the VAE loader, so the dense decoder never reaches the GPU: it is replaced on the CPU by the exported
checkpoint (2.0 GiB of packed weights instead of 9.7 GiB of FP32), moved to the device, and decoded under bf16 autocast.
The decoder keeps the pipeline's tiling (at 2560x1440: 8x13 tiles of 256 px with 64 px overlap) and the fused Triton
block kernels, which are on by default (`FASTVIDEO_H3_VAE_NVFP4_FUSED`); `FASTVIDEO_H3_VAE_TILE_BATCH` decodes several
tiles per decoder call (4 is bit-identical and faster here), and `FASTVIDEO_H3_VAE_NVFP4_CUDA_GRAPH` replays the small
fused calls. The MP4 is unaffected: the same 121 frames are trimmed and written.
Warm decode of one refined latent at 2560x1440 (124 frames) on one GB200, median of 3, with the released decode as the
reference:
| Decoder | Decode (s) | Peak GPU memory (GiB) | PSNR (dB) | SSIM |
|---|---:|---:|---:|---:|
| released H3 VAE (dense, fp16 autocast) | 23.3 | 18.4 | - | - |
| `quality-36L` (fused) | 6.6 | 6.2 | 41.3 | 0.979 |
| `quality-36L` (fused, tile batch 4) | 5.0 | 6.3 | 41.3 | 0.979 |
| `quality-36L` (eager NVFP4 GEMM) | 67.0 | 6.2 | 41.3 | 0.979 |
| `fast-8L` (fused) | 1.7 | 5.3 | 32.8 | 0.892 |
| `fast-8L` (eager NVFP4 GEMM) | 16.1 | 5.2 | 32.8 | 0.892 |
The fused kernels are an exact speedup: the eager and fused rows decode bit-identical frames, and so does batching
tiles, so the PSNR/SSIM columns measure the tier and nothing else. The eager NVFP4 GEMM path loses to the dense fp16
decoder at these tile sizes (67 s against 23 s), so keep `FASTVIDEO_H3_VAE_NVFP4_FUSED` on. PSNR against the released
decode is bounded by the released decode itself: the refined latent already sits 45.2 dB from the NVlabs reference (see
below), so 41.3 dB is a decoder-only difference of the same order. `fast-8L` softens fine texture (fur, foliage) while
keeping structure; use it only when the decode budget is tight.
## Refining on a FastH3 DiT
`refine_transformer_dir` swaps the refine DiT for another MiniMax-H3 transformer while the rest of the release (VAEs,
encoders, upscaler, merged LoRA, Sharp branches) stays put. Point it at a transformer component directory, or at a
checkpoint whose `transformer/` subdirectory is one, such as a FastH3 export:
```python
PipelineSelection(components=ComponentConfig(),
experimental={"refine_transformer_dir": "/models/FastH3-QAD-v2"})
```
The released LoRA and Sharp branches are base-H3-derived adapters, so they keep loading over the chosen DiT. A packed
NVFP4 FastH3 export needs its quantization profile
(`EngineConfig(quantization=QuantizationConfig(transformer_quant="NVFP4", layer_profile="h3_dit_vsa"))`) and refuses a
dense attention backend, because its `to_gate_compress` linears are built only for VSA. Do not set
`FASTVIDEO_H3_ADALN_TABLE` for a different sigma grid than the one a checkpoint's `adaln_tables.pt` covers; the refine
grid is not FastH3's 8-step schedule, so load the real AdaLN projections.
This swap does not reproduce the release's refine. On the fox draft, FastH3 V2 BF16 walking its own DMD rungs from 500, the
best adapter subset reached 26 dB PSNR and 0.84 SSIM against the base-H3 refine (the base reproduces itself at 45 dB), with no
faster forwards at this sequence length. Draft with FastH3 and refine with the base DiT instead.
`draft_latent_path` skips the draft video: a `.pt` holding the normalized native H3 draft latent `[C, T, h, w]`
(float32) goes straight to the upscaler, so a draft generated by a FastH3 t2v run never needs decoding to pixels and
re-encoding. Starting from `vae.encode_pixels(...).latent_dist`, sample it and apply `vae.normalize_latents` before
saving, as the draft encoder does. A batched latent is accepted only with a leading dimension of 1.
```bash
python examples/inference/basic/basic_open_h3_regenerate_2k.py --video draft.mp4 --prompt "..." \
--refine-transformer /models/FastH3-8-Step-V2 --attention-backend VIDEO_SPARSE_ATTN_H3
```
## Regenerating a FastH3 draft
Only the draft model changes: a FastH3 checkpoint drafts at 768P (1344x768, 124 frames) and the regen stack stays as
released (base MiniMax-H3, merged LoRA, Sharp branches, three Euler steps). All H3-family drafts share the H3 VAE's latent
space (`[24, 37, 48, 84]` for a 124-frame 768P clip), so a FastH3 draft latent goes straight to the upscaler.
```bash
# FastH3 QAD (V2.1) NVFP4 draft, NVFP4 decode at 2K
python examples/inference/basic/basic_fasth3_to_regen2k.py \
--draft-model FastVideo/FastH3-QAD-v2 --draft-quant h3_dit_vsa \
--vae-decoder fasth3-nvfp4-vae/quality-36L/decoder-nvfp4.safetensors \
--prompt "A red fox trots through fresh snow" --output outputs/fasth3_regen2k
# FastH3 Trim NVFP4 draft: --draft-model FastVideo/FastVideo-FastH3-Trim-8-Step-NVFP4 --draft-quant h3_dit_vsa
# FastH3 V2 BF16 draft: --draft-model FastVideo/FastVideo-FastH3-8-Step-V2 (no --draft-quant)
```
Two handoffs are available:
- **Video:** decode the draft to an MP4 and pass it as `video_path`. The regen decodes it, resamples and VAE-encodes it (about
11 s).
- **Latent:** pass the draft's normalized native latent `[C, T, h, w]` as `draft_latent_path`, or per request as a `.pt`
`video_path` (through the OpenAI server it must be a local path under `server.media_roots`; not exercised in this run). The encode drops to 0.6 s and the
draft is never decoded to pixels.
The two give different 2K videos (24-27 dB PSNR against each other). On smooth content the MP4 route is much sharper (a
portrait: Laplacian variance 180-244 against 84-106 for the latent route). Writing the same draft as a lossless MP4 gives 85,
close to the latent route, so the extra energy comes from the H.264 artifacts of the draft MP4, which the refine turns into
texture. The latent route also leaves PSNR against the draft within 0.8 dB of the MP4 route. Prefer it: it is faster and does
not amplify codec noise.
Measured on one GB200 with nothing else on the node (regen at `TORCH_SDPA`, seed 1766580932, warm requests; fox, portrait
and harbor prompts, 5.2 s clips):
| Draft model | Draft | Regen: MP4 / latent / latent + NVFP4 decoder | Text to 2K (latent handoff) |
|---|---:|---:|---:|
| FastH3 QAD V2.1 NVFP4 | 36 s | 106 s / 96 s / 79 s | 132 s (115 s) |
| FastH3 Trim NVFP4 | 34 s | 106 s / 96 s / 79 s | 130 s (113 s) |
| FastH3 V2 BF16 | 35 s | 106 s / 96 s / 79 s | 131 s (114 s) |
| base H3 50-step | 316 s | 106 s / 96 s / 79 s | 412 s (395 s) |
The regen costs the same for every draft: about 70 s of refine (3 forwards at 133k tokens) and 24 s of decode. Setting
`h3_vae_nvfp4_decoder` to `quality-36L/decoder-nvfp4.safetensors` from `FastVideo/FastH3-NVFP4-VAE-Decoder` cuts the decode
to 7 s (82 s on the first call of a process) with no visible metric change. Against the draft, the 2K output keeps the scene
(PSNR 19-22 dB at 768P: the refine regenerates texture, so pixel agreement stays low for base and FastH3 drafts alike) and
raises Laplacian variance 1.7-3.3x with a 1.3-1.5x flicker ratio for every draft model. FastH3 drafts are sharper than base
drafts before the regen, so their absolute PSNR against the draft is about 2 dB lower.
## Parity with the reference
Measured on one GB200 (BF16, `TORCH_SDPA` with cuDNN attention) against the reference `standard_fwd` with the released
VAE, same draft, seed (1766580932), prompt and VAE posterior seed (42). The reference's noise is a CUDA generator draw
that FastVideo reproduces (the same `torch.Generator(device="cuda")` seed), so no injected noise was needed.
| Stage / quantity | Fox draft (FastVideo H3 t2v, 768P) | Dance draft (576P, project page) |
|---|---|---|
| Draft latent, upscaled latent, DiT input | bit-identical | bit-identical |
| One DiT forward (LoRA + Sharp), relative L2 | 1.1e-2 | 2.6e-2 |
| Refined latent, relative L2 | 1.3e-2 | 3.7e-2 |
| Decoded video PSNR / SSIM (21 frames) | 45.2 dB / 0.993 | 42.1 dB / 0.992 |
The differences come from text-encoder rounding (relative L2 1e-3 on the prompt embeddings) and BF16 kernel choices;
the reference reports the same order (1.4% relative L2 on the refined latent between its own runs).
Warm timings, one GB200 (seconds):
| | Upscale | Refine (3 steps) | VAE decode | Total |
|---|---:|---:|---:|---:|
| Reference `standard_fwd` | 0.87 | 71.6 | 15.0 | 87.5 |
| FastVideo (end to end, incl. draft encode 5-10 s and decode to host) | 5.9-10.7 (encode + upscale + silence) | 69.5 | 25.0 | 107.5 |
The first request builds nothing extra but loads weights (about 70 s once). The decode stage is slower than the
reference because it also copies 121 frames to host memory and the MP4 encode is outside both columns.
## Licences
The release is not covered by one licence; its `README.md` says so, and the runtime code's licence does not replace the
model licences. FastVideo only loads these weights and does not redistribute them.
- The refiner LoRA, the fast H3 VAE decoder and `base_model/` (a copy of `MiniMaxAI/MiniMax-H3` at `6818f6c`: DiT, VAEs,
text encoder) follow the
[MiniMax H3 Community License](https://huggingface.co/MiniMaxAI/MiniMax-H3/blob/6818f6c32d12b210915e44ad56a4228c2608f160/LICENSE),
including its territory, use and redistribution terms (`base_model/LICENSE`, `NOTICE`). The Qwen3-VL text encoder's
Apache-2.0 text ships as `LICENSE-Qwen.txt`.
- The latent upscaler follows its own licence:
[LBH-123-AI/Comfyui_Minimax_h3_latent_Upscaler](https://github.com/LBH-123-AI/Comfyui_Minimax_h3_latent_Upscaler)
(MIT, per the reference code) and its [weights](https://huggingface.co/LBH-123-AI/Minimax_h3_latent_Upscaler)
(`latent_upsampler/LICENSE`, Apache-2.0). `minimax_h3_latent_upscaler.py` is a port of that node's network.
- The merged LoRA includes [LightX2V FL2VA Turbo](https://huggingface.co/lightx2v/Minimax-h3-Turbo) and
[LMS Sharp](https://huggingface.co/Alissonerdx/Minimax-H3-ComfyUI), which keep their own licences; the Sharp
`adaln_t_table` is read from [Comfy-Org/MiniMax-H3](https://huggingface.co/Comfy-Org/MiniMax-H3).
+18
View File
@@ -114,6 +114,24 @@ the Preview checkpoint's sparse VSA blocks. Packed-varlen changes floating-point
reduction order relative to fixed-length FA4, so treat it as a speed/quality
evaluation option rather than an exact-parity mode.
### cuDNN SDPA for H3 dense attention (opt-in)
With the `FLASH_ATTN` backend, MiniMax-H3's dense, unmasked, inference-only DiT
self-attention can use cuDNN's fused SDPA engine instead of FlashAttention:
```bash
export FASTVIDEO_MINIMAX_H3_DENSE_ATTN=cudnn
```
It keeps the same precision contract (bf16 operands, fp32 softmax/accumulation,
bf16 output) but uses a different reduction order, so treat it as a
speed/quality option rather than an exact-parity mode. Training calls, compiled
regions, head sizes above 128, non-contiguous inputs, and the sparse VSA blocks
stay on their existing paths. Standalone kernel timings at 37.7k and 109k tokens
are within measurement noise of FA4; an in-model A/B on one GB200 measured a
2.2% faster DiT forward. Requires CUDA and a torch build with
`aten::_scaled_dot_product_cudnn_attention` (cuDNN 9.x).
### FP4 Flash Attention 4 (Blackwell only)
**`FLASH_ATTN`** with **`--nvfp4_fa4`**
+237
View File
@@ -0,0 +1,237 @@
# Pika H3 Flex
Pika H3 Flex is a post-trained MiniMax-H3 model that keeps H3's packed
video/audio transformer, Qwen3-VL conditioner, and video/audio VAEs. Pika ships
two rank-128 PEFT adapters instead of a second base checkpoint:
| Adapter | Trunk | Tasks |
|---|---|---|
| `fl2va/adapter.safetensors` | `transformer` | T2VA, I2VA, L2VA, FL2VA |
| `ref2va/adapter.safetensors` | `transformer_ref` | Ref2VA |
FastVideo detects `Pika-Labs/Pika-H3-Flex`, downloads only the selected task's
adapter, and applies it as an ordinary LoRA. The released adapter is selected
automatically; pass `lora_path` only when overriding it with a compatible
Pika adapter.
## Sampling contract
Pika's sampler is fixed, not a tunable step count:
* 6 transformer forwards over clean-time rungs
`1000, 750, 500, 250, 100, 40, 0`;
* video shift `12.0`, audio shift `3.0`;
* no CFG, `guidance_scale=1.0`, empty negative prompt;
* each step re-noises the predicted clean sample with fresh Gaussian noise from
a generator seeded `seed + 1`.
`num_inference_steps` must therefore be `6`; other values are rejected. This is
not a PDD checkpoint and it has no trained VSA gates, so PDD fused blocks and
VSA sparsity are not used.
## Tasks
The commands below assume a FlashAttention-4 host; see
[Attention backend](#attention-backend) before running them elsewhere.
```bash
export FASTVIDEO_ATTENTION_BACKEND=FLASH_ATTN FASTVIDEO_FA4=1
# text to video + audio
python examples/inference/basic/basic_pika_h3_flex.py --task t2va \
--prompt "a paper boat drifting down a rainy street"
# 480p portrait, ten seconds
python examples/inference/basic/basic_pika_h3_flex.py --task t2va \
--resolution 480p --aspect 9:16 --duration 10 \
--prompt "a lighthouse at dusk, waves breaking below"
# first frame, last frame, or both
python examples/inference/basic/basic_pika_h3_flex.py --task i2va \
--image first.png --prompt "the camera slowly pushes in"
python examples/inference/basic/basic_pika_h3_flex.py --task fl2va \
--image first.png --last-image last.png --prompt "the flower opens"
# ordered references, including reference video and audio
python examples/inference/basic/basic_pika_h3_flex.py --task ref2va \
--reference image:person.png --reference video:dance.mp4 \
--reference audio:voice.wav --prompt "the person dances to the music"
```
Ref2VA accepts up to 12 references: 9 images, 3 videos, and 3 audio. These
counts are enforced locally, as is the rule that audio-only requests are
invalid. A video reference needs at least 22 decoded frames at 24 fps. At least
one image or video is required, and the Ref2VA pipeline must be selected with
`PikaH3FlexRef2VAModularPipeline` (the example does this).
Pika's hosted API also documents a 2–15 s limit per video/audio reference and a
15 s total per modality. **The local path does not currently enforce those
duration caps**; it trims each reference to the requested output duration. Treat
the per-reference and per-modality limits as recommendations until the checks
are added.
## Canvas and frames
Pika uses an exact 32-pixel bucket lattice. A request that matches a bucket is
kept exactly; otherwise FastVideo picks the tier from the requested short edge
(`<= 480` → p480, else p768) and snaps to the nearest aspect bucket in that tier.
| Tier | Short edge | Max pixels | Reference-image short edge |
|---|---|---|---|
| p480 | 480 | 832×480 | 1024 |
| p768 | 768 | 1344×768 | 2048 |
Frame counts follow Pika's `17n + 5` grid at 24 fps (5–15 s, 120–362 aligned
frames). The default 16:9 p768 canvas is 1344×768 and 124 frames.
| Duration | 5 s | 10 s | 15 s |
|---|---|---|---|
| `num_frames` | 124 | 243 | 362 |
The example script picks the bucket with `--resolution {480p,768p}` and
`--aspect` (`21:9`, `16:9`, `3:2`, `4:3`, `5:4`, `1:1`, `4:5`, `3:4`, `2:3`,
`9:16`, `9:21`), and the frame count with `--duration` seconds. `--height`,
`--width`, and `--num-frames` override them.
Through the API, a request that omits `height` and `width` uses the 16:9
bucket of the configured tier:
```python
PipelineSelection(experimental={"pika_resolution_tier": "p480"}) # 832x480 when the size is omitted
```
An explicit `height`/`width` always wins and selects its own tier. "Omitted"
means absent from a request parsed from a mapping or config file, or from a
`generate_video(...)` call without `height`/`width`; a `GenerationRequest`
constructed in Python counts `SamplingConfig`'s 1280×720 default as set, which
snaps to the 768p 16:9 bucket. Pass the size explicitly in that case.
Pika center-crops both keyframes to the bucket. MiniMax-H3 stretches its first
frame instead; do not reuse that fit for Pika.
## Attention backend
Run Pika with FlashAttention-4 on Hopper or Blackwell:
```bash
FASTVIDEO_ATTENTION_BACKEND=FLASH_ATTN FASTVIDEO_FA4=1 \
python examples/inference/basic/basic_pika_h3_flex.py --task t2va --prompt "..."
```
FA4 is opt-in. Installing `flash_attn.cute` is not enough:
`fastvideo/attention/utils/flash_attn_default.py` only selects it when
`FASTVIDEO_FA4=1`, and neither the example's defaults nor `--lossless-accel`
set that variable. With `FASTVIDEO_FA4=1` and an unusable FA4 install the run
fails at import instead of silently falling back.
Without FA4, the `FLASH_ATTN` backend uses FA3 or FA2 if one is installed. On a
host with neither (common on Blackwell), backend selection falls back to
`TORCH_SDPA`. SDPA at 768p is much slower and can run out of memory, so do not
use it for 768p benchmarks or production runs.
## Performance and memory
All measurements: one GB200, 768p 16:9 (1344×768), 5 s (124 frames), six
forwards, warm requests, FA4, every component resident (`--offload resident`).
| Configuration (T2VA) | Generation time |
|---|---|
| Official `pika-h3-pro` CLI | 59.7 s |
| FastVideo, BF16 | 26.2 s |
| FastVideo, NVFP4 `quality-36L` decoder | 18.0 s |
| FastVideo, NVFP4 `fast-8L` decoder | 17.4 s |
The 36L decoder measured 40.5 dB PSNR against the BF16 decoder on the same
latents. The 8L decoder was timed but not scored here.
Task coverage, seed 42, NVFP4 text encoder, BF16 decoder, also with
`FASTVIDEO_MINIMAX_H3_FUSIONS=all`:
| Task | Wall | Denoise | Decode | Peak memory | Check |
|---|---|---|---|---|---|
| T2VA | 25.49 s | 16.70 s | 8.04 s | 104.6 GiB | on prompt; 21.7 dB PSNR vs the BF16-encoder output |
| I2VA | 27.37 s | 18.02 s | 8.23 s | 104.9 GiB | first frame vs input 26.5 dB, SSIM 0.715 |
| FL2VA | 28.95 s | 19.49 s | 8.12 s | 105.2 GiB | first frame 39.6 dB / 0.971, last frame 29.1 dB / 0.819 |
| Ref2VA, two image references | 51.17 s | 38.42 s | 7.74 s | 108.5 GiB | 64,241 packed tokens; both references used |
The NVFP4 text encoder's first request in a fresh process paid a one-time
first-use cost of about 183 s; later requests did not.
Peak resident memory for the BF16 FastVideo T2VA run was about 129 GiB. The
official CLI's sampling peak was 71.7 GiB; the two figures measure different
things (process-resident memory versus the sampling phase).
The example's default `--offload lazy` loads components on demand and offloads
the encoders and VAE. It fits smaller devices but reloads the DiT on every
request: the same T2VA request took 48.06 s (28.31 s denoise) on GB200 in that
mode. Use `--offload resident` when the device holds every component.
## Training-free acceleration
The 6 forwards already use FastVideo's normal inference path. Available
lossless switches:
* `FASTVIDEO_MINIMAX_H3_EXACT_KERNELS=all` — exact fused H3 kernels;
* `FASTVIDEO_H3_REF2VA_MEMO_ENTRIES=16` — cache reference conditioning
(Ref2VA);
* `FASTVIDEO_H3_VAE_TILE_PARALLEL=1` — parallel VAE tiles;
* `FASTVIDEO_ULYSSES_A2A=auto` — Ulysses all-to-all for multi-GPU sequence
parallelism;
* `OffloadConfig(lazy_module_load=True)` and `dit_layerwise=True` for smaller
GPUs;
* `CompileConfig(enabled=True)` for `torch.compile`;
* FlashAttention-4 through `FASTVIDEO_ATTENTION_BACKEND=FLASH_ATTN
FASTVIDEO_FA4=1` (see [Attention backend](#attention-backend)).
`--lossless-accel` in the example enables the first four before the generator
starts. It does not enable FA4; set the two attention variables at launch. Kernel work beyond the existing exact kernels is intentionally not part
of this integration.
## NVFP4 on Blackwell
Pika can reuse FastVideo's existing H3 NVFP4 VAE decoder and NVFP4 Qwen3-VL
text encoder. Both are weight formats, not new sampler paths; the six-forward
Pika schedule is unchanged.
```bash
# 36-block quality decoder: needs the full 36-block H3 VAE
python examples/inference/basic/basic_pika_h3_flex.py --task t2va \
--prompt "a paper boat drifting down a rainy street" \
--nvfp4-decoder ./FastH3-NVFP4-VAE-Decoder/quality-36L/decoder-nvfp4.safetensors
# 8-block fast decoder: depth-cuts any H3 VAE with at least 8 blocks
python examples/inference/basic/basic_pika_h3_flex.py --task ref2va \
--reference image:person.png --reference video:dance.mp4 \
--prompt "the person dances to the music" \
--nvfp4-decoder ./FastH3-NVFP4-VAE-Decoder/fast-8L/decoder-nvfp4.safetensors \
--nvfp4-text-encoder ./fasth3-nvfp4-encoder/text_encoder
```
* `quality-36L` keeps the full decoder depth and is the closest match to Pika's
dense decoder. `fast-8L` is much smaller and faster but is a distilled
student; expect a visible quality drop. Pika's six-step re-noise latents are
not the student's original training distribution, so validate 8L on your
prompts before using it for delivery.
* The NVFP4 decoder replaces only `vae.decoder`; keyframe and reference
encoding still use the FP32 VAE encoder.
* The NVFP4 text encoder quantizes the Qwen3-VL language stack while keeping
the vision tower in bf16, so Ref2VA reference images/videos still work.
* Do not select `taeh3` with an NVFP4 decoder; `h3-vae` is required.
* Both options require Blackwell FP4 hardware. The fused decoder kernels and
CUDA graphs are already enabled by default; see
`FASTVIDEO_H3_VAE_NVFP4_FUSED` and `FASTVIDEO_H3_VAE_NVFP4_CUDA_GRAPH`.
* There is no NVFP4 **VAE encoder** path. The H3 video VAE encoder is a causal
3D CNN and stays FP32; quantizing it would need a separate convolution
kernel/quantization effort.
If a process first loads the 8L decoder it depth-cuts the VAE to eight blocks;
loading 36L afterwards in the same process is not supported. Choose one decoder
tier per process.
## Hardware
The selected H3 trunk plus Qwen3-VL does not fit comfortably on a 24 GB card
without offload. A single 80 GB GPU can run with text-encoder offload; use
layerwise DiT offload or multiple GPUs for smaller devices. Ref2VA encodes
reference images at up to a 2048 short edge, so its conditioning memory is
higher than T2VA's.
+6
View File
@@ -68,7 +68,10 @@ 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) |
| 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) |
| stable_audio | `FastVideo/stable-audio-open-small-Diffusers` | T2V | [basic_stable_audio_small.py](https://github.com/hao-ai-lab/FastVideo/blob/main/examples/inference/basic/basic_stable_audio_small.py) |
| wan | `Wan-AI/Wan2.1-T2V-1.3B-Diffusers` | T2V | [basic.py](https://github.com/hao-ai-lab/FastVideo/blob/main/examples/inference/basic/basic.py) |
@@ -120,6 +123,9 @@ and distilled) generate video with audio from text, optionally plus an image (se
`kandinsky6_sr` IDs upscale an existing video (see the
[Video SR guide](kandinsky6_sr.md)).
**Note (Open-H3-Regenerate-2K)**: `open_h3_regenerate_2k` regenerates an existing 768P MiniMax-H3 draft video at
2560x1440 (video to video, silent output); see the [Open-H3-Regenerate-2K guide](open_h3_regenerate_2k.md).
**Note (LTX-2.5)**: T2V and I2V produce synchronized video and audio. The
official `Lightricks/LTX-2.5` repository uses gated, split component files, so
it must first be converted to FastVideo's component layout. See the
+14
View File
@@ -395,6 +395,20 @@ function resolveConfig(catalog, selections = {}) {
}
}
validateTopology(catalog, config);
const engine = config.generator?.engine ?? {};
const experimental = config.generator?.pipeline?.experimental ?? {};
if (engine.quantization?.layer_profile && engine.quantization.transformer_quant?.toLowerCase() !== "nvfp4") {
throw invalidValue(
"A quantization layer profile requires NVFP4 transformer quantization.",
"generator.engine.quantization.transformer_quant",
);
}
if (experimental.video_decode_backend === "taeh3" && experimental.h3_vae_nvfp4_decoder) {
throw invalidValue(
"TAEH3 uses its own decoder. Clear the NVFP4 decoder override or select h3-vae.",
"generator.pipeline.experimental.h3_vae_nvfp4_decoder",
);
}
const argv = [...catalog.runtime.launch_argv];
const environment = Object.entries(catalog.env).map(([key, value]) => `${key}=${shellQuote(value)}`);
const sample = sampleRequest(catalog, config);
+8
View File
@@ -32,6 +32,14 @@ export const WORKLOADS = Object.freeze({
extension: "wav",
media_type: "audio/wav",
}),
v2v: Object.freeze({
input: "video",
output: "video",
endpoint: "/v1/videos/sync",
extension: "mp4",
media_type: "video/mp4",
reference: "video_reference",
}),
v2a: Object.freeze({
input: "video",
output: "audio",
-1
View File
@@ -1 +0,0 @@
/Users/aryank/os/FastVideo-review-int/docs/node_modules
@@ -12,6 +12,25 @@ function at(value, path) {
return path.split(".").reduce((current, key) => current?.[key], value);
}
test("every local NVFP4 encoder choice includes its download requirement", () => {
const path = "generator.pipeline.components.text_encoder_weights";
const catalogs = buildCatalogs().filter((catalog) => catalog.controls.some((control) => control.path === path));
assert.ok(catalogs.length > 0);
for (const catalog of catalogs) {
const option = catalog.controls.find((control) => control.path === path);
if (!option.schema.enum?.includes("./fasth3-nvfp4-encoder/text_encoder")) continue;
assert.ok(
catalog.requirements.some(
(requirement) =>
requirement.includes("hf download ") &&
requirement.includes("--include 'text_encoder/*'") &&
requirement.includes("--local-dir fasth3-nvfp4-encoder"),
),
`${catalog.id} must explain how to create the selected encoder directory`,
);
}
});
function fixture({ interfaceName = "rest", backend = "cuda", workload = "t2v" } = {}) {
return {
id: "example/deployment",
+22
View File
@@ -16,6 +16,27 @@ import {
const require = createRequire(import.meta.url);
test("configuration output rejects ignored decoder overrides and incompatible quantization profiles", () => {
const data = catalog();
data.base_config.generator.pipeline.experimental = {
video_decode_backend: "taeh3",
h3_vae_nvfp4_decoder: "decoder.safetensors",
};
assert.throws(
() => resolveConfig(data),
(error) => {
assert.equal(error.path, "generator.pipeline.experimental.h3_vae_nvfp4_decoder");
return /TAEH3 uses its own decoder/.test(error.message);
},
);
data.base_config.generator.pipeline.experimental.h3_vae_nvfp4_decoder = null;
assert.doesNotThrow(() => resolveConfig(data));
data.base_config.generator.engine.quantization = { transformer_quant: "FP8", layer_profile: "h3_dit_vsa" };
assert.throws(() => resolveConfig(data), /requires NVFP4/);
data.base_config.generator.engine.quantization.transformer_quant = "NVFP4";
assert.doesNotThrow(() => resolveConfig(data));
});
function catalog() {
return {
id: "example/rest",
@@ -488,6 +509,7 @@ test("sample requests follow shared workload media", () => {
t2i: { endpoint: /\/v1\/images\/generations/, output: /--output output\.json/, reference: null },
t2a: { endpoint: /\/v1\/videos\/sync/, output: /--output output\.wav/, reference: null },
v2a: { endpoint: /\/v1\/videos\/sync/, output: /--output output\.wav/, reference: "video_reference" },
v2v: { endpoint: /\/v1\/videos\/sync/, output: /--output output\.mp4/, reference: "video_reference" },
};
for (const [workload, expected] of Object.entries(expectations)) {
const data = catalog();
+1 -1
View File
@@ -56,7 +56,7 @@ torchrun --nproc_per_node=$GPU_NUM \
| `--preprocess.dataset_path` | Path to dataset (HF repo ID or local folder) |
| `--preprocess.dataset_output_dir` | Output directory for Parquet files |
| `--preprocess.video_loader_type` | Video decoder: `torchcodec` or `torchvision` |
| `--preprocess.max_height` / `max_width` | Target resolution for videos |
| `--preprocess.max_height` / `--preprocess.max_width` | Target resolution for videos |
| `--preprocess.num_frames` | Number of frames to extract per video |
| `--preprocess.train_fps` | Target FPS for frame extraction |
+3 -3
View File
@@ -1,12 +1,12 @@
# 🧠 Finetuning
# Fine-tuning
This guide covers finetuning video diffusion models with FastVideo, including full finetuning and LoRA.
## Training Arguments
## Training arguments
FastVideo training scripts use several argument groups:
### Training Arguments
### Loop and model arguments
| Argument | Description |
|----------|-------------|
+23 -23
View File
@@ -12,7 +12,7 @@ parquet that step produces.
## What you need
| | |
| Requirement | Value |
|---|---|
| GPU | 48GB or more for the LoRA overfit; multi-GPU for full-parameter runs |
| Disk | ~40GB for the checkpoint (DiT 17GB + Qwen2.5-VL 16GB + ByT5/VAE) |
@@ -145,16 +145,10 @@ a real problem — stop and investigate rather than burning GPU hours.
## 4. Scale out
`examples/train/configs/fine_tuning/hunyuan15/t2v.yaml` runs
full-parameter training across 8 GPUs with FSDP. Full-parameter training
does not fit one 80GB card:
full-parameter training across 8 GPUs with FSDP. One 80 GB card cannot hold it:
It reads `data/hunyuan15_preprocessed` (and its `validation_prompts.json`),
not the overfit directory above — either preprocess into that path (set
`FASTVIDEO_TEST_HUNYUAN15_OVERFIT_OUTPUT_DIR=data/hunyuan15_preprocessed`) or
pass `--training.data.data_path` and `--callbacks.validation.dataset_file`.
| | bf16 |
|---|---|
| Item | Per rank (bf16) |
|---|---:|
| Weights | 16.7 GB |
| Gradients | 16.7 GB |
| AdamW `exp_avg` + `exp_avg_sq` | 33.4 GB |
@@ -163,21 +157,27 @@ pass `--training.data.data_path` and `--callbacks.validation.dataset_file`.
| **Total** | **~80 GB** |
That subtotal is independent of sequence length, so shortening the clip
(`--training.data.num_latent_t`) barely helps — measured peak memory moved
by 0.07GB going from 21 latent frames down to 9. Options that do move the
needle:
(`--training.data.num_latent_t`) barely helps: measured peak memory moved by
0.07 GB going from 21 latent frames to 9. Options that do move the needle:
- **LoRA** (what the overfit config uses) — gradients and optimizer state
shrink to the adapter parameters, ~30GB total.
- **FSDP sharding across GPUs** — the 66.8GB subtotal divides by the number
of shards.
- **A memory-light optimizer** — SGD with momentum keeps a single state
tensor instead of two. `torch.optim.Adafactor` is smaller still, but is
not DTensor-safe: its in-place `.square_()` fails under FSDP2 with
`aten.pow_.Scalar: in-place operations that require placement changes
are not supported`.
- **LoRA** (what the overfit config uses): gradients and optimizer state
shrink to the adapter parameters, about 30 GB total.
- **FSDP sharding across GPUs**: the 66.8 GB subtotal divides by the number of
shards.
- **A memory-light optimizer**: SGD with momentum keeps a single state tensor
instead of two. `torch.optim.Adafactor` is smaller still, but is not
DTensor-safe. Its in-place `.square_()` fails under FSDP2 with
`aten.pow_.Scalar: in-place operations that require placement changes are
not supported`.
Set `training.dit_precision: bf16` for any single-device run. The default
The config reads `data/hunyuan15_preprocessed` and its
`validation_prompts.json`, not the overfit directory above. Either preprocess
into that path (set
`FASTVIDEO_TEST_HUNYUAN15_OVERFIT_OUTPUT_DIR=data/hunyuan15_preprocessed`) or
pass `--training.data.data_path` and `--callbacks.validation.dataset_file`.
The shipped overfit config already sets `training.dit_precision: bf16`. If you
write your own config, set it for any single-device run. The default
is fp32, which doubles weights, gradients, and optimizer state.
## Troubleshooting
+253
View File
@@ -0,0 +1,253 @@
# FastH3 NVFP4 QAD
This page describes the shared quantization-aware distillation (QAD) recipe for
the FastH3 (MiniMax-H3) family. One recipe covers two model profiles:
| Profile | Model plugin | Task | Sampling | VSA tile |
|---|---|---|---|---|
| OmniRef | `omniref_pdd.MiniMaxH3OmniRefPDDModel` | Ref2VA | 8-forward PDD ladder | 128 |
| V2 T2AV QAD | `v2_t2v.MiniMaxH3V2T2VModel` | T2VA | 8-step DMD | 64 |
Both profiles use the same shared recipe: NVFP4 W4A4 linears in all 50 blocks,
the FP4 sparse-attention emulator, and the same objective and guards. QAD
trains the student against a frozen bf16 teacher so that quantization error is
absorbed by the weights instead of appearing as drift, ghosting or audio loss
at inference. The published OmniRef QAD checkpoint includes FP4 attention; the
published FastH3-QAD-v2 checkpoint predates it and keeps BF16 VSA attention (see
[Released checkpoints](#released-checkpoints)).
The runnable configs are:
- `examples/train/configs/knowledge_distillation/minimax_h3/omniref_nvfp4_qad.yaml`
- `examples/train/configs/knowledge_distillation/minimax_h3/v2_nvfp4_qad.yaml`
## Released checkpoints
| Role | Checkpoint |
|---|---|
| OmniRef QAD result | `FastVideo/FastH3-OmniRef-NVFP4-QAD` |
| FastH3-QAD-v2 result | `FastVideo/FastH3-QAD-v2` |
| V2 bf16 teacher / starting weights | `FastVideo/FastVideo-FastH3-8-Step-V2` |
| NVFP4 consumer baseline | `FastVideo/FastVideo-FastH3-8-Step-V2-NVFP4-Consumer` |
The OmniRef config starts from the **calibrated OmniRef checkpoint used as QAD
step 0**, not from the released OmniRef QAD result; point `init_from` at that
checkpoint. The V2 config starts from the bf16 V2 snapshot.
The published FastH3-QAD-v2 checkpoint is the **linears-only variant** of this
schedule: NVFP4 W4A4 linears with BF16 VSA attention, 300 steps. The shared
FP4 recipe on this page additionally runs the FP4 attention emulator from step
0; it is the same training code and objective, but it does not reproduce the
published FastH3-QAD-v2 checkpoint bit for bit. Set
`models.student.qad.fp4_attention.enabled: false` to get the published
linears-only variant.
## Target numerics
The student starts from the bf16 masters, and the deployed numerics are active
from the first forward, so every update sees the quantization error it is
trained to absorb.
- **Linears:** NVFP4 W4A4 for `attn.to_q`, `attn.to_k`, `attn.to_v`,
`attn.to_out`, `ff.fc_in` and `ff.fc_out` in every block.
- FFN inputs use static activation scales from the calibrated `amax` table.
- Attention and gate linears use the unit activation scale; their `amax`
entries are optional.
- `skip_blocks` is empty: no block falls back to bf16 linears.
- **Attention:** the FP4 sparse-attention emulator for VSA tile 128 (OmniRef)
or tile 64 (V2 T2AV). Two-level P quantization and K smoothing are on. The
emulator reproduces the deployed kernel's block order and rounding on any
CUDA GPU and provides straight-through gradients.
- **Kept frozen:** the VSA compression gates and every norm stay at their
released values. Training the gate weights drifts the sparse-attention tile
selection and produces ghosting; the gate linears still execute the deployed
NVFP4 format, they are just not updated. `proj_in`, the time embedder,
`proj_out` and the audio projections stay fp32 and frozen, matching
inference.
The calibrated `amax` tables for both profiles are vendored under
`examples/train/calibration/fasth3/`:
| Table | sha256 |
|---|---|
| `amax-omniref-mse-bf16-top8.json` | `14b93ec3…df083d` |
| `amax-consumer.json` | `0800516c…d39e52` |
`qad.linears.amax_sha256` pins the table; the run copies it into the output
directory and refuses to start if the bytes differ.
## Training objective
For each row the frozen bf16 teacher walks the profile's sampling ladder. At
the same inputs and timesteps, the student minimizes:
- **Output regression:** per-forward x0 error, normalized per forward and
modality by its step-0 value.
- **Layer-wise reconstruction:** each attention and FFN sub-layer receives the
teacher's input (stop-gradient) and matches the teacher sub-layer's output.
The loss is weighted per row group (target video and audio 1.0,
conditioning 0.25) because audio is a small fraction of the packed tokens.
`recon_normalize: step0` divides it by its step-0 value so `recon_lambda`
balances the two terms.
From `rollout_start_step`, `rollout_fraction` of the rows continue from the
student's own trajectory instead of the teacher's, which reduces exposure bias
in the sampled output.
## Evaluation and guards
- **Teacher-forced eval** every `eval_every` steps on held-out rows.
- **Full eval** every `full_eval_every` steps: the student's own sampled output
against the teacher at the same seed, keyframe alignment, and saved audio and
video latents.
- **Noise floor:** `noise_floor_eval` re-runs the step-0 eval at seed + 1 so a
metric move can be compared with teacher seed noise.
- **Best checkpoint:** selected by the teacher-forced `score`. A checkpoint is
eligible only if the keys in `eligibility_keys` (audio error and endpoint)
stay within their step-0 guardrails.
- **Auto-stop:** after `patience` consecutive bad evals.
- **Step-0 cache:** `step0_cache` fingerprints the teacher, student,
calibration, quantization plan, eval set and loss weights, so reruns skip the
baseline evaluation.
- **Resume:** checkpoints carry the step-0 baseline and guard state; do not
delete `method.qad_baseline` if you edit a checkpoint by hand.
## Data preparation
V2 T2AV is data-free: it needs prompts and their Qwen3-VL text embeddings, not
VAE latents.
1. Build the split manifests from prompt sources (parquet shards with a
`prompt` column, JSONL with a `prompt` field):
```bash
python scripts/distill/minimax_h3_v2_qad/build_v2_prompts.py \
--parquet 'data/prompts/*.parquet' --out-dir data/fasth3-v2 --eval-count 30
```
This writes `train.jsonl` and `eval.jsonl` plus an `embeddings/` directory.
2. Fill the embeddings (safe to rerun and shard across GPUs):
```bash
python scripts/distill/minimax_h3_v2_qad/precompute_v2_text.py \
--config examples/inference/basic/basic_fasth3_spark_v2_nvfp4.yaml \
--model-path data/models/FastH3-V2 \
--manifests data/fasth3-v2/train.jsonl data/fasth3-v2/eval.jsonl \
--shard 0 --num-shards 8
```
The V2 config's `train_manifest` and `eval_manifest` point at these files.
OmniRef training rows are preprocessed Ref2VA parquet rows that carry the
reference latents, text embeddings and geometry; the encoded dataset itself is
an input to this recipe and is not produced by the training code. Provide a
`MANIFEST.jsonl` with `case`, `id`, `parquet` and `resolution` fields and point
`models.student.data.manifests` at it.
`scripts/distill/minimax_h3_omniref_qad/generate_omniref_latents.py` builds the
held-out eval plan and runs the PDD ladder once per row: use `--eval-dir` to
write the held-out `manifest.json` consumed by `models.student.data.eval_manifest`
(and the raw latents for decoding).
## Running
Fill in the placeholder paths in the config (starting checkpoint, data
manifests, output directory), then launch with `torchrun`.
OmniRef, 4 nodes x 4 GPUs (sequence parallel 4):
```bash
torchrun --nnodes=4 --nproc_per_node=4 --node_rank=$NODE_RANK \
--master_addr=$MASTER_ADDR --master_port=$MASTER_PORT \
-m fastvideo.train.entrypoint.train \
--config examples/train/configs/knowledge_distillation/minimax_h3/omniref_nvfp4_qad.yaml
```
V2 T2AV, 4 nodes x 4 GPUs (16-way HSDP, lr 5e-6, 300 steps):
```bash
torchrun --nnodes=4 --nproc_per_node=4 --node_rank=$NODE_RANK \
--master_addr=$MASTER_ADDR --master_port=$MASTER_PORT \
-m fastvideo.train.entrypoint.train \
--config examples/train/configs/knowledge_distillation/minimax_h3/v2_nvfp4_qad.yaml
```
To reproduce the published FastH3-QAD-v2 linears-only checkpoint, disable FP4 attention
for the same schedule:
```bash
--models.student.qad.fp4_attention.enabled false
```
## FP4-attention adaptation
The internal follow-up that this recipe tracks started from a linears-only
300-step DCP, enabled FP4 attention, and trained 300 more steps at lr 1.5e-6
with no warmup and a constant LR on 16 GPUs. It is an **adaptation of the
linears-only checkpoint to FP4 attention**, not a second epoch of the FP4
stage 1 above: the source checkpoint's attention numerics were BF16, and the
continuation needs a DCP of that linears-only model (the packed HF release is
not one). It is therefore not part of the public reproduction path, and the
published FastH3-QAD-v2 checkpoint remains the linears-only result. The exact overrides
are listed at the bottom of the V2 config.
## Checkpoints
Runs write regular `checkpoint-<step>/` DCP checkpoints plus a `best/`
directory:
- `best/`: the best eligible state (weights only), refreshed at every eval and
never pruned.
- `checkpoint-<step>/`: weights, optimizer, RNG state and the method baseline;
the last step is always saved.
The saved state is the bf16 master plus the quantization plan, not packed NVFP4
weights; inference applies the same converter used for the released model. Use
`fastvideo/train/entrypoint/dcp_to_diffusers.py` to export a checkpoint if your
deployment path expects Diffusers weights. It detects the weights-only marker
on a tagged checkpoint, so `best/` can be exported directly:
```bash
python -m fastvideo.train.entrypoint.dcp_to_diffusers \
--checkpoint outputs/v2_1_nvfp4_qad/best \
--output-dir outputs/v2_1_nvfp4_qad/export
```
## Code map
| Path | Purpose |
|---|---|
| `fastvideo/train/methods/knowledge_distillation/pdd_qad.py` | Method: ladder, losses, evaluation, guards |
| `fastvideo/train/methods/knowledge_distillation/pdd_qad_metrics.py` | Rung aggregation and score |
| `fastvideo/train/methods/knowledge_distillation/pdd_qad_recon.py` | Layer-wise reconstruction hooks |
| `fastvideo/train/methods/knowledge_distillation/pdd_qad_step0.py` | Step-0 baseline cache |
| `fastvideo/train/models/minimax_h3/omniref_pdd.py` | OmniRef Ref2VA profile |
| `fastvideo/train/models/minimax_h3/v2_t2v.py` | V2 T2AV profile |
| `fastvideo/layers/quantization/nvfp4_qad.py` | NVFP4 linear emulation and STE |
| `fastvideo/attention/backends/fp4_vsa_qat.py` | VSA fine-attention override |
| `fastvideo-kernel/python/fastvideo_kernel/triton_kernels/attn_qat_vsa_train.py` | Tile 64/128 FP4 block-sparse Triton kernel |
| `fastvideo-kernel/python/fastvideo_kernel/triton_kernels/attn_qat_vsa_reference.py` | PyTorch numeric reference |
| `scripts/distill/minimax_h3_v2_qad/build_v2_prompts.py` | V2 prompt split manifests |
| `scripts/distill/minimax_h3_v2_qad/precompute_v2_text.py` | V2 Qwen3-VL text embeddings |
| `scripts/distill/minimax_h3_omniref_qad/generate_omniref_latents.py` | OmniRef held-out eval plan and latents |
| `examples/train/calibration/fasth3/` | Pinned FFN activation `amax` tables |
## Expected behavior
Teacher-forced video and audio error both fall over the first tens of steps,
and the student's own video samples improve beyond the teacher's seed noise.
Audio samples improve more slowly and can remain within noise even when
teacher-forced audio error clearly drops. Gains slow after roughly 60-100
steps; the remaining error is dominated by FP4 attention. Evaluate a checkpoint
with the full-eval samples, not the teacher-forced score alone, before
promoting it.
Released reference points (lower is better):
| Run | Metric | Baseline | Selected |
|---|---|---|---|
| FastH3-QAD-v2 (linears-only) | mean per-forward x0 rel-L2 | 0.431 | 0.351 (video 0.382, audio 0.319) |
| OmniRef QAD | teacher-forced score | 0.362 | 0.303 at step 120 (video 0.198, audio 0.409) |
The FastH3-QAD-v2 baseline is the NVFP4 Consumer model before QAD; the OmniRef baseline
is the calibrated checkpoint at QAD step 0.
@@ -12,6 +12,17 @@ format, which stores already augmented `ref_visual_anchor` tensors. Keep using
`preprocess_minimax_h3_ref2va` for that format. Do not rename or directly combine
shards from these two schemas.
The SFT preprocessor keeps each target clip's own geometry: the canvas follows
the clip's aspect ratio at the inference short edge (`resolve_canvas_size`), and
the frame count is the longest `17 * n + 5` that fits the clip, capped at 362
(about 15 seconds; a longer clip trains on its start). Clips under 124 frames at
24 fps fail. Target audio is cut to the samples covered by that frame count's
whole audio latents, so the record matches what inference generates. To train
on those lengths, leave `training.data.num_latent_t` and `num_frames` unset in
the training config. When they are set, every row is cropped to that length;
for example, the 124-frame overfit values crop a 10-second clip to its first
5.17 seconds while its caption still describes all 10.
## Input
Provide your own local MiniMax-H3 checkpoint with `vae`, `audio_vae`, `tokenizer`,
+384
View File
@@ -0,0 +1,384 @@
# Pika H3 Flex NVFP4 QAD
This page describes how the NVFP4 version of
[Pika H3 Flex](https://huggingface.co/Pika-Labs/Pika-H3-Flex) is trained: a
calibration pass followed by quantization-aware distillation (QAD) against
the bf16 Flex model, for both Flex adapters. It applies the shared FastH3 QAD
recipe ([FastH3 NVFP4 QAD](minimax_h3_fp4_qad.md)) to Flex's six-forward
re-noise sampler.
| Adapter | Trunk | Tasks | Model plugin |
|---|---|---|---|
| Ref2VA | `transformer_ref` | image / video / audio references | `flex.MiniMaxH3FlexRef2VAModel` |
| FL2VA | `transformer` | T2VA, I2VA (first frame), L2VA (last frame), FL2VA (both) | `flex.MiniMaxH3FlexFL2VAModel` |
Training runs in two stages per adapter:
1. **NVFP4 linears** (`*_nvfp4_qad.yaml`): W4A4 linears with BF16 dense
attention.
2. **Dense FP4 attention** (`*_fp4attn_qad.yaml`): continues from stage 1's
best checkpoint with FlashAttention-4 NVFP4 attention numerics.
The stage-1 result is the released checkpoint below; stage 2 is optional and
trades a small quality cost for faster long clips (see
[Dense FP4 attention](#stage-2-dense-fp4-attention)).
## Released checkpoint
| Role | Checkpoint |
|---|---|
| Teacher (bf16 adapters) | `Pika-Labs/Pika-H3-Flex` on `MiniMaxAI/MiniMax-H3` @ `42ed227` |
| QAD result (packed NVFP4, both trunks) | `FastVideo/Pika-H3-Flex-NVFP4-QAD` |
The release contains packed NVFP4 `transformer_ref` (Ref2VA) and `transformer`
(FL2VA) trunks with the adapters already merged, an NVFP4 Qwen3-VL text
encoder, an NVFP4 VAE decoder and the Pika schedulers. It targets Blackwell
GPUs and loads without a conversion step (validated on GB200):
```bash
FASTVIDEO_ATTENTION_BACKEND=FLASH_ATTN FASTVIDEO_FA4=1 \
python examples/inference/basic/basic_pika_h3_flex.py \
--model-path FastVideo/Pika-H3-Flex-NVFP4-QAD --nvfp4-dit h3_dit \
--nvfp4-decoder <snapshot>/vae_decoder_nvfp4/decoder-nvfp4.safetensors \
--task ref2va --reference image:character.png --reference video:motion.mp4 \
--prompt "$(cat prompt.txt)" --resolution 480p --duration 5
```
## Target numerics
- **Linears:** NVFP4 W4A4 for `attn.to_q`, `attn.to_k`, `attn.to_v`,
`attn.to_out`, `ff.fc_in` and `ff.fc_out` in all 50 blocks, active from
step 0. FFN inputs use static activation scales (`448 * 6 / amax`) from the
calibrated table; attention linears use the unit activation scale.
- **Attention:** BF16 dense attention in stage 1. Flex has no VSA gate, so
there is no sparse-attention stage and no gate to freeze. Stage 2 switches
to the dense FP4 emulator (below).
- **Trained:** only the six quantized linear families. Norms, AdaLN, the token
refiner, embeddings and the fp32 boundary modules stay frozen.
- **Teacher:** the merged bf16 Flex trunk, frozen. The student starts from the
same weights with fp32 masters.
### Merged adapters
Flex ships each adapter as an fp32 PEFT LoRA (rank and alpha 128) over the
MiniMax-H3 trunk. QAD needs the teacher as plain weights, so
`merge_pika_adapter.py` merges `W + (alpha / rank) * B @ A` for every adapted
module in fp32 and rounds once to bf16. The merged directory also carries
`pika_flex_qad.json`, the sampling contract the model plugin reads (six
forwards at knots 1000/750/500/250/100/40, video shift 12, audio shift 3,
stochastic re-noise between forwards).
Exported trunks that already contain the adapter are marked by
`pika_h3_flex_merged.json` at the model root; the Pika pipeline then skips
loading the adapter again.
## Training objective
Each optimizer step takes one row. The frozen teacher walks the six-forward
ladder; three of the six forwards are trained per row. At each trained forward
the student predicts from the teacher's state:
- **Output:** relative L2 of the student's x0 prediction against the
teacher's, for video and audio.
- **Reconstruction:** relative L2 of every block's output against the
teacher's block output (`objective: output+recon`, `recon_lambda: 1.0`).
Both terms are normalized by their step-0 values. From step 20, 25% of rows
roll out from the student's own states instead of the teacher's. The learning
rate is 5e-6 (cosine to 0.3x after a 10-step warmup); gradients are clipped at
1.0. The re-noise draws are seeded per row and per forward, so teacher and
student see identical noise.
## Evaluation and guards
Every `eval_every` steps the run scores held-out rows (teacher-forced x0 error
per forward, averaged into `eval/score`); every `full_eval_every` steps it also
samples each row with the student's own six forwards and compares the result
with the teacher's (`endpoint/video`, `endpoint/audio`) and, for keyframe
tasks, the first and last frames.
- **Eligibility:** a checkpoint can become `best/` only if its full-eval
audio, endpoint and keyframe metrics stay within `step0 * (1 + tolerance)`.
With `noise_floor_eval: true` the tolerance per metric is twice its seed
noise at step 0 (re-evaluated at seed + 1), at least 2%.
- **Patience:** the run stops after `patience` consecutive evaluations that are
worse than step 0 on the score, or (full evaluations only) misaligned on the
endpoint and keyframe metrics.
Keyframe metrics matter most for FL2VA: the last-frame match is the metric most
sensitive to the learning rate (see [Expected behavior](#expected-behavior)).
## Data preparation
QAD regresses on the teacher, so rows need conditioning (text embeddings and
reference latents) and target geometry, not curated targets. The starting
point is a set of encoded Ref2VA rows in the packed schema
(`fastvideo/dataset/dataloader/schema.py`, `pyarrow_schema_ref2va`) with a
`MANIFEST.jsonl` listing `case`, `id`, `parquet` and `resolution`, plus a
held-out manifest, as in the FastH3 OmniRef recipe (see
[Data preparation](minimax_h3_fp4_qad.md#data-preparation) there). Raw clips can
be encoded into this format with
`fastvideo/pipelines/preprocess/preprocess_minimax_h3_packed_ref2va.py`. Row
ids must contain the source video's id as `asset-<32 hex digits>`; clips of the
same video share it. Below, the source rows are `data/omniref/MANIFEST.jsonl`
and their held-out manifest is `data/omniref/eval-manifest.json`.
1. **Merge each adapter** into its trunk:
```bash
python scripts/distill/pika_h3_flex_qad/merge_pika_adapter.py \
--base <MiniMax-H3 snapshot @42ed227> --adapter Pika-Labs/Pika-H3-Flex \
--task ref2va --output data/models/pika-flex-ref2va-merged
python scripts/distill/pika_h3_flex_qad/merge_pika_adapter.py \
--base <MiniMax-H3 snapshot @42ed227> --adapter Pika-Labs/Pika-H3-Flex \
--task fl2va --output data/models/pika-flex-fl2va-merged
```
2. **Build Ref2VA rows** with `build_flex_ref2va_rows.py` (`plan` on CPU,
`encode` sharded over GPUs, then `finalize`). Each reference is decoded
from its stored latents and re-encoded at Flex's reference sizing, so the
source media is not needed. The plan re-tiers the rows the QAD config will
train on and adds input mixes the source set does not cover: a standalone
audio reference, mixed image and video references, 6-9 image references,
and two video references.
`plan` reads the QAD plan from `--plan-config`. The Flex rows do not exist
yet at this point, so plan with a copy of `ref2va_nvfp4_qad.yaml`
(`plan-ref2va.yaml` below) whose `manifests` lists only
`data/omniref/MANIFEST.jsonl` and whose `eval_manifest` is
`data/omniref/eval-manifest.json`. `--eval-manifest` is the source held-out
manifest; its clips are kept out of the new rows.
```bash
python scripts/distill/pika_h3_flex_qad/build_flex_ref2va_rows.py plan --mode ref2va \
--source-manifest data/omniref/MANIFEST.jsonl --plan-config plan-ref2va.yaml \
--eval-manifest data/omniref/eval-manifest.json --output-dir data/flex-ref2va
CUDA_VISIBLE_DEVICES=$i python scripts/distill/pika_h3_flex_qad/build_flex_ref2va_rows.py encode \
--model-path data/models/pika-flex-ref2va-merged --output-dir data/flex-ref2va \
--shard $i --num-shards 8
python scripts/distill/pika_h3_flex_qad/build_flex_ref2va_rows.py finalize \
--output-dir data/flex-ref2va --eval-manifest data/omniref/eval-manifest.json
```
`finalize` merges the shard manifests into `data/flex-ref2va/MANIFEST.jsonl`
and writes `data/flex-ref2va/eval-manifest.json`: the source held-out rows,
with each row that was re-tiered pointing at its Flex-sized parquet. The
Ref2VA configs list the Flex manifest ahead of the source manifest, so
Flex-sized rows replace source rows with the same id and resolution.
3. **Build FL2VA rows** from the same source rows. `--mode fl2va` builds T2VA,
I2VA, L2VA and FL2VA rows whose keyframes are the clip's exact first/last
frames, with the caption's reference paragraphs rewritten as keyframe
definitions. `--fl2va-counts` and `--resolution-split` size the set
(defaults: 1,100 rows, 80% 480p). FL2VA holds out its eval rows from its own
groups, so `finalize` takes no `--eval-manifest` and writes an empty one.
```bash
python scripts/distill/pika_h3_flex_qad/build_flex_ref2va_rows.py plan --mode fl2va \
--source-manifest data/omniref/MANIFEST.jsonl \
--eval-manifest data/omniref/eval-manifest.json --output-dir data/flex-fl2va
CUDA_VISIBLE_DEVICES=$i python scripts/distill/pika_h3_flex_qad/build_flex_ref2va_rows.py encode \
--model-path data/models/pika-flex-fl2va-merged --output-dir data/flex-fl2va \
--shard $i --num-shards 8
python scripts/distill/pika_h3_flex_qad/build_flex_ref2va_rows.py finalize --output-dir data/flex-fl2va
```
Rows that fail to encode (for example a reference video longer than Flex's
15-second limit) are recorded per shard; `encode --retry-skipped` retries only
those. The configs train on rows up to 345 frames at 480p and 768p
(`per_group: {480p: 160, 768p: 40}` per case) and hold out 3 rows per case.
## Calibration
`calibrate_flex_nvfp4.py` picks the static FFN activation scales on rows walked
by the bf16 teacher over its six forwards:
1. `collect` (torchrun): forward pre-hooks record each row's per-layer input
amax, split into target video, target audio and conditioning tokens.
`--max-rows` takes the first rows of the plan; leave rows uncollected for
`mse`, which stops if every planned row was collected.
2. `candidates` (CPU): per layer, the max over rows plus clipped alternatives
(per-row p99/p90, and 0.7/0.5/0.35/0.25 of the max).
3. `mse` (torchrun): on disjoint rows plus the rows that set the maxima, each
candidate's NVFP4 GEMM output is compared with the bf16 output.
4. `choose` (CPU): per layer, the candidate with the lowest mean of video and
audio relative error (`amax-mse-balanced.json`), next to the all-token
criterion (`amax-mse.json`) and the plain max.
```bash
torchrun --nproc_per_node 4 scripts/distill/pika_h3_flex_qad/calibrate_flex_nvfp4.py collect \
--config examples/train/configs/knowledge_distillation/pika_h3_flex/ref2va_nvfp4_qad.yaml \
--output-dir calib/ref2va --shard $NODE --num-shards $NODES --max-rows $COLLECT_ROWS \
--training.distributed.num_gpus 4 --training.distributed.hsdp_shard_dim 4
python scripts/distill/pika_h3_flex_qad/calibrate_flex_nvfp4.py candidates --output-dir calib/ref2va
torchrun --nproc_per_node 4 scripts/distill/pika_h3_flex_qad/calibrate_flex_nvfp4.py mse \
--config <same config> --output-dir calib/ref2va --shard $NODE --num-shards $NODES \
--training.distributed.num_gpus 4 --training.distributed.hsdp_shard_dim 4
python scripts/distill/pika_h3_flex_qad/calibrate_flex_nvfp4.py choose --output-dir calib/ref2va
```
Compare the candidate tables by the QAD harness's step-0 score (run the QAD
config with each table and read `eval/score` at step 0) and pin the best one.
For both adapters the video/audio-balanced table scored best:
| Adapter | Calibration rows | Step-0 score: max / all-token MSE / balanced |
|---|---|---|
| Ref2VA | 1,133 | 0.3311 / 0.3299 / **0.3260** |
| FL2VA | 546 | 0.2730 / 0.2741 / **0.2724** |
The selected tables are vendored and pinned by sha256:
| Table | sha256 |
|---|---|
| `examples/train/calibration/pika_h3_flex/amax-ref2va-balanced.json` | `ad60b45f…da90b373` |
| `examples/train/calibration/pika_h3_flex/amax-fl2va-balanced.json` | `b690d117…553e0daa` |
## Running
Point the placeholder paths in the config at the merged model, the row
manifests and an output directory, then launch with `torchrun`. The stage-1
configs assume 16 GPUs and the stage-2 configs 12 (sequence parallel 4,
HSDP over all GPUs); the released FL2VA stage-1 run used 24. Rows reach about
58k tokens; on GB200 the runs use about 90-100 GB per GPU.
```bash
torchrun --nnodes=4 --nproc_per_node=4 --node_rank=$NODE_RANK \
--master_addr=$MASTER_ADDR --master_port=$MASTER_PORT \
-m fastvideo.train.entrypoint.train \
--config examples/train/configs/knowledge_distillation/pika_h3_flex/ref2va_nvfp4_qad.yaml
```
Loading and the step-0 evaluation of a multi-node run can exceed the default
10-minute process-group timeout; set `FASTVIDEO_PG_TIMEOUT_MIN=120`. Any config
key can be overridden on the command line (`--training.distributed.num_gpus 24
--training.distributed.hsdp_shard_dim 24` for 6 nodes, for example). Stage 2
launches the same way with `ref2va_fp4attn_qad.yaml` or `fl2va_fp4attn_qad.yaml`
once stage 1 has written `best/`.
## Stage 2: dense FP4 attention
Deployment can additionally run attention through FlashAttention-4's NVFP4
path: Q and K are quantized to NVFP4 per 16 channels with a unit global scale,
P and V stay BF16. Stage 2 adapts the stage-1 student to these numerics with
`fp4_dense_qat.py`, which routes every dense attention layer through the FP4
attention training kernel with full block lists and straight-through
gradients. `scripts/distill/pika_h3_flex_qad/check_fp4_attention_parity.py`
checks that the emulator matches the FA4 kernel (on a Blackwell GPU): the
emulator-to-kernel distance is under 1% of the FP4 quantization error.
Plain FP4 attention costs 13-17% on the step-0 score. Two exact Q/K transforms
and two BF16 blocks recover most of it before any training:
- **smooth-K:** subtract each head's token-mean of K before quantizing
(softmax is invariant to a per-query shift).
- **Hadamard:** rotate Q and K by the same orthonormal 128-point Hadamard
matrix (`Q H (K H)^T = Q K^T`), spreading outlier channels across the
16-channel scale blocks.
- **BF16 blocks 45 and 49:** the two attention layers with the largest FP4
error keep BF16 attention.
| Per-layer FP4 attention error vs BF16 (real activations) | FL2VA | Ref2VA |
|---|---|---|
| Plain FP4 | 0.115 | 0.113 |
| smooth-K + Hadamard | 0.095 | 0.095 |
| + blocks 45 and 49 in BF16 | 0.083 | 0.080 |
The stage-2 configs set `qk_transform: smooth_k+hadamard` and
`bf16_blocks: [45, 49]`, start from stage 1's `best/`
(`method.init_student_dcp`) and train at 5e-6 (Ref2VA) or 3e-6 (FL2VA). Deploy
a stage-2 checkpoint with the same settings:
```bash
python examples/inference/basic/basic_pika_h3_flex.py ... \
--nvfp4-attention smooth_k+hadamard --nvfp4-attention-bf16-blocks 45,49
```
(equivalently `FASTVIDEO_NVFP4_FA4=1`,
`FASTVIDEO_NVFP4_FA4_QK_TRANSFORM=smooth_k+hadamard`,
`FASTVIDEO_NVFP4_FA4_BF16_BLOCKS=45,49`).
Measured on one GB200 (FL2VA, NVFP4 DiT and decoder, warm), FP4 attention
helps long clips and is neutral on short ones:
| Clip | BF16 attention | FP4 attention | FP4 + transforms |
|---|---|---|---|
| 480p, 5 s | 5.87 s | 5.66 s | 6.08 s |
| 768p, 15 s | 99.7 s | 84.7 s | 87.4 s |
The transforms currently run as separate PyTorch ops before the FP4 quantizer;
fusing them into the quantizer removes most of their cost.
## Export
Checkpoints hold fp32 masters in DCP format. Export a checkpoint to the packed
NVFP4 format the inference loader reads, then verify it:
```bash
# 1. bf16 trunk from the student's masters (CPU)
python scripts/distill/pika_h3_flex_qad/export_student_bf16.py \
--dcp outputs/pika_flex_ref2va_nvfp4_qad/best/dcp \
--base data/models/pika-flex-ref2va-merged --trunk transformer_ref \
--out exports/ref2va/transformer_bf16
# 2. pack NVFP4 linears with the pinned activation scales
python scripts/checkpoint_conversion/convert_minimax_h3_modelopt_nvfp4_dit.py \
--src exports/ref2va/transformer_bf16 --dst exports/ref2va/transformer_ref \
--quantize-ffn --quantize-attention \
--act-amax examples/train/calibration/pika_h3_flex/amax-ref2va-balanced.json
# 3. byte-level parity with the QAD numerics (1 GPU)
python scripts/distill/pika_h3_flex_qad/check_nvfp4_export_parity.py \
--export exports/ref2va/transformer_ref --bf16 exports/ref2va/transformer_bf16 \
--amax examples/train/calibration/pika_h3_flex/amax-ref2va-balanced.json
```
The parity check confirms that packed weights and block scales are
byte-identical to what training quantized and that the deployed NVFP4 GEMM
matches the training GEMM bit for bit. Repeat for FL2VA with
`--trunk transformer` and its table, then assemble a model directory:
```bash
python scripts/distill/pika_h3_flex_qad/assemble_flex_release.py --output release/Pika-H3-Flex-NVFP4 \
--base <MiniMax-H3 snapshot> --pika <Pika-H3-Flex snapshot> \
--transformer-ref exports/ref2va/transformer_ref --ref2va-merged data/models/pika-flex-ref2va-merged \
--transformer exports/fl2va/transformer --fl2va-merged data/models/pika-flex-fl2va-merged
```
`--text-encoder` and `--vae-decoder-nvfp4` add NVFP4 text-encoder and decoder
exports.
## Code map
| Path | Purpose |
|---|---|
| `fastvideo/train/models/minimax_h3/flex.py` | Flex model plugins: sampling contract, re-noise ladder, FL2VA keyframe rows, QAD install |
| `fastvideo/train/methods/knowledge_distillation/pdd_qad.py` | Shared QAD method: losses, evaluation, guards |
| `fastvideo/attention/backends/fp4_dense_qat.py` | Dense FP4 attention emulator and install hook |
| `fastvideo/attention/utils/nvfp4_qk_precondition.py` | Exact Q/K transforms shared by training and inference |
| `fastvideo/attention/backends/flash_attn.py` | Deployed FA4 NVFP4 attention (`nvfp4_fa4`, `nvfp4_qk_transform`) |
| `fastvideo/pipelines/basic/pika_h3_flex/adapter.py` | Adapter loading and the merged-adapter marker |
| `scripts/distill/pika_h3_flex_qad/merge_pika_adapter.py` | Merge an adapter into its trunk |
| `scripts/distill/pika_h3_flex_qad/build_flex_ref2va_rows.py` | Ref2VA and FL2VA training rows |
| `scripts/distill/pika_h3_flex_qad/calibrate_flex_nvfp4.py` | FFN activation-scale calibration |
| `scripts/distill/pika_h3_flex_qad/check_fp4_attention_parity.py` | Emulator vs FA4 NVFP4 kernel parity |
| `scripts/distill/pika_h3_flex_qad/export_student_bf16.py` | DCP masters to a bf16 trunk |
| `scripts/distill/pika_h3_flex_qad/check_nvfp4_export_parity.py` | Packed export vs QAD numerics parity |
| `scripts/distill/pika_h3_flex_qad/assemble_flex_release.py` | Self-contained NVFP4 model directory |
| `examples/train/configs/knowledge_distillation/pika_h3_flex/` | Stage-1 and stage-2 configs for both adapters |
| `examples/train/calibration/pika_h3_flex/` | Pinned FFN activation `amax` tables |
## Expected behavior
Teacher-forced error falls steadily for the first 30-60 steps and flattens by
step 90-120. Video improves faster than audio. Results of the released runs
(held-out rows, lower is better, distances to the bf16 Flex teacher):
| Run | Step 0 (PTQ) | Selected | Notes |
|---|---|---|---|
| Ref2VA stage 1, 16 GPUs | 0.326 | **0.270** at step 120 (-17.1%) | video x0 0.185, audio x0 0.356 |
| FL2VA stage 1, 24 GPUs | 0.272 | **0.234** at step 60 (-14.1%) | first-frame 0.072, last-frame 0.247 |
- FL2VA's last-frame match can drift above its tolerance while the score keeps
improving; at 5e-6 this stopped stage 2 at step 60 with no eligible
checkpoint, so its stage-2 config uses 3e-6.
- A lower teacher-forced score with a failed guard is not a better model:
promote only eligible checkpoints, and review the full-eval samples
(`eval_save_dir`) before release.
+2 -2
View File
@@ -18,7 +18,7 @@ should use the config-driven system documented here.
### Launch with the helper script
```bash
bash examples/train/run.sh examples/train/distill_wan2.1_t2v_1.3B_dmd2.yaml
bash examples/train/run.sh examples/train/configs/distribution_matching/wan/dmd2_t2v.yaml
```
The script auto-detects available GPUs and sets up `torchrun`. Override with
@@ -35,7 +35,7 @@ NUM_GPUS=4 NNODES=2 NODE_RANK=0 \
```bash
torchrun --nproc_per_node=8 \
fastvideo/train/entrypoint/train.py \
--config examples/train/distill_wan2.1_t2v_1.3B_dmd2.yaml
--config examples/train/configs/distribution_matching/wan/dmd2_t2v.yaml
```
### CLI flags
@@ -1,6 +1,5 @@
# Start Ray on both Sparks and source spark_pair_env.sh first; see spark_pair.md.
# FastH3-QAD-v2 eight-forward video+audio on two DGX Sparks over QSFP RoCE.
# Uses the published fast-8L NVFP4 decoder on both workers (FASTVIDEO_H3_VAE_NVFP4_DECODER) with parallel temporal chunks.
# Download the complete release checkpoint as described in
# docs/getting_started/installation/spark_performance.md.
#
@@ -10,8 +9,8 @@
# settings below are an inference choice, not its linears-only QAD recipe.
# The NVFP4 encoder and lightweight VAE ship in the same repository.
#
# FASTVIDEO_H3_VSA_FP4=1 FASTVIDEO_MINIMAX_H3_FUSIONS=all FASTVIDEO_NVFP4_MM_BACKEND=cudnn FASTVIDEO_NVFP4_MM_ROW_SPLIT=table \
# FASTVIDEO_H3_VAE_TILE_BATCH=12 FASTVIDEO_H3_VAE_INT8_OVERLAY=0 \
# FASTVIDEO_H3_VSA_FP4=1 FASTVIDEO_MINIMAX_H3_FUSIONS=all FASTVIDEO_NVFP4_MM_BACKEND=cutlass \
# FASTVIDEO_H3_VAE_TILE_BATCH=1 \
# FASTVIDEO_VSA_TRITON=1 FASTVIDEO_VSA_PTX_BLACKWELL=0 FASTVIDEO_FA4=0 \
# FASTVIDEO_ATTENTION_BACKEND=VIDEO_SPARSE_ATTN_H3 FASTVIDEO_STAGE_LOGGING=1 \
# fastvideo generate --config examples/inference/basic/basic_fasth3_spark_pair_qad_v2_nvfp4.yaml
@@ -37,7 +36,7 @@ generator:
lazy_module_load: false
compile:
enabled: false
vae_enabled: false # Published NVFP4 decoder uses its fused eager inference path.
vae_enabled: false
pipeline:
workload_type: t2v
vae_tiling: true
@@ -48,11 +47,6 @@ generator:
prefix_precision: fp8
two_level_p: true
smooth_k: true
sp_head_chunks: 7
sp_pooled_key_mean: true
sp_direct_keys: true
sp_projection_overlap: true
sp_input_gather: true
VSA_sparsity: 0.8
VSA_tile_size: 64
h3_sequential_load: false
@@ -1,5 +1,4 @@
# FastH3-QAD-v2 eight-forward video+audio on one DGX Spark.
# Uses the published fast-8L NVFP4 decoder selected with FASTVIDEO_H3_VAE_NVFP4_DECODER (see spark_performance.md).
# Download the complete release checkpoint as described in
# docs/getting_started/installation/spark_performance.md.
#
@@ -35,7 +34,7 @@ generator:
lazy_module_load: false
compile:
enabled: false
vae_enabled: false # Packed NVFP4 decoder has its own fused inference path.
vae_enabled: true
pipeline:
workload_type: t2v
vae_tiling: true
@@ -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()
@@ -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()
@@ -0,0 +1,73 @@
# SPDX-License-Identifier: Apache-2.0
"""Qwen-Image-2.1 generation, editing, references and transparent PNG output."""
import argparse
from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig,
GenerationRequest,
GeneratorConfig,
InputConfig,
OffloadConfig,
OutputConfig,
ParallelismConfig,
PipelineSelection,
SamplingConfig,
)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--model-path", default="Qwen/Qwen-Image-2.1")
parser.add_argument("--revision")
parser.add_argument("--prompt", default="A red ceramic teapot, isolated on a transparent background.")
parser.add_argument("--reference", action="append", default=[], help="Ordered reference image; repeat up to ten times")
parser.add_argument("--negative-prompt", default=None)
parser.add_argument("--true-cfg-scale", type=float, default=1.0)
parser.add_argument("--height", type=int, default=1024)
parser.add_argument("--width", type=int, default=1024)
parser.add_argument("--reference-resolution", type=int, default=1024)
parser.add_argument("--steps", type=int, default=40)
parser.add_argument("--seed", type=int, default=42)
parser.add_argument("--use-kv-cache", action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--output", default="outputs/qwen_image21/image.png")
args = parser.parse_args()
if len(args.reference) > 10:
parser.error("At most ten ordered reference images are supported")
workload = "i2i" if args.reference else "t2i"
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path=args.model_path,
revision=args.revision,
engine=EngineConfig(
num_gpus=1,
parallelism=ParallelismConfig(tp_size=1, sp_size=1),
offload=OffloadConfig(dit=True, dit_layerwise=True, text_encoder=True, vae=True,
lazy_module_load=True),
),
pipeline=PipelineSelection(
workload_type=workload,
experimental={
"reference_resolution": args.reference_resolution,
"use_kv_cache": args.use_kv_cache,
},
),
))
try:
generator.generate(
GenerationRequest(
prompt=args.prompt,
negative_prompt=args.negative_prompt,
inputs=InputConfig(references=args.reference or None),
sampling=SamplingConfig(height=args.height, width=args.width, num_frames=1, fps=1,
num_inference_steps=args.steps, guidance_scale=1.0,
true_cfg_scale=args.true_cfg_scale, seed=args.seed),
output=OutputConfig(output_path=args.output, save_video=True, return_frames=False),
))
finally:
generator.shutdown()
if __name__ == "__main__":
main()
@@ -114,9 +114,10 @@ def build_parser() -> argparse.ArgumentParser:
parser.add_argument("--vsa-ref-keep-rate", type=float, default=None,
help="fraction of each reference video's tiles a query keeps (p2_multi_region); "
"default is the contract's trained value")
parser.add_argument("--vsa-impl", choices=("auto", "reference", "simd"), default="auto",
parser.add_argument("--vsa-impl", choices=("auto", "reference", "simd", "sdpa", "nax"), default="auto",
help="sparse attention backend; auto uses the SIMD Metal kernel for 128-token tiles and "
"gather+SDPA otherwise, reference forces gather+SDPA")
"gather+SDPA otherwise, reference forces gather+SDPA; sdpa shares K/V with a tile mask "
"and nax uses M5 TensorOps over selected blocks (both opt-in, incompatible with --reuse-prefix-kv)")
parser.add_argument("--reuse-prefix-kv", action="store_true",
help="opt-in, lossy: record the text/reference rows' per-block K/V on step 1 and run later "
"steps on the target audio/video rows only (faster, slightly different output)")
@@ -145,6 +146,8 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser.error("Ref2VA needs at least one image or video reference")
if args.model_root is None and (args.export is None or args.base_model_path is None):
parser.error("give --model-root, or --export together with --base-model-path")
if args.vsa_impl in ("sdpa", "nax") and args.reuse_prefix_kv:
parser.error(f"--vsa-impl {args.vsa_impl} does not support --reuse-prefix-kv")
return args
@@ -0,0 +1,39 @@
# Replace source.png and mask.png with your own images. The mask is an ordered
# reference interpreted by the model, not a hard pixel-preservation constraint.
generator:
model_path: Qwen/Qwen-Image-2.1
engine:
num_gpus: 1
parallelism:
tp_size: 1
sp_size: 1
offload:
dit: true
dit_layerwise: true
text_encoder: true
vae: true
lazy_module_load: true
pipeline:
workload_type: i2i
experimental:
reference_resolution: 1024
use_kv_cache: true
request:
prompt: In image 1, turn the object highlighted by the mask in image 2 blue.
inputs:
references:
- source.png
- mask.png
sampling:
height: 1024
width: 1024
num_frames: 1
fps: 1
num_inference_steps: 40
guidance_scale: 1.0
true_cfg_scale: 1.0
seed: 42
output:
output_path: outputs/qwen_image21/edit.png
save_video: true
return_frames: false
@@ -0,0 +1,40 @@
# Run on an allocated CUDA GPU after installing FastVideo and providing weights.
# fastvideo generate --config examples/inference/basic/qwen_image21_t2i.yaml
generator:
model_path: Qwen/Qwen-Image-2.1
engine:
num_gpus: 1
compile:
enabled: false
backend: inductor
fullgraph: false
parallelism:
tp_size: 1
sp_size: 1
offload:
dit: true
dit_layerwise: true
text_encoder: true
vae: true
lazy_module_load: true
pipeline:
workload_type: t2i
experimental:
kv_cache_device: cuda
reference_resolution: 1024
use_kv_cache: true
request:
prompt: A red ceramic teapot, isolated on a transparent background.
sampling:
height: 1024
width: 1024
num_frames: 1
fps: 1
num_inference_steps: 40
guidance_scale: 1.0
true_cfg_scale: 1.0
seed: 42
output:
output_path: outputs/qwen_image21/teapot.png
save_video: true
return_frames: false
@@ -19,7 +19,3 @@ export NCCL_NVLS_ENABLE="${NCCL_NVLS_ENABLE:-0}"
# runaway and SIGTERMs the worker around shard 11/14.
export RAY_memory_monitor_refresh_ms="${RAY_memory_monitor_refresh_ms:-0}"
export RAY_memory_usage_threshold="${RAY_memory_usage_threshold:-1.0}"
# Measured on a two-Spark QSFP pair: eight NCCL channels per peer and high-priority
# NCCL streams let the sequence-parallel exchanges overlap the projection GEMMs.
export NCCL_NCHANNELS_PER_NET_PEER="${NCCL_NCHANNELS_PER_NET_PEER:-8}"
export TORCH_NCCL_HIGH_PRIORITY="${TORCH_NCCL_HIGH_PRIORITY:-1}"
@@ -0,0 +1,47 @@
# OpenAI-compatible FastH3 V2.1 NVFP4 server with a per-request community LoRA allow-list.
# Do not combine with generator.pipeline.components.lora_path (a startup adapter); the server refuses both.
# Clients select an adapter with {"lora": {"name": "<id>", "scale": 0.8}} on POST /v1/videos/sync.
generator:
model_path: /path/to/FastVideo-FastH3-8-Step-V2.1-NVFP4-Consumer
engine:
num_gpus: 1
quantization:
transformer_quant: NVFP4
layer_profile: h3_dit_vsa
offload:
dit: true
dit_layerwise: true
text_encoder: true
image_encoder: true
vae: true
pin_cpu_memory: true
compile:
enabled: false
pipeline:
workload_type: t2v
server:
host: 0.0.0.0
port: 8000
output_dir: outputs/openai_fasth3_lora_allowlist
lora:
adapters:
- id: fal/MiniMax-H3-Realism-People-LoRA
path: fal/MiniMax-H3-Realism-People-LoRA
default_strength: 0.8
min_strength: 0.0
max_strength: 1.5
description: Photoreal people and faces for MiniMax-H3.
license: Apache-2.0
source_url: https://huggingface.co/fal/MiniMax-H3-Realism-People-LoRA
default_request:
negative_prompt: ""
sampling:
height: 768
width: 1344
num_frames: 124
fps: 24
# The V2.1 consumer checkpoint samples a 9-point sigma grid for its 8 DMD forwards.
num_inference_steps: 9

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