Skip to content

fastwan

FastWan-oriented helpers for the experimental MLX runtime path.

Classes

fastvideo.mlx_runtime.fastwan.MLXQuantizationSpec dataclass

MLXQuantizationSpec(mode: str, bits: int | None = None, group_size: int | None = None)

MLX quantized-matmul configuration for DiT linear weights.

fastvideo.mlx_runtime.fastwan.MLXWanDiT

MLXWanDiT(weights: dict[str, array], blocks: list[MLXWanTransformerBlock], config: dict, *, compile: bool = False)

Experimental FP16 Wan/FastWan DiT forward path in MLX.

Source code in fastvideo/mlx_runtime/fastwan.py
def __init__(
    self,
    weights: dict[str, mx.array],
    blocks: list[MLXWanTransformerBlock],
    config: dict,
    *,
    compile: bool = False,
) -> None:
    import os

    self.weights = weights
    self.blocks = blocks
    self.config = config
    self.num_heads = int(config["num_attention_heads"])
    self.head_dim = int(config["attention_head_dim"])
    self.hidden_size = self.num_heads * self.head_dim
    self.ffn_dim = int(config["ffn_dim"])
    self.in_channels = int(config["in_channels"])
    self.out_channels = int(config["out_channels"])
    self.patch_size = tuple(config["patch_size"])
    self.freq_dim = int(config["freq_dim"])
    # Opt-in graph fusion. With fixed weights and static shapes, the whole
    # denoise-step forward is a pure function of (latents, timestep) -- a
    # good mx.compile target. Off by default so the eager path stays the
    # baseline; enable via constructor or FASTVIDEO_MLX_COMPILE=1 and verify
    # with the benchmark's SSIM ~= 1.0 check.
    self._enable_compile = compile or os.environ.get("FASTVIDEO_MLX_COMPILE", "0") == "1"
    self._compiled_forward: Callable[..., Any] | None = None
    self._compiled_signature: tuple | None = None

fastvideo.mlx_runtime.fastwan.MLXWanTransformerBlock

MLXWanTransformerBlock(weights: dict[str, array], *, dim: int, ffn_dim: int, num_heads: int, eps: float = 1e-06)

Dense T2V Wan transformer block for the experimental MLX runtime.

This mirrors the non-VSA PyTorch block for single-process dense attention. Rotary embeddings and sequence-parallel paths are intentionally left out of this first parity target.

Source code in fastvideo/mlx_runtime/fastwan.py
def __init__(self, weights: dict[str, mx.array], *, dim: int, ffn_dim: int, num_heads: int, eps: float = 1e-6):
    self.weights = weights
    self.dim = dim
    self.ffn_dim = ffn_dim
    self.num_heads = num_heads
    self.head_dim = dim // num_heads
    self.eps = eps
    self.attn2 = MLXWanT2VCrossAttention(weights, dim=dim, num_heads=num_heads, eps=eps)

fastvideo.mlx_runtime.fastwan.UnsupportedMLXQuantizationError

Bases: ValueError

A quantization mode the installed MLX build cannot execute.

Raised by :func:ensure_quantization_supported before any model weights are loaded, so callers (CLI flags, benchmark sweeps) can fail fast with an actionable message -- or skip the mode -- instead of crashing deep inside mx.quantize mid-load.

Functions:

fastvideo.mlx_runtime.fastwan.apply_rotary_emb

apply_rotary_emb(x, cos, sin, *, is_neox_style: bool = False)

Apply FastVideo's rotary convention to MLX tensors.

Parameters:

Name Type Description Default
x

[batch, seq, heads, head_dim]

required
cos/sin

[seq, head_dim] for Wan's full-dimension rotate-pair style, or [seq, head_dim // 2] for traditional RoPE.

required
Source code in fastvideo/mlx_runtime/fastwan.py
def apply_rotary_emb(x, cos, sin, *, is_neox_style: bool = False):
    """Apply FastVideo's rotary convention to MLX tensors.

    Args:
        x: [batch, seq, heads, head_dim]
        cos/sin: [seq, head_dim] for Wan's full-dimension rotate-pair style,
          or [seq, head_dim // 2] for traditional RoPE.
    """
    import mlx.core as mx

    head_size = x.shape[-1]
    rope_dim = cos.shape[-1]
    cos = cos[None, :, None, :]
    sin = sin[None, :, None, :]
    x_float = x.astype(mx.float32)

    if rope_dim == head_size:
        x_pairs = x_float.reshape(*x.shape[:-1], -1, 2)
        x_real = x_pairs[..., 0]
        x_imag = x_pairs[..., 1]
        x_rotated = mx.stack([-x_imag, x_real], axis=-1).reshape(*x.shape)
        return (x_float * cos + x_rotated * sin).astype(x.dtype)

    if is_neox_style:
        x1, x2 = mx.split(x_float, 2, axis=-1)
        o1 = x1 * cos - x2 * sin
        o2 = x2 * cos + x1 * sin
        return mx.concatenate([o1, o2], axis=-1).astype(x.dtype)

    x1 = x_float[..., ::2]
    x2 = x_float[..., 1::2]
    o1 = x1 * cos - x2 * sin
    o2 = x2 * cos + x1 * sin
    return mx.stack([o1, o2], axis=-1).reshape(*x.shape).astype(x.dtype)

fastvideo.mlx_runtime.fastwan.ensure_quantization_supported

ensure_quantization_supported(spec: MLXQuantizationSpec | None) -> None

Raise :class:UnsupportedMLXQuantizationError if spec cannot run here.

Source code in fastvideo/mlx_runtime/fastwan.py
def ensure_quantization_supported(spec: MLXQuantizationSpec | None) -> None:
    """Raise :class:`UnsupportedMLXQuantizationError` if ``spec`` cannot run here."""
    if spec is None:
        return
    error = quantization_support_error(spec)
    if error is None:
        return
    import mlx.core as mx

    mlx_version = getattr(mx, "__version__", "unknown")
    raise UnsupportedMLXQuantizationError(f"MLX quantization mode '{spec.label}' is not supported by the installed mlx "
                                          f"({mlx_version}): {error}. Upgrade mlx or pick a supported mode "
                                          f"(int8 is currently the most reliable quality/memory target).")

fastvideo.mlx_runtime.fastwan.fastwan_shape

fastwan_shape(*, height: int, width: int, num_frames: int, vae_temporal_compression: int = 4, vae_spatial_compression: int = 8, patch_size: tuple[int, int, int] = (1, 2, 2), num_heads: int = 12, head_dim: int = 128) -> FastWanShape

Return the approximate DiT token shape for Wan/FastWan T2V inference.

Source code in fastvideo/mlx_runtime/fastwan.py
def fastwan_shape(
        *,
        height: int,
        width: int,
        num_frames: int,
        vae_temporal_compression: int = 4,
        vae_spatial_compression: int = 8,
        patch_size: tuple[int, int, int] = (1, 2, 2),
        num_heads: int = 12,
        head_dim: int = 128,
) -> FastWanShape:
    """Return the approximate DiT token shape for Wan/FastWan T2V inference."""
    latent_frames = (num_frames - 1) // vae_temporal_compression + 1
    latent_height = height // vae_spatial_compression
    latent_width = width // vae_spatial_compression
    patch_frames = latent_frames // patch_size[0]
    patch_height = latent_height // patch_size[1]
    patch_width = latent_width // patch_size[2]
    tokens = patch_frames * patch_height * patch_width
    return FastWanShape(
        height=height,
        width=width,
        num_frames=num_frames,
        latent_frames=latent_frames,
        latent_height=latent_height,
        latent_width=latent_width,
        patch_frames=patch_frames,
        patch_height=patch_height,
        patch_width=patch_width,
        tokens=tokens,
        hidden_size=num_heads * head_dim,
        num_heads=num_heads,
        head_dim=head_dim,
    )

fastvideo.mlx_runtime.fastwan.gelu_tanh

gelu_tanh(x)

tanh-approximate GELU, as used by Wan's FFN.

mlx.nn.gelu_approx is the same tanh approximation behind a fused kernel. On the 1.3B FFN shape (32760x8960) it is bit-identical to the expanded expression below and 3.3x faster — 28.9ms -> 8.7ms per layer, which is 0.6s per denoise step across 30 layers.

Source code in fastvideo/mlx_runtime/fastwan.py
def gelu_tanh(x):
    """tanh-approximate GELU, as used by Wan's FFN.

    ``mlx.nn.gelu_approx`` is the same tanh approximation behind a fused
    kernel. On the 1.3B FFN shape (32760x8960) it is bit-identical to the
    expanded expression below and 3.3x faster — 28.9ms -> 8.7ms per layer,
    which is 0.6s per denoise step across 30 layers.
    """
    import mlx.nn as nn

    return nn.gelu_approx(x)

fastvideo.mlx_runtime.fastwan.mlx_block_weights_from_diffusers_safetensors

mlx_block_weights_from_diffusers_safetensors(checkpoint_path: str | Path, *, block_index: int = 0, quantization: str | MLXQuantizationSpec | None = None, dtype=None) -> dict[str, array]

Load one Diffusers-format Wan block into the MLX dense-block key layout.

Source code in fastvideo/mlx_runtime/fastwan.py
def mlx_block_weights_from_diffusers_safetensors(
    checkpoint_path: str | Path,
    *,
    block_index: int = 0,
    quantization: str | MLXQuantizationSpec | None = None,
    dtype=None,
) -> dict[str, mx.array]:
    """Load one Diffusers-format Wan block into the MLX dense-block key layout."""
    from safetensors import safe_open

    prefix = f"blocks.{block_index}."
    key_map = _WAN_BLOCK_KEY_MAP

    spec = MLXQuantizationSpec.from_name(quantization) if (quantization is None
                                                           or isinstance(quantization, str)) else quantization
    ensure_quantization_supported(spec)
    matrix_targets = {target for target in key_map.values() if target.endswith(".weight") and "norm" not in target}
    weights = {}
    with safe_open(str(checkpoint_path), framework="pt", device="cpu") as handle:
        available = set(handle.keys())
        for source_name, target_name in key_map.items():
            full = prefix + source_name
            if full not in available:
                # Biases are optional: e.g. Wan2.1-14B has bias-free attention/FFN.
                # The block forward already fetches biases via ``.get(...)``.
                if source_name.endswith(".bias"):
                    continue
                raise KeyError(f"missing required block weight: {full}")
            array = _load_mx_array_from_safetensor(handle, full, dtype)
            loaded = quantize_matrix(array, spec) if target_name in matrix_targets else array
            _eval_loaded_weight(loaded)
            weights[target_name] = loaded
            del array
    return weights

fastvideo.mlx_runtime.fastwan.quantization_support_error

quantization_support_error(spec: MLXQuantizationSpec) -> str | None

Probe whether the installed MLX build supports spec.

Runs a tiny mx.quantize + mx.quantized_matmul with exactly the arguments :func:quantize_matrix / :func:linear use, so the result reflects the real runtime path. The affine (int8/int4) modes are stable across MLX releases, but the mxfp8/mxfp4/nvfp4 mode strings require newer MLX builds and raise otherwise. Returns None when the mode works, else the underlying error message. Cached per spec.

Source code in fastvideo/mlx_runtime/fastwan.py
def quantization_support_error(spec: MLXQuantizationSpec) -> str | None:
    """Probe whether the installed MLX build supports ``spec``.

    Runs a tiny ``mx.quantize`` + ``mx.quantized_matmul`` with exactly the
    arguments :func:`quantize_matrix` / :func:`linear` use, so the result
    reflects the real runtime path. The affine (int8/int4) modes are stable
    across MLX releases, but the ``mxfp8``/``mxfp4``/``nvfp4`` mode strings
    require newer MLX builds and raise otherwise. Returns ``None`` when the
    mode works, else the underlying error message. Cached per spec.
    """
    key = (spec.mode, spec.bits, spec.group_size)
    if key not in _QUANT_SUPPORT_CACHE:
        import mlx.core as mx

        try:
            probe_dim = max(spec.group_size or 0, 64)
            weight = mx.zeros((probe_dim, probe_dim), dtype=mx.float16)
            quantized = quantize_matrix(weight, spec)
            y = linear(mx.zeros((1, probe_dim), dtype=mx.float16), quantized)
            mx.eval(y)
            _QUANT_SUPPORT_CACHE[key] = None
        except Exception as exc:  # noqa: BLE001 - MLX raises varied error types per backend/version.
            _QUANT_SUPPORT_CACHE[key] = f"{type(exc).__name__}: {exc}"
    return _QUANT_SUPPORT_CACHE[key]

fastvideo.mlx_runtime.fastwan.torch_block_state_from_diffusers_safetensors

torch_block_state_from_diffusers_safetensors(checkpoint_path: str | Path, *, block_index: int = 0) -> dict[str, Tensor]

Load one Diffusers-format Wan block into FastVideo's dense block keys.

Source code in fastvideo/mlx_runtime/fastwan.py
def torch_block_state_from_diffusers_safetensors(
    checkpoint_path: str | Path,
    *,
    block_index: int = 0,
) -> dict[str, torch.Tensor]:
    """Load one Diffusers-format Wan block into FastVideo's dense block keys."""
    from safetensors import safe_open

    prefix = f"blocks.{block_index}."
    key_map = _WAN_BLOCK_KEY_MAP

    state = {}
    with safe_open(str(checkpoint_path), framework="pt", device="cpu") as handle:
        available = set(handle.keys())
        for source_name, target_name in key_map.items():
            full = prefix + source_name
            if full not in available:
                # Biases are optional: e.g. Wan2.1-14B has bias-free attention/FFN.
                if source_name.endswith(".bias"):
                    continue
                raise KeyError(f"missing required block weight: {full}")
            state[target_name] = handle.get_tensor(full).float()
    return state