| import enum |
| import os |
| import os.path as osp |
| import time |
| from pathlib import Path |
| from typing import MutableSequence, TypeAlias |
|
|
| import torch |
| import typer |
| from torch.export import Dim |
| from torch.nn import functional as F |
|
|
| from ..logger import logger |
| from . import nn |
| from .t2s_model_abc import AttentionABC, FeedForward, T2SDecoderABC, TransformerBlockABC, TransformerDecoderABC |
|
|
| Tensor = torch.Tensor |
|
|
| KVCache: TypeAlias = tuple[Tensor, Tensor] |
|
|
| app = typer.Typer( |
| context_settings={"help_option_names": ["-h", "--help"]}, |
| add_completion=False, |
| ) |
|
|
|
|
| class Stage(str, enum.Enum): |
| embed = "embed" |
| decode = "decode" |
|
|
|
|
| class KVCacheONNX: |
| @staticmethod |
| def empty(kv_cache): |
| assert len(kv_cache) == 2 |
| k_cache, v_cache = kv_cache |
|
|
| k_cache[:] = 0 |
| v_cache[:] = 0 |
|
|
| @staticmethod |
| def update_cache( |
| input_pos: Tensor, k_val: Tensor, v_val: Tensor, kv_cache: tuple[Tensor, Tensor], cache_idx: Tensor |
| ): |
| |
| k_out, v_out = kv_cache |
| ip0 = input_pos - 1 |
|
|
| k_out[cache_idx, :, ip0, None] = k_val |
| v_out[cache_idx, :, ip0, None] = v_val |
|
|
| return k_out, v_out |
|
|
| @staticmethod |
| def prefill_kv(k_val: Tensor, v_val: Tensor, kv_cache: tuple[Tensor, Tensor]): |
| |
| k_cache, v_cache = kv_cache |
|
|
| k_cache[..., : k_val.shape[1], :] = k_val.transpose(1, 2) |
| v_cache[..., : v_val.shape[1], :] = v_val.transpose(1, 2) |
|
|
| @staticmethod |
| def init_cache(batch_size: int, max_seq_length: int, n_heads: int, head_dim: int, dtype: torch.dtype): |
| cache_shape = (batch_size, n_heads, max_seq_length, head_dim) |
|
|
| return (torch.zeros(cache_shape, dtype=dtype), torch.zeros(cache_shape, dtype=dtype)) |
|
|
|
|
| class AttentionONNX(AttentionABC): |
| def __init__(self, n_heads: int, head_dim: int, max_seq_length: int): |
| super().__init__(n_heads, head_dim, max_seq_length) |
|
|
| self.in_proj = nn.Linear(self.hidden_dim, self.hidden_dim * 3, bias=True) |
| self.out_proj = nn.Linear(self.hidden_dim, self.hidden_dim, bias=True) |
|
|
| def __call__(self, *args, **kwds): |
| pass |
|
|
| def onnx_prefill(self, x: Tensor, kv_cache: KVCache, attn_mask: Tensor) -> Tensor: |
| bsz, seqlen, _ = x.shape |
|
|
| torch._check(attn_mask.size(-2) == x.size(-2)) |
|
|
| q, k, v = self.in_proj(x.unsqueeze(0)).chunk(3, dim=-1) |
|
|
| q, k, v = map(lambda x: x.contiguous().view(bsz, seqlen, self.n_head, self.head_dim), (q, k, v)) |
|
|
| KVCacheONNX.prefill_kv(k, v, kv_cache) |
|
|
| q, k, v = map(lambda x: x.transpose(1, 2), (q, k, v)) |
|
|
| attn = F.scaled_dot_product_attention(q, k, v, attn_mask) |
|
|
| attn = attn.transpose(1, 2).contiguous().view(1, -1, self.hidden_dim) |
|
|
| output = self.out_proj(attn) |
|
|
| return output |
|
|
| def onnx_decode(self, x: Tensor, input_pos: Tensor, kv_cache: KVCache, cache_idx: Tensor, attn_mask: Tensor): |
| bsz, seqlen, _ = x.shape |
|
|
| torch._check(attn_mask.size(-2) == 1) |
|
|
| q, k, v = self.in_proj(x).chunk(3, dim=-1) |
|
|
| q, k, v = map(lambda x: x.reshape(bsz, seqlen, self.n_head, self.head_dim), (q, k, v)) |
|
|
| q, k, v = map(lambda x: x.swapaxes(1, 2), (q, k, v)) |
|
|
| kv_cache = KVCacheONNX.update_cache(input_pos, k, v, kv_cache, cache_idx) |
|
|
| max_idx = int(input_pos.max()) |
|
|
| q, k, v = map(lambda x: x[..., :max_idx, :], (q, *kv_cache)) |
|
|
| mask = attn_mask[..., :max_idx] |
|
|
| attn = F.scaled_dot_product_attention(q, k, v, attn_mask=mask) |
|
|
| attn = attn.swapaxes(1, 2).reshape(bsz, seqlen, self.hidden_dim) |
|
|
| attn = self.out_proj(attn) |
|
|
| return attn |
|
|
|
|
| class TransformerBlockONNX(TransformerBlockABC): |
| def __init__(self, n_head: int, ffn_dim: int, hidden_dim: int, max_seq_length: int) -> None: |
| super().__init__(n_head, ffn_dim, hidden_dim, max_seq_length) |
|
|
| self.attention: AttentionONNX = AttentionONNX(n_head, hidden_dim, max_seq_length) |
| self.feed_forward = FeedForward(hidden_dim, ffn_dim) |
| self.attention_norm = nn.LayerNorm(self.hidden_dim) |
| self.ffn_norm = nn.LayerNorm(self.hidden_dim) |
|
|
| def onnx_prefill(self, x: Tensor, attn_mask: Tensor, kv_cache: KVCache): |
| h = self.attention_norm( |
| x |
| + self.attention.onnx_prefill( |
| x, |
| kv_cache, |
| attn_mask, |
| ) |
| ) |
| out = self.ffn_norm(h + self.feed_forward(h)) |
|
|
| return out |
|
|
| def onnx_decode(self, x: Tensor, input_pos: Tensor, kv_cache: KVCache, cache_idx: Tensor, attn_mask: Tensor): |
| h = self.attention_norm( |
| x |
| + self.attention.onnx_decode( |
| x, |
| input_pos, |
| kv_cache, |
| cache_idx, |
| attn_mask, |
| ) |
| ) |
| out = self.ffn_norm(h + self.feed_forward(h)) |
| return out |
|
|
|
|
| class TransformerDecoderONNX(TransformerDecoderABC): |
| def __init__( |
| self, |
| hidden_dim: int, |
| n_layer: int, |
| n_head: int, |
| ffn_dim: int, |
| vocab_size: int, |
| max_seq_length: int, |
| max_batch_size: int, |
| ) -> None: |
| super().__init__(hidden_dim, n_layer, n_head, ffn_dim, vocab_size, max_seq_length, max_batch_size) |
|
|
| self.layers: MutableSequence[TransformerBlockONNX] = nn.ModuleList( |
| TransformerBlockONNX(n_head, ffn_dim, hidden_dim, max_seq_length) for _ in range(n_layer) |
| ) |
|
|
| def onnx_prefill(self, x: Tensor, mask: Tensor, *kv_caches: KVCache): |
| for layer, kv_cache in zip(self.layers, kv_caches): |
| x = layer.onnx_prefill( |
| x, |
| mask, |
| kv_cache, |
| ) |
| return x |
|
|
| def onnx_decode( |
| self, |
| input_pos: Tensor, |
| x: Tensor, |
| cache_idx: Tensor, |
| attn_mask: Tensor, |
| *kv_caches: KVCache, |
| ): |
| for layer, kv_cache in zip(self.layers, kv_caches): |
| x = layer.onnx_decode( |
| x, |
| input_pos, |
| kv_cache, |
| cache_idx, |
| attn_mask, |
| ) |
|
|
| return x |
|
|
|
|
| class T2SDecoderONNX(T2SDecoderABC): |
| def __init__(self, config: dict, max_seq_length: int = 2000, max_batch_size: int = 10) -> None: |
| super().__init__(config, max_seq_length, max_batch_size) |
|
|
| self.bert_proj = nn.Linear(1024, self.embedding_dim) |
| self.ar_predict_layer = nn.Linear(self.hidden_dim, self.vocab_size, bias=False) |
|
|
| self.h = TransformerDecoderONNX( |
| self.hidden_dim, self.n_layer, self.n_head, self.ffn_dim, self.vocab_size, max_seq_length, max_batch_size |
| ) |
|
|
| def pre_forward(self, session) -> tuple[list[Tensor], dict[str, Tensor]]: |
| return super().pre_forward(session) |
|
|
| def post_forward(self, idx: int, session) -> None: |
| return super().post_forward(idx, session) |
|
|
| def embed_onnx_( |
| self, |
| x: Tensor, |
| x_len: Tensor, |
| y: torch.Tensor, |
| bert_features: Tensor, |
| ): |
| B = x.shape[0] |
| D = self.embedding_dim |
| T_TOTAL = 500 |
| xy_pos = torch.zeros((B, T_TOTAL, D)).to(bert_features[0].dtype) |
|
|
| bert_features = bert_features.transpose(1, 2) |
|
|
| y_len = y.shape[1] |
| y_emb = self.ar_audio_embedding(y) |
| y_pos = self.ar_audio_position.prefill(y_emb) |
|
|
| for bs, x_, len_, bert_feature in zip(torch.arange(x.shape[0]), x, x_len, bert_features): |
| x_emb = self.ar_text_embedding(x_[:len_]) |
|
|
| bert = self.bert_proj(bert_feature[:len_]) |
|
|
| print(bert.shape, bert_feature[:len_]) |
|
|
| return bert, bert_feature[:len_].unsqueeze(0) |
|
|
| return bert[:20].unsqueeze(0), None |
| x_emb = x_emb + bert |
| x_pos = self.ar_text_position.prefill(x_emb.unsqueeze(0)) |
|
|
| xy_pos[None, bs, :len_] = bert |
| |
|
|
| return xy_pos[:, -1], None |
|
|
| return xy_pos[: x.shape[0]], x_len |
|
|
| def embed_onnx( |
| self, |
| x: torch.Tensor, |
| x_len: torch.Tensor, |
| y: torch.Tensor, |
| bert_features: torch.Tensor, |
| ): |
| |
| bert_features = bert_features.transpose(1, 2) |
|
|
| Ty = y.shape[1] |
| Tx = x.shape[1] |
| B = x.shape[0] |
| D = self.embedding_dim |
| T_TOTAL = 500 |
|
|
| |
| col = torch.arange(Tx, device=x.device).unsqueeze(0) |
| mask_x = col < x_len.view(-1, 1) |
| mask_x3 = mask_x.unsqueeze(-1) |
|
|
| torch._check((Ty >= 0) and (Ty <= 250), "y_len out of range") |
| torch._check((Tx >= 0) and (Tx <= 250), "x_len out of range") |
|
|
| y_emb = self.ar_audio_embedding(y) |
| y_pos = self.ar_audio_position.prefill(y_emb) |
|
|
| x_emb_full = self.ar_text_embedding(x) |
| bert_full = self.bert_proj(bert_features[[0], : x_len[0]]) |
|
|
| print(bert_full[0].shape, bert_features[0, : x_len[0]]) |
|
|
| return bert_full[0], bert_features[0, : x_len[0]] |
|
|
| x_sum_full = x_emb_full + bert_full |
| x_pos_full = self.ar_text_position.prefill(x_sum_full) |
|
|
| xy_pos = torch.zeros((B, T_TOTAL, D), dtype=x_pos_full.dtype, device=x_pos_full.device) |
|
|
| xy_pos[:, :Tx, :] = torch.where( |
| mask_x3, |
| bert_full[:, :Tx, :].to(xy_pos.dtype), |
| xy_pos[:, :Tx, :], |
| ) |
|
|
| return xy_pos[:, -1], None |
|
|
| |
| |
| offsets = x_len.clamp(min=0, max=T_TOTAL - Ty) |
| idx_y = offsets.unsqueeze(1) + torch.arange(Ty, device=x_pos_full.device) |
| |
| |
| idx_y3 = idx_y.unsqueeze(-1).expand(B, Ty, D) |
| y_pos_b = y_pos.expand(B, Ty, D).to(xy_pos.dtype) |
| xy_pos = xy_pos.scatter(1, idx_y3, y_pos_b) |
|
|
| return xy_pos, x_len |
|
|
|
|
| def torchscript_export(model: T2SDecoderONNX, stage="embed"): |
| if stage == "embed": |
| x = torch.randint(1, 600, (model.max_batch_size, 50)) |
| x_len = torch.randint(30, 50, (model.max_batch_size,)) |
| y = torch.randint(1, 600, (1, 200)) |
| bert_features = torch.rand((model.max_batch_size, 1024, 50)) |
|
|
| x_len[-1] = 50 |
|
|
| mask = torch.arange(x_len.max().item(), device=x.device).unsqueeze(0) < x_len.unsqueeze(1) |
|
|
| x = x * mask |
| bert_features = bert_features * mask.unsqueeze(1) |
|
|
| try: |
| a, c = model.embed_onnx_(x, x_len, y, bert_features) |
| b, d = model.embed_onnx(x, x_len, y, bert_features) |
| print("-" * 20) |
| print(a - b, (a - b).sum(), (a - b).square().mean()) |
| print(c - d, (c - d).sum(), (c - d).square().mean()) |
| exit() |
| assert torch.allclose(a, b, atol=1e-6, rtol=1e-8), (a - b).square().mean() |
|
|
| setattr(model, "forward", model.embed_onnx) |
| scripted_model = torch.jit.script(model, example_inputs=[(x, x_len, y, bert_features)]) |
|
|
| onnx_program = torch.onnx.export( |
| scripted_model, |
| (x, x_len, y, bert_features), |
| input_names=["text", "text_len", "prompt", "bert_features"], |
| output_names=["xy_pos", "input_pos"], |
| dynamic_axes={ |
| "text": {0: "Batch_Size", 1: "Sequence_Length_X"}, |
| "prompt": {0: "Batch_Size", 1: "Sequence_Length_Y"}, |
| "bert_features": {0: "Batch_Size", 1: "Sequence_Length_X"}, |
| }, |
| opset_version=21, |
| training=False, |
| do_constant_folding=True, |
| external_data=False, |
| ) |
| assert onnx_program |
| onnx_program.save("onnx_export/AR_Embedding_TorchScript.onnx") |
|
|
| except Exception: |
| logger.bind(show_locals=False).exception("") |
|
|
|
|
| def dynamo_export(model: T2SDecoderONNX, stage="embed"): |
| if stage == "embed": |
| x = torch.randint(1, 600, (model.max_batch_size, 50)) |
| x_len = torch.randint(30, 50, (model.max_batch_size,)) |
| y = torch.randint(1, 600, (1, 200)) |
| bert_features = torch.rand((model.max_batch_size, 1024, 50)) |
|
|
| x_len[-1] = 50 |
|
|
| mask = torch.arange(x_len.max().item(), device=x.device).unsqueeze(0) < x_len.unsqueeze(1) |
|
|
| x = x * mask |
| bert_features = (bert_features.transpose(1, 2) * mask.unsqueeze(-1)).transpose(1, 2) |
|
|
| dynamic_shapes = [ |
| { |
| 0: Dim("Batch_Size", min=1, max=4), |
| 1: Dim("Sequence_Length_X", min=1, max=50), |
| }, |
| { |
| 0: Dim("Batch_Size", min=1, max=4), |
| }, |
| { |
| 1: Dim("Sequence_Length_Y", min=1, max=250), |
| }, |
| { |
| 0: Dim("Batch_Size", min=1, max=4), |
| 2: Dim("Sequence_Length_X", min=1, max=50), |
| }, |
| ] |
| try: |
| a = model.embed_onnx_(x, x_len, y, bert_features)[0] |
| b = model.embed_onnx(x, x_len, y, bert_features)[0] |
| print(a - b, (a - b).square().mean()) |
| exit() |
| assert torch.allclose(a, b, atol=1e-6, rtol=1e-8), (a - b).square().mean() |
|
|
| setattr(model, "forward", model.embed_onnx) |
| onnx_program = torch.onnx.export( |
| model, |
| (x, x_len, y, bert_features), |
| input_names=["text", "text_len", "prompt", "bert_features"], |
| output_names=["xy_pos", "input_pos"], |
| dynamo=True, |
| dynamic_shapes=dynamic_shapes, |
| opset_version=21, |
| training=False, |
| do_constant_folding=True, |
| external_data=False, |
| ) |
| assert onnx_program |
| onnx_program.save("onnx_export/AR_Embedding_Dynamo.onnx") |
| except Exception: |
| logger.bind(show_locals=False).exception("") |
|
|
|
|
| @app.command() |
| def export( |
| ckpt_path: Path = typer.Option( |
| ..., |
| "--ckpt-path", |
| file_okay=True, |
| dir_okay=False, |
| exists=True, |
| readable=True, |
| show_default=False, |
| help="AR Checkpoint", |
| ), |
| dynamo: bool = typer.Option(False, is_flag=True, flag_value=True, help="Use Torch Dynamo"), |
| stages: list[Stage] = typer.Option([Stage.embed], "--stages", help="Stage to export"), |
| ): |
| os.makedirs("onnx_export", exist_ok=True) |
| dict_s1 = torch.load(ckpt_path, "cpu", mmap=True) |
| condig = dict_s1["config"] |
| model = T2SDecoderONNX(condig, 2000, 4) |
| state_dict = dict_s1["weight"] |
| model.load_state_dict(state_dict) |
|
|
| for stage in stages: |
| if dynamo: |
| dynamo_export(model, stage) |
| else: |
| torchscript_export(model, stage) |
|
|
|
|
| def get_prog_name() -> str: |
| script_rel = ".".join(["GPT_SoVITS", "Accelerate", "PyTorch", osp.basename(__file__)]).strip(".py") |
| return f"python -s -m {script_rel}" |
|
|
|
|
| if __name__ == "__main__": |
| t = time.perf_counter() |
| app(prog_name=get_prog_name()) |
| logger.info(f"Exec Time: {time.perf_counter() - t:.2f} secs") |
|
|