Initial commit
This commit is contained in:
382
zipvoice/bin/tensorrt_export.py
Normal file
382
zipvoice/bin/tensorrt_export.py
Normal file
@@ -0,0 +1,382 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright 2025 Xiaomi Corp. (authors: Zengwei Yao)
|
||||
# Copyright 2025 Nvidia Corp. (authors: Yuekai Zhang)
|
||||
#
|
||||
# See ../../../../LICENSE for clarification regarding multiple authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
This script exports a pre-trained ZipVoice or ZipVoice-Distill model from PyTorch to
|
||||
ONNX.
|
||||
|
||||
Usage:
|
||||
|
||||
python3 -m zipvoice.bin.tensorrt_export \
|
||||
--model-name zipvoice_distill \
|
||||
--model-dir models/zipvoice_distill \
|
||||
--checkpoint-name model.pt \
|
||||
--trt-engine-file-name fm_decoder.fp16.max_batch_4.plan \
|
||||
--tensorrt-model-dir models/zipvoice_distill_trt || exit 1
|
||||
|
||||
`--model-name` can be `zipvoice` or `zipvoice_distill`,
|
||||
which are the models before and after distillation, respectively.
|
||||
"""
|
||||
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
import math
|
||||
|
||||
import safetensors.torch
|
||||
import torch
|
||||
from torch import Tensor, nn
|
||||
|
||||
from zipvoice.models.zipvoice import ZipVoice
|
||||
from zipvoice.models.zipvoice_distill import ZipVoiceDistill
|
||||
from zipvoice.tokenizer.tokenizer import SimpleTokenizer
|
||||
from zipvoice.utils.checkpoint import load_checkpoint
|
||||
from zipvoice.utils.common import AttributeDict
|
||||
from zipvoice.utils.scaling_converter import convert_scaled_to_non_scaled
|
||||
from zipvoice.models.modules.zipformer import CompactRelPositionalEncoding
|
||||
|
||||
# Monkey-patching CompactRelPositionalEncoding.extend_pe
|
||||
def extend_pe(self, x: Tensor, left_context_len: int = 0) -> None:
|
||||
"""Reset the positional encodings."""
|
||||
T = x.size(0) + left_context_len
|
||||
|
||||
# if self.pe is not None:
|
||||
# # self.pe contains both positive and negative parts
|
||||
# # the length of self.pe is 2 * input_len - 1
|
||||
# if self.pe.size(0) >= T * 2 - 1:
|
||||
# self.pe = self.pe.to(dtype=x.dtype, device=x.device)
|
||||
# return
|
||||
|
||||
# if T == 4, x would contain [ -3, -2, 1, 0, 1, 2, 3 ]
|
||||
x = torch.arange(-(T - 1), T, device=x.device).to(torch.float32).unsqueeze(1)
|
||||
|
||||
freqs = 1 + torch.arange(self.embed_dim // 2, device=x.device)
|
||||
|
||||
# `compression_length` this is arbitrary/heuristic, if it is larger we have more
|
||||
# resolution for small time offsets but less resolution for large time offsets.
|
||||
compression_length = self.embed_dim**0.5
|
||||
# x_compressed, like X, goes from -infinity to infinity as T goes from -infinity
|
||||
# to infinity; but it does so more slowly than T for large absolute values of T.
|
||||
# The formula is chosen so that d(x_compressed )/dx is 1 around x == 0, which is
|
||||
# important.
|
||||
x_compressed = (
|
||||
compression_length
|
||||
* x.sign()
|
||||
* ((x.abs() + compression_length).log() - math.log(compression_length))
|
||||
)
|
||||
|
||||
# if self.length_factor == 1.0, then length_scale is chosen so that the
|
||||
# FFT can exactly separate points close to the origin (T == 0). So this
|
||||
# part of the formulation is not really heuristic.
|
||||
# But empirically, for ASR at least, length_factor > 1.0 seems to work better.
|
||||
length_scale = self.length_factor * self.embed_dim / (2.0 * math.pi)
|
||||
|
||||
# note for machine implementations: if atan is not available, we can use:
|
||||
# x.sign() * ((1 / (x.abs() + 1)) - 1) * (-math.pi/2)
|
||||
# check on wolframalpha.com: plot(sign(x) * (1 / ( abs(x) + 1) - 1 ) * -pi/2 ,
|
||||
# atan(x))
|
||||
x_atan = (x_compressed / length_scale).atan() # results between -pi and pi
|
||||
|
||||
cosines = (x_atan * freqs).cos()
|
||||
sines = (x_atan * freqs).sin()
|
||||
|
||||
pe = torch.zeros(x.shape[0], self.embed_dim, device=x.device)
|
||||
pe[:, 0::2] = cosines
|
||||
pe[:, 1::2] = sines
|
||||
pe[:, -1] = 1.0 # for bias.
|
||||
|
||||
self.pe = pe.to(dtype=x.dtype)
|
||||
|
||||
|
||||
CompactRelPositionalEncoding.extend_pe = extend_pe
|
||||
|
||||
|
||||
def get_trt_kwargs_dynamic_batch(
|
||||
min_batch_size: int = 1,
|
||||
opt_batch_size: int = 2,
|
||||
max_batch_size: int = 4,
|
||||
) -> Dict:
|
||||
"""Get keyword arguments for TensorRT with dynamic batch size."""
|
||||
feat_dim = 300
|
||||
min_seq_len = 100
|
||||
opt_seq_len = 200
|
||||
max_seq_len = 3000
|
||||
min_shape = [(min_batch_size, min_seq_len, feat_dim), (min_batch_size,), (min_batch_size, min_seq_len), (min_batch_size,)]
|
||||
opt_shape = [(opt_batch_size, opt_seq_len, feat_dim), (opt_batch_size,), (opt_batch_size, opt_seq_len), (opt_batch_size,)]
|
||||
max_shape = [(max_batch_size, max_seq_len, feat_dim), (max_batch_size,), (max_batch_size, max_seq_len), (max_batch_size,)]
|
||||
input_names = ["x", "t", "padding_mask", "guidance_scale"]
|
||||
return {
|
||||
"min_shape": min_shape,
|
||||
"opt_shape": opt_shape,
|
||||
"max_shape": max_shape,
|
||||
"input_names": input_names,
|
||||
}
|
||||
|
||||
|
||||
def convert_onnx_to_trt(
|
||||
trt_model: str, trt_kwargs: Dict, onnx_model: str, dtype: torch.dtype = torch.float16
|
||||
):
|
||||
"""
|
||||
Convert an ONNX model to a TensorRT engine.
|
||||
|
||||
Args:
|
||||
trt_model (str): The path to save the TensorRT engine.
|
||||
trt_kwargs (Dict): Keyword arguments for TensorRT.
|
||||
onnx_model (str): The path to the ONNX model.
|
||||
dtype (torch.dtype, optional): The data type to use. Defaults to torch.float16.
|
||||
"""
|
||||
logging.info("Converting onnx to trt...")
|
||||
network_flags = 1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)
|
||||
logger = trt.Logger(trt.Logger.INFO)
|
||||
builder = trt.Builder(logger)
|
||||
network = builder.create_network(network_flags)
|
||||
parser = trt.OnnxParser(network, logger)
|
||||
config = builder.create_builder_config()
|
||||
# config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 << 32) # 4GB
|
||||
if dtype == torch.float16:
|
||||
config.set_flag(trt.BuilderFlag.FP16)
|
||||
|
||||
profile = builder.create_optimization_profile()
|
||||
# load onnx model
|
||||
with open(onnx_model, "rb") as f:
|
||||
if not parser.parse(f.read()):
|
||||
for error in range(parser.num_errors):
|
||||
print(parser.get_error(error))
|
||||
raise ValueError('failed to parse {}'.format(onnx_model))
|
||||
# set input shapes
|
||||
for i in range(len(trt_kwargs['input_names'])):
|
||||
profile.set_shape(trt_kwargs['input_names'][i], trt_kwargs['min_shape'][i], trt_kwargs['opt_shape'][i], trt_kwargs['max_shape'][i])
|
||||
if dtype == torch.float16:
|
||||
tensor_dtype = trt.DataType.HALF
|
||||
elif dtype == torch.bfloat16:
|
||||
tensor_dtype = trt.DataType.BF16
|
||||
elif dtype == torch.float32:
|
||||
tensor_dtype = trt.DataType.FLOAT
|
||||
else:
|
||||
raise ValueError('invalid dtype {}'.format(dtype))
|
||||
# set input and output data type
|
||||
for i in range(network.num_inputs):
|
||||
input_tensor = network.get_input(i)
|
||||
input_tensor.dtype = tensor_dtype
|
||||
for i in range(network.num_outputs):
|
||||
output_tensor = network.get_output(i)
|
||||
output_tensor.dtype = tensor_dtype
|
||||
config.add_optimization_profile(profile)
|
||||
engine_bytes = builder.build_serialized_network(network, config)
|
||||
# save trt engine
|
||||
with open(trt_model, "wb") as f:
|
||||
f.write(engine_bytes)
|
||||
logging.info("Succesfully convert onnx to trt...")
|
||||
|
||||
|
||||
def get_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--tensorrt-model-dir",
|
||||
type=str,
|
||||
default="exp",
|
||||
help="Dir to the exported models",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--model-name",
|
||||
type=str,
|
||||
default="zipvoice",
|
||||
choices=["zipvoice", "zipvoice_distill"],
|
||||
help="The model used for inference",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--model-dir",
|
||||
type=str,
|
||||
default=None,
|
||||
help="The model directory that contains model checkpoint, configuration "
|
||||
"file model.json, and tokens file tokens.txt. Will download pre-trained "
|
||||
"checkpoint from huggingface if not specified.",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--checkpoint-name",
|
||||
type=str,
|
||||
default="model.pt",
|
||||
help="The name of model checkpoint.",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--trt-engine-file-name",
|
||||
type=str,
|
||||
default=None,
|
||||
help="The name of TensorRT engine file.",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--max-batch-size",
|
||||
type=int,
|
||||
default=4,
|
||||
help="The maximum batch size to use for TensorRT.",
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
def export_onnx_fm_decoder(
|
||||
model: torch.nn.Module,
|
||||
filename: str,
|
||||
opset_version: int = 18,
|
||||
distill: bool = False,
|
||||
) -> None:
|
||||
"""Export the flow matching decoder model to ONNX format.
|
||||
|
||||
Args:
|
||||
model:
|
||||
The input model
|
||||
filename:
|
||||
The filename to save the exported ONNX model.
|
||||
opset_version:
|
||||
The opset version to use.
|
||||
"""
|
||||
|
||||
|
||||
feat_dim, seq_len = model.feat_dim, 200
|
||||
|
||||
t = torch.tensor(0.5, dtype=torch.float32).unsqueeze(0)
|
||||
guidance_scale = torch.tensor(1.0, dtype=torch.float32).unsqueeze(0)
|
||||
padding_mask = torch.zeros(1, seq_len, dtype=torch.bool)
|
||||
x = torch.randn(1, seq_len, feat_dim, dtype=torch.float32)
|
||||
text_condition = torch.randn(1, seq_len, feat_dim, dtype=torch.float32)
|
||||
speech_condition = torch.randn(1, seq_len, feat_dim, dtype=torch.float32)
|
||||
xt= torch.cat([x, text_condition, speech_condition], dim=2)
|
||||
xt = xt.repeat(2, 1, 1)
|
||||
t = t.repeat(2)
|
||||
padding_mask = padding_mask.repeat(2, 1)
|
||||
guidance_scale = guidance_scale.repeat(2)
|
||||
|
||||
inputs_tensors = [xt, t, padding_mask]
|
||||
input_names = ['x', 't', 'padding_mask']
|
||||
dynamic_axes = {
|
||||
'x': {0: 'N', 1: 'T'},
|
||||
't': {0: 'N'},
|
||||
'padding_mask': {0: 'N', 1: 'T'},
|
||||
}
|
||||
if distill:
|
||||
inputs_tensors.append(guidance_scale)
|
||||
input_names.append('guidance_scale')
|
||||
dynamic_axes['guidance_scale'] = {0: 'N'}
|
||||
estimator = model.fm_decoder
|
||||
estimator = torch.jit.trace(estimator, inputs_tensors)
|
||||
torch.onnx.export(
|
||||
estimator,
|
||||
inputs_tensors,
|
||||
filename,
|
||||
opset_version=opset_version,
|
||||
input_names=input_names,
|
||||
output_names=['v'],
|
||||
dynamic_axes=dynamic_axes,
|
||||
dynamo=False,
|
||||
)
|
||||
logging.info(f"Exported to {filename}")
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def main():
|
||||
parser = get_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
params = AttributeDict()
|
||||
params.update(vars(args))
|
||||
|
||||
params.model_dir = Path(params.model_dir)
|
||||
if not params.model_dir.is_dir():
|
||||
raise FileNotFoundError(f"{params.model_dir} does not exist")
|
||||
for filename in [params.checkpoint_name, "model.json", "tokens.txt"]:
|
||||
if not (params.model_dir / filename).is_file():
|
||||
raise FileNotFoundError(f"{params.model_dir / filename} does not exist")
|
||||
model_ckpt = params.model_dir / params.checkpoint_name
|
||||
model_config = params.model_dir / "model.json"
|
||||
token_file = params.model_dir / "tokens.txt"
|
||||
|
||||
logging.info(f"Loading model from {params.model_dir}")
|
||||
|
||||
tokenizer = SimpleTokenizer(token_file)
|
||||
tokenizer_config = {"vocab_size": tokenizer.vocab_size, "pad_id": tokenizer.pad_id}
|
||||
|
||||
with open(model_config, "r") as f:
|
||||
model_config = json.load(f)
|
||||
|
||||
if params.model_name == "zipvoice":
|
||||
model = ZipVoice(
|
||||
**model_config["model"],
|
||||
**tokenizer_config,
|
||||
)
|
||||
distill = False
|
||||
else:
|
||||
assert params.model_name == "zipvoice_distill"
|
||||
model = ZipVoiceDistill(
|
||||
**model_config["model"],
|
||||
**tokenizer_config,
|
||||
)
|
||||
distill = True
|
||||
|
||||
if str(model_ckpt).endswith(".safetensors"):
|
||||
safetensors.torch.load_model(model, model_ckpt)
|
||||
elif str(model_ckpt).endswith(".pt"):
|
||||
load_checkpoint(filename=model_ckpt, model=model, strict=True)
|
||||
else:
|
||||
raise NotImplementedError(f"Unsupported model checkpoint format: {model_ckpt}")
|
||||
|
||||
device = torch.device("cpu")
|
||||
model = model.to(device)
|
||||
model.eval()
|
||||
|
||||
convert_scaled_to_non_scaled(model, inplace=True, is_onnx=True)
|
||||
|
||||
logging.info("Exporting model")
|
||||
tensorrt_model_dir = Path(params.tensorrt_model_dir)
|
||||
tensorrt_model_dir.mkdir(parents=True, exist_ok=True)
|
||||
opset_version = 18
|
||||
|
||||
|
||||
fm_decoder_onnx_file = tensorrt_model_dir / "fm_decoder.onnx"
|
||||
|
||||
export_onnx_fm_decoder(
|
||||
model=model,
|
||||
filename=fm_decoder_onnx_file,
|
||||
opset_version=opset_version,
|
||||
distill=distill,
|
||||
)
|
||||
|
||||
logging.info("Exported to TensorRT model")
|
||||
|
||||
trt_engine_file = f'{str(tensorrt_model_dir)}/{params.trt_engine_file_name}'
|
||||
trt_kwargs = get_trt_kwargs_dynamic_batch(min_batch_size=1, opt_batch_size=2, max_batch_size=params.max_batch_size)
|
||||
convert_onnx_to_trt(trt_engine_file, trt_kwargs, fm_decoder_onnx_file, dtype=torch.float16)
|
||||
|
||||
logging.info("Done!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s"
|
||||
logging.basicConfig(format=formatter, level=logging.INFO, force=True)
|
||||
import tensorrt as trt
|
||||
main()
|
||||
Reference in New Issue
Block a user