Signed-off-by: BeatSeat <wendavid552@gmail.com> Signed-off-by: Yueqian Lin <70319226+linyueqian@users.noreply.github.com> Co-authored-by: Yueqian Lin <70319226+linyueqian@users.noreply.github.com>
292 lines
13 KiB
Python
292 lines
13 KiB
Python
# Copyright 2022 The OpenAI Authors and The HuggingFace Inc. team. All rights reserved.
|
|
# 2025 Zhipu AI Inc (authors: CogAudio Group Members)
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
"""WhisperVQEncoder: HF WhisperEncoder + VQ codebook + inter-layer pooling.
|
|
|
|
Built on standard ``WhisperConfig``. VQ-specific parameters are patched onto
|
|
the config object after ``from_pretrained`` so that callers never need a
|
|
separate config class. GLM-TTS (and future VQ-based TTS models) only need::
|
|
|
|
from transformers import WhisperConfig
|
|
from vllm_omni.model_executor.models.common.whisper_vq import WhisperVQEncoder
|
|
|
|
cfg = WhisperConfig.from_pretrained(checkpoint_dir)
|
|
cfg.pooling_kernel_size = None # passed through from checkpoint
|
|
cfg.pooling_type = "max" # or set by caller
|
|
cfg.pooling_position = 0
|
|
cfg.quantize_vocab_size = 32768
|
|
cfg.quantize_position = 16
|
|
cfg.quantize_encoder_only = True
|
|
model = WhisperVQEncoder(cfg)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
import re
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
import torch
|
|
import torch.nn.functional as F
|
|
from torch import Tensor, nn
|
|
from transformers import WhisperConfig
|
|
from transformers.modeling_outputs import BaseModelOutput
|
|
from transformers.models.whisper.modeling_whisper import WhisperEncoder
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@dataclass
|
|
class QuantizedBaseModelOutput(BaseModelOutput):
|
|
quantized_token_ids: torch.LongTensor | None = None
|
|
|
|
|
|
def vector_quantize(inputs: Tensor, codebook: Tensor) -> tuple[Tensor, Tensor, Tensor]:
|
|
"""Nearest-neighbour codebook lookup."""
|
|
embedding_size = codebook.size(1)
|
|
inputs_flatten = inputs.reshape(-1, embedding_size)
|
|
codebook_sqr = torch.sum(codebook**2, dim=1)
|
|
inputs_sqr = torch.sum(inputs_flatten**2, dim=1, keepdim=True)
|
|
distances = torch.addmm(
|
|
codebook_sqr + inputs_sqr,
|
|
inputs_flatten,
|
|
codebook.t(),
|
|
alpha=-2.0,
|
|
beta=1.0,
|
|
)
|
|
_, indices_flatten = torch.min(distances, dim=1)
|
|
codes_flatten = torch.index_select(codebook, dim=0, index=indices_flatten)
|
|
codes = codes_flatten.view_as(inputs)
|
|
return codes, indices_flatten, distances
|
|
|
|
|
|
_LEGACY_LAYER_REMAP = (
|
|
(re.compile(r"^layers\.(\d+)\.attn\.query\."), r"layers.\1.self_attn.q_proj."),
|
|
(re.compile(r"^layers\.(\d+)\.attn\.key\."), r"layers.\1.self_attn.k_proj."),
|
|
(re.compile(r"^layers\.(\d+)\.attn\.value\."), r"layers.\1.self_attn.v_proj."),
|
|
(re.compile(r"^layers\.(\d+)\.attn\.out\."), r"layers.\1.self_attn.out_proj."),
|
|
(re.compile(r"^layers\.(\d+)\.attn_ln\."), r"layers.\1.self_attn_layer_norm."),
|
|
(re.compile(r"^layers\.(\d+)\.mlp_ln\."), r"layers.\1.final_layer_norm."),
|
|
(re.compile(r"^layers\.(\d+)\.mlp\.0\."), r"layers.\1.fc1."),
|
|
(re.compile(r"^layers\.(\d+)\.mlp\.2\."), r"layers.\1.fc2."),
|
|
)
|
|
|
|
|
|
def remap_legacy_whisper_vq_state_dict(state_dict: dict[str, Tensor]) -> dict[str, Tensor]:
|
|
"""Map CogAudio/GLM-TTS fork parameter names onto HF WhisperEncoder names."""
|
|
remapped: dict[str, Tensor] = {}
|
|
for name, tensor in state_dict.items():
|
|
new_name = name
|
|
if new_name == "embed_positions":
|
|
new_name = "embed_positions.weight"
|
|
for pattern, replacement in _LEGACY_LAYER_REMAP:
|
|
new_name = pattern.sub(replacement, new_name)
|
|
remapped[new_name] = tensor
|
|
return remapped
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# main encoder class
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class WhisperVQEncoder(WhisperEncoder):
|
|
"""HF Whisper encoder with optional VQ codebook and pooling.
|
|
|
|
Uses a standard ``WhisperConfig`` with the following VQ-specific attrs
|
|
patched on by the caller (or loaded from the checkpoint's config.json):
|
|
|
|
* ``pooling_kernel_size`` -- int | None
|
|
* ``pooling_type`` -- "max" | "avg"
|
|
* ``pooling_position`` -- int (0-based layer index)
|
|
* ``quantize_vocab_size`` -- int | None
|
|
* ``quantize_position`` -- int (0-based layer index)
|
|
* ``quantize_encoder_only`` -- bool
|
|
"""
|
|
|
|
def __init__(self, config: WhisperConfig):
|
|
super().__init__(config)
|
|
embed_dim = config.d_model
|
|
max_source_positions = config.max_source_positions
|
|
|
|
# Truncate layers when quantize_encoder_only is set.
|
|
qpos = int(getattr(config, "quantize_position", 0) or 0)
|
|
if getattr(config, "quantize_encoder_only", False):
|
|
self.layers = nn.ModuleList(list(self.layers[:qpos]))
|
|
self.layer_norm = None
|
|
else:
|
|
qpos = max(qpos, int(config.encoder_layers)) # never exceed total
|
|
|
|
# Optional inter-layer pooling.
|
|
self.pooling_layer: nn.Module | None = None
|
|
ksize = getattr(config, "pooling_kernel_size", None)
|
|
if ksize is not None:
|
|
ptype = str(getattr(config, "pooling_type", "max") or "max")
|
|
self.pooling_layer = {
|
|
"max": nn.MaxPool1d(kernel_size=int(ksize)),
|
|
"avg": nn.AvgPool1d(kernel_size=int(ksize)),
|
|
}[ptype]
|
|
|
|
# Optional vector-quantization codebook.
|
|
self.codebook: nn.Embedding | None = None
|
|
self.embed_positions2: nn.Embedding | None = None
|
|
vq_sz = getattr(config, "quantize_vocab_size", None)
|
|
if vq_sz is not None:
|
|
self.codebook = nn.Embedding(int(vq_sz), embed_dim)
|
|
pos2_len = max_source_positions
|
|
if ksize is not None:
|
|
pos2_len = math.ceil(max_source_positions / int(ksize))
|
|
self.embed_positions2 = nn.Embedding(pos2_len, embed_dim)
|
|
|
|
# cached for forward()
|
|
self._pooling_position = int(getattr(config, "pooling_position", 0) or 0)
|
|
self._quantize_position = qpos if vq_sz is not None else -1
|
|
|
|
# ------------------------------------------------------------------
|
|
# forward / helpers
|
|
# ------------------------------------------------------------------
|
|
|
|
@property
|
|
def device(self) -> torch.device: # type: ignore[override]
|
|
return self.conv1.weight.device
|
|
|
|
@property
|
|
def dtype(self) -> torch.dtype: # type: ignore[override]
|
|
return self.conv1.weight.dtype
|
|
|
|
def load_state_dict(
|
|
self,
|
|
state_dict: dict[str, Tensor],
|
|
strict: bool = True,
|
|
assign: bool = False,
|
|
): # type: ignore[override]
|
|
remapped = remap_legacy_whisper_vq_state_dict(state_dict)
|
|
try:
|
|
return super().load_state_dict(remapped, strict=strict, assign=assign)
|
|
except TypeError:
|
|
return super().load_state_dict(remapped, strict=strict)
|
|
|
|
def forward(
|
|
self,
|
|
input_features: Tensor,
|
|
attention_mask: Tensor | None = None,
|
|
**_: Any,
|
|
) -> QuantizedBaseModelOutput:
|
|
batch_size, _, _ = input_features.shape
|
|
hidden_states = F.gelu(self.conv1(input_features))
|
|
hidden_states = F.gelu(self.conv2(hidden_states))
|
|
hidden_states = hidden_states.permute(0, 2, 1)
|
|
seq_len = int(hidden_states.shape[1])
|
|
|
|
pos_embed = self._position_embeddings(seq_len, hidden_states.device, hidden_states.dtype)
|
|
hidden_states = hidden_states + pos_embed
|
|
|
|
valid_mask = self._downsample_attention_mask(attention_mask, batch_size, seq_len, hidden_states.device)
|
|
hidden_states = hidden_states.masked_fill(~valid_mask.unsqueeze(-1), 0)
|
|
|
|
quantized_token_ids = None
|
|
for idx, layer in enumerate(self.layers):
|
|
layer_mask = self._make_layer_attention_mask(valid_mask, hidden_states.dtype)
|
|
layer_outputs = layer(
|
|
hidden_states,
|
|
attention_mask=layer_mask,
|
|
layer_head_mask=None,
|
|
output_attentions=False,
|
|
)
|
|
hidden_states = layer_outputs[0] if isinstance(layer_outputs, tuple) else layer_outputs
|
|
hidden_states = hidden_states.masked_fill(~valid_mask.unsqueeze(-1), 0)
|
|
|
|
if idx + 1 == self._pooling_position and self.pooling_layer is not None:
|
|
hidden_states, valid_mask = self._apply_pooling(hidden_states, valid_mask)
|
|
|
|
if idx + 1 == self._quantize_position and self.codebook is not None:
|
|
hidden_states, quantized_token_ids = self._apply_vq(hidden_states, valid_mask)
|
|
|
|
if self.layer_norm is not None:
|
|
hidden_states = self.layer_norm(hidden_states)
|
|
hidden_states = hidden_states.masked_fill(~valid_mask.unsqueeze(-1), 0)
|
|
|
|
return QuantizedBaseModelOutput(
|
|
last_hidden_state=hidden_states,
|
|
quantized_token_ids=quantized_token_ids,
|
|
)
|
|
|
|
def _position_embeddings(self, seq_len: int, device: torch.device, dtype: torch.dtype) -> Tensor:
|
|
ep = self.embed_positions
|
|
if isinstance(ep, nn.Embedding):
|
|
positions = torch.arange(seq_len, device=device)
|
|
return ep(positions).to(dtype=dtype).unsqueeze(0)
|
|
return ep[:seq_len].to(device=device, dtype=dtype).unsqueeze(0)
|
|
|
|
def _downsample_attention_mask(
|
|
self,
|
|
attention_mask: Tensor | None,
|
|
batch_size: int,
|
|
seq_len: int,
|
|
device: torch.device,
|
|
) -> Tensor:
|
|
if attention_mask is None:
|
|
return torch.ones(batch_size, seq_len, dtype=torch.bool, device=device)
|
|
conv_stride = int(self.conv1.stride[0]) * int(self.conv2.stride[0])
|
|
downsampled = attention_mask[:, ::conv_stride].to(device=device, dtype=torch.bool)
|
|
if downsampled.shape[1] < seq_len:
|
|
pad = seq_len - int(downsampled.shape[1])
|
|
downsampled = F.pad(downsampled, (0, pad), value=False)
|
|
return downsampled[:, :seq_len]
|
|
|
|
@staticmethod
|
|
def _make_layer_attention_mask(valid_mask: Tensor, dtype: torch.dtype) -> Tensor:
|
|
min_value = torch.finfo(dtype).min
|
|
additive = torch.zeros(valid_mask.shape, dtype=dtype, device=valid_mask.device)
|
|
additive = additive.masked_fill(~valid_mask, min_value)
|
|
return additive[:, None, None, :]
|
|
|
|
def _apply_pooling(self, hidden_states: Tensor, valid_mask: Tensor) -> tuple[Tensor, Tensor]:
|
|
k = int(self.config.pooling_kernel_size) # type: ignore[attr-defined]
|
|
pooled_segments: list[Tensor] = []
|
|
pooled_masks: list[Tensor] = []
|
|
for batch_idx in range(int(hidden_states.shape[0])):
|
|
length = int(valid_mask[batch_idx].sum().item())
|
|
segment = hidden_states[batch_idx, :length]
|
|
if segment.numel() == 0:
|
|
pooled = hidden_states.new_zeros((0, hidden_states.shape[-1]))
|
|
else:
|
|
segment_t = segment.unsqueeze(0).transpose(1, 2)
|
|
if int(segment_t.shape[-1]) % k != 0:
|
|
segment_t = F.pad(segment_t, (0, k - int(segment_t.shape[-1]) % k))
|
|
pooled = self.pooling_layer(segment_t).transpose(1, 2).squeeze(0) # type: ignore[misc]
|
|
pooled_segments.append(pooled)
|
|
pooled_masks.append(torch.ones(pooled.shape[0], dtype=torch.bool, device=hidden_states.device))
|
|
|
|
max_len = max((int(seg.shape[0]) for seg in pooled_segments), default=0)
|
|
output = hidden_states.new_zeros((hidden_states.shape[0], max_len, hidden_states.shape[-1]))
|
|
output_mask = torch.zeros(hidden_states.shape[0], max_len, dtype=torch.bool, device=hidden_states.device)
|
|
for i, segment in enumerate(pooled_segments):
|
|
if segment.shape[0] > 0:
|
|
output[i, : segment.shape[0]] = segment
|
|
output_mask[i, : segment.shape[0]] = pooled_masks[i]
|
|
return output, output_mask
|
|
|
|
def _apply_vq(self, hidden_states: Tensor, valid_mask: Tensor) -> tuple[Tensor, Tensor | None]:
|
|
output = hidden_states.clone()
|
|
token_ids = torch.full(
|
|
(hidden_states.shape[0], hidden_states.shape[1]),
|
|
-1,
|
|
dtype=torch.long,
|
|
device=hidden_states.device,
|
|
)
|
|
for batch_idx in range(int(hidden_states.shape[0])):
|
|
length = int(valid_mask[batch_idx].sum().item())
|
|
if length <= 0:
|
|
continue
|
|
segment = hidden_states[batch_idx : batch_idx + 1, :length]
|
|
quantized, indices, _ = vector_quantize(segment, self.codebook.weight) # type: ignore[union-attr]
|
|
if self.embed_positions2 is not None:
|
|
quantized = quantized + self.embed_positions2.weight[:length].to(dtype=quantized.dtype).unsqueeze(0)
|
|
output[batch_idx, :length] = quantized.squeeze(0)
|
|
token_ids[batch_idx, :length] = indices.reshape(-1)
|
|
return output.masked_fill(~valid_mask.unsqueeze(-1), 0), token_ids
|