From fcf0be38bcb65a0e67c1416ce378fa68efa39747 Mon Sep 17 00:00:00 2001 From: Brian Date: Sun, 12 Apr 2026 13:50:34 -0400 Subject: [PATCH] t5 encoder fp8 seems to be working --- Dockerfile | 8 + config.yml | 21 +- configs/lightx2v/wan22_i2v_gguf_distill.json | 35 +++ docker-compose.yml | 1 + requirements.txt | 3 + server/video.py | 42 +++- server/video_models/wan22.py | 235 ++++++++++++++++--- tests/component/sample_avatar.png | Bin 0 -> 64039 bytes tests/component/sample_avatar.webp | Bin 0 -> 17124 bytes tests/component/test_02_wan22_loras.py | 38 ++- tests/component/test_09_gguf_generate.py | 83 +++++++ tests/component/test_10_t5_encode.py | 88 +++++++ tests/unit/test_video_config.py | 18 +- 13 files changed, 505 insertions(+), 67 deletions(-) create mode 100644 configs/lightx2v/wan22_i2v_gguf_distill.json create mode 100644 tests/component/sample_avatar.png create mode 100644 tests/component/sample_avatar.webp create mode 100644 tests/component/test_09_gguf_generate.py create mode 100644 tests/component/test_10_t5_encode.py diff --git a/Dockerfile b/Dockerfile index 1dd8ec3..1e8df1f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -53,6 +53,14 @@ RUN python3.11 -m pip install --no-cache-dir \ "git+https://github.com/ModelTC/LightX2V.git" || \ echo "LightX2V install failed — config.video.enabled must stay false until fixed" # +# sgl-kernel (fp8 T5 encoder acceleration). The PyPI wheel lacks SM120 +# (Blackwell) CUTLASS kernels; use SGLang's cu128 wheel index instead. +# Our wan22.py patches fp8_scaled_mm → torch._scaled_mm at runtime for +# Blackwell GPUs, but the sgl_kernel package itself must still be present. +RUN python3.11 -m pip install --no-cache-dir --no-deps \ + "sgl-kernel @ https://github.com/sgl-project/whl/releases/download/v0.3.14.post1/sgl_kernel-0.3.14.post1%2Bcu128-cp310-abi3-manylinux2014_x86_64.whl" || \ + echo "sgl-kernel install failed — fp8 T5 will fall back to bf16" +# # MuseTalk (audio-driven lip-sync) — same story. RUN python3.11 -m pip install --no-cache-dir \ "git+https://github.com/TMElyralab/MuseTalk.git" || \ diff --git a/config.yml b/config.yml index 597e5e8..ebafaa2 100644 --- a/config.yml +++ b/config.yml @@ -32,16 +32,23 @@ video: casual gestures, natural lighting, soft focus background prompt_reply_words: 18 # max words lifted from reply to inject as {reply_hint} - # Model sources for the video stack. The fp8 e4m3 4-step distilled DIT - # weights from lightx2v/Wan2.2-Distill-Models are ~15 GB each (vs ~28 GB - # bf16) — that's the "save VRAM" path. T5/VAE/tokenizer still come from - # the Wan-AI base repo. Both repos download on first run into - # HF_HOME=/cache/huggingface. + # Model sources for the video stack. T5/VAE/tokenizer come from the + # Wan-AI base repo. DIT weights come from wan22_dit_repo in the format + # specified by wan22_dit_quant_scheme. Both repos download on first run + # into HF_HOME=/cache/huggingface. + # + # Supported dit_quant_scheme values: + # fp8-sgl — fp8 e4m3 safetensors (~15 GB/expert, from lightx2v/Wan2.2-Distill-Models) + # gguf-Q4_K_M — GGUF 4-bit (~9.65 GB/expert, from QuantStack/Wan2.2-I2V-A14B-GGUF) + # gguf-Q8_0 — GGUF 8-bit (~15.4 GB/expert) + # (any gguf- supported by LightX2V — see base_model.py MM_WEIGHT_REGISTER) models: wan22_base_repo: Wan-AI/Wan2.2-I2V-A14B - wan22_fp8_repo: lightx2v/Wan2.2-Distill-Models + wan22_dit_repo: QuantStack/Wan2.2-I2V-A14B-GGUF + wan22_dit_quant_scheme: gguf-Q4_K_M + wan22_t5_quantized: true wan22_model_cls: wan2.2_moe_distill - wan22_config_json: /app/configs/lightx2v/wan22_i2v_fp8_distill.json + wan22_config_json: /app/configs/lightx2v/wan22_i2v_gguf_distill.json musetalk_path: TMElyralab/MuseTalk # LoRAs applied to the fp8 base at load time via runtime switch_lora. diff --git a/configs/lightx2v/wan22_i2v_gguf_distill.json b/configs/lightx2v/wan22_i2v_gguf_distill.json new file mode 100644 index 0000000..f8730cf --- /dev/null +++ b/configs/lightx2v/wan22_i2v_gguf_distill.json @@ -0,0 +1,35 @@ +{ + "_comment": "Wan2.2 i2v MoE 4-step distill, GGUF quantized. Uses QuantStack/Wan2.2-I2V-A14B-GGUF checkpoints instead of fp8 safetensors. GGUF does not support block-level offload so offload_granularity is set to 'model' — the entire DIT is moved to GPU when active. With Q4_K_M (~9.65 GB per expert) this fits comfortably in 24+ GB VRAM. high_noise_quantized_ckpt / low_noise_quantized_ckpt are filled in at runtime by server/video_models/wan22.py. IMPORTANT: GGUF dequantizes to fp16, so you must set DTYPE=FP16 in the container environment.", + + "infer_steps": 4, + "target_video_length": 81, + "text_len": 512, + + "resize_mode": "adaptive", + "resolution": "480p", + "target_height": 480, + "target_width": 480, + "fps": 16, + + "self_attn_1_type": "flash_attn3", + "cross_attn_1_type": "flash_attn3", + "cross_attn_2_type": "flash_attn3", + + "sample_guide_scale": [3.5, 3.5], + "sample_shift": 5.0, + "enable_cfg": false, + + "cpu_offload": true, + "offload_granularity": "model", + "t5_cpu_offload": true, + "vae_cpu_offload": false, + + "use_image_encoder": false, + + "boundary_step_index": 2, + "denoising_step_list": [1000, 750, 500, 250], + + "dit_quantized": true, + "dit_quant_scheme": "gguf-Q4_K_M", + "t5_quantized": false +} diff --git a/docker-compose.yml b/docker-compose.yml index e10d168..f57fa6a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,7 @@ services: - ./configs:/app/configs:ro - ./server:/app/server:ro - ./static:/app/static:ro + - ./tests:/app/tests - ./run.py:/app/run.py:ro deploy: resources: diff --git a/requirements.txt b/requirements.txt index 7bdba86..8108a4b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,6 +20,9 @@ pyyaml imageio[ffmpeg]>=2.34 av>=12.0 pyzmq>=25.0 +gguf>=0.6.0 +# sgl-kernel: installed from SGLang's cu128 wheel index in Dockerfile +# (PyPI version lacks SM120/Blackwell CUDA kernels) # LightX2V (Wan2.2-Lightning) and MuseTalk are installed from source in the # Dockerfile because neither ships a stable PyPI release yet. See lines # "LightX2V from source" / "MuseTalk from source" in Dockerfile. diff --git a/server/video.py b/server/video.py index 9a98b69..4f36bca 100644 --- a/server/video.py +++ b/server/video.py @@ -58,15 +58,18 @@ class VideoConfig: loras: list[LoRASpec] = field(default_factory=list) # Model paths — can be overridden via config.yml.video.models. - # wan22_base_repo : HF repo id (or local dir) providing T5/VAE/tokenizer. - # The bf16 DIT shards in this repo are skipped — we - # replace them with the fp8 files from wan22_fp8_repo. - # wan22_fp8_repo : HF repo id (or local dir) providing the two fp8 e4m3 - # 4-step distilled DIT checkpoints (~15 GB each). - # wan22_config_json: path to the LightX2V inference config template the - # Wan22Pipeline will fill in with absolute ckpt paths. + # wan22_base_repo : HF repo id (or local dir) providing T5/VAE/tokenizer. + # The bf16 DIT shards in this repo are skipped — we + # replace them with quantised files from wan22_dit_repo. + # wan22_dit_repo : HF repo id (or local dir) providing the quantised + # DIT checkpoints (fp8 or GGUF). + # wan22_dit_quant_scheme : quantisation format, e.g. "fp8-sgl" or "gguf-Q4_K_M". + # wan22_config_json : path to the LightX2V inference config template the + # Wan22Pipeline will fill in with absolute ckpt paths. wan22_base_repo: str = "Wan-AI/Wan2.2-I2V-A14B" - wan22_fp8_repo: str = "lightx2v/Wan2.2-Distill-Models" + wan22_dit_repo: str = "lightx2v/Wan2.2-Distill-Models" + wan22_dit_quant_scheme: str = "fp8-sgl" + wan22_t5_quantized: bool = False wan22_config_json: str = "/app/configs/lightx2v/wan22_i2v_fp8_distill.json" wan22_model_cls: str = "wan2.2_moe_distill" musetalk_model_path: str = "TMElyralab/MuseTalk" @@ -121,8 +124,18 @@ class VideoConfig: wan22_base_repo=str( models_raw.get("wan22_base_repo", "Wan-AI/Wan2.2-I2V-A14B") ), - wan22_fp8_repo=str( - models_raw.get("wan22_fp8_repo", "lightx2v/Wan2.2-Distill-Models") + wan22_dit_repo=str( + models_raw.get( + "wan22_dit_repo", + # Backwards compat: fall back to old key name. + models_raw.get("wan22_fp8_repo", "lightx2v/Wan2.2-Distill-Models"), + ) + ), + wan22_dit_quant_scheme=str( + models_raw.get("wan22_dit_quant_scheme", "fp8-sgl") + ), + wan22_t5_quantized=bool( + models_raw.get("wan22_t5_quantized", False) ), wan22_config_json=str( models_raw.get( @@ -204,16 +217,19 @@ class VideoEngine: from server.video_models.musetalk import MuseTalkEngine log.info( - "Loading Wan2.2-Lightning fp8 pipeline (base=%s, fp8=%s)...", - self.cfg.wan22_base_repo, self.cfg.wan22_fp8_repo, + "Loading Wan2.2 pipeline (base=%s, dit=%s, quant=%s)...", + self.cfg.wan22_base_repo, self.cfg.wan22_dit_repo, + self.cfg.wan22_dit_quant_scheme, ) self._wan22 = Wan22Pipeline( base_repo=self.cfg.wan22_base_repo, - fp8_repo=self.cfg.wan22_fp8_repo, + dit_repo=self.cfg.wan22_dit_repo, config_json=self.cfg.wan22_config_json, model_cls=self.cfg.wan22_model_cls, resolution=self.cfg.resolution, fps=self.cfg.fps, + dit_quant_scheme=self.cfg.wan22_dit_quant_scheme, + t5_quantized=self.cfg.wan22_t5_quantized, ) if self.cfg.loras: self._wan22.load_loras(self.cfg.loras) diff --git a/server/video_models/wan22.py b/server/video_models/wan22.py index 327bbae..3ecf5bd 100644 --- a/server/video_models/wan22.py +++ b/server/video_models/wan22.py @@ -1,4 +1,4 @@ -"""Wan2.2-Lightning fp8 image-to-video wrapper via LightX2V. +"""Wan2.2-Lightning image-to-video wrapper via LightX2V. This wrapper targets LightX2V's actual Python entry points (verified against the upstream ``lightx2v.infer.main`` in ModelTC/LightX2V@main): @@ -25,10 +25,12 @@ Two HuggingFace repos are consumed on first run (cached under HF_HOME): - Wan-AI/Wan2.2-I2V-A14B — T5 encoder, VAE, tokenizer/config only. The bf16 DIT shards under high_noise_model/ and low_noise_model/ are SKIPPED via - ignore_patterns — we replace them with fp8. - - lightx2v/Wan2.2-Distill-Models — exactly two safetensors files: - the fp8 e4m3 4-step distilled high/low - noise DIT checkpoints (~15 GB each). + ignore_patterns — we replace them with + quantised checkpoints from dit_repo. + - dit_repo (configurable) — quantised DIT checkpoints. Supported + formats: + * fp8 safetensors (lightx2v/Wan2.2-Distill-Models) + * GGUF (QuantStack/Wan2.2-I2V-A14B-GGUF) """ from __future__ import annotations @@ -47,13 +49,22 @@ if TYPE_CHECKING: log = logging.getLogger(__name__) +# --- fp8 distill filenames -------------------------------------------------- FP8_HIGH_NOISE_FILE = "wan2.2_i2v_A14b_high_noise_scaled_fp8_e4m3_lightx2v_4step.safetensors" FP8_LOW_NOISE_FILE = "wan2.2_i2v_A14b_low_noise_scaled_fp8_e4m3_lightx2v_4step.safetensors" +# --- GGUF filenames (QuantStack layout: HighNoise/.gguf) --------------- +GGUF_HIGH_NOISE_TEMPLATE = "HighNoise/Wan2.2-I2V-A14B-HighNoise-{quant}.gguf" +GGUF_LOW_NOISE_TEMPLATE = "LowNoise/Wan2.2-I2V-A14B-LowNoise-{quant}.gguf" + +# --- fp8 T5 encoder (lightx2v/Encoders repo) -------------------------------- +T5_FP8_REPO = "lightx2v/Encoders" +T5_FP8_FILE = "models_t5_umt5-xxl-enc-fp8.safetensors" + # The Wan-AI base repo ships bf16 DIT weight shards (~28 GB) alongside the -# T5/VAE/tokenizer support files (~12 GB). We only need the latter — the fp8 -# files from the distill repo replace the DIT weights entirely. We must keep -# the config.json / index.json metadata under high_noise_model/ and +# T5/VAE/tokenizer support files (~12 GB). We only need the latter — the +# quantised files from dit_repo replace the DIT weights entirely. We must +# keep the config.json / index.json metadata under high_noise_model/ and # low_noise_model/ (LightX2V's set_config reads architecture params like # ``dim`` from them) and the tokenizer files under google/. BASE_REPO_IGNORE_PATTERNS = [ @@ -66,8 +77,68 @@ BASE_REPO_IGNORE_PATTERNS = [ ] +def _patch_fp8_scaled_mm_for_blackwell() -> None: + """Replace sgl_kernel.fp8_scaled_mm with torch._scaled_mm on Blackwell. + + sgl_kernel's CUTLASS-based fp8 GEMM doesn't ship SM120 kernels yet. + PyTorch 2.8+'s native ``_scaled_mm`` works on all architectures + including Blackwell. This patch is idempotent. + """ + try: + import sgl_kernel # type: ignore[import-not-found] + except ImportError: + return # no sgl_kernel → fp8 T5 not in use + + if getattr(sgl_kernel, "_fp8_patched_for_blackwell", False): + return + + import torch + + if not torch.cuda.is_available(): + return + + cap = torch.cuda.get_device_capability() + if cap[0] < 12: + return # only patch on Blackwell+ + + _orig = sgl_kernel.fp8_scaled_mm + + def _torch_fp8_scaled_mm( + a: torch.Tensor, + b: torch.Tensor, + a_scale: torch.Tensor, + b_scale: torch.Tensor, + out_dtype: torch.dtype, + bias: torch.Tensor | None = None, + ) -> torch.Tensor: + # torch._scaled_mm expects (M,K) @ (N,K).t() with: + # scale_a: scalar or (M,1) + # scale_b: scalar or (1,N) + # sgl_kernel provides scale_b as (N,1) — transpose it. + if b_scale.dim() == 2 and b_scale.shape[1] == 1: + b_scale = b_scale.t() + # _scaled_mm requires B to be column-major (stride(0)==1). + bt = b.t().contiguous().t() + out = torch._scaled_mm( + a, bt, + scale_a=a_scale, scale_b=b_scale, + out_dtype=out_dtype, bias=bias, + ) + return out + + sgl_kernel.fp8_scaled_mm = _torch_fp8_scaled_mm + sgl_kernel._fp8_patched_for_blackwell = True + log.info("Patched sgl_kernel.fp8_scaled_mm → torch._scaled_mm for Blackwell (SM%d%d).", *cap) + + class Wan22Pipeline: - """Wrapper around LightX2V's Wan2.2 MoE distill runner using fp8 weights. + """Wrapper around LightX2V's Wan2.2 MoE distill runner. + + Supports two DIT quantisation formats: + * **fp8** — ``dit_quant_scheme="fp8-sgl"`` (default, from + ``lightx2v/Wan2.2-Distill-Models``) + * **GGUF** — ``dit_quant_scheme="gguf-Q4_K_M"`` (or any quant level, + from ``QuantStack/Wan2.2-I2V-A14B-GGUF``) Constructor downloads (if needed) both HF repos, writes a runtime JSON config with absolute ckpt paths, then drives ``lightx2v.infer.init_runner``. @@ -77,23 +148,34 @@ class Wan22Pipeline: def __init__( self, base_repo: str, - fp8_repo: str, + dit_repo: str, config_json: str, model_cls: str = "wan2.2_moe_distill", resolution: int = 480, fps: int = 16, + dit_quant_scheme: str = "fp8-sgl", + t5_quantized: bool = False, ): self.base_repo = base_repo - self.fp8_repo = fp8_repo + self.dit_repo = dit_repo self.config_json_template = config_json self.model_cls = model_cls self.resolution = resolution self.fps = fps + self.dit_quant_scheme = dit_quant_scheme + self.t5_quantized = t5_quantized self._applied_loras: list[LoRASpec] = [] - # 1. Resolve / download base repo (T5/VAE/config) and fp8 DIT ckpts. + self._is_gguf = dit_quant_scheme.startswith("gguf-") + + # 1. Resolve / download base repo (T5/VAE/config) and DIT ckpts. self._model_root = self._ensure_base_repo(base_repo) - self._fp8_high, self._fp8_low = self._ensure_fp8_checkpoints(fp8_repo) + self._dit_high, self._dit_low = self._ensure_dit_checkpoints( + dit_repo, dit_quant_scheme, + ) + self._t5_fp8_ckpt = ( + self._ensure_t5_fp8() if t5_quantized else None + ) # 2. Materialize a runtime JSON config with absolute ckpt paths. self._runtime_json_path = self._build_runtime_config() @@ -105,13 +187,17 @@ class Wan22Pipeline: config_json=self._runtime_json_path, ) - # 4. set_config → init_runner. Runner construction triggers weight load. - # Imports are scoped here so ``import server.video_models.wan22`` - # never pulls in lightx2v (tests can import this module on CPU). + # 4. Import LightX2V (scoped here so ``import server.video_models.wan22`` + # never pulls in lightx2v — tests can import this module on CPU). from lightx2v.utils.set_config import set_config # type: ignore[import-not-found] from lightx2v.utils.input_info import init_empty_input_info # type: ignore[import-not-found] from lightx2v.infer import init_runner # type: ignore[import-not-found] + _patch_fp8_scaled_mm_for_blackwell() + + # 5. Load all models under default DTYPE=BF16 so T5 (which is + # hardcoded to bf16 weights) initialises its offload buffers + # correctly. We flip to FP16 *after* init_runner completes. log.info("LightX2V set_config (model_cls=%s, model_path=%s)", model_cls, self._model_root) self._config = set_config(args) @@ -124,6 +210,52 @@ class Wan22Pipeline: self._runner = init_runner(self._config) log.info("LightX2V runner loaded; weights resident.") + # 6. GGUF: switch global DTYPE to FP16 for inference. GGUF DIT + # dequantises to fp16, and many intermediate tensors inside the + # DIT forward pass are allocated via GET_DTYPE(). The T5 encoder + # is wrapped to temporarily restore BF16 during its forward. + if self._is_gguf: + os.environ["DTYPE"] = "FP16" + from lightx2v.utils.envs import GET_DTYPE # type: ignore[import-not-found] + GET_DTYPE.cache_clear() + log.info("Set DTYPE=FP16 for GGUF (GET_DTYPE()=%s)", GET_DTYPE()) + self._patch_t5_dtype_for_gguf() + + # --- GGUF dtype compatibility patch ---------------------------------------- + + def _patch_t5_dtype_for_gguf(self) -> None: + """Wrap the T5 encoder so it temporarily restores DTYPE=BF16. + + The T5 encoder is hardcoded to bfloat16 weights (wan_runner.py). When + the global DTYPE is FP16 (required for GGUF DIT), the T5's CPU-offload + path breaks because intermediate tensor dtypes no longer match the bf16 + weights. We wrap ``run_text_encoder`` to temporarily flip GET_DTYPE() + back to bf16, then restore fp16 before the DIT runs. + """ + import os + import types + from lightx2v.utils.envs import GET_DTYPE, GET_SENSITIVE_DTYPE # type: ignore[import-not-found] + + runner = self._runner + orig_run_text_encoder = runner.run_text_encoder.__func__ + + def bf16_text_encoder(self_runner, *args, **kwargs): + # Flip DTYPE to BF16 so the T5 encoder works with its bf16 weights. + os.environ["DTYPE"] = "BF16" + GET_DTYPE.cache_clear() + GET_SENSITIVE_DTYPE.cache_clear() + try: + result = orig_run_text_encoder(self_runner, *args, **kwargs) + finally: + # Restore FP16 for the DIT / rest of the pipeline. + os.environ["DTYPE"] = "FP16" + GET_DTYPE.cache_clear() + GET_SENSITIVE_DTYPE.cache_clear() + return result + + runner.run_text_encoder = types.MethodType(bf16_text_encoder, runner) + log.info("Patched T5 encoder to use BF16 under GGUF FP16 pipeline.") + # --- Weight provisioning ------------------------------------------------- @staticmethod @@ -132,7 +264,7 @@ class Wan22Pipeline: If ``base_repo`` is already a local directory, use it as-is. Otherwise snapshot_download the HF repo into HF_HOME, skipping the bf16 DIT - shards (they're replaced by the fp8 files). + shards (they're replaced by the quantised files). """ if os.path.isdir(base_repo): return base_repo @@ -145,42 +277,75 @@ class Wan22Pipeline: ) @staticmethod - def _ensure_fp8_checkpoints(fp8_repo: str) -> tuple[str, str]: - """Return (high_noise_path, low_noise_path) for the fp8 i2v MoE pair. + def _ensure_dit_checkpoints( + dit_repo: str, + dit_quant_scheme: str, + ) -> tuple[str, str]: + """Return (high_noise_path, low_noise_path) for the DIT pair. - - If ``fp8_repo`` is a local directory, expect both files inside it. - - Otherwise treat it as a HF repo id and download only the two files - we need (not the ~150 GB of other variants in that repo). + Supports both fp8 safetensors and GGUF formats. """ - if not fp8_repo: - raise ValueError("fp8_repo must be a HF repo id or local directory.") - if os.path.isdir(fp8_repo): - high = os.path.join(fp8_repo, FP8_HIGH_NOISE_FILE) - low = os.path.join(fp8_repo, FP8_LOW_NOISE_FILE) + if not dit_repo: + raise ValueError("dit_repo must be a HF repo id or local directory.") + + is_gguf = dit_quant_scheme.startswith("gguf-") + + if is_gguf: + # Extract quant level, e.g. "gguf-Q4_K_M" → "Q4_K_M" + quant = dit_quant_scheme.replace("gguf-", "") + high_file = GGUF_HIGH_NOISE_TEMPLATE.format(quant=quant) + low_file = GGUF_LOW_NOISE_TEMPLATE.format(quant=quant) + else: + high_file = FP8_HIGH_NOISE_FILE + low_file = FP8_LOW_NOISE_FILE + + # Local directory? + if os.path.isdir(dit_repo): + high = os.path.join(dit_repo, high_file) + low = os.path.join(dit_repo, low_file) if not (os.path.isfile(high) and os.path.isfile(low)): raise FileNotFoundError( - f"fp8 checkpoints not found in {fp8_repo}: expected " - f"{FP8_HIGH_NOISE_FILE} and {FP8_LOW_NOISE_FILE}" + f"DIT checkpoints not found in {dit_repo}: expected " + f"{high_file} and {low_file}" ) return high, low + + # HuggingFace download. from huggingface_hub import hf_hub_download - log.info("Downloading fp8 i2v DIT checkpoints from %s ...", fp8_repo) - high = hf_hub_download(repo_id=fp8_repo, filename=FP8_HIGH_NOISE_FILE) - low = hf_hub_download(repo_id=fp8_repo, filename=FP8_LOW_NOISE_FILE) + log.info("Downloading %s DIT checkpoints from %s ...", + dit_quant_scheme, dit_repo) + high = hf_hub_download(repo_id=dit_repo, filename=high_file) + low = hf_hub_download(repo_id=dit_repo, filename=low_file) return high, low + @staticmethod + def _ensure_t5_fp8() -> str: + """Download the fp8 T5 encoder from lightx2v/Encoders (if not cached). + + Returns the local path to the safetensors file (~6 GB). + """ + from huggingface_hub import hf_hub_download + log.info("Downloading fp8 T5 encoder from %s ...", T5_FP8_REPO) + return hf_hub_download(repo_id=T5_FP8_REPO, filename=T5_FP8_FILE) + def _build_runtime_config(self) -> str: """Load the template JSON, inject absolute ckpt paths, persist to temp.""" with open(self.config_json_template, "r", encoding="utf-8") as f: cfg = json.load(f) # Drop editorial comments before passing to LightX2V. cfg.pop("_comment", None) - cfg["high_noise_quantized_ckpt"] = self._fp8_high - cfg["low_noise_quantized_ckpt"] = self._fp8_low + cfg["high_noise_quantized_ckpt"] = self._dit_high + cfg["low_noise_quantized_ckpt"] = self._dit_low cfg.setdefault("fps", self.fps) + # T5 fp8 quantization. + if self._t5_fp8_ckpt: + cfg["t5_quantized"] = True + cfg["t5_quant_scheme"] = "fp8-sgl" + cfg["t5_quantized_ckpt"] = self._t5_fp8_ckpt + tmp = tempfile.NamedTemporaryFile( - prefix="wan22_fp8_", suffix=".json", + prefix="wan22_dit_", suffix=".json", mode="w", delete=False, encoding="utf-8", ) json.dump(cfg, tmp, indent=2) diff --git a/tests/component/sample_avatar.png b/tests/component/sample_avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..d0844688d6ad6ff306942d64f9921ccae61b8be0 GIT binary patch literal 64039 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 A0{{R3 literal 0 HcmV?d00001 diff --git a/tests/component/test_02_wan22_loras.py b/tests/component/test_02_wan22_loras.py index f41e857..790b35c 100644 --- a/tests/component/test_02_wan22_loras.py +++ b/tests/component/test_02_wan22_loras.py @@ -1,15 +1,22 @@ -"""Phase 2 component test: Wan2.2-Lightning fp8 pipeline + LoRA stacking. +"""Phase 2 component test: Wan2.2 pipeline + LoRA stacking. Verifies: -- ``Wan22Pipeline`` loads successfully against the fp8 distill path - (exercises the real LightX2V set_config → init_runner flow). +- ``Wan22Pipeline`` loads successfully (exercises the real LightX2V + set_config -> init_runner flow). - ``load_loras`` / ``unload_loras`` survive with the two user LoRAs at ``/cache/loras/wan22-[HL]-e8.safetensors``. -Requires GPU and a first-run download of both HF repos (base support files -~12 GB, fp8 DIT ~30 GB). If LightX2V isn't installed the test is skipped. +Supports both fp8 and GGUF DIT quantisation. Set the ``DIT_QUANT`` +environment variable to switch (default: ``fp8-sgl``). -Run: + DIT_QUANT=gguf-Q4_K_M docker compose exec voice-chat \ + python -m tests.component.test_02_wan22_loras + +Requires GPU and a first-run download of both HF repos (base support files +~12 GB, DIT size depends on quant — fp8 ~30 GB, GGUF Q4_K_M ~19 GB). +If LightX2V isn't installed the test is skipped. + +Run (default fp8): docker compose exec voice-chat python -m tests.component.test_02_wan22_loras """ from __future__ import annotations @@ -21,7 +28,17 @@ from tests.component._common import get_logger log = get_logger("test_02") -CONFIG_JSON = "/app/configs/lightx2v/wan22_i2v_fp8_distill.json" +# --- Quant-dependent defaults ------------------------------------------------ + +DIT_QUANT = os.environ.get("DIT_QUANT", "fp8-sgl") + +if DIT_QUANT.startswith("gguf-"): + CONFIG_JSON = "/app/configs/lightx2v/wan22_i2v_gguf_distill.json" + DIT_REPO = "QuantStack/Wan2.2-I2V-A14B-GGUF" +else: + CONFIG_JSON = "/app/configs/lightx2v/wan22_i2v_fp8_distill.json" + DIT_REPO = "lightx2v/Wan2.2-Distill-Models" + LORA_HIGH = "/cache/loras/wan22-H-e8.safetensors" LORA_LOW = "/cache/loras/wan22-L-e8.safetensors" @@ -37,15 +54,16 @@ def run(): from server.video import LoRASpec log.info("[case 1] Instantiate Wan22Pipeline " - "(first run downloads ~42 GB total)...") + "(quant=%s, dit_repo=%s)...", DIT_QUANT, DIT_REPO) try: pipe = Wan22Pipeline( base_repo="Wan-AI/Wan2.2-I2V-A14B", - fp8_repo="lightx2v/Wan2.2-Distill-Models", + dit_repo=DIT_REPO, config_json=CONFIG_JSON, model_cls="wan2.2_moe_distill", resolution=480, fps=16, + dit_quant_scheme=DIT_QUANT, ) except Exception as e: log.error("FAIL: Wan22Pipeline construction raised: %s", e) @@ -56,7 +74,7 @@ def run(): log.info(" PASS: pipeline constructed") # --- LoRAs --- - log.info("[case 2] load_loras with empty list → no-op") + log.info("[case 2] load_loras with empty list -> no-op") pipe.load_loras([]) log.info(" PASS") diff --git a/tests/component/test_09_gguf_generate.py b/tests/component/test_09_gguf_generate.py new file mode 100644 index 0000000..bb56404 --- /dev/null +++ b/tests/component/test_09_gguf_generate.py @@ -0,0 +1,83 @@ +"""Quick smoke test: generate a video clip with the GGUF pipeline. + +Calls Wan22Pipeline.generate_i2v directly (no MuseTalk, no VideoEngine) +and writes the result to tests/component/_out/phase9_gguf.mp4. + +Run: + docker compose exec -e DIT_QUANT=gguf-Q4_K_M voice-chat \ + python -m tests.component.test_09_gguf_generate +""" +from __future__ import annotations + +import os +import sys + +from tests.component._common import ensure_sample_avatar, get_logger, write_bytes + +log = get_logger("test_09") + +DIT_QUANT = os.environ.get("DIT_QUANT", "gguf-Q4_K_M") + +if DIT_QUANT.startswith("gguf-"): + CONFIG_JSON = "/app/configs/lightx2v/wan22_i2v_gguf_distill.json" + DIT_REPO = "QuantStack/Wan2.2-I2V-A14B-GGUF" +else: + CONFIG_JSON = "/app/configs/lightx2v/wan22_i2v_fp8_distill.json" + DIT_REPO = "lightx2v/Wan2.2-Distill-Models" + + +def run(): + try: + from server.video_models.wan22 import Wan22Pipeline + except ImportError as e: + log.error("Import failed: %s", e) + sys.exit(0) + + avatar = ensure_sample_avatar() + log.info("Avatar: %s", avatar) + + log.info("Building pipeline (quant=%s)...", DIT_QUANT) + pipe = Wan22Pipeline( + base_repo="Wan-AI/Wan2.2-I2V-A14B", + dit_repo=DIT_REPO, + config_json=CONFIG_JSON, + model_cls="wan2.2_moe_distill", + resolution=480, + fps=16, + dit_quant_scheme=DIT_QUANT, + t5_quantized=True, + ) + log.info("Pipeline ready.") + + # Debug: verify DTYPE is set correctly for GGUF + from lightx2v.utils.envs import GET_DTYPE + log.info("GET_DTYPE() = %s (DTYPE env = %s)", GET_DTYPE(), os.environ.get("DTYPE")) + + log.info("Generating 3-second i2v clip...") + frames = pipe.generate_i2v( + image_path=avatar, + prompt="a person looking at the camera, natural lighting, soft focus background", + seconds=3, + seed=42, + ) + log.info("Got frames: shape=%s dtype=%s", frames.shape, frames.dtype) + + # Encode to MP4 + import imageio.v3 as iio + import tempfile + + with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tf: + tmp = tf.name + try: + iio.imwrite(tmp, frames, fps=16, codec="libx264") + with open(tmp, "rb") as f: + mp4_bytes = f.read() + finally: + os.remove(tmp) + + out = write_bytes("phase9_gguf.mp4", mp4_bytes) + log.info("PASS: video written to %s (%d bytes, %d frames)", out, len(mp4_bytes), frames.shape[0]) + + +if __name__ == "__main__": + run() diff --git a/tests/component/test_10_t5_encode.py b/tests/component/test_10_t5_encode.py new file mode 100644 index 0000000..bf7084a --- /dev/null +++ b/tests/component/test_10_t5_encode.py @@ -0,0 +1,88 @@ +"""Smoke test: T5 text encoding under GGUF pipeline. + +Builds the Wan22Pipeline (loads all weights including DIT) but only +exercises the T5 encoder — no image-to-video generation. Validates that +the DTYPE=FP16 ↔ BF16 patching lets T5 encode a prompt successfully. + +Run: + docker compose exec -e DIT_QUANT=gguf-Q4_K_M voice-chat \ + python -m tests.component.test_10_t5_encode +""" +from __future__ import annotations + +import os +import sys + +from tests.component._common import get_logger + +log = get_logger("test_10") + +DIT_QUANT = os.environ.get("DIT_QUANT", "gguf-Q4_K_M") + +if DIT_QUANT.startswith("gguf-"): + CONFIG_JSON = "/app/configs/lightx2v/wan22_i2v_gguf_distill.json" + DIT_REPO = "QuantStack/Wan2.2-I2V-A14B-GGUF" +else: + CONFIG_JSON = "/app/configs/lightx2v/wan22_i2v_fp8_distill.json" + DIT_REPO = "lightx2v/Wan2.2-Distill-Models" + + +def run(): + try: + from server.video_models.wan22 import Wan22Pipeline + except ImportError as e: + log.error("Import failed: %s", e) + sys.exit(0) + + log.info("Building pipeline (quant=%s) — this loads T5 + DIT weights...", DIT_QUANT) + pipe = Wan22Pipeline( + base_repo="Wan-AI/Wan2.2-I2V-A14B", + dit_repo=DIT_REPO, + config_json=CONFIG_JSON, + model_cls="wan2.2_moe_distill", + resolution=480, + fps=16, + dit_quant_scheme=DIT_QUANT, + t5_quantized=True, + ) + log.info("Pipeline ready.") + + # Check DTYPE state after init + from lightx2v.utils.envs import GET_DTYPE + log.info("GET_DTYPE() = %s (DTYPE env = %s)", GET_DTYPE(), os.environ.get("DTYPE")) + + # Run only the T5 text encoder + runner = pipe._runner + prompt = "a person looking at the camera, natural lighting" + + log.info("Running T5 text encoder on prompt: %r", prompt) + import copy + input_info = copy.deepcopy(pipe._input_info_template) + input_info.prompt = prompt + + runner.run_text_encoder(input_info) + log.info("T5 encode complete.") + + # Inspect output — check all dataclass fields for tensor results + import torch + for attr in vars(input_info): + val = getattr(input_info, attr) + if isinstance(val, torch.Tensor): + log.info(" %s: shape=%s dtype=%s device=%s", attr, val.shape, val.dtype, val.device) + + # Verify DTYPE is back to FP16 after T5 runs (if GGUF) + if DIT_QUANT.startswith("gguf-"): + current = GET_DTYPE() + import torch + expected = torch.float16 + if current == expected: + log.info("PASS: DTYPE correctly restored to FP16 after T5 encode.") + else: + log.error("FAIL: DTYPE is %s after T5, expected %s", current, expected) + sys.exit(1) + + log.info("PASS: T5 encoding succeeded under %s pipeline.", DIT_QUANT) + + +if __name__ == "__main__": + run() diff --git a/tests/unit/test_video_config.py b/tests/unit/test_video_config.py index 54e5333..9eb7b56 100644 --- a/tests/unit/test_video_config.py +++ b/tests/unit/test_video_config.py @@ -105,7 +105,8 @@ def test_models_section_override(): { "models": { "wan22_base_repo": "/local/weights/wan22", - "wan22_fp8_repo": "/local/weights/wan22-fp8", + "wan22_dit_repo": "/local/weights/wan22-dit", + "wan22_dit_quant_scheme": "gguf-Q4_K_M", "wan22_config_json": "/local/cfg/fp8.json", "wan22_model_cls": "wan2.2_moe", "musetalk_path": "/local/weights/musetalk", @@ -113,7 +114,20 @@ def test_models_section_override(): } ) assert cfg.wan22_base_repo == "/local/weights/wan22" - assert cfg.wan22_fp8_repo == "/local/weights/wan22-fp8" + assert cfg.wan22_dit_repo == "/local/weights/wan22-dit" + assert cfg.wan22_dit_quant_scheme == "gguf-Q4_K_M" assert cfg.wan22_config_json == "/local/cfg/fp8.json" assert cfg.wan22_model_cls == "wan2.2_moe" assert cfg.musetalk_model_path == "/local/weights/musetalk" + + +def test_models_section_backwards_compat_fp8_repo(): + """Old config key wan22_fp8_repo still works via fallback.""" + cfg = VideoConfig.from_dict( + { + "models": { + "wan22_fp8_repo": "/local/weights/wan22-fp8", + } + } + ) + assert cfg.wan22_dit_repo == "/local/weights/wan22-fp8"