[Feat] Support online quantization (fp8/int8) for DreamID-Omni (#3902)

Signed-off-by: Yuanheng Zhao <jonathan.zhaoyh@gmail.com>
This commit is contained in:
Yuanheng Zhao authored and GitHub committed 2026-05-28 02:31:58 +00:00
1 parent 8766c0c59f
commit 39705f9ee8
5 files changed
+397 -52

No files matched your search

@@ -102,3 +102,5 @@ Key arguments:
- `--audio-negative-prompt`: negative prompt for audio generation.
- `--enable-cpu-offload`: enable CPU offload (defaults False).
- `--cache-backend`: enable `cache_dit` for acceleration.
- `--quantization`: online (dynamic) quantization method — `fp8` or `int8`.
(VAEs, the T5 text encoder, norms and modulation stay bf16.)
@@ -4,6 +4,7 @@
import argparse
import re
import time
from typing import Any
import numpy as np
from PIL import Image
@@ -41,6 +42,12 @@ def parse_args() -> argparse.Namespace:
choices=[1, 2, 3, 4],
help="Number of GPUs used for classifier free guidance parallel size (max 4 branches).",
)
parser.add_argument(
"--tensor-parallel-size",
type=int,
default=1,
help=("Number of GPUs used for tensor parallelism (TP) inside the DiT."),
)
parser.add_argument(
"--video-negative-prompt",
default="jitter, bad hands, blur, distortion",
@@ -52,6 +59,13 @@ def parse_args() -> argparse.Namespace:
help="Negative prompt for audio.",
)
parser.add_argument("--output", default="dreamid_output.mp4", help="Output video path.")
parser.add_argument(
"--quantization",
type=str,
default=None,
choices=["fp8", "int8"],
help=("Online (dynamic) quantization method for the DreamID-Omni transformer. "),
)
parser.add_argument(
"--enable-cpu-offload",
action="store_true",
@@ -100,6 +114,27 @@ def load_image_and_audio(image_paths, audio_paths):
return image, audio
def _extract_peak_memory_mb(result: Any) -> float:
"""Pull worker-reported peak VRAM (MiB) generation result.
Mirrors vllm_omni/entrypoints/openai/serving_video.py:_extract_peak_memory_mb.
"""
if isinstance(result, list):
result = result[0] if result else None
if result is None:
return 0.0
val = getattr(result, "peak_memory_mb", 0.0)
if not val:
inner = getattr(result, "request_output", None)
if isinstance(inner, list):
inner = inner[0] if inner else None
val = getattr(inner, "peak_memory_mb", 0.0)
try:
return float(val or 0.0)
except (TypeError, ValueError):
return 0.0
def main() -> None:
args = parse_args()
if args.prompt is None and args.prompt_file is None:
@@ -142,6 +177,7 @@ def main() -> None:
parallel_config = DiffusionParallelConfig(
cfg_parallel_size=args.cfg_parallel_size,
tensor_parallel_size=args.tensor_parallel_size,
use_hsdp=args.use_hsdp,
hsdp_shard_size=args.hsdp_shard_size,
hsdp_replicate_size=args.hsdp_replicate_size,
@@ -162,7 +198,7 @@ def main() -> None:
"scm_steps_policy": "dynamic",
}
omni = Omni(
omni_kwargs = dict(
model=args.model,
parallel_config=parallel_config,
model_type=args.model_type,
@@ -171,6 +207,9 @@ def main() -> None:
cache_backend=args.cache_backend,
cache_config=cache_config,
)
if args.quantization is not None:
omni_kwargs["quantization"] = args.quantization
omni = Omni(**omni_kwargs)
start = time.perf_counter()
outputs = omni.generate(prompt, sampling_params)
elapsed = time.perf_counter() - start
@@ -208,6 +247,9 @@ def main() -> None:
f.write(video_bytes)
print(f"Saved generated video to {output_path}")
print(f"Total time: {elapsed:.2f}s")
peak_mb = _extract_peak_memory_mb(outputs)
if peak_mb:
print(f"Worker peak GPU memory (reserved): {peak_mb:.2f} MiB ({peak_mb / 1024:.2f} GiB)")
if __name__ == "__main__":
@@ -1,18 +1,28 @@
import re
from typing import TYPE_CHECKING
import torch
import torch.nn as nn
from vllm.distributed import get_tensor_model_parallel_world_size
from vllm.logger import init_logger
from vllm.model_executor.layers.linear import ColumnParallelLinear
from vllm_omni.diffusion.attention.layer import Attention
if TYPE_CHECKING:
from vllm.model_executor.layers.quantization.base_config import QuantizationConfig
try:
from dreamid_omni.modules.model import WanLayerNorm, WanRMSNorm
from dreamid_omni.modules.model import WanLayerNorm
except ImportError:
raise ImportError("Failed to import from dependency 'dreamid_omni'.")
from vllm_omni.diffusion.distributed.utils import get_local_device
from vllm_omni.diffusion.models.dreamid_omni.wan2_2 import WanModel, rope_apply
from vllm_omni.diffusion.models.dreamid_omni.wan2_2 import (
DistributedRMSNorm,
WanModel,
rope_apply,
)
logger = init_logger(__name__)
@@ -243,26 +253,36 @@ class FusedBlock(nn.Module):
class FusionModel(nn.Module):
_layerwise_offload_blocks_attrs = ["fused_blocks"]
packed_modules_mapping = {
"to_qkv": ["q", "k", "v"],
}
@staticmethod
def _is_fused_block(name: str, module) -> bool:
return "fused_blocks" in name and name.split(".")[-1].isdigit()
_hsdp_shard_conditions = [_is_fused_block]
def __init__(self, video_config=None, audio_config=None):
def __init__(
self,
video_config=None,
audio_config=None,
*,
quant_config: "QuantizationConfig | None" = None,
):
super().__init__()
has_video = True
has_audio = True
self.device = get_local_device()
if video_config is not None:
self.video_model = WanModel(**video_config)
self.video_model = WanModel(quant_config=quant_config, prefix="video_model", **video_config)
else:
has_video = False
self.video_model = None
logger.warning("No video model is provided!")
if audio_config is not None:
self.audio_model = WanModel(**audio_config)
self.audio_model = WanModel(quant_config=quant_config, prefix="audio_model", **audio_config)
else:
has_audio = False
self.audio_model = None
@@ -272,11 +292,12 @@ class FusionModel(nn.Module):
assert len(self.video_model.blocks) == len(self.audio_model.blocks)
self.num_blocks = len(self.video_model.blocks)
self.inject_cross_attention_kv_projections()
self.inject_cross_attention_kv_projections(quant_config=quant_config)
self.num_heads = self.video_model.num_heads
tp_size = get_tensor_model_parallel_world_size()
self.full_num_heads = self.video_model.num_heads
self.num_heads = self.full_num_heads // tp_size
self.head_dim = self.video_model.dim // self.video_model.num_heads
# Make a single shared instance to pass in at forward time
self.attn = Attention(
num_heads=self.num_heads,
head_size=self.head_dim,
@@ -315,21 +336,42 @@ class FusionModel(nn.Module):
return super().load_state_dict(state_dict, strict=strict, assign=assign)
def inject_cross_attention_kv_projections(self):
for vid_block in self.video_model.blocks:
vid_block.cross_attn.k_fusion = nn.Linear(vid_block.dim, vid_block.dim)
vid_block.cross_attn.v_fusion = nn.Linear(vid_block.dim, vid_block.dim)
vid_block.cross_attn.pre_attn_norm_fusion = WanLayerNorm(vid_block.dim, elementwise_affine=True)
vid_block.cross_attn.norm_k_fusion = (
WanRMSNorm(vid_block.dim, eps=1e-6) if vid_block.qk_norm else nn.Identity()
def inject_cross_attention_kv_projections(
self,
quant_config: "QuantizationConfig | None" = None,
):
def _make_kv_proj(dim: int, prefix: str) -> ColumnParallelLinear:
# K/V projections for the cross-modal (a2v / v2a) attention path.
# Inputs are full latents, so we quantize matched to self_attn.
return ColumnParallelLinear(
dim,
dim,
bias=True,
gather_output=False,
return_bias=False,
quant_config=quant_config,
prefix=prefix,
)
for audio_block in self.audio_model.blocks:
audio_block.cross_attn.k_fusion = nn.Linear(audio_block.dim, audio_block.dim)
audio_block.cross_attn.v_fusion = nn.Linear(audio_block.dim, audio_block.dim)
audio_block.cross_attn.pre_attn_norm_fusion = WanLayerNorm(audio_block.dim, elementwise_affine=True)
audio_block.cross_attn.norm_k_fusion = (
WanRMSNorm(audio_block.dim, eps=1e-6) if audio_block.qk_norm else nn.Identity()
tp_size = get_tensor_model_parallel_world_size()
for i, vid_block in enumerate(self.video_model.blocks):
cross = vid_block.cross_attn
block_prefix = f"video_model.blocks.{i}.cross_attn"
cross.k_fusion = _make_kv_proj(vid_block.dim, f"{block_prefix}.k_fusion")
cross.v_fusion = _make_kv_proj(vid_block.dim, f"{block_prefix}.v_fusion")
cross.pre_attn_norm_fusion = WanLayerNorm(vid_block.dim, elementwise_affine=True)
cross.norm_k_fusion = (
DistributedRMSNorm(vid_block.dim // tp_size, eps=1e-6) if vid_block.qk_norm else nn.Identity()
)
for i, audio_block in enumerate(self.audio_model.blocks):
cross = audio_block.cross_attn
block_prefix = f"audio_model.blocks.{i}.cross_attn"
cross.k_fusion = _make_kv_proj(audio_block.dim, f"{block_prefix}.k_fusion")
cross.v_fusion = _make_kv_proj(audio_block.dim, f"{block_prefix}.v_fusion")
cross.pre_attn_norm_fusion = WanLayerNorm(audio_block.dim, elementwise_affine=True)
cross.norm_k_fusion = (
DistributedRMSNorm(audio_block.dim // tp_size, eps=1e-6) if audio_block.qk_norm else nn.Identity()
)
def _detach_blocks_from_backbones(self) -> None:
@@ -13,6 +13,11 @@ from PIL import Image, ImageOps
from torch import nn
from torchvision.transforms import Compose, Normalize
from tqdm import tqdm
from vllm.distributed import (
get_tensor_model_parallel_rank,
get_tensor_model_parallel_world_size,
)
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin
@@ -124,7 +129,8 @@ class DreamIDOmniPipeline(nn.Module, CFGParallelMixin, SupportImageInput, Suppor
self.text_encoder = self.text_model.model
# Fusion model — weights are loaded later via load_weights()
self.model = FusionModel(VIDEO_CONFIG, AUDIO_CONFIG)
quant_config = getattr(self.od_config, "quantization_config", None)
self.model = FusionModel(VIDEO_CONFIG, AUDIO_CONFIG, quant_config=quant_config)
self.transformer = self.model
fusion_path = self.od_config.tf_model_config.get("fusion", None)
@@ -236,10 +242,75 @@ class DreamIDOmniPipeline(nn.Module, CFGParallelMixin, SupportImageInput, Suppor
return ref_vae_latents, ref_audio_lengths
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
prefix = "model."
state_dict = {name[len(prefix) :]: tensor for name, tensor in weights if name.startswith(prefix)}
self.model.load_state_dict(state_dict, strict=True)
return {prefix + k for k in state_dict}
pipeline_prefix = "model."
qkv_stacked_mapping = [
(".self_attn.to_qkv", ".self_attn.q", "q"),
(".self_attn.to_qkv", ".self_attn.k", "k"),
(".self_attn.to_qkv", ".self_attn.v", "v"),
]
params_dict = dict(self.model.named_parameters())
loaded_local: set[str] = set()
tp_size = get_tensor_model_parallel_world_size()
tp_rank = get_tensor_model_parallel_rank() if tp_size > 1 else 0
def _maybe_shard_weight(weight: torch.Tensor, param: torch.Tensor) -> torch.Tensor:
"""Slice a checkpoint tensor to this rank's TP shard when the
param is a TP-local shape but the checkpoint stores the full one.
"""
if tp_size <= 1 or weight.shape == param.shape:
return weight
if weight.ndim == 1 and weight.numel() == param.numel() * tp_size:
return weight.chunk(tp_size, dim=0)[tp_rank]
if weight.ndim == 2:
if weight.shape[0] == param.shape[0] * tp_size:
return weight.chunk(tp_size, dim=0)[tp_rank]
if weight.shape[1] == param.shape[1] * tp_size:
return weight.chunk(tp_size, dim=1)[tp_rank]
return weight
for full_name, loaded_weight in weights:
if not full_name.startswith(pipeline_prefix):
continue
name = full_name[len(pipeline_prefix) :]
# Fused-QKV stacking: route checkpoint's separate
# - ...self_attn.{q,k,v}: weights into the corresponding
# - ...self_attn.to_qkv : via its sharded weight_loader.
stacked_hit = False
for fused_name, raw_name, shard_id in qkv_stacked_mapping:
if raw_name not in name:
continue
stacked = name.replace(raw_name, fused_name)
if stacked not in params_dict:
continue
param = params_dict[stacked]
param.weight_loader(param, loaded_weight, shard_id)
loaded_local.add(stacked)
stacked_hit = True
break
if stacked_hit:
continue
if name not in params_dict:
logger.warning(f"Skipping DreamID-Omni weight {full_name} -- not found in model parameters")
continue
param = params_dict[name]
weight_loader = getattr(param, "weight_loader", None)
if weight_loader is not None:
# Parallel Linears handle TP sharding internally
weight_loader(param, loaded_weight)
else:
default_weight_loader(param, _maybe_shard_weight(loaded_weight, param))
loaded_local.add(name)
# Detach must happen before `DiffusersPipelineLoader._process_weights_after_loading`
# and before any forward pass that triggers layerwise-offload hooks.
self.model._detach_blocks_from_backbones()
return {pipeline_prefix + k for k in loaded_local}
def get_scheduler_time_steps(self, sampling_steps, solver_name="unipc", device=0, shift=5.0):
torch.manual_seed(4)
+213 -25
View File
@@ -6,6 +6,16 @@ import torch.amp as amp
import torch.nn as nn
from diffusers.configuration_utils import ConfigMixin, register_to_config
from diffusers.models.modeling_utils import ModelMixin
from vllm.distributed import (
get_tensor_model_parallel_world_size,
tensor_model_parallel_all_reduce,
)
from vllm.model_executor.layers.linear import (
ColumnParallelLinear,
QKVParallelLinear,
RowParallelLinear,
)
from vllm.model_executor.layers.quantization.base_config import QuantizationConfig
from vllm_omni.diffusion.attention.layer import Attention
@@ -17,7 +27,6 @@ try:
MLPProj,
ModulationAdd,
WanLayerNorm,
WanRMSNorm,
rope_apply, # diff from wan2.2
rope_params,
sinusoidal_embedding_1d,
@@ -26,42 +35,99 @@ except ImportError:
raise ImportError("Failed to import from dependency 'dreamid_omni'.")
class DistributedRMSNorm(nn.Module):
"""RMSNorm that computes global RMS across tensor parallel ranks.
Mirrors `vllm_omni/diffusion/models/wan2_2/wan2_2_transformer.py::DistributedRMSNorm`
"""
def __init__(self, hidden_size: int, eps: float = 1e-6):
super().__init__()
self.eps = eps
self.weight = nn.Parameter(torch.ones(hidden_size))
def forward(self, x: torch.Tensor) -> torch.Tensor:
tp_size = get_tensor_model_parallel_world_size()
x_dtype = x.dtype
x_float = x.float()
local_sum_sq = x_float.pow(2).sum(dim=-1, keepdim=True)
local_count = x.shape[-1]
if tp_size > 1:
local_sum_sq = tensor_model_parallel_all_reduce(local_sum_sq)
local_count = local_count * tp_size
rms = torch.sqrt(local_sum_sq / local_count + self.eps)
return (x_float / rms * self.weight.float()).to(x_dtype)
class WanSelfAttention(nn.Module):
"""Optimized self-attention module using vLLM layers."""
def __init__(self, dim, num_heads, window_size=(-1, -1), qk_norm=True, eps=1e-6):
def __init__(
self,
dim,
num_heads,
window_size=(-1, -1),
qk_norm=True,
eps=1e-6,
*,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
):
assert dim % num_heads == 0
super().__init__()
self.dim = dim
self.num_heads = num_heads
self.head_dim = dim // num_heads
self.window_size = window_size
self.qk_norm = qk_norm
self.eps = eps
# layers
self.q = nn.Linear(dim, dim)
self.k = nn.Linear(dim, dim)
self.v = nn.Linear(dim, dim)
self.o = nn.Linear(dim, dim)
self.norm_q = WanRMSNorm(dim, eps=eps) if qk_norm else nn.Identity()
self.norm_k = WanRMSNorm(dim, eps=eps) if qk_norm else nn.Identity()
self.to_qkv = QKVParallelLinear(
hidden_size=dim,
head_size=self.head_dim,
total_num_heads=num_heads,
bias=True,
quant_config=quant_config,
prefix=f"{prefix}.to_qkv" if prefix else "to_qkv",
)
self.num_heads = self.to_qkv.num_heads
self.num_kv_heads = self.to_qkv.num_kv_heads
self.tp_inner_dim = self.num_heads * self.head_dim
self.tp_kv_dim = self.num_kv_heads * self.head_dim
self.o = RowParallelLinear(
dim,
dim,
bias=True,
input_is_parallel=True,
return_bias=False,
quant_config=quant_config,
prefix=f"{prefix}.o" if prefix else "o",
)
self.norm_q = DistributedRMSNorm(self.tp_inner_dim, eps=eps) if qk_norm else nn.Identity()
self.norm_k = DistributedRMSNorm(self.tp_kv_dim, eps=eps) if qk_norm else nn.Identity()
# Unified attention layer
self.attn = Attention(
num_heads=self.num_heads,
head_size=self.head_dim,
num_kv_heads=self.num_heads,
num_kv_heads=self.num_kv_heads,
softmax_scale=1.0 / (self.head_dim**0.5),
causal=False,
prefix=prefix,
)
# query, key, value function
def qkv_fn(self, x):
b, s, n, d = *x.shape[:2], self.num_heads, self.head_dim
b, s = x.shape[:2]
qkv, _ = self.to_qkv(x)
q_size = self.num_heads * self.head_dim
kv_size = self.num_kv_heads * self.head_dim
q, k, v = qkv.split([q_size, kv_size, kv_size], dim=-1)
q = self.norm_q(self.q(x)).view(b, s, n, d)
k = self.norm_k(self.k(x)).view(b, s, n, d)
v = self.v(x).view(b, s, n, d)
q = self.norm_q(q).view(b, s, self.num_heads, self.head_dim)
k = self.norm_k(k).view(b, s, self.num_kv_heads, self.head_dim)
v = v.view(b, s, self.num_kv_heads, self.head_dim)
return q, k, v
def forward(self, x, seq_lens, grid_sizes, freqs, ref_lengths=None, freqs_scaling=None):
@@ -87,14 +153,97 @@ class WanSelfAttention(nn.Module):
return x
class WanT2VCrossAttention(WanSelfAttention):
def qkv_fn(self, x, context):
b, n, d = x.size(0), self.num_heads, self.head_dim
class WanT2VCrossAttention(nn.Module):
"""Text -> latent cross-attention."""
# compute query, key, value
q = self.norm_q(self.q(x)).view(b, -1, n, d)
k = self.norm_k(self.k(context)).view(b, -1, n, d)
v = self.v(context).view(b, -1, n, d)
def __init__(
self,
dim,
num_heads,
window_size=(-1, -1),
qk_norm=True,
eps=1e-6,
*,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
):
super().__init__()
assert dim % num_heads == 0
self.dim = dim
self.head_dim = dim // num_heads
self.window_size = window_size
self.qk_norm = qk_norm
self.eps = eps
# Separate Q/K/V — Q's input differs from K/V's input (text context)
self.q = ColumnParallelLinear(
dim,
dim,
bias=True,
gather_output=False,
return_bias=False,
quant_config=quant_config,
prefix=f"{prefix}.q" if prefix else "q",
)
self.k = ColumnParallelLinear(
dim,
dim,
bias=True,
gather_output=False,
return_bias=False,
quant_config=quant_config,
prefix=f"{prefix}.k" if prefix else "k",
)
self.v = ColumnParallelLinear(
dim,
dim,
bias=True,
gather_output=False,
return_bias=False,
quant_config=quant_config,
prefix=f"{prefix}.v" if prefix else "v",
)
tp_size = get_tensor_model_parallel_world_size()
self.num_heads = num_heads // tp_size
self.num_kv_heads = self.num_heads
self.tp_inner_dim = self.num_heads * self.head_dim
self.norm_q = DistributedRMSNorm(self.tp_inner_dim, eps=eps) if qk_norm else nn.Identity()
self.norm_k = DistributedRMSNorm(self.tp_inner_dim, eps=eps) if qk_norm else nn.Identity()
self.o = RowParallelLinear(
dim,
dim,
bias=True,
input_is_parallel=True,
return_bias=False,
quant_config=quant_config,
prefix=f"{prefix}.o" if prefix else "o",
)
# Kept for interface parity with WanSelfAttention;
# the active cross-attention path in DreamID-Omni uses the shared FusionModel.attn.
# Disable kv quant to prevent affecting quality.
self.attn = Attention(
num_heads=self.num_heads,
head_size=self.head_dim,
num_kv_heads=self.num_kv_heads,
softmax_scale=1.0 / (self.head_dim**0.5),
causal=False,
prefix=prefix,
disable_kv_quant=True,
)
def qkv_fn(self, x, context):
b = x.size(0)
q = self.q(x)
k = self.k(context)
v = self.v(context)
q = self.norm_q(q).view(b, -1, self.num_heads, self.head_dim)
k = self.norm_k(k).view(b, -1, self.num_kv_heads, self.head_dim)
v = v.view(b, -1, self.num_kv_heads, self.head_dim)
return q, k, v
@@ -142,6 +291,9 @@ class WanAttentionBlock(nn.Module):
cross_attn_norm=False,
eps=1e-6,
additional_emb_length=None,
*,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
):
super().__init__()
self.dim = dim
@@ -154,7 +306,15 @@ class WanAttentionBlock(nn.Module):
# layers
self.norm1 = WanLayerNorm(dim, eps)
self.self_attn = WanSelfAttention(dim, num_heads, window_size, qk_norm, eps)
self.self_attn = WanSelfAttention(
dim,
num_heads,
window_size,
qk_norm,
eps,
quant_config=quant_config,
prefix=f"{prefix}.self_attn" if prefix else "self_attn",
)
self.norm3 = WanLayerNorm(dim, eps, elementwise_affine=True) if cross_attn_norm else nn.Identity()
if cross_attn_type == "i2v_cross_attn":
assert additional_emb_length is not None, "additional_emb_length should be specified for i2v_cross_attn"
@@ -167,9 +327,32 @@ class WanAttentionBlock(nn.Module):
(-1, -1),
qk_norm,
eps,
quant_config=quant_config,
prefix=f"{prefix}.cross_attn" if prefix else "cross_attn",
)
self.norm2 = WanLayerNorm(dim, eps)
self.ffn = nn.Sequential(nn.Linear(dim, ffn_dim), nn.GELU(approximate="tanh"), nn.Linear(ffn_dim, dim))
# FFN layout: Stay compatible with upstream DreamID-Omni checkpoints
self.ffn = nn.Sequential(
ColumnParallelLinear(
dim,
ffn_dim,
bias=True,
gather_output=False,
return_bias=False,
quant_config=quant_config,
prefix=f"{prefix}.ffn.0" if prefix else "ffn.0",
),
nn.GELU(approximate="tanh"),
RowParallelLinear(
ffn_dim,
dim,
bias=True,
input_is_parallel=True,
return_bias=False,
quant_config=quant_config,
prefix=f"{prefix}.ffn.2" if prefix else "ffn.2",
),
)
# modulation
self.modulation = ModulationAdd(dim, 6)
@@ -243,6 +426,9 @@ class WanModel(ModelMixin, ConfigMixin):
cross_attn_norm=True,
temporal_rope_scaling_factor=1.0,
eps=1e-6,
*,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
):
r"""
Initialize the diffusion model backbone.
@@ -357,8 +543,10 @@ class WanModel(ModelMixin, ConfigMixin):
cross_attn_norm,
eps,
additional_emb_length,
quant_config=quant_config,
prefix=f"{prefix}.blocks.{layer_idx}" if prefix else f"blocks.{layer_idx}",
)
for _ in range(num_layers)
for layer_idx in range(num_layers)
]
)