This repository aims to optimize inference of the Parler TTS family of models and implements Parler TTS from scratch borrowing code from
A rough architecture of Parler TTS is as follows:
The decoder model takes 2 inputs:
At every decoding step, the decoder generates a token from each of the codebooks of DAC parallelly following a delayed pattern resulting in the generation of 9 tokens at every step:

Source: Simple and Controllable Music Generation 4
The decoder takes prompts and descriptions as embeddings. This leads to a complicated pipeline that requires maintaining an additional model for each forward pass. Instead, if the decoder can be trained on text and audio tokens directly, it would avoid the need for an external text encoder. Another possibility to make the model inference more streamlined is by converting the description to tags and using them as tokens while decoding. This would avoid the cross-attention with description embeddings and make the architecture much simpler. This would also avoid costly cross-attention if the description text is too big.
The following analysis was done on parler-tts/parler-tts-mini-v1 running on Nvidia RTX 3080 Ti.
Using the official repository, I get the following numbers:

(Average of 10 runs)
We can see that as the number of input tokens increases (and the description tokens), the throughput decreases due to cross-attention on the complete description. However, as the output tokens increase, the throughput does not go down drastically. In the real world, if the description is too long, the throughput and time to the first token will suffer greatly.
The FLOP requirements for the mini v1 model assuming fp16 cores and an input sequence length of 64 are ~36 GFLops. The memory requirement for the same model is 950 MB assuming fp16 weights. For 3080 Ti (912 GB/s memory bandwidth and 34 TFlops fp16 compute), I get the following latencies: $L_{memory_bandwidth} = 1 * 10^{-3}s$ and $L_{compute} = 1.1 * 10^{-3}s$. This equates to ~800 toks/s (I am assuming a bit here, discounting for KV cache movement). Looking at the FLOPs requirement and the memory bandwidth requirement, the model does not seem to be either memory-bound or compute-bound on the current generation speeds.
On looking at the profile of the decoder, you can see lots of gaps in the GPU stream:

This is because the GPU is extremely fast at executing the kernels and is kept waiting for the CPU to send it to the next kernel. This leads to the model being overhead-bound. To reduce the overhead bound, we can capture the complete execution of the model as a single graph and launch all the kernels at once as CUDA graphs.
To implement the CUDA graphs, I implemented the following:
After compilation, I get the following profile of the decoder:


We can now see that all the kernels are being executed one after the other which speeds up the overall execution and drastically increases the throughput. You can also see how multiple kernels are being executed from a single 'cudaGraphLaunch` instruction from the CPU.
However, one drawback of compiling the model with static caches is that we have to preallocate the memory for the KV caches which can be huge depending on the batch size and the embedding dimension. This also leads to the attention kernel taking more time to execute than eager mode because now it has to transfer the complete KV cache for every forward pass.

In the official implementation, it takes ~13 microseconds for the attention kernel.
VS

In my custom implementation, it took ~95 microseconds for the attention kernel.
To reduce the load on the memory bandwidth, I tried quantizing the model as well. I quantized the model to int8 to see if the attention kernels would take less time or not. Surprisingly, it took a similar amount of time and the overall execution time increased. I suspect this is due to the computation overhead that is associated with converting the weights back to bf16 for computation.
Other than this, I also implemented a few other optimizations:
Overall, after implementing all of the above, I get the following improvements.

(Average of 10 runs)
I have also implemented batching, but I am unable to verify the output of the audio for the complete batch.
Finally, on a batch size of 1, these are the results:
Keeping output tokens fixed, I achieved an improvement of 10x in end-to-end latency while decoding 1024 tokens (11s of audio) with 1024 tokens as input.

Keeping input tokens fixed, I achieved an improvement of 1.3x in end-to-end latency while decoding 2048 tokens (23s of audio) with 64 tokens as input.

I achieve an RTFx of 1.3 on the extreme cases in both the scenarios whereas, HuggingFace's implementation gets an RTFx of < 0.4 for long input sequences and < 1 for long output tokens.
I have replicated the official implementation of the streamer that can decode the audio tokens as they come and stream the audio. The decoder puts the audio tokens to a queue and the audio tokenizer decodes all the collected tokens into audio.
I have currently not integrated this streamer with my custom implementation of ParlerTTS.
17 commits
Python
61.3%
Jupyter Notebook
38.7%
This repository aims to optimize inference of the Parler TTS family of models and implements Parler TTS from scratch borrowing code from
A rough architecture of Parler TTS is as follows:
The decoder model takes 2 inputs:
At every decoding step, the decoder generates a token from each of the codebooks of DAC parallelly following a delayed pattern resulting in the generation of 9 tokens at every step:

Source: Simple and Controllable Music Generation 4
The decoder takes prompts and descriptions as embeddings. This leads to a complicated pipeline that requires maintaining an additional model for each forward pass. Instead, if the decoder can be trained on text and audio tokens directly, it would avoid the need for an external text encoder. Another possibility to make the model inference more streamlined is by converting the description to tags and using them as tokens while decoding. This would avoid the cross-attention with description embeddings and make the architecture much simpler. This would also avoid costly cross-attention if the description text is too big.
The following analysis was done on parler-tts/parler-tts-mini-v1 running on Nvidia RTX 3080 Ti.
Using the official repository, I get the following numbers:

(Average of 10 runs)
We can see that as the number of input tokens increases (and the description tokens), the throughput decreases due to cross-attention on the complete description. However, as the output tokens increase, the throughput does not go down drastically. In the real world, if the description is too long, the throughput and time to the first token will suffer greatly.
The FLOP requirements for the mini v1 model assuming fp16 cores and an input sequence length of 64 are ~36 GFLops. The memory requirement for the same model is 950 MB assuming fp16 weights. For 3080 Ti (912 GB/s memory bandwidth and 34 TFlops fp16 compute), I get the following latencies: $L_{memory_bandwidth} = 1 * 10^{-3}s$ and $L_{compute} = 1.1 * 10^{-3}s$. This equates to ~800 toks/s (I am assuming a bit here, discounting for KV cache movement). Looking at the FLOPs requirement and the memory bandwidth requirement, the model does not seem to be either memory-bound or compute-bound on the current generation speeds.
On looking at the profile of the decoder, you can see lots of gaps in the GPU stream:

This is because the GPU is extremely fast at executing the kernels and is kept waiting for the CPU to send it to the next kernel. This leads to the model being overhead-bound. To reduce the overhead bound, we can capture the complete execution of the model as a single graph and launch all the kernels at once as CUDA graphs.
To implement the CUDA graphs, I implemented the following:
After compilation, I get the following profile of the decoder:


We can now see that all the kernels are being executed one after the other which speeds up the overall execution and drastically increases the throughput. You can also see how multiple kernels are being executed from a single 'cudaGraphLaunch` instruction from the CPU.
However, one drawback of compiling the model with static caches is that we have to preallocate the memory for the KV caches which can be huge depending on the batch size and the embedding dimension. This also leads to the attention kernel taking more time to execute than eager mode because now it has to transfer the complete KV cache for every forward pass.

In the official implementation, it takes ~13 microseconds for the attention kernel.
VS

In my custom implementation, it took ~95 microseconds for the attention kernel.
To reduce the load on the memory bandwidth, I tried quantizing the model as well. I quantized the model to int8 to see if the attention kernels would take less time or not. Surprisingly, it took a similar amount of time and the overall execution time increased. I suspect this is due to the computation overhead that is associated with converting the weights back to bf16 for computation.
Other than this, I also implemented a few other optimizations:
Overall, after implementing all of the above, I get the following improvements.

(Average of 10 runs)
I have also implemented batching, but I am unable to verify the output of the audio for the complete batch.
Finally, on a batch size of 1, these are the results:
Keeping output tokens fixed, I achieved an improvement of 10x in end-to-end latency while decoding 1024 tokens (11s of audio) with 1024 tokens as input.

Keeping input tokens fixed, I achieved an improvement of 1.3x in end-to-end latency while decoding 2048 tokens (23s of audio) with 64 tokens as input.

I achieve an RTFx of 1.3 on the extreme cases in both the scenarios whereas, HuggingFace's implementation gets an RTFx of < 0.4 for long input sequences and < 1 for long output tokens.
I have replicated the official implementation of the streamer that can decode the audio tokens as they come and stream the audio. The decoder puts the audio tokens to a queue and the audio tokenizer decodes all the collected tokens into audio.
I have currently not integrated this streamer with my custom implementation of ParlerTTS.
17 commits
Python
61.3%
Jupyter Notebook
38.7%