XXXXRT666
Init
d4d21ad
Raw
History Blame Contribute Delete
15.5 kB
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
):
# input_pos: [B, ], k_val: [B, H, 1, D]
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_val: [B, S, H, D]
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): # type: ignore
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) # type: ignore
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( # type: ignore
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
# xy_pos[None, bs, len_ : len_ + y_len] = y_pos
return xy_pos[:, -1], None
return xy_pos[: x.shape[0]], x_len
def embed_onnx(
self,
x: torch.Tensor, # [B, Tx]
x_len: torch.Tensor, # [B]
y: torch.Tensor, # [1, Ty, D]
bert_features: torch.Tensor, # [B, 1024, Tx]
):
# [B, 1024, Tx] -> [B, Tx, 1024]
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
# mask: [B, Tx],[j] Col < x_len[i]
col = torch.arange(Tx, device=x.device).unsqueeze(0) # [1, Tx]
mask_x = col < x_len.view(-1, 1) # [B, Tx]
mask_x3 = mask_x.unsqueeze(-1) # [B, Tx, 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) # [1, Ty, D]
y_pos = self.ar_audio_position.prefill(y_emb) # [1, Ty, D]
x_emb_full = self.ar_text_embedding(x) # [B, Tx, D]
bert_full = self.bert_proj(bert_features[[0], : x_len[0]]) # [B, Tx, D]
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 # [B, Tx, D]
x_pos_full = self.ar_text_position.prefill(x_sum_full) # [B, Tx, D]
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
# Start From offset=x_len, Ty
# [Ty] Index: offsets + [0..Ty-1]
offsets = x_len.clamp(min=0, max=T_TOTAL - Ty) # [B]
idx_y = offsets.unsqueeze(1) + torch.arange(Ty, device=x_pos_full.device) # [B, Ty]
# scatter to dim=1
# expand index to [B, Ty, D]
idx_y3 = idx_y.unsqueeze(-1).expand(B, Ty, D)
y_pos_b = y_pos.expand(B, Ty, D).to(xy_pos.dtype) # [B, Ty, D]
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")