3
stars
3
commits
2
linked in READMEs
May 21, 2026
updated
π Part of the Lance MLX collection on mlx-community.
MLX port of the 48-channel Wan2.2 3D causal VAE bundled with ByteDance's Lance unified multimodal model. Converted to bf16 for Apple Silicon. ~705 M parameters, encoder + decoder in a single safetensors file.
β οΈ This is NOT the public
wan2.2_vae.safetensors(which is 16-channel and incompatible with Lance). This is the re-trained 48-channel variant Lance ships with, required by both Lance image and video pipelines.
π’ Production-ready as of 2026-05-21. Roundtrip MAD β 7/255 in u8 domain on real photographs at 768Β².
| Component | Status |
|---|---|
Encoder (Wan22VAEEncoder) | β Loads cleanly, 86 keys mapped |
Decoder (Wan22VAEDecoder) | β Loads cleanly, 110 keys mapped |
| Streaming feature cache (1+4+4+β¦ chunked encode) | β
Per-conv feat_cache works across temporal chunks |
Per-channel latent normalization (VAE22_MEAN, VAE22_STD) | β 48-channel mean/std applied after encode, reversed before decode |
Install the mlx-video module (provides the Wan22VAEEncoder / Wan22VAEDecoder classes):
from huggingface_hub import snapshot_download
import mlx.core as mx
from mlx_video.models.wan_2.vae22 import (
Wan22VAEEncoder, Wan22VAEDecoder, denormalize_latents,
)
repo = snapshot_download("mlx-community/Wan2.2-VAE-Lance-bf16")
weights = mx.load(f"{repo}/vae.safetensors")
enc = Wan22VAEEncoder(z_dim=48, dim=160)
enc.load_weights([
(k, v) for k, v in weights.items()
if k.startswith("encoder.") or k.startswith("conv1.")
])
mx.eval(enc.parameters())
dec = Wan22VAEDecoder(z_dim=48, dim=160, dec_dim=256)
dec.load_weights([
(k, v) for k, v in weights.items()
if k.startswith("decoder.") or k.startswith("conv2.")
])
mx.eval(dec.parameters())
import numpy as np
from PIL import Image
img = Image.open("photo.jpg").convert("RGB").resize((768, 768))
arr = np.asarray(img, dtype=np.float32) / 127.5 - 1.0 # [-1, 1]
x = mx.array(arr[None, None, ...]) # (1, 1, H, W, 3)
z = enc(x) # (1, 1, 48, 48, 48)
print("latent shape:", z.shape)
# mean β -0.07, std β 0.60 (per-channel normalized)
z_denorm = denormalize_latents(z) # apply per-channel std/mean
decoded = dec(z_denorm) # (1, T'>=1, H', W', 3) in [-1, 1]
out_img = ((np.array(decoded[0, 0]) + 1.0) * 127.5).clip(0, 255).astype(np.uint8)
Image.fromarray(out_img).save("roundtrip.png")
dim_mult=(1,2,4,4), num_res_blocks=2, temperal_downsample=(False, True, True)dim_mult=(1,2,4,4), num_res_blocks=2, temperal_upsample=(True, True, False)| Input | Output dims | Per-pixel MAD ([0, 255] u8) | Max abs error |
|---|---|---|---|
768Β² photo (edit_img.jpg, painting) | 768Β² | 7.36 / 255 | 0.82 / 1.0 |
Loaded in ~0.3 s on M5 Max 128 GB; encode 0.55 s, decode 1.65 s.
| File | Size | Notes |
|---|---|---|
vae.safetensors | 1.41 GB | Encoder + decoder, bf16 (197 keys including conv1, conv2, encoder.*, decoder.*) |
vae_conversion_report.json | β | PyTorch β MLX conversion provenance: 62 conv3d + 10 conv2d + 50 RMS gamma + 2 attn-norm gamma + 170 renamed + 72 other |
Source: bytedance-research/Lance/Wan2.2_VAE.pth (PyTorch, 704.7 M params, 196 tensors after splitting nested modules).
Converted via scripts/06_convert_wan_vae.py which:
(out, in, T, H, W) β (out, T, H, W, in) for MLX channels-last convention.vae.* prefix in the original.Both Lance's image and video pipelines need this VAE. Publishing it once decouples versioning: a fix or upgrade to the VAE doesn't force a re-download of either ~12 GB LLM. The companion repos (mlx-community/Lance-3B-bf16 and mlx-community/Lance-3B-Video-bf16) bundle a copy for convenience, but power users should pin this one and use it across both.
Apache 2.0. The original Wan2.2 VAE weights are Β© Alibaba; this MLX port is Β© the lance-mlx contributors. See NOTICE in the lance-mlx repo for attribution.
github.com/xocialize/lance-mlxbytedance-research/Lance/Wan2.2_VAE.pthmlx-community/Lance-3B-bf16mlx-community/Lance-3B-Video-bf163 commits
3
stars
3
commits
2
linked in READMEs
May 21, 2026
updated
π Part of the Lance MLX collection on mlx-community.
MLX port of the 48-channel Wan2.2 3D causal VAE bundled with ByteDance's Lance unified multimodal model. Converted to bf16 for Apple Silicon. ~705 M parameters, encoder + decoder in a single safetensors file.
β οΈ This is NOT the public
wan2.2_vae.safetensors(which is 16-channel and incompatible with Lance). This is the re-trained 48-channel variant Lance ships with, required by both Lance image and video pipelines.
π’ Production-ready as of 2026-05-21. Roundtrip MAD β 7/255 in u8 domain on real photographs at 768Β².
| Component | Status |
|---|---|
Encoder (Wan22VAEEncoder) | β Loads cleanly, 86 keys mapped |
Decoder (Wan22VAEDecoder) | β Loads cleanly, 110 keys mapped |
| Streaming feature cache (1+4+4+β¦ chunked encode) | β
Per-conv feat_cache works across temporal chunks |
Per-channel latent normalization (VAE22_MEAN, VAE22_STD) | β 48-channel mean/std applied after encode, reversed before decode |
Install the mlx-video module (provides the Wan22VAEEncoder / Wan22VAEDecoder classes):
from huggingface_hub import snapshot_download
import mlx.core as mx
from mlx_video.models.wan_2.vae22 import (
Wan22VAEEncoder, Wan22VAEDecoder, denormalize_latents,
)
repo = snapshot_download("mlx-community/Wan2.2-VAE-Lance-bf16")
weights = mx.load(f"{repo}/vae.safetensors")
enc = Wan22VAEEncoder(z_dim=48, dim=160)
enc.load_weights([
(k, v) for k, v in weights.items()
if k.startswith("encoder.") or k.startswith("conv1.")
])
mx.eval(enc.parameters())
dec = Wan22VAEDecoder(z_dim=48, dim=160, dec_dim=256)
dec.load_weights([
(k, v) for k, v in weights.items()
if k.startswith("decoder.") or k.startswith("conv2.")
])
mx.eval(dec.parameters())
import numpy as np
from PIL import Image
img = Image.open("photo.jpg").convert("RGB").resize((768, 768))
arr = np.asarray(img, dtype=np.float32) / 127.5 - 1.0 # [-1, 1]
x = mx.array(arr[None, None, ...]) # (1, 1, H, W, 3)
z = enc(x) # (1, 1, 48, 48, 48)
print("latent shape:", z.shape)
# mean β -0.07, std β 0.60 (per-channel normalized)
z_denorm = denormalize_latents(z) # apply per-channel std/mean
decoded = dec(z_denorm) # (1, T'>=1, H', W', 3) in [-1, 1]
out_img = ((np.array(decoded[0, 0]) + 1.0) * 127.5).clip(0, 255).astype(np.uint8)
Image.fromarray(out_img).save("roundtrip.png")
dim_mult=(1,2,4,4), num_res_blocks=2, temperal_downsample=(False, True, True)dim_mult=(1,2,4,4), num_res_blocks=2, temperal_upsample=(True, True, False)| Input | Output dims | Per-pixel MAD ([0, 255] u8) | Max abs error |
|---|---|---|---|
768Β² photo (edit_img.jpg, painting) | 768Β² | 7.36 / 255 | 0.82 / 1.0 |
Loaded in ~0.3 s on M5 Max 128 GB; encode 0.55 s, decode 1.65 s.
| File | Size | Notes |
|---|---|---|
vae.safetensors | 1.41 GB | Encoder + decoder, bf16 (197 keys including conv1, conv2, encoder.*, decoder.*) |
vae_conversion_report.json | β | PyTorch β MLX conversion provenance: 62 conv3d + 10 conv2d + 50 RMS gamma + 2 attn-norm gamma + 170 renamed + 72 other |
Source: bytedance-research/Lance/Wan2.2_VAE.pth (PyTorch, 704.7 M params, 196 tensors after splitting nested modules).
Converted via scripts/06_convert_wan_vae.py which:
(out, in, T, H, W) β (out, T, H, W, in) for MLX channels-last convention.vae.* prefix in the original.Both Lance's image and video pipelines need this VAE. Publishing it once decouples versioning: a fix or upgrade to the VAE doesn't force a re-download of either ~12 GB LLM. The companion repos (mlx-community/Lance-3B-bf16 and mlx-community/Lance-3B-Video-bf16) bundle a copy for convenience, but power users should pin this one and use it across both.
Apache 2.0. The original Wan2.2 VAE weights are Β© Alibaba; this MLX port is Β© the lance-mlx contributors. See NOTICE in the lance-mlx repo for attribution.
github.com/xocialize/lance-mlxbytedance-research/Lance/Wan2.2_VAE.pthmlx-community/Lance-3B-bf16mlx-community/Lance-3B-Video-bf163 commits