Skip to content

Support LingBot-Video (MoE-30B-A3B) - #1545

Open
NancyFyong wants to merge 37 commits into
modelscope:mainfrom
NancyFyong:lingbot_moe
Open

Support LingBot-Video (MoE-30B-A3B)#1545
NancyFyong wants to merge 37 commits into
modelscope:mainfrom
NancyFyong:lingbot_moe

Conversation

@NancyFyong

@NancyFyong NancyFyong commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

LingBot-Video (MoE-30B-A3B) Integration

Summary

Add the MoE variant of LingBot-VideoMoE-30B-A3B, 30.1B total parameters with ~2.9B activated per token — on top of the Dense-1.3B integration merged in #1539. Supported:

  • ✅ Text-to-video inference
  • ✅ Image-to-video inference (condition on a first frame via input_image)
  • ✅ Text-to-image inference (same pipeline and DiT with num_frames=1)
  • ✅ Video-to-video (input_video + denoising_strength)
  • ✅ Low VRAM inference (FP8 weights + disk offload) for all three tasks
  • ✅ LoRA training and full-parameter training (T2V / TI2V)

MoE-30B-A3B is a drop-in second checkpoint for LingBotVideoPipeline: one checkpoint serves all three tasks, exactly like Dense-1.3B. The pipeline, VAE, text encoder and processor are unchanged — this PR adds the sparse-MoE path inside LingBotVideoDiT, one registry entry, the VRAM map entries the sparse stack needs, and the examples.

Model Components

Component File Class Params
DiT diffsynth/models/lingbot_video_dit.py LingBotVideoDiT (MoE FFN path) 30.1B total / ~2.9B active
Text Encoder diffsynth/models/krea2_text_encoder.py (reused) Krea2TextEncoder (Qwen3-VL-4B-Instruct) 4.44B
VAE diffsynth/models/qwen_image_vae.py (reused) QwenImageVAE (8x spatial / 4x temporal) 0.13B
Pipeline diffsynth/pipelines/lingbot_video.py (reused) LingBotVideoPipeline -

The MoE FFN is built from LingBotVideoRouter, LingBotVideoGroupedExperts and LingBotVideoSparseMoeBlock. Routing follows the checkpoint config: 48 blocks, 128 routed experts with top-8 selection, group-limited routing (4 groups, top-2 groups), sigmoid gate with a selection-only e_score_correction_bias, norm_topk_prob, routed_scaling_factor=2.5, and 1 shared expert. Expert matmuls use torch._grouped_mm on CUDA with a per-expert loop fallback elsewhere.

Registered in diffsynth/configs/model_configs.py under lingbot_video_series:

{
    # Example: ModelConfig(model_id="Robbyant/lingbot-video-moe-30b-a3b", origin_file_pattern="transformer/diffusion_pytorch_model*.safetensors")
    "model_hash": "65b83aa625cd362ff5ff3409fb367a6f",
    "model_name": "lingbot_video_dit",
    "model_class": "diffsynth.models.lingbot_video_dit.LingBotVideoDiT",
    "state_dict_converter": "diffsynth.utils.state_dict_converters.lingbot_video_dit.LingBotVideoDiTStateDictConverter",
    "extra_kwargs": {'depth': 48, 'axes_lens': (4096, 512, 512), 'num_experts': 128, 'moe_intermediate_size': 768, 'n_group': 4, 'topk_group': 2, 'n_shared_experts': 1, 'routed_scaling_factor': 2.5},
}

Examples

All paths are relative to examples/lingbot_video/:

Task Inference Low VRAM inference LoRA training Full training
T2V (+ V2V) model_inference/lingbot-video-moe-30b-a3b_t2v.py model_inference_low_vram/lingbot-video-moe-30b-a3b_t2v.py model_training/lora/lingbot-video-moe-30b-a3b_t2v.sh model_training/full/lingbot-video-moe-30b-a3b_t2v.sh
TI2V model_inference/lingbot-video-moe-30b-a3b_ti2v.py model_inference_low_vram/lingbot-video-moe-30b-a3b_ti2v.py model_training/lora/lingbot-video-moe-30b-a3b_ti2v.sh model_training/full/lingbot-video-moe-30b-a3b_ti2v.sh
T2I model_inference/lingbot-video-moe-30b-a3b_t2i.py model_inference_low_vram/lingbot-video-moe-30b-a3b_t2i.py - -

Post-training validation scripts live in model_training/validate_lora/ and model_training/validate_full/, and the training scripts launch through model_training/full/accelerate_config_moe.yaml.

Prompts and the TI2V condition frame come from the published example dataset directories that Dense already uses (lingbot_video/lingbot-video-dense-1.3b_{t2v,ti2v,t2i}/), so no new dataset upload is needed. Docs: docs/en/Model_Details/LingBot-Video.md, docs/zh/Model_Details/LingBot-Video.md; both README example tables gain three MoE rows.

Results

Text-to-videolingbot-video-moe-30b-a3b_t2v.py

Video-to-video — same script, V2V section (denoising_strength=0.7, seed 1)

Image-to-videolingbot-video-moe-30b-a3b_ti2v.py

Text-to-imagelingbot-video-moe-30b-a3b_t2i.py

Low VRAM inferencemodel_inference_low_vram/, same prompts and seeds

Training — outputs of validate_lora/ and validate_full/ after running the training scripts

Dependencies

No new packages. torch._grouped_mm is used opportunistically behind a fallback, so no minimum-version bump is needed.

Notes

  • VRAM. 480x832x81 in bf16 peaks at ~80 GiB; the low-VRAM examples run the same workloads at ~42 GiB with essentially unchanged step time (16.6 -> 16.9 s/it). Those examples set vram_limit to the whole card, so lowering it trades speed for memory: with vram_limit=12.0 the single-frame T2I run peaks at 12.2 GiB and the 81-frame T2V run at 15.5 GiB (38 s/it). The DiT alone is ~57 GB on disk.
  • Low-VRAM module map. LingBotVideoBlock, LingBotVideoRouter and LingBotVideoGroupedExperts are registered as AutoWrappedNonRecurseModule, so each module manages only its own bare parameters and the 128 experts per layer stay streamable instead of being deep-copied as one 1.25 GB block. That wrapper leaves parameter casting to the model, so the router weight, the correction bias and the three expert tensors are cast to the token device and dtype at use, the same way scale_shift_table already is. e_score_correction_bias is an nn.Parameter(requires_grad=False) rather than a buffer so that skip_model_initialization restores it too. Dense also goes through this map; its bf16 and low-VRAM outputs were checked after the change.
  • Captions. LingBot-Video is trained on structured JSON captions, and MoE-30B-A3B is markedly less tolerant of plain prose than Dense-1.3B — a free-form sentence degrades into texture noise. The examples load the structured captions shipped with the example dataset; see the prompt-rewriting section in the docs.
  • Training. The LoRA and full-parameter scripts launch through examples/lingbot_video/model_training/full/accelerate_config_moe.yaml (DeepSpeed ZeRO-2, optimizer CPU offload, bf16), following the wanvideo/Wan2.1-T2V-14B precedent where the LoRA script reuses the full-training config. LoRA runs at 28.3 s/it with 88.5 GiB per GPU on 4 GPUs. The full-parameter scripts additionally enable --use_gradient_checkpointing_offload, without which the 30B backward pass does not fit. --lora_target_modules "to_q,to_k,to_v,to_out" only reaches the attention projections: the routed experts and the router are stored as bare parameter tensors for grouped matmul, so LoRA leaves them frozen and full-parameter training is needed to update the expert stack — noted in the docs.
  • Refiner. The release also publishes a refiner/ DiT for a second-stage refinement pass. It is not wired into LingBotVideoPipeline and is intentionally out of scope here; the examples and docs use the base transformer/ only.

NancyFyong and others added 30 commits July 25, 2026 15:44
Model code:
- LingBotVideoDiT (diffsynth/models/lingbot_video_dit.py) video denoiser
- Qwen3-VL text-encoder wrapper (lingbot_video_text_encoder.py)
- T2V/V2V pipeline (diffsynth/pipelines/lingbot_video.py)
- LingBotVideoUniPCScheduler (FlowMatchScheduler subclass): UniPC multistep
  for inference, flow-matching schedule for training
- Reuse QwenImageVAE via an additive, backward-compatible 5D-video
  encode/decode path in qwen_image_vae.py
- Register models + state-dict converters in model_configs.py

Training (issue's core deliverable):
- LingBotVideoTrainingModule (examples/lingbot_video/model_training/train.py)
  with the flow-matching SFT objective
- Attention-only LoRA launch script (to_q,to_k,to_v,to_out; MoE/FFN/router
  left frozen)

Examples + docs:
- T2V/V2V inference and low-VRAM examples
- examples/lingbot_video/README.md

Part of the SFT training support for modelscope#1530.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… LingBot-Video

- docs/{en,zh}/Model_Details/LingBot-Video.md + index.rst toctree entries
- two-stage prompt rewriter (structured JSON captions) + offline caption rewrite tool
- LoRA training validation script
- relocate low-VRAM inference into model_inference_low_vram/

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Register the MoE 30B-A3B DiT (128 experts, top-8 group-limited routing, 1
shared expert) and add VRAM management module maps for the LingBot-Video DiT
and text encoder, which previously had no entry and fell back to whole-model
wrapping. The MoE experts store their weights as grouped nn.Parameter rather
than nn.Linear, so the expert container itself is wrapped -- without that the
experts, which are the bulk of the model, would stay resident.

Also fixes three issues surfaced while validating the MoE path:

- _run_grouped_experts guarded the fast path with hasattr(torch, "_grouped_mm"),
  which is True on CPU even though the kernel is CUDA-only. Now device-aware.
- The block and DiT forwards derived the activation dtype from .weight.dtype.
  Under VRAM management the wrapped layer holds its weight in the offload dtype
  (or on meta for disk offload), so this read the wrong dtype. Resolved from
  computation_dtype instead.
- The fp32-pinned timestep and modulation MLPs were fed fp32 activations while
  VRAM management wraps every nn.Linear at computation_dtype, raising a dtype
  mismatch on any low-VRAM run. Resolved per-layer.

The dense low-VRAM example set only vram_limit, which is a no-op without
offload_dtype/offload_device, so VRAM management never activated; corrected
along with the same claim in the docs.

Validated against the official implementation with identical weights: router
selection and gate scores are bit-exact, and the full DiT matches bit-exactly
at fp32 (bf16 differs only because diffusers' .to() downcasts the fp32-pinned
modules). The registered model_hash matches the released checkpoint headers
across all 977 keys with no shape mismatches.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t rewriter

- Scheduler: use FlowMatchScheduler (Wan template) instead of a bespoke
  UniPC scheduler; flow_match.py restored byte-identical to main.
- VAE: restore QwenImageVAE to upstream; the 5D-video encode/decode and
  latent normalisation now live in LingBotVideoPipeline (encode_video /
  decode_video).
- Rewriter: keep only normalize_caption in diffsynth core; move the
  two-stage prompt rewriter + system prompts into the inference examples.
- Examples: merge into one lingbot-video-dense-1.3b.py (T2V / V2V /
  optional rewrite); drop the separate _rewrite.py.
- Training .sh: use --model_id_with_origin_paths instead of local --model_paths.
- Docs: sync en/zh Model_Details + example README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Under low-VRAM offload the AutoWrapped modules compute weights in bf16 but do
not cast their inputs, and reading `.weight.dtype` from outside the wrapper
returns the resident fp8 dtype. The parameter-free sinusoidal `time_proj`
always returns fp32, so feeding it straight into the bf16 `time_embedder` MLP
raised "mat1 and mat2 must have the same dtype, but got Float and BFloat16";
`proj_out` had the mirror bug via `self.proj_out.weight.dtype` (fp8 under
offload). Cast both inputs to the running compute dtype (`joint`) instead,
which is bf16 under offload and matches the weight dtype in a full-precision
run. No effect on the normal (non-offload) path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The trimmed diffsynth/pipelines/lingbot_video_prompt_rewriter.py held only
normalize_caption (caption -> compact-JSON serialisation) after the prompt
rewriter was moved to the examples. Its sole core consumer is the pipeline,
which already calls it internally, so keeping a separate one-function module
added a misleadingly-named file ("prompt_rewriter" that no longer rewrites)
for no benefit.

Move normalize_caption (and its _serialize_caption/_caption_from_sample
helpers) into diffsynth/pipelines/lingbot_video.py as module-level functions
and delete the old module. All importers now pull it from lingbot_video; the
example inference/training scripts already imported the pipeline, so this only
tidies their imports. The prompt-rewriting logic stays out of core, in
examples/lingbot_video/model_inference/prompt_rewriter.py.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Companion to the previous commit, which only recorded the deletion of
lingbot_video_prompt_rewriter.py. This commit adds normalize_caption (and its
_serialize_caption/_caption_from_sample helpers) into
diffsynth/pipelines/lingbot_video.py as module-level functions, and repoints
every importer (the pipeline itself, the model_inference / low_vram example
scripts, the example prompt_rewriter, train.py and rewrite_captions.py) at
diffsynth.pipelines.lingbot_video. Restores a working tree: the two commits
together move the single remaining function into the pipeline module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Collapse the AI-generated multi-paragraph banners and docstrings to concise
one-line "why" comments matching DiffSynth's light comment style. No code changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ixes to the MoE branch

Brings the reviewer-mi804 integration fixes from lingbot_sft onto the MoE-30B-A3B
branch so both variants share one conventions-aligned implementation:

- Scheduler: drop LingBotVideoUniPCScheduler, use FlowMatchScheduler (Wan template);
  diffsynth/diffusion/flow_match.py restored byte-identical to main.
- VAE: restore diffsynth/models/qwen_image_vae.py byte-identical to main; the 5D-video
  encode/decode lives in the pipeline (encode_video / decode_video).
- Rewriter: move the prompt-rewriting engine out of diffsynth/ into
  examples/lingbot_video/model_inference/{prompt_rewriter,system_prompts}.py; the core
  keeps only normalize_caption, folded into the pipeline module.
- Examples/docs/training .sh updated to model_id_with_origin_paths and the merged layout.

Conflict resolutions:
- lingbot_video_dit.py: kept the MoE resolve_bulk_dtype() dtype handling (superset of
  the sft joint.dtype fix, and consistent with the rest of the file), plus sft's trimmed
  comments and the router fp32 pin.
- low-VRAM dense example + docs: kept sft's corrected vram_limit wording, kept the MoE
  section and MoE-specific files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
time_embedder is fp32-pinned (LINGBOT_VIDEO_FP32_MODULES), so under a standard
load its weights stay fp32 while joint (the bulk hidden state) is bf16. Casting
the fp32 timestep_proj to joint.dtype fed bf16 into the fp32 Linear, raising
"mat1 and mat2 must have the same dtype" on any non-offload run. Cast to the
layer's own weight dtype instead: fp32 under standard load, bf16 under low-VRAM
offload (where the wrapper casts these MLPs to the compute dtype). proj_out keeps
joint.dtype since it is a bulk layer held in fp8 under offload.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Kept the MoE-branch resolve_bulk_dtype() helper for the time_embedder
input cast (handles VRAM-wrapped Linears); the sft-branch used a plain
.weight.dtype which the MoE version supersedes.
Condition generation on a first frame via a new `input_image` argument.
Dense-1.3B reuses its T2V checkpoint (DiT unchanged, in_channels=16); the
condition frame is used twice, matching the original LingBot-Video i2v
pipeline: fed to the Qwen3-VL text encoder as a visual reference (image
tokens prepended to the prompt), and VAE-encoded to a clean latent pinned
into the first temporal slot before sampling and after every scheduler step.

- LingBotVideoUnit_ImageEmbedder builds the cond latent (via encode_video)
  and the smart-resized VLM image; no-op for T2V/V2V.
- LingBotVideoUnit_PromptEmbedder passes the image to the processor when present.
- Ported smart_resize / cover-resize+center-crop / _vlm_image helpers.
- Example lingbot-video-dense-1.3b_ti2v.py + released first frame and caption.
- EN/ZH docs and example README document the TI2V path and `input_image`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
t2i is text-to-video with num_frames=1 through the same pipeline and DiT
(no separate image weight), matching the official runner which sets
num_frames=1 and swaps in the still-image negative prompt.

- Add DEFAULT_NEGATIVE_PROMPT_IMAGE (verbatim from the official pipeline):
  drops the temporal/motion terms that cannot apply to a single frame.
- Example lingbot-video-dense-1.3b_t2i.py + released still-image caption
  prompts/t2i_example.json; saves the single returned frame as PNG.
- EN/ZH docs and example README document the t2i path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- model_inference_low_vram: add ti2v (input_image) and t2i (num_frames=1 +
  DEFAULT_NEGATIVE_PROMPT_IMAGE) low-VRAM variants, mirroring the t2v script.
- train.py: add opt-in --first_frame_as_condition for image-to-video LoRA. It
  conditions each clip on its own first frame; FlowMatchSFTLoss already pins the
  clean first-frame latent and excludes it from the loss, so no core change is
  needed. A distinct condition column still works via --extra_inputs input_image.
- model_training: add ti2v LoRA launch script and validate_lora example.
- docs (en/zh) + example README: reference the new scripts and TI2V LoRA.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… reviewer's refactor (531ba56)

- TI2V + T2I inference / low-VRAM examples rewritten to the new pipeline API: read
  structured captions from JSON, pass pipe.default_negative_prompt(_image), drop the
  removed module-level normalize_caption / DEFAULT_NEGATIVE_PROMPT_IMAGE imports.
- Add default_negative_prompt_image attribute to LingBotVideoPipeline (T2I variant
  with temporal terms removed) so t2i examples can reference it symmetrically with
  default_negative_prompt.
- TI2V LoRA training script aligned to the reviewer's new t2v LoRA (dataset path,
  num_frames=81, num_epochs=5, dataset_repeat=50); validate_lora rewritten to use
  pipe.default_negative_prompt + json.load.
- New TI2V full-parameter training script + validate_full script (parallel to the
  reviewer's t2v full training), toggled via --first_frame_as_condition.
- Docs rewritten to match the standard Model_Details template: single-checkpoint
  overview covering T2V / TI2V / T2I with 3-row Examples table, input_image param
  documented, prompt-rewriter moved under Model Inference, VAE-internals text
  dropped per reviewer's "keep it about model info + usage" directive.
- README.md / README_zh.md: add Update History entry and Video Synthesis series
  block (Quick Start + Examples table) for LingBot-Video.
Low-VRAM TI2V feeds the condition frame through the Qwen3-VL vision tower,
but the LingBotVideoTextEncoder VRAM-management map did not cover its module
types. The vision LayerNorm / patch-embed weights stayed on `meta` and the
vision RoPE `inv_freq` (a non-persistent buffer, absent from the checkpoint)
stayed on CPU, so a TI2V run under offload died with a cuda/cpu device
mismatch. T2V / T2I never touch that path.

Add LayerNorm, Qwen3VLVisionPatchEmbed and Qwen3VLVisionRotaryEmbedding to
the map. PatchEmbed is wrapped whole (not its inner Conv3d) because its
forward reads `self.proj.weight.dtype` to cast the input, which under offload
would otherwise pick up the fp8 dtype and crash on the bf16 bias.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The input_image param (the TI2V condition frame) was grouped under a
'# Video-to-video' comment. Relabel it '# Image-to-video (TI2V)' to match
wan_video.py's param grouping and the file's own TI2V terminology.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Address reviewer comments on lingbot_video.py (:20, :185, :214):

- Move the module-level TI2V constants (IMAGE_MIN/MAX_TOKEN_NUM, MAX_RATIO,
  SPATIAL_MERGE_SIZE) and helpers (smart_resize, _round/_ceil/_floor_by_factor,
  _pixel_tensor_to_pil) plus the pipeline methods preprocess_cond_image,
  _vision_patch_size and _vlm_image into LingBotVideoUnit_ImageEmbedder, the
  only consumer. Move IMG_PROMPT_TEMPLATE into LingBotVideoUnit_PromptEmbedder.
- Move the pre-loop first-frame latent pin out of __call__ and into
  LingBotVideoUnit_ImageEmbedder.process; reorder self.units so
  InputVideoEmbedder (which produces `latents`) runs before ImageEmbedder
  (which pins into it) and before PromptEmbedder (which consumes vlm_image).
  __call__ keeps only the per-step re-pin inside the denoise loop.

Behavior-preserving for inference and training (the loss overwrites `latents`
and does its own first-frame pin, so the unit-level pin is inert there).
Verified on GPU: TI2V pins frame 0 to the condition frame; T2V no-op path OK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…g, keep MoE-30B

Brings the lingbot_sft refactor and features onto the MoE branch:
- Pipeline: adopt sft's unit-based LingBotVideoPipeline (model-agnostic; the
  MoE variant runs through it unchanged via self.dit).
- New Dense-1.3B examples/scripts: TI2V and T2I inference (+ low-VRAM), full
  and LoRA training, validate_full/validate_lora; scripts/ relocation.

Conflict resolutions:
- lingbot_video_dit.py: keep MoE's resolve_bulk_dtype / fp32-pinned modules
  (incl. router). It is the later superset of sft's 7caae83 x.dtype fix, and
  the file is built on it throughout.
- vram_management_module_maps.py: union both maps. The DiT map keeps MoE's
  fine-grained entries (GroupedExperts wrapped, NOT the whole block, so the
  walker doesn't keep all experts resident); the text-encoder map takes sft's
  Qwen3-VL vision-tower entries for TI2V low-VRAM.
- docs (en/zh): integrate the MoE-30B-A3B sections with sft's TI2V/T2I/training
  content into one coherent doc (4-row model table, T2I-aware params).

Style: align the MoE inference examples to the refactored dense layout
(section banners, explicit negative_prompt=pipe.default_negative_prompt).

Verified in the moe worktree with local dense-1.3b weights: the DiT parses and
imports; dense-1.3b normal and low-VRAM (fp8 offload) inference both produce
correct frame counts with no dtype/device errors (the 7caae83 regression case);
a tiny random MoE DiT forward runs grouped_mm on CUDA with finite output.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ventions

- lingbot_video_dit.py: drop unused LINGBOT_VIDEO_FP32_MODULES /
  should_keep_in_fp32, remove docstring and inline comments, keep
  norm_out_modulation call on one line
- model_configs.py: keep only the ModelConfig example comment, collapse
  extra_kwargs onto a single line
- vram_management_module_maps.py: drop explanatory comments
- MoE inference examples: remove all comments and section dividers
- docs: restore the standard template layout, fold the MoE description
  into the model overview section
- README / README_zh: add the missing MoE row so the examples table
  matches the docs model overview table
- use the released structured-JSON caption from prompts/t2v_example_1.json
  instead of free-form prose, matching the Dense T2V / TI2V / T2I examples
- switch the low-VRAM example to the Dense disk-offload profile
  (disk -> cpu fp8 -> cuda bf16, vram_limit - 0.5)
- add the video-to-video block to the low-VRAM example so it covers the
  same tasks as the Dense low-VRAM T2V example
…example dataset

- T2V examples now download the released structured caption through
  dataset_snapshot_download, reusing the lingbot-video-dense-1.3b dataset
  directory, exactly like the Dense T2V examples
- add TI2V and T2I MoE examples (inference + low VRAM) mirroring the Dense
  templates: shared prompts/ captions, assets/ti2v_first_frame.png,
  default_negative_prompt_image and num_frames=1 for T2I
- docs and README: split the MoE row into T2V / TI2V / T2I rows and mention
  the MoE variant in the release note
Disk offload only materialises weights for module types listed in the VRAM
map, so the per-type MoE map left two owners of raw parameters on `meta`:
`LingBotVideoBlock.scale_shift_table` and the router weight / correction
bias. Both bf16 and low VRAM Dense and MoE inference crashed with
"Tensor on device meta is not on the expected device cuda:0".

- map `LingBotVideoBlock` and `LingBotVideoRouter` to
  `AutoWrappedNonRecurseModule`, which manages a module's own parameters
  while its children keep their individual wrappers, and drop the dead
  `torch.nn.LayerNorm` entry (`norm_out` has no affine parameters)
- cast `scale_shift_table` to the modulation dtype/device in the block
  forward, following `wan_video_dit.DiTBlock`
- keep `e_score_correction_bias` as a non-trainable parameter so the disk
  loader restores its checkpoint values instead of leaving CPU zeros, and
  add it in float32

Verified on Robbyant/lingbot-video-moe-30b-a3b: all six MoE examples and
the Dense low VRAM examples run, low VRAM peaks at 29.5 GiB versus 74.5 GiB
for bf16, and the low VRAM T2I output matches the bf16 output to 16.9/255.
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

…the MoE branch

Upstream squash-merged the Dense-1.3B integration with review changes, so
realign the MoE side with what landed:

- Drop lingbot_video_text_encoder and its converter; the pipeline now loads
  Qwen3-VL through Krea2TextEncoder, and the Qwen3-VL vision-tower entries
  live in the Krea2TextEncoder VRAM map.
- Keep a single LingBotVideoDiT VRAM map entry: the MoE variant needs
  LingBotVideoBlock / LingBotVideoRouter as AutoWrappedNonRecurseModule and
  LingBotVideoGroupedExperts as AutoWrappedModule so the 128 experts stay
  individually streamable under offload.
- Rename the MoE examples to the upstream _t2v / _ti2v / _t2i scheme and
  regenerate them line-by-line from the merged Dense examples: Qwen3-VL text
  encoder + processor configs, captions and condition frames pulled from
  data/diffsynth_example_dataset, upstream output naming.
- Re-add the MoE registry entry, the three MoE rows in both docs tables and
  both README tables, the MoE paragraph in Model Overview and the release note.
…rectories

Dense and MoE share one example dataset on ModelScope; only
lingbot_video/lingbot-video-dense-1.3b_{t2v,ti2v,t2i}/ exists, so the MoE
examples read the same captions and condition frame instead of a MoE-specific
directory that was never published.
The bulk dtype is already the dtype of the incoming hidden states, and the
wrapped linears cast their own weights, so the helper only restated what
x.dtype / joint.dtype already carry. Removing it makes the file identical to
the Dense implementation except for the offload fixes; TI2V output stays
bit-identical in bf16 and under low-VRAM offload.
…fload

AutoWrappedNonRecurseModule leaves parameter casting to the model, and the
grouped-expert matmuls read experts.w1/w2/w3 directly instead of going through
a forward, so an AutoWrappedModule wrapper never got the chance to cast them.
With a vram_limit that actually forces layers to stay on CPU, the router
projection and the expert matmuls therefore hit CPU weights:

    RuntimeError: Expected all tensors to be on the same device,
    but got mat2 is on cpu

Move the experts to AutoWrappedNonRecurseModule as well and cast the router
weight, the correction bias and the three expert tensors to the token device
and dtype at use, matching how scale_shift_table is already handled. bf16 and
default low-VRAM outputs stay bit-identical.
@NancyFyong
NancyFyong marked this pull request as ready for review July 30, 2026 12:07
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants