Skip to content

minimax_h3_video_vae

MiniMax-H3 video VAE for the Apple Silicon MLX runtime.

Faithful MLX port of fastvideo/models/vaes/minimax_h3_video.py (itself parity-validated against the official diffusers implementation):

  • Encoder: causal 3D CNN — causal temporal padding, reflect spatial padding, per-frame GroupNorm, residual blocks, spatial/temporal downsampling, then a 1x1x1 quant_conv producing mean/logvar channels.
  • Decoder: 1x1x1 post_quant_conv, then a 36-layer ViT — per-head Q/K RMSNorm, three-axis rotary embedding (theta 100, rotary width 48 of 64 head dims), register tokens plus a final zero class token, SwiGLU feed-forward, residual scale vectors, FP32 norm accumulation, final LayerNorm, output projection, and channel-major unpatchification (temporal patch 4, spatial patch 16x16) — with the exact clip chunking (clip_length=17, token_drop=3), frame pre-padding, overlap blending, tail trimming, and optional spatial tiling of the released model.

Released weights are FP32; the loader streams shards so peak memory stays bounded, and can optionally store bf16/fp16 after callers have measured the dtype drift against the FP32 acceptance gate.

Production code here never imports PyTorch. Torch parity references live in the tests under tests/local_tests/minimax_h3/.

Classes

fastvideo.mlx_runtime.minimax_h3_video_vae.MLXMiniMaxH3VideoVAE

MLXMiniMaxH3VideoVAE(weights: dict[str, Any], config: MiniMaxH3VideoVAEConfigView, *, has_encoder: bool = True)

Encoder + decoder for the released MiniMax-H3 video VAE.

Source code in fastvideo/mlx_runtime/minimax_h3_video_vae.py
def __init__(self, weights: dict[str, Any], config: MiniMaxH3VideoVAEConfigView, *, has_encoder: bool = True):
    self.weights = weights
    self.config = config
    self.has_encoder = has_encoder
    self.latent_channels = config.latent_channels
    self.spatial_compression_ratio = config.spatial_compression_ratio
    self.temporal_compression_ratio = config.temporal_compression_ratio
    self.num_heads = config.decoder_num_attention_heads
    self.head_dim = config.decoder_attention_head_dim
    self.dim = self.num_heads * self.head_dim
    self.rotary_dim = int(self.head_dim * config.decoder_rope_dim_ratio)
    self._blocks: list[dict[str, Any]] | None = None
    arch = config
    mean = arch.latents_mean if arch.latents_mean is not None else [0.0] * arch.latent_channels
    std = arch.latents_std if arch.latents_std is not None else [1.0] * arch.latent_channels
    self._latents_mean = np.asarray(mean, dtype=np.float32).reshape(1, -1, 1, 1, 1)
    self._latents_std = np.asarray(std, dtype=np.float32).reshape(1, -1, 1, 1, 1)
    self._pixel_mean = np.asarray(PIXEL_MEAN, dtype=np.float32).reshape(1, -1, 1, 1, 1)
    self._pixel_std = np.asarray(PIXEL_STD, dtype=np.float32).reshape(1, -1, 1, 1, 1)

Methods:

fastvideo.mlx_runtime.minimax_h3_video_vae.MLXMiniMaxH3VideoVAE.decode
decode(z, *, tiled: bool = True, tile_sample_min_height: int = 256, tile_sample_min_width: int = 256, tile_sample_min_overlap_height: int = 64, tile_sample_min_overlap_width: int = 64)

Chunked decode of normalized latents (1, C, T_lat, H', W') -> (1, 3, T, H, W).

Source code in fastvideo/mlx_runtime/minimax_h3_video_vae.py
def decode(self,
           z,
           *,
           tiled: bool = True,
           tile_sample_min_height: int = 256,
           tile_sample_min_width: int = 256,
           tile_sample_min_overlap_height: int = 64,
           tile_sample_min_overlap_width: int = 64):
    """Chunked decode of normalized latents (1, C, T_lat, H', W') -> (1, 3, T, H, W)."""
    cfg = self.config
    tokens_chunk_size = cfg.tokens_chunk_size
    token_drop = cfg.token_drop
    temporal_ratio = self.temporal_compression_ratio
    chunk_num_frames = tokens_chunk_size * temporal_ratio
    num_tokens = z.shape[2] + token_drop
    pad_tokens = (-num_tokens) % tokens_chunk_size
    num_chunks = (num_tokens + pad_tokens) // tokens_chunk_size - int(token_drop > 0)
    if pad_tokens > 0:
        tail = mx.repeat(z[:, :, -1:], pad_tokens, axis=2)
        z = mx.concatenate([z, tail], axis=2)

    def decode_one(chunk):
        if tiled:
            return self.decode_clip_tiled(chunk, tile_sample_min_height, tile_sample_min_width,
                                          tile_sample_min_overlap_height, tile_sample_min_overlap_width)
        return self._decode_clip(chunk)

    decoded_chunks = []
    overlap = None
    for index in range(num_chunks):
        start = index * tokens_chunk_size
        clip = decode_one(z[:, :, start:start + tokens_chunk_size + cfg.token_overlap])
        for overlap_index in range(int(token_drop > 0) + 1):
            frame_start = overlap_index * chunk_num_frames
            chunk = clip[:, :, frame_start:frame_start + chunk_num_frames]
            chunk = chunk[:, :, cfg.frame_pre_padding:]
            if overlap_index == 0:
                if overlap is not None:
                    chunk = self._blend(overlap, chunk, cfg.frame_overlap, dim=-3)
                mx.eval(chunk)  # materialize per clip; keeps the lazy graph bounded
                decoded_chunks.append(chunk)
            else:
                overlap = chunk
                mx.eval(overlap)
    if overlap is not None:
        decoded_chunks.append(overlap)
    decoded = mx.concatenate(decoded_chunks, axis=2)

    if pad_tokens > 0:
        intra_tail = cfg.clip_length % temporal_ratio
        num_tokens_before_pad = z.shape[2] - pad_tokens
        pad_frames = sum(intra_tail if intra_tail and (num_tokens_before_pad + offset) %
                         tokens_chunk_size == 0 else temporal_ratio for offset in range(pad_tokens))
        decoded = decoded[:, :, :-pad_frames]
    return decoded
fastvideo.mlx_runtime.minimax_h3_video_vae.MLXMiniMaxH3VideoVAE.decode_clip_tiled
decode_clip_tiled(z, tile_sample_min_height: int, tile_sample_min_width: int, tile_sample_min_overlap_height: int = 64, tile_sample_min_overlap_width: int = 64)

One clip through the decoder with spatial tiling (memory bounded).

Source code in fastvideo/mlx_runtime/minimax_h3_video_vae.py
def decode_clip_tiled(self,
                      z,
                      tile_sample_min_height: int,
                      tile_sample_min_width: int,
                      tile_sample_min_overlap_height: int = 64,
                      tile_sample_min_overlap_width: int = 64):
    """One clip through the decoder with spatial tiling (memory bounded)."""
    height = z.shape[-2] * self.spatial_compression_ratio
    width = z.shape[-1] * self.spatial_compression_ratio
    y_starts, y_lengths, y_overlaps = self._split_tiles(height, tile_sample_min_height,
                                                        tile_sample_min_overlap_height)
    x_starts, x_lengths, x_overlaps = self._split_tiles(width, tile_sample_min_width, tile_sample_min_overlap_width)
    ratio = self.spatial_compression_ratio
    rows = []
    for y_start, y_length in zip(y_starts, y_lengths, strict=False):
        row = []
        for x_start, x_length in zip(x_starts, x_lengths, strict=False):
            tile = z[..., y_start // ratio:y_start // ratio + y_length // ratio,
                     x_start // ratio:x_start // ratio + x_length // ratio]
            row.append(self._decode_clip(tile))
        rows.append(row)
    return self._stitch_tiles(rows, y_overlaps, x_overlaps)
fastvideo.mlx_runtime.minimax_h3_video_vae.MLXMiniMaxH3VideoVAE.encode
encode(pixels)

Normalized pixels (1, 3, T, H, W) -> (mean, logvar) each (1, C, T', H', W').

Pads the clip to clip_length and drops token_drop trailing moment frames exactly like the reference _encode.

Source code in fastvideo/mlx_runtime/minimax_h3_video_vae.py
def encode(self, pixels):
    """Normalized pixels (1, 3, T, H, W) -> (mean, logvar) each (1, C, T', H', W').

    Pads the clip to ``clip_length`` and drops ``token_drop`` trailing
    moment frames exactly like the reference ``_encode``.
    """
    if not self.has_encoder:
        raise RuntimeError("This MLX H3 video VAE was loaded without encoder weights.")
    cfg = self.config
    num_frames = pixels.shape[2]
    if num_frames % cfg.clip_length != 0:
        pad_frames = (-num_frames) % cfg.clip_length
        tail = mx.repeat(pixels[:, :, -1:], pad_frames, axis=2)
        pixels = mx.concatenate([pixels, tail], axis=2)
    moment_chunks: list[Any] = []
    for index in range(pixels.shape[2] // cfg.clip_length):
        clip = pixels[:, :, index * cfg.clip_length:(index + 1) * cfg.clip_length]
        moment_chunks.append(self._encode_clip(clip))
    moments = mx.concatenate(moment_chunks, axis=2)
    if cfg.token_drop > 0:
        moments = moments[:, :, :-cfg.token_drop]
    mean, logvar = mx.split(moments, 2, axis=1)
    logvar = mx.clip(logvar, -30.0, 20.0)
    return mean, logvar
fastvideo.mlx_runtime.minimax_h3_video_vae.MLXMiniMaxH3VideoVAE.encode_keyframe
encode_keyframe(pixels)

Single-frame conditioning encode without chunk padding.

Source code in fastvideo/mlx_runtime/minimax_h3_video_vae.py
def encode_keyframe(self, pixels):
    """Single-frame conditioning encode without chunk padding."""
    if pixels.shape[2] != 1:
        raise ValueError(f"encode_keyframe expects exactly one frame, got {pixels.shape}.")
    moments = self._encode_clip(pixels)
    mean, logvar = mx.split(moments, 2, axis=1)
    logvar = mx.clip(logvar, -30.0, 20.0)
    return mean, logvar
fastvideo.mlx_runtime.minimax_h3_video_vae.MLXMiniMaxH3VideoVAE.sample_posterior staticmethod
sample_posterior(mean, logvar, noise)

Reparameterization with an explicit noise array (deterministic parity).

Source code in fastvideo/mlx_runtime/minimax_h3_video_vae.py
@staticmethod
def sample_posterior(mean, logvar, noise):
    """Reparameterization with an explicit noise array (deterministic parity)."""
    return mean + mx.exp(0.5 * logvar) * noise

fastvideo.mlx_runtime.minimax_h3_video_vae.MiniMaxH3VideoVAEConfigView dataclass

MiniMaxH3VideoVAEConfigView(in_channels: int = 3, out_channels: int = 3, latent_channels: int = 24, block_out_channels: tuple[int, ...] = (128, 256, 256, 512, 512, 1024), layers_per_block: int = 2, spatial_downsample_factors: tuple[int, ...] = (2, 2, 2, 2, 1, 1), temporal_downsample_factors: tuple[int, ...] = (1, 2, 2, 1, 1, 1), norm_num_groups: int = 32, norm_eps: float = 1e-06, decoder_num_layers: int = 36, decoder_num_attention_heads: int = 32, decoder_attention_head_dim: int = 64, decoder_num_register_tokens: int = 4, decoder_ffn_mult: int = 4, decoder_rope_theta: float = 100.0, decoder_rope_dim_ratio: float = 0.75, decoder_norm_eps: float = 1e-05, clip_length: int = 17, token_drop: int = 3, latents_mean: tuple[float, ...] | None = None, latents_std: tuple[float, ...] | None = None)

Architecture constants (defaults mirror the released vae/config.json).

Attributes

fastvideo.mlx_runtime.minimax_h3_video_vae.MiniMaxH3VideoVAEConfigView.tokens_chunk_size property
tokens_chunk_size: int

Latent frames decoded per clip chunk (ceil(clip_length / ratio)).

Functions:

fastvideo.mlx_runtime.minimax_h3_video_vae.mlx_h3_video_vae_from_dir

mlx_h3_video_vae_from_dir(vae_dir: str | Path, *, include_encoder: bool = True, storage_dtype: str = 'fp32', config: MiniMaxH3VideoVAEConfigView | None = None) -> MLXMiniMaxH3VideoVAE

Load the released H3 video VAE, streaming one shard at a time.

storage_dtype="fp32" keeps the released numerics. bf16/fp16 halve residency; measure drift against the FP32 acceptance gate before shipping a reduced-dtype configuration.

Source code in fastvideo/mlx_runtime/minimax_h3_video_vae.py
def mlx_h3_video_vae_from_dir(vae_dir: str | Path,
                              *,
                              include_encoder: bool = True,
                              storage_dtype: str = "fp32",
                              config: MiniMaxH3VideoVAEConfigView | None = None) -> MLXMiniMaxH3VideoVAE:
    """Load the released H3 video VAE, streaming one shard at a time.

    ``storage_dtype="fp32"`` keeps the released numerics. bf16/fp16 halve
    residency; measure drift against the FP32 acceptance gate before shipping
    a reduced-dtype configuration.
    """
    import mlx.core as mx

    vae_dir = Path(vae_dir)
    config = config or MiniMaxH3VideoVAEConfigView.from_vae_dir(vae_dir)
    cast_dtype = _DTYPE_MAP[storage_dtype]

    wanted_prefixes: tuple[str, ...] = ("post_quant_conv.", "decoder.")
    if include_encoder:
        wanted_prefixes = ("quant_conv.", "encoder.", "post_quant_conv.", "decoder.")

    weights: dict[str, Any] = {}
    for shard in _shards(vae_dir):
        arrays = mx.load(str(shard))
        for key, source in arrays.items():
            if not key.startswith(wanted_prefixes):
                continue
            array = source.astype(cast_dtype)
            if array.ndim == 5:  # conv3d (O, I, kT, kH, kW) -> (O, kT, kH, kW, I)
                array = _ct(array, 0, 2, 3, 4, 1)
            mx.eval(array)
            del source
            weights[key] = array
        del arrays
        gc.collect()
        mx.clear_cache()

    required = [
        "post_quant_conv.weight", "post_quant_conv.bias", "decoder.proj_in.weight", "decoder.norm_out.weight",
        "decoder.proj_out.weight", "decoder.register_tokens"
    ]
    missing = [key for key in required if key not in weights]
    if missing:
        raise KeyError(f"H3 video VAE at {vae_dir} is missing required tensors: {missing}")
    if include_encoder:
        encoder_required = [
            "quant_conv.weight", "encoder.conv_in.weight", "encoder.conv_out.weight", "encoder.norm_out.weight"
        ]
        missing = [key for key in encoder_required if key not in weights]
        if missing:
            raise KeyError(f"H3 video VAE encoder at {vae_dir} is missing required tensors: {missing}")
    vae = MLXMiniMaxH3VideoVAE(weights, config, has_encoder=include_encoder)
    expected_blocks = config.decoder_num_layers
    found_blocks = {int(key.split(".")[2]) for key in weights if key.startswith("decoder.transformer_blocks.")}
    if found_blocks != set(range(expected_blocks)):
        raise KeyError(f"H3 video VAE decoder blocks incomplete: found {sorted(found_blocks)} "
                       f"of {expected_blocks}.")
    return vae