MLX port of Lance for Apple Silicon. Lance is a 3B-active / ~12B-total parameter dual-stream Mixture-of-Transformer-Experts model that unifies image and video understanding, generation, and editing in a single framework. This package brings Lance to Apple Silicon via MLX, with weights hosted on the mlx-community HuggingFace organization.
5
stars
121
commits
Python
primary language
Jun 21, 2026
updated
Note: "Lance" here refers to ByteDance Intelligent Creation Lab's unified multimodal model (paper, weights), not Lance/LanceDB (the columnar data format).
MLX port of Lance for Apple Silicon. Lance is a 3B-active / ~12B-total parameter dual-stream Mixture-of-Transformer-Experts model that unifies image and video understanding, generation, and editing in a single framework. This package brings Lance to Apple Silicon via MLX, with weights hosted on the mlx-community HuggingFace organization.
mlx-community)All repos live in the Lance MLX collection for one-click browsing.
| Task | Use | Why |
|---|---|---|
t2i (textβimage) | Lance-3B-bf16 | Only validated path; quant variants all degrade β see research notes below |
image_edit | Lance-3B-bf16 | Same precision requirement as t2i |
t2v (textβvideo) | Lance-3B-Video-bf16 | Production within n_lat β€ 16,128 |
video_edit | Lance-3B-Video-bf16 | Same envelope as t2v |
x2t_image (VQA) on 32 GB+ Macs | Lance-3B-bf16 | Full bf16 quality |
x2t_image (VQA) on 8-16 GB Macs | Lance-3B-AWQ-INT4 | 27% size, 6-9Γ faster decode, ~4/6 oracle parity with bf16 |
x2t_video (VQA) | Lance-3B-Video-bf16 | No video-VQA quant variant β bf16 only |
| Wan2.2 VAE (standalone) | Wan2.2-VAE-Lance-bf16 | Shared by image + video pipelines |
| Repo | Status | Notes |
|---|---|---|
mlx-community/Lance-3B-bf16 | π’ Production | t2i, image_edit, x2t_image (full quality, ~15 GB) |
mlx-community/Lance-3B-AWQ-INT4 | π’ Production (VQA only) | x2t_image on 8-16 GB Macs (5.65 GB repo, 3.31 GB LLM, 6-9Γ faster long-form decode). Don't use for t2i β quant research notes below. |
mlx-community/Lance-3B-8bit | β οΈ Superseded | Naive 8-bit broken on image gen; Lance-3B-AWQ-INT4 is the working compressed variant for VQA. Kept for historical reproducibility. |
mlx-community/Wan2.2-VAE-Lance-bf16 | π’ Production | 48-ch Wan2.2 VAE (standalone, shared by image + video pipelines) |
mlx-community/Lance-3B-Video-bf16 | π’ Production through 768Β²Γ25f | t2v (photoreal after Phase 5j + 5m fixes), x2t_video, video_edit |
Bottom line: bf16 is the only production-grade variant for image generation. AWQ-INT4 ships for VQA only. We are not actively developing further quantization variants.
What we tried (each linked to a notes writeup):
mlx-community/Lance-3B-8bit is from this lineage.bits < 8 gate so no 8-bit variant possible.src/lance_mlx/quant/ (awq.py, calibrate.py). Produces shippable VQA variant at 4-bit; doesn't close the image-gen gap.What we learned (phase5c3_awq_port/PHASE_5C3H_FINDINGS.md):
Forward pointer for downstream mlxEngine work: notes/mlx_engine_quant_notes.md β captures the Lance-specific constraints and the two speculative paths that target compounding directly (hybrid precision per layer position, or step-conditional AWQ calibration) rather than per-layer quant precision.
For now, bf16 is the recommended path for any image generation use case, on any Apple Silicon hardware that can fit it. AWQ-INT4 unlocks Lance VQA on smaller Macs.
π’ Image and video both production-quality through n_lat β€ 16,128 (2026-05-23). Image pipelines (t2i, image_edit, x2t_image) reproduce the bf16 PyTorch reference. Video pipelines (t2v, video_edit, x2t_video) landed two production fixes: Phase 5j position-ID fix (latent_pos_base=0, closed issue #2) and Phase 5m CFG-renorm fix (v0.5.2, cfg_renorm_type="channel"). Production envelope: n_lat β€ 16,128 (256Β² through 768Β²Γ25f, 480Γ704Γ17f) β verified across two diagnostic prompts (panda surfing, bus + Big Ben). Phase 5m root cause: the legacy "global" CFG-renorm cap computed a single scalar L2 over the full velocity tensor, so at ~2Γ n_lat the same cap silently over-suppressed high-frequency detail. "channel" clamps per-channel without dragging the aggregate. Within the envelope, n_lat β quality is stochastic seed Γ scale, not a monotonic degradation curve. At n_lat β₯ ~30k (768Β²Γ49f, 480Γ848Γ121f) Phase 5m partially closes the original pure-noise failure to a milder "structured-but-degraded with mesh artifacts" regime β model attempts the scene but VAE outputs colored geometric tiles overlaid throughout (verified manually 2026-05-23, final std=0.623 vs ~0.88 for clean runs β channel renorm clamps too aggressively at late timesteps once n_lat hits ~30k, pushing latents outside the VAE's trained distribution). Narrowed scope of issue #1.
| Capability | Status |
|---|---|
| Convert HF safetensors β MLX bf16 (both checkpoints + Wan2.2 VAE) | β
scripts/02_convert.py, scripts/06_convert_wan_vae.py |
Load Lance_3B + Lance_3B_Video into LanceModel | β 0 missing keys, dummy forward verified |
| x2t_image VQA (image β text answer) | β Production, upstream-faithful preprocessing (2026-06-11). Fine reads (chart values) require the upstream bucket-crop geometry β now the default; see "x2t preprocessing fidelity" below. |
| KV cache for fast autoregressive decode | β 1.7Γβ2.8Γ speedup on long generations |
| t2i (text β image generation) | β Production. Photorealistic, prompt-aligned output. |
| image_edit (instruction-based) | β Production. "Remove hat" preserves identity + style + signature; "Add pearl necklace" leaves rest intact. |
| t2v (text β video) | β Production. Photoreal/CGI-quality output at n_lat β€ 16,128 (256β768Β² Γ β€25f; 480Γ704Γ17f; 640Β²Γ17f) after Phase 5j position-ID fix + Phase 5m CFG-renorm fix. Verified across two diagnostic prompts (panda surfing, bus + Big Ben). β Degraded with mesh artifacts at n_lat β₯ ~30k (768Β²Γ49f, 480Γ848Γ121f) β issue #1 narrowed from "pure noise" to "structured-but-degraded mesh artifacts". |
| x2t_video (video VQA) | β Validated against Phase 0 oracle. Cooking video β kitchen+pan+spatula+tomato+meat all content-correct in 17.5 s. |
| video_edit (instruction-based) | β Same envelope as t2v: works at β€9,216 latent tokens after Phase 5d fix. |
| 8-bit + 4-bit quants + HF community variants | π¬ Closed as research β see "Quantization research" above (AWQ-INT4 shipped for VQA; bf16 only for image gen) |
Try it:
# Install
git clone https://github.com/xocialize/lance-mlx && cd lance-mlx && uv sync
# Download production-ready image MVP (~15 GB):
HF_HUB_DISABLE_XET=1 uv run huggingface-cli download mlx-community/Lance-3B-bf16
# t2i β photorealistic text-to-image:
HF_HUB_DISABLE_XET=1 uv run python scripts/08_t2i_demo.py \
--prompt "A photorealistic tabby cat holding a colorful STOP sign." \
--lance-weights ~/.cache/huggingface/hub/models--mlx-community--Lance-3B-bf16/snapshots/*/ \
--vae-weights ~/.cache/huggingface/hub/models--mlx-community--Lance-3B-bf16/snapshots/*/vae.safetensors
# image_edit β instruction-based editing:
HF_HUB_DISABLE_XET=1 uv run python scripts/13_image_edit_demo.py \
--input-image my_photo.jpg \
--instruction "Remove the hat from the painting." \
--lance-weights .../Lance-3B-bf16 --vae-weights .../vae.safetensors
# x2t_image β image VQA:
HF_HUB_DISABLE_XET=1 uv run python scripts/04_x2t_image_demo.py \
--case 03 \
--lance-weights .../Lance-3B-bf16 \
--vit-weights .../Lance-3B-bf16/vit.safetensors
The demo scripts above are the per-task drivers used to validate the pipelines. The installed console entry point (
lance-mlx generate) wraps the same pipelines β see "Quick start" below. Note the--memory-mode/--lossless-decodeknobs documented in "Hardware" are currently library-API options onLancePipeline.from_pretrained/.generate(and the demo scripts), not yet flags on thelance-mlx generateCLI.
Upstream Lance does not preprocess x2t images with the HF Qwen2.5-VL
smart-resize. Its ViT stream is BucketResize (deterministic center-crop to the
nearest aspect-ratio bucket at resolution_vitΒ² area) + DivisibleCrop(28) +
CLIP normalize, with the vision span attending bidirectionally in prefill and
logits masked to len(tokenizer). Using HF smart-resize instead produces
systematic wrong answers on fine chart reads (the same wrong value every
run β oracle case-02 answered "43" instead of "29%" across 14 runs and three
independent MLX implementations).
UnderstandingPipeline.generate is now upstream-faithful by default
(preprocess="upstream", vision_full_attn=True, resolution="image_768res");
the byte-exact transform lives in pipeline/upstream_und_preprocess.py
(verified max|diff| = 0.0 against upstream's verbatim torchvision code). The
legacy path is available via preprocess="hf".
Parity scope note: with preprocessing byte-exact, position ids exactly equal
to HF get_rope_index, and every ViT stage at cosine 1.000000 vs PyTorch on the
CPU stream, residual answer differences vs a CUDA capture are greedy knife-edge
flips from backend accumulation noise (Apple-GPU fp32 matmul β 8e-4 rel/op,
flash-attn ordering, autocast structure) β token-exact equality with
foreign-hardware captures is not an achievable or meaningful gate for
autoregressive generation. Gate semantically. Diagnostic tooling:
scripts/46_upstream_exact_gate.py (ablation grid), scripts/47_vit_vs_pytorch.py +
scripts/48_vit_stage_bisect.py (ViT PT-parity + stage bisect), scripts/49_crossfeed_pt_vit.py (PT-ViT cross-feed).
Two schedulers ship in this repo (src/lance_mlx/scheduler/solvers.py β DPMSolverPlusPlus2M, with an Euler warm-up fallback):
HF_HUB_DISABLE_XET=1 uv run python scripts/08_t2i_demo.py \
--prompt "A photorealistic tabby cat holding a colorful STOP sign." \
--lance-weights ~/.cache/huggingface/hub/models--mlx-community--Lance-3B-bf16/snapshots/*/ \
--vae-weights ~/.cache/huggingface/hub/models--mlx-community--Lance-3B-bf16/snapshots/*/vae.safetensors \
--scheduler dpm --steps 12
HF_HUB_DISABLE_XET=1 uv run python scripts/08_t2i_demo.py \
--prompt "A photorealistic tabby cat holding a colorful STOP sign." \
--lance-weights ~/.cache/huggingface/hub/models--mlx-community--Lance-3B-bf16/snapshots/*/ \
--vae-weights ~/.cache/huggingface/hub/models--mlx-community--Lance-3B-bf16/snapshots/*/vae.safetensors \
--scheduler euler --steps 30
| Mode | Steps | Generate time | vs baseline |
|---|---|---|---|
| DPM-Solver++(2M) | 12 | ~78s | 2.4Γ |
| Default Euler | 30 | ~196s | β |
Benchmarked on Lance-3B-bf16, 768Β², seed=42, cfg_scale=4.0, M-series Apple Silicon.
See HANDOFF.md for the phased roadmap (start with the β Verified findings (2026-05-19) section β it supersedes earlier guesses). Phase 0 parity-oracle capture runbook lives at Docs/RUNPOD_PHASE0.md. Per-phase technical notes in notes/. Open items / current backlog: BACKLOG.md. Measured memory footprints: LIMITS.md.
The package installs a lance-mlx console script (lance_mlx.__main__:main) with a single
generate subcommand. --weights is required:
uv pip install lance-mlx
# Image generation
lance-mlx generate --task t2i --prompt "..." --weights mlx-community/Lance-3B-bf16
# Image editing
lance-mlx generate --task image_edit --image foo.jpg --prompt "Remove the hat" --weights mlx-community/Lance-3B-bf16
# Image understanding (VQA)
lance-mlx generate --task x2t_image --image foo.png --prompt "What is this?" --weights mlx-community/Lance-3B-bf16
# Video generation
lance-mlx generate --task t2v --prompt "..." --weights mlx-community/Lance-3B-Video-bf16
generate flags: --task {t2i,t2v,image_edit,video_edit,x2t_image,x2t_video} (required),
--weights (required), --prompt, --image, --video, --output (default outputs/),
--seed (42), --steps (30), --cfg (4.0), --timestep-shift (3.5), --resolution (768),
--frames (50), --fps (12).
t2i β text-to-image (768Β²)t2v β text-to-video (480p, 12 fps, β€121 frames)image_edit β instruction-based image editingvideo_edit β instruction-based video editingx2t_image β image understanding / VQA / captioningx2t_video β video understanding / VQA / captioningLLM_UND, LLM_GEN), each initialized from Qwen2.5-VL-3B-Instruct, with per-expert FFN, output projection, and QK-normLLM_UND (autoregressive next-token); Wan2.2 3D causal VAE latent tokens β LLM_GEN (flow-matching velocity prediction)wan2.2_vae.safetensors)Blaizzy/mlx-vlm for the Qwen2.5-VL ViT and autoregressive decode infrastructure (pinned: f2e19deβ¦)Blaizzy/mlx-video for the Wan2.2 VAE and flow-matching sampler (pinned: 87db56aβ¦)Per-task floor with the memory_mode knob (auto / parallel / relay) introduced 2026-06-02:
| RAM | Image (t2i, image_edit) | Video (t2v, video_edit) | VQA (x2t_image) | Notes |
|---|---|---|---|---|
| 8β16 GB | β
bf16 via memory_mode=relay (single-shot per pipeline load) | β
via relay (e.g. 256Β²Γ61f fits in ~8.7 GB) | β
Lance-3B-AWQ-INT4 (3.3 GB LLM) | auto resolves to relay below 18 GiB |
| 24 GB+ | β
bf16 via memory_mode=parallel (reusable pipeline) | β
via parallel | β bf16 or AWQ-INT4 | auto resolves to parallel |
memory_mode=relay produces byte-identical output to parallel (MD5-verified
on real Lance-3B-bf16); it sheds the UND tower after prefill and the GEN tower
before VAE decode, so peak β heaviest single phase rather than the sum. Pipeline
is single-shot β re-prefill needs the UND tower reloaded. parallel keeps
everything resident for repeated calls. The default auto mode chooses by
the GPU/accelerator budget (ws β₯ 18 GiB β parallel, else relay; unresolved β parallel).
(relay/parallel are byte-identical to each other regardless of which VAE decode mode is
selected β the schedule doesn't change the math.) memory_mode is set at
LancePipeline.from_pretrained / honored at .generate; relay is a load-time decision
(GEN tower + VAE load lazily) and resolve_memory_mode lives in model/lance_llm.py.
generate(..., lossless_decode=True) (default) uses a bit-identical streaming decode
(vae_stream.decode_streaming); lossless_decode=False keeps the lossy trapezoidal-blend tiling
(~1.5β4.8 / 255 off the reference). The streaming decode bounds the memory a lossless decode
needs: against the naive whole dec(z) (the only prior bit-identical option) it is lighter in
every measured case β via temporal streaming (flat in length) and spatial halo-tiling β at zero
pixel cost (50-case bit-identity, max|Ξ|=0). Measured true footprints (ri_phys, 16 GB;
method + raw in LIMITS.md and results/decode_lossless/):
| output | whole dec(z) | lossless streaming | lossy blend | on 16 GB |
|---|---|---|---|---|
| 256Β² video β 121f | 15.41 | 8.05 | 12.47 | lossless β lighter + exact |
| 512Β² video β 61f | (OOM) | 12.64 | (OOM >20) | lossless β only path that fits |
| 1024Β² image | 15.60 | 12.17 | 12.53 | lossless β lighter + exact |
| 768Β² video (β₯13f) | (OOM) | >~21 (lower boundΒΉ) | 13.2β16.3 (+~3 swap) | lossy only on 16 GB (lossless: 24 GB+ [P]Β²) |
ΒΉ The lossless 768Β² decode exceeds 16 GB; our only measurement is a watchdog abort at >~21 GB, a lower bound β the exact peak is unmeasured. Β² We have no 24 GB machine to confirm it, so "fits 24 GB" is an unverified inference from the >~21 GB footprint, not a measurement.
When to keep the lossy fallback. Lossy is not a general low-memory mode β at 256Β²/512Β²
video and all images the lossless streaming decode is already the lighter path (and exact), so
there is no reason to use lossy there. Lossy earns its keep in exactly one case: 768Β²
video on a 16 GB Mac, where the lossless decode exceeds ~21 GB and does not fit while the lossy
blend (13.2β16.3 GB, +~3 GB swap at 25f) does. The n_lat β€ 16,128 envelope (256Β² β 768Β²Γ25f)
was established with that lossy decode: a 16 GB machine can generate 768Β²Γ25f but only
lossily decodes it β use lossless_decode=False there, or a bigger-RAM machine for
bit-identical 768Β² video (24 GB+ [P] β inferred from the >~21 GB lower bound; we have not measured
the true peak or confirmed 24 GB suffices). The default lossless_decode=True is left to
repo-owner discretion for main.
.
βββ HANDOFF.md phased port plan (this is the spec)
βββ BACKLOG.md open items / current backlog
βββ LIMITS.md measured memory footprints + method
βββ pyproject.toml hatchling build; uv-managed env (deps pin mlx-vlm/mlx-video)
βββ src/lance_mlx/
β βββ __init__.py
β βββ __main__.py `lance-mlx generate` CLI entry point
β βββ bench.py Timer + RunRecord + JSONL logging
β βββ io.py image/video IO + muxing
β βββ model/
β β βββ lance_llm.py dual-expert MoT backbone + resolve_memory_mode
β β βββ _loader.py weight loading
β β βββ mape.py modality-aware RoPE
β β βββ flow_head.py velocity prediction head
β β βββ time_embedder.py timestep embedding
β β βββ routing.py token modality routing
β β βββ latent_pos_embed.py latent position ids (Phase 5j fix)
β β βββ prefix_cache.py prefix/KV cache for AR decode
β β βββ vae_bridge.py Wan2.2 VAE interface
β β βββ vae_stream.py bit-identical streaming/halo-tiled decode
β βββ pipeline/
β β βββ t2i.py text-to-image flow loop (memory_mode, lossless_decode)
β β βββ t2v.py text-to-video flow loop
β β βββ image_edit.py
β β βββ video_edit.py
β β βββ understanding.py x2t_image + x2t_video AR decode
β β βββ upstream_und_preprocess.py byte-exact upstream x2t preprocessing
β βββ scheduler/
β β βββ solvers.py DPMSolverPlusPlus2M (+ Euler warm-up)
β βββ quant/
β βββ awq.py AWQ port (alpha-search + scale fusion)
β βββ calibrate.py calibration harness
βββ scripts/ ~50 numbered phase scripts + per-task demos:
β βββ 00_capture_oracle.py Phase 0 PyTorch reference capture (cloud GPU)
β βββ 01_inspect_keys.py weight topology audit
β βββ 02_convert.py Lance checkpoint β MLX bf16 conversion
β βββ 06_convert_wan_vae.py Wan2.2 VAE conversion
β βββ 06_publish_hf.py HF upload
β βββ 08_t2i_demo.py t2i driver (--scheduler dpm|euler)
β βββ 13_image_edit_demo.py image_edit driver
β βββ 04_x2t_image_demo.py x2t_image VQA driver
β βββ 10_t2v_demo.py / 14_x2t_video_demo.py / 15_video_edit_demo.py
β βββ 46β49_*.py ViT/upstream parity diagnostics
βββ prompts/
β βββ t2i_eval.json
β βββ t2v_eval.json
β βββ understanding_eval.json
βββ tests/
β βββ fixtures/ Phase 0 PyTorch reference outputs
β βββ test_routing.py
β βββ test_mape.py
β βββ test_lance_model.py
β βββ test_mot_layer.py
β βββ test_scheduler.py
β βββ test_decode_stream.py
β βββ test_prefix_equiv.py
β βββ test_bench.py
β βββ test_model_smoke.py
βββ notes/ phase-by-phase educational notes
βββ Docs/ RUNPOD_PHASE0.md + runbooks
βββ results/ measured footprints / decode validation
βββ vendor/ read-only reference clones
This MLX port: Apache 2.0.
Lance model weights: Apache 2.0 (ByteDance Intelligent Creation Lab). Wan2.2 VAE: Apache 2.0 (Alibaba). Qwen2.5-VL: Apache 2.0 (Alibaba).
See LICENSE and NOTICE for full attribution.
@article{fu2026lance,
title={Lance: Unified Multimodal Modeling by Multi-Task Synergy},
author={Fu, Fengyi and Huang, Mengqi and Wu, Shaojin and others},
journal={arXiv preprint arXiv:2605.18678},
year={2026}
}
Python
100.0%
MLX port of Lance for Apple Silicon. Lance is a 3B-active / ~12B-total parameter dual-stream Mixture-of-Transformer-Experts model that unifies image and video understanding, generation, and editing in a single framework. This package brings Lance to Apple Silicon via MLX, with weights hosted on the mlx-community HuggingFace organization.
5
stars
121
commits
Python
primary language
Jun 21, 2026
updated
Note: "Lance" here refers to ByteDance Intelligent Creation Lab's unified multimodal model (paper, weights), not Lance/LanceDB (the columnar data format).
MLX port of Lance for Apple Silicon. Lance is a 3B-active / ~12B-total parameter dual-stream Mixture-of-Transformer-Experts model that unifies image and video understanding, generation, and editing in a single framework. This package brings Lance to Apple Silicon via MLX, with weights hosted on the mlx-community HuggingFace organization.
mlx-community)All repos live in the Lance MLX collection for one-click browsing.
| Task | Use | Why |
|---|---|---|
t2i (textβimage) | Lance-3B-bf16 | Only validated path; quant variants all degrade β see research notes below |
image_edit | Lance-3B-bf16 | Same precision requirement as t2i |
t2v (textβvideo) | Lance-3B-Video-bf16 | Production within n_lat β€ 16,128 |
video_edit | Lance-3B-Video-bf16 | Same envelope as t2v |
x2t_image (VQA) on 32 GB+ Macs | Lance-3B-bf16 | Full bf16 quality |
x2t_image (VQA) on 8-16 GB Macs | Lance-3B-AWQ-INT4 | 27% size, 6-9Γ faster decode, ~4/6 oracle parity with bf16 |
x2t_video (VQA) | Lance-3B-Video-bf16 | No video-VQA quant variant β bf16 only |
| Wan2.2 VAE (standalone) | Wan2.2-VAE-Lance-bf16 | Shared by image + video pipelines |
| Repo | Status | Notes |
|---|---|---|
mlx-community/Lance-3B-bf16 | π’ Production | t2i, image_edit, x2t_image (full quality, ~15 GB) |
mlx-community/Lance-3B-AWQ-INT4 | π’ Production (VQA only) | x2t_image on 8-16 GB Macs (5.65 GB repo, 3.31 GB LLM, 6-9Γ faster long-form decode). Don't use for t2i β quant research notes below. |
mlx-community/Lance-3B-8bit | β οΈ Superseded | Naive 8-bit broken on image gen; Lance-3B-AWQ-INT4 is the working compressed variant for VQA. Kept for historical reproducibility. |
mlx-community/Wan2.2-VAE-Lance-bf16 | π’ Production | 48-ch Wan2.2 VAE (standalone, shared by image + video pipelines) |
mlx-community/Lance-3B-Video-bf16 | π’ Production through 768Β²Γ25f | t2v (photoreal after Phase 5j + 5m fixes), x2t_video, video_edit |
Bottom line: bf16 is the only production-grade variant for image generation. AWQ-INT4 ships for VQA only. We are not actively developing further quantization variants.
What we tried (each linked to a notes writeup):
mlx-community/Lance-3B-8bit is from this lineage.bits < 8 gate so no 8-bit variant possible.src/lance_mlx/quant/ (awq.py, calibrate.py). Produces shippable VQA variant at 4-bit; doesn't close the image-gen gap.What we learned (phase5c3_awq_port/PHASE_5C3H_FINDINGS.md):
Forward pointer for downstream mlxEngine work: notes/mlx_engine_quant_notes.md β captures the Lance-specific constraints and the two speculative paths that target compounding directly (hybrid precision per layer position, or step-conditional AWQ calibration) rather than per-layer quant precision.
For now, bf16 is the recommended path for any image generation use case, on any Apple Silicon hardware that can fit it. AWQ-INT4 unlocks Lance VQA on smaller Macs.
π’ Image and video both production-quality through n_lat β€ 16,128 (2026-05-23). Image pipelines (t2i, image_edit, x2t_image) reproduce the bf16 PyTorch reference. Video pipelines (t2v, video_edit, x2t_video) landed two production fixes: Phase 5j position-ID fix (latent_pos_base=0, closed issue #2) and Phase 5m CFG-renorm fix (v0.5.2, cfg_renorm_type="channel"). Production envelope: n_lat β€ 16,128 (256Β² through 768Β²Γ25f, 480Γ704Γ17f) β verified across two diagnostic prompts (panda surfing, bus + Big Ben). Phase 5m root cause: the legacy "global" CFG-renorm cap computed a single scalar L2 over the full velocity tensor, so at ~2Γ n_lat the same cap silently over-suppressed high-frequency detail. "channel" clamps per-channel without dragging the aggregate. Within the envelope, n_lat β quality is stochastic seed Γ scale, not a monotonic degradation curve. At n_lat β₯ ~30k (768Β²Γ49f, 480Γ848Γ121f) Phase 5m partially closes the original pure-noise failure to a milder "structured-but-degraded with mesh artifacts" regime β model attempts the scene but VAE outputs colored geometric tiles overlaid throughout (verified manually 2026-05-23, final std=0.623 vs ~0.88 for clean runs β channel renorm clamps too aggressively at late timesteps once n_lat hits ~30k, pushing latents outside the VAE's trained distribution). Narrowed scope of issue #1.
| Capability | Status |
|---|---|
| Convert HF safetensors β MLX bf16 (both checkpoints + Wan2.2 VAE) | β
scripts/02_convert.py, scripts/06_convert_wan_vae.py |
Load Lance_3B + Lance_3B_Video into LanceModel | β 0 missing keys, dummy forward verified |
| x2t_image VQA (image β text answer) | β Production, upstream-faithful preprocessing (2026-06-11). Fine reads (chart values) require the upstream bucket-crop geometry β now the default; see "x2t preprocessing fidelity" below. |
| KV cache for fast autoregressive decode | β 1.7Γβ2.8Γ speedup on long generations |
| t2i (text β image generation) | β Production. Photorealistic, prompt-aligned output. |
| image_edit (instruction-based) | β Production. "Remove hat" preserves identity + style + signature; "Add pearl necklace" leaves rest intact. |
| t2v (text β video) | β Production. Photoreal/CGI-quality output at n_lat β€ 16,128 (256β768Β² Γ β€25f; 480Γ704Γ17f; 640Β²Γ17f) after Phase 5j position-ID fix + Phase 5m CFG-renorm fix. Verified across two diagnostic prompts (panda surfing, bus + Big Ben). β Degraded with mesh artifacts at n_lat β₯ ~30k (768Β²Γ49f, 480Γ848Γ121f) β issue #1 narrowed from "pure noise" to "structured-but-degraded mesh artifacts". |
| x2t_video (video VQA) | β Validated against Phase 0 oracle. Cooking video β kitchen+pan+spatula+tomato+meat all content-correct in 17.5 s. |
| video_edit (instruction-based) | β Same envelope as t2v: works at β€9,216 latent tokens after Phase 5d fix. |
| 8-bit + 4-bit quants + HF community variants | π¬ Closed as research β see "Quantization research" above (AWQ-INT4 shipped for VQA; bf16 only for image gen) |
Try it:
# Install
git clone https://github.com/xocialize/lance-mlx && cd lance-mlx && uv sync
# Download production-ready image MVP (~15 GB):
HF_HUB_DISABLE_XET=1 uv run huggingface-cli download mlx-community/Lance-3B-bf16
# t2i β photorealistic text-to-image:
HF_HUB_DISABLE_XET=1 uv run python scripts/08_t2i_demo.py \
--prompt "A photorealistic tabby cat holding a colorful STOP sign." \
--lance-weights ~/.cache/huggingface/hub/models--mlx-community--Lance-3B-bf16/snapshots/*/ \
--vae-weights ~/.cache/huggingface/hub/models--mlx-community--Lance-3B-bf16/snapshots/*/vae.safetensors
# image_edit β instruction-based editing:
HF_HUB_DISABLE_XET=1 uv run python scripts/13_image_edit_demo.py \
--input-image my_photo.jpg \
--instruction "Remove the hat from the painting." \
--lance-weights .../Lance-3B-bf16 --vae-weights .../vae.safetensors
# x2t_image β image VQA:
HF_HUB_DISABLE_XET=1 uv run python scripts/04_x2t_image_demo.py \
--case 03 \
--lance-weights .../Lance-3B-bf16 \
--vit-weights .../Lance-3B-bf16/vit.safetensors
The demo scripts above are the per-task drivers used to validate the pipelines. The installed console entry point (
lance-mlx generate) wraps the same pipelines β see "Quick start" below. Note the--memory-mode/--lossless-decodeknobs documented in "Hardware" are currently library-API options onLancePipeline.from_pretrained/.generate(and the demo scripts), not yet flags on thelance-mlx generateCLI.
Upstream Lance does not preprocess x2t images with the HF Qwen2.5-VL
smart-resize. Its ViT stream is BucketResize (deterministic center-crop to the
nearest aspect-ratio bucket at resolution_vitΒ² area) + DivisibleCrop(28) +
CLIP normalize, with the vision span attending bidirectionally in prefill and
logits masked to len(tokenizer). Using HF smart-resize instead produces
systematic wrong answers on fine chart reads (the same wrong value every
run β oracle case-02 answered "43" instead of "29%" across 14 runs and three
independent MLX implementations).
UnderstandingPipeline.generate is now upstream-faithful by default
(preprocess="upstream", vision_full_attn=True, resolution="image_768res");
the byte-exact transform lives in pipeline/upstream_und_preprocess.py
(verified max|diff| = 0.0 against upstream's verbatim torchvision code). The
legacy path is available via preprocess="hf".
Parity scope note: with preprocessing byte-exact, position ids exactly equal
to HF get_rope_index, and every ViT stage at cosine 1.000000 vs PyTorch on the
CPU stream, residual answer differences vs a CUDA capture are greedy knife-edge
flips from backend accumulation noise (Apple-GPU fp32 matmul β 8e-4 rel/op,
flash-attn ordering, autocast structure) β token-exact equality with
foreign-hardware captures is not an achievable or meaningful gate for
autoregressive generation. Gate semantically. Diagnostic tooling:
scripts/46_upstream_exact_gate.py (ablation grid), scripts/47_vit_vs_pytorch.py +
scripts/48_vit_stage_bisect.py (ViT PT-parity + stage bisect), scripts/49_crossfeed_pt_vit.py (PT-ViT cross-feed).
Two schedulers ship in this repo (src/lance_mlx/scheduler/solvers.py β DPMSolverPlusPlus2M, with an Euler warm-up fallback):
HF_HUB_DISABLE_XET=1 uv run python scripts/08_t2i_demo.py \
--prompt "A photorealistic tabby cat holding a colorful STOP sign." \
--lance-weights ~/.cache/huggingface/hub/models--mlx-community--Lance-3B-bf16/snapshots/*/ \
--vae-weights ~/.cache/huggingface/hub/models--mlx-community--Lance-3B-bf16/snapshots/*/vae.safetensors \
--scheduler dpm --steps 12
HF_HUB_DISABLE_XET=1 uv run python scripts/08_t2i_demo.py \
--prompt "A photorealistic tabby cat holding a colorful STOP sign." \
--lance-weights ~/.cache/huggingface/hub/models--mlx-community--Lance-3B-bf16/snapshots/*/ \
--vae-weights ~/.cache/huggingface/hub/models--mlx-community--Lance-3B-bf16/snapshots/*/vae.safetensors \
--scheduler euler --steps 30
| Mode | Steps | Generate time | vs baseline |
|---|---|---|---|
| DPM-Solver++(2M) | 12 | ~78s | 2.4Γ |
| Default Euler | 30 | ~196s | β |
Benchmarked on Lance-3B-bf16, 768Β², seed=42, cfg_scale=4.0, M-series Apple Silicon.
See HANDOFF.md for the phased roadmap (start with the β Verified findings (2026-05-19) section β it supersedes earlier guesses). Phase 0 parity-oracle capture runbook lives at Docs/RUNPOD_PHASE0.md. Per-phase technical notes in notes/. Open items / current backlog: BACKLOG.md. Measured memory footprints: LIMITS.md.
The package installs a lance-mlx console script (lance_mlx.__main__:main) with a single
generate subcommand. --weights is required:
uv pip install lance-mlx
# Image generation
lance-mlx generate --task t2i --prompt "..." --weights mlx-community/Lance-3B-bf16
# Image editing
lance-mlx generate --task image_edit --image foo.jpg --prompt "Remove the hat" --weights mlx-community/Lance-3B-bf16
# Image understanding (VQA)
lance-mlx generate --task x2t_image --image foo.png --prompt "What is this?" --weights mlx-community/Lance-3B-bf16
# Video generation
lance-mlx generate --task t2v --prompt "..." --weights mlx-community/Lance-3B-Video-bf16
generate flags: --task {t2i,t2v,image_edit,video_edit,x2t_image,x2t_video} (required),
--weights (required), --prompt, --image, --video, --output (default outputs/),
--seed (42), --steps (30), --cfg (4.0), --timestep-shift (3.5), --resolution (768),
--frames (50), --fps (12).
t2i β text-to-image (768Β²)t2v β text-to-video (480p, 12 fps, β€121 frames)image_edit β instruction-based image editingvideo_edit β instruction-based video editingx2t_image β image understanding / VQA / captioningx2t_video β video understanding / VQA / captioningLLM_UND, LLM_GEN), each initialized from Qwen2.5-VL-3B-Instruct, with per-expert FFN, output projection, and QK-normLLM_UND (autoregressive next-token); Wan2.2 3D causal VAE latent tokens β LLM_GEN (flow-matching velocity prediction)wan2.2_vae.safetensors)Blaizzy/mlx-vlm for the Qwen2.5-VL ViT and autoregressive decode infrastructure (pinned: f2e19deβ¦)Blaizzy/mlx-video for the Wan2.2 VAE and flow-matching sampler (pinned: 87db56aβ¦)Per-task floor with the memory_mode knob (auto / parallel / relay) introduced 2026-06-02:
| RAM | Image (t2i, image_edit) | Video (t2v, video_edit) | VQA (x2t_image) | Notes |
|---|---|---|---|---|
| 8β16 GB | β
bf16 via memory_mode=relay (single-shot per pipeline load) | β
via relay (e.g. 256Β²Γ61f fits in ~8.7 GB) | β
Lance-3B-AWQ-INT4 (3.3 GB LLM) | auto resolves to relay below 18 GiB |
| 24 GB+ | β
bf16 via memory_mode=parallel (reusable pipeline) | β
via parallel | β bf16 or AWQ-INT4 | auto resolves to parallel |
memory_mode=relay produces byte-identical output to parallel (MD5-verified
on real Lance-3B-bf16); it sheds the UND tower after prefill and the GEN tower
before VAE decode, so peak β heaviest single phase rather than the sum. Pipeline
is single-shot β re-prefill needs the UND tower reloaded. parallel keeps
everything resident for repeated calls. The default auto mode chooses by
the GPU/accelerator budget (ws β₯ 18 GiB β parallel, else relay; unresolved β parallel).
(relay/parallel are byte-identical to each other regardless of which VAE decode mode is
selected β the schedule doesn't change the math.) memory_mode is set at
LancePipeline.from_pretrained / honored at .generate; relay is a load-time decision
(GEN tower + VAE load lazily) and resolve_memory_mode lives in model/lance_llm.py.
generate(..., lossless_decode=True) (default) uses a bit-identical streaming decode
(vae_stream.decode_streaming); lossless_decode=False keeps the lossy trapezoidal-blend tiling
(~1.5β4.8 / 255 off the reference). The streaming decode bounds the memory a lossless decode
needs: against the naive whole dec(z) (the only prior bit-identical option) it is lighter in
every measured case β via temporal streaming (flat in length) and spatial halo-tiling β at zero
pixel cost (50-case bit-identity, max|Ξ|=0). Measured true footprints (ri_phys, 16 GB;
method + raw in LIMITS.md and results/decode_lossless/):
| output | whole dec(z) | lossless streaming | lossy blend | on 16 GB |
|---|---|---|---|---|
| 256Β² video β 121f | 15.41 | 8.05 | 12.47 | lossless β lighter + exact |
| 512Β² video β 61f | (OOM) | 12.64 | (OOM >20) | lossless β only path that fits |
| 1024Β² image | 15.60 | 12.17 | 12.53 | lossless β lighter + exact |
| 768Β² video (β₯13f) | (OOM) | >~21 (lower boundΒΉ) | 13.2β16.3 (+~3 swap) | lossy only on 16 GB (lossless: 24 GB+ [P]Β²) |
ΒΉ The lossless 768Β² decode exceeds 16 GB; our only measurement is a watchdog abort at >~21 GB, a lower bound β the exact peak is unmeasured. Β² We have no 24 GB machine to confirm it, so "fits 24 GB" is an unverified inference from the >~21 GB footprint, not a measurement.
When to keep the lossy fallback. Lossy is not a general low-memory mode β at 256Β²/512Β²
video and all images the lossless streaming decode is already the lighter path (and exact), so
there is no reason to use lossy there. Lossy earns its keep in exactly one case: 768Β²
video on a 16 GB Mac, where the lossless decode exceeds ~21 GB and does not fit while the lossy
blend (13.2β16.3 GB, +~3 GB swap at 25f) does. The n_lat β€ 16,128 envelope (256Β² β 768Β²Γ25f)
was established with that lossy decode: a 16 GB machine can generate 768Β²Γ25f but only
lossily decodes it β use lossless_decode=False there, or a bigger-RAM machine for
bit-identical 768Β² video (24 GB+ [P] β inferred from the >~21 GB lower bound; we have not measured
the true peak or confirmed 24 GB suffices). The default lossless_decode=True is left to
repo-owner discretion for main.
.
βββ HANDOFF.md phased port plan (this is the spec)
βββ BACKLOG.md open items / current backlog
βββ LIMITS.md measured memory footprints + method
βββ pyproject.toml hatchling build; uv-managed env (deps pin mlx-vlm/mlx-video)
βββ src/lance_mlx/
β βββ __init__.py
β βββ __main__.py `lance-mlx generate` CLI entry point
β βββ bench.py Timer + RunRecord + JSONL logging
β βββ io.py image/video IO + muxing
β βββ model/
β β βββ lance_llm.py dual-expert MoT backbone + resolve_memory_mode
β β βββ _loader.py weight loading
β β βββ mape.py modality-aware RoPE
β β βββ flow_head.py velocity prediction head
β β βββ time_embedder.py timestep embedding
β β βββ routing.py token modality routing
β β βββ latent_pos_embed.py latent position ids (Phase 5j fix)
β β βββ prefix_cache.py prefix/KV cache for AR decode
β β βββ vae_bridge.py Wan2.2 VAE interface
β β βββ vae_stream.py bit-identical streaming/halo-tiled decode
β βββ pipeline/
β β βββ t2i.py text-to-image flow loop (memory_mode, lossless_decode)
β β βββ t2v.py text-to-video flow loop
β β βββ image_edit.py
β β βββ video_edit.py
β β βββ understanding.py x2t_image + x2t_video AR decode
β β βββ upstream_und_preprocess.py byte-exact upstream x2t preprocessing
β βββ scheduler/
β β βββ solvers.py DPMSolverPlusPlus2M (+ Euler warm-up)
β βββ quant/
β βββ awq.py AWQ port (alpha-search + scale fusion)
β βββ calibrate.py calibration harness
βββ scripts/ ~50 numbered phase scripts + per-task demos:
β βββ 00_capture_oracle.py Phase 0 PyTorch reference capture (cloud GPU)
β βββ 01_inspect_keys.py weight topology audit
β βββ 02_convert.py Lance checkpoint β MLX bf16 conversion
β βββ 06_convert_wan_vae.py Wan2.2 VAE conversion
β βββ 06_publish_hf.py HF upload
β βββ 08_t2i_demo.py t2i driver (--scheduler dpm|euler)
β βββ 13_image_edit_demo.py image_edit driver
β βββ 04_x2t_image_demo.py x2t_image VQA driver
β βββ 10_t2v_demo.py / 14_x2t_video_demo.py / 15_video_edit_demo.py
β βββ 46β49_*.py ViT/upstream parity diagnostics
βββ prompts/
β βββ t2i_eval.json
β βββ t2v_eval.json
β βββ understanding_eval.json
βββ tests/
β βββ fixtures/ Phase 0 PyTorch reference outputs
β βββ test_routing.py
β βββ test_mape.py
β βββ test_lance_model.py
β βββ test_mot_layer.py
β βββ test_scheduler.py
β βββ test_decode_stream.py
β βββ test_prefix_equiv.py
β βββ test_bench.py
β βββ test_model_smoke.py
βββ notes/ phase-by-phase educational notes
βββ Docs/ RUNPOD_PHASE0.md + runbooks
βββ results/ measured footprints / decode validation
βββ vendor/ read-only reference clones
This MLX port: Apache 2.0.
Lance model weights: Apache 2.0 (ByteDance Intelligent Creation Lab). Wan2.2 VAE: Apache 2.0 (Alibaba). Qwen2.5-VL: Apache 2.0 (Alibaba).
See LICENSE and NOTICE for full attribution.
@article{fu2026lance,
title={Lance: Unified Multimodal Modeling by Multi-Task Synergy},
author={Fu, Fengyi and Huang, Mengqi and Wu, Shaojin and others},
journal={arXiv preprint arXiv:2605.18678},
year={2026}
}
Python
100.0%