Compare commits

...
19 changed files with 1028 additions and 49 deletions
@@ -0,0 +1,147 @@
# SPDX-License-Identifier: Apache-2.0
"""Generate 5-second FastH3 videos with a supported Preview adapter."""
from __future__ import annotations
import argparse
import os
import statistics
import sys
import time
from pathlib import Path
from huggingface_hub import hf_hub_download
FASTVIDEO_ROOT = Path(__file__).resolve().parents[3]
sys.path.insert(0, str(FASTVIDEO_ROOT))
from fastvideo import VideoGenerator # noqa: E402
from fastvideo.api import ( # noqa: E402
CompileConfig,
ComponentConfig,
EngineConfig,
GenerationRequest,
GeneratorConfig,
OffloadConfig,
OutputConfig,
ParallelismConfig,
PipelineSelection,
SamplingConfig,
)
# FastVideo exposes these three backend switches only through environment variables.
os.environ.update({
"FASTVIDEO_FA4": "1",
"FASTVIDEO_MINIMAX_H3_FUSIONS": "all",
"FASTVIDEO_VSA_SM100A": "0",
})
os.environ.pop("FASTVIDEO_INFERENCE_TORCH_COMPILE", None)
VARIANT_BACKENDS = {
"dense-datafree": "FLASH_ATTN",
"vsa-datafree": "VIDEO_SPARSE_ATTN_H3",
"vsa-synthetic-step1300": "VIDEO_SPARSE_ATTN_H3",
"vsa-synthetic-step1900": "VIDEO_SPARSE_ATTN_H3",
}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("variant", choices=VARIANT_BACKENDS)
parser.add_argument("--prompt", required=True)
return parser.parse_args()
def main() -> None:
"""Run one compile warmup and three measured generations with one fixed recipe."""
args = parse_args()
attention_backend = VARIANT_BACKENDS[args.variant]
experimental = {
"attention_backend": attention_backend,
"inference_torch_compile": attention_backend == "FLASH_ATTN",
"vae_parallel_decode": True,
"vae_parallel_decode_strategy": "gather",
}
if attention_backend == "VIDEO_SPARSE_ATTN_H3":
experimental.update({
"VSA_sparsity": 0.9,
"VSA_tile_size": 64,
})
adapter_path = hf_hub_download(
repo_id="FastVideo/FastVideo-FastH3-4-step-Preview-v1-LoRA",
filename=f"{args.variant}/adapter_model.safetensors",
)
output_dir = FASTVIDEO_ROOT / "outputs/fasth3_lora_preview" / args.variant
output_dir.mkdir(parents=True, exist_ok=True)
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="MiniMaxAI/MiniMax-H3",
pipeline=PipelineSelection(
components=ComponentConfig(lora_path=adapter_path, lora_strength=1.0),
experimental=experimental,
),
engine=EngineConfig(
num_gpus=4,
parallelism=ParallelismConfig(tp_size=1, sp_size=4),
offload=OffloadConfig(dit=False, dit_layerwise=False),
compile=CompileConfig(vae_enabled=True),
),
)
)
generation_count = 4
warmup_count = 1
measured_count = generation_count - warmup_count
measured_seconds: list[float] = []
print(f"Variant: {args.variant} ({attention_backend})")
print(f"Output directory: {output_dir}")
try:
for generation_index in range(generation_count):
measured = generation_index >= warmup_count
if measured:
measured_index = generation_index - warmup_count + 1
label = f"measured {measured_index}/{measured_count}"
output_path = output_dir / f"fasth3_all_run_{measured_index:02d}.mp4"
else:
warmup_index = generation_index + 1
label = f"warmup {warmup_index}/{warmup_count}"
output_path = output_dir / f"_fasth3_warmup_{warmup_index:02d}.mp4"
request = GenerationRequest(
prompt=args.prompt,
negative_prompt="",
sampling=SamplingConfig(
height=768,
width=1344,
num_frames=345, # <-- Change video length: 5sec: 124; 10sec: 243; 15sec: 345.
fps=24,
num_inference_steps=5,
guidance_scale=1.0,
batch_cfg=False,
seed=1000,
),
output=OutputConfig(
output_path=str(output_path),
save_video=True,
return_frames=False,
),
)
started = time.perf_counter()
result = generator.generate(request)
elapsed = time.perf_counter() - started
if measured:
measured_seconds.append(elapsed)
suffix = "" if measured else " (excluded from median)"
print(f"[{label}] {result.video_path or output_path}: {elapsed:.3f}s{suffix}")
finally:
generator.shutdown()
print(f"Median E2E wall time: {statistics.median(measured_seconds):.3f}s")
if __name__ == "__main__":
main()
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
PROMPT="integrated_multimodal_description: A red fox runs through fresh snow at dawn. overall_soundscape: Fast pawsteps in snow, winter wind, and distant birds."
# Options: dense-datafree, vsa-datafree, vsa-synthetic-step1300, vsa-synthetic-step1900
VARIANT="vsa-datafree"
exec python "$(dirname "$0")/basic_fasth3_simplified.py" \
"$VARIANT" \
--prompt "$PROMPT"
@@ -0,0 +1,126 @@
# SPDX-License-Identifier: Apache-2.0
"""Profile one FastH3 Preview generation with NVTX after warmup."""
from __future__ import annotations
import argparse
from pathlib import Path
import torch
from huggingface_hub import hf_hub_download
from fastvideo import VideoGenerator
from fastvideo.api import (
CompileConfig,
ComponentConfig,
EngineConfig,
GenerationRequest,
GeneratorConfig,
OffloadConfig,
OutputConfig,
ParallelismConfig,
PipelineSelection,
QuantizationConfig,
SamplingConfig,
)
from fastvideo.profiler import nvtx_range
VARIANT_BACKENDS = {
"dense-datafree": "FLASH_ATTN",
"vsa-datafree": "VIDEO_SPARSE_ATTN_H3",
"vsa-synthetic-step1300": "VIDEO_SPARSE_ATTN_H3",
"vsa-synthetic-step1900": "VIDEO_SPARSE_ATTN_H3",
}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("variant", choices=VARIANT_BACKENDS)
parser.add_argument("--prompt", required=True)
parser.add_argument("--output", required=True)
parser.add_argument("--num-frames", type=int, default=345)
parser.add_argument("--warmup-runs", type=int, default=3)
return parser.parse_args()
def main() -> None:
"""Warm the selected FastH3 recipe, then profile one identical generation."""
args = parse_args()
output_dir = Path(args.output)
output_dir.mkdir(parents=True, exist_ok=True)
attention_backend = VARIANT_BACKENDS[args.variant]
experimental = {
"attention_backend": attention_backend,
"inference_torch_compile": attention_backend == "FLASH_ATTN",
"vae_parallel_decode": True,
"vae_parallel_decode_strategy": "gather",
}
if attention_backend == "VIDEO_SPARSE_ATTN_H3":
experimental.update({
"VSA_sparsity": 0.9,
"VSA_tile_size": 64,
})
adapter_path = hf_hub_download(
repo_id="FastVideo/FastVideo-FastH3-4-step-Preview-v1-LoRA",
filename=f"{args.variant}/adapter_model.safetensors",
)
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="MiniMaxAI/MiniMax-H3",
pipeline=PipelineSelection(
components=ComponentConfig(lora_path=adapter_path, lora_strength=1.0),
experimental=experimental,
),
engine=EngineConfig(
num_gpus=4,
parallelism=ParallelismConfig(tp_size=1, sp_size=4),
offload=OffloadConfig(dit=False, dit_layerwise=False),
compile=CompileConfig(vae_enabled=True),
quantization=QuantizationConfig(transformer_quant="MXFP8"),
),
)
)
request = GenerationRequest(
prompt=args.prompt,
negative_prompt="",
sampling=SamplingConfig(
height=768,
width=1344,
num_frames=args.num_frames,
fps=24,
num_inference_steps=5,
guidance_scale=1.0,
batch_cfg=False,
seed=1000,
),
output=OutputConfig(
output_path=str(output_dir / "fasth3_profile.mp4"),
save_video=True,
return_frames=False,
),
)
try:
for warmup_index in range(args.warmup_runs):
warmup_result = generator.generate(request)
print(f"Warmup {warmup_index + 1}/{args.warmup_runs}: {warmup_result.video_path}")
torch.cuda.profiler.start()
try:
with nvtx_range("fasth3.profiled_generation"):
measured_result = generator.generate(request)
finally:
torch.cuda.profiler.stop()
print(f"Profiled output: {measured_result.video_path}")
if measured_result.generation_time is not None:
print(f"Generation time: {measured_result.generation_time:.2f}s")
finally:
generator.shutdown()
if __name__ == "__main__":
main()
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
# Capture one FastH3 Preview generation with CUDA and FastVideo NVTX ranges.
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
WORKTREE_ROOT="$(cd -- "${SCRIPT_DIR}/../../.." && pwd)"
WORKSPACE_ROOT="$(cd -- "${WORKTREE_ROOT}/.." && pwd)"
PYTHON_BIN="${WORKSPACE_ROOT}/.venv-fv/bin/python"
NSYS_BIN=/usr/local/cuda/bin/nsys
PROFILE_SCRIPT="${SCRIPT_DIR}/basic_fasth3_simplified_profile.py"
export CUDA_VISIBLE_DEVICES=0,1,2,3
export FASTVIDEO_FA4=1
export FASTVIDEO_INFERENCE_TORCH_COMPILE=0
export FASTVIDEO_MINIMAX_H3_FUSIONS=all
export FASTVIDEO_NVTX_PROFILE=1
export FASTVIDEO_VSA_SM100A=0
export PYTHONUNBUFFERED=1
export PYTHONPATH="${WORKTREE_ROOT}${PYTHONPATH:+:${PYTHONPATH}}"
PROMPT="integrated_multimodal_description: A red fox runs through fresh snow at dawn. overall_soundscape: Fast pawsteps in snow, winter wind, and distant birds."
# Options: dense-datafree, vsa-datafree, vsa-synthetic-step1300, vsa-synthetic-step1900
VARIANT="vsa-datafree"
NUM_FRAMES=345
WARMUP_RUNS=3
PROFILE_ID="${VARIANT}_4gpu_sp4_${NUM_FRAMES}frames"
RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)-$$"
RESULT_DIR="${WORKTREE_ROOT}/runs/fasth3_lora_preview_profile/${PROFILE_ID}/${RUN_ID}"
MEDIA_DIR="${RESULT_DIR}/media"
RUN_LOG="${RESULT_DIR}/run.log"
mkdir -p "${MEDIA_DIR}"
"${NSYS_BIN}" profile \
--trace=cuda,nvtx \
--sample=none \
--cpuctxsw=none \
--capture-range=cudaProfilerApi \
--capture-range-end=stop \
--output="${RESULT_DIR}/${PROFILE_ID}" \
"${PYTHON_BIN}" "${PROFILE_SCRIPT}" \
"${VARIANT}" \
--num-frames "${NUM_FRAMES}" \
--warmup-runs "${WARMUP_RUNS}" \
--prompt "${PROMPT}" \
--output "${MEDIA_DIR}" \
2>&1 | tee "${RUN_LOG}"
printf 'RESULT_DIR=%s\n' "${RESULT_DIR}"
printf 'NSYS_REPORT=%s\n' "${RESULT_DIR}/${PROFILE_ID}.nsys-rep"
printf 'MEDIA_DIR=%s\n' "${MEDIA_DIR}"
+282
View File
@@ -0,0 +1,282 @@
# SPDX-License-Identifier: Apache-2.0
"""MXFP8 block quantization and linear operations for Blackwell inference."""
from __future__ import annotations
import torch
import torch.nn.functional as F
import triton
import triton.language as tl
from quack.mx_utils import to_blocked, to_mx
MXFP8_BLOCK_SIZE = 32
_BLOCKS_PER_PROGRAM = 16
def _validate_mxfp8_matrix(matrix: torch.Tensor) -> None:
"""Validate one row-major matrix before MXFP8 quantization."""
if matrix.ndim != 2:
raise ValueError(f"MXFP8 quantization requires a 2D tensor, got shape {tuple(matrix.shape)}.")
if matrix.dtype not in (torch.bfloat16, torch.float32):
raise TypeError(f"MXFP8 quantization requires BF16 or FP32 input, got {matrix.dtype}.")
if matrix.shape[1] % MXFP8_BLOCK_SIZE:
raise ValueError(
f"MXFP8 reduction dimension must be divisible by {MXFP8_BLOCK_SIZE}, got {matrix.shape[1]}.")
@torch.compile(dynamic=True, fullgraph=True)
def _quantize_mxfp8_weight_blockwise(matrix: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
values, natural_scales = to_mx(matrix.contiguous(), MXFP8_BLOCK_SIZE)
return values, to_blocked(natural_scales)
def quantize_mxfp8_weight_blockwise(matrix: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
"""Prequantize a weight with Quack and return hardware-blocked scales."""
_validate_mxfp8_matrix(matrix)
return _quantize_mxfp8_weight_blockwise(matrix)
@triton.jit
def _store_mxfp8_blocks(
matrix,
quantized_ptr,
scale_ptr,
row,
first_block,
row_count,
column_count,
scale_column_count,
BLOCKS_PER_PROGRAM: tl.constexpr,
):
"""Quantize row blocks and store scales in cuBLAS's 32x4x4 layout."""
element_offsets = tl.arange(0, BLOCKS_PER_PROGRAM * 32)
columns = first_block * 32 + element_offsets
valid_elements = (row < row_count) & (columns < column_count)
matrix = tl.reshape(matrix, (BLOCKS_PER_PROGRAM, 32))
# FLOOR scaling encodes exponent(max_abs) - 8 as one E8M0 byte per 32 values.
max_abs = tl.max(tl.abs(matrix), axis=1)
exponent = (max_abs.to(tl.int32, bitcast=True) >> 23) & 0xFF
scale_biased = tl.maximum(tl.minimum(exponent - 8, 255), 0)
scale_biased = tl.where(max_abs != max_abs, 255, scale_biased)
scale_bits = scale_biased.to(tl.int32) << 23
scale = tl.maximum(scale_bits.to(tl.float32, bitcast=True), 1.1754943508222875e-38)
scaled_matrix = tl.maximum(tl.minimum(matrix / scale[:, None], 448.0), -448.0)
tl.store(
quantized_ptr + row * column_count + columns,
tl.reshape(scaled_matrix.to(tl.float8e4nv), (BLOCKS_PER_PROGRAM * 32, )),
mask=valid_elements,
)
blocks = first_block + tl.arange(0, BLOCKS_PER_PROGRAM)
# Map natural [row, K / 32] coordinates into cuBLAS's padded 128x4 tiles.
row_block = row // 128
row_in_block = row % 128
column_block = blocks // 4
column_in_block = blocks % 4
scale_offset = (
(row_block * (scale_column_count // 4) + column_block) * 512
+ (row_in_block % 32) * 16
+ (row_in_block // 32) * 4
+ column_in_block
)
tl.store(scale_ptr + scale_offset, scale_biased.to(tl.uint8), mask=blocks < scale_column_count)
@triton.jit
def _quantize_mxfp8_kernel(
matrix_ptr,
quantized_ptr,
scale_ptr,
row_count,
column_count,
scale_column_count,
BLOCKS_PER_PROGRAM: tl.constexpr,
):
"""Load BF16 activation blocks for direct MXFP8 quantization."""
row = tl.program_id(0)
first_block = tl.program_id(1) * BLOCKS_PER_PROGRAM
element_offsets = tl.arange(0, BLOCKS_PER_PROGRAM * 32)
columns = first_block * 32 + element_offsets
matrix = tl.load(
matrix_ptr + row * column_count + columns,
mask=(row < row_count) & (columns < column_count),
other=0.0,
).to(tl.float32)
_store_mxfp8_blocks(
matrix,
quantized_ptr,
scale_ptr,
row,
first_block,
row_count,
column_count,
scale_column_count,
BLOCKS_PER_PROGRAM,
)
@triton.jit
def _swiglu_quantize_mxfp8_kernel(
preactivation_ptr,
quantized_ptr,
scale_ptr,
row_count,
column_count,
scale_column_count,
BLOCKS_PER_PROGRAM: tl.constexpr,
):
"""Apply value-first SwiGLU and directly quantize its BF16 result."""
row = tl.program_id(0)
first_block = tl.program_id(1) * BLOCKS_PER_PROGRAM
element_offsets = tl.arange(0, BLOCKS_PER_PROGRAM * 32)
columns = first_block * 32 + element_offsets
valid_elements = (row < row_count) & (columns < column_count)
packed_row_offset = row * (2 * column_count)
values = tl.load(
preactivation_ptr + packed_row_offset + columns,
mask=valid_elements,
other=0.0,
).to(tl.float32)
gates = tl.load(
preactivation_ptr + packed_row_offset + column_count + columns,
mask=valid_elements,
other=0.0,
).to(tl.float32)
postactivation = (values * gates * tl.sigmoid(gates)).to(tl.bfloat16).to(tl.float32)
_store_mxfp8_blocks(
postactivation,
quantized_ptr,
scale_ptr,
row,
first_block,
row_count,
column_count,
scale_column_count,
BLOCKS_PER_PROGRAM,
)
def _allocate_mxfp8_outputs(matrix: torch.Tensor, column_count: int) -> tuple[torch.Tensor, torch.Tensor, int]:
"""Allocate contiguous values and a complete padded blocked-scale buffer."""
row_count = matrix.shape[0]
scale_column_count = triton.cdiv(column_count // MXFP8_BLOCK_SIZE, 4) * 4
blocked_scale_size = triton.cdiv(row_count, 128) * 128 * scale_column_count
quantized = torch.empty((row_count, column_count), dtype=torch.float8_e4m3fn, device=matrix.device)
scale_storage = torch.empty(blocked_scale_size, dtype=torch.uint8, device=matrix.device)
return quantized, scale_storage.view(torch.float8_e8m0fnu), scale_column_count
def quantize_mxfp8_blockwise(matrix: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
"""Quantize activation rows and write hardware-blocked scales directly."""
_validate_mxfp8_matrix(matrix)
matrix = matrix.contiguous()
row_count, column_count = matrix.shape
quantized, blocked_scales, scale_column_count = _allocate_mxfp8_outputs(matrix, column_count)
grid = (triton.cdiv(row_count, 128) * 128, triton.cdiv(scale_column_count, _BLOCKS_PER_PROGRAM))
_quantize_mxfp8_kernel[grid](
matrix,
quantized,
blocked_scales.view(torch.uint8),
row_count,
column_count,
scale_column_count,
BLOCKS_PER_PROGRAM=_BLOCKS_PER_PROGRAM,
num_warps=8,
)
return quantized, blocked_scales
def swiglu_quantize_mxfp8_blockwise(preactivation: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
"""Apply H3 value-first SwiGLU and quantize the BF16 result to MXFP8."""
if preactivation.ndim != 2:
raise ValueError(f"MXFP8 SwiGLU requires a 2D tensor, got shape {tuple(preactivation.shape)}.")
if preactivation.dtype != torch.bfloat16:
raise TypeError(f"MXFP8 SwiGLU requires BF16 input, got {preactivation.dtype}.")
if preactivation.shape[1] % (2 * MXFP8_BLOCK_SIZE):
raise ValueError(
"MXFP8 SwiGLU requires each packed half to be divisible by "
f"{MXFP8_BLOCK_SIZE}, got packed width {preactivation.shape[1]}.")
preactivation = preactivation.contiguous()
row_count = preactivation.shape[0]
column_count = preactivation.shape[1] // 2
quantized, blocked_scales, scale_column_count = _allocate_mxfp8_outputs(preactivation, column_count)
grid = (triton.cdiv(row_count, 128) * 128, triton.cdiv(scale_column_count, _BLOCKS_PER_PROGRAM))
_swiglu_quantize_mxfp8_kernel[grid](
preactivation,
quantized,
blocked_scales.view(torch.uint8),
row_count,
column_count,
scale_column_count,
BLOCKS_PER_PROGRAM=_BLOCKS_PER_PROGRAM,
num_warps=8,
)
return quantized, blocked_scales
def mxfp8_scaled_mm(
activation_values: torch.Tensor,
activation_scales: torch.Tensor,
weight_values: torch.Tensor,
weight_scales: torch.Tensor,
bias: torch.Tensor | None,
) -> torch.Tensor:
"""Multiply two MXFP8 matrices and return a BF16 matrix."""
return F.scaled_mm(
mat_a=activation_values,
mat_b=weight_values.mT,
scale_a=activation_scales,
scale_recipe_a=F.ScalingType.BlockWise1x32,
scale_b=weight_scales,
scale_recipe_b=F.ScalingType.BlockWise1x32,
swizzle_a=F.SwizzleType.SWIZZLE_32_4_4,
swizzle_b=F.SwizzleType.SWIZZLE_32_4_4,
bias=bias,
output_dtype=torch.bfloat16,
)
def _resolve_merged_linear(linear: torch.nn.Module) -> torch.nn.Module:
"""Return a linear whose weight includes every active inference adapter."""
base_layer = getattr(linear, "base_layer", linear)
if base_layer is linear:
return base_layer
if not getattr(linear, "merged", False) and not getattr(linear, "disable_lora", False):
raise RuntimeError("MXFP8 feed-forward requires active LoRA weights to be merged before inference.")
return base_layer
def mxfp8_swiglu_feed_forward(
hidden_states: torch.Tensor,
fc_in: torch.nn.Module,
fc_out: torch.nn.Module,
) -> torch.Tensor:
"""Run the MiniMax-H3 feed-forward network with MXFP8 GEMMs."""
fc_in_base = _resolve_merged_linear(fc_in)
fc_out_base = _resolve_merged_linear(fc_out)
fc_in_method = fc_in_base.quant_method
fc_out_method = fc_out_base.quant_method
preactivation = fc_in_method.apply(fc_in_base, hidden_states, fc_in_base.bias)
output_shape = (*hidden_states.shape[:-1], fc_out_base.output_size)
preactivation_2d = preactivation.reshape(-1, preactivation.shape[-1])
activation_values, activation_scales = swiglu_quantize_mxfp8_blockwise(preactivation_2d)
output_2d = fc_out_method.apply_quantized(
fc_out_base,
activation_values,
activation_scales,
fc_out_base.bias,
)
return output_2d.reshape(output_shape)
__all__ = [
"MXFP8_BLOCK_SIZE",
"mxfp8_scaled_mm",
"mxfp8_swiglu_feed_forward",
"quantize_mxfp8_blockwise",
"quantize_mxfp8_weight_blockwise",
"swiglu_quantize_mxfp8_blockwise",
]
+12 -1
View File
@@ -2,7 +2,16 @@ from typing import Literal, get_args
from fastvideo.layers.quantization.base_config import QuantizationConfig
QuantizationMethods = Literal[None, "AbsMaxFP8", "FP8", "NVFP4", "nvfp4_qat", "nvfp4_qat_train", "fp8_qat_train"]
QuantizationMethods = Literal[
None,
"AbsMaxFP8",
"FP8",
"MXFP8",
"NVFP4",
"nvfp4_qat",
"nvfp4_qat_train",
"fp8_qat_train",
]
QUANTIZATION_METHODS: list[str] = list(get_args(QuantizationMethods))
@@ -52,6 +61,7 @@ def get_quantization_config(quantization: str) -> type[QuantizationConfig]:
# lazy import to avoid triggering `torch.compile` too early
from .absmax_fp8 import AbsMaxFP8Config
from .fp8_config import FP8Config
from .mxfp8_config import MXFP8Config
from .nvfp4_config import NVFP4Config
from .nvfp4_qat_config import NVFP4QATConfig
from .nvfp4_qat_train_config import NVFP4QATTrainConfig
@@ -60,6 +70,7 @@ def get_quantization_config(quantization: str) -> type[QuantizationConfig]:
method_to_config: dict[str, type[QuantizationConfig]] = {
"AbsMaxFP8": AbsMaxFP8Config,
"FP8": FP8Config,
"MXFP8": MXFP8Config,
"NVFP4": NVFP4Config,
"nvfp4_qat": NVFP4QATConfig,
"nvfp4_qat_train": NVFP4QATTrainConfig,
@@ -0,0 +1,149 @@
# SPDX-License-Identifier: Apache-2.0
"""MXFP8 quantization for MiniMax-H3 transformer-block feed-forward layers."""
from __future__ import annotations
import re
from typing import Any
import torch
from torch.nn.parameter import Parameter
from fastvideo.layers.quantization.base_config import QuantizationConfig, QuantizeMethodBase
from fastvideo.logger import init_logger
from fastvideo.models.utils import set_weight_attrs
logger = init_logger(__name__)
_MINIMAX_H3_FF_PREFIX = re.compile(r"(?:^|\.)transformer_blocks\.\d+\.ff\.(?:fc_in|fc_out)$")
class MXFP8QuantizeMethod(QuantizeMethodBase):
"""Dynamically quantize activations against prequantized MXFP8 weights."""
def create_weights(
self,
layer: torch.nn.Module,
input_size_per_partition: int,
output_partition_sizes: list[int],
input_size: int,
output_size: int,
params_dtype: torch.dtype,
**extra_weight_attrs: Any,
) -> None:
"""Create the BF16 checkpoint weight and non-persistent MXFP8 buffers."""
del input_size, output_size
weight = Parameter(
torch.empty(
sum(output_partition_sizes),
input_size_per_partition,
dtype=params_dtype,
),
requires_grad=False,
)
set_weight_attrs(weight, {"input_dim": 1, "output_dim": 0})
layer.register_parameter("weight", weight)
set_weight_attrs(weight, extra_weight_attrs)
layer.register_buffer("_mxfp8_weight", None, persistent=False)
layer.register_buffer("_mxfp8_weight_scale", None, persistent=False)
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
"""Prequantize one adapter-merged BF16 linear weight."""
from fastvideo.layers.mxfp8linear import quantize_mxfp8_weight_blockwise
quantized_weight, blocked_scales = quantize_mxfp8_weight_blockwise(layer.weight.detach())
layer._mxfp8_weight = quantized_weight
layer._mxfp8_weight_scale = blocked_scales
def apply(
self,
layer: torch.nn.Module,
hidden_states: torch.Tensor,
bias: torch.Tensor | None = None,
) -> torch.Tensor:
"""Quantize one BF16 activation and apply the prequantized linear."""
from fastvideo.layers.mxfp8linear import quantize_mxfp8_blockwise
original_shape = hidden_states.shape
hidden_states_2d = hidden_states.reshape(-1, hidden_states.shape[-1])
activation_values, activation_scales = quantize_mxfp8_blockwise(hidden_states_2d)
output_2d = self.apply_quantized(layer, activation_values, activation_scales, bias)
return output_2d.reshape(*original_shape[:-1], layer.output_size)
def apply_quantized(
self,
layer: torch.nn.Module,
activation_values: torch.Tensor,
activation_scales: torch.Tensor,
bias: torch.Tensor | None = None,
) -> torch.Tensor:
"""Apply one linear to an activation that is already in MXFP8."""
from fastvideo.layers.mxfp8linear import mxfp8_scaled_mm
if layer._mxfp8_weight is None or layer._mxfp8_weight_scale is None:
raise RuntimeError(f"MXFP8 weight buffers are not initialized for {layer.prefix}.")
return mxfp8_scaled_mm(
activation_values,
activation_scales,
layer._mxfp8_weight,
layer._mxfp8_weight_scale,
bias,
)
class MXFP8Config(QuantizationConfig):
"""Select MXFP8 for the main MiniMax-H3 transformer-block FFN linears."""
def get_name(self) -> str:
return "MXFP8"
def get_supported_act_dtypes(self) -> list[torch.dtype]:
return [torch.bfloat16]
@classmethod
def get_min_capability(cls) -> int:
return 100
@staticmethod
def get_config_filenames() -> list[str]:
return []
@classmethod
def from_config(cls, config: dict[str, Any]) -> MXFP8Config:
del config
return cls()
def get_quant_method(self, layer: torch.nn.Module, prefix: str) -> QuantizeMethodBase | None:
from fastvideo.layers.linear import LinearBase
if isinstance(layer, LinearBase) and _MINIMAX_H3_FF_PREFIX.search(prefix):
return MXFP8QuantizeMethod()
return None
def convert_model_to_mxfp8(model: torch.nn.Module) -> int:
"""Prequantize every MXFP8-tagged weight and return the converted count."""
converted_count = 0
with torch.no_grad():
for module in model.modules():
quant_method = getattr(module, "quant_method", None)
if not isinstance(quant_method, MXFP8QuantizeMethod):
continue
if converted_count == 0:
if not torch.cuda.is_available() or torch.cuda.get_device_capability()[0] < 10:
raise RuntimeError(
"MXFP8 inference requires an NVIDIA Blackwell GPU with compute capability 10.0+.")
quant_method.process_weights_after_loading(module)
converted_count += 1
if converted_count:
logger.info("Prequantized %d MiniMax-H3 feed-forward linear weights to MXFP8", converted_count)
torch.cuda.empty_cache()
return converted_count
__all__ = [
"MXFP8Config",
"MXFP8QuantizeMethod",
"convert_model_to_mxfp8",
]
+19 -12
View File
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
"""LTX-2 NVFP4 quantization (FlashInfer-backed).
"""NVFP4 linear quantization for supported transformer layer sets.
NVFP4 is NVIDIA's block-scaled FP4 format (e2m1 mantissa, fp32 alpha,
``layout_128x4`` scale layout, group size 16) — distinct from
@@ -8,9 +8,8 @@ explicitly so downstream callers don't conflate it with other FP4
variants that may land later (e.g. AMD's MX-FP4 or vendor-neutral
e3m0).
Upstreamed from ``FastVideo-internal`` so consumers that load LTX-2
weights with NVFP4 quantization can drive the public package
end-to-end.
The registered config targets the curated LTX-2 deployment set and the
main MiniMax-H3 transformer-block FFN linears.
`flashinfer` is imported lazily inside the call paths that need it.
This keeps ``import fastvideo`` cheap on hosts where flashinfer is
@@ -20,6 +19,7 @@ use time, with a clear error.
from __future__ import annotations
import logging
import re
from typing import Any
import torch
@@ -75,6 +75,7 @@ _LTX2_NVFP4_BLOCK_LINEAR_SUFFIXES = (
_LTX2_NVFP4_LINEAR_PREFIXES = frozenset(f"ltx2.blocks.{block_idx}.{suffix}" for block_idx in range(48)
for suffix in _LTX2_NVFP4_BLOCK_LINEAR_SUFFIXES) | frozenset(
("ltx2.adaln_single.linear", ))
_MINIMAX_H3_NVFP4_FF_PREFIX = re.compile(r"(?:^|\.)transformer_blocks\.\d+\.ff\.(?:fc_in|fc_out)$")
def is_ltx2_nvfp4_linear_prefix(prefix: str) -> bool:
@@ -82,6 +83,11 @@ def is_ltx2_nvfp4_linear_prefix(prefix: str) -> bool:
return prefix in _LTX2_NVFP4_LINEAR_PREFIXES
def is_minimax_h3_nvfp4_linear_prefix(prefix: str) -> bool:
"""Return whether *prefix* is a main MiniMax-H3 transformer-block FFN linear."""
return _MINIMAX_H3_NVFP4_FF_PREFIX.search(prefix) is not None
def _is_ltx2_refine_only_prefix(prefix: str) -> bool:
return any(prefix.endswith(suffix) for suffix in _LTX2_REFINE_ONLY_SUFFIXES)
@@ -421,13 +427,12 @@ class NVFP4QuantizeMethod(QuantizeMethodBase):
class NVFP4Config(QuantizationConfig):
"""LTX-2-specific NVFP4 quantization configuration.
"""Select NVFP4 for the supported LTX-2 and MiniMax-H3 linear sets.
NVFP4 is NVIDIA's block-scaled FP4 (e2m1 mantissa, fp32 alpha,
``layout_128x4`` scale layout, group size 16). Today this class
hardcodes the LTX-2 layer paths it covers. When a second model
wants NVFP4, lift the layer-path list into a config field
instead of hardcoding it here.
``layout_128x4`` scale layout, group size 16). LTX-2 uses its curated
attention and FFN deployment set. MiniMax-H3 uses only ``fc_in`` and
``fc_out`` in each main transformer-block FFN.
"""
def __init__(self, layer_profile: str = "refine", retain_original_weights: bool | None = None):
@@ -468,9 +473,10 @@ class NVFP4Config(QuantizationConfig):
def get_quant_method(self, layer: torch.nn.Module, prefix: str):
from fastvideo.layers.linear import LinearBase
# Use the superset at build/load time, then switch active subset
# dynamically in NVFP4QuantizeMethod.apply based on stage profile.
if isinstance(layer, LinearBase) and is_ltx2_nvfp4_linear_prefix(prefix):
# LTX-2 switches its active subset by stage at runtime. MiniMax-H3
# uses the fixed main-transformer FFN set selected by its prefix.
if isinstance(layer, LinearBase) and (is_ltx2_nvfp4_linear_prefix(prefix)
or is_minimax_h3_nvfp4_linear_prefix(prefix)):
method = NVFP4QuantizeMethod(layer_prefix=prefix)
method._retain_original_weights = self.retain_original_weights
return method
@@ -552,4 +558,5 @@ __all__ = [
"NVFP4QuantizeMethod",
"convert_model_to_nvfp4",
"is_ltx2_nvfp4_linear_prefix",
"is_minimax_h3_nvfp4_linear_prefix",
]
+43 -29
View File
@@ -21,6 +21,7 @@ from fastvideo.distributed.communication_op import (
)
from fastvideo.distributed.parallel_state import get_sp_world_size, model_parallel_is_initialized
from fastvideo.layers.linear import ReplicatedLinear
from fastvideo.layers.quantization.mxfp8_config import MXFP8QuantizeMethod
from fastvideo.layers.mlp import MLP
from fastvideo.layers.quantization import QuantizationConfig
from fastvideo.layers.visual_embedding import Timesteps
@@ -119,8 +120,14 @@ class MiniMaxH3FeedForward(nn.Module):
prefix=f"{prefix}.fc_out",
)
self.fuse_swiglu = fuse_swiglu
self.use_mxfp8 = isinstance(self.fc_in.quant_method, MXFP8QuantizeMethod) and isinstance(
self.fc_out.quant_method, MXFP8QuantizeMethod)
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
if self.use_mxfp8:
from fastvideo.layers.mxfp8linear import mxfp8_swiglu_feed_forward
return mxfp8_swiglu_feed_forward(hidden_states, self.fc_in, self.fc_out)
hidden_states, _ = self.fc_in(hidden_states)
if self.fuse_swiglu and _can_run_minimax_h3_fusion(hidden_states):
hidden_states = minimax_h3_swiglu(hidden_states)
@@ -514,41 +521,48 @@ class MiniMaxH3TransformerBlock(nn.Module):
rotary_emb: tuple[torch.Tensor, torch.Tensor],
original_seq_len: int,
) -> torch.Tensor:
shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = (
t.to(hidden_states.dtype) for t in self.adaln_proj(temb))
with nvtx_range("minimax_h3.transformer_block.adaln_projection"):
shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = (
t.to(hidden_states.dtype) for t in self.adaln_proj(temb))
use_modulate_fusion = self.fuse_modulate and _can_run_minimax_h3_fusion(hidden_states)
if use_modulate_fusion:
norm_hidden_states = fused_rmsnorm_modulate(
hidden_states,
self.norm1.weight,
scale_msa,
shift_msa,
adaln_indices,
self.norm1.eps,
)
with nvtx_range("minimax_h3.transformer_block.modulate_fusion"):
norm_hidden_states = fused_rmsnorm_modulate(
hidden_states,
self.norm1.weight,
scale_msa,
shift_msa,
adaln_indices,
self.norm1.eps,
)
else:
norm_hidden_states = self.norm1(hidden_states)
norm_hidden_states = norm_hidden_states * (
1.0 + scale_msa.index_select(0, adaln_indices)) + shift_msa.index_select(0, adaln_indices)
attention_output = self.attn(norm_hidden_states, rotary_emb, original_seq_len)
with nvtx_range("minimax_h3.transformer_block.no_modulate_fusion"):
norm_hidden_states = self.norm1(hidden_states)
norm_hidden_states = norm_hidden_states * (
1.0 + scale_msa.index_select(0, adaln_indices)) + shift_msa.index_select(0, adaln_indices)
with nvtx_range("minimax_h3.transformer_block.self_attention"):
attention_output = self.attn(norm_hidden_states, rotary_emb, original_seq_len)
if use_modulate_fusion:
hidden_states, norm_hidden_states = fused_residual_gate_rmsnorm_modulate(
hidden_states,
attention_output,
gate_msa,
self.norm2.weight,
scale_mlp,
shift_mlp,
adaln_indices,
self.norm2.eps,
)
with nvtx_range("minimax_h3.transformer_block.modulate_fusion"):
hidden_states, norm_hidden_states = fused_residual_gate_rmsnorm_modulate(
hidden_states,
attention_output,
gate_msa,
self.norm2.weight,
scale_mlp,
shift_mlp,
adaln_indices,
self.norm2.eps,
)
else:
hidden_states = hidden_states + gate_msa.index_select(0, adaln_indices) * attention_output
norm_hidden_states = self.norm2(hidden_states)
norm_hidden_states = norm_hidden_states * (
1.0 + scale_mlp.index_select(0, adaln_indices)) + shift_mlp.index_select(0, adaln_indices)
feed_forward_output = self.ff(norm_hidden_states)
with nvtx_range("minimax_h3.transformer_block.no_modulate_fusion"):
hidden_states = hidden_states + gate_msa.index_select(0, adaln_indices) * attention_output
norm_hidden_states = self.norm2(hidden_states)
norm_hidden_states = norm_hidden_states * (
1.0 + scale_mlp.index_select(0, adaln_indices)) + shift_mlp.index_select(0, adaln_indices)
with nvtx_range("minimax_h3.transformer_block.feed_forward"):
feed_forward_output = self.ff(norm_hidden_states)
return hidden_states + gate_mlp.index_select(0, adaln_indices) * feed_forward_output
+21 -7
View File
@@ -36,14 +36,14 @@ def _summarize_param_names(names: set[str]) -> str:
return ", ".join(f"{family} x{count}" if count > 1 else family for family, count in sorted(families.items()))
def _maybe_quantize_model(model: nn.Module) -> None:
"""Quantize NVFP4- or FP8-tagged linear layers in-place after weights are loaded.
def _maybe_quantize_model(model: nn.Module, *, defer_weight_conversion_until_lora_merge: bool = False) -> None:
"""Quantize inference linear weights after checkpoint loading.
Walks the module tree once, looking for layers whose ``quant_method``
is an :class:`NVFP4QuantizeMethod` or :class:`FP8QuantizeMethod` (attached
at construction time by the respective ``get_quant_method``). When at least
Walks the module tree once, looking for layers whose ``quant_method`` is an
inference quantization method attached at construction time. When at least
one such layer exists, calls the matching conversion function to register
quantized weight buffers on each targeted layer.
quantized weight buffers on each targeted layer. NVFP4 and MXFP8 conversion
can be deferred until a configured inference LoRA is merged.
The walk returns on the first quantized layer found so unquantized callers
pay only an ``isinstance`` check per module. Both imports are deferred so
@@ -70,12 +70,19 @@ def _maybe_quantize_model(model: nn.Module) -> None:
FP8QuantizeMethod,
convert_model_to_fp8,
)
from fastvideo.layers.quantization.mxfp8_config import (
MXFP8QuantizeMethod,
convert_model_to_mxfp8,
)
qat_train_attached = 0
qat_train_skipped = 0
for mod in model.modules():
qm = getattr(mod, "quant_method", None)
if isinstance(qm, NVFP4QuantizeMethod):
if defer_weight_conversion_until_lora_merge:
logger.info("Deferring NVFP4 weight conversion until the inference LoRA merge completes")
return
logger.info("Converting loaded model weights for NVFP4 linear layers")
convert_model_to_nvfp4(model)
return
@@ -87,6 +94,13 @@ def _maybe_quantize_model(model: nn.Module) -> None:
logger.info("Converting loaded model weights for FP8 linear layers")
convert_model_to_fp8(model)
return
if isinstance(qm, MXFP8QuantizeMethod):
if defer_weight_conversion_until_lora_merge:
logger.info("Deferring MXFP8 weight conversion until the inference LoRA merge completes")
return
logger.info("Converting loaded model weights for MXFP8 linear layers")
convert_model_to_mxfp8(model)
return
# QAT-train configs are mutually exclusive with the inference schemes
# above (one quant_config per model), so when they're active the loop
# always runs to completion and the counts below are model-wide.
@@ -287,7 +301,7 @@ def maybe_load_fsdp_model(
# responsibility is just to materialize the quantized weight buffers
# from the freshly-loaded bf16 weights. No-op when no quantized layers
# are present (lazy imports inside the helper).
_maybe_quantize_model(model)
_maybe_quantize_model(model, defer_weight_conversion_until_lora_merge=lora_path is not None)
compile_in_loader = enable_torch_compile and training_mode
if compile_in_loader:
+19
View File
@@ -43,6 +43,24 @@ def _get_hook_ctx(module: nn.Module | None):
return nullcontext()
def _convert_quantized_weights_after_lora_merge(transformer_modules: dict[str, nn.Module]) -> None:
"""Pack deferred NVFP4 or MXFP8 weights after all LoRA layers are merged."""
from fastvideo.layers.quantization.mxfp8_config import MXFP8QuantizeMethod
from fastvideo.layers.quantization.mxfp8_config import convert_model_to_mxfp8
from fastvideo.layers.quantization.nvfp4_config import NVFP4QuantizeMethod
from fastvideo.layers.quantization.nvfp4_config import convert_model_to_nvfp4
for transformer_module in transformer_modules.values():
for module in transformer_module.modules():
quant_method = getattr(module, "quant_method", None)
if isinstance(quant_method, NVFP4QuantizeMethod):
convert_model_to_nvfp4(transformer_module)
break
if isinstance(quant_method, MXFP8QuantizeMethod):
convert_model_to_mxfp8(transformer_module)
break
def _named_module_by_prefix(module: nn.Module,
prefixes: list[str]) -> list[tuple[str | None, list[tuple[str, nn.Module]]]]:
none_list: list[tuple[str, nn.Module]] = []
@@ -532,6 +550,7 @@ class LoRAPipeline(ComposedPipelineBase):
lora_path)
if unmatched:
logger.warning("LoRA adapter %s: %d weights did not reach a layer", lora_path, len(unmatched))
_convert_quantized_weights_after_lora_merge(self.trainable_transformer_modules)
def merge_lora_weights(self) -> None:
for (
@@ -0,0 +1,140 @@
# SPDX-License-Identifier: Apache-2.0
"""MiniMax-H3 NVFP4 targeting and inference-LoRA lifecycle tests."""
from __future__ import annotations
import pytest
import torch
import torch.nn as nn
import fastvideo.layers.quantization.mxfp8_config as mxfp8
import fastvideo.layers.quantization.nvfp4_config as nvfp4
class _TaggedModel(nn.Module):
"""Minimal model whose child carries one quantization method."""
def __init__(self, quant_method: object) -> None:
super().__init__()
self.linear = nn.Module()
self.linear.quant_method = quant_method
@pytest.mark.parametrize(
"prefix",
[
"transformer_blocks.0.ff.fc_in",
"transformer_blocks.49.ff.fc_out",
"minimax_h3.transformer_blocks.12.ff.fc_in",
],
)
def test_is_minimax_h3_nvfp4_linear_prefix_main_ffn_linear(prefix: str) -> None:
assert nvfp4.is_minimax_h3_nvfp4_linear_prefix(prefix)
@pytest.mark.parametrize(
"prefix",
[
"transformer_blocks.0.attn.to_q",
"transformer_blocks.0.adaln_proj.linear",
"token_refiner.refiner_blocks.0.ff.fc_in",
"transformer_blocks.0.ff.extra",
],
)
def test_is_minimax_h3_nvfp4_linear_prefix_non_main_ffn_linear(prefix: str) -> None:
assert not nvfp4.is_minimax_h3_nvfp4_linear_prefix(prefix)
def test_nvfp4config_get_quant_method_minimax_h3_feed_forward() -> None:
if not torch.cuda.is_available():
pytest.skip("NVFP4QuantizeMethod construction requires CUDA")
from fastvideo.models.dits.minimax_h3 import MiniMaxH3FeedForward
feed_forward = MiniMaxH3FeedForward(
hidden_size=16,
ffn_dim=32,
quant_config=nvfp4.NVFP4Config(),
prefix="minimax_h3.transformer_blocks.0.ff",
)
assert isinstance(feed_forward.fc_in.quant_method, nvfp4.NVFP4QuantizeMethod)
assert isinstance(feed_forward.fc_out.quant_method, nvfp4.NVFP4QuantizeMethod)
def test_minimax_h3_feed_forward_nvfp4_cuda_similarity() -> None:
"""Run both H3 FFN GEMMs through NVFP4 and compare with BF16."""
if not torch.cuda.is_available() or torch.cuda.get_device_capability()[0] < 10:
pytest.skip("MiniMax-H3 NVFP4 inference requires an NVIDIA Blackwell GPU")
pytest.importorskip("flashinfer")
from fastvideo.models.dits.minimax_h3 import MiniMaxH3FeedForward
previous_default_dtype = torch.get_default_dtype()
torch.set_default_dtype(torch.bfloat16)
try:
dense = MiniMaxH3FeedForward(128, 256, prefix="minimax_h3.transformer_blocks.0.ff")
quantized = MiniMaxH3FeedForward(
128,
256,
quant_config=nvfp4.NVFP4Config(),
prefix="minimax_h3.transformer_blocks.0.ff",
)
finally:
torch.set_default_dtype(previous_default_dtype)
generator = torch.Generator().manual_seed(20260901)
with torch.no_grad():
for parameter in dense.parameters():
parameter.copy_(torch.randn(parameter.shape, generator=generator, dtype=parameter.dtype) * 0.02)
quantized.load_state_dict(dense.state_dict(), strict=True)
dense = dense.cuda().eval()
quantized = quantized.cuda().eval()
nvfp4.convert_model_to_nvfp4(quantized)
inputs = torch.randn(2, 128, 128, device="cuda", dtype=torch.bfloat16)
with torch.inference_mode():
dense_output = dense(inputs)
quantized_output = quantized(inputs)
similarity = torch.nn.functional.cosine_similarity(
dense_output.float().flatten(),
quantized_output.float().flatten(),
dim=0,
)
assert dense_output.dtype == torch.bfloat16
assert quantized_output.dtype == torch.bfloat16
assert similarity > 0.97
assert quantized.fc_in.weight is None
assert quantized.fc_out.weight is None
def test_maybe_quantize_model_lora_defers_nvfp4(monkeypatch: pytest.MonkeyPatch) -> None:
from fastvideo.models.loader.fsdp_load import _maybe_quantize_model
model = _TaggedModel(object.__new__(nvfp4.NVFP4QuantizeMethod))
converted: list[nn.Module] = []
monkeypatch.setattr(nvfp4, "convert_model_to_nvfp4", converted.append)
_maybe_quantize_model(model, defer_weight_conversion_until_lora_merge=True)
assert converted == []
_maybe_quantize_model(model)
assert converted == [model]
def test_convert_quantized_weights_after_lora_merge_dispatches_nvfp4(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from fastvideo.pipelines.lora_pipeline import _convert_quantized_weights_after_lora_merge
model = _TaggedModel(object.__new__(nvfp4.NVFP4QuantizeMethod))
nvfp4_converted: list[nn.Module] = []
mxfp8_converted: list[nn.Module] = []
monkeypatch.setattr(nvfp4, "convert_model_to_nvfp4", nvfp4_converted.append)
monkeypatch.setattr(mxfp8, "convert_model_to_mxfp8", mxfp8_converted.append)
_convert_quantized_weights_after_lora_merge({"transformer": model})
assert nvfp4_converted == [model]
assert mxfp8_converted == []
@@ -17,6 +17,7 @@ from fastvideo.tests.ssim.inference_similarity_utils import (
logger = init_logger(__name__)
REQUIRED_GPUS = 1
pytestmark = pytest.mark.skip(reason="Disabled pending removal of DreamX World support.")
device_reference_folder = resolve_inference_device_reference_folder(logger)
@@ -36,6 +36,7 @@ from fastvideo.worker.multiproc_executor import MultiprocExecutor
logger = init_logger(__name__)
REQUIRED_GPUS = 1
pytestmark = pytest.mark.skip(reason="Disabled pending removal of HunyuanGameCraft support.")
# ---------------------------------------------------------------------------
# Device-dependent reference folder
@@ -19,6 +19,7 @@ from fastvideo.tests.ssim.reference_utils import (
logger = init_logger(__name__)
REQUIRED_GPUS = 1
pytestmark = pytest.mark.skip(reason="Disabled pending removal of GLM-Image support.")
REPO_ROOT = Path(__file__).resolve().parents[3]
LOCAL_WEIGHTS_DIR = Path(os.getenv("GLM_IMAGE_LOCAL_WEIGHTS_DIR", REPO_ROOT / "official_weights" / "glm_image"))
@@ -34,6 +34,7 @@ from fastvideo.tests.utils import compute_video_ssim_torchvision, write_ssim_res
logger = init_logger(__name__)
REQUIRED_GPUS = 1
pytestmark = pytest.mark.skip(reason="Disabled pending removal of LongCat support.")
# Device-specific reference folder
device_reference_folder = resolve_device_reference_folder(
@@ -24,6 +24,7 @@ from fastvideo.worker.multiproc_executor import MultiprocExecutor
logger = init_logger(__name__)
REQUIRED_GPUS = 1
pytestmark = pytest.mark.skip(reason="Disabled pending removal of Matrix Game 2.0 support.")
device_reference_folder = resolve_device_reference_folder(
(
@@ -19,6 +19,7 @@ from fastvideo.tests.ssim.reference_utils import (
logger = init_logger(__name__)
REQUIRED_GPUS = 4
pytestmark = pytest.mark.skip(reason="Disabled pending removal of TurboDiffusion and TurboWan support.")
device_name = get_cuda_device_name()
device_reference_folder = resolve_device_reference_folder(
@@ -19,6 +19,7 @@ from fastvideo.tests.ssim.reference_utils import (
logger = init_logger(__name__)
REQUIRED_GPUS = 1
pytestmark = pytest.mark.skip(reason="Disabled pending removal of Z-Image support.")
ZIMAGE_MIN_SSIM = 0.98
ZIMAGE_MODEL_PATH = os.getenv(